#!/usr/bin/env tsx // The one-core Phase 3 slice 2 measurement: the three polled payloads whose // inputs are PURE LOADERS, printed deterministically so two runs can be diffed. // // WHY THESE THREE. Slice 2 moves eight polling routes behind `/api/view/[name]` // and lifts two route-resident builders into `common/views/`. The claim is "no // number moved". Five of the eight payloads are built from live singletons (the // job registry, the scheduler, the worker pool) and cannot be reproduced // offline — their content depends on what the server is doing at that instant. // The remaining three — `widgetActionable`, `cleanable` and `widgetSync` — are // folds over loaders that read only the corpus on disk, so they CAN be computed // in-process, offline, and diffed. // // STRICTLY READ-ONLY. It opens `settings.json`, the scheduler state file and the // per-channel `snapshot.json`/`config.json` through the ordinary loaders, and // writes nothing anywhere. // // NEVER BOOT AN EDITOR FOR THIS. The loaders are called in-process; // instrumentation.ts is not loaded, so no runner, sweep or scheduler is armed // against the live corpus. // // TIME IS FROZEN. `widgetSyncInputs()` stamps `now: Date.now()`, and two of the // fields it feeds (`scheduler.nextRunAt`, `scheduler.overdue`) are relative to // it, so a run an hour later would "move a number" that no code change touched. // The script overrides `now` with the constant below; everything else about the // inputs is what the route would see. // // THE TWO RUNS DELIBERATELY EXERCISE DIFFERENT CODE. Before the slice, the // `widgetActionable` and `cleanable` folds lived inline in their route files // (`api/widget/{actionable,cleanable}/route.ts`), which cannot be imported // offline; this script carried a copy. After the slice they are // `buildWidgetActionablePayload` / `buildCleanablePayload` in `common/views/`, // which is what it imports now. That is the point: an empty diff says the moved // builders agree with the route bodies they replaced, over the real corpus. // // Usage, from anywhere in the repo (TRANSCRIPTS_DIR only if the checkout has no // `transcripts/` of its own, e.g. a worktree): // TRANSCRIPTS_DIR=…/yt-dlp-transcript-browser/transcripts \ // pnpm --filter yt-dlp-transcript-common exec tsx ../plans/tools/phase3-view-numbers.ts import { getPaths } from "../../common/lib/paths"; import { buildCleanablePayload, type CleanablePayload, } from "../../common/views/cleanable"; import { buildWidgetActionablePayload, type WidgetActionablePayload, } from "../../common/views/widgetActionable"; import { buildWidgetSyncPayload, type WidgetSyncPayload, } from "../../common/views/widgetSync"; import { cleanableChannels } from "../../editor/app/cleanup/lib/loadCleanup"; import { loadActionableSummary, widgetActionableRows, } from "../../editor/app/lib/actionable/loadActionable"; import { widgetSyncInputs } from "../../editor/app/widget/lib/syncInputs"; // 2026-01-01T00:00:00Z. Any fixed instant does; what matters is that both runs // use the same one. Deliberately in the past, so "is some channel due now?" is // answered the same way on every machine and every day. const FROZEN_NOW = Date.UTC(2026, 0, 1); // Deterministic JSON: object keys sorted, arrays in their payload order (which // IS the thing under test for both widget payloads — the sort is part of the // contract the slice moves). function sortedJson(value: unknown): string { return JSON.stringify( value, (_key, v) => { if (v === null || typeof v !== "object" || Array.isArray(v)) return v; const out: Record = {}; for (const k of Object.keys(v as Record).sort()) { out[k] = (v as Record)[k]; } return out; }, 2, ); } async function main() { const paths = getPaths(); // /api/view/widgetActionable — the handler's own mapping, then the pure fold. const summary = await loadActionableSummary(paths); const widgetActionable: WidgetActionablePayload = buildWidgetActionablePayload(widgetActionableRows(summary.rows)); // /api/view/cleanable — the lean per-channel snapshot read, then the sum. const cleanable: CleanablePayload = buildCleanablePayload( await cleanableChannels(paths), ); // /api/view/widgetSync — the route's own inputs with the clock frozen. const widgetSync: WidgetSyncPayload = buildWidgetSyncPayload({ ...(await widgetSyncInputs()), now: FROZEN_NOW, }); process.stdout.write( sortedJson({ widgetActionable, cleanable, widgetSync }) + "\n", ); } await main();