// THE ONE TABLE that turns an operation's state into words on the video page. // // No directive: pure, and imported by both the server component that builds a // panel and the client component that draws it. It reaches lib/operations only // for TYPES — that module imports node:fs through the digest, diarization and // attribution servers, so a value import here would drag the whole filesystem // graph into a "use client" bundle. // // The present / stale / missing rows are digest.spec.ts's contract ("not // generated", "current", and a stale string that must contain "stale"), so // those three strings do not move without that spec moving with them. import type { OperationClassification, OperationGroup, OperationSettingsBlock, } from "yt-dlp-transcript-common/lib/operations"; // The serializable half of VideoOperationView — what crosses to the client. // No `target`: it is an erased `unknown` the registry narrows per entry, and // the only consumer that narrows it is the digest body, on the server. export type OperationPanelView = { id: string; label: string; group: OperationGroup; settingsBlock?: OperationSettingsBlock; state: OperationClassification; enabled: boolean; outputs: { name: string; present: boolean }[]; // The kind's own words for what a deferred video is waiting for. Reaches the // client as a PROP, never by reading the registry. deferredHint?: string; dependsOn: { id: string; label: string }[]; }; export function stateCopy(view: OperationPanelView): string { switch (view.state) { case "present": return "current"; case "stale": return "stale — regenerating would replace this"; // `partial` deliberately CONTAINS "stale": a digest with some fresh // sections and some superseded ones is a fraction of the cost of a full // regeneration, and saying so is why the state exists — but a reader (and // digest.spec.ts's toContainText("stale")) must still see the same word. case "partial": return "partly stale — regenerating would replace the stale sections"; case "missing": return "not generated"; case "missing-input": return "input gone — needs media"; // "held — " plus the kind's OWN hint, verbatim. The hints are plural // fragments completing "N videos are …" and are kept that way on purpose: // one copy of a sentence cannot drift from itself, and operations.test.ts // pins the digest one. A singular twin would be a second copy. case "deferred": return view.deferredHint ? `held — ${view.deferredHint}` : "held"; case "blocked": return `waiting on ${joinLabels(view.dependsOn.map((d) => d.label))}`; case "not-applicable": return "not applicable"; } } export function stateTone( state: OperationClassification, ): "success" | "warning" | "muted" { switch (state) { case "present": return "success"; case "stale": case "partial": case "missing-input": return "warning"; case "missing": case "deferred": case "blocked": case "not-applicable": return "muted"; } } // "A", "A and B", "A, B and C". The same join OperationDetail.tsx uses for the // same sentence, so a blocked video reads the same on both surfaces. function joinLabels(labels: string[]): string { if (labels.length <= 1) return labels[0] ?? ""; return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`; }