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:
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()];
+}