Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 5e68c64bb6ebcc503e6bb54c071bbc0ec1fdb4a5
parent b5c8184f379b40b6e7bfae1f924966cbd51747f8
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 31 Aug 2026 12:51:02 -0400

transcode: the operation is deleted — it never fired in production

Built 2026-05-19 for two failures — yt-dlp's -x step failing, or a codec
whisper could not read — and neither has ever happened here. Across 68
channels: no snapshot has ever listed a video as missing its target format, no
failed-transcodings file has ever held an id (the two on disk are 0 bytes), and
only four channels even met the stage's gate. It could not have mattered
either: resolveAudioFile falls back to any real audio file when audio.<fmt> is
absent, so a video whose audio is in another format transcribes from that file.

Gone: the four controllers, the four job kinds, the catalog entry — which was
the only taker of `appliesTo`, so that field and operationApplies go with it —
and AUDIO_TRANSCODE_SOURCE_PREFERENCE, the second audio ordering that existed
to pick a transcode source. The catalog is seven ids.

Kept: transcodeAudio and its two download-path callers, which is where the app
actually runs ffmpeg, and both wrong-format-audio cleanup sweeps.

The comments that used transcode as their example now state the rule instead:
an external operation may share a runner's queue and have no runner of its own,
which is why `runner` is asked before the queue key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Diffstat:
Dcommon/controller/failedTranscodings.ts | 50--------------------------------------------------
Dcommon/controller/removeFailedTranscodings.ts | 60------------------------------------------------------------
Dcommon/controller/transcodeFailures.ts | 179-------------------------------------------------------------------------------
Dcommon/controller/transcodeUntranscoded.ts | 54------------------------------------------------------
Mcommon/controller/videoOperations.ts | 4++--
Mcommon/jobs/jobKinds.test.ts | 5++++-
Mcommon/jobs/jobKinds.ts | 24------------------------
Mcommon/lib/mediaFiles.test.ts | 18------------------
Mcommon/lib/mediaFiles.ts | 27+++++++--------------------
Mcommon/lib/operations.test.ts | 36+++++-------------------------------
Mcommon/lib/operations.ts | 51++++++---------------------------------------------
Mcommon/lib/pauseGates.test.ts | 8++------
Mcommon/lib/pauseGates.ts | 11++++++-----
Meditor/app/operations/[id]/page.tsx | 6+++---
Meditor/app/operations/components/OperationDetail.tsx | 5++---
Meditor/app/operations/lanes.ts | 10+++++-----
Meditor/e2e/operation-settings.spec.ts | 2--
17 files changed, 42 insertions(+), 508 deletions(-)

diff --git a/common/controller/failedTranscodings.ts b/common/controller/failedTranscodings.ts @@ -1,50 +0,0 @@ -import path from "node:path"; -import { readFile, rename, writeFile } from "node:fs/promises"; -import type { Paths } from "../lib/paths"; - -export function failedTranscodingsFile(paths: Paths, slug: string): string { - return path.join(paths.channelsDir, slug, "failed-transcodings"); -} - -export async function loadFailedTranscodings( - paths: Paths, - slug: string, -): Promise<string[]> { - try { - const raw = await readFile(failedTranscodingsFile(paths, slug), "utf8"); - return raw.split("\n").filter(Boolean); - } catch { - return []; - } -} - -export async function pruneFailedTranscodings( - failureListFile: string, - idsToRemove: Set<string>, -): Promise<{ remaining: number; pruned: number }> { - let raw: string; - try { - raw = await readFile(failureListFile, "utf8"); - } catch { - return { remaining: 0, pruned: 0 }; - } - const original = raw.split("\n").filter(Boolean); - const filtered = original.filter((id) => !idsToRemove.has(id)); - const tmp = `${failureListFile}.tmp-${process.pid}`; - await writeFile(tmp, filtered.length ? filtered.join("\n") + "\n" : ""); - await rename(tmp, failureListFile); - return { - remaining: filtered.length, - pruned: original.length - filtered.length, - }; -} - -export async function clearFailedTranscodings( - paths: Paths, - slug: string, -): Promise<void> { - const file = failedTranscodingsFile(paths, slug); - const tmp = `${file}.tmp-${process.pid}`; - await writeFile(tmp, ""); - await rename(tmp, file); -} diff --git a/common/controller/removeFailedTranscodings.ts b/common/controller/removeFailedTranscodings.ts @@ -1,60 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import type { Paths } from "../lib/paths"; -import { - clearFailedTranscodings, - loadFailedTranscodings, -} from "./failedTranscodings"; - -const { pathExists, remove } = fs; - -export type RemoveFailedTranscodingsOptions = { - channelSlug: string; - paths: Paths; - onLog?: (msg: string) => void; - signal?: AbortSignal; -}; - -export type RemoveFailedTranscodingsResult = { - removed: number; - missing: number; -}; - -export async function removeFailedTranscodings({ - channelSlug, - paths, - onLog, - signal, -}: RemoveFailedTranscodingsOptions): Promise<RemoveFailedTranscodingsResult> { - const log = onLog ?? ((m: string) => console.log(m)); - const dataDir = path.join(paths.channelsDir, channelSlug, "data"); - const ids = await loadFailedTranscodings(paths, channelSlug); - if (ids.length === 0) { - log("No failed-transcodings to remove."); - return { removed: 0, missing: 0 }; - } - - let removed = 0; - let missing = 0; - for (const id of ids) { - if (signal?.aborted) { - log("Cancelled"); - break; - } - const videoDir = path.join(dataDir, id); - if (!(await pathExists(videoDir))) { - log(`Skipping ${id}: directory does not exist`); - missing++; - continue; - } - await remove(videoDir); - log(`Removed ${id}`); - removed++; - } - - await clearFailedTranscodings(paths, channelSlug); - log( - `Removed ${removed} video dir(s) listed in failed-transcodings; ${missing} were already gone.`, - ); - return { removed, missing }; -} diff --git a/common/controller/transcodeFailures.ts b/common/controller/transcodeFailures.ts @@ -1,179 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import pLimit from "p-limit"; -import type { Paths } from "../lib/paths"; -import type { AudioFormat } from "../lib/channelConfig"; -import { - AUDIO_TRANSCODE_SOURCE_PREFERENCE, - audioFilesToRemove, - pickPreferredAudio, -} from "../lib/mediaFiles"; -import { transcodeAudio } from "./transcode"; -import { - failedTranscodingsFile, - pruneFailedTranscodings, -} from "./failedTranscodings"; - -const { pathExists, readdir, readFile, appendFile, ensureFile } = fs; - -function pickSourceAudio( - entries: string[], - targetFormat: AudioFormat, -): string | null { - // Shares the app's media-file predicate. The inline filter that used to live - // here was another copy of the old denylist, so it could hand ffmpeg an - // audio.en-orig.vtt subtitle as a "source audio" to transcode from. - const candidates = audioFilesToRemove(entries, { - targetAudioFile: `audio.${targetFormat}`, - wrongFormatOnly: true, - }); - return pickPreferredAudio(candidates, AUDIO_TRANSCODE_SOURCE_PREFERENCE); -} - -export type TranscodeBatchResult = { - attempted: number; - succeeded: number; - failed: number; - skipped: number; -}; - -export type TranscodeIdsOptions = { - ids: string[]; - channelSlug: string; - paths: Paths; - targetFormat: AudioFormat; - concurrency?: number; - onLog?: (msg: string) => void; - signal?: AbortSignal; -}; - -export async function transcodeIdsToFormat({ - ids, - channelSlug, - paths, - targetFormat, - concurrency, - onLog, - signal, -}: TranscodeIdsOptions): Promise<TranscodeBatchResult> { - const log = onLog ?? ((m: string) => console.log(m)); - const channelDir = path.join(paths.channelsDir, channelSlug); - const dataDir = path.join(channelDir, "data"); - const transcodeFailureFile = failedTranscodingsFile(paths, channelSlug); - const limit = pLimit(concurrency ?? 1); - - await ensureFile(transcodeFailureFile); - const previousTranscodeFailures = new Set( - (await readFile(transcodeFailureFile, "utf-8")).split("\n").filter(Boolean), - ); - const succeededIds = new Set<string>(); - - let attempted = 0; - let succeeded = 0; - let failed = 0; - let skipped = 0; - - await Promise.all( - ids.map((id) => - limit(async () => { - if (signal?.aborted) { - skipped++; - return; - } - const videoDir = path.join(dataDir, id); - if (!(await pathExists(videoDir))) { - log(`Skipping ${id}: directory no longer exists`); - skipped++; - return; - } - const entries = await readdir(videoDir); - if (entries.includes(`audio.${targetFormat}`)) { - log(`Skipping ${id}: audio.${targetFormat} already exists`); - skipped++; - return; - } - const source = pickSourceAudio(entries, targetFormat); - if (!source) { - log(`Skipping ${id}: no source audio file found`); - skipped++; - return; - } - attempted++; - try { - log(`Transcoding ${id}: ${source} → audio.${targetFormat}`); - await transcodeAudio({ - paths, - videoDir, - sourceFilename: source, - targetFormat, - onLog: log, - signal: signal ?? new AbortController().signal, - }); - succeeded++; - succeededIds.add(id); - } catch (err) { - if (signal?.aborted) { - log(`Transcode ${id} cancelled`); - skipped++; - return; - } - log(`FAILED TO TRANSCODE ${id}: ${String(err)}`); - failed++; - if (!previousTranscodeFailures.has(id)) { - await appendFile(transcodeFailureFile, `${id}\n`); - previousTranscodeFailures.add(id); - } - } - }), - ), - ); - - if (succeededIds.size > 0) { - const { pruned, remaining } = await pruneFailedTranscodings( - transcodeFailureFile, - succeededIds, - ); - log( - `Pruned ${pruned} from failed-transcodings; ${remaining} remain.`, - ); - } - - return { attempted, succeeded, failed, skipped }; -} - -export type TranscodeFailuresOptions = { - channelSlug: string; - paths: Paths; - targetFormat: AudioFormat; - concurrency?: number; - onLog?: (msg: string) => void; - signal?: AbortSignal; -}; - -export type TranscodeFailuresResult = TranscodeBatchResult; - -export async function runTranscodeFailures({ - channelSlug, - paths, - targetFormat, - concurrency, - onLog, - signal, -}: TranscodeFailuresOptions): Promise<TranscodeFailuresResult> { - const channelDir = path.join(paths.channelsDir, channelSlug); - const failureListFile = path.join(channelDir, "failed-transcriptions"); - await ensureFile(failureListFile); - const ids = (await readFile(failureListFile, "utf-8")) - .split("\n") - .filter(Boolean); - - return transcodeIdsToFormat({ - ids, - channelSlug, - paths, - targetFormat, - concurrency, - onLog, - signal, - }); -} diff --git a/common/controller/transcodeUntranscoded.ts b/common/controller/transcodeUntranscoded.ts @@ -1,54 +0,0 @@ -import type { Paths } from "../lib/paths"; -import { readChannelConfig } from "./channels"; -import { readChannelSnapshot } from "./channelSnapshot"; -import { - transcodeIdsToFormat, - type TranscodeBatchResult, -} from "./transcodeFailures"; - -export type TranscodeUntranscodedOptions = { - channelSlug: string; - paths: Paths; - concurrency?: number; - onLog?: (msg: string) => void; - signal?: AbortSignal; -}; - -export async function runTranscodeUntranscoded({ - channelSlug, - paths, - concurrency, - onLog, - signal, -}: TranscodeUntranscodedOptions): Promise<TranscodeBatchResult> { - const log = onLog ?? ((m: string) => console.log(m)); - const config = await readChannelConfig(paths, channelSlug); - if (!config?.audioFormat) { - throw new Error( - `Channel ${channelSlug} has no audioFormat configured; set one before running this action.`, - ); - } - const snapshot = await readChannelSnapshot(paths, channelSlug); - const ids = snapshot?.buckets?.untranscoded ?? []; - if (!snapshot) { - log( - "No snapshot on disk — regenerate the channel snapshot first so the untranscoded bucket is populated.", - ); - } - if (ids.length === 0) { - log("Nothing to transcode: untranscoded bucket is empty."); - return { attempted: 0, succeeded: 0, failed: 0, skipped: 0 }; - } - log( - `Transcoding ${ids.length} untranscoded video(s) → audio.${config.audioFormat}`, - ); - return transcodeIdsToFormat({ - ids, - channelSlug, - paths, - targetFormat: config.audioFormat, - concurrency, - onLog: log, - signal, - }); -} diff --git a/common/controller/videoOperations.ts b/common/controller/videoOperations.ts @@ -13,8 +13,8 @@ // is deliberately separate so a sidecar captured before its feature was // switched off is still something the operator can see. // -// THE EXTERNAL OPERATIONS ARE DELIBERATELY ABSENT. Download, transcode and -// transcription already have a per-video surface — the pipeline stage cards +// THE EXTERNAL OPERATIONS ARE DELIBERATELY ABSENT. Download and transcription +// already have a per-video surface — the pipeline stage cards // inside the editor's VideoPanel, which carry their own per-video actions. This // reader is over OPERATIONS, the backfill/derived-data registry, and that // asymmetry is intentional rather than an omission. Sync is absent for a diff --git a/common/jobs/jobKinds.test.ts b/common/jobs/jobKinds.test.ts @@ -94,7 +94,10 @@ test("labels match the old JOB_KIND_LABELS map exactly", () => { test("label-less and unknown kinds fall back to the raw kind", () => { // Replayable kinds that never had a label entry must still fall back. assert.equal(jobKindLabel("store-playlist"), "store-playlist"); - assert.equal(jobKindLabel("transcode-failures"), "transcode-failures"); + assert.equal( + jobKindLabel("clear-failed-transcriptions"), + "clear-failed-transcriptions", + ); // Genuinely unknown kind. assert.equal(jobKindLabel("totally-unknown"), "totally-unknown"); assert.equal(getJobKind("totally-unknown"), undefined); diff --git a/common/jobs/jobKinds.ts b/common/jobs/jobKinds.ts @@ -345,30 +345,6 @@ const JOB_KINDS: Record<string, JobKindMeta> = { replayable: true, queueKeyStrategy: "custom", }, - "transcode-failures": { - kind: "transcode-failures", - drainable: false, - replayable: true, - queueKeyStrategy: "custom", - }, - "transcode-untranscoded": { - kind: "transcode-untranscoded", - drainable: false, - replayable: true, - queueKeyStrategy: "custom", - }, - "remove-failed-transcodings": { - kind: "remove-failed-transcodings", - drainable: false, - replayable: true, - queueKeyStrategy: "custom", - }, - "clear-failed-transcodings": { - kind: "clear-failed-transcodings", - drainable: false, - replayable: true, - queueKeyStrategy: "custom", - }, "clean-extra-audio-formats": { kind: "clean-extra-audio-formats", drainable: false, diff --git a/common/lib/mediaFiles.test.ts b/common/lib/mediaFiles.test.ts @@ -2,7 +2,6 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { AUDIO_READ_PREFERENCE, - AUDIO_TRANSCODE_SOURCE_PREFERENCE, audioFilesToRemove, findSourceMedia, isPartAudioFile, @@ -176,16 +175,6 @@ describe("pickPreferredAudio", () => { ); }); - it("prefers m4a when picking a transcode source", () => { - assert.equal( - pickPreferredAudio( - ["audio.opus", "audio.mp3", "audio.m4a"], - AUDIO_TRANSCODE_SOURCE_PREFERENCE, - ), - "audio.m4a", - ); - }); - it("falls back to a stable sort, not readdir order", () => { assert.equal(pickPreferredAudio(["audio.wav", "audio.aac"]), "audio.aac"); assert.equal(pickPreferredAudio(["audio.aac", "audio.wav"]), "audio.aac"); @@ -194,11 +183,4 @@ describe("pickPreferredAudio", () => { it("returns null for no candidates", () => { assert.equal(pickPreferredAudio([]), null); }); - - it("keeps the two orderings distinct", () => { - assert.notDeepEqual( - [...AUDIO_READ_PREFERENCE], - [...AUDIO_TRANSCODE_SOURCE_PREFERENCE], - ); - }); }); diff --git a/common/lib/mediaFiles.ts b/common/lib/mediaFiles.ts @@ -143,32 +143,19 @@ export function isPartAudioFile(name: string): boolean { } // --------------------------------------------------------------------------- -// Which audio file to use when a video dir holds more than one. -// -// This ordering was copy-pasted into three controllers (diarizeOne, -// transcribeOne, transcodeFailures) with TWO DIFFERENT ORDERS, which reads like -// drift but is not: the first two are picking something to READ and the third is -// picking something to TRANSCODE FROM. Both orders are kept, named for what they -// mean, so the difference is visible instead of looking like a bug. - -// Reading: mp3 first, because that is what this corpus overwhelmingly holds -// (900 audio.mp3 against 12 audio.mp4 and 1 audio.aac) and what the transcribe -// and diarize paths were tuned against. +// Which audio file to use when a video dir holds more than one: mp3 first, +// because that is what this corpus overwhelmingly holds (900 audio.mp3 against +// 12 audio.mp4 and 1 audio.aac) and what the transcribe and diarize paths were +// tuned against. A second ordering — the format most likely to be the ORIGINAL +// download, for picking a transcode SOURCE — left with the transcode operation +// on 2026-08-30; pickPreferredAudio keeps its `preference` parameter and every +// caller (transcribeOne, diarizeOne, operations.ts) takes the default. export const AUDIO_READ_PREFERENCE: ReadonlyArray<string> = [ "audio.mp3", "audio.m4a", "audio.opus", ]; -// Transcoding: prefer the format most likely to be the ORIGINAL download rather -// than a previous re-encode, so a repeated transcode does not stack generations -// of lossy loss. -export const AUDIO_TRANSCODE_SOURCE_PREFERENCE: ReadonlyArray<string> = [ - "audio.m4a", - "audio.opus", - "audio.mp3", -]; - // Pick one audio file from a set of candidates by preference, falling back to a // stable sort so the answer never depends on readdir order. export function pickPreferredAudio( diff --git a/common/lib/operations.test.ts b/common/lib/operations.test.ts @@ -9,7 +9,6 @@ import { backfillLaneOperations, resolveBackfillLaneOperations, orderByDependencies, - operationApplies, operationCatalog, operationCostBasis, operationGroup, @@ -1224,7 +1223,6 @@ test("the catalog covers every operation, dispatched here or not", () => { for (const id of [ "sync", "download", - "transcode", "transcription", "diarization", "attribution-diarized", @@ -1246,9 +1244,9 @@ test("the catalog covers every operation, dispatched here or not", () => { test("`runner` names the auto-queue runner, and only for the two that have one", () => { // The console reads this instead of asking whether the id happens to be // "download" or "transcription". `dispatch` cannot answer it: both of those - // are `external` WITH a runner, and a future `transcode` would be `external` - // with none — so an id-shaped guess would hand transcode the transcription - // runner's controls. + // are `external` WITH a runner, and an external operation sharing a runner's + // queue with no runner of its own is a legal entry — so an id-shaped guess + // would hand such an entry that runner's controls. const runners = new Map(operationCatalog().map((o) => [o.id, o.runner])); assert.equal(runners.get("download"), "download"); assert.equal(runners.get("transcription"), "transcription"); @@ -1265,30 +1263,6 @@ test("`runner` names the auto-queue runner, and only for the two that have one", assert.equal(runners.get("sync"), undefined); }); -test("`appliesTo` is transcode's alone, and it reads the channel config", () => { - // A channel that never transcodes must not grow a dead Transcode stage, and - // the three surfaces that gate it used to each spell the rule themselves. - // Every other operation applies to every channel: absent, not `() => true`. - for (const op of operationCatalog()) { - assert.equal( - op.appliesTo !== undefined, - op.id === "transcode", - `${op.id} ${op.appliesTo ? "declares" : "lacks"} appliesTo`, - ); - } - const transcribing = { handling: "transcribe", audioFormat: "mp3" } as const; - assert.equal(operationApplies("transcode", transcribing), true); - assert.equal( - operationApplies("transcode", { handling: "youtube", audioFormat: "mp3" }), - false, - ); - assert.equal(operationApplies("transcode", { handling: "transcribe" }), false); - // No appliesTo means every channel — and so does an id the catalog does not - // know, for the reason operationLabel returns the id rather than throwing. - assert.equal(operationApplies("download", { handling: "youtube" }), true); - assert.equal(operationApplies("not-an-operation", { handling: "youtube" }), true); -}); - test("`settingsBlock` names the settings.json block, off the descriptor", () => { // The operation page renders an operation's settings form by switching on // THIS, not on a table keyed by operation id — the rule slice 2 set for @@ -1344,8 +1318,8 @@ test("the three speaker operations share one group; digest does not", () => { assert.equal(operationGroup("digest"), "digest"); assert.equal(operationGroup("download"), "media"); assert.equal(operationGroup("transcription"), "transcript"); - // Sync is its own group, not `media`: that group is download + transcode's - // /channels column group, and sync draws no column. + // Sync is its own group, not `media`: that group is download's /channels + // column group, and sync draws no column. assert.equal(operationGroup("sync"), "sync"); // An id the catalog does not know is null, NOT filed under the first group. assert.equal(operationGroup("no-such-operation"), null); diff --git a/common/lib/operations.ts b/common/lib/operations.ts @@ -74,7 +74,6 @@ // into an existing pass and `lane` gets the concurrent queue and the share. import type { Paths } from "./paths"; -import type { ChannelConfig } from "./channelConfig"; import type { AttributionSettings, DiarizationSettings, @@ -1362,27 +1361,6 @@ export const EXTERNAL_OPERATIONS: readonly ExternalOperation[] = [ runner: "download", }, { - id: "transcode", - label: "Transcode", - hint: "Re-encoding kept media to the channel's audio format. Dispatched from the channel page's Transcode stage and the video page; a channel with no audioFormat never has this work.", - group: "media", - shortLabel: "Transcode", - costBasis: "one ffmpeg pass per video, on the CPU", - // Where the channel page already runs it (transcodeDefaultQueueKey is - // TRANSCRIPTION_QUEUE): it is CPU-bound, but sharing the transcription - // queue is what keeps an ffmpeg pass from racing whisper over the same - // media file. - lane: { queueKey: TRANSCRIPTION_QUEUE, contendsFor: "cpu" }, - dependsOn: ["download"], - dispatch: "external", - scope: "video", - trigger: "backlog", - // No runner: nothing in the auto-queue feeds this. /operations/transcode - // is the "no console here" panel, on purpose. - appliesTo: (config) => - config.handling === "transcribe" && !!config.audioFormat, - }, - { id: "transcription", label: "Transcription", hint: "Turning audio into a transcript. Dispatched by the auto-transcribe runner across the worker pool.", @@ -1421,26 +1399,18 @@ export type OperationDescriptor = { trigger: OperationTrigger; // The auto-queue runner that dispatches this, when one does. `dispatch` alone // cannot answer it: download and transcription are `external` WITH a runner, - // and a future `transcode` would be `external` with none. A console that - // guessed from the id would hand transcode the transcription runner's - // controls — a live Start button over the wrong lane. Absent for every - // backfill kind: those are dispatched by the sweep and the arbiter. + // and an external operation with none is a legal entry (transcode was one, + // 2026-08-26 → 08-30). A console that guessed from the id would hand such an + // entry the transcription runner's controls — a live Start button over the + // wrong lane. Absent for every backfill kind: those are dispatched by the + // sweep and the arbiter. // // Typed to the auto-queue kinds on purpose; the sync heartbeat is a runner in // the IA doc's sense but not one of these, which is why /operations/sync is // chosen off `trigger`. runner?: AutoQueueKind; - // Whether this operation applies to a channel AT ALL, off its config. Absent - // means every channel. The first (and so far only) taker is transcode: a - // youtube-handling channel or one with no audioFormat never has that work, - // and the three surfaces that gate the Transcode stage used to each spell - // `handling === "transcribe" && !!audioFormat` for themselves. Config-scoped - // on purpose — it is a fact about the channel's configuration, not about any - // video's state, which is what `state()` on a backfill operation answers. - appliesTo?: (config: ChannelConfig) => boolean; // See Operation.settingsBlock. Absent for every external operation but sync: - // nothing in settings.json configures a download or a transcode as an - // operation. + // nothing in settings.json configures a download as an operation. settingsBlock?: OperationSettingsBlock; }; @@ -1468,15 +1438,6 @@ export function operationCatalog(): OperationDescriptor[] { ]; } -// Whether an operation applies to a channel at all, off its config. True for an -// operation that declares no `appliesTo` — and true for an id the catalog does -// not know, for the same reason operationLabel returns the id: a dangling id is -// tolerated, and a gate that silently hid a stage on a typo would be worse than -// one that showed it. -export function operationApplies(id: string, config: ChannelConfig): boolean { - return operationCatalog().find((o) => o.id === id)?.appliesTo?.(config) ?? true; -} - // The label for a dependency id, from anywhere in the catalog. Returns the id // itself for something unknown rather than throwing — a dangling dependency is // already tolerated everywhere else here. diff --git a/common/lib/pauseGates.test.ts b/common/lib/pauseGates.test.ts @@ -84,13 +84,9 @@ test("isGateHeld touches only its own lane's field", () => { assert.equal(isGateHeld(partial, "digest"), true); }); -test("pauseLaneFor answers for every catalog id, and transcode is null", () => { +test("pauseLaneFor answers for every catalog id", () => { const expected: Record<string, PauseLane | null> = { download: "download", - // No runner, and it shares TRANSCRIPTION_QUEUE — which is exactly why the - // runner is asked before the queue key. A queue-key map alone would hand - // transcode the transcription pause. - transcode: null, transcription: "transcription", diarization: "backfill", "attribution-diarized": "backfill", @@ -101,7 +97,7 @@ test("pauseLaneFor answers for every catalog id, and transcode is null", () => { sync: null, }; const ids = operationCatalog().map((o) => o.id); - assert.equal(ids.length, 8); + assert.equal(ids.length, 7); for (const id of ids) { assert.ok(id in expected, `catalog gained ${id} with no expected lane`); assert.equal(pauseLaneFor(id), expected[id], `pauseLaneFor(${id})`); diff --git a/common/lib/pauseGates.ts b/common/lib/pauseGates.ts @@ -52,12 +52,13 @@ type _RunnerLanesArePauseLanes = AutoQueueKind extends PauseLane ? true : never; // share the backfill queue and therefore share one gate, and this function is // how a page finds the gate it is really operating. // -// `runner` IS ASKED FIRST, and that order is load-bearing. transcode shares -// TRANSCRIPTION_QUEUE and has no runner at all, so a queue-key map alone would -// hand it the transcription pause — a live Pause button over a lane that would -// never dispatch it. Its own answer is null. +// `runner` IS ASKED FIRST, and that order is load-bearing. An external +// operation may share a runner's queue and have no runner of its own (transcode +// did, on TRANSCRIPTION_QUEUE, until 2026-08-30); a queue-key map alone would +// hand it that runner's pause — a live Pause button over a lane that would +// never dispatch it. Such an entry's own answer is null. // -// Walks operationCatalog() — all eight ids, external ones included — not +// Walks operationCatalog() — all seven ids, external ones included — not // OPERATION_BY_ID, which knows only the four registry entries. Sync is in that // walk and its answer is null: the scheduler's `enabled` is its own switch, not // a lane gate. diff --git a/editor/app/operations/[id]/page.tsx b/editor/app/operations/[id]/page.tsx @@ -147,9 +147,9 @@ export default async function OperationPage({ if (!op) notFound(); // OFF THE DESCRIPTOR, never off the id. `dispatch` alone cannot answer this — - // download and transcription are `external` WITH a runner, and a future - // `transcode` would be `external` with none — so the registry states it and - // this reads it. + // download and transcription are `external` WITH a runner, and an external + // operation sharing a runner's queue with none of its own is a legal entry — + // so the registry states it and this reads it. const runnerKind: AutoQueueKind | null = op.runner ?? null; // Likewise for the sweep lane. Null for an operation neither sweep dispatches; // OperationDetail renders that as "no console here" rather than borrowing one. diff --git a/editor/app/operations/components/OperationDetail.tsx b/editor/app/operations/components/OperationDetail.tsx @@ -134,9 +134,8 @@ export function OperationDetail({ {operationSettings} </> ) : ( - // STILL THE FALLTHROUGH, for a registered operation nothing drives — - // transcode today. A cadence console is a fourth arm, not a - // replacement for this one. + // STILL THE FALLTHROUGH, for a registered operation nothing drives. + // A cadence console is a fourth arm, not a replacement for this one. <NoConsoleView id={id} data={data} dependsOn={dependsOn} /> )} diff --git a/editor/app/operations/lanes.ts b/editor/app/operations/lanes.ts @@ -63,11 +63,11 @@ type _SweepLanesArePauseLanes = SweepLaneId extends PauseLane ? true : never; // // The id-shaped version of this ("digest" ? digest : backfill) answered // "backfill" for every operation that was not digest, including ones no sweep -// dispatches at all. It was right only by coincidence of today's registry: a -// registered `transcode` — external, no runner — would have rendered the -// backfill sweep's whole console, with a live Start button ARMING THE BACKFILL -// SWEEP, under a "Transcode" heading. The queue key is what the arbiter -// actually dispatches on, so it is what this asks. +// dispatches at all. It was right only by coincidence of today's registry: an +// external entry with no runner of its own would have rendered the backfill +// sweep's whole console, with a live Start button ARMING THE BACKFILL SWEEP, +// under that operation's heading. The queue key is what the arbiter actually +// dispatches on, so it is what this asks. // // Null is a real answer, not an error. A caller must render it as "no console // here" rather than falling back to a lane. diff --git a/editor/e2e/operation-settings.spec.ts b/editor/e2e/operation-settings.spec.ts @@ -235,8 +235,6 @@ test("an operation with no settings block renders no settings form", async ({ }) => { // The switch is over the DESCRIPTOR's block, so an operation that declares // none gets nothing — not an empty fieldset, and not another operation's. - await page.goto("/operations/transcode"); - await expect(page.locator("form[data-settings-block]")).toHaveCount(0); await page.goto("/operations/download"); await expect(page.locator("form[data-settings-block]")).toHaveCount(0); });