Archilyzer · Source

archilyzer

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

commit 688894977476c3b78f8a31837fc74cb026772873
parent f40684a54269b56b0881a9b29830e88e3adc3cb8
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 23 Sep 2026 19:35:45 -0400

plans: slice 2 record, the view-numbers tool, two comments that named deleted routes

`plans/tools/phase3-view-numbers.ts` dumps the three pure-loader view payloads
(widgetActionable, cleanable, widgetSync) as sorted-key JSON, offline and
read-only, with `now` frozen. Before (main) vs after (this branch) over the real
corpus: empty diff.

`plans/one-core-phase-3.md` gains "Slice 2, as shipped": shas, gate counts
(common 1638, editor unit 61, e2e subset 174/174), line counts, and every
deviation. `PipelineBand.tsx` and `widget/lib/syncInputs.ts` now say their old
path is rewritten to /api/view/….

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

Diffstat:
Meditor/app/components/dashboard/PipelineBand.tsx | 2+-
Meditor/app/widget/lib/syncInputs.ts | 3++-
Mplans/one-core-phase-3.md | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplans/tools/phase3-view-numbers.ts | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 199 insertions(+), 2 deletions(-)

diff --git a/editor/app/components/dashboard/PipelineBand.tsx b/editor/app/components/dashboard/PipelineBand.tsx @@ -137,7 +137,7 @@ export function PipelineBand({ {jobList.length > 0 && jobs ? ( // The same one list /jobs draws, with no tail: the band is the head. - // The cockpit above already polls /api/jobs/active + // The cockpit above already polls /api/jobs/active (rewritten to /api/view/activeJobs) // (DashboardCockpit.tsx) and this component polls it again while // anything is live — the double poll predates this change and is out of // scope here. diff --git a/editor/app/widget/lib/syncInputs.ts b/editor/app/widget/lib/syncInputs.ts @@ -10,7 +10,8 @@ import { getChannelBriefs } from "../../lib/requestCache"; // four values. They are gathered here rather than in the route because BOTH the // route and the dashboard's SSR seed need them, and a builder that lived in a // route file was the smell that made `app/page.tsx` import out of -// `api/widget/sync/route.ts` to render a page. +// `api/widget/sync/route.ts` to render a page. (That route is gone; /api/widget/sync +// is rewritten to /api/view/widgetSync, whose handler calls this.) // // `getChannelBriefs` is the per-request memo (lib/requestCache.ts): the // dashboard derives the channel list three ways in one render, and this is one diff --git a/plans/one-core-phase-3.md b/plans/one-core-phase-3.md @@ -477,3 +477,81 @@ claimed fixed; the reads are in-memory). > `buildAutoQueueStatusPayload` (`editor/app/operations/status.ts`) reads configs and state > ONCE via `Promise.all` and hands the same pair to all four lanes. `getAutoRunnerStatus` is > still called per lane and is genuinely in-memory. + +## Slice 2, as shipped — one polling route (2026-09-23) + +Branch `one-core/phase-3-s2` off `54cf1b31`, four commits, not merged: + +| commit | what | +|---|---| +| `5397f83c` | `common/views/{cleanable,widgetActionable}.ts` + tests; the two widget routes call them; seven type importers repointed off `api/widget/*/route` | +| `074f09a2` | `common/views/names.ts` (`VIEW_NAMES`, `ViewName`, `VIEW_CONTRACT`) + test; `editor/app/api/view/{[name]/route.ts,views.ts,pulseView.ts,views.test.ts}`; eight route files deleted; eight `rewrites()`; `e2e/view-route.spec.ts` | +| `edf56370` | `usePolledPayload` `git mv` to `editor/app/lib/`; JobsTable, useOperationsStatus, SyncConsole folded onto it; every client poll URL is `/api/view/<name>` | +| (this commit) | `plans/tools/phase3-view-numbers.ts`, two stale comments, this record | + +**One route, eight rewrites, no redirect.** `/api/view/[name]` is `force-dynamic`, checks +the name against `VIEW_NAMES` before any handler runs (unknown ⇒ 404), and dispatches into +a total `Record<ViewName, handler>`, so a name with no handler is a tsc error. It has no +auth and no `EDITOR_TEST_ROUTES` guard, same as the eight routes it replaces. Each handler +constructs its own inputs exactly as its old route did; there is no shared constructor in +the dispatcher. Pulse is the one `observe` view; `views.test.ts` reads `pulseView.ts` as +text, requires `observeInputs(` and bans `liveInputs(`, `getRegistry(`, `getSettings(`, +`getWorkerPool(`. `/api/widget/presets` stays its own route. Heal/no-heal unchanged. + +Gates: + +| gate | result | +|---|---| +| `pnpm -r exec tsc --noEmit` | clean after every commit | +| common tests | **1638/1638** (was 1625; +13: cleanable 3, widgetActionable 6, names 4) | +| editor unit (`tsx --test "app/**/*.test.ts"`) | **61/61** (was 59; +2: pulse textual guard, cleanable assignability) | +| `pnpm run test:scripts` | 156 pass / 1 skip / 0 fail | +| `next build` (editor) | clean; `├ ƒ /api/view/[name]` (dynamic), `├ ƒ /api/widget/presets` kept | +| e2e subset, 20 specs (`pulse`, `auto-refresh`, `dashboard`, `dashboard-paths`, `widget`, `jobs`, `jobs-active-order`, `jobs-channel`, `workers`, `worker-remote`, `auto-queue`, `lane-runner`, `scheduler`, `ops-api`, `disk-space`, `perf-budget`, `backfill`, `channel-storage`, `channel-rename`, `view-route`) | **174 passed, 0 failed, exit 0**, 8.0 min, one run, nothing re-run | + +**Numbers.** `plans/tools/phase3-view-numbers.ts` (offline tsx, read-only loaders, no +server) dumps `widgetActionable`, `cleanable` and `widgetSync` as sorted-key JSON over the +real corpus (`TRANSCRIPTS_DIR` → the primary checkout's `transcripts/`), with `now` frozen +at 2026-01-01Z so `widgetSync.scheduler.{nextRunAt,overdue}` cannot drift. Before (at +main, route bodies copied into a temporary variant of the script, since the views did not +exist yet) vs after (the committed script, importing the views): **diff empty**, 4,574 +bytes each. + +Line counts: the eight deleted routes were 190 lines; their replacement is 185 (route 41, +`views.ts` 87, `pulseView.ts` 57) + a 66-line test. `common/views/` gains 144 non-test +lines (`names` 55, `cleanable` 42, `widgetActionable` 47) and 129 test lines. `editor/app` +over commits 1–3, `--no-renames`: +421 / −345. + +Deviations: + +1. **`e2e/auto-refresh.spec.ts`: one line changed.** It counts the BROWSER's own requests by + `pathname === "/api/pulse"`; the client now sends `/api/view/pulse`, so it would count 0. + It matches `/api/view/pulse` now. No assertion that calls an old path was edited — those + are the rewrite's regression test. +2. **The cleanable assignability check lives in the editor** (`api/view/views.test.ts`, + `[A] extends [B]`): `CleanableChannelRow` is an editor type a common test cannot import. + The view's row type keeps the wire name `CleanableChannel` (MonitorWidget imports it). +3. **The hook gained `{ immediate?: boolean }`** (default true) and an unmount guard on + `refetch`. useOperationsStatus and SyncConsole are SSR-seeded and never fetched on + mount; they pass `immediate: false`. Both carried the unmount guard by hand. +4. **Behaviour change: `setInterval` → serial polling** for those two. The hook schedules + the next tick only after the previous fetch settles, so a slow response can no longer + stack requests. Cadences unchanged (3 s, 5 s). +5. **JobsTable's `enabled` is `polling` state**, seeded from the props and adjusted during + render with `if (polling !== anyLive) setPolling(anyLive)` — React's documented + "adjust state when a prop changes" form. It is needed because the poll's result feeds + the rows `anyLive` derives from, so the hook must be called first. It cannot loop: + `setPolling` only toggles the hook's timer effect and does not change `polled` in the + same render, so the immediate re-render computes the same `anyLive`, the guard is false, + and it settles after one extra render; new `polled` data only arrives from a completed + fetch. "An idle page makes no requests", freshest-snapshot-wins, `mergeJobRows` and the + never-nulled `data` are unchanged. +6. **Two comments named deleted routes**: `PipelineBand.tsx:140` and the + `widget/lib/syncInputs.ts` header now say the old path is rewritten to `/api/view/…`. +7. **Commit trailers** on commits 2–3 were rewritten with `filter-branch` after e2e exited + (they had picked up the wrong model line); the shas above are post-rewrite. Code + unchanged by the rewrite. + +Not done here, by design: no batch route, no auth on views, `operations/status.ts`, +`requestCache.ts` and `liveInputs.ts` untouched. The full editor suite was not run; only +the 20-spec subset above. diff --git a/plans/tools/phase3-view-numbers.ts b/plans/tools/phase3-view-numbers.ts @@ -0,0 +1,118 @@ +#!/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 { + actionableDigestReachableCount, + actionableUndownloadedCount, + actionableUntranscribedCount, + loadActionableSummary, +} 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<string, unknown> = {}; + for (const k of Object.keys(v as Record<string, unknown>).sort()) { + out[k] = (v as Record<string, unknown>)[k]; + } + return out; + }, + 2, + ); +} + +async function main() { + const paths = getPaths(); + + // /api/view/widgetActionable — the shell's map through the four count + // helpers, then the pure fold. + const summary = await loadActionableSummary(paths); + const widgetActionable: WidgetActionablePayload = + buildWidgetActionablePayload( + summary.rows.map((row) => ({ + slug: row.channel.slug, + undownloaded: actionableUndownloadedCount(row), + untranscribed: actionableUntranscribedCount(row), + digestReachable: actionableDigestReachableCount(row), + })), + ); + + // /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();