# Running state The working memory for the local-AI derived-corpus work. Rewritten at the end of every session, before context is cleared. See [`README.md`](README.md) for the protocol. **Now (2026-10-09): release 18 is complete on `r18/integration`, and `main` is merged into it (`4cffda3f`, 94 commits ahead of `main` `e921f82f`, 0 behind: a fast-forward).** The merge brought main's ~70 commits since 2026-10-06 (umtool articles + notes, `ops transcribe` with word timings, fetch-windows, metadata refresh, channel create/rename/delete over ops; record: [`landed-2026-10.md`](landed-2026-10.md)); the changelogs keep both sides, r18 first. Nothing of release 18 is live. - **Owed, in order:** fast-forward `main` to `r18/integration` from a session that is not worktree-isolated (the primary clean first); the rollout per `~/reports/release-18/RUNBOOK.html` (editor rebuild + restart, Publish now, jeralyzer preview, hub tombstones, step 5b `r18-eva-sites.sh` + production deploys of hasanalyzer and bonnellyzer, policies, the lane on; the script guard sha updated to the merge); then pruning — the five `r18-*` slice worktrees and their branches, `render-local-sources`; the ~170 merged branches and the stale worktrees (`diet-series`, `export-gumroad-tip`, `r13-phase-5`) are listed for the operator to approve, never deleted unasked. - **The five working sessions** (Super Hasanalyzer, Super Jeralyzer, Super Jasolyzer, candace owens corpus analysis, unified media aggregation) are doing corpus and content work and hold no unmerged code. The tooling gaps they reported are releases 19–20. - **Releases 19–20 are in flight on three tracks** (index: `PLAN.md`, "Beyond the AI track: releases"): Track A [`release-19.md`](release-19.md) A1–A9 (agents run the archive: ops, CLI, MCP) then [`release-20.md`](release-20.md) (data model); Track B release 19 B1–B6 (machine safety and tooling); Track C docs and plans (OPERATING.md and `archilyzer docs cli`, doc fixes, homepage docs, FACTS). Integration branch `r19/integration`, branched from `4cffda3f`. - **The `en` → 0 cues caption bug (filed 2026-10-01) is CLOSED** by `200a3105` (en-orig first, empty tracks fall through, cue-block VTTs parse, a one-shot index re-read); verified against the tree and an index fixture by release 20 D3 (2026-10-10; FACTS, "The caption-track rule"). Left open: the stats cache does not see that pass's re-reads. - **Release 20 D2 (Twitch ids) is HELD for an operator ruling**: built and kept on `r20/twitch-ids`, reverted on `r20/d2-r20`; the two options are in `release-20.md` ("Slice D2 — held"). **Previously (2026-10-06): release 18 — publishing as queueable stages — is complete on `r18/integration`** (record: [`release-18.md`](release-18.md): slices S1 the stage contract, stamps, lock, bundles and CLI; S2 deploy hardening; S3 the status, the queue and the publish lane; S4 the surfaces; S5 the image half; S6 the records — each reviewed, merged `--no-ff`; `main` `edadc712` merged in first). **Not on `main` yet, and nothing of it is live.** - **Owed, in order** (the plan's steps 5–6): fast-forward `main` to `r18/integration` from a session that is not worktree-isolated (an empty `status --short` in the primary first; if `main` moved, merge it into the integration branch and re-gate instead); remove the idle `r18-*` worktrees (keep `r18-integration` until the rollout is done); then the rollout: ONE editor rebuild + restart (adapt `~/reports/release-17/scripts/r17-*.sh`), Publish now with every policy off (= the index update alone; `/` answers under 5 s throughout), a jeralyzer build → preview deploy → live-check verdict, the hub with its tombstone probes (open question 1 for real), production deploys site by site, then policies (`build` for all six; `preview` where wanted; `production` for none until a week of lane runs reads clean) and the lane on (`checkEveryMinutes` 10, `refreshEveryMinutes` 360); `archilyzer doctor`; build `runtime-vulkan` and `runtime-cuda` once. - **The Eva tags leak (found 2026-10-06):** `eva-topic` is published on hasanalyzer (1, hasanabi) and bonnellyzer (3, omnibased) — the three Eva rules fire on every channel but Eva's own. Ruled: they exist on Anilyzer only (and a future VTuber site). Fixed in code on `tags/site-scope` (a tag's `sites`, merged here); the DATA change is owed once the new code is live: `~/reports/release-18/scripts/r18-eva-sites.sh` (scopes the three to anilyzer, updates the index, rebuilds the three sites), then production deploys of hasanalyzer and bonnellyzer. - **What changes for the operator:** /sites → **Publish** replaces "Build all sites" and the hub/homepage sections (Publish now, Build all stale, a row per target with index | built | deployed | live chips); **Build stats dataset is gone** (the index update builds the stats); `/operations/publish` is the lane; a site's **Publish policy** is on its form; `pnpm ops publish {"verb": …}`; `archilyzer publish …` (PUBLISH.md). Every stage is a child process under one publish lock, so a build no longer starves the editor. **Previously (2026-10-02, 03:20): `main` is `a6155bf2` (+ plans commits) — release 17, the media tier, is complete and LIVE: the editor on `:3001` (`Dt4zuo5uJcPKZ9fKk0L-f`, restarted 02:51 after the migration) and umtool on `:3050` (02:40, `UMTOOL_MEDIA_DIR` set to a folder on the Platter).** Record: [`release-17.md`](release-17.md) (slices D0, T1, T2, T3, U1, U2, XP, RL, each with its review; the Rollout section). The operator's runbook is `~/reports/release-17/RUNBOOK.html`; its scripts are `~/reports/release-17/scripts/`. - **The migration ran (02:40–02:51, editor stopped):** ten channels moved to the media tier (realcandaceo, omnimirror, leaflit-rumble, cornbreadman, shondo-vods, friday-night-tights, piratesoftware, kirsche, nuxanor-kick, HasanAbiVODs3). **12:19–12:34 (2026-10-02, editor stopped): the big three too** — rekietalaw, the-quartering-rumble and omnibased migrated in 860 s; nothing on the retired layout remains; `/home` 77 GB → 43 GB free. The auto-subs purge (channel page → Cleanup → "Superseded auto-captions", shown only when the bucket is non-empty) applies to 24 HasanAbiVODs3 videos and none of omnibased: the plan's 7.6 GB counted every `en-orig.vtt`, most of which are their video's only transcript and are never purged. `--reclaim` (the platter's text copies) later. - **The saved-video store move (209 GB → the Platter) is DEFERRED:** started 11:45 offline through `relocateSavedVideos`, stopped at 6.8 GB because the platter wrote at 2 MB/s (a 1 GiB `dd` on the idle drive: 4.7 MB/s; reads 55 MB/s). The operator cleared the marker (12:58); the store is "In place" again and its writers are free. Move it again from the Storage card when the drive writes at a sane rate; the 7.4 GB partial copy under `/saved-videos` is reused. The operator floated "text on the platter as a per-channel option" as a release valve for SSD space — not built; a candidate slice. `transcripts/channels` is a paused Syncthing folder: unpausing now syncs the migrated text to the peers. - **Live proof:** a migrated video page in 0.2 s, its audio and transcript stream through the links (206), the dashboard answered in 1.4–2.1 s during three simultaneous report regenerations (D0), `/storage` says "legacy (3 to migrate)", umtool's `move-out --all` moved ten `out/` trees (12.3 GB) to the Platter. - **Owed to the operator (release 17):** X posts private — create the private site (Sites → New, Audience: Private, every channel), set "Where X posts appear: Private" on `/settings`, rebuild + deploy hasanalyzer, anilyzer and jeralyzer, REBUILD + DEPLOY THE HUB (it serves X posts today), decide on old Cloudflare deployments, re-register the MCP against the private build; the saved-video store move when the platter writes properly; watch the download lane page (YouTube's backoff was at 6 failures at 03:18, no hold; "Clear hold" is the way out of a hold). - **Rulings this release:** an implementer's commits carry its own model's `Co-Authored-By` (never rewritten to another model's); never write the refused identifier suffix as text in a record; never run a whole e2e suite while other agents work (an OOM kill at 22:41 took the desktop session and a transcriber). **Previously (2026-10-01, 00:10): `main` is `271db070` — releases 14, 15 and 16 (CK the "Search in" row, DX the setup on the homepage's AI and MCP doc), release 13's three slices that had never landed (W3 the build image, W1 the editor lows, W2 the export/docs lows, each brought to `main` by a reviewed merge), and the report-video on-screen deck from the parallel session (`plans/onscreen-deck.md`).** Records: [`release-16.md`](release-16.md), [`release-13.md`](release-13.md) (its Rollout is now written), [`release-14.md`](release-14.md), [`release-15.md`](release-15.md). The operator's runbook is `~/reports/release-15/RUNBOOK.html` (its banner carries the current two lines). - **Live (2026-10-01, 01:31) — everything:** the editor on `:3001` (`3Wc3BdFQWKuLx9PbNZvmB`), umtool on `:3050` (`LRRG_c7H8GRM6ZpHbqvt0`, the deck), the index and stats, the homepage from `2cf43a69` (mirror `6b34c2aa`, 2,029 commits; 20/20), the hub (6 sites) and the six sites (Search in, the `@Archilyzer` link, the Use-with-AI link to the homepage doc, the sixth chart colour) — the operator ran `r15-umtool-restart.sh` and `r15-deploy-all.sh` at 00:26–01:31. `realcandaceo` is still held by its own interrupted media move (the operator's Resume). - **Release 16 is complete and LIVE on `:3001` (2026-10-01 12:54, `Xfavvt2XajdNzOvvn0NtL`):** FK (`a44dc38d`, forms keep what was typed), RM (`ae23a695`, a move refuses over a running job, holds the channel's writers, mirrors its copy; Reconcile and resume), XL (`a07bfa21`, the Connect window is the operator's own browser without automation signals; the fetchers use the operator's Firefox login via `social.x.cookieSource`). The posts deck (`458f4fcd`) is live on umtool (restarted 11:13). `realcandaceo`'s move completed 2026-10-01 (five stale scratch files removed from the destination copy by hand, then Resume) — the case RM now handles itself. - **Owed:** the build-only Build all through the rebuilt build image, when the machine is idle; the operator's by-hand check of the X section (Check, then Connect or the browser login). Follow-up filed: `export-search.spec`'s "Deleted" checkbox locator matches a result card's "Select … for AI" box by substring (`exact: true`). - **Settings changed by hand through `writeSettings` (2026-09-30):** the X social link is `x.com/Archilyzer`; `storage.health.budgetMs` 20000 and `probeTimeoutMs` 10000 (the 3 s default held six Platter channels while renders churned the disk — the operator's ruling was to tune, not to change the detector). - **Rules changed:** the homepage build gate is `build:nodata`, never `run build` (its `prebuild` indexes whatever `transcripts/` is visible — a three-minute stray index build against the real corpus happened during W2's catch-up; identical code, two records, stopped). - **Ruled and built (2026-09-30):** the source's history under `/source/git/` by stagit (SG; Sendforge was out: JS for history, no sub-path); the drive-health timings as `settings.storage.health` (DT). - **Follow-ups the reviews named:** the editor's and export's build traces list dot-directories (cosmetic while `standalone` is off); `REQUIRED_TOKENS` lacks `--chart-other` (the e2e pins it); `export`'s `WorkspaceView` `splitOn` has a one-paint flash from a localStorage restore; "Load more results" never resumes a leaf that settled at its cap (`runQueryTree`'s `setHitLimit` reaches running leaves only; on `main` too — release 16 CK re-review R-I1; wants a spec). - `/changelog/` overflows a 390 px phone: long inline code in a released entry (`export/CHANGELOG.md:103`, the 133-character `export/app/ask/{useAskChat,…}` list) cannot wrap. Wrap `` in the changelog renderer, then drop the `test.fail` in `export/e2e/responsive.spec.ts` (release 16 DX). - The JSX entity/whitespace hazard sweep: 22 texts in 19 files run a word into the element before them (FACTS, "Use with AI is the homepage's AI and MCP doc"; release 16 DX). **Previously (2026-09-28, night): the stats cache key fix — built, reviewed (SHIP AFTER FIXES, then SHIP on re-review; every touch-up done), not merged.** (Merged `10cefd15` the same evening; the recount rolled out with release 15, the homepage at 77,547 transcripts on 2026-09-30.) The branch is `fix/stats-cache-key`, and [`stats-cache-key.md`](stats-cache-key.md) holds the record, the review and the rollout. FACTS has "The stats cache key". - **What it fixes:** the homepage showed Jasolyzer as 0 transcripts, 0 channels, 0 hours while it served 1,889 videos. Instance-wide it showed 49,798 transcripts of about 77,000. - **The cause:** `statsByPath` was keyed on the metadata mtime alone. It is now keyed on the index's own record as well, and a transcript always has a date. - **Added by the review:** - a guard against clearing a newer cache (`ARCHILYZER_STATS_ALLOW_DOWNGRADE`); - an unmounted drive's stats are kept, and a cache clear with one refuses; - "not indexed yet" and "not indexable" are counted apart. - **Owed after the merge:** the rollout in the record, in its order. First, rebuild and restart :3001 (until then, never press "Build stats dataset"). Then index, then one full stats pass of 10–30 min, then the homepage, the hub and the sites. The homepage deploy waits on release 12's step 0: it runs the source publish. - **Merge note:** `homepage/social-visible` merged `main` (`10cefd15`) at `4d11542c`; the one conflict, `homepage/CHANGELOG.md`'s `[Unreleased]`, kept both sides. - **CLOSED by release 15 slice IG (branch `r15/index-hold`, [`release-15.md`](release-15.md)): the index build no longer treats an unmounted drive as an empty channel.** It holds the channel: not rescanned, its index records and shared pages kept, and the `Diff:` line says ` Held: N channel(s), K video(s) kept.` A full rebuild with a channel held refuses unless `ARCHILYZER_INDEX_ALLOW_HELD=1`. FACTS has "The index build's hold". Until that branch is merged and rolled out, the rollout's step 3 `Diff:` check stays the safeguard: thousands removed means a drive was missing. **Previously (2026-09-28, evening): release 12 — the source mirror — is merged to `main` and NOT rolled out.** [`release-12.md`](release-12.md) holds Q's and R's records, their reviews, "Merged" and "Rollout". The operator's runbook is `~/reports/release-12/RUNBOOK.html`, with its scripts in `~/reports/release-12/scripts/`. The plan is [`source-mirror.md`](source-mirror.md). - **What merged:** - **Q** (`4855f70b`, and the changelog fix `e6c5d2e3`) fixes the hardcoded umtool paths. - **R** (`ffdeb2cd`) adds `archilyzer source publish`: - a fresh bare clone of `main` is rewritten by git-filter-repo with the operator's scrub rules, then repacked for git's dumb HTTP; - an audit gate refuses a denied literal anywhere; - the raw tree at `/source/tree/` and the tarball are published beside the mirror, and the `/source/` page shows them; - a refusal withdraws the source from `public/` and `out/`; - `deployHomepage` refuses any `out/` it cannot vouch for. - **The operator's side is prepared:** - `~/.config/archilyzer/` holds a scrub file (3 rules) and a denylist (3 literals: the user name, the host name, an email address). NEVER print them; - `git-filter-repo` 2.47.0 is installed with pipx; - `source publish --check` exits 0. With `main` at `ffdeb2cd` it would publish `8188e02a7d04`: 2,473 files, 69.8 MB. The parent's first check, on `e56101fdee5d`, gave `20c367613f75`. - **Owed, in order** (release-12.md "Rollout"): 0. The operator completes the denylist (real name, handles), then runs `source publish --check`. **No deploy of any kind before this: a Pages preview is public and permanent until it is deleted.** 1. The song link, before any umtool restart. 2. Rebuild and restart :3001 with `~/.local/bin` on PATH. It runs 0.10.0 (`BUILD_ID` `vWCJb87ktCy5akih_pM9X`), which has no source step, no withdrawal and no deploy check. 3. A FRESH `archilyzer build homepage` in the primary. The current `out/` predates `/source`, so the deploy check refuses it. 4. The preview `--preview source` and its live checks. 5. Production. 6–7. What to do if the edge refuses the clone, and if anything private ever ships: delete that deployment. 8. The cut (`release cut editor …`: 2 bullets; export has none) and the worktrees. - **Baselines now:** common **2,149**, homepage unit **7**, homepage e2e **36**; editor unit 85, `test:scripts` 185 + 1 and mcp 269 are unchanged. **Previously (2026-09-28, afternoon): release 11 is LIVE as 0.10.0, and Jasolyzer is launched.** The rollout ran 12:11–14:05 ([`release-11.md`](release-11.md), "Rollout, as done", every deploy and job id): the cut (editor `24c8352e`, export `bf6904e8`), ONE :3001 restart (`BUILD_ID` `vWCJb87ktCy5akih_pM9X`, on `main` `e6c5d2e3`), a Settings save (two ran; the second byte-identical — `buildPipeline.mode` dropped, the x.com icon `currentColor`), and the six sites by name + the hub + the homepage (the homepage through `pnpm ops build-homepage {"deploy":true}` — O4's live proof). **Jasolyzer** (https://jasolyzer.pages.dev; `piratesoftware`, 1,889 videos; archives and transcript downloads off; vermilion; a draft description the operator may reword) is the sixth site in every footer, the hub and the homepage. **The LM chat-only tier is closed as moot** (Legal Mindset's filter has `includeLivestreams: true`). - **The external drive stalled the rollout** (11:35–12:57): an SMR disk (`sdb`, JMicron JMS578 UAS) under a 10 GB remux timed out, reset and froze :3001 — all four libuv workers blocked in its ext4 reads. No data errors. **Open, for a later slice:** one stalled drive freezes the whole editor. **Operator options:** `echo 180 | sudo tee /sys/block/sdb/device/timeout` (not persistent) and the durable `options usb-storage quirks=152d:a578:u` (a replug; do it with the lanes paused). - **Release 12 belongs to a parallel session** (the source mirror, [`source-mirror.md`](source-mirror.md); slice Q merged at `4855f70b`, slice R next). **This session's bundles are release 13** ([`release-13.md`](release-13.md)): W1 editor lows, W2 export/docs lows, W3 the build image (with W3b: container Build all was broken on `main` — `.next` symlink + the image's baked `public/` — and a wrong-site guard now sits before every site deploy), all reviewed SHIP; then one-core Phase 5 (P0, the plan doc `one-core-phase-5.md`, awaits the operator's OK and five rulings). - **Worktrees:** the seven `r11-*` stay until release 12's are gone (index-based ports). **Previously (2026-09-28, morning): release 11 is merged to `main` in full and NOT rolled out.** The overnight of 2026-09-28 ([`release-11.md`](release-11.md): every slice's record, then "Integration, as merged", then "Rollout"). `main` = `eb28a341` + three integration `plans:` commits (FACTS `eeff74c3`, `brand-and-themes.md` `3643c974`, then this STATE and the record); no code changed at integration. Nothing was deployed, cut, pushed or restarted; :3001 still runs `BUILD_ID` `S07zTu3MKTjHJa9eDM1GF` (2026-09-26 21:46). - **The slices, in merge order:** - **O4 — the homepage builds and deploys from `/sites`** (`baaa4b47`): a Homepage section after Hub (Build homepage / Deploy after build / Deploy homepage, a preview branch box), jobs `build-homepage` / `deploy-homepage` / `build-deploy-homepage`, `/api/ops/build-homepage|deploy-homepage` and `pnpm ops build-homepage|deploy-homepage`; `JobLane` survives Strict Mode. - **O3 — runner lows** (`c1d4790a`): an `error` recheck keeps a video "Missing?" (5 real videos flip at the next build: jeralyzer 4, rekietalyzer 1); a failed forced source download keeps the subtitle pass's status and fails the job with yt-dlp's line (`sourceFetchFailure`); a bucket's retry keeps its log (`cookies-mode.spec:241` deterministic); the cut-release lows L3 + L4. - **O6-A — one-core Phase 4 slice 3, checkpoint A** (`cb9d02b2`): `archilyzer doctor`, `run `, `mcp`, every bin a subcommand, `pnpm archilyzer`; `common/lib/ports.mjs` and `common/lib/envVars.ts` → generated `ENVIRONMENT.md`; `PUBLISH.md` absorbs the two deploy docs; the build mode is a label (then dropped outright: O6c, below). - **O1 — hub lows** (`6cd3a9fe`): `/ask` waits for the hub's list, says when there are no archives, shows a scope line; a member's missing live chat on its chip with a live-chat Retry; `normalizeSocialSvg` themes a single-colour icon (the x.com fix); `mcp/README.md`'s `fetch_clip` env lines. Three DRAFT copy lines (ruled 2026-09-28: shipped as written, O1c). - **O2 — homepage and brand lows** (`139a648e`): a custom hex fitted per base (`perBaseColor`), `--chart-6` (a rust, Vermilion's family, validated), the mark's forced-colours outline, the homepage e2e on a synthetic summary. - **O5 — umtool brand fonts** (`15d5d646`): the rail / ledger / scroll / chart in IBM Plex Sans under the brand, fitted by the face's own advances; IBM Plex Mono Bold vendored; an unbranded render byte-identical (334 files). - **O2b — follow-ups** (`69e7f59e`, + changelog nits `59d2f877`): the hub's added archives fitted per base; `` passed over like ``; `no-data.spec`. - **O6-B — checkpoint B** (`eb28a341`): every test-only env var `E2E_`-prefixed and declared in its playwright config; the harnesses read `ports.mjs`. **One-core Phase 4 is done** (the optional SETUP/PLAN.md consolidations aside). - **O6c — the build-mode stub dropped** (2026-09-28 morning, on the operator's ruling; branch `r11/phase-4-s3` tip after `5b6a66ec`): `buildPipeline.mode`, the `/sites` toggle and the Settings select are gone; an old `settings.json` with `mode` loads and drops it on the next save; Build all still uses containers whenever `docker version` answers. - **Gates on the merged tree** (`eb28a341`; every number in the record): tsc clean; common 2,112, editor unit 85, `test:scripts` 185 + 1 skip, mcp 269, homepage unit 2; the three generated-doc checks exit 0; editor, export site + hub, homepage and umtool builds ok; e2e export 206, `e2e:hub` 33, `e2e:2origin` 3, homepage 31 with 0 skipped (no `homepage/public` data), umtool 175 + 2 failed + 45 skipped (the known `mix.spec` pair; alone ×3 36/36), editor full 652 + 1 failed + 12 skipped of 665 in 39.5 min (`transcript-source.spec.ts:76`, a flake new to the records: alone ×3 9/9). Verdict: **FLAKES-ONLY**, no regression. The primary's `export/public` was byte-identical at every checkpoint. - **What the operator owes, in order** (`release-11.md`, "Rollout"; the parent's runbook `~/reports/overnight-2026-09-28/MORNING.html` has the scripts): 1. ~~**The copy rulings**~~ **Ruled 2026-09-28 (the operator: "ship the drafts as written but I don't like this regex bracket link thing").** The four lines in `export/app/ask/hubScopeCopy.ts` ship as written. Follow-up O1c (`a4ffa959`, `r11/hub-lows`) removed the DRAFT markers and replaced the [bracket] + regex link with `` copy`…${link("…")}…` ``. No reader-visible change, so no bullet. Record: `release-11.md`, "Operator rulings and follow-up O1c". 2. **The operator questions the records raise:** - ~~Should Build all honour `buildPipeline.mode` (today a label; O6)?~~ **Ruled 2026-09-28: dropped** ("just drop it for now rather than keep the stub"). Follow-up O6c removed the mode, the `/sites` toggle and the Settings select; an old `settings.json` with `mode` still loads. Build all uses containers whenever `docker version` answers, as before. - ~~Is `--chart-6`'s rust near `--state-gone` acceptable (O2)?~~ **Ruled 2026-09-28: accepted; Vermilion keeps slot 6.** - ~~Settle the five 410'd Rumble videos with a "Full-check unexpected" on `rekietalaw-rumble` and `the-quartering-rumble` (O3)?~~ **Done 2026-09-28 13:32Z** (the operator: "full check the rumble channels"; run by the parent on :3001 through `platform:rumble`): all five read deleted, 0 errors, so they publish as Deleted at the next build, not "Missing?" (jeralyzer's and rekietalyzer's counts do not rise by 4 and 1). Record: `release-11.md`, "Operator actions (2026-09-28 morning)". 3. **A release cut** (`archilyzer release cut all next --commit`; editor 8 and export 9 pending bullets — export's is public on every `/changelog`). 4. **The :3001 restart** on the new `main` (it brings O4's Homepage section and routes, O3's retry-bucket and cut-form fixes, O6's copy and the digest replay that keeps `ids` — restart before retrying any `archilyzer run digest` job — and O2's sidebar outline). 5. **One settings write** after the restart (any Save): it re-normalizes the stored social icons, so the x.com icon loses its `fill="white"`. 6. **The deploys:** the five sites by name, then the hub, then the homepage — the homepage through the new `/sites` → Homepage section (Build homepage, then Deploy homepage), which is O4's live proof. 7. **Re-upload the YouTube picture, watermark and banner** (`~/reports/archilyzer-media/brand/`), still owed since release 10. 8. Optional: rebuild + restart umtool on :3050. Its app changed only in `lib/tools.mjs` (the shared tool probe) and two test-only variable names; O5's render changes already reach it from disk (the live :3050 spawns `report-to-video/*`), and an unbranded render is byte-identical. - **Release 14 (2026-09-28): HP merged (`bfa1ff3c`, final review SHIP); T1 and H1/H2 built on `r14/two-grounds-headers`, not merged** — `plans/export-header-first-search.md`, record in `release-14.md`. S1 (a clear screen until the first Search) is next, and S2 is a candidate. - **HP** (merged): the homepage's header carries the social row and one theme toggle at every width, through the shared `SocialLinks`; its wordmark's text drops first on a very small screen; Changelog is in the footer; the cards wear the site's wordmark; a social icon is checked by an allowlist on save and at render; `featured` on a social link. - **`r14/two-grounds-headers`** (built, `0f358ee7` + the records): - the final review's Lows (F1, F3, F4, F5, F8); - F8 opts every cwd-derived path op in the three Next apps out of Turbopack's tracing, with the guard widened to `scripts/next-build-trace.test.mjs`; - the charts' foreground separators are withdrawn for a 2 px gap in the surface's colour, the shared charts included; - **T1:** two grounds, Light and Dark, in every app; a stored Sepia reads as Light before paint and is rewritten; each site wears its own accent, and the accent picker is gone; - **H1/H2:** every site's header and the hub's carry the social row and the toggle as one group; an Archilyzer link to the homepage's new `#instances` replaces the sites dropdown and the hub link; Changelog is in the footer; the wordmark's text drops exactly when it does not fit, for any title; the inline nav starts at `lg`. - **Reviewed SHIP AFTER FIXES; the fixes are in:** - stacked areas in the shared charts keep their coloured edge, with the surface gap on stacked bars only (H1); - the growth chart measures thickness at right angles before it takes a gap, so no band is covered; the gaps now read as dashes, and folding the small sites into "Other" or small multiples is the operator's call (M1); - the hub URL texts say what the setting does now (M2); - the Lows (L1, L2, L5, L6, L9, L10); L3 and L7 are left, and L4 needed nothing. - **Owed:** the parent's merge, then the rollout in `release-14.md` ("## Rollout"): the editor, then the homepage (with `#instances`) before any site, then the hub, then the six sites. - **The narrow header, as ruled after the review, is built** (`447ded9a`). Below 520 px every header shows the name and only the links marked **Keep in header on small screens** (none marked → none); from 520 px it shows every link, up to four. - **Re-reviewed SHIP; its findings are in** (`1a2e3342`, `4ea1c495`, `11a33f7a`): - below the switch the export bar's two gaps are 8 px (ruled 2026-09-29): every real title shows in full at 360 px under touch with one marked link (Rekietalyzer at exactly 360); - the wordmark's reservation is a minimum width, so wider-rendering text is never clipped; - the switch is `32.5rem`, so it moves with the reader's text size, and the homepage's wordmark fit is keyed by the wide row's count from it; - a marked link keeps its configured place (ruled as built). - **The operator's settings:** the link to keep in the header is marked in Settings BEFORE any build; the parent does this when the branch lands (`release-14.md`, "## Rollout", precondition 5). - **Next candidates:** one-core Phase 5 (projects join the core, `plans/one-core.md`); the Diagnostics cards keeping their retry log (O3's found-and-left); `ChartView.tsx`'s five-slot cycle reaching `--chart-6` (O2); O5's two wording lows in `svg-faces.mjs` / the README (kerning is not conservative; `FALLBACK_EM` has exceptions); the LM chat-only tier config; the `Dockerfile.build` image refresh; `transcript-source.spec.ts:76`'s stale-snapshot race (FACTS, "What the integration gate found"); removing the seven `r11-*` worktrees, this one included once `main` carries its tip (it re-sorts the port blocks; `diet-series` stays). **Release 10 is fully rolled out as 0.9.4 (2026-09-28, 00:53).** The blocks below this one are the history, newest first from "Accent swap and mark contrast rolled out". - **Slice MR** (the mark ring on dark) merged `2b10f31b`; **0.9.4** cut as editor `2a6f6884` + export `ee9d41fa` (record: `release-10.md`, "Slice MR, as shipped"). - **Deployed from `ee9d41fa`** (`~/reports/release-10/tmp/r10-*.log`): the five sites by name (jeralyzer `df279ac1`, anilyzer `e367536d`, bonnellyzer `41e9b1aa`, hasanalyzer `a48342b6`, rekietalyzer `05444c21`), then the hub `10d6946f`, then the homepage `7ca70923`. - **Verified live:** `--mark-ring` on every surface; `/changelog` shows 0.9.4. - **Owed by the operator:** - the :3001 editor restart (the favicon and the sidebar mark's ring; :3001 still runs the post-brand build of 2026-09-26 21:47, `BUILD_ID` `S07zTu3MKTjHJa9eDM1GF`); - re-uploading the YouTube picture, watermark and banner (`~/reports/archilyzer-media/brand/`). - **Next:** release 11, the overnight of 2026-09-28 ([`release-11.md`](release-11.md)). **Previously (2026-09-26, afternoon): release 10 is merged to `main` in full and NOT rolled out.** It is the brand (S0–S3: the Found-line mark and base × accent reader themes; its own final commit `bb6f378f`, then cut as editor + export 0.9.0 at 01:37, `0e0d8f59` + `31798769`), plus brand S4 and the lows L1 + L2. Those three merged 2026-09-26 12:52 on the operator's word, before the rollout, and the integration pass gated them together. Record: [`release-10.md`](release-10.md) (the L2 and L1 records, then "Integration (S4 + L1 + L2), as merged"); the brand's plan and per-slice records, S4's included: [`brand-and-themes.md`](brand-and-themes.md). **Hub link hidden again (2026-09-26 night, operator: "Hide the hub again for now"):** `9217eaaa` `homepage/app/page.tsx` `HUB_LINK_ENABLED = false` (the release 9 C3 flip reversed); the hub stays deployed and reachable by URL, the sites' header backlink points at the homepage (not the hub) so nothing else changes. Homepage e2e 27/27; redeployed to production from the primary (see the line below this block's homepage entry). **Merged to main (2026-09-26), NOT rolled out:** - **Slice P, cut a release from the CLI — merged `2edaf4f8`** (`cli/cut-release` @ `e23782e8`, reviewed SHIP after fixes; common 2,015, scripts 174 + 1, e2e list 53/53). One writer `common/controller/cutRelease.ts`; the `/sites` form is an adapter (its version box also takes `next`/`next-minor`); `archilyzer release show|cut [--commit] [--date]` locally with no editor running (`all` cuts both or neither; a real calendar date; a version newer than the latest heading); `pnpm ops cut-release --json '{"workspace","version","commit"}'` → `/api/ops/cut-release` (exists only on an editor built from this main). No package.json bump, no tag — the heading is the version. Proved read-only from the primary: `release show` → editor 0.9.0 + 9 pending, export 0.9.0 + 4 pending, next-minor 0.10.0. The runbook's step 0 is now that command. Plan: [`cut-release-cli.md`](cut-release-cli.md). - **Slice O, the Ko-fi mark on every site — merged `6a5167a7`** (`export/kofi-mark` @ `ec08bc7e`, reviewed SHIP; export e2e 204/204, hub 24/24). The official `kofi_symbol.svg`, byte-identical at `export/public/kofi-symbol.svg`, is the LAST item of the footer's social row on every site and the hub (accessible name "Ko-fi", no copy, official colours, offline-cached like the icons); the hub's bare text link is gone. SUPERSEDES the 2026-09-25 "hub + homepage only" ruling on the operator's word (2026-09-26: "show kofi in the social section on all sites"). Homepage untouched. Live only after every site + the hub rebuild (runbook steps 5–6). Plan: [`kofi-mark-on-sites.md`](kofi-mark-on-sites.md). **Reversed by `export/drop-kofi`** (Ko-fi dropped for now, 2026-09-26; record in `release-10.md`). - **Slice N, the whole-recording fetch downloads anyway + metadata history — merged `6a1678a0`** (`editor/full-fetch-media` @ `115f9656`, reviewed SHIP after fixes; full editor e2e 641/2/12 of 655, both failures classified: `tags.spec:220` load flake 3/3 alone, `cookies-mode.spec:241` a pre-existing log-unmount in `DownloadStage.tsx`, low). `forceMedia` on `downloadOneManaged`: Persist source video, `fetch_clip` `full: true` and Persist kept now fetch + persist the source on a captions-only channel even with a transcript on disk (Whisper transcript untouched, no audio beside one; YouTube subs are re-fetched as on any re-download). Every `metadata.info.json` rewrite appends a normalized diff to `metadata.history.json` (cap 200), shown on the video page. NEEDS the :3001 restart (runbook step 1); live proof after it: `full: true` on `teamrcn/dbnS-cBgStY` → a file in `saved-videos/`, and a history entry. Plan: [`full-fetch-forces-media.md`](full-fetch-forces-media.md). - **Slice M, `fetch_clip`, merged after the integration pass and ALREADY IN EFFECT:** `mcp/fetch-clip` @ `fa44984a` → `main` `64f26a71`. The MCP gains ONE tool that asks the live editor's `POST /api/media/fetch-window` for a cited moment's media (a window ≤ 15 min, or `full: true` for the whole recording into the saved-video store); the ask/sweep plans send media through it and never a hand-run yt-dlp. Live-proved 2026-09-26 evening against :3001 (YouTube window fetched then cached; Rumble embed id → slug dir; full recording fetched then cached); `archilyzer` re-registered with `ARCHILYZER_EDITOR_URL` + `WORKER_TOKEN`. Nothing to restart. The `/ask` fresh-session proof is DONE too (19:15Z, a Kirsche clip through `fetch_clip` with `source: remote:…anilyzer…`, 47 s). Two editor-side lows found by the proof (record, "Slice M — fetch_clip, merged and proved live"): full mode is a silent no-op on a captions-only channel whose video already has a transcript; full mode rewrites `metadata.info.json`. Plan: [`mcp-fetch-clip.md`](mcp-fetch-clip.md). - **The merges**, in the planned order: S4 `dcb04f61` (`brand/media` @ `333d2826`) → L2 `41ddc382` (`r10/runner-lows` @ `14e96739`) → L1 `5c0a6ef9` (`r10/hub-lows` @ `b6b47ec1`). The code merged clean; the conflicts were `plans/release-10.md` (the record sections, all kept) and one `editor/CHANGELOG.md` `[Unreleased]` join (S4's bullet and L2's four in one section above `[0.9.0]`). - **Integration tip:** `brand/found-line` = `main` `4ac32a2e` + the integration `plans:` commits (FACTS `9e3047e3`, then this STATE and the record). `4ac32a2e` is `5c0a6ef9` plus the plans-only `plans/mcp-fetch-clip.md`, committed to `main` during the pass; the integration commits were rebased onto it. No code changed at integration. The parent fast-forwards `main` to the tip, and the runbook's `FINAL` is that tip. - **What is in it:** - **S4, Archilyzer Media:** `common/lib/brandMedia.ts` (the lockup with its letters as glyph outlines); `common/bin/brand-media.ts` (the YouTube assets, already generated in `~/reports/archilyzer-media/brand/`, with `INDEX.html` there; upload any time); umtool report-to-video's opt-in `render.brand: "archilyzer-media"` with vendored OFL fonts (a manifest without it renders byte-identical, 334 files checked); and the brand choice in umtool's new-project form. - **L1, hub:** subs 404 = an empty manifest, one retry, never fails an archive; `/ask` honours the scope chips (none in scope: disabled + one line); the official instances in the homepage's order and colours; the playwright config and the compose scripts never write through `export/public` links (`common/bin/_publicFile.ts`; the FACTS workarounds are retired). - **L2, runner:** YouTube's "try again later" soft block backs off (batch, runner, scan, a prefetch early exit; the availability check stops); `verifyBeforeClean` never judges an unprobed suspect; the boot pass waits at most 60 s for the storage pass; `/jobs` shows `cancelReason`; the safeRevalidate warning is counted. - **Gates on the merged tree** (`5c0a6ef9`; every number in the record): - tsc clean; - common 1,954, editor unit 85, `test:scripts` 173 + 1 skip (the report-to-video tests included), mcp 219; - editor, export site + hub, homepage and umtool builds ok, the icon checks green; - e2e: export 204, `e2e:hub` 24, `e2e:2origin` 3 (the `export/public` links left in place: the primary's `export/public` and a `sw.js` sentinel byte-identical throughout), homepage 27; - umtool 175 + 2 failed + 45 skipped (the known order-dependent `mix.spec` pair; alone 6/6); - editor full 634 + 6 failed + 12 skipped in 54.6 min, under the live editor's load. All six are flakes (fixture EEXIST, the pulse timing tripwire, two sync-deep stamp races, a 7 s task timer, the diarization snapshot poll); alone ×3: 18/18. No integration regression. - **Next action: the operator re-cuts the editor + export `[Unreleased]` notes, then the rollout per `~/reports/release-10/RUNBOOK.html`** (`make-runbook.py` beside it; the scripts in `scripts/`). Both changelogs have a new `[Unreleased]` above the 0.9.0 cut. Export's (L1's three hub bullets) is public on every site's `/changelog` from the next site build; the editor's holds S4's and L2's. Cut on `/sites` → Release notes with "Commit changelog" unticked whenever the primary's tree is dirty (it refuses one; at the 0.9.0 cut an untracked `settings.json.pre-priority-*` made it so, and the tree was clean at this pass), then commit both files by hand. Then, per the runbook: 1. restart :3001 on the new `main` (one editor restart; md5 before / pre-restart / after-boot; smoke); 2. optionally, rebuild and restart umtool on :3050 with `r10-umtool.sh`, for the form's brand choice; 3. set each site's accent and wordmark lead; 4. preview Anilyzer; 5. deploy the five sites by name, then the hub, then the homepage. - **Jeralyzer already serves the brand, in Signal.** A build-deploy on the live editor built Jeralyzer from `main` @ `386ac995` and deployed it to production (job `01M3F36N7SCKGC5JV27EBDFYPN`, 2026-09-26 10:51–11:06, no agent recorded). That tree was the brand plus the 0.9.0 notes, before S4/L1/L2. Its accent is unset, so it wears Signal until the runbook's step 4 (every site by name) rebuilds it in Brass. The same build left the primary's `export/public` as a Jeralyzer site compose (no hub `sw.js`, `hub-sites.json` or `hub-summary.json`). - **umtool is no longer untouched.** Its report-to-video scripts changed on disk, and the live :3050 already spawns them (it still runs release 8's build, `a9YAe_dwhuM6DiCPzIwMD`). That is safe: a render that does not opt in is byte-identical. Its app changed only for the new-project form's brand choice, which appears after the optional `r10-umtool.sh` rebuild and restart. - **Copy rulings still owed by the operator:** *(RULED 2026-09-28: all four hub copy lines ship as written, each now carrying its link in the line (the tagged template `copy` with `link()` replaced the [bracket] regex); `/ask` shows the scope line. Release 11 follow-up O1c, merged `e1ef1923`.)* - `NO_ARCHIVES_IN_SCOPE` (`export/app/ask/hubScopeCopy.ts`). The draft is "No archives selected. Choose some on the [hub's front page] to ask.", with the bracketed words a link to `/`. It ships as drafted unless changed. - Whether `/ask` shows a "Searching N of M archives" scope line. - **New lows (not started):** *(ALL CLOSED 2026-09-28 by release 11, merged to `main` and not yet rolled out: the icon by O1 — `normalizeSocialSvg` themes a single-colour icon, applied at the next settings write; slice N's forced download and `cookies-mode.spec:241` by O3; `/ask` on a zero-archive hub and the chip's missing live chat by O1 (DRAFT copy); `resolveMaybeMissingState` by O3; umtool's rail/ledger/chart font and the bold IBM Plex Mono by O5. See `release-11.md`.)* - the operator's x.com social icon is invisible on the light footer (1.11:1): its `` carries `fill="white"` and `normalizeSocialSvg` defaults only the root fill — fix by re-pasting the icon without the path fill in `settings.json` (found by slice O's screenshots); - slice N: a forced source download that fails on a video with a transcript marks the download failed, the fetch job still ends `done` with no file, and a `source-media.*.part` can linger; - `cookies-mode.spec.ts:241` intermittent under load: `NeedsCookiesList` returns null when its bucket empties and unmounts the retry's run log (`DownloadStage.tsx:442`) — keep the card mounted while a run lives, as `SourceVideoSection` does; - `/ask` waits forever ("Loading transcripts…") on a hub with zero archives; - the chip doesn't show a member's missing live chat (it needs a subs-only Retry); - `resolveMaybeMissingState` reads an `error` probe newer than the scan as `available` (display only: the published presence badge); - umtool's rail, ledger and chart text stays Fira Sans under the brand; - no bold IBM Plex Mono is vendored (the HyperFrames band's `fontBold`). - **Worktrees that can go:** `brand-mark`, `brand-themes`, `brand-media`, `r10-hub-lows` and `r10-runner-lows` are all merged. `brand-found-line` can go once `main` carries its tip. Removing any of them re-sorts the port blocks (`diet-series` moves up). **Accent swap and mark contrast rolled out (2026-09-27, 15:21):** - **Accent swap.** On the operator's word ("matches the subjects' branding") **hasanalyzer = violet** and **bonnellyzer = blue**, set through the site form; the md5 check shows only those two `site.json` files changed. The mapping is now: | Site | Accent | |---|---| | jeralyzer | brass | | hasanalyzer | violet | | anilyzer | sakura | | bonnellyzer | blue | | rekietalyzer | green | | jasolyzer | vermilion | - **Slice MC** (mark contrast) merged `7f64e507`. The mark's dim lines now reach 3:1 against their tile: child `#6b5d47`, Archilyzer `#586977`, and every lit value stays at least 2:1 above them. The rules are pinned in `common/lib/brand.ts`. The chart passes the validator for adjacent pairs in both orders. - **Release 0.9.3** cut: editor `cca3b7df`, export `5267f454`. - **Deployed** by `r10-sites.sh` plus `r10-home.sh` from `5267f454`: - the five sites (jeralyzer `f6ec1484`, anilyzer `dc65b57b`, and so on); - the hub `9c5e0b0c`; - the homepage `716ab0d6`. - **Verified live:** each site's `data-accent`, the header dim `#6b5d47`, the icons' dims, the parent `#586977` on the hub and homepage, and the homepage stripes and summary on the swapped accents. - **Owed by the operator:** - the regenerated YouTube picture, watermark and banner need re-uploading (`~/reports/archilyzer-media/brand/`); - the editor's own favicon updates at its next restart. - **Open, the operator's call:** on the dark base the ink tile still merges into the ink page. A hairline ring or a lighter tile on dark would fix it. **Release 10 rolled out (2026-09-27, 02:28):** - **Accents and wordmark leads** were set through the live editor's site form; the md5 check shows exactly the six `site.json` files changed: | Site | Accent | Lead | |---|---|---| | jeralyzer | brass | Jer | | hasanalyzer | blue | Hasan | | anilyzer | sakura | Ani | | bonnellyzer | violet | Bonnell | | rekietalyzer | green | Rekieta | | jasolyzer | vermilion | Jaso | Rekietalyzer moved from vermilion to green because no five-accent set passes the dataviz validator on all pairs; see `release-10.md`, "Slice AC". - **Slice AC** (accent colours) merged at `edf8dabc`, then **0.9.2** was cut (editor `9bb0a012`, export `d9edb240`). - **Deploys:** `r10-sites.sh` ran from `d9edb240` (01:55–02:27), then `r10-home.sh`: - jeralyzer `52254b28`, anilyzer `8417624e`, bonnellyzer `db2b1820`, hasanalyzer `ca2d5073`, rekietalyzer: see the log; - the hub `92aad41e`, whose `hub-summary.json` covers 5 instances; - the homepage `1eb1f60d`. - **Verified live:** every site has its `data-accent` and split wordmark, the hint is `text-brand`, and no page links to Ko-fi. The homepage cards use `var(--swatch-)`, the chart uses `--chart-1..5`, and both summaries carry `accentId`. - **The editor was not restarted, on the operator's word.** :3001 has run a post-brand build since 2026-09-26 21:47 (`BUILD_ID` `S07zTu3MKTjHJa9eDM1GF`, main ≈ `9801a034`). - **Skipped:** the runbook's Anilyzer preview / phone check, on the operator's choice to deploy all five directly. - **Still optional:** `r10-umtool.sh`. - **New lows:** *(both CLOSED 2026-09-28 by release 11 O2, not yet rolled out: `--chart-6`, a validated rust in Vermilion's family; a custom hex fitted per base through `perBaseColor`.)* - A sixth published site's chart colour would nearly duplicate Anilyzer's magenta. Fix this before Jasolyzer gets a `siteUrl`. - A custom hex is not fitted per base on the homepage cards. **Deployed 2026-09-26 23:58 (release 10, partial rollout):** Ko-fi dropped for now (merged `a54014e5`); release **0.9.1** cut (editor `98d4bf6f`, export `bc1b0a03`, `archilyzer release cut all next --commit`); the hub (ops job `01M3GF6SKBJFGH4R7YY3TRMBZE`, deploy `0cda9466`) and the homepage (`r10-home.sh`, deploy `665b9faf`) redeployed from `bc1b0a03`. Both are verified live: no Ko-fi link, `/kofi-symbol.svg` 404, `data-accent="signal"`, and the homepage `/changelog` shows 0.9.1. **Still owed** (runbook, in order): - the :3001 editor restart; - each site's accent + wordmark lead, set in the site form; - the Anilyzer preview; - the five sites. Jeralyzer is live on the brand in Signal until then. **Live on :3001 — unchanged since 2026-09-25, 19:40:** `0213f6c8` (release 9 slice F + hub C1), `BUILD_ID` `P0VMdKX7gbdsaiS5GvorF`; umtool's build untouched (`a9YAe_dwhuM6DiCPzIwMD`; its report-to-video scripts on disk are `main`'s, above). Live sites: - https://archilyzer-hub.pages.dev — C1 + C1b + C2 (archilyzer theme, "Official Instances" with the homepage's figures, scope chips, per-archive state, "N of M archives answered", archive named on every card, bare Ko-fi footer link), deploy `6ef7f472` (20:50); - https://archilyzer.pages.dev — **release 10's build** (the brand, in Signal) from `main` `555bc454`, deploy `857e0092` (2026-09-26 17:19). It was deployed early, on the operator's word, by the runbook's own `r10-home.sh`, so **the homepage half of the runbook's step 5 is done**. Its `/changelog` shows export's `[Unreleased]` until the re-cut and a redeploy. There is no UI deploy yet: [`homepage-deploy-from-ui.md`](homepage-deploy-from-ui.md); - https://jeralyzer.pages.dev — **the brand, in Signal**, deploy `35593886` (2026-09-26 11:06, above); - the other four official sites at release 8's slices Z + E (17:45 / 18:10 deploys); they carry no Ko-fi, and with Ko-fi dropped (`export/drop-kofi`) release 10's rebuild will not add it. **The release 10 candidates of 2026-09-25 evening are all done** (L1 and L2 above). Next after release 10's rollout: the homepage deploy from `/sites` ([`homepage-deploy-from-ui.md`](homepage-deploy-from-ui.md), one small slice), Phase 4 slice 3 (`plans/one-core.md`), the LM chat-only tier config, the `Dockerfile.build` image refresh, and the new lows above. **The 2026-09-25 evening plan is complete** (steps A–C: release 9 F + C1 live on :3001; C1b, C2, C3 on the hub and the homepage). The operator's rulings (C2's federated search as proposed; a bare Ko-fi link on the homepage and hub footers only — superseded 2026-09-26 by slice O, the mark on every site, and then **Ko-fi dropped for now** (2026-09-26, `export/drop-kofi`: no Ko-fi link or asset on any site, the hub or the homepage); the Paramount Tactical filter clear) are recorded in `release-9.md`. Paramount Tactical downloads in full at lane pace since the operator cleared its `Quartering` include filter (19:35). See the [release-9 rollout record](release-9.md#rollout-2026-09-25-evening--0213f6c8-live-on-3001-third-restart-of-the-day). See the [release-8 rollout record](release-8.md#rollout-2026-09-25-late-afternoon--0e72ef73-live-on-3001-second-restart-of-the-day). **Last updated:** 2026-09-25 (late afternoon). **Release 8 is live, the day's second restart.** - Release 8 (`bb3dbb4c` → `0e72ef73`) has two slices, each Opus-reviewed: SHIP AFTER FIXES, then SHIP. - **V, video titles** (`one-core/r8-video-titles` → `faa92c51`), the operator's 13:40 ask. The channel video list shows and searches titles: index → scan store → `metadata.info.json`, memoized per channel by the `data/` mtime after the review measured Rumble lists at 5 s per render. The video page shows scan-store metadata for an undownloaded video. - **S, one shared auto-queue state** (`one-core/r8-state-share` → `0e72ef73`). This is the root cause of the 13:33 `state.json` revert, which recurred at ~14:18: four lanes booted concurrently and each held a private copy. The in-flight read is now cached, and a manual Sync/scan 429 cooldown writes through the live object. - Record: [`release-8.md`](release-8.md). - Gate on `0e72ef73`: tsc 0, common 1,810, editor unit 75, scripts 161 + 1 skip, `next build` ok, the union of both slices' specs 86/86. No numbers changed. - Rollout. md5 80 → 80, with one explained difference: `paramount-tactical/config.json`, the operator's `downloadFilter.include: "Quartering"` plus a `lastFullDownloadAt` stamp. The deferral and `fails: 13` survived the restart. Smoke `SMOKE_FAIL=1` with both flags explained: a moving jobs list, and a correct deferral where the old check expected `[]`. Titles are proved live on `/channels/paramount-tactical/videos`. - **Second-restart proof:** no `legal-mindset` re-scan on either of today's boots. - **Paramount Tactical.** The boot-dispatched metadata scan ran at about 10/min with 0 429s. It was cancelled at 377/1,382 on the operator's ask, and its store kept the 377. download-missing then settled **376** non-matching videos by the filter with no request and is downloading the rest. `pnpm ops keep-videos` ran live for the first time: 1 match (`NV1QqS9NOiU`), marked. - **The owed `teamrcn` sync ran** and moved exactly `lastSyncedAt` + `lastFullSweepAt`, but it reads `failed`: `Invariant: static generation store missing in revalidatePath`. A QUEUED job's own `revalidatePath` runs outside any request. - Operator questions answered today: the 429s are not concurrent downloads, and `downloadFilter.include` has a UI (release 7). **Open:** - **the queued-job `revalidatePath` fix**, small. A `safeRevalidate` in the editor, or drop the job-body revalidations for `requestChannelSnapshot`. Until then, an ops job that queues reads `failed` after doing its work. The running download-missing `01M3CY7FHQNMTA376HA724BKK7` is expected to end that way; - **the keep-videos re-run** after download-missing finishes (dry run first, `match: "TheQuartering|Quartering"`). Optionally, the rest of the Paramount Tactical scan (~1,000) for list titles; - the `/jobs` stall label misreads a slow scan (`STUCK · POSSIBLY-STALLED` at 10/min); stale `queued` metas after a restart; the smoke's deferred check should become "well-formed"; - S's optional merge nit (merge `until` and `fails` separately); V's memo nits and L3; - the evening pacing watch: `grep -h 'deferred 6h' transcripts/.jobs/*.log`; - the 410 re-read (release 7 step 11), not observed; - candidate: YouTube `--sleep-requests` in `common/ytdlp/platformArgs.mjs`, not applied; - worktrees to remove: `one-core-r8-{video-titles,state-share}`, `one-core-r7-{pacing,cli,keep}`, `one-core-r5-{exports,rumble}`, `one-core-r6-followups`, `one-core-phase-4-s1`; - `thequartering-X`'s transcripts manifest goes live at the next jeralyzer build + deploy. **Next:** the `revalidatePath` fix (a small slice), then Phase 4 slice 3: config + docs, `doctor`, `run`, `mcp`, and `PUBLISH.md`. **Last updated:** 2026-09-25 (afternoon). **Release 7 is live, with release 6 riding along.** - Release 7 (`0032ed8a` → `bb3dbb4c`), three slices, each Opus-reviewed: **Y** pacing (`one-core/r7-pacing` → `2497d20b`): a rate-limited video is deferred 6 h (`videoDeferrals`, persisted beside `platformBackoff`) so the lane moves on after the cooldown; an identical metadata-scan error refreshes its `at` once a cooldown old. **C** the CLI (`one-core/r7-cli` → `3049be43`): `common/bin/archilyzer.ts`, named publish entry points, export's `build` = the CLI (`build:nodata` / `prebuild` gone), docker scripts on the CLI, hub build + deploy (`/sites`, `pnpm ops`, CLI), `build|deploy homepage`, the posts-only 404 fixed in the composer. **K** `pnpm ops keep-videos` (`one-core/r7-keep` → `ac2c4aee`), the operator's mid-rollout ask. Plus `211d4666`, a test-only guard (below). Record: [`release-7.md`](release-7.md). - Final suites on `3049be43`: editor 626 passed / 5 failed / 12 skipped (53.1 min, machine in swap) — four load timeouts and one REAL isolation bug: `ops-api.spec.ts` deploy-hub started a deploy job of the hub a 2origin run had left in `export/out` (nothing reached Cloudflare), fixed `211d4666`; the five specs on `bb3dbb4c`: 55/55. Export 192/192, hub 8/8, 2origin 3/3. - Rollout: install no-op; numbers 1,353 paths / 3,859 lines (corpus growth); md5 80 → 80 identical across the restart; smoke `SMOKE_FAIL=0`; a Rekietalyzer `skipData` build through `pnpm ops` (27 s, no `prebuild`, no data phase). The old server had already run C's new `build site` script live at 13:00 (a Rekietalyzer build-deploy, 81 files). - **Pacing proved live within 10 minutes of boot:** the old build had looped 12× on `paramount-tactical-videos/_60iFE_FBPQ`; the new runner's first 429 logged `… (attempt 13). _60iFE_FBPQ deferred 6h; next video after cooldown.` - Operator questions answered: the 429s are NOT concurrent downloads (one serialized `platform:youtube` queue, 0 overlaps in 36 h; all 26 are subtitle fetches, multiplied by the old re-pick loop); `downloadFilter.include` has a UI (Configure form). **Open:** - the owed `teamrcn` sync (step 10) and the Paramount Tactical metadata scan, queued behind the youtube cooldown; then `pnpm ops keep-videos {"slug":"paramount-tactical","match":"TheQuartering"}` (dry run first) — the channel was renamed from `paramount-tactical-videos` at ~13:29; - the evening watch: `grep -h 'deferred 6h' transcripts/.jobs/*.log`; the scan-error refresh proof needs a second restart within 24 h; - the 410 re-read (step 11), not observed today; - candidate: YouTube `--sleep-requests` in `common/ytdlp/platformArgs.mjs` — not applied; - worktrees to remove (`one-core-r7-{pacing,cli,keep}`, `one-core-r5-*`, `one-core-r6-followups`, `one-core-phase-4-s1`); - `thequartering-X`'s transcripts manifest goes live at the next jeralyzer build + deploy. **Next:** Phase 4 slice 3 (config + docs, `doctor`, `run`, `mcp`; `PUBLISH.md` absorbing `DEPLOY_DOCKER.md` + `DEPLOY_CLOUDFLARE.md`, and the two stale `archilyzer.pages.dev` hub hints C left: `SettingsForm.tsx:48`, the `homepage.ts:31` comment). **Last updated:** 2026-09-25 (early morning). **Release 5 is live; release 6 is merged.** - Release 5 (`f4da04a9` → `93dcb532`): slice R (`one-core/r5-rumble` → `3adaea9b`): one yt-dlp arg builder with a platform table (Rumble: `--impersonate chrome --sleep-requests 1` on every spawn), a 429 mid-sweep is "incomplete" (paged walk + platform cooldown, never a listing, never a stamp), a bare 403 backs the platform off. Slice X (`one-core/r5-exports` → `93dcb532`): `transcriptDownloads` (absent = on) on `site.json` AND `homepage.json`, gating the three per-video export controls on the export site; the site and hub forms carry the checkbox. Final suites on `93dcb532`: editor 628/628 (no flake), export 192/192, hub 8/8. Record: [`release-5.md`](release-5.md). - Rollout: ONE restart; smoke green; boot rewrote nothing; proof sync of `rekietalaw-rumble` and the first complete `the-quartering-rumble` sweep in 44 days (8,045 listed, was 7,866, paced past page 300 with no 429; the job finished 01:50 with 97 videos archived, 0 errors, both stamps set; its 4 maybe-missing checks are upstream 410s that read `error` until the follow-ups' 410→deleted fix is rolled out); exports off on the five published sites (form → build-index → build-deploy each, verified at the public URLs; Anilyzer's owed production deploy rode along, now at spec 4). - Release 6 (overnight, operator-approved): follow-ups (410 → deleted, umtool gets the platform args through ONE table in `common/ytdlp/platformArgs.mjs`, atomic remote-transcript write, stale comment, measure-nav routes) and Phase 4 slice 1 (`buildDeployCore.ts` → `common/publish/build.ts`, aws-sdk deps to common; the move only — entry points, docker scripts and build:hub wait for the CLI slice). Record: [`release-6.md`](release-6.md). 188 orphan `*.tmp-*` files deleted (filtered). - Found and filed, not fixed: [`youtube-lane-pacing.md`](youtube-lane-pacing.md) — the evening's three YouTube cooldowns were ONE Short retried 12× from the head of the queue; the fix is a 6-hour per-video quarantine (small slice, recommended next), plus `metadataScanStore.ts:249-252` (scan error timestamp never refreshes → 142 members-only videos re-scanned every runner start). **Next:** Phase 4 slice 2 (`common/bin/archilyzer.ts` + the rest of spec item 1), the pacing slice, then the release-6 rollout (one restart) with the pacing fix riding along if it is ready. **Last updated:** 2026-09-24 (evening). **Release 4 is on `main` @ `e172749b`, and one-core Phase 3 is COMPLETE. Phase 4 is next.** The prelude was plans only: `4130aca1` (the rollout record) and `bbad0977` (the owed sync sweep). The three plan files `77f63356`, `42caf3cc` and `cc89abfa` were filed during the release. Three worktrees were cut off `4130aca1` and merged in this order: - `one-core/phase-3-p` → `77a32de2`; - `one-core/phase-3-w` → `ddad13f4`, which merged P and re-gated as `44dd843f`; - `one-core/phase-3-s3b` → `e172749b`. Release record: [`one-core-phase-3.md`](one-core-phase-3.md#release-2026-09-24-evening--rollout-prelude-slice-p-slice-w-slice-3b), with the P, W and 3b "as shipped" sections above it. What is still open is in [`one-core-phase-3.md`](one-core-phase-3.md#next--phase-4). Anchors are in [`FACTS.md`](FACTS.md#one-core-phase-3-release-4--slices-p-w-3b-verified-2026-09-24-main--e172749b). Previous: release 3, slices 3a and 4b, on `main` @ `ef88ac4d` ([record](one-core-phase-3.md#release-2026-09-24--rollout-slice-3a-slice-4b)), live on :3001 as `9ab10d77`. - **Slice P: /channels rack polish.** - One layer ladder (`channels/components/rackLayout.ts`: popover > thead > group header > identity, plus the deck). Channel rows no longer paint over a group's station buttons. - Four audit fixes: pinned group controls, an Advanced panel that scrolls into view, the section rule as an inset shadow, and the region as `isolate`. - The Transcribe station counts `downloadedNoTranscript` + `downloadedAutoSubsOnly`, minus exclusions, for every handling, and queues exactly those ids as two bucket jobs. - No number moved (`phase3-view-numbers.ts`, diff empty). - **Slice W: one write idiom.** - `writeFileAtomic` / `copyFileAtomic` sit under `writeJsonAtomic`, with one per-path chain and one unique temp name. - 26 sites were folded (19 JSON, 7 text/binary), and both per-module-copy write counters are deleted. A failed write no longer leaves a temp file behind. - No byte moved (`phase3-writers-numbers.ts`: 315 samples equal the old idiom's bytes, diff empty). - **Slice 3b: the video page's chore cards, one module each.** - `VideoPanel.tsx` went from 1,839 to 455 lines, the assembly only. It now draws 15 modules under `videos/[id]/components/cards/`, pinned by `lib/videoChoreCards.ts` and its test. - The stage half needed nothing: the flow work had already made `computeStageStatuses` / `computeChannelFlow` the one fold. - Labels are identical (126 entries, same md5). **Gates.** - On `e172749b`, re-run for the record: editor unit 72/72. - On 3b's merged tip `2e322e37`, whose tree equals `e172749b`'s: tsc clean, common 1738, scripts 156 + 1 skip, both builds green, the 19 video-page specs 108/108. - On W's merged tip `44dd843f`: mcp 219, the 20 writer specs 140/140. Full e2e on `e172749b`: editor **624 passed + 1 load flake (`lane-runner.spec.ts:361`, rerun 3× green)**; export **188/188** (worktree `one-core-phase-3-s3b` detached at `e172749b`, ports 3311/3310, logs `final-e2e-r4-editor.log` / `final-e2e-r4-export.log`) **Next.** 1. **The exports-off release**, [`site-exports-off.md`](site-exports-off.md): a per-site option, off on the operator's five published sites, which need a rebuild and a deploy. 2. **The Rumble slice**, [`rumble-sweep-pacing.md`](rumble-sweep-pacing.md): step 0 is `--impersonate chrome` through a per-platform args table (the embed endpoint 403s every download, upstream #17496); step 2 is pacing the full sweep. The operator applied step 1 on 2026-09-24. 3. **One-core Phase 4** (CLI, entry points, config, docs). `buildDeployCore.ts` imports nothing from `editor/**`. `PUBLISH.md` does not exist yet. The release-4 rollout and the owed one-sync sweep ride in the prelude of whichever release comes next. **Still owed (operator):** - the Anilyzer **production** deploy (the preview is up, see below); - the other five sites to corpus spec 4; - the LM chat-only tier (shipped, not configured); - the **188 orphan temp files** in `transcripts/`, listed by `find transcripts -name '*.tmp-*'`. 175 of them are `.auto-queue/state.json.tmp-*` from the 2026-09-11 full-disk day. They are safe to delete while nothing runs. **Owed (code):** - `transcribeOne.ts:173` writes the remote transcript with no temp. It is the one real `writeFileAtomic` candidate left. - The export page writers `buildIndex.ts:971` / `buildStats.ts:220` need restructuring, which is out of W's scope. - The deferred follow-ups in the phase-3 "Next" section. **Previously:** 2026-09-22 — **the release *curated-tags follow-ups + debts sweep* is on `main` @ `766e0873`.** Base `4ac8ceda`; three branches merged in the order `1a011d96` alone → `tags/rules-and-ops` (`9837f066`) → `export/tags-followups` (`cb254919`) → `editor/debts` (`766e0873`). Anchors for every seam are in [`FACTS.md`](FACTS.md#the-2026-09-22-release--curated-tags-follow-ups--debts-sweep-main--766e0873). **Final full-suite run on 766e0873 (docs commits after it change no code): editor 600/600 (30.0 m), export 188/188, e2e:2origin 3/3 — all detached in the release worktrees; live contracts (jeralyzer corpus.json / llms.txt, anilyzer tags.json 96/282/56) unchanged** ### Curated per-video tags — shipped, reviewed, deployed to Anilyzer The plan is [`curated-tags.md`](curated-tags.md); it is now a shipped design, not work in flight. `transcripts/tags.json` is the authoritative vocabulary AND every assignment, written through the ONE path `applyTagAssignments` (`common/lib/curatedTagsStore.ts`) by all three writers (editor UI, `pnpm ops`, umtool); the MCP gained a read-only `list_tags` and a `tags` filter and no write tool. The record field is **`curatedTags`**, never `tags`. S1.1 froze the model (`d8c9d252`), then S1 ‖ S2 ‖ S3; review fixes `86cbb616`, `468f3a31`; the two re-apply bugs `edf0e204` (it held the event loop for minutes) and `8db8a3be` (the assignment-only pass destructured a bare key). **Rollout, 2026-09-21 → 09-22.** Seeded through `pnpm ops tags --file` (never by hand — the file carries provenance). First derivation: examined 77,224, re-derived 831 in 623 s; 103 transcript pages rewritten, 906 unchanged. **All three seed patterns were then wrong against the real corpus and were corrected** — the full before/after is in `curated-tags.md`'s rollout log, and the shape of the error is worth carrying: a bare `elfpire` matched a friends-list boilerplate on MommaOcco (200 false hits), `@PapaElfpire` in chat, and "Jason Angelfire" / "channelfireball" / "RekietaLawLive" in ASR. **ASR never spells "Elfpire"**, so the caption rule is a misspelling alternation with `\b` on both ends. **Anilyzer is live with Eva tags** (`4ac8ceda`), reviewed, with 21 suppressions. Two bugs the live data found were fixed there. Every other site stays at corpus spec 3 until the operator chooses to rebuild it — a pre-spec-4 site serves no `/tags.json`, which the MCP reports as empty-with-a-warning rather than as a silent miss. **And a rule can exclude channels** (`1a011d96`): `channelsExclude` is applied AFTER `channels` and **the exclusion wins**. It is part of `hashCuratedRules`, which is the trap — see the post-release block below (all done but the deploy). ### `storage/debts-1` — merged 2026-09-21 (`7589c50c` → `e109b6ac`, fast-forward) The three things S5/S6 named and left out; two of them are now done and the plan file says so. `assertRelocationRootPresent` (`1fccabac`) — a move can no longer `mkdir` its destination under an absent mount, checked from the preview AND immediately before each copy phase, on the channel mover and the saved-video mover both. A `/review` section for auto-paused channels (`bbc1899e`) — `common/views/review.ts` `autoPausedRows`, and a channel a PERSON paused is not a row. **Only the per-unit reachability re-check in the two lane runners is still open**, still deliberately deferred to the five-minute storage watch. Around them: `clips/` counted in `totalMediaBytes` (`7589c50c`) so the platter's bytes stop lying, clip-window eviction BY AGE (`4416bb58`, `e4f9fa1f`), the relocate space check freed from how much RAM is free (`1271e21f`), a fan-out that says which jobs it started (`94b24fdf`), the full-source path (`fab61a49`), and the umtool suite SKIPPING rather than going red without the song data (`1ab07623` → `007c12e5`). Anchors: [`FACTS.md`](FACTS.md#storage-debts-verified-2026-09-21-branch-storagedebts-1). ### `feat/filtered-channel-followups` — merged 2026-09-21 (`337477d5`) Nine commits, `f142f7d9` → `c442cb60`, behind the per-channel download filter. A rejection takes its own prefetch directory with it; the metadata scan is DISPATCHED by the download lane, not waited for, and goes in the pick log like any other unit; a filtered-out livestream gets a third answer rather than settling forever; `videoHasAnyArtifact` turned out to be `isVideoDownloaded` under a second name. Review (`5c02e7da`) found the discard was a deny-list and four things it let through. Anchors: [`FACTS.md`](FACTS.md#filtered-channel-follow-ups-verified-2026-09-21-branch-featfiltered-channel-followups). ### The release itself — what each branch landed **A · `tags/rules-and-ops` → `9837f066`.** `1a011d96` (merged alone, first) `channelsExclude`. `dcd065e7` a site layer carries NO rules — dropped on read AND on write, so a hand-written site rule is removed from the file on the next save. `d653b504` `--wait` survives a failed poll: the log route is POLLED (`{content, nextOffset, status, …}`), a poll failure is not a job failure, there is an `/api/jobs/active` fallback, probe failures are bounded, a `--wait-timeout`, and `followJob` is importable with injected `fetch`/`sleep`. `52f5c64c` both build routes take `siteId` OR `siteIds` via `reqSiteIds`, and `build-deploy` fans out returning `{ok, jobs, skipped, jobId?}`. `6214eeb9` `wt rm` finds the directory `wt add` made (`worktreeDirFor`, one function for both verbs). `729f64ef` a measure-nav comment. `bad9ea43` the four-lane status poll reads the corpus ONCE. `d0df901b` the `typeof process` guard in `contract.ts`. `94750a1b` review fixes — a bad site id is 400 *before* any job, and a recovered poll answering `queued`/`running` is not an outcome. **B · `export/tags-followups` → `cb254919`.** `e942dc7f` tag chip groups fold (`
` per group, local state, `"n selected"` on the summary). `ce5379f2` `SearchResults`' leaf sections read `SCOPE_LABELS` — one scope-label table, no second copy anywhere. `f40c4257` + `7a38b60b` hub `/ask` prerenders, and the fix is that `AskHub` needs the WHOLE provider stack (`PlayerProvider` → `MultiSiteDataProvider` → `SearchSessionProvider`), never an opt-out; `2e13c0ab` its spec, `export/e2e-hub/ask.spec.ts`. `befd2fbf` MCP: a tag filter takes posts OUT of the search, reported in `postsScanned.skippedForTagFilter` and in a footer sentence, because posts carry no curated tags and the export UI does the same. **C · `editor/debts` → `766e0873`.** `94c965d7` + `674c976b` the channel Storage panel evicts that channel's clip windows (`ClipWindowsCard` with a `slug`, disabled by the panel's `blockedReason`, by-age caveat on screen). `d23883f7` rename and delete refuse with `channelMediaBusyReason`. `1b3d4837` Sync all skips a channel whose media drive is not mounted. `1cace934` worker auth's 503-when-unset asserted. `1e0b10a9` `.gitignore` `/editor/content`. **S0-pause landed** — `98fa001a` the four legacy pause fields and their migration deleted (`migrateHeldToLanes` gone), `27b6be0f` `held` defaults per lane via `defaultHeldFor` (backfill `true`, the others `false`), `ad20ffd8` the editor readers gone, `7d2e889a` `settingsWrite.test.ts`. Review fixes `4d2fa0ca` + `70c2443a` (**every** `/api/test/*` route 404s unless `EDITOR_TEST_ROUTES=1`), `8ad4ea77` (the `held` default asserted through `sanitizeAutoQueue`), `67f17b30`, `307c9b81`. ### Gates measured for this release `tsc --noEmit` clean in `common`, `editor`, `export`, `mcp`. Unit: common **1604** (was 1592), mcp **219** (was 216), `pnpm run test:scripts` **151 passed + 1 deliberate LIVE skip** (was 138). Per-branch e2e: A `tags.spec.ts ops-api.spec.ts` **24/24**; B export **188/188**, hub **6/6**, `e2e:2origin` **3/3**; C 20 specs **184/184**. **`e2e:2origin` is GREEN for the first time since hub `/ask` existed.** The FACTS entry calling it known-red on the base is closed — a red run there is a regression now, not a known failure to skip past. ### Housekeeping Worktrees `storage-locations-s0..s4` and `s0-pause` removed. Branches deleted: `storage/locations-s0..s4`, `storage/debts-1`, `feat/filtered-channel-followups`, `one-core/phase-1`, the seven `umtool/*`, `one-core/s0-pause`. **Kept:** `parked/download-filter-preview`, `p1-e2e`, `worktree-duplicates-page`, `diet-series`. The three release worktrees and branches (`tags/rules-and-ops`, `export/tags-followups`, `editor/debts`) are gone — checked 2026-09-23, neither `git worktree list` nor `git branch` shows them. Nothing unmerged was deleted. ### Post-release operator steps — done 2026-09-22 evening except the deploy Run in the order the live editor forced (its pre-release sanitizer would have dropped `channelsExclude`, so the restart came first): 1. **Rebuilt and restarted the editor and umtool on `472d5b59`** (`:3001`, `:3050`; both started with `pnpm run start -H 0.0.0.0` as before). Done. 2. **Re-sent each Eva tag definition** via `pnpm ops tags --file`, built from the live defs in `transcripts/tags.json` (presentation fields intact) with `rules[].channels` empty and `channelsExclude: ["elfpire-eva", "elfpire-eva-x"]`. On disk: `channels` absent, the exclude present, all **48 assignments** untouched. Done. 3. **`pnpm ops build-index --wait`** (job `01M36471VHTYX9V7DXYZY3R6B3`, 330 s): the log said `curated tags: rules 864eac5c (changed), examined 77224, re-derived 0`. Examined = the full walk the hash change forces; re-derived = records whose tags MOVED, and none did, because the corpus has 71 channels and the old 69-channel allow-list was already "everything but Eva's two". The exclude pays off when the next channel joins. Done. 4. **`pnpm ops build-site --json '{"siteId":"anilyzer"}' --wait`** (job `01M364J54CA4ENG1SXQV865GM7`): composed into `export/public` and exported to `export/out`; `tags.json` counts **96 / 282 / 56**, byte-identical to live. No new false positive to suppress. Done — **built, not deployed**. **2026-09-23 — preview deployments shipped (`ef4a9f6d`, branch `deploy/preview`, 9 commits, Sonnet-reviewed):** `preview: ""` on the new `deploy-site` route (deploy-only of `export/out`, which now refuses a bundle built for another site), on `build-deploy`, on `pnpm ops`, and as a "Deploy preview" control on the site's Publish tab. Cloudflare Pages makes any non-production branch a Preview environment at `https://..pages.dev`. Anilyzer's release build is up at **https://tags-exclude.anilyzer.pages.dev** (deployment `2fafe052`, Environment = Preview); production is untouched (last production deploy still `4ac8ced`). Its data is byte-identical to production (96/282/56) — what differs is the export UI of this release (folding tag-chip groups, leaf-section labels). Editor restarted on `ef4a9f6d`; umtool unchanged. See `DEPLOY_CLOUDFLARE.md` "Preview deployments". **Still owed: the production deploy** — `pnpm ops build-deploy --json '{"siteId":"anilyzer"}' --wait`, then `curl https://anilyzer.pages.dev/tags.json | jq '.tags[].count'` and `list_tags {source:"remote:https://anilyzer.pages.dev"}`. The operator chose to hold it. Other sites stay at corpus spec 3 until the operator chooses. **Out of scope, decided:** hub tag federation. A tag filter works per site; the hub does not aggregate `/tags.json` across members. **Previously:** 2026-09-20 — **storage locations S5 + S6 shipped** on branch `storage/locations-s5-s6` (worktree `/home/user/Projects/storage-locations-s5-s6`, off `main` @ `60183f0`; **merged since — the "unmerged" below is stale, and the three things it names as left out are down to one, see the 2026-09-22 block above**). The operator's two asks that evening — *"disk space and current storage volume as columns on the channels menu, and let me filter by volume"* and *"progress on relocate jobs since rsync gives progress"* — plus the four things around them: per-channel media bytes in the snapshot (`totalMediaBytes`), the corpus volume as a first-class `/storage` row (`internal`, synthetic), the saved-video store made movable (`relocateDir.ts` is the channel mover's factored core; `relocateSavedVideos` is new), and a five-minute drive watch that auto-pauses a channel whose drive went away and restores its tier when it comes back (`ChannelPriorityEntry.autoPaused`). Slice record and what was deliberately left out: [`storage-locations.md`](storage-locations.md#s6--the-storage-surfaces-the-operator-asked-for-shipped-2026-09-20). Seams: [`FACTS.md`](FACTS.md#storage-locations-s5s6-verified-2026-09-20-branch-storagelocations-s5-s6). **Left out, named:** `assertRelocationRootPresent` (a move can still `mkdir` under an absent mount), a `/review` section for auto-paused channels, and the per-unit reachability re-check in the two lane runners. **Previously:** 2026-09-15 (Phase 3 slice 1 and the rack landed; see the Next block) — 2026-09-13: **gate A passed and `main` moved; the interlude shipments and one-core Phase 2 are all merged on one branch**, `integrate/2026-09-storage-priority`, tip **`bd3d4ec`**, unmerged. Off `e74f005`: `relocate-channel-media` (from `storage/relocate-media`), then `channel-priority` (from `channel-priority/s5`), then Phase 2's five slices in the order their reviews cleared — `7f86aef` S1, `b7a351c` S2b, `858aabc` S2c, `9026007` S3, `bd3d4ec` S2a. The branch waits on **operator gate A**, then a fast-forward to `main`. The two resolutions that are not a union of both sides — `listChannelMeta`'s return shape and the pair of bulk bars at the foot of `ChannelsTable` — are pinned in [`FACTS.md`](FACTS.md#the-storage--priority-integration-verified-2026-09-12--where-the-two-branches-collide); Phase 2's seams are in [`FACTS.md`](FACTS.md#one-core-phase-2-verified-2026-09-12-branch-integrate2026-09-storage-priority) and the slice-level record is [`one-core-phase-2.md`](one-core-phase-2.md#phase-2--shipped-2026-09-12). **The release-candidate gates on `bd3d4ec`, measured 2026-09-12 from the integration worktree (`/home/user/Projects/integrate-2026-09`, worktree #8 — editor 3801, test 3811, export 3810, export-e2e 3820), e2e behind the machine-global queue lock, one worker:** `tsc --noEmit` clean in all 7 workspace packages; `common` **1159/1159**; `mcp` **205/205**; `pnpm test:scripts` **78 passed / 1 skipped**; editor and export `next build` clean; the compose-site fixture byte-identical except `generatedAt` and the four `_headers` lines S2c's CORS change added, with the composed hub's `_headers` byte-identical to the pre-S2c literal; export e2e **172/172**, hub **5/5**, the editor suite **533/533** in 23.1 min with neither known flake firing, and umtool **129 passed / 40 failed / 2 skipped** — the same 40 environmental failures S2b measured, spec for spec; and `curl https://jeralyzer.pages.dev/corpus.json` byte-identical to `plans/tools/jeralyzer-corpus-2026-09-12.json` (12,380 bytes) — which says the snapshot is still current, not that a rebuild would match, because jeralyzer has not been rebuilt. `e2e:2origin` was not run: it shells `build:hub`, whose `/ask` prerender is red on the base and predates Phase 2. **The operator runbook below is unchanged and still the sequence to follow** — the channel-priority migration first, with the editor stopped, then the platter mount, then the saved-video store, then the channels. Nothing in Phase 2 touches it. **Previously:** 2026-09-08 — **one-core Phase 1 shipped** on branch `one-core/phase-1` (`7f294df` → `81a663f` plus a docs commit, 36 commits, not merged): **dispatch is one scheduler, and the lane is the noun.** The slice-level record — every sha range, every divergence, both operator gates — is [`one-core-phase-1.md`](one-core-phase-1.md); the umbrella is [`one-core.md`](one-core.md) and the anchors are [`FACTS.md`](FACTS.md#one-core-phase-1-verified-2026-09-08). **No live number moved**: the before/after diff of `plans/tools/phase1-numbers.ts` over the real 78,000-video corpus is EMPTY for 1.1, 1.2, 1.4 and 1.5, and for 1.3 is 18 added lines / 0 removed — the two sweeps' scopes migrating into lane trees, both `enabled: false`. - **1.1 — four lanes in the model.** `AutoQueueKind` is `transcription | download | digest | backfill`, exported as `LANES`, and `PauseLane` is an alias of it. `retainLeaves` was DELETED rather than de-moded: `pending[leaf]` is what the leaf drew, so a post-hoc filter over it can only re-apply a rule the draw already applied. The rule moved into the draw. - **1.2 — the runner runs operations.** `digestBatch.ts` (647) + `backfillBatch.ts` (937) → one `controller/operationBatch.ts`. The status poll costs ~150 ms more for the two new lanes and a `(mtime, size)` parse memo gives ~140 ms of it back. Three runner-only bugs review found (a spend cap a 60 s TTL could reset, a context swapped under an in-flight unit, a dead engine re-probed every tick) and one the e2e spec found: **every runner held its own copy of `.auto-queue/state.json`, which is serialized whole**, so each persist clobbered the other lanes' pick log. They share one object now. - **1.3 — the sweeps and the arbiter retire.** Arming is tree authoring; ten settings fields migrate on read (`common/lib/laneMigration.ts`, a pure function tested through the sanitizer) and are then deleted with their sanitizers and forms. `reach` and `digest.recencyOrder` are RETIRED, not migrated, and the note says why. The one rendered change in the whole phase is here and was kept on purpose: the /jobs strip's four lane rows fold `gateHeld` now, so a held transcription row reads *Holding* where it read *Idle*. - **1.4 — pause is lane state.** One key, `autoQueue[lane].held`, behind the unchanged `isGateHeld` / `withGateHeld`. The four legacy fields survive as `legacyGateHeld`'s read-time input in `lib/laneMigration.ts` — NOT in `pauseGates.ts`, which would have pulled the operation registry into every reader of settings.json. The transcription intent-vs-pool asymmetry is intact. - **1.5 — one work list per lane in the snapshot.** `backfill.download` and `backfill.transcription` are the fold of each lane's default buckets, so `snapshot.backfill[op].ids` is where all four lanes' work lives; the runner reads it and falls back to the same fold, which is the migration (nothing is regenerated). `plans/tools/phase1-worklist-check.ts` checks all 68 live snapshots read-only: **OK**, download 9, transcription 882, zero already carrying an entry. The 882 is the POLICY-FREE work list; the transcription lane still draws **2,754** today, because the live `autoQueue.transcription.replaceAutoSubs` is `true` and `defaultDrawsForPolicy` appends `downloadedAutoSubsOnly` (1,872) at the tail of the draw. **Two operator gates are outstanding, and one of them gates a merge.** - **Gate A** — the digest lane's runner drives a production pass and comes back after a restart. Run it from **`one-core/phase-1-gate-a`** (`0438a72`), which is the code as 1.2 left it; the step-by-step runbook, including the exact `settings.json` block and the channel to point it at, is in `one-core-phase-1.md`'s "Operator gate A" section. It gates the MERGE of 1.3 onward, not their authorship, which is why the branch exists. - ~~**Gate B** — before the backfill lane is ever `enabled` in production, decide `backfill.allowRedownload` and the channel scope. As configured today it would re-fetch audio for ~66,540 videos. The migration left the lane disabled; the tree's channel leaves are the scope from now on.~~ — **DONE 2026-09-23: reachable media only** (`allowRedownload: false`, lane held, still disabled). Record: [`one-core-phase-1.md`](one-core-phase-1.md#gate-b--passed-2026-09-23). - ~~**The four legacy pause fields go only once the live `settings.json` carries all four `held` keys**~~ — **DONE 2026-09-22 (S0-pause, in `editor/debts`).** The four fields (`transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused`, and the inverted `backfill.enabled`) are deleted from `SiteSettings`, from every sanitizer and from `writeSettings`' merge literal, so a file that still spells one loses it on the next write; `legacyGateHeld` and `migrateHeldToLanes` are gone. The warning this bullet carried was heeded: `defaultHeldFor(lane)` is `lane === "backfill"`, so **backfill defaults `true`**, and the invariant is asserted where a reader actually hits it — through `sanitizeAutoQueue` in `common/jobs/autoQueuePolicy.test.ts`, not through a unit test of the default itself. The backfill lane is now off twice over on a fresh install, unarmed AND held, which is gate B kept as two deliberate acts. **All four e2e flakes are fixed** (`plans/deflake-e2e.md`, `a32612a` → `fae99f1`), and the first three's class is worth remembering: **a debounced write survives a test boundary until the next reset.** `video-page.spec.ts:216` ("Delete directory wrong-id confirmation") asserted a video directory still existed after a refused delete and found it gone — nothing deleted it, `reconcileVideoDirs` RENAMED it. Snapshot generation moves `data/` to `data/` when the two differ, and the `one-youtube-channel-with-data` fixture was the only directory in the tree where they did; the previous test armed the debounced regen, and `resetData` copied the fixture back in BEFORE it invalidated, so the regen renamed the fresh copy. The fixture's id now matches its directory and `resetData` quiesces first (that route's `resetSnapshotScheduler()` clears the armed timer; a regen already past the debounce is not awaited and keeps walking, which is why the fixture fix is the half that actually closes it), so the two halves of the hazard are both closed. The export "no FOUC" theme tests read `data-theme` after a `waitUntil: "commit"` reload that does not promise the head script has run — the script now marks itself `data-theme-ready` and they wait for it. And ask-chat's "Stop aborts mid-sweep" raced a 500 ms route timer; the test holds batch 2 open on a latch it releases itself. No retries, `test.slow`, or serial markers were added anywhere. **The fourth is fixed too, and it was never the batch** (`fae99f1`, [`attribution-batch-hang.md`](attribution-batch-hang.md)). `attribution.spec.ts:346` "Run from the video page runs that video and no other" failed 1-2 runs in 10 waiting the full 90 s for the batch's "1 done". Instrumented at `fea2996` — `console.error` with ISO timestamps, never through the job's own `onLog` — it reproduced 3 times in 25, and every failure reads the same: ``` [TRACE 2026-09-08T20:25:21.661Z] attributeOne.post-write attrvid0002 [TRACE 2026-09-08T20:25:21.661Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=4 closed=false :: Attribute attrvid0002 (text-only): 2 speaker(s)… [TRACE 2026-09-08T20:25:21.662Z] runPool returned attribution-channel [TRACE 2026-09-08T20:25:21.662Z] onLog job=01M21B4PDQZC6YWGZH38WYV37N n=5 closed=false :: Backfill attribution-channel: 1 done, 0 already current… [TRACE 2026-09-08T20:25:21.663Z] fn.finally job=01M21B4PDQZC6YWGZH38WYV37N status=done Error: locator.getAttribute: Test timeout of 120000ms exceeded. - waiting for getByLabel('Run Speaker names (from the transcript) output') ``` The job finished in **65 ms**, wrote all five lines including "1 done", enqueued every one of them with `closed=false`, and finalized `done`. **All five hypotheses the plan ranked are dead, H1's mechanism included** — no `stream.cancel` fired in 240 instrumented tests, so the early SSE teardown H1 described (and the log-file polling it prescribed) was never happening; all that held of H1 was its disposition, server innocent and failure client-side. The real cause is a sixth mechanism outside the table: the log panel was **gone** — not stale, not short, absent. The failure snapshot shows the record body and the Run button and no `
` at all.
`AttributionBody` returned two fragments of different SHAPES (`[p, RunOne]` with nothing on
disk, `[dl, ul, p, RunOne]` with a record; `DiarizationBody` the same at `[dl, p, RunOne]`), so
`reconcileChildrenArray`'s index pass broke at 0 and found `ul`/`p` where the UNKEYED `RunOne`
had been — a positional type mismatch, which is what unmounts a child and takes
`StreamActionLog`'s `log` state with it. React preserves state across a KEYED move, so a key
alone or a fixed position alone would each have sufficed; the fix ships both. The
refresh that lands the record is the one `StreamActionLog` fires itself when the run ends, so
the panel is wiped at the instant the summary line reaches it, and the spec could only pass in
the ~200 ms window before that. **The earlier note here — "the job never reaches a terminal
state", "the hang is in the BATCH" — was the artifact, and so was "the log FILE has no more
than the panel": the file had everything, and no reconcile could have helped a component whose
state had been destroyed** — nor would the plan's own H1 fix have helped, for the same reason.
Do not trust either claim again. Both bodies are one two-child list now, RunOne last and keyed;
the spec asserts the log still says "1 done" AFTER the pill reads "current", which is red on
every run at `fea2996` (5/5) and green at the fix. `attribution.spec.ts --repeat-each 20`:
**20/20**. The durable rule — **a component carrying a `StreamActionLog` must not be UNMOUNTED
when the record its run produced lands**, whether by an unkeyed move or by a parent branch
swap — is in `FACTS.md`.

**Those instances are now closed, and a fourth with them** (`c19098b`, follow-ups in
`12d1778`; the specs that catch them in `b207e84`). `SourceVideoSection` early-returned the
persisted view once the pointer landed; `IncompleteTranscriptBanner` and `ShortAudioBanner`
were dropped by the parent when the run cleared their condition; and `PerFileTranscribeRow`
swapped its panel for "Transcribe disabled — transcript already present." the moment its own
run wrote transcript.json. All four are the same shape now: message and run panel in separate
fixed slots, a `ranHere` flag set when a run starts there keeping the panel mounted across the
refresh, and `StreamActionLog`'s existing `disabled` prop fed the now-cleared condition so a
kept panel is not a second Run button. The banners' `role="alert"` and warning aria-label are
conditional on the flag, so the box that outlives the warning does not keep announcing it.
`PerFileTranscodeRow` was checked and is **not** an instance: its condition is the file's
extension against a static format list, which a transcode cannot change. Each fix has an e2e
case that reads the log's closing line only AFTER the state the run produced is on screen; all
four are red without the fix and green with it, and `--repeat-each 10` over the four specs is
340/340.

**The last full suite: 518 passed, 0 failed of 518, 31.8 min** from a worktree of `12d1778`
(one worker behind the queue lock). Previously **515/515 in 24.6 min at `65abc11`**, the
de-flake tip; the three added cases are the difference. The four flakes are closed and the
suite is clean end to end. Previously, at `2133d94`, it was 514/1 — slice 1.5's own new band
assertion, and it was right: writing the two new snapshot entries doubled `/channels`' Download
and Transcribe coverage, because that page passes the external ids into `buildOperationBands`
and `addRegistryEntry` folded them on top of `addExternalBands`. Fixed in `d8754d2`.

**2026-09-11 — [`relocate-channel-media.md`](relocate-channel-media.md) SHIPPED**, all three
slices, on branch `storage/relocate-media` (`4059dad` → `affe525`, 24 commits off `61eae05`,
**unmerged**; the suite is **523/523 in 22.3 min** at `affe525`, and
`common` is 971/971). A channel's `data/` can be an absolute symlink to
another drive with `config.dataDir` recording the target, moved by a Storage panel on the
channel page or in bulk from `/channels`; four guards plus a `needsMedia` flag on the job kind
stand between an unmounted drive and a re-download, and the low-disk gate now measures the
volume the bytes are going to and latches per volume. **Nothing on disk moved** — the mechanism
shipped, the bytes did not. The slice table, the two review rounds and eight divergences are in
that plan's "As shipped (2026-09-11)"; the anchors are in
[`FACTS.md`](FACTS.md#one-core-phase-1-verified-2026-09-08).

**2026-09-11 — channel priority shipped** on branch `channel-priority/s5`
(`28bfee3` → the tip below, four merge commits plus eight of its own, **not merged**):
**one tier per channel, one focus, four COMPILED lane trees.** S0 (`28bfee3`) is the model;
S1 (`2710195`) dispatch; S2 (`76c5784`) sync; S3 (`88da736`) the `/channels` UI and the one
writer; S4 (`9fcc8b4`) the banner and the read-only tree; S5 merged all four in order (no
conflicts — the slices touched disjoint files), closed the twelve parked review findings, ran
the migration's dry run against the live corpus and deleted `excludeFromSync`. Anchors:
[`FACTS.md`](FACTS.md#channel-priority-verified-2026-09-11); the record with every divergence
is [`channel-priority.md`](channel-priority.md)'s "as shipped".

- **The model compiles, it is not consulted.** `settings.channelPriority` → `prio-focus` >
  `prio-normal` > `prio-low` > `prio-all`, all strict, per lane. No dispatch code changed: a
  focus holds the rest because strict descent already does, re-asked on every grant. An absent
  document is today's behaviour byte for byte.
- **The data-loss find.** `backfillReacquire.decideKeep` asked `policyDrawsBucket` on the
  transcription root — on a COMPILED root a channel paused for transcription has no leaf, so
  it would have answered "no-leaf" and unlinked the re-acquired audio of exactly the channels
  an operator had just put on hold. The pause is asked first and KEEPS. A hold is never a stop.
- **The order that nearly went.** The bypass is all-or-nothing, so the first tier click on
  `/channels` stops the stored trees being dispatched from — and the two hand-made 9+9 lane
  orders live only there. The one writer now seeds from `channelPriorityFromLegacy` when the
  stored document says nothing and the stored roots are not already compiled. And a corpus
  whose roots ARE compiled recompiles even for a default document, or ending a focus would
  leave `prio-focus` at the head of a tree the runner is bypassing to.
- **One writer of `root` too.** `saveAutoQueueAction` and `armLaneAction`-with-a-scope refuse
  server-side while a model exists; `prioritizeChannelDownloadAction` is a priority edit;
  create/delete recompile.
- **`excludeFromSync` is deleted** — field, sanitizer clause, action, toggle, and all five
  legacy reads. `excludeFromBuild`/`excludeFromCleanup` untouched.

**The full editor suite at `a8e908a`: 528 passed, 0 failed of 528, 22.9 min** from this
worktree behind the queue lock (518 + the 10 new priority cases). Three specs went red on
the way and all three were right: `channel-priority.spec.ts:172` caught End focus leaving a
stale `prio-focus` at the head of the stored tree, and `new-channel-onboarding.spec.ts:111`
was still reading "Add to top of auto-queue" as a hand-written leaf.

**Two flakes cost a suite run and are named so the next session does not re-chase them.** An
earlier run at `a8e908a` was 526/528: `no-subs-fallback.spec.ts:114` failed on an `EEXIST`
inside `resetData`'s `cp` (a filesystem race in the harness, 3/3 green on re-run), and
`backfill.spec.ts:457` failed on `uncheck()` not changing a controlled checkbox — the
`PolicyTreeEditor` adopt-effect race, where the 3 s status poll calls `setForm` between the
click and its assertion. Measured after: 1 failure in 6 runs of that case at `a8e908a`, 0 in
9 at `4d256a2`, and the clean 528/528 above. Its payload path for a DEFAULT priority
document is unchanged by this branch, so it reads as the known adopt race rather than a
regression — but the sample is small, and it is the case to look at first if it recurs.

**⚠ THE MIGRATION MUST RUN BEFORE THE FIRST BOOT OF THIS CODE, not after it.**
`excludeFromSync` is deleted and `parseChannelConfig` drops the key, so on a corpus that has
not been migrated the 15 channels that carried it read as *saying nothing about sync* — and
four places act on that the moment the editor starts: `syncScheduler.ts:132` (selection),
`:259` (`autoSyncEligible`, so the projection changes on sight),
`channels/actions.ts:524` (*Sync all*) and `channelGroupSections.ts:140` (a group's Sync).
The automatic tick is the one thing that does NOT fire on the live box — its
`heartbeatSeconds` is 0 — but *Sync all* or a group Sync in that window sweeps every channel
the operator had excluded. Nothing else moves: no lane's membership, no download, no
transcription. **Order: stop the editor → run the migration → start it.** The script takes
its own timestamped `settings.json.pre-priority-` backup beside the file before its
first write (refusing to overwrite an existing one), so the backup is not a step anyone can
skip.

**IT HAS NOT BEEN RUN — that is the operator's step at rollout.**
`common/bin/migrate-channel-priority.ts --dry-run` against a READ-ONLY copy of the live
`settings.json` (no config.json or settings.json mtime moved) says: **68 channels, 28
entries, 14 ranked, 15 pinned `sync=paused`, no focus, every base tier `normal`**, and all
four compiled roots `prio-normal(68) > prio-all`. The ranked order is
`quartering-live, the-quartering-rumble, the-quartering, HasanAbiVODs3, nuxanor, hasanabi,
darlingstrawb, rekietalaw-rumble, chibi-reviews, nux-taku, destiny, omnivods-odysee,
leaflit-rumble, piratesoftware` — dense 0–13, merged index then the transcription lane's
order then slug. No lane's membership moves; only `sync` loses anyone.

**The rollout is the operator's, and the two shipments share one sequence. The migration
goes first, because it is the only step that must happen before this code ever boots.**

1. **Run the channel-priority migration, with the editor stopped.** `excludeFromSync` is a
   deleted field, so an unmigrated corpus reads its 15 channels as saying nothing about sync
   the moment the editor starts. Order: **stop the editor →
   `pnpm -C common exec tsx bin/migrate-channel-priority.ts` → start it again.** Run
   `--dry-run` first; the real run takes its own timestamped
   `settings.json.pre-priority-` backup beside the file, and is a no-op on a second run.
   Never run it against a live editor: a running one holds settings in memory and writes them
   back on its own schedule.
2. **Mount the platter.** `sdb1` (1.8 T, ext4) at `/mnt/platter` with `nofail` in fstab;
   create `/mnt/platter/archilyzer-media` and `/mnt/platter/archilyzer-saved-videos`, owned by
   `user`. Nothing below works before this, and no code needed it.
3. **The saved-video store, by hand** — the relocate plan's runbook: rsync
   `transcripts/saved-videos/` out, verify with `--dry-run --itemize-changes`, move the
   original aside, symlink the store root, restart, then delete the original. **That is the
   step that frees the 130 GB**, it needs no code at all (every pointer carries an absolute
   `dir` and nothing walks the store root), and it goes before the channel moves because a
   100 %-full `/home` is a hazard to every unrelated writer while they run.
4. **Channels through the UI, largest deprioritized first.** Storage panel per channel, or tick
   rows on `/channels` and use the bulk bar; `omnimirror` (130.3 GB) is the obvious first move.
   Sizes are in the relocate plan's table — they are sizes, not priorities.

**The gates, all run on the merged tip from the integration worktree
(`/home/user/Projects/integrate-2026-09`, worktree #8 — editor 3801, test 3811, export 3810):**
`tsc --noEmit` clean in all six packages; `common` **1051/1051**; `mcp` **205/205**;
`pnpm test:scripts` **71 passed / 1 skipped**; `next build` clean; the editor suite
**533 passed, 0 failed of 533, 24.2 min** behind the queue lock, one worker. No fix commit was
needed — the six conflicts were the whole of the work.

**Both suite totals are the sum of the branches', which is how we know nothing was dropped in
the conflicts.** `common` is 908 at `e74f005` + 63 relocate (971 at `bc62248`) + 80 priority
(988 at `996f182`) = **1051**. The editor suite is 518 at `12d1778` + 5 relocate (523) + 10
priority (528) = **533**. Neither known flake reappeared: `backfill.spec.ts:457` (the
`PolicyTreeEditor` adopt race) was green on its first run, and so was every video-page case.

**The merge review (2026-09-12, on `c7f7b90`) found nothing above low.** Every conflicted
file reproduces both parents' deltas line for line, and the six auto-merged files are exact
unions; the sanitizer carries both branches' new keys on read and write. Three notes, none a
blocker: (1) with rows ticked on `/channels`, BOTH bulk bars render — storage first, then
priority — each announcing its own count and Clear, sharing one selection; no e2e on either
branch could see the pair. (2) The relocate plan's "Step 0 / Step 1" labels are stale
against the order above — the store rsync targets the platter, so the mount comes first; the
runbook above is the one to follow. (3) `syncAllChannelsAction` skips paused channels but
not unreachable ones (`channels/actions.ts:536-543`); an unmounted channel is refused inside
`syncAction` by the `needsMedia` guard and surfaces as a failed sync, not a skip reason.
Pre-existing on the relocate branch.

**Gate A PASSED 2026-09-13** on the production editor (record in
[`one-core-phase-1.md`](one-core-phase-1.md#gate-a--passed-2026-09-13)), and **`main` is
fast-forwarded to `241d357`** — the integration, Phase 2 and the gate record. Every merged
branch and worktree is deleted; what remains is `one-core/phase-1` (the primary checkout,
until it switches to `main`), `one-core/s0-pause` (parked for the next release),
`diet-series`, `p1-e2e` and `worktree-duplicates-page`.

**The `/channels` rack shipped 2026-09-14** and `main` is fast-forwarded to **`1bc8765`**
(`64b5573` → `1bc8765`, forked from `a1058d0`; tsc clean, common 1159, editor e2e **533/533**
on the identical tree — the fast-forward got a `tsc --noEmit` smoke, not a second 25-minute
suite). One deck, a sticky rack and a meter bridge; the record is
[`editor-channels-rack.md`](editor-channels-rack.md). The worktree and branch are deleted.
It merged BEFORE the rollout's channel moves on purpose: those go through the `/channels`
bulk deck, which the rack rewrote, so the operator does them on the merged page. **One-core Phase 3 slice 1 SHIPPED 2026-09-15** — `main` `1faa987` → **`838da4a`**, fifteen
code commits in four fast-forwarded sub-slices (A serial, B ∥ C, D serial), record in
[`one-core-phase-3.md`](one-core-phase-3.md#slice-1--shipped-2026-09-15). Every payload the
editor draws is a pure function in `common/views/` handed its singletons, readings and
clock; the layer guard proves it (allow-list **11 → 10**, two new tests, a textual ban on
getters, readers and `Date.now`); common **1264/1264**, the full editor suite **533/533**.
Nothing rendered, routed or serialized moved; the one live change is `/operations` reading
`.auto-queue/state.json` once per poll instead of four times. **Next, in parallel:** the
rollout below (operator-physical), then S0-pause one boot later, then **Phase 3 slice 2**
(unplanned; `one-core.md` §Phase 3 — the plan file's "What slice 2 inherits" is the start). Phase 3 slice 3 rewrites `ChannelsTable.tsx` again;
landing the rack first keeps that a rebase-free rewrite of one file.

**The rollout:** the **O2–O9 rollout**, editor stopped. **The migration script only exists on
`main`**, so the checkout switches first; the rule is "migrate before the first BOOT", not
before the switch. Done 2026-09-13: `git switch main`, then the migration for real — 67
channels (one fewer than the 68 planned), 27 entries, 14 ranked, 14 `sync=paused`, no focus,
all `normal`, backup `settings.json.pre-priority-2026-09-13T14-34-26-361Z`. Still to do:
`pnpm install --frozen-lockfile`, the platter mount and the saved-video store rsync,
`pnpm --filter editor exec next build`, start with `-H 0.0.0.0`, the channel moves through
the UI, then `git branch -d one-core/phase-1`.

**Then S0-pause lands, in the release AFTER this one.** The branch `one-core/s0-pause`
(`2aeb358`) is ready and reviewed — it deletes `transcriptionsPaused`, `downloadsPaused`,
`digest.digestsPaused`, `backfill.enabled` and `legacyGateHeld`. It is held back for a
sequencing reason, not a quality one: a main-era `settings.json` has never carried
`autoQueue..held`, that key first appears when merged code writes settings back, and the
same release cannot both introduce the writer and delete the read-time migration that covers
every file written before it. An install taking it straight from an older release loses its
pauses — transcription, downloads and digests come up running and the backfill lane comes up
held. Boot this release once, confirm all four `held` keys are in the live `settings.json`,
then take it.

**Then the rest of one-core Phase 3** — views in the core, editor as shell, four slices
([`one-core.md`](one-core.md) §Phase 3); slice 1 shipped (above), **slices 2 and 4a shipped
2026-09-23** (see the head of this file), slices 3 and 4b are the next release. Read `common/architecture.test.ts`'s allow-list
first: it is the shortest accurate statement of what is still tangled. Phase 2 left it at
**eleven entries, byte-identical to the base**, while ADDING `"components"` to
`FORBIDDEN.lib` — the forbidden list grew and the debt ledger did not.

**Previously:** 2026-09-07 — **one-core Phase 0 shipped** on branch `one-core/phase-0`
(`df5eb48` → `1691c4f`, six commits, not merged): **guardrails and dead weight**, every item a
deletion, a move or a guard. `common/architecture.test.ts` is the layering (back-edges 16 → 12,
the list can only shrink); `scripts/report-to-video` → `umtool/report-to-video`;
`common/package.json` has an `exports` map; one `pageFileName` and one `CONTRACT`. **Nothing on
disk in a corpus changed and nothing on the wire changed** — proved with a fixture corpus
composed through `build-index` + `compose-site` before and after, identical apart from
`generatedAt`. Detail: "Phase 0, as shipped" in [`one-core.md`](one-core.md); anchors in
[`FACTS.md`](FACTS.md#one-core-phase-0-verified-2026-09-07).

**Previously:** 2026-08-31 — **the transcode operation is removed** (`904f1a9` → `f717a36`): it never fired in production. The census over 68 channels found the `untranscoded` bucket empty in every one, both `failed-transcodings` files 0 bytes, and only four channels even passing the stage's gate — and `resolveAudioFile` falls back to any real audio file anyway, so transcription never needed it. Gone: four controllers, four job kinds, the channel stage and station, the catalog entry and with it `appliesTo`/`operationApplies` (catalog 8 → 7). Kept: `transcodeAudio` and the download path, the per-file *Transcode …* rows, both wrong-format sweeps — whose bucket is `wrongFormatAudio` now. The slice-5 leftovers rode first (`measure-nav.mjs`, `BUILD_KINDS`, `EditorAliasesClient`). Plan: [`editor-transcode-removed.md`](editor-transcode-removed.md).

**Previously:** 2026-08-30 — **editor IA slice 5 shipped** (`140212a` → `df9b839`):
**a site has tabs, and the family has one page.** Charts, Search aliases, Deploy, Build and
Homepage were five sidebar entries beside *Sites*, three reading the site from a `?site=`
param the picker had to seed, one about no site at all, one about the family's own hub.
`/sites/` is now **Settings · Charts · Search aliases · Publish** — a layout, the site
named in the PATH, and the picker reading that path through a new pure `siteIdFromPathname()`
so the two can never disagree (it writes it to storage, so Dashboard and Channels follow).
`/sites` is the family page: the list, *Release notes*, *Build all sites* with the
Basic/Docker mode, the *Hub*, and the *Pool* — the corpus-wide jobs — under a disclosure.
The five routes 307, and a `?site=` bookmark rides a `has` rule to that site's tab.
`build/` and `deploy/` — already one module in two directories — are `sites/lib` +
`sites/components`. The nav is **eleven**, the IA doc's end state, held by `nav.test.ts`; the
IA doc's UI slices are complete. **Nothing on disk changes.** See "Slice 5, as shipped" in
`editor-operations-ia.md`.
Previously: 2026-08-30 — **editor IA slice 8c shipped** (`2226680` → `21feed3`):
**`/jobs` is one list, and slice 8 is complete.** The design question #12 asked first — what
is a ROW — is answered *a row is one job*, whichever of three places knows about it: a registry
record, a `.log` + sidecar the registry has forgotten, or a scheduler slot whose record was
evicted (a phantom). One `JobRowView`, one builder, one adapter per source, merged VIEWS by id
with the live row winning. The page is one `` with no mode: the live head (running,
queued in queue order, anything that finished in the last half-minute) polled at 1 s while any
row is non-terminal, the paged history below it on the global pulse. **The scheduler's drift
check rides the live payload**, so every surface that draws work — `/jobs`, the dashboard, the
widget, `/api/jobs/active` — frees a running slot whose record is terminal or gone, after
drawing it once; `/api/pulse` does not build it and must not. `/jobs/active` and `/jobs/queue`
307; the sidebar's *Active* entry and its running badge are gone. The five pages that
hand-rolled a six-field copy of a running job now call `liveJobRows`, so their cards show the
progress bars they used to drop. **Nothing on disk changes.** See "Slice 8c, as shipped" in
`editor-operations-ia.md`.
Previously: 2026-08-29 — **editor IA slice 8a+8b shipped** (`ea04b4e` → `fecac0f`):
**sync is a catalogued operation and the schedule is its page.** `OperationDescriptor` gains
`scope` ("video" | "channel") and `trigger` ("backlog" | "cadence"), `SYNC_OPERATION` is first
in `operationCatalog()`, and `/operations/sync` — today's `/scheduler`, which 307s — is the
per-channel cadence table with the **whole `syncScheduler` block** below it. Two things the IA
doc's wording could not buy: `runner` is typed `AutoQueueKind` and the heartbeat is not one, so
the console is chosen off `trigger`; and a band's populations are videos, so the rail row is a
composed `SyncRailRow` rather than a hollow band. `saveSchedulerSettingsAction` is the block's
one writer (it had two), `WorkersField` and a new `WorkersConfigForm` move to `/workers` with
`saveWorkersAction` as *its* one writer, and the worker-tag vocabulary is the first consumer to
filter on `scope === "video"`. The four lane-note strings are one `sweepLaneNote` beside
`deriveLaneState` — they were two copies and a paraphrase, and nothing pinned them.
The two Storage chores that ride the heartbeat (keep-latest check, saved-video backup) are
labelled as such everywhere they show; **no tick logic, no settings key and nothing on disk
changed**. The `/jobs` fold — the last third of slice 8 — is its own plan. See "Slice 8a+8b, as
shipped" in `editor-operations-ia.md`.
Previously: 2026-08-28 — **editor IA slice 6 shipped** (`6986efc` → `d5ca90a`): the
video page shows one panel per registry operation. `common/controller/videoOperations.ts` is
the reader — one `readVideoFiles`, one `resolveTarget` and one `state()` per entry, never
re-derived — and `digestSectionStates` in `lib/digest.ts` is now the ONE per-section fold the
digest operation's `state()` and the panel both count through, so the page's duplicate of it
is gone. `DigestPanel` is the digest's BODY inside a generic `OperationPanel`; diarization and
both attribution operations get bodies of their own, each with a **Run** that runs that one
video on the speaker lane (`ids` now threads through `runBackfillChannelJob` →
`countBackfillWork` → the batch, and rides in `spec.params` so a replay stays scoped). A
record whose feature is switched off is still shown, marked *switched off*. `hasDigest` was
the channel COUNT's helper, not the page's — it is now private to `channels.ts`;
`hasAttribution` was dead and is deleted; `hasDiarization` is the live cleanup guard and
stays. Every digest aria-label is byte-identical; the heading is "Digest" rather than "AI
digest"; nothing on disk changed. See "Slice 6, as shipped" in `editor-operations-ia.md`.
Previously: 2026-08-28 — **editor IA slice 7 shipped** (`c924f73` → `685cba3`): one
pause. `common/lib/pauseGates.ts` is the only place the four gates' polarity is known
(`backfill.enabled` is inverted), `pauseLaneAction` / `resumeLaneAction` replace eight
actions in two files, and `components/lanes/pauseControl.tsx` is the one control every lane
surface draws — the dashboard, the widget, `/workers`, `/jobs/active`, both sweep panels and,
new, the runner pages, where it sits beside Start/Drain/Stop. The gate is keyed by LANE, not
by operation: three speaker operations share one backfill queue and therefore one pause. A
held runner lane can read *Holding* on the operations rail for the first time, and
`/operations/transcription` says "transcriptions are paused" rather than "no enabled worker
to run it" (`pauseAll` disables every worker, so the runner could not tell the two apart).
Every aria-label is byte-identical; nothing on disk changed; the widget wire fields
`digest.paused` and `backfill.enabled` are both `held` now. See "Slice 7, as shipped" in
`editor-operations-ia.md`.
Previously: 2026-08-28 — **editor IA slice 4 shipped** (`a623958` → `43c4e26`, docs
`808e3fc`, e2e follow-up `97ee12a`): `/actionable` is gone and redirects to `/operations`. Its per-operation
sections are the channel-work tables on `/operations/download|transcription|digest`, report
freshness and the two refresh controls are on `/channels`, the two cleanup tables are on
`/cleanup`, and duplicate clusters plus media-integrity findings are a new `/review` under
Corpus. The nav is twelve. The dashboard's "No digest" is "Digest to do" — a rename, not a
re-sourcing: `actionableNoDigestCount` already returned the digest band's `reachable`, so no
number on any screen moved. The widget wire field is `digestReachable`. Nothing on disk
changed. See "Slice 4, as shipped" in `editor-operations-ia.md`.
Previously: 2026-08-28 — **the re-acquire hand-off shipped** (`e450c2c`, `f661677`,
`bec4775`): re-acquired audio on a subtitle channel is no longer
deleted under a transcription that the auto-queue policy would have started on it. It is
handed over when `autoQueue.transcription` would draw the video from
`downloadedAutoSubsOnly`, kept while a transcription task is running on it, and removed
otherwise; audio that vanishes mid-transcription is now a skip rather than a permanent
`failed-transcriptions` entry; and speaker attribution records which transcript the names
were made from, so an ASR→whisper replacement goes stale (for records written from now on).
Runbook item 2 **step 4 is resolved** — see below. Previously: 2026-08-27 — **the `deferred` cause is found and fixed** (`fed4b01`,
`ca42f7b`): a backfill re-acquire on a `handling: "youtube"` channel was running the channel's
own subtitle download, so it re-fetched captions, landed no audio, and left ~16,000 videos'
`transcript.cues.json` stale by mtime. It now forces a per-video transcribe override and
re-normalizes after every fetch. The census is in FACTS.md; the operator's four remaining
steps are item 2 of "Recommended next". Previously: 2026-08-26 (late) — **unified-ops step 1 landed**: `buckets.noDigest` is
deleted and `snapshot.backfill.digest` is the one digest work list. No rendered number moved
(the fallback's migration was already complete on disk, 68/68), and **IA slice 4 is now
unblocked**. One finding for the operator: corpus-wide `deferred` is **16,156**, against 1 on
08-10 — see the dated section below. Earlier the same day: **editor IA slice 3 landed**: an operation's settings live on its operation page, and the four `*FormPresent` markers are gone. See the dated section below. Earlier the same day: **the vocabulary pass landed**: the registry is
`operations.ts` and says `Operation`, the three id spaces slice 2 handed it say "speakers",
"unit" has one meaning, and `transcode` is a registry entry. See the dated section below.
Earlier the same day: editor IA slice 2 (speakers are a real stage; one runner path) landed,
and slice 1 was committed with it. Previously: 2026-08-24 — **`main` is green again, `feat/channel-groups` is merged, and
this file caught up with two sessions it had no entry for.** The 2026-08-12 session below
landed in `7d32438` on 08-18; everything from 08-19 to 08-24 — recency ordering, the
arbiter, lane guards, the pipelines band, worker tags and slots, LLM fan-out, unit
executors, the sweep scope console — shipped on `main` with no STATE.md entry. The
context-clear protocol in [`README.md`](README.md) was skipped twice; this block is the
catch-up. Commit bodies are the primary record and carry the measurements — read them
with `git log -1 --format=%b ` rather than trusting a paraphrase here.

**Later the same day (16:xx).** Operator decision: the homepage does NOT link to the hub for now (`3dad5944`, `homepage/app/page.tsx` `HUB_LINK_ENABLED = false`; the URL stays in `homepage.json` for the hub's own build; redeployed as https://e04f6dd7.archilyzer.pages.dev — 0 "Search all archives" on the live page). Next: a ground-up homepage refresh (describe the project; the five sites as "Official instances"; plain copy, fewer subtitles) on a preview branch for review.

## What landed 2026-08-18 → 08-24

- **Chapter-boundary quality metric** (`7d32438`) — the 08-12 narrative below, committed.
  `lib/boundaryScore.ts`, `--score-existing`, and the READ-RECALL-NOT-PRECISION finding.
- **Recency ordering, four commits** (`74f8e51` → `efea56f` → `72745fe` → `bddae39`). A
  per-runner `order` (listed | newest | oldest) sorts within a rule; `digest.recencyOrder`
  and `backfill.order` do the same inside a batch; `reach` ("channel" | "corpus") extends it
  to the CHANNEL plan. The date comes from a layered key: a key-only scan of `index.mdb`'s
  `byChannel` (~300 ms for 78,583 videos, but only videos WITH a transcript — 122 of 870
  pending), then an 8 KB tail read of `metadata.info.json` (868/870, 0.19 ms each,
  memoized), then playlist position marked `estimated`. **Directory name ≠ metadata id on
  ~14.5% of this corpus** (`the-quartering-rumble/data/v1007ay` holds `vxe1ae`), so the
  index lookup is scoped to the owning channel or channel A's dir name inherits channel
  B's date. Measured: 11,329 candidates ordered in 118 ms; the sweep re-plans in ~1.5 s.
  `/auto-queue` became a dispatcher board saying what it would run next and why.
- **Operation leaves, lane guards, the arbiter** (`3299a53`, `09d4598`, `ab68635`) —
  [`unified-operations-model.md`](unified-operations-model.md) steps **2, 3, 4 DONE; 1, 5,
  6 open**. (That file's "designed, not built" header is stale by three steps; its step
  list is current.) `AutoQueueMatch.operation` beside `bucket`, claim key
  `${operation}\0${id}` so digest and diarization cannot steal each other's work;
  `common/controller/laneGuards.ts` holds digest's six rules as a preflight (throws) and a
  per-pull gate (holds, never stops); `common/controller/arbiter.ts` runs on `queueKey ""`,
  one unit per lane per pass, one job per channel, re-plans every pass, and REFUSES beside
  an armed sweep. Not persisted across restart — that is step 6's.
- **The console and the band** (`1f2b7c2`, `43173c8`, `7fa975d`) — `/auto-queue` is one
  comparison rail over four pipelines; `components/pipelines/` holds the five-population
  band (reachable / needs-media / blocked / deferred / present — never summed, ~91× apart on
  this corpus). `band.ts` must stay directive-free and value-import-free: a server component
  calls its pure readings, and `"use client"` on it 500s every channel page at request
  time while `pnpm build` passes — caught by e2e, now pinned by a unit test.
- **/channels, `group`, `costBasis`, and a word that was three things** (`4d57a5e`,
  `a5c4e48`, `07227ea`) — six bands per channel row; on the live corpus every row draws the
  same stripe and that uniformity IS the finding. Operations declare `group` and
  `costBasis`. **The fact that hid: `attribution-text` is ON, reachable on 11,337 videos of
  one channel, its unit is the transcript CHUNK (~194,000 model calls corpus-wide), and it
  has completed ONE video** — it read as a quiet row because "11,337 reachable" looks the
  same whether the unit is an audio pass or a per-chunk call. "Backfill" no longer names a
  figure anywhere; it is a queue key, listed with its members only where the shared pause
  and sweep live.
- **Workers: tags route, slots multiply, endpoints fan out, executors run units**
  (`1ff2256`, `549c77b`, `2fcd08f`, `16004ac`, `eee3a07`). `Worker.tags` is consulted
  (`common/lib/workers.ts`); a remote is N slots, blank N filled from its own
  `/api/worker/health` through `controller/remoteCapacity.ts`; `WorkerKind "llm"` is a box
  running nothing but `ollama serve`, leased per call with only `baseUrl` swapped (outside
  the freshness identity → zero churn), verified against `/api/tags` for the exact model
  tag; every backfill kind declares `inputs / outputs / applyResult` and
  `/api/worker/unit` runs one unit on a corpus-less box (`controller/remoteUnit.ts`,
  `workerServer.startWorkerUnit`), refusing anything that is not a backfill kind. Executor
  deployment is in RUNNING_IN_DOCKER.md. `worker-unit.spec.ts` 6/6.
- **Sweep scope console** (`dee7500`, `215d4e3`) — pick operations and channels before
  arming; scope is written by the ARM action only; a read-only "also runs on …" line per
  row derived through the same `workerMatches` the pool grants by.
- **Channel groups on /channels** (`66ec9eb`, `421a13c`, merged 2026-08-24 from the
  `feat/channel-groups` worktree, which had sat uncommitted since 08-11). One table
  sectioned by the site's authored groups, each header carrying that group's slice of the
  pipeline as stations whose figure IS the button. Rebased over `4d57a5e`: the branch's row
  component now renders main's `PipelineCell` columns, sections join to rows by slug, and
  the header's `colSpan` is `8 + columns.length` rather than a literal.

**Housekeeping, 2026-08-24.** `common` had failed `tsc` since `dee7500` (three test
fixtures missing the widened `SweepKindCounts` fields; the node runner does not typecheck,
so 821/821 hid it) — fixed in `6ea5ea2`. The `flexsearch-spike` worktree
(`feat/client-flexsearch-index`, last touched 06-27, superseded by
`common/components/searchIndex.worker.ts`) was **abandoned at `b4dee06`** — reflog only.
74 already-merged local branches deleted; the empty May stash dropped.
[`relocate-channel-media.md`](relocate-channel-media.md) is now tracked: designed and
verified against the tree, **not started**, and its real prerequisite is operational —
`sdb1` (1.8 T platter) is not mounted. **Not covered here:** `umtool` (27 commits on 08-18,
a separate workspace package) is documented in `umtool/docs` and AGENTS.md.

### 2026-08-26 — speakers become a real stage, and there is one way to run an operation

Six commits, `1da6f22` → `1058b98`, each its own reviewable diff.
[`editor-operations-ia.md`](editor-operations-ia.md) has the full "Slice 2, as shipped"
section; [`FACTS.md`](FACTS.md) has the invariants. Slice 1 is committed too (`9b65b83`) —
it had been sitting uncommitted as 50 changed files, which would have made slice 2 one
unreviewable diff.

**Two live bugs fell out of the refactor, neither of them the point of it:**

- **The arbiter never called diarization's `laneFor`.** `laneForOperation` special-cased
  digest and returned the DECLARED `.lane` for everything else, so a sortformer/vulkan
  diarization reserved as `contendsFor: "cpu"` while holding ~4.4 GB of the same 8 GB card
  the transcription engine wants. Diarization has declared a `laneFor` since it was written;
  nothing asked it. Now one resolution off the kind's own declaration, and digest gains the
  `laneFor` it should always have had.
- **`/operations/` decided what to render from four hardcoded id tests.** A registered
  `transcode` — external, no runner — would have drawn the BACKFILL SWEEP'S console under a
  "Transcode" heading, with a live Start button arming a corpus-scale GPU commitment. The
  descriptor states it now (`runner?`, and `sweepLaneIdFor` off `lane.queueKey`), and **null
  is a real answer** rendered as a "no console here" panel.

**One runner path.** `common/controller/operationJobs.ts`. There were four copies of "run one
operation over one channel" and no two agreed: the editor's backfill summary line printed
`deferred` and `blocked`, the sweep's printed `skipped`, and neither printed the other's — so
"what did that run actually do" depended on which button started it.

**The load-bearing decision in it: the runners do NOT drain.** Draining is the SWEEP's
property, and the editor consumes the same stream client-side to draw a live log — a runner
that drained would leave every stage card's log dead. The three callers that want sequencing
drain at their own call site. This is the thing most likely to be "fixed" back.

**`StageId "backfill"` → `"speakers"`.** It was a queue key wearing a stage's name; nobody can
arm, pause or run "a backfill". `?stage=backfill` still resolves (`STAGE_ALIASES`) — an unknown
`?stage=` falls back to the overview, which is right for a retired stage and wrong for a
renamed one. **Two assertions would have passed vacuously after the rename** and were caught:
a `notEqual` against a value the union can no longer hold, and a count-0 against a label
nothing renders.

**Decisions worth keeping:**
- **`laneFor` must stay OPTIONAL.** `backfillBatch.ts` reads its PRESENCE as the marker for
  the GPU idle-only rule, so "give every kind a `laneFor` defaulting to `lane`" is not a no-op
  — it enrols every kind in that rule and revives a recorded regression.
- **`laneForOperation` asks `getBackfillKind`, not `operationCatalog()`.** A catalog lookup
  would hand download and transcription a lane and the arbiter would start a backfill channel
  job for work no backfill kind can do.
- **NOT one `OperationStage.tsx`**, which is what the slice plan said. The two cards overlap
  in presentation and diverge in everything that acts; merging them means a component
  branching on its own identity. `OperationWork.tsx` is the presentational half, and the
  criterion is written into the file.
- **The `/channels` backfill group button is the LANE, not one operation**, so it does not go
  through the dispatcher.
- **The population aria-labels still say "backfill"** — 16 spec lines, deliberately left for
  the vocabulary pass to keep them out of this slice's e2e risk. *(Done later the same day —
  they say "speakers"; see the entry above.)*

**Recommended next**, with unified-ops step 1 (the old #2) now shipped:

1. **GPU yield on a quiet box** — still the gate on arming the sweep, still unmeasured.
2. **The operator runbook for the 16,156 `deferred`** — **CAUSE FOUND AND FIXED 2026-08-27**
   (`fed4b01` the fix + units, `ca42f7b` the e2e and the wording). It was **neither
   accumulation nor a regression in the cues gate**: it was one armed backfill sweep. With
   `allowRedownload: true`, every video on a `handling: "youtube"` channel is diarization
   `missing-input`, which dispatched a re-acquire that ran the channel's OWN
   `--skip-download --write-subs --write-auto-subs` download — rewriting
   `metadata.info.json`, landing no audio, and leaving `transcript.cues.json` stale by mtime.
   ~16,000 videos on eight channels, 2026-08-22 → 08-26, zero diarizations. Census, trail and
   numbers: FACTS.md "Verified 2026-08-27 — why `deferred` was 16,156". The fix forces a
   per-video transcribe override and re-normalizes after every fetch. **Four operator steps
   remain, and none was run from that slice:**

   1. Keep `sweepEnabled: false` until `fed4b01` is on the running editor. (It is off now.)
   2. **Clear the 16,081**: Digest stage card → *Normalize transcripts* on the-quartering,
      destiny, chibi-reviews, nux-taku, leaflit, kirsche, quartering-live, HasanAbiVODs3 (and
      shondo-vods, 22). Pure re-parse, no network; identical cues in 230/240 sampled cases;
      digests are not invalidated (`isSectionFresh` is provenance-keyed). Expect corpus
      `deferred` → ~76, the `missing`/`no-meta` tail.
   3. **Re-affirm `allowRedownload` knowing what it now does.** With the fix the sweep will
      *really* fetch audio for the **66,540** youtube-handling `missingInput` videos
      corpus-wide, deleting each file after use (or handing it to auto-transcribe — step 4).
      The walk is **channel-major, in the configured order, with no inter-download sleep**:
      `sleepBetweenDownloadsSeconds` is the channel SYNC path's setting and the backfill
      sweep does not consult it (`backfillSweep.ts:55, 335-368`), so the earlier "30 s
      between downloads → ≥ 23 days" figure here was wrong. Scope it with `sweepChannels`, or
      turn the flag off, before arming. That is a policy decision, not a code one.
   4. ~~**A known interaction, flagged not fixed**~~ — **RESOLVED 2026-08-28 by `e450c2c`.**
      The interaction was real: `autoQueue.transcription` is enabled with
      `replaceAutoSubs: true` and picks from snapshots that regenerate ~1 s after a download
      finishes, so a re-acquired `audio.mp3` on an ASR-only video is exactly what it looks
      for — and the backfill's `finally` would delete it mid-whisper, which is not "a failed
      unit, nothing lost" but a video appended to `failed-transcriptions`, honoured
      permanently by the manual per-channel batch. Now the backfill hands the file over
      instead (`decideKeep`), vetoes cleanup while a transcribe task is running on the video,
      and `transcribeOne` treats vanished audio as a `no-audio` SKIP on both the local and the
      remote path. **The operator no longer needs to turn `replaceAutoSubs` off for the corpus
      run**; the trade is that handed-off audio persists like any other download until a
      Clean-audio sweep. Mechanics and the file:line trail: FACTS.md "Verified 2026-08-27 —
      the re-acquire / auto-transcribe hand-off".
3. ~~**Editor IA slice 4** (`/actionable` dissolves)~~ — **DONE 2026-08-28**, `a623958` →
   `43c4e26`. Plan: [`editor-ia-slice-4.md`](editor-ia-slice-4.md); outcome: "Slice 4, as
   shipped" in `editor-operations-ia.md` and the FACTS section "Verified 2026-08-28 — editor
   IA slice 4 seams".
4. **Relocate `omnimirror`'s media** once the platter is mounted (131 GB off a 94%-full SSD).
5. **Phase 6 Ollama `/ask`** — genuinely independent; a good parallel task.
6. Decide `attribution-text`'s fate on that one channel: ~194,000 chunk-level calls is a
   sweep-sized commitment that was never priced.
7. ~~**Unified-ops step 1**~~ — **DONE 2026-08-26**, `efb0cf9` → `00b1c8a`. Plan:
   [`unified-ops-step-1.md`](unified-ops-step-1.md); outcome: the dated entry below and the
   FACTS section "Verified 2026-08-26 — unified-ops step 1".
8. ~~**Editor IA slice 3** (per-operation settings)~~ — **DONE 2026-08-26**, `9515083` →
   `43bb519`. Plan: [`editor-ia-slice-3.md`](editor-ia-slice-3.md); outcome: the dated entry
   below and "Slice 3, as shipped" in `editor-operations-ia.md`.
9. ~~**Editor IA slice 7** (one pause)~~ — **DONE 2026-08-28**, `c924f73` → `685cba3`. Plan:
   [`editor-ia-slice-7.md`](editor-ia-slice-7.md); outcome: "Slice 7, as shipped" in
   `editor-operations-ia.md` and the FACTS section "Verified 2026-08-28 — editor IA slice 7
   seams". It is also **half of unified-ops step 5**: one definition and one writer among the
   controls, but the four settings fields are still four settings fields — step 6 moves the
   storage behind `isGateHeld`/`withGateHeld`.
10. ~~**Editor IA slice 6** (the video page: one panel per operation)~~ — **DONE 2026-08-28**,
    `6986efc` → `d5ca90a`. Plan: [`editor-ia-slice-6.md`](editor-ia-slice-6.md); outcome:
    "Slice 6, as shipped" in `editor-operations-ia.md` and the FACTS section "Verified
    2026-08-28 — editor IA slice 6 seams". The registry's per-video reader is
    `common/controller/videoOperations.ts`, and a fifth operation now needs no page.
11. ~~**Editor IA slice 8a+8b** (sync is an operation; its settings and the workers' live on
    their pages)~~ — **DONE 2026-08-29**, `ea04b4e` → `fecac0f`. Plan:
    [`editor-ia-slice-8ab.md`](editor-ia-slice-8ab.md); outcome: "Slice 8a+8b, as shipped" in
    `editor-operations-ia.md` and the FACTS section "Verified 2026-08-29 — editor IA slice
    8a+8b seams".
12. ~~**The `/jobs` fold — the last third of slice 8**~~ — **DONE 2026-08-30**, `2226680` →
    `21feed3`. Plan: [`editor-ia-slice-8c.md`](editor-ia-slice-8c.md); outcome: "Slice 8c, as
    shipped" in `editor-operations-ia.md` and the FACTS section "Verified 2026-08-30 — editor
    IA slice 8c seams". The design question was answered **"a row is one job"** — and because
    it was, there is no mode: one list, live head and paged tail, one `` per job. Slice 8
    is complete.
13. ~~**Editor IA slice 5** (Sites absorb the five)~~ — **DONE 2026-08-30**, `140212a` →
    `df9b839`. Plan: [`editor-ia-slice-5.md`](editor-ia-slice-5.md); outcome: "Slice 5, as
    shipped" in `editor-operations-ia.md` and the FACTS section "Verified 2026-08-30 — editor
    IA slice 5 seams". A site has tabs and the family page has the globals; the five satellite
    routes 307. **The nav is eleven** — the IA doc's end state — and the IA doc's UI slices are
    complete.
14. ~~**The transcode operation**~~ — **REMOVED 2026-08-30**, `904f1a9` → `f717a36`. Plan:
    [`editor-transcode-removed.md`](editor-transcode-removed.md); census in FACTS "Verified
    2026-08-30 — transcode: what it was, what stayed". The band question is closed by
    deletion; `EXTERNAL_BAND_IDS` is two — and since one-core 1.5 it is two because it is
    `EXTERNAL_OPERATIONS.map(o => o.id)`, so a third external pipeline would join the rail
    by being declared rather than by editing that constant.

**Recommended next (editor IA).** The UI slices are done and the transcode band is no longer a
candidate. What is left is **Phase 6** and **slice 9**, which is gated on the sweep running in
production.

---

### 2026-08-26 (late) — unified-ops step 1: the digest work list has one definition

Three commits: `1db30c3` (the plan), `efb0cf9` (the deletion), `00b1c8a` (de-specialising the
readers), plus this docs commit. Plan: [`unified-ops-step-1.md`](unified-ops-step-1.md);
measurements in [`FACTS.md`](FACTS.md) under "Verified 2026-08-26 — unified-ops step 1".

1. **The migration was already over, so the step was a deletion, not a rewiring.** All **68 of
   68** snapshots on disk carry `backfill.digest` with a numeric `eligible` (`generatedAt`
   08-11 → 08-26), so `digestWorkOf`'s `"bucket"` branch was reading nothing. **No rendered
   number moved.** The "published coverage percentage will change" warning in the step's own
   text had already happened, silently, between 08-11 and 08-26.
2. **The two counters disagreed by 11,777 videos** (bucket 59,159 vs entry 47,382) and could
   not have been reconciled: on 65 of 68 channels the delta is exactly `deferred`, because the
   registry gates on a current `cues.json` and the bucket did not. Deleting the bucket is what
   makes there be one definition — the registry's `state()`, which is also what the runner
   dispatches from.
3. **No regeneration, and no regen CLI.** Old snapshots keep a stray `noDigest` key until
   their next routine regen; `normalizeBuckets` picks only the keys it knows and there is no
   snapshot schema version. An offline writer would race the live editor's
   `snapshotScheduler` on the same files, and there is nothing it would need to do.
4. **The `noDigest` view-model names were deliberately kept** — `DashboardChannel.noDigest`,
   the `/api/widget/actionable` wire field, `actionableNoDigestCount`, `DigestStage`'s
   `noDigestIds`. They mean "videos with no current digest", which is still what they hold;
   renaming a wire field is its own change. **Renamed in slice 4** (2026-08-28): the first
   three are `digestReachable` / `actionableDigestReachableCount`, because the number is the
   digest band's `reachable` and the honest name says so. `DigestStage`'s `noDigestIds` is
   untouched — it is a list of ids, not this count.
5. **The transcode band was NOT taken with this step.** Three docs filed it "with step 1" only
   because both were thought to need one regeneration of the snapshots; step 1 needs none, so
   the coupling is void. It is its own snapshot-shape change (the writer recording a transcode
   population, `appliesTo(config)` and a kept-media denominator) and its own plan.
6. **Digest is no longer special to any reader.** Four `id === DIGEST_OPERATION_ID` fallbacks
   went (`buildBands.ts`, `sweepPreview.ts` ×2, `sweepRecency.ts`). No entry now means for
   digest what it means for diarization: no work known, coverage unknown — an unfilled band
   outline, never a filled 0 %.

**THE FINDING TO ACT ON: corpus-wide `deferred` is 16,156.** It was **1** on 2026-08-10, right
after the normalize pass. `destiny` 3,869, `the-quartering` 3,762, `chibi-reviews` 2,699,
`nux-taku` 1,802, `leaflit` 1,612, `kirsche` 1,197. That is 16,156 videos the digest lane will
not touch, and it was invisible to a reader of the old bucket.

> **Corrected 2026-08-27.** The "either accumulation or a regression" framing above was wrong
> on both counts, and it is left here only because the census that settled it started from
> these numbers. It was ONE ARMED BACKFILL SWEEP re-acquiring media on subtitle channels with
> the channel's own subtitle-download config: metadata rewritten, no audio landed, cues stale
> by mtime. Fixed in `fed4b01` / `ca42f7b`; the census and the file:line trail are in FACTS.md
> under "Verified 2026-08-27 — why `deferred` was 16,156", and the operator runbook is item 2
> of "Recommended next" above. The Digest card's **Normalize transcripts** button is still
> what clears the 16,081 already on disk — deliberately NOT run from either slice.

---

### 2026-08-26 (evening) — editor IA slice 3: an operation's settings are on its page

Five commits: `9515083` (the plan, with the two uncommitted doc files), `f14ceb5` (the
seams), `43bb519` (the move), `b303817` (copy, the `/settings` pointer, docs) and `7c34bc5`
(the one collision the move created — see 6 below). The as-shipped record is in `editor-operations-ia.md`
("Slice 3, as shipped") and the new file paths are in `FACTS.md` under the seams census;
this entry is the decisions.

1. **The markers were the deletion.** Four hidden `*FormPresent` inputs gated the four
   settings blocks, and every one existed because ONE form saved everything: an unchecked
   checkbox is absent from a FormData, so a submit from a form lacking a block would read
   that block's every switch as off — a disarmed corpus sweep, a dropped diarization capture
   lane, `allowRedownload` on against a full disk, ~194,000 model calls armed. One form per
   block makes that structural. 562 lines out of `SettingsForm`, 171 out of its action.
2. **The lane block is per LANE, not per operation** (the decision recorded while scoping,
   and it held). `settings.backfill` governs `BACKFILL_QUEUE`, shared by diarization and both
   attribution operations, so `LaneSettingsForm` renders beside `SweepLane` in
   `SweepOperationView` — one definition, three renders, exactly where the shared pause and
   sweep already are.
3. **`settingsBlock` is on the descriptor**, a closed union, switched over exhaustively —
   never a table keyed by operation id. Both attribution operations get one form because they
   declare one block. Same rule slice 2 set for `runner` and `sweepLaneIdFor`.
4. **The `role="status"` boundary is now written down.** `SaveBar`'s "only role=status on
   this page" means RUNNER pages (`auto-subs-replace.spec.ts:415` depends on it); sweep-fed
   pages have settings forms with their own status regions and no policy tree.
5. **Two blocks gained their first form-driving spec.** Attribution and the lane had none —
   which is how the most expensive switch in the console went untested from a browser. The
   new `operation-settings.spec.ts` also proves structurally what the markers used to buy:
   saving the diarization form beside the lane form leaves `backfill.enabled` alone. 7/7.

6. **Moving a component moves its SENTENCES into someone else's selectors.** The lane form's
   paragraph still said "…so it lives with the plan it produces", and `The plan` is the
   heading of the sweep console now directly above it inside the same `section[data-lane]`.
   `backfill.spec.ts:484` scopes `getByText("The plan")` there and `getByText` matches
   substrings, so it resolved to two elements. Reworded, with a comment marking the phrase
   off-limits in that file. (The same commit keys the two server-built settings slots: an
   element created in a Server Component and rendered by a client component warns on React's
   dev key validation, naming a render method that did not create it.)

Verified: tsc in all six packages, `common` 828/828, editor units 85/85, the new spec 7/7,
and a fixture check of every operation page (digest → one form; diarization and both
attribution pages → operation form then lane form; download/transcode/transcription → none;
`/settings` down to its four remaining fieldsets and one ``, carrying the pointer).
The affected e2e specs ran twice: 104/107 then, after the fix, 28/28 on the three that had
failed — two of those three (`auto-subs-replace.spec.ts:394`, `cadence-ui.spec.ts:212`) were
timeouts and passed on a re-run untouched.

Deliberately not done: the three other file-local `Field` copies; `WorkersField` (slice 8);
the pause trio (slice 7); `/actionable` (slice 4). No persisted key or field `name` changed,
and no route retired.

### 2026-08-26 (later) — the vocabulary pass: the registry says "operation"

Three commits after `e0703b4`, sized so the 60-file identifier sweep is reviewable on its own.
The mapping table, the "unit" rule, the list of what deliberately still says "backfill" and
the transcode-band boundary are in `FACTS.md` ("Verified 2026-08-26 — the vocabulary pass");
this entry is the decisions.

1. **Mechanical rename.** `backfillKinds.ts` → `operations.ts`, ~25 `Backfill*` exports →
   `Operation*`, tsc-verified, zero behaviour change. The rule: "backfill" survives where it
   names the LANE and its persisted contracts, goes where it named the kind. So
   `laneBackfillKinds` became `backfillLaneOperations` — the lane word moved to the front
   rather than vanishing, because once `Lane` is generic "the lane's operations" must say
   which lane. Persisted keys (`snapshot.backfill`, `settings.backfill`, `kindIds`,
   `sweepKinds`, the job kinds) are untouched; "kind" was not chased outside the export table.
2. **The id spaces.** `StationId "backfill"` → `"speakers"` on `/channels`, with the label
   **derived** (`speakersLabel`, "Derived data" under default settings) rather than the
   literal "Speakers" — the same rule the stage title already follows, so a button never
   claims work its lane is not doing. `/actionable`'s section id → `speakers`, count column
   "reachable". `BackfillStage` → `SpeakersStage`, and its populations say "speakers" (15 spec
   lines). `PauseBackfillButton.tsx` deleted (zero importers). "unit" is one word: dispatch
   keeps it, cost is "cost basis", the `Figure({unit})` prop was a population label and says so.
3. **`transcode` registered**, with the registry's first `appliesTo(config)` replacing three
   verbatim copies of `handling === "transcribe" && !!audioFormat`. `/operations/transcode`
   is the no-console panel slice 2 built. **Its band is not registered**: a snapshot carries
   no `handling`/`audioFormat`, so a pure band cannot tell "never transcodes" from
   "finished" — that waits for the snapshot writer to record it (unified-ops step 1).
   **Removed 2026-08-30** — see item 14 above.

**Decisions worth keeping:**
- **The station label is derived, not "Speakers".** `channel-stage-selection.spec.ts` pins
  "Derived data" as the honest fallback; `channel-groups.spec.ts` now asserts the same string
  on the group button.
- **`backfill*Action` names stay** — `backfillChannelAction` and
  `start|stopBackfillSweepAction` run or gate the LANE, and their aria-labels
  (`pause backfill`, `start backfill sweep`) are asserted by specs that test the lane.
  (`pause|resumeBackfillAction` were named here too; **slice 7 deleted them** along with the
  other seven pause actions — the aria-labels stayed, which is the part that mattered.)
- **Plans docs were not search-replaced.** They are dated history; FACTS carries the mapping
  and a line saying earlier sections are pre-rename.

### 2026-08-25 — the editor gets its noun: `/operations`, and a nav with four groups

**The backend found its organizing noun — the operation — and the UI had not.** Counted
rather than estimated: 28 pages behind a 19-link nav whose "Pool" group held ten unrelated
tools, four channel tables, four job lists, three renderings of lane state, and four
different answers to "what needs doing". Digest and attribution are first-class in the
REGISTRY and were second-class in the UI: no route, no stage, no per-video view, two settings
fieldsets for one concept.

[`editor-operations-ia.md`](editor-operations-ia.md) is the UI half of
[`unified-operations-model.md`](unified-operations-model.md), written down whole — the four
nouns (Corpus, Operations, Sites, Machine), the seven places "operation as the noun" honestly
breaks and what to do about each, the nav end state, the reconciliations with unified-ops
steps 5/6, PLAN.md Phase 11a and relocate-channel-media §6, and **nine slices, each
shippable, each deleting something**.

**Slice 1 shipped.** `git mv app/auto-queue app/operations`; `/operations` is the board (the
rail, the arbiter, a sync row) and `/operations/` is one operation, resolved against
`operationCatalog()` so an unknown id 404s and a NEW registry entry gets a page with no route
work. `/auto-queue` redirects. The **lane `