Archilyzer · Source

archilyzer

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

commit 17db195a27e03e7315e39048d11bbffc71cadbe7
parent 42576a316d6907a3234d2ebedd8379a176697dd7
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  7 Sep 2026 15:42:49 -0400

plans: the one-core umbrella plan

The whole-app simplification plan agreed with the operator on 2026-09-07:
one core in `common/`, two products (Archive, Project), one hinge
(`ArchiveReader`), seven phases. Phase 0 — guardrails and dead weight — is
the slice this branch implements.

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

Diffstat:
Aplans/one-core.md | 384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 384 insertions(+), 0 deletions(-)

diff --git a/plans/one-core.md b/plans/one-core.md @@ -0,0 +1,384 @@ +# One core for Archilyzer — a holistic simplification plan + +## Context + +The workspace has grown since 2026-04 into seven packages (~200k source lines, 659 commits, +accelerating 40 → 233 commits/month) by building the boat while sailing it: each scheduler, +sweep, runner, derived feature, publish target and tool landed with its own module, page, +settings block, entry point and doc. Two design docs already diagnose halves of this and are +mostly shipped — `plans/unified-operations-model.md` ("four schedulers for one kind of work"; +steps 1–4 done, 5 half, 6 open) and `plans/editor-operations-ia.md` ("the backend found its +noun, the UI had not"; 8 of 9 slices shipped). This plan is the whole-app version of the same +question, and the answer is **yes**: there is one core underneath all of it, it is already +half-built, and the simplification is mostly *deleting the layers that predate it* rather +than writing a new one. + +Decisions taken with the operator (2026-09-07): + +- **Scope: sites AND projects.** umtool's project model and `scripts/report-to-video` become + project kinds in the core, consuming archives only through the published contract. + **Everything report-to-video lives under the umtool umbrella** — the scripts move to + `umtool/report-to-video/` (workspace package `umtool-report-to-video`) in Phase 0, and + umtool is the home of every project *kind*; the core holds only the project model. + umtool reads as **"universal media tool"** (the operator's backronym) — the one place + media-shaped work on the archive lives, whatever the project kind. Use that expansion in + `umtool/docs/README.md` and the root README's doc index. +- **Consolidate in place, step by step.** Every step deletes something and lands green. + On-disk formats, `settings.json` keys, live URLs and the `corpus.json` v3 contract are + frozen. Retired routes redirect, never 404 (the umtool rule both consoles already cite). +- **Daemon split: yes, as the last phase.** +- **All four pains are in scope**; the phases below are ordered by dependency, not taste. + +## What the survey found + +Five read-only surveys (common, editor, publish, tooling, docs/ops) on 2026-09-05. The +short version, file-anchored; the numbers only matter where they change what to do. + +**The core is real and already the organizing idea.** `common/lib/operations.ts` is a +registry of `OperationDescriptor`s (state per video, lane, cost, scope, trigger); +`common/jobs/autoQueuePolicy.ts` is a genuine HTB-style rule tree; `common/lib/corpus.ts` +defines the public contract once; `common/lib/site.ts` makes Site a first-class type; +`common/lib/paths.ts` is the single path/env override surface; `common/components/` (17k +lines) is the shared UI all three Next apps draw. That is the core. Everything else is what +grew around it before it existed. + +**What grew around it** (each is a self-described clone or a documented pre-core layer): + +1. **Dispatch, still four ways.** `controller/digestSweep.ts` (368), `backfillSweep.ts` + (517, header: "cloned from digestSweep.ts"), `autoRunner.ts` (1194), and the sync + heartbeat, beside `controller/arbiter.ts` (416) which is built, not persisted, and + refuses to run next to an armed sweep. `backfillBatch.ts` (937) says in its header it is + structurally `digestBatch.ts`. `arbiterWork.ts` says it is "SEPARATE from autoRunner's + buildChannelWork, and not a refactor of it". Four sweep planners (`lib/sweepPlan.ts`, + `controller/sweepPreview.ts`, `sweepRecency.ts`, `buildDigestSweepPlan`). +2. **Two work-classification systems in one snapshot.** `channelSnapshot.ts:70-180`: + `buckets` (~20 closed string-list fields, the pre-registry shape) beside + `backfill: Record<opId, entry>` (the registry shape, and "no longer the backfill lane" — + readers must use `backfillLaneEntriesOf()` or count 75,000 videos wrong). +3. **One contract writer, five hand-rolled readers.** Writer: `controller/buildIndex.ts` + + `bin/compose-site.ts`. Readers that each re-walk `corpus.json → manifest → slugToPage → + page-NNNN.json`: `mcp/src/source.ts` (1388, three transports, the best one), + `common/components/{transcript,subs,posts,digest}Cache.ts` (browser), + `export/app/lib/offlineCache.ts`, `scripts/report-to-video/cues.mjs` (270, zero shared + code), and umtool via report-to-video. `pageFileName` exists three times in + `lib/manifest.ts` (lines 40, 46, 98); version constants live in three files + (`corpus.ts`, `siteDescriptor.ts`, `manifest.ts`); `shipsPwa()` and the `_headers` CORS + block are duplicated with "keep in sync" comments. `mcp/src/search.ts` (1563) is a + server-side re-implementation of the ranking the viewer runs in + `common/components/searchPipeline.ts` (852). `mcp/package.json` does not even declare + its dependency on common (it resolves through the workspace by accident). +4. **The editor holds core logic and draws each noun several ways.** Read-model builders + live in the app, not the core: `jobs/active/buildActiveJobs.ts` (517), + `api/widget/sync/route.ts` (325 — the dashboard imports a builder *from a route file*), + `operations/lanes.ts` (368), `channels/[slug]/lib/channelFlow.ts` (582), + `stageStatus.ts` (586), `workers/buildWorkers.ts`. Three channel tables, a running job + drawn five ways (`JobsTable`, `RunningJobsList`, `LaneStrip`, `InFlightList`, + `MonitorWidget`), two vocabularies for the channel pipeline (`stages/*` cards vs + `flow/*` stations), the widget re-rendering the dashboard's payloads through a second + 1317-line renderer. 239 server actions in 40 modules; settings written from ≥6 files. + Seven 11-line API routes exist only to poll what a page already renders. +5. **The publish verb has seven entry points** (root `pnpm build`, export's + `build`/`build:nodata`/`build:hub` — the first two differ only by name —, four editor + actions, `docker/publish-site.sh`, `docker/build-site.sh`), all funnelling into + `editor/app/sites/lib/buildDeployCore.ts`, which lives in the *editor*, so the CLI and + docker paths cannot share it. `build:hub` has no operator entry point. +6. **Projects are a second system.** umtool (25k + 10k of `song/` scripts) has its own + project model, data root (`~/reports`), LMDB index, nav rule, FACTS file (`quirks.md`), + and hardcoded archive origin (`lib/archive.ts`); `umtool/lib/decisions.ts:23` calls + report-to-video "the second video pipeline in this repo". `corpus` means three things + (`common/lib/corpus.ts`, mcp's source handle, `umtool/lib/corpus.ts` = pitch histogram). +7. **Config and docs.** `settings.json` has ~90 leaf keys validated by 1,694 lines of + hand-written sanitizers (`lib/settings.ts`, the hottest code cluster in git: 146 touches + with its form and action); `settings.json.example` omits 26 of 28 live keys and carries + three dead ones; 68 app env vars plus 16 disjoint docker ones; `PARALLEL_TRANSCRIBE_LIMIT` + is documented and read nowhere. Ten top-level docs overlap pairwise (worktrees ×3, + container ×2, install ×2); `DEPLOY_DOCKER.md` is about building sites, not running them. + `common/bin/` is 58% one-off bake-off harnesses (3,828 of 6,545 lines). +8. **Layering exists only by convention.** `common/package.json` has no `exports`, so all + ~400 files are public API; `lib → controller` (8 imports), `lib → jobs` (6), + `jobs → controller` (2) and `controller ↔ ytdlp` are real back-edges. The one + architecture guard that exists (`controller/noCorpusWalkInRenderPaths.test.ts`) is a + grep, and it works — so the pattern is proven. + +**What is healthy and stays**: the on-disk corpus (`transcripts/channels/<slug>/data/<id>/` ++ sidecars), the LMDB index, the operation registry, the policy tree, the job registry with +distinct queue keys as the only concurrency mechanism, `paths.ts`, the shared component +library, the Playwright suite as the UI's test, the e2e queue lock, and the seven "what must +not be lost" invariants in `unified-operations-model.md` plus the naming hazards in +`plans/FACTS.md` ("Naming hazards"; `SUB_FILE_RE`; `summaries`; `band.ts` directive-free). + +## The target: one core, two products, one hinge + +``` + ┌──────────────── core (common/) ────────────────┐ + products │ model → corpus → operations → dispatch → publish │ views → ui + └──────────────────────────────────────────────────┘ + Archive: corpus on disk ──operations──▶ derived sidecars ──publish──▶ site (contract) + │ + ArchiveReader ◀──┘ (fs | http | hub) + Project: archives (via ArchiveReader only) ──project kind──▶ report → video / song + (kinds live in umtool/: umtool/report-to-video, umtool/song) + shells: editor (archive console) · viewer (export) · homepage · umtool (project console) + · mcp (archive over MCP) · archilyzer CLI · daemon (phase 6) +``` + +- **Two product nouns.** An **Archive** is a corpus turned into derived artifacts and + published as one or more Sites. A **Project** is a derived work (cited report, video, + song) built *from* archives. Projects and the MCP and the viewer read an archive the same + way: through the published contract. That hinge — one `ArchiveReader` — is what makes the + "sites and projects" core one thing rather than two repos sharing a folder. +- **Six core layers, dependency strictly downward, enforced by a test.** + `model` (types, on-disk formats, paths, sidecar declarations, settings schema; browser-safe) + → `corpus` (read side of a local corpus; enumerators typed by cost tier; snapshot) + → `operations` (the registry; `state()` and `run(unit)` per entry; engines stay in + `scripts/` behind `paths.ts`) → `dispatch` (job registry + policy tree + persisted arbiter; + *the* scheduler) → `publish` (index, compose, contract, ArchiveReader) → `views` (pure + read-model builders every UI draws) → `ui` (shared React). `projects` sits beside + `operations` and depends on `publish` only. +- **Every app is a shell.** It owns routing, forms and layout; it imports views and calls + core verbs. A core verb (`buildSite`, `runOperation`, `composeHub`) has exactly one + implementation, and the CLI, the editor action, the docker script and the e2e fixture all + call it. +- **One CLI, `archilyzer`.** Subcommands over the same verbs replace 22 bins, five + report-to-video bins, umtool's bin, four docker scripts and the pnpm-script twins. +- **Not a directory move.** `common/` keeps its name and its directories; the layers are + enforced by a dependency-direction test, and new code lands in three new directories + (`common/views/`, `common/lib/archive/`, `common/projects/`). Renaming the package to + `archilyzer-core` is a one-line sed left for the end, if wanted. + +## The phases + +Ordered by dependency: 1 unblocks 3, 2 unblocks 5, 3 and 4 make 6 small. Each phase is +several slices in the repo's existing cadence (a slice = one reviewable diff that deletes +something, with a `plans/` note when it ships). Sizes are slices, not days. + +### Phase 0 — Guardrails and dead weight (1 slice) + +Makes the later deletions safe and cheap. + +- **Dependency-direction test** `common/architecture.test.ts`, modelled on + `controller/noCorpusWalkInRenderPaths.test.ts`: `lib` may not import `controller` or + `jobs`; `jobs` may not import `controller`; `components` may not import `controller`, + `jobs` or `ytdlp`. Seed it with an allow-list of today's 16 back-edges so it passes, then + burn the list down (the `lib/settings.ts → jobs` re-export of `AutoQueueSettings` moves + the type into `lib`; `lib/operations.ts → controller` moves the two functions it needs). +- **`common/package.json` `exports`** as subpath patterns (`"./lib/*"`, `"./controller/*"`, + …) so deep imports keep working but the surface is declared; `mcp/package.json` declares + `yt-dlp-transcript-common: workspace:*`. +- **Delete**: root `sites/` (dead example), `common/bin/migrate-to-sites.ts` (one-shot, + done), `sites/jeralyzer/site.json.example`, the tail of `create-archives.sh`, the six + stale `export/.compose-cache/*.json` / `.r2-staging` trees from the working dir. Move + `bin/digest-bakeoff.ts`, `attribution-bakeoff.ts`, `attribution-pilot.ts` to + `plans/tools/` beside their results in `plans/bakeoff/`. +- **`git mv scripts/report-to-video umtool/report-to-video`.** A rename only: the + workspace entry in `pnpm-workspace.yaml`, the package name (`umtool-report-to-video`), + the root `test:scripts` glob, umtool's two path references (`lib/report/driver.mjs:14` + resolves `../scripts/report-to-video`; `bin/umtool.mjs:516` prints the literal command), + and the AGENTS.md / README clip sections. Behaviour, bins and exports are unchanged; + `pnpm test:scripts` proves it. +- **One `pageFileName`** in `lib/manifest.ts`; one `CONTRACT` object in `lib/corpus.ts` + holding every version constant (`CORPUS_SPEC_VERSION`, `SITE_DESCRIPTOR_VERSION`, the + four manifest versions, `SUMMARIES_PAGE_SIZE`, layer names) — the three files re-export + from it, nothing changes on the wire. + +### Phase 1 — Dispatch: one scheduler (4–5 slices) + +Finishes `unified-operations-model.md` steps 5 and 6 and deletes what they retire. This is +the phase with live numbers on a 78,000-video corpus; every slice is measured +before/after with offline `tsx` over the real corpus, never a second editor. + +1. **Persist the arbiter** (`.arbiter/state.json`, same shape as + `jobs/autoQueueState.ts`), and let `instrumentation.ts` resume it. Precondition named by + the design doc: the arbiter has run in production once — this slice is the run. +2. **Pause is node state** (step 5's remainder). `lib/pauseGates.ts` keeps `isGateHeld` / + `withGateHeld` as the only API; storage moves from four settings fields to `held` on the + tree root that dispatches the operation. The four fields stay readable for one release + as a migration read, then are deleted with their sanitizers. +3. **Retire `sweepEnabled` / `sweepKinds` / `sweepChannels` / `backfill.weight`** into tree + leaves carrying `operation`, `posture: "passive"`, `order` (the shape the design doc + already specifies). `lib/sweepPlan.ts` becomes the one planner; + `controller/sweepPreview.ts` and `sweepRecency.ts` fold into it (recency stays cached + through `controller/recencyIndex.ts`). +4. **Delete `digestSweep.ts`, `backfillSweep.ts`, and `autoRunner.ts`'s dispatch loop.** The + arbiter dispatches download and transcription too (they are catalogued operations + already — `EXTERNAL_OPERATIONS`). `arbiterWork.buildChannelWork` is the one projection; + autoRunner's copy goes. `digestBatch.ts` + `backfillBatch.ts` → one `operationBatch.ts` + parameterised by the descriptor (`inputs / outputs / applyResult` already exist per + kind). The GPU idle-only rule keys off `posture`, not off the presence of `laneFor` + (FACTS.md records that trap; the test that pins it moves with the rule). +5. **One work list in the snapshot.** Download and transcription gain `state()` in the + registry so `snapshot.backfill[op]` (typed as `operations`, disk key unchanged) covers + every operation. A slice-long assertion that `buckets.undownloaded ≡ operations.download.ids` + and `buckets.untranscribed ≡ operations.transcription.ids` on all 68 snapshots runs + before the bucket read path is deleted; the storage/cleanup buckets + (`wrongFormatAudio`, cleanup sizes) remain buckets — they are not operations. + +Deleted by the end: two sweeps, one runner loop, one batch, two planners, four settings +fields with their sanitizers and forms, `SweepLane`/`ArbiterBar`'s sweep-vs-arbiter +distinction on `/operations`. Kept verbatim: the seven "must not be lost" invariants. + +### Phase 2 — The contract: one `ArchiveReader` (3 slices) + +1. **`common/lib/archive/`**: `contract.ts` (re-exports the `CONTRACT` object from phase 0 + plus the walk as *code*: `manifestUrl(layer, slug)`, `shardUrl(...)`, `recordFor(id)`), + `reader.ts` (the `ArchiveReader` interface — `corpus()`, `manifest(layer, slug)`, + `shard(layer, slug, n)`, `record(layer, slug, id)` — and the HTTP transport, + browser-safe), `reader-fs.ts` (node), `reader-hub.ts` (federation). The implementation is + `mcp/src/source.ts` moved, caches included; mcp's `ShardSource` becomes a re-export. +2. **Every reader uses it.** `common/components/{transcript,subs,posts,digest}Cache.ts` + become thin memo wrappers over the HTTP reader; `export/app/lib/offlineCache.ts` keys its + cache off `contract.ts` paths (this also fixes the open `duplicates.json` offline gap); + `umtool/report-to-video/cues.mjs` imports the reader — the package declares + `yt-dlp-transcript-common` and its bins run under `tsx` (a shebang change; umtool's + driver invokes them by path today). `shipsPwa()`, the `_headers` block and + `HubSiteEntry` are defined once in `contract.ts` / `publish`. +3. **One search pipeline.** The ranking, windowing and snippet logic in + `mcp/src/search.ts` and `common/components/searchPipeline.ts` become one + `common/lib/search/` module with no React; `SearchSessionContext` and the MCP tools both + call it. `mcp/bench/bench.ts` is the regression gate. + +Deleted: four hand walks of the shard scheme, mcp's private reader, one of two search +pipelines. Verified against the live contract: `curl https://jeralyzer.pages.dev/corpus.json` +before and after, byte-identical output from `compose-site` on the fixture corpus. + +### Phase 3 — Views in the core, editor as shell (4 slices) + +1. **`common/views/`**: move `buildActiveJobs`, `buildWorkers`, `lanes.ts`, + `channelFlow.ts` + `stageStatus.ts`, the widget sync builder and `pulse` out of + `editor/app/**` into pure functions over (snapshot, registry, settings, pool). Each keeps + its unit tests; the architecture test forbids `views` from importing React or Next. +2. **One polling route.** `/api/view/[name]` serves any view; the seven thin routes become + rewrites to it (retired routes redirect). Worker protocol (`/api/worker/*`), the external + cron tick, file serving and the four `/api/test/*` routes stay. +3. **One drawing per noun.** Channel row: `channels/components/ChannelsTable.tsx` with a + column set replaces the dashboard's and `ChannelWorkTable`. Job in flight: the + `JobRowView` renderer from slice 8c with a `variant` replaces `RunningJobsList`, + `LaneStrip`, `InFlightList`. Channel pipeline: keep `flow/*` stations (registry-derived, + with the hand-listed bookend chores the IA doc protects) and delete the eight `stages/*` + cards' duplicated status logic, leaving each stage's *body*. Widget: `MonitorWidget` + composes the same components with `compact`; `widget/builder` keeps laying out those + components. `VideoPanel.tsx` (1586) splits into per-operation panels the registry + already enumerates (`videoOperationPanels.ts`). +4. **One settings writer and one schema.** Adopt one schema library (recommend `zod`; + veto if unwanted) for `settings.json`, `site.json`, channel `config.json` and every + sidecar. `lib/settings.ts`'s ten `sanitizeX` + eight `defaultX` + ~40 clamp constants + become one schema with defaults; `getSettings` / `writeSettings` stay the one + reader/writer. A single `saveSettingsBlock(block, patch)` server action replaces the + per-block writers spread over six files; forms stay where the IA put them. The sidecar + `X.ts` / `X-server.ts` pairs become one `sidecar(name, schema)` declaration each + (read, atomic write, mtime freshness, the `transcript.<x>.<y>` naming guard). Channel + `config.json`'s mutable sync state (`lastSyncedAt`, `lastFullSweepAt`, …) is declared + as such in the schema; splitting the file is *not* in scope (disk is frozen). + `settings.json.example` and the docs' key table are generated from the schema. + +Deleted: six payload builders from the app, seven API routes, two channel tables, three +job renderers, eight stage status derivations, ~1,400 lines of hand sanitizers. + +### Phase 4 — CLI, entry points, config, docs (3 slices) + +1. **`buildSite` moves to the core.** `editor/app/sites/lib/buildDeployCore.ts` → + `common/publish/build.ts` (`buildSite(id, opts)`, `deploySite`, `buildAll(mode)`, + `composeHub`, `composeHomepage`). The editor actions call it as jobs; `docker/build-site.sh` + and `publish-site.sh` call the CLI; `build` / `build:nodata` twins become one script with + `--nodata`; `build:hub` gets a CLI and an editor entry (it has neither). +2. **`common/bin/archilyzer.ts`**: `index`, `compose site|hub|homepage`, `build site <id>|all`, + `deploy site <id>`, `sync tick`, `run <operation> <channel> [ids…]`, `mcp`, `settings + example`, `doctor` (the env/binary/port checks now scattered across `paths.ts`, umtool's + doctor and `worktree.mjs`). Existing bins become one-line subcommands and are removed + from `package.json` scripts once nothing references them. Port defaults exist once + (`scripts/worktree.mjs` today says it "mirrors" three other places). +3. **Config and docs.** Env vars: `paths.ts` stays the one override surface; the fifteen + test-only vars get an `E2E_` prefix and live in `playwright.config.ts`; the docker + `ARCHILYZER_*` set stays separate (different process) but is listed in the same + generated table; `PARALLEL_TRANSCRIBE_LIMIT` is removed from the docs. Docs by audience: + `README.md` (use an archive), `SETUP.md` (run your own — absorbs `SCHEDULED_SYNC.md` and + `WORKTREES.md`), `PUBLISH.md` (absorbs `DEPLOY_DOCKER.md` and `DEPLOY_CLOUDFLARE.md`), + `RUNNING_IN_DOCKER.md`, `CONTRIBUTING.md`, `AGENTS.md`; stale claims (whisper as the + only engine, transcode as a stage) corrected; `PLAN.md`'s phase table becomes a pointer + to `STATE.md` so status has one owner. + +### Phase 5 — Projects join the core (3 slices) + +1. **`common/projects/`** holds only the *model*: `Project` (`kind`, `dir`, manifest, + state, open decisions, build) and a `ProjectKind` registry mirroring + `OperationDescriptor` (`id`, `scaffold`, `build`, `verify`, `surfaces`). umtool's + `lib/projects/*` (report, scaffold, decisions, folders, the LMDB project index with its + "if a value exists only in the index, that is a bug" rule) moves here; + `umtool/lib/paths.mjs` roots (`REPORTS_ROOT`, `SONG_REPORTS`) become `paths.ts` entries. +2. **Two kinds registered, both from umtool's tree.** `report-video` is + `umtool/report-to-video/` (moved in Phase 0; now reading cues through the phase-2 reader + and taking `siteOrigin` from the project manifest — `umtool/lib/archive.ts`'s hardcoded + origin and channel go). `song` is `umtool/song/`, a kind whose scripts are invoked by + the registry, not by relative path. `umtool/lib/corpus.ts` is renamed `pitch.ts`. The + core never imports a kind; umtool registers its kinds at boot, the way the editor's + `instrumentation.ts` arms runners. +3. **umtool becomes the projects console**: a shell over `common/projects` and + `common/views`, on the same `ui` components and the same nav rule; its `doctor` folds into + the CLI's. The `.ts`/`.mjs` twin files that exist only so `bin/umtool.mjs` can run under + plain node go away when that bin becomes `archilyzer project …` under `tsx`. The editor's + one cross-app coupling (`UMTOOL_URL` on the video page) becomes a project link the + registry knows how to make. + +Deleted: a second project model, a second doctor, a second nav rule, a second FACTS file's +worth of quirks that stop being true, the hardcoded archive origin, ~20 twin files. + +### Phase 6 — The daemon (3 slices, last) + +1. **A `Core` interface** with one in-process implementation: everything + `editor/instrumentation.ts` arms (reaper, heartbeat, pause restore, arbiter) plus the + verbs the server actions call (start/stop/pause, run operation, build site). No + behaviour change; the actions now go through the interface. +2. **`archilyzer serve`** hosts that implementation: the worker protocol routes, the + external tick, `/api/view/*` and a small control API over HTTP/SSE. `instrumentation.ts` + becomes "connect to `CORE_URL`, else boot in-process" so `pnpm dev:editor` is unchanged. +3. **Consequences cashed in.** `playwright.config.ts` runs one daemon per worker and + `fullyParallel: true` (the four `globalThis` singletons were the stated reason it could + not); "do not boot a second editor against the real corpus" becomes "one daemon per + corpus", which the daemon enforces with a lock file; the docker `editor` service runs the + daemon and the console as two processes of one image; `ARCHILYZER_IDLE_BOOT` is a daemon + flag. The e2e queue lock stays for the shared-port case. + +## What must not be lost + +Read before any slice; each was paid for once. `plans/unified-operations-model.md` "What +must not be lost" (reachable ≠ needs-media; a zero limit is a hold; re-derive from disk +every pull; one job per channel; the `never` default on `candidateAction`; distinct queue +keys are the only concurrency mechanism; derived sidecars are not disk-gated). +`plans/FACTS.md` "Naming hazards" (`SUB_FILE_RE` claims `transcript.<x>.<y>`; `summaries` is +the metadata index; `report` means three things), `band.ts` carries no directive and no +value import, `laneFor` presence is load-bearing until phase 1 step 4 changes the rule, and +the AGENTS.md container facts (glibc only goes forward, `PARAKEET_GGML_VULKAN`, the +`vulkan:` boot line). Directory name ≠ metadata id on ~14.5% of the corpus. + +## Verification + +Per slice, in this order; nothing ships on a paraphrase of a number. + +- `pnpm -r exec tsc --noEmit` and `pnpm --filter yt-dlp-transcript-common test` (the + architecture test included). `pnpm test:scripts` for report-to-video (glob repointed at + `umtool/report-to-video/*.test.mjs` in Phase 0); `pnpm --filter yt-dlp-transcript-mcp + test` and `bench` for phase 2. +- **Numbers do not move.** For phases 1 and 3: an offline `tsx` script over the real corpus + dumps every `ChannelSnapshot` count, every band figure and every view payload before and + after; the diff must be empty or explained in the slice's plan note. Never a second editor + against `transcripts/`. +- **The contract does not move.** For phases 0, 2 and 4: `compose-site` over + `editor/test-transcripts/` before and after is byte-identical; `curl` of the live + `corpus.json` and `llms.txt` still parses through the new `contract.ts`; the MCP smoke + bench passes against `TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev`. +- **e2e**, detached behind the queue lock (~24 min; use `setsid nohup` and a log monitor, + not a foreground wait). Retired routes are asserted to 307 in `nav.test.ts` / + `next.config.ts`; the 21–22 known-failing base specs in FACTS.md are not chased. +- **Run the thing.** Phase 1: the arbiter dispatches one real unit per lane on the corpus + and survives a restart. Phase 4: `archilyzer build site jeralyzer` produces the same + `out/` as the editor's Build button. Phase 5: `archilyzer project build` regenerates an + existing report video's manifest identically. Phase 6: two editors against one daemon, + and `e2e` at `workers: 4`. + +## How to run this + +Keep the repo's cadence: one plan file per slice under `plans/` (this document is the +umbrella), each slice committed with its measurements in the commit body, `STATE.md` +updated before context is cleared. The first concrete slice to write is **Phase 0** — it +is one session, deletes only dead weight, and its architecture test is what makes every +later deletion cheap to verify.