Archilyzer · Source

archilyzer

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

commit 0bac8912a74c74a1eecbf39e2ec6acd240efa01c
parent 0eeb9b078b28318840251e41be196246d047a51a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 22 Aug 2026 18:43:49 -0400

pipelines: one band instrument, three scales

The comparison rail's five-fill vocabulary is about to be needed by two more
surfaces (the /channels strip, the channel-page station foot), so the model and
the renderer move out of /auto-queue and into components/pipelines/.

Nothing changes on screen. The client/server split that band.ts documents is
preserved exactly — band.ts stays import-free so client components can take it,
buildBands.ts keeps the channelSnapshot import that would otherwise drag execa
into the browser bundle.

StateBand carries the three sizes. `strip` and `station` do not animate: the
rail eases one band per lane, /channels would ease 408 of them on every poll.

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

Diffstat:
Meditor/app/auto-queue/components/OperationRail.tsx | 133++++++-------------------------------------------------------------------------
Meditor/app/auto-queue/components/SweepLane.tsx | 2+-
Meditor/app/auto-queue/lanes.ts | 4++--
Deditor/app/auto-queue/lib/operationBand.ts | 69---------------------------------------------------------------------
Deditor/app/auto-queue/lib/operationBands.test.ts | 200-------------------------------------------------------------------------------
Deditor/app/auto-queue/lib/operationBands.ts | 196-------------------------------------------------------------------------------
Aeditor/app/components/pipelines/StateBand.tsx | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/components/pipelines/band.ts | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/components/pipelines/buildBands.test.ts | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/components/pipelines/buildBands.ts | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 687 insertions(+), 591 deletions(-)

diff --git a/editor/app/auto-queue/components/OperationRail.tsx b/editor/app/auto-queue/components/OperationRail.tsx @@ -1,6 +1,12 @@ "use client"; -import { bandCoverage, type OperationBand } from "../lib/operationBand"; +import type { OperationBand } from "../../components/pipelines/band"; +import { + bandCoverage, + bandDenominator, + BandLegend, + StateBand, +} from "../../components/pipelines/StateBand"; import type { LaneState } from "../../components/lanes/laneState"; import { LANE_DOT, LANE_TEXT, LANE_WORD } from "../../components/lanes/laneState"; @@ -41,63 +47,6 @@ export type RailLaneState = { // quietly sharing one denominator to make the bars comparable would be a lie // about what is being compared. -// The five populations, in the order they are laid down. `present` first so a -// band reads left-to-right as "done → can do → cannot do yet → cannot do at -// all", which is the same direction the channel transit line runs. -const SEGMENTS = [ - { - key: "present", - label: "done", - // Solid, muted. The artifact exists; it is not where attention goes. - className: "bg-muted-foreground/45", - }, - { - key: "reachable", - label: "reachable now", - // THE one saturated segment. - className: "bg-info", - }, - { - key: "blocked", - label: "blocked upstream", - // Hatched — waiting on an operation this system produces, so it will clear - // itself without anyone doing anything. - className: "bg-warning/25 [background-image:repeating-linear-gradient(45deg,currentColor_0_1px,transparent_1px_4px)] text-warning/70", - }, - { - key: "deferred", - label: "held by a gate", - // Dotted — a gate (stale cues, a duration window) is holding it. - className: "bg-transparent [background-image:radial-gradient(currentColor_0.5px,transparent_0.5px)] [background-size:3px_3px] text-muted-foreground", - }, - { - key: "missingInput", - label: "media gone", - // Hollow outline — there is nothing to do. An opt-in re-download is the - // only thing that would ever change it, and it must never look like work. - className: "bg-transparent ring-1 ring-inset ring-border", - }, -] as const; - -type SegmentKey = (typeof SEGMENTS)[number]["key"]; - -function valueOf(band: OperationBand, key: SegmentKey): number { - if (key === "present") return band.present ?? 0; - return band[key]; -} - -// The denominator a band's segments are drawn against. -// -// `eligible` when it is known. When it is NOT — a third of the snapshots on -// disk predate the field — falling back to the sum of the work counts would -// silently redraw the band as "100% accounted for", so instead the band renders -// as an un-filled outline and says `coverage unknown`. Unknown is not zero, and -// it is not full either. -function denominatorOf(band: OperationBand): number | null { - if (band.eligible != null && band.eligible > 0) return band.eligible; - return null; -} - export function OperationRail({ bands, states, @@ -127,7 +76,7 @@ export function OperationRail({ /> ))} </ul> - <Legend /> + <BandLegend className="border-t border-border px-4 py-2" /> </div> ); } @@ -141,7 +90,7 @@ function RailRow({ lane: RailLaneState; selected: boolean; }) { - const denominator = denominatorOf(band); + const denominator = bandDenominator(band); const coverage = bandCoverage(band); return ( @@ -162,7 +111,7 @@ function RailRow({ /> {band.label} </span> - <Band band={band} denominator={denominator} /> + <StateBand band={band} size="rail" /> {/* PLAIN TEXT, never a heading — the suite scopes whole tests with locator("section", { has: heading "Auto-transcribe" }), and a second element with that accessible name would make every one ambiguous. */} @@ -209,65 +158,3 @@ function Figure({ </span> ); } - -function Band({ - band, - denominator, -}: { - band: OperationBand; - denominator: number | null; -}) { - if (denominator === null) { - // Unknown denominator: an outline and nothing else. Drawing the work counts - // against their own sum would claim the band is fully accounted for, which - // is a different (and worse) statement than "we cannot say". - return ( - <span - aria-hidden="true" - className="h-2.5 min-w-32 flex-1 rounded-sm border border-dashed border-border" - /> - ); - } - return ( - <span - aria-hidden="true" - className="flex h-2.5 min-w-32 flex-1 overflow-hidden rounded-sm bg-muted" - > - {SEGMENTS.map((seg) => { - const pct = (valueOf(band, seg.key) / denominator) * 100; - if (pct <= 0) return null; - return ( - <span - key={seg.key} - // The ONE piece of motion on the page: a segment eases to its new - // width when the number actually changes. Never on poll (the width - // is the same, so no transition fires) and never at all under - // prefers-reduced-motion, which every animation in this codebase - // pairs with. - className={`h-full transition-[width] duration-500 motion-reduce:transition-none ${seg.className}`} - style={{ width: `${Math.min(100, pct)}%` }} - /> - ); - })} - </span> - ); -} - -// The legend is the only place the pattern vocabulary is named, and it is named -// in WORDS as well as swatches — a pattern nobody can read the meaning of is -// just noise, and this is the page's one non-obvious visual convention. -function Legend() { - return ( - <ul className="flex flex-wrap gap-x-4 gap-y-1 border-t border-border px-4 py-2 text-xs text-muted-foreground"> - {SEGMENTS.map((seg) => ( - <li key={seg.key} className="flex items-center gap-1.5"> - <span - aria-hidden="true" - className={`h-2.5 w-4 shrink-0 rounded-sm ${seg.className}`} - /> - {seg.label} - </li> - ))} - </ul> - ); -} diff --git a/editor/app/auto-queue/components/SweepLane.tsx b/editor/app/auto-queue/components/SweepLane.tsx @@ -21,7 +21,7 @@ import { } from "../../jobs/actions"; import { saveLaneOrderAction } from "../actions"; import type { SweepLaneStatus } from "../lanes"; -import type { OperationBand } from "../lib/operationBand"; +import type { OperationBand } from "../../components/pipelines/band"; import { OrderReach } from "./OrderReach"; import { formatElapsed } from "./dispatch"; diff --git a/editor/app/auto-queue/lanes.ts b/editor/app/auto-queue/lanes.ts @@ -19,7 +19,7 @@ import { getChannelBriefs } from "../lib/requestCache"; import { buildOperationBands, type OperationBand, -} from "./lib/operationBands"; +} from "../components/pipelines/buildBands"; // The two SWEEP-fed lanes, for a console that has to show four pipelines and // only has runners for two of them. @@ -79,7 +79,7 @@ export type ArbiterStatus = { export type AutoQueueLanesPayload = { digest: SweepLaneStatus; backfill: SweepLaneStatus; - // One band per pipeline, in rail order. See lib/operationBands.ts. + // One band per pipeline, in rail order. See components/pipelines/buildBands.ts. bands: OperationBand[]; arbiter: ArbiterStatus; }; diff --git a/editor/app/auto-queue/lib/operationBand.ts b/editor/app/auto-queue/lib/operationBand.ts @@ -1,69 +0,0 @@ -// The band TYPE and the two pure functions over it. -// -// SPLIT FROM operationBands.ts DELIBERATELY, and the reason is a build error -// rather than tidiness: the builder imports channelSnapshot, which imports -// runYtdlp, which imports execa — so a client component importing the builder -// drags a server-only process runner into the browser bundle, and `next build` -// fails with a module-not-found on `node:child_process`. (Nothing catches that -// in e2e, which runs in dev mode and never prerenders.) -// -// So the rail imports THIS, and the builder stays server-side. Nothing here may -// import anything but types. - -// The five populations a pipeline can put a video in, plus the denominator. -// -// Rendered as fill PATTERNS first and hue second — solid / hatched / dotted / -// hollow — because report-to-video's claim rail established here BY MEASUREMENT -// that no four-colour palette clears all-pairs colour-blindness. Pattern -// survives CVD, greyscale, and a dim laptop at 2am, which is when this console -// is actually read. -export type OperationBand = { - id: string; - label: string; - // How many videos this pipeline has an OPINION about. null = at least one - // channel cannot say. - eligible: number | null; - // Videos it is done with. null for the same reason. - present: number | null; - // What the lane can do RIGHT NOW — missing + stale + partial. The one - // saturated segment on the page: glancing down the rail shows where the - // colour is, i.e. where work can happen at all. - reachable: number; - // Waiting on an operation THIS SYSTEM produces (attribution on diarization, - // digest on transcription). Hatched: it will clear itself, upstream. - blocked: number; - // The media is gone. Hollow: there is nothing to do here, and an opt-in - // re-download is the only thing that would change it. - missingInput: number; - // Held by a gate — stale cues, a duration window. Dotted. - deferred: number; - // Whether the operation is dispatched by this system at all. False for the - // two external pipelines, which are here because the rail's whole point is - // that you never have to switch lanes to learn that this one is idle BECAUSE - // another one is. - dispatched: boolean; -}; - -// Sum that returns null if ANY term is null. Same rule digestEligibleKnown uses -// in the widget payload: a partial sum is a denominator smaller than its own -// numerator, which is a worse lie than admitting the number is not knowable. -export function sumOrNull( - values: ReadonlyArray<number | null>, -): number | null { - let total = 0; - for (const v of values) { - if (v == null) return null; - total += v; - } - return total; -} - -// The fraction of a band that is `present`, or null when either half is unknown. -// Null renders as an unfilled outline — never as 0, which on a fully-digested -// channel would read as "nothing digested". -export function bandCoverage(band: OperationBand): number | null { - if (band.present == null || band.eligible == null || band.eligible <= 0) { - return null; - } - return Math.min(1, band.present / band.eligible); -} diff --git a/editor/app/auto-queue/lib/operationBands.test.ts b/editor/app/auto-queue/lib/operationBands.test.ts @@ -1,200 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import type { ChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; -import type { BackfillSnapshotEntry } from "yt-dlp-transcript-common/lib/backfillKinds"; -import { - bandCoverage, - buildOperationBands, - sumOrNull, - type OperationBand, -} from "./operationBands"; - -// Run from this directory: -// cd editor/app/auto-queue/lib && ../../../../node_modules/.bin/tsx --test operationBands.test.ts - -function snapshotOf(patch: Partial<ChannelSnapshot> = {}): ChannelSnapshot { - return { - generatedAt: "2026-08-21T00:00:00.000Z", - totals: { videos: 100, transcribed: 40, downloaded: 60 }, - buckets: {} as ChannelSnapshot["buckets"], - ...patch, - } as ChannelSnapshot; -} - -function entryOf(patch: Partial<BackfillSnapshotEntry>): BackfillSnapshotEntry { - return { - missing: 0, - stale: 0, - partial: 0, - missingInput: 0, - deferred: 0, - blocked: 0, - ids: [], - ...patch, - } as BackfillSnapshotEntry; -} - -const bandOf = (bands: OperationBand[], id: string): OperationBand => { - const found = bands.find((b) => b.id === id); - assert.ok(found, `no band for ${id}`); - return found; -}; - -test("the four work states are kept apart and never summed", () => { - // The measured shape of this corpus in miniature: diarization is dominated by - // missing media and attribution-diarized by blocked work. Any code that added - // them would report both lanes as busy. - const bands = buildOperationBands({ - snapshots: [ - snapshotOf({ - backfill: { - diarization: entryOf({ - missing: 647, - missingInput: 77_276, - eligible: 78_019, - }), - "attribution-diarized": entryOf({ - missing: 94, - blocked: 77_923, - eligible: 78_019, - }), - }, - }), - ], - operationIds: ["diarization", "attribution-diarized"], - }); - const dia = bandOf(bands, "diarization"); - assert.equal(dia.reachable, 647); - assert.equal(dia.missingInput, 77_276); - assert.equal(dia.blocked, 0); - const attr = bandOf(bands, "attribution-diarized"); - assert.equal(attr.reachable, 94); - assert.equal(attr.blocked, 77_923); - assert.equal(attr.missingInput, 0); -}); - -test("one channel that cannot report `eligible` voids the whole denominator", () => { - // The partial-sum trap. A snapshot predating `eligible` contributes videos to - // the corpus but nothing to the denominator, so summing what IS known gives a - // denominator smaller than its own numerator. - const bands = buildOperationBands({ - snapshots: [ - snapshotOf({ - backfill: { diarization: entryOf({ missing: 1, eligible: 500 }) }, - }), - snapshotOf({ - // No `eligible` — an older snapshot. - backfill: { diarization: entryOf({ missing: 2 }) }, - }), - ], - operationIds: ["diarization"], - }); - const dia = bandOf(bands, "diarization"); - assert.equal(dia.reachable, 3, "work counts still sum"); - assert.equal(dia.eligible, null, "the denominator does not"); - assert.equal(dia.present, null); - assert.equal(bandCoverage(dia), null, "and coverage renders as unknown"); -}); - -test("coverage is null, never 0, when the denominator is unknown", () => { - // A 0 here would read as "nothing digested" on a fully digested channel. - assert.equal( - bandCoverage({ present: null, eligible: 10 } as OperationBand), - null, - ); - assert.equal( - bandCoverage({ present: 5, eligible: null } as OperationBand), - null, - ); - assert.equal(bandCoverage({ present: 5, eligible: 0 } as OperationBand), null); - assert.equal(bandCoverage({ present: 5, eligible: 10 } as OperationBand), 0.5); -}); - -test("digest falls back to the bucket when a snapshot predates its registry entry", () => { - // Do NOT stop reading buckets.noDigest until every snapshot has regenerated: - // the fallback is all that keeps a stale channel from reading "all digested". - const bands = buildOperationBands({ - snapshots: [ - snapshotOf({ - buckets: { noDigest: ["a", "b", "c"] } as ChannelSnapshot["buckets"], - }), - ], - operationIds: ["digest"], - }); - const digest = bandOf(bands, "digest"); - assert.equal(digest.reachable, 3); - // The bucket cannot say how many are done, and it must not pretend to. - assert.equal(digest.eligible, null); - assert.equal(digest.present, null); -}); - -test("the external pipelines get bands from totals and buckets", () => { - const bands = buildOperationBands({ - snapshots: [ - snapshotOf({ - totals: { videos: 100, transcribed: 40, downloaded: 60 }, - undownloadedIds: ["u1", "u2", "u3"], - buckets: { - downloadedNoTranscript: ["d1", "d2"], - noTranscript: Array.from({ length: 60 }, (_, i) => `n${i}`), - untranscribable: ["x1", "x2"], - partialDownloads: ["p1"], - } as unknown as ChannelSnapshot["buckets"], - }), - ], - operationIds: [], - }); - const download = bandOf(bands, "download"); - // Every video the playlist knows about, not just the dirs that exist. - assert.equal(download.eligible, 103); - assert.equal(download.present, 60); - assert.equal(download.reachable, 4, "3 never fetched + 1 partial"); - assert.equal(download.dispatched, false); - - const transcription = bandOf(bands, "transcription"); - assert.equal(transcription.eligible, 98, "100 videos less 2 untranscribable"); - assert.equal(transcription.present, 40); - assert.equal(transcription.reachable, 2, "audio in hand"); - // The rest of noTranscript is waiting on the DOWNLOAD lane — blocked on an - // operation this system produces, not reachable and not missing media. - assert.equal(transcription.blocked, 56); - assert.equal(transcription.missingInput, 0); -}); - -test("ids excluded from download are deferred, not reachable", () => { - // A channel deliberately not fetching members-only videos is not a lane with - // work to do, and the two must stay separable rather than one being netted - // off the other. - const bands = buildOperationBands({ - snapshots: [ - snapshotOf({ - totals: { videos: 0, transcribed: 0, downloaded: 0 }, - undownloadedIds: ["ok", "gone"], - excludedFromDownload: { deleted: ["gone"] }, - } as Partial<ChannelSnapshot>), - ], - operationIds: [], - }); - const download = bandOf(bands, "download"); - assert.equal(download.reachable, 1); - assert.equal(download.deferred, 1); -}); - -test("a switched-off operation gets no band at all", () => { - // Absent, not zero: an empty work list because nobody enabled the feature is - // not the same as being finished, and a full green bar would claim it was. - const bands = buildOperationBands({ - snapshots: [snapshotOf({ backfill: { diarization: entryOf({ missing: 5 }) } })], - operationIds: [], - }); - assert.equal( - bands.find((b) => b.id === "diarization"), - undefined, - ); -}); - -test("sumOrNull latches null and never returns a partial total", () => { - assert.equal(sumOrNull([1, 2, 3]), 6); - assert.equal(sumOrNull([1, null, 3]), null); - assert.equal(sumOrNull([]), 0); -}); diff --git a/editor/app/auto-queue/lib/operationBands.ts b/editor/app/auto-queue/lib/operationBands.ts @@ -1,196 +0,0 @@ -import type { ChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; -import { - digestWorkOf, - excludedDownloadIdSet, -} from "yt-dlp-transcript-common/controller/channelSnapshot"; -import { - DIGEST_KIND_ID, - operationLabel, - presentBackfillWork, - reachableBackfillWork, -} from "yt-dlp-transcript-common/lib/backfillKinds"; -import { sumOrNull, type OperationBand } from "./operationBand"; - -export type { OperationBand } from "./operationBand"; -export { bandCoverage, sumOrNull } from "./operationBand"; - -// THE COMPARISON RAIL'S MODEL: one band per pipeline, summed across the corpus. -// -// This is the corpus-wide twin of channelFlow's transit line, and it holds the -// same two invariants for the same reasons — they are the two ways every earlier -// version of this number was wrong: -// -// 1. WORK THE LANE CAN DO IS NEVER SUMMED WITH WORK IT CANNOT. `reachable`, -// `blocked`, `missingInput` and `deferred` are four separate fields on four -// different axes, and nothing here adds them. On the live corpus that is not -// pedantry: attribution-diarized is 94 reachable against 77,923 blocked, and -// diarization is 647 against 77,276 with no media. A single "remaining" -// figure would say the same thing about a lane that is finished and a lane -// that cannot start. -// 2. UNKNOWN IS NOT ZERO. `eligible` and `present` are `number | null`, and one -// null poisons the whole sum deliberately — a third of the snapshots on disk -// predate `eligible`, and "three channels are done and the fourth is -// unknown" is not a number. The band renders a null denominator as an -// unfilled outline, never as 0% progress. -// -// WHY THE RATIO IS THE STORY, AND WHY EACH BAND KEEPS ITS OWN DENOMINATOR. -// Three of the four pipelines are dominated by a non-actionable state, so a -// count renders them as "94" and "647" and tells you nothing. And digest's -// eligible population is genuinely a different set from diarization's — sharing -// one denominator across the rail to make the bars comparable would be a lie -// about what is being compared. Each band states its own, in its own header. -// -// Pure and snapshot-only: common/controller/noCorpusWalkInRenderPaths.test.ts -// bans a corpus walk from a render path, and this feeds a 3-second poll. - -function emptyBand(id: string, dispatched: boolean): OperationBand { - return { - id, - label: operationLabel(id), - eligible: 0, - present: 0, - reachable: 0, - blocked: 0, - missingInput: 0, - deferred: 0, - dispatched, - }; -} - -// Fold one snapshot's entry for a registry operation into a band. `null` for -// either coverage half latches for the whole corpus. -function addRegistryEntry(band: OperationBand, snapshot: ChannelSnapshot): void { - const entry = snapshot.backfill?.[band.id]; - if (!entry) return; - band.reachable += reachableBackfillWork(entry); - band.missingInput += entry.missingInput; - // `?? 0` at every read: snapshots written before these fields existed lack - // them, and undefined poisons the sum to NaN. - band.blocked += entry.blocked ?? 0; - band.deferred += entry.deferred ?? 0; - band.eligible = sumOrNull([band.eligible, entry.eligible ?? null]); - band.present = sumOrNull([band.present, presentBackfillWork(entry)]); -} - -export type BuildOperationBandsInput = { - snapshots: ReadonlyArray<ChannelSnapshot | null>; - // Registry operations to build a band for, in rail order. Comes from - // allBackfillKinds(), so a switched-off feature is simply absent — which is - // the honest rendering: an empty work list because nobody enabled it is not - // the same as being finished. - operationIds: ReadonlyArray<string>; -}; - -// The two pipelines this system counts but does not dispatch through the -// operation registry. They are on the rail anyway, and deliberately: -// -// The rail exists so you never have to switch lanes to learn that THIS lane is -// idle because ANOTHER one is — and on this corpus that is the normal case, not -// the exception (diarization is 99.2% media-gone; attribution-diarized is 99.9% -// blocked behind diarization). Leaving transcription and download off it would -// remove exactly the two lanes whose state explains the other four. -// -// Their numbers do NOT come from BackfillKind.state() — they have no entry, -// because EXTERNAL_OPERATIONS registers them for the dependency graph and not -// for dispatch. They come from `totals` and the buckets, using the SAME -// definitions the channel transit line already uses for its Download and -// Transcribe stations, so a corpus figure and a channel figure cannot disagree -// about what "downloaded" means. -function addExternalBands( - bands: Map<string, OperationBand>, - snapshot: ChannelSnapshot, -): void { - const totals = snapshot.totals ?? { videos: 0, transcribed: 0, downloaded: 0 }; - const buckets = snapshot.buckets; - const undownloaded = snapshot.undownloadedIds ?? []; - const excluded = excludedDownloadIdSet(snapshot); - - const download = bands.get("download"); - if (download) { - // Eligible is every video the playlist knows about: the dirs that exist - // plus the ids that have never been fetched. `totals.videos` alone would be - // a denominator that grows only as work completes. - download.eligible = sumOrNull([ - download.eligible, - totals.videos + undownloaded.length, - ]); - download.present = sumOrNull([download.present, totals.downloaded]); - // Partial downloads are reachable work like any other — the same rule - // backfillBatch applies to `partial`. - download.reachable += - undownloaded.filter((id) => !excluded.has(id)).length + - (buckets?.partialDownloads?.length ?? 0); - // Excluded ids have LEFT the line: a channel deliberately not fetching - // them is not a lane with work to do. They are deferred, not reachable — - // and never subtracted from anything, so the two stay separable. - download.deferred += undownloaded.filter((id) => excluded.has(id)).length; - } - - const transcription = bands.get("transcription"); - if (transcription) { - // A video marked untranscribable is not eligible — it is not work anyone is - // waiting on, and counting it would put a permanent ceiling under 100%. - const untranscribable = buckets?.untranscribable?.length ?? 0; - transcription.eligible = sumOrNull([ - transcription.eligible, - Math.max(0, totals.videos - untranscribable), - ]); - transcription.present = sumOrNull([ - transcription.present, - totals.transcribed, - ]); - // Reachable = the audio is in hand. Everything else without a transcript is - // waiting on the DOWNLOAD lane, which is exactly what `blocked` means here - // — an operation this system produces, one station upstream. - const downloadedNoTranscript = ( - buckets?.downloadedNoTranscript ?? [] - ).filter((id) => !excluded.has(id)).length; - const noTranscript = buckets?.noTranscript?.length ?? 0; - transcription.reachable += downloadedNoTranscript; - transcription.blocked += Math.max( - 0, - noTranscript - untranscribable - downloadedNoTranscript, - ); - } -} - -// The rail, left to right. Download and transcription lead because everything -// else depends on them; digest and the backfill kinds follow in registry order. -export const EXTERNAL_BAND_IDS = ["download", "transcription"] as const; - -export function buildOperationBands({ - snapshots, - operationIds, -}: BuildOperationBandsInput): OperationBand[] { - const bands = new Map<string, OperationBand>(); - for (const id of EXTERNAL_BAND_IDS) bands.set(id, emptyBand(id, false)); - for (const id of operationIds) { - if (!bands.has(id)) bands.set(id, emptyBand(id, true)); - } - - for (const snapshot of snapshots) { - if (!snapshot) continue; - addExternalBands(bands, snapshot); - for (const id of operationIds) { - const band = bands.get(id); - if (!band) continue; - if (id === DIGEST_KIND_ID && !snapshot.backfill?.[id]) { - // A snapshot written before digest joined the registry has no entry, and - // digestWorkOf is the fallback that reads the same population off the - // buckets with the transcript and cues-staleness gates applied. Without - // it a stale channel reads "all digested", which is the one failure mode - // the source:"bucket" fallback exists to prevent. - const work = digestWorkOf(snapshot); - band.reachable += work.reachable; - band.blocked += work.blocked; - band.deferred += work.deferred; - band.eligible = sumOrNull([band.eligible, work.eligible]); - band.present = sumOrNull([band.present, work.present]); - continue; - } - addRegistryEntry(band, snapshot); - } - } - - return [...bands.values()]; -} diff --git a/editor/app/components/pipelines/StateBand.tsx b/editor/app/components/pipelines/StateBand.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { bandCoverage, type OperationBand } from "./band"; + +// THE INSTRUMENT. One component, three scales, one vocabulary. +// +// The comparison rail built for /auto-queue answers "what can this pipeline do +// right now" for the whole corpus. Shrunk, the same instrument answers it per +// channel; shrunk again, per station on a channel page. Nothing new is invented +// here — the reason the three surfaces read as one system is that they are +// literally the same five fills, from the same model, in the same order. +// +// EXACTLY ONE SATURATED COLOUR, EVERYWHERE: `reachable`. Every other fill is +// texture on neutral. That makes "where the colour is" mean "where work can +// happen now" at table scale, at rail scale and at station scale — and a second +// accent would break that reading in all three places at once. +// +// FILL PATTERN FIRST, HUE SECOND. Not a stylistic whim: report-to-video's claim +// rail established here BY MEASUREMENT that no four-colour palette clears +// all-pairs colour-blindness. Pattern (solid / hatched / dotted / hollow) +// survives CVD, greyscale, and a dim laptop at 2am, which is when these +// consoles are actually read. +// +// EVERY FILL IS A TOKEN from common/styles/tokens.css. No new colour ships with +// this instrument, and that is load-bearing rather than frugal. + +// The five populations, in the order they are laid down. `present` first so a +// band reads left-to-right as "done → can do → cannot do yet → cannot do at +// all", which is the same direction the channel transit line runs. +export const SEGMENTS = [ + { + key: "present", + label: "done", + // Solid, muted. The artifact exists; it is not where attention goes. + className: "bg-muted-foreground/45", + }, + { + key: "reachable", + label: "can run now", + // THE one saturated segment. + className: "bg-info", + }, + { + key: "blocked", + label: "blocked upstream", + // Hatched — waiting on an operation this system produces, so it will clear + // itself without anyone doing anything. + className: + "bg-warning/25 [background-image:repeating-linear-gradient(45deg,currentColor_0_1px,transparent_1px_4px)] text-warning/70", + }, + { + key: "deferred", + label: "held by a gate", + // Dotted — a gate (stale cues, a duration window) is holding it. + className: + "bg-transparent [background-image:radial-gradient(currentColor_0.5px,transparent_0.5px)] [background-size:3px_3px] text-muted-foreground", + }, + { + key: "missingInput", + label: "media gone", + // Hollow outline — there is nothing to do. An opt-in re-download is the + // only thing that would ever change it, and it must never look like work. + className: "bg-transparent ring-1 ring-inset ring-border", + }, +] as const; + +export type SegmentKey = (typeof SEGMENTS)[number]["key"]; + +export function segmentValue(band: OperationBand, key: SegmentKey): number { + if (key === "present") return band.present ?? 0; + return band[key]; +} + +// The denominator a band's segments are drawn against. +// +// `eligible` when it is known. When it is NOT — a snapshot can predate the +// field, and snapshots lapse — falling back to the sum of the work counts would +// silently redraw the band as "100% accounted for", so instead the band renders +// as an un-filled outline and says `coverage unknown`. Unknown is not zero, and +// it is not full either. +export function bandDenominator(band: OperationBand): number | null { + if (band.eligible != null && band.eligible > 0) return band.eligible; + return null; +} + +// The three scales. Height and minimum width only — the fills, the order and +// the meanings are identical, which is the entire point. +// +// `strip` DOES NOT ANIMATE, and that is a deliberate subtraction rather than an +// oversight. The rail eases a segment to its new width because it draws one band +// per lane and a change there is news; /channels draws 408 of them, and 408 +// bands easing on a poll is a light show, not an instrument. +const SIZES = { + rail: { bar: "h-2.5 min-w-32 flex-1", animate: true }, + strip: { bar: "h-1.5 w-full", animate: false }, + station: { bar: "h-1 w-full", animate: false }, +} as const; + +export type BandSize = keyof typeof SIZES; + +// The sentence a band carries in its tooltip and its accessible name. +// +// THE FIGURES ARE STATED SEPARATELY AND NEVER SUMMED — the same rule the rail's +// figure row holds, moved somewhere it applies to every scale at once. On the +// measured corpus `reachable` and `missingInput` are four orders of magnitude +// apart on diarization; one "remaining" number would say the same thing about a +// lane that is finished and a lane that cannot start. +export function bandSentence(band: OperationBand): string { + const parts: string[] = [`${band.reachable.toLocaleString()} can run now`]; + if (band.blocked > 0) { + parts.push(`${band.blocked.toLocaleString()} blocked upstream`); + } + if (band.deferred > 0) { + parts.push(`${band.deferred.toLocaleString()} held by a gate`); + } + if (band.missingInput > 0) { + parts.push(`${band.missingInput.toLocaleString()} have no media left`); + } + const denominator = bandDenominator(band); + parts.push( + band.present == null || denominator == null + ? "coverage unknown" + : `${band.present.toLocaleString()} done of ${denominator.toLocaleString()}`, + ); + return parts.join(" · "); +} + +export function StateBand({ + band, + size, + className = "", +}: { + band: OperationBand; + size: BandSize; + className?: string; +}) { + const denominator = bandDenominator(band); + const spec = SIZES[size]; + + if (denominator === null) { + // Unknown denominator: an outline and nothing else. Drawing the work counts + // against their own sum would claim the band is fully accounted for, which + // is a different (and worse) statement than "we cannot say". + return ( + <span + aria-hidden="true" + className={`block rounded-sm border border-dashed border-border ${spec.bar} ${className}`} + /> + ); + } + + return ( + <span + aria-hidden="true" + className={`flex overflow-hidden rounded-sm bg-muted ${spec.bar} ${className}`} + > + {SEGMENTS.map((seg) => { + const pct = (segmentValue(band, seg.key) / denominator) * 100; + if (pct <= 0) return null; + return ( + <span + key={seg.key} + // The rail's one piece of motion: a segment eases to its new width + // when the number actually changes. Never on poll (the width is the + // same, so no transition fires) and never at all under + // prefers-reduced-motion, which every animation here pairs with. + className={`h-full ${ + spec.animate + ? "transition-[width] duration-500 motion-reduce:transition-none" + : "" + } ${seg.className}`} + style={{ width: `${Math.min(100, pct)}%` }} + /> + ); + })} + </span> + ); +} + +// The legend is the only place the pattern vocabulary is named, and it is named +// in WORDS as well as swatches — a pattern nobody can read the meaning of is +// just noise, and this is the one non-obvious visual convention these pages +// share. +export function BandLegend({ className = "" }: { className?: string }) { + return ( + <ul + className={`flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground ${className}`} + > + {SEGMENTS.map((seg) => ( + <li key={seg.key} className="flex items-center gap-1.5"> + <span + aria-hidden="true" + className={`h-2.5 w-4 shrink-0 rounded-sm ${seg.className}`} + /> + {seg.label} + </li> + ))} + </ul> + ); +} + +export { bandCoverage }; diff --git a/editor/app/components/pipelines/band.ts b/editor/app/components/pipelines/band.ts @@ -0,0 +1,76 @@ +// The band TYPE and the two pure functions over it. +// +// SPLIT FROM buildBands.ts DELIBERATELY, and the reason is a build error rather +// than tidiness: the builder imports channelSnapshot, which imports runYtdlp, +// which imports execa — so a client component importing the builder drags a +// server-only process runner into the browser bundle, and `next build` fails +// with a module-not-found on `node:child_process`. (Nothing catches that in +// e2e, which runs in dev mode and never prerenders.) +// +// So StateBand and every client surface import THIS, and the builder stays +// server-side. Nothing here may import anything but types. +// +// LIVES UNDER components/pipelines/ RATHER THAN auto-queue/, because three +// surfaces at three scales now render the same five fills: the corpus rail on +// /auto-queue, the per-channel strip on /channels, and the station foot on a +// channel page. That they are literally one model, one component and one +// palette is the reason the three read as one instrument rather than three +// charts that happen to rhyme. + +// The five populations a pipeline can put a video in, plus the denominator. +// +// Rendered as fill PATTERNS first and hue second — solid / hatched / dotted / +// hollow — because report-to-video's claim rail established here BY MEASUREMENT +// that no four-colour palette clears all-pairs colour-blindness. Pattern +// survives CVD, greyscale, and a dim laptop at 2am, which is when this console +// is actually read. +export type OperationBand = { + id: string; + label: string; + // How many videos this pipeline has an OPINION about. null = at least one + // channel cannot say. + eligible: number | null; + // Videos it is done with. null for the same reason. + present: number | null; + // What the lane can do RIGHT NOW — missing + stale + partial. The one + // saturated segment on the page: glancing down the rail shows where the + // colour is, i.e. where work can happen at all. + reachable: number; + // Waiting on an operation THIS SYSTEM produces (attribution on diarization, + // digest on transcription). Hatched: it will clear itself, upstream. + blocked: number; + // The media is gone. Hollow: there is nothing to do here, and an opt-in + // re-download is the only thing that would change it. + missingInput: number; + // Held by a gate — stale cues, a duration window. Dotted. + deferred: number; + // Whether the operation is dispatched by this system at all. False for the + // two external pipelines, which are here because the rail's whole point is + // that you never have to switch lanes to learn that this one is idle BECAUSE + // another one is. + dispatched: boolean; +}; + +// Sum that returns null if ANY term is null. Same rule digestEligibleKnown uses +// in the widget payload: a partial sum is a denominator smaller than its own +// numerator, which is a worse lie than admitting the number is not knowable. +export function sumOrNull( + values: ReadonlyArray<number | null>, +): number | null { + let total = 0; + for (const v of values) { + if (v == null) return null; + total += v; + } + return total; +} + +// The fraction of a band that is `present`, or null when either half is unknown. +// Null renders as an unfilled outline — never as 0, which on a fully-digested +// channel would read as "nothing digested". +export function bandCoverage(band: OperationBand): number | null { + if (band.present == null || band.eligible == null || band.eligible <= 0) { + return null; + } + return Math.min(1, band.present / band.eligible); +} diff --git a/editor/app/components/pipelines/buildBands.test.ts b/editor/app/components/pipelines/buildBands.test.ts @@ -0,0 +1,200 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import type { ChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; +import type { BackfillSnapshotEntry } from "yt-dlp-transcript-common/lib/backfillKinds"; +import { + bandCoverage, + buildOperationBands, + sumOrNull, + type OperationBand, +} from "./buildBands"; + +// Run from this directory: +// cd editor/app/components/pipelines && ../../../../node_modules/.bin/tsx --test buildBands.test.ts + +function snapshotOf(patch: Partial<ChannelSnapshot> = {}): ChannelSnapshot { + return { + generatedAt: "2026-08-21T00:00:00.000Z", + totals: { videos: 100, transcribed: 40, downloaded: 60 }, + buckets: {} as ChannelSnapshot["buckets"], + ...patch, + } as ChannelSnapshot; +} + +function entryOf(patch: Partial<BackfillSnapshotEntry>): BackfillSnapshotEntry { + return { + missing: 0, + stale: 0, + partial: 0, + missingInput: 0, + deferred: 0, + blocked: 0, + ids: [], + ...patch, + } as BackfillSnapshotEntry; +} + +const bandOf = (bands: OperationBand[], id: string): OperationBand => { + const found = bands.find((b) => b.id === id); + assert.ok(found, `no band for ${id}`); + return found; +}; + +test("the four work states are kept apart and never summed", () => { + // The measured shape of this corpus in miniature: diarization is dominated by + // missing media and attribution-diarized by blocked work. Any code that added + // them would report both lanes as busy. + const bands = buildOperationBands({ + snapshots: [ + snapshotOf({ + backfill: { + diarization: entryOf({ + missing: 647, + missingInput: 77_276, + eligible: 78_019, + }), + "attribution-diarized": entryOf({ + missing: 94, + blocked: 77_923, + eligible: 78_019, + }), + }, + }), + ], + operationIds: ["diarization", "attribution-diarized"], + }); + const dia = bandOf(bands, "diarization"); + assert.equal(dia.reachable, 647); + assert.equal(dia.missingInput, 77_276); + assert.equal(dia.blocked, 0); + const attr = bandOf(bands, "attribution-diarized"); + assert.equal(attr.reachable, 94); + assert.equal(attr.blocked, 77_923); + assert.equal(attr.missingInput, 0); +}); + +test("one channel that cannot report `eligible` voids the whole denominator", () => { + // The partial-sum trap. A snapshot predating `eligible` contributes videos to + // the corpus but nothing to the denominator, so summing what IS known gives a + // denominator smaller than its own numerator. + const bands = buildOperationBands({ + snapshots: [ + snapshotOf({ + backfill: { diarization: entryOf({ missing: 1, eligible: 500 }) }, + }), + snapshotOf({ + // No `eligible` — an older snapshot. + backfill: { diarization: entryOf({ missing: 2 }) }, + }), + ], + operationIds: ["diarization"], + }); + const dia = bandOf(bands, "diarization"); + assert.equal(dia.reachable, 3, "work counts still sum"); + assert.equal(dia.eligible, null, "the denominator does not"); + assert.equal(dia.present, null); + assert.equal(bandCoverage(dia), null, "and coverage renders as unknown"); +}); + +test("coverage is null, never 0, when the denominator is unknown", () => { + // A 0 here would read as "nothing digested" on a fully digested channel. + assert.equal( + bandCoverage({ present: null, eligible: 10 } as OperationBand), + null, + ); + assert.equal( + bandCoverage({ present: 5, eligible: null } as OperationBand), + null, + ); + assert.equal(bandCoverage({ present: 5, eligible: 0 } as OperationBand), null); + assert.equal(bandCoverage({ present: 5, eligible: 10 } as OperationBand), 0.5); +}); + +test("digest falls back to the bucket when a snapshot predates its registry entry", () => { + // Do NOT stop reading buckets.noDigest until every snapshot has regenerated: + // the fallback is all that keeps a stale channel from reading "all digested". + const bands = buildOperationBands({ + snapshots: [ + snapshotOf({ + buckets: { noDigest: ["a", "b", "c"] } as ChannelSnapshot["buckets"], + }), + ], + operationIds: ["digest"], + }); + const digest = bandOf(bands, "digest"); + assert.equal(digest.reachable, 3); + // The bucket cannot say how many are done, and it must not pretend to. + assert.equal(digest.eligible, null); + assert.equal(digest.present, null); +}); + +test("the external pipelines get bands from totals and buckets", () => { + const bands = buildOperationBands({ + snapshots: [ + snapshotOf({ + totals: { videos: 100, transcribed: 40, downloaded: 60 }, + undownloadedIds: ["u1", "u2", "u3"], + buckets: { + downloadedNoTranscript: ["d1", "d2"], + noTranscript: Array.from({ length: 60 }, (_, i) => `n${i}`), + untranscribable: ["x1", "x2"], + partialDownloads: ["p1"], + } as unknown as ChannelSnapshot["buckets"], + }), + ], + operationIds: [], + }); + const download = bandOf(bands, "download"); + // Every video the playlist knows about, not just the dirs that exist. + assert.equal(download.eligible, 103); + assert.equal(download.present, 60); + assert.equal(download.reachable, 4, "3 never fetched + 1 partial"); + assert.equal(download.dispatched, false); + + const transcription = bandOf(bands, "transcription"); + assert.equal(transcription.eligible, 98, "100 videos less 2 untranscribable"); + assert.equal(transcription.present, 40); + assert.equal(transcription.reachable, 2, "audio in hand"); + // The rest of noTranscript is waiting on the DOWNLOAD lane — blocked on an + // operation this system produces, not reachable and not missing media. + assert.equal(transcription.blocked, 56); + assert.equal(transcription.missingInput, 0); +}); + +test("ids excluded from download are deferred, not reachable", () => { + // A channel deliberately not fetching members-only videos is not a lane with + // work to do, and the two must stay separable rather than one being netted + // off the other. + const bands = buildOperationBands({ + snapshots: [ + snapshotOf({ + totals: { videos: 0, transcribed: 0, downloaded: 0 }, + undownloadedIds: ["ok", "gone"], + excludedFromDownload: { deleted: ["gone"] }, + } as Partial<ChannelSnapshot>), + ], + operationIds: [], + }); + const download = bandOf(bands, "download"); + assert.equal(download.reachable, 1); + assert.equal(download.deferred, 1); +}); + +test("a switched-off operation gets no band at all", () => { + // Absent, not zero: an empty work list because nobody enabled the feature is + // not the same as being finished, and a full green bar would claim it was. + const bands = buildOperationBands({ + snapshots: [snapshotOf({ backfill: { diarization: entryOf({ missing: 5 }) } })], + operationIds: [], + }); + assert.equal( + bands.find((b) => b.id === "diarization"), + undefined, + ); +}); + +test("sumOrNull latches null and never returns a partial total", () => { + assert.equal(sumOrNull([1, 2, 3]), 6); + assert.equal(sumOrNull([1, null, 3]), null); + assert.equal(sumOrNull([]), 0); +}); diff --git a/editor/app/components/pipelines/buildBands.ts b/editor/app/components/pipelines/buildBands.ts @@ -0,0 +1,196 @@ +import type { ChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; +import { + digestWorkOf, + excludedDownloadIdSet, +} from "yt-dlp-transcript-common/controller/channelSnapshot"; +import { + DIGEST_KIND_ID, + operationLabel, + presentBackfillWork, + reachableBackfillWork, +} from "yt-dlp-transcript-common/lib/backfillKinds"; +import { sumOrNull, type OperationBand } from "./band"; + +export type { OperationBand } from "./band"; +export { bandCoverage, sumOrNull } from "./band"; + +// THE COMPARISON RAIL'S MODEL: one band per pipeline, summed across the corpus. +// +// This is the corpus-wide twin of channelFlow's transit line, and it holds the +// same two invariants for the same reasons — they are the two ways every earlier +// version of this number was wrong: +// +// 1. WORK THE LANE CAN DO IS NEVER SUMMED WITH WORK IT CANNOT. `reachable`, +// `blocked`, `missingInput` and `deferred` are four separate fields on four +// different axes, and nothing here adds them. On the live corpus that is not +// pedantry: attribution-diarized is 94 reachable against 77,923 blocked, and +// diarization is 647 against 77,276 with no media. A single "remaining" +// figure would say the same thing about a lane that is finished and a lane +// that cannot start. +// 2. UNKNOWN IS NOT ZERO. `eligible` and `present` are `number | null`, and one +// null poisons the whole sum deliberately — a third of the snapshots on disk +// predate `eligible`, and "three channels are done and the fourth is +// unknown" is not a number. The band renders a null denominator as an +// unfilled outline, never as 0% progress. +// +// WHY THE RATIO IS THE STORY, AND WHY EACH BAND KEEPS ITS OWN DENOMINATOR. +// Three of the four pipelines are dominated by a non-actionable state, so a +// count renders them as "94" and "647" and tells you nothing. And digest's +// eligible population is genuinely a different set from diarization's — sharing +// one denominator across the rail to make the bars comparable would be a lie +// about what is being compared. Each band states its own, in its own header. +// +// Pure and snapshot-only: common/controller/noCorpusWalkInRenderPaths.test.ts +// bans a corpus walk from a render path, and this feeds a 3-second poll. + +function emptyBand(id: string, dispatched: boolean): OperationBand { + return { + id, + label: operationLabel(id), + eligible: 0, + present: 0, + reachable: 0, + blocked: 0, + missingInput: 0, + deferred: 0, + dispatched, + }; +} + +// Fold one snapshot's entry for a registry operation into a band. `null` for +// either coverage half latches for the whole corpus. +function addRegistryEntry(band: OperationBand, snapshot: ChannelSnapshot): void { + const entry = snapshot.backfill?.[band.id]; + if (!entry) return; + band.reachable += reachableBackfillWork(entry); + band.missingInput += entry.missingInput; + // `?? 0` at every read: snapshots written before these fields existed lack + // them, and undefined poisons the sum to NaN. + band.blocked += entry.blocked ?? 0; + band.deferred += entry.deferred ?? 0; + band.eligible = sumOrNull([band.eligible, entry.eligible ?? null]); + band.present = sumOrNull([band.present, presentBackfillWork(entry)]); +} + +export type BuildOperationBandsInput = { + snapshots: ReadonlyArray<ChannelSnapshot | null>; + // Registry operations to build a band for, in rail order. Comes from + // allBackfillKinds(), so a switched-off feature is simply absent — which is + // the honest rendering: an empty work list because nobody enabled it is not + // the same as being finished. + operationIds: ReadonlyArray<string>; +}; + +// The two pipelines this system counts but does not dispatch through the +// operation registry. They are on the rail anyway, and deliberately: +// +// The rail exists so you never have to switch lanes to learn that THIS lane is +// idle because ANOTHER one is — and on this corpus that is the normal case, not +// the exception (diarization is 99.2% media-gone; attribution-diarized is 99.9% +// blocked behind diarization). Leaving transcription and download off it would +// remove exactly the two lanes whose state explains the other four. +// +// Their numbers do NOT come from BackfillKind.state() — they have no entry, +// because EXTERNAL_OPERATIONS registers them for the dependency graph and not +// for dispatch. They come from `totals` and the buckets, using the SAME +// definitions the channel transit line already uses for its Download and +// Transcribe stations, so a corpus figure and a channel figure cannot disagree +// about what "downloaded" means. +function addExternalBands( + bands: Map<string, OperationBand>, + snapshot: ChannelSnapshot, +): void { + const totals = snapshot.totals ?? { videos: 0, transcribed: 0, downloaded: 0 }; + const buckets = snapshot.buckets; + const undownloaded = snapshot.undownloadedIds ?? []; + const excluded = excludedDownloadIdSet(snapshot); + + const download = bands.get("download"); + if (download) { + // Eligible is every video the playlist knows about: the dirs that exist + // plus the ids that have never been fetched. `totals.videos` alone would be + // a denominator that grows only as work completes. + download.eligible = sumOrNull([ + download.eligible, + totals.videos + undownloaded.length, + ]); + download.present = sumOrNull([download.present, totals.downloaded]); + // Partial downloads are reachable work like any other — the same rule + // backfillBatch applies to `partial`. + download.reachable += + undownloaded.filter((id) => !excluded.has(id)).length + + (buckets?.partialDownloads?.length ?? 0); + // Excluded ids have LEFT the line: a channel deliberately not fetching + // them is not a lane with work to do. They are deferred, not reachable — + // and never subtracted from anything, so the two stay separable. + download.deferred += undownloaded.filter((id) => excluded.has(id)).length; + } + + const transcription = bands.get("transcription"); + if (transcription) { + // A video marked untranscribable is not eligible — it is not work anyone is + // waiting on, and counting it would put a permanent ceiling under 100%. + const untranscribable = buckets?.untranscribable?.length ?? 0; + transcription.eligible = sumOrNull([ + transcription.eligible, + Math.max(0, totals.videos - untranscribable), + ]); + transcription.present = sumOrNull([ + transcription.present, + totals.transcribed, + ]); + // Reachable = the audio is in hand. Everything else without a transcript is + // waiting on the DOWNLOAD lane, which is exactly what `blocked` means here + // — an operation this system produces, one station upstream. + const downloadedNoTranscript = ( + buckets?.downloadedNoTranscript ?? [] + ).filter((id) => !excluded.has(id)).length; + const noTranscript = buckets?.noTranscript?.length ?? 0; + transcription.reachable += downloadedNoTranscript; + transcription.blocked += Math.max( + 0, + noTranscript - untranscribable - downloadedNoTranscript, + ); + } +} + +// The rail, left to right. Download and transcription lead because everything +// else depends on them; digest and the backfill kinds follow in registry order. +export const EXTERNAL_BAND_IDS = ["download", "transcription"] as const; + +export function buildOperationBands({ + snapshots, + operationIds, +}: BuildOperationBandsInput): OperationBand[] { + const bands = new Map<string, OperationBand>(); + for (const id of EXTERNAL_BAND_IDS) bands.set(id, emptyBand(id, false)); + for (const id of operationIds) { + if (!bands.has(id)) bands.set(id, emptyBand(id, true)); + } + + for (const snapshot of snapshots) { + if (!snapshot) continue; + addExternalBands(bands, snapshot); + for (const id of operationIds) { + const band = bands.get(id); + if (!band) continue; + if (id === DIGEST_KIND_ID && !snapshot.backfill?.[id]) { + // A snapshot written before digest joined the registry has no entry, and + // digestWorkOf is the fallback that reads the same population off the + // buckets with the transcript and cues-staleness gates applied. Without + // it a stale channel reads "all digested", which is the one failure mode + // the source:"bucket" fallback exists to prevent. + const work = digestWorkOf(snapshot); + band.reachable += work.reachable; + band.blocked += work.blocked; + band.deferred += work.deferred; + band.eligible = sumOrNull([band.eligible, work.eligible]); + band.present = sumOrNull([band.present, work.present]); + continue; + } + addRegistryEntry(band, snapshot); + } + } + + return [...bands.values()]; +}