# Release 11 — release 10's follow-ups, the homepage deploy from `/sites`, umtool's brand fonts, one-core Phase 4 slice 3 `main` at `ee9d41fa` (release 10 fully rolled out as 0.9.4 on 2026-09-28 at 00:53: the five sites, the hub `10d6946f` and the homepage `7ca70923`; the :3001 editor restart is still owed). Release 11 is the overnight session of 2026-09-28, which the operator asked for with all four candidate bundles: the lows left by release 10, the homepage deploy from `/sites` ([`homepage-deploy-from-ui.md`](homepage-deploy-from-ui.md)), umtool's brand fonts, and one-core Phase 4 slice 3 ([`one-core.md`](one-core.md), "Phase 4"). Rules: `plans/tools/implementer-rules.md`, with the commit trailer this release's prompts give. **The operator's standing choices for the night:** - **Copy:** the three items that need words ship with **draft lines**, one marked `DRAFT` constant each in `export/app/ask/hubScopeCopy.ts`, for a morning ruling. - **Merging:** each slice merges to `main` after its review. **No cut and no deploy**; an unattended build-deploy would publish the merged code with its `[Unreleased]` notes, and the operator accepts that. - **Cleanup:** the 14 merged worktrees of releases 9–10 were removed first (55 GiB freed; `diet-series` kept). ## The slices | Slice | Branch | What | Owns | |---|---|---|---| | O3 | `r11/runner-lows` | `resolveMaybeMissingState` reads a newer `error` probe as `maybe_missing` (+ an export check of the presence badge); a failed forced download keeps the subtitle pass's status; the `cookies-mode.spec:241` flake (keep the run log mounted); cut-release review lows L3 + L4 | `common/lib/availability*.ts`, the maybe-missing part of `buildIndex.ts`, `downloadOneManaged.ts`, `DownloadStage.tsx`, `common/controller/cutRelease.ts`, `api/ops/cut-release` | | O4 | `r11/homepage-deploy-ui` | The homepage's build and deploy from `/sites`: three job kinds, the refusals, a Homepage section, `pnpm ops build-homepage` / `deploy-homepage` | `editor/app/sites/**`, the two new ops routes, the job-kind entries, `scripts/archilyzer-ops.mjs` verbs, `builtHomepageProblem` | | O1 | `r11/hub-lows` | A "built-ins loaded" flag; three DRAFT copy lines (a zero-archive `/ask`, a member's missing live chat with a subs-only Retry, "Searching N of M archives"); `normalizeSocialSvg` fills children; `mcp/README.md`'s `fetch_clip` env lines | `export/app/ask/**`, `export/app/components/hub/**`, `SearchDataContext.tsx`, `siteRegistry.ts`, `normalizeSocialSvg`, `mcp/README.md` | | O2 | `r11/homepage-lows` | A custom hex fitted per base on the homepage cards; a 6th validated chart slot; a forced-colours fallback for the mark ring; the homepage e2e fixture copy | `homepage/app/components/*`, `common/lib/siteColor.ts` / `homepageChart.ts` / `homepageChartData.ts`, `BrandMark.tsx`, the homepage e2e setup | | O5 | `r11/umtool-fonts` | The rail, ledger and chart text in Plex Sans under the brand preset; a vendored bold IBM Plex Mono; an unbranded render byte-identical | `umtool/report-to-video/**` | | O6 | `r11/phase-4-s3` | `archilyzer doctor`, `run `, `mcp`; `PUBLISH.md` absorbing `DEPLOY_DOCKER.md` + `DEPLOY_CLOUDFLARE.md`; the env-var docs; the `E2E_` prefix cleanup LAST | `common/bin/archilyzer.ts`, the docs, then the playwright configs + helpers | **Waves:** O3 ‖ O4 ‖ O6 (the marathon) first; O1 ‖ O2 as the first two merge; O5 last. O6 merges `main` once O1–O5 have landed, then does its `E2E_` rename. Then one integration gate on `main`, and the morning runbook (`~/reports/overnight-2026-09-28/MORNING.html`). **Priority if the night runs short:** O3 > O4 > O1 > O2 > O5 > O6's remainder. ## Record ### Slice O4, as shipped — deploy the homepage from /sites (2026-09-28) The operator's ask (2026-09-26): **"Is there a way to redeploy archilyzer.pages.dev from the UI?"** There was not: `/sites` built and deployed the hub, and the homepage was a CLI (`archilyzer build|deploy homepage`) with no job, no `/jobs` row and no `pnpm ops` verb. Plan: [`homepage-deploy-from-ui.md`](homepage-deploy-from-ui.md), decisions 1–5, now with an "As shipped" note. Branch `r11/homepage-deploy-ui` off `main` `2162db92`, worktree `/home/user/Projects/r11-homepage-deploy-ui`, one Opus implementer. Scratch files `o4-*` in the job's `tmp/overnight`. **What shipped.** - **A Homepage section on `/sites`, after Hub.** The Hub paragraph lost its homepage sentence, and the new section says it instead: the `homepage` package, `homepage/out`, the project `archilyzer`, and the same code as `archilyzer build|deploy homepage`. Its group, **Homepage build**, is `HubBuildButtons`' shape: **Build homepage**, **Deploy after build** (unticked), **Deploy homepage**, one `JobLane` at a time (**Build homepage** / **Build & deploy homepage** / **Deploy homepage**). - **A preview branch box** (textbox "preview branch", empty = production). It is judged by `previewBranchProblem`, the function the action and the route refuse with. A bad name greys out Deploy homepage, and Build homepage when Deploy after build is ticked, and shows the sentence (`role="status"`, "preview problem"). - **One line saying what a deploy ships:** "Deploy homepage ships `homepage/out`, built , to `archilyzer` (production): https://archilyzer.pages.dev." (`PROJECT_URL`). With no build it says "`homepage/out` holds no build yet …". With a preview it gives the alias and says "the live site is left alone". Under it: the homepage reads the index as it stands, so run Build index first. - **Three job kinds,** via `runManagedFunction` in `editor/app/sites/lib/homepageDeployActions.ts`: - `build-homepage` on queue `build`; - `deploy-homepage` and `build-deploy-homepage` on queue `deploy`; - the bodies are `buildHomepage` / `deployHomepage`, unchanged. The build-deploy deploys only on exit 0. - **Refusals before any job:** a bad preview name, and for a deploy-only, `builtHomepageProblem(homepageOutDir(paths))` ("homepage/out holds no build — build the homepage first"). `deployHomepage` re-checks both inside the job with its own sentences, untouched. - **Ops:** - `/api/ops/build-homepage` `{deploy?, preview?}`: a preview without deploy is a 400. - `/api/ops/deploy-homepage` `{preview?}` answers `previewUrl` = `previewAliasUrl(HOMEPAGE_PAGES_PROJECT, preview)`. - `pnpm ops build-homepage` / `deploy-homepage`, in the verb list, the header examples and the usage. - **common:** `builtHomepageProblem` and `builtHomepageAt` (index.html's mtime) beside `builtHubProblem` in `builtExport.ts`. `homepageOutDir(paths)` exported from `publish/build.ts`, so the editor does not re-derive the path. - **Found and fixed: `JobLane` under Strict Mode.** The launch effect gated its updates on a flag local to it, set by its cleanup. `next dev`'s mount → cleanup → mount set that flag, and the second run returned early on `startedRef`, so the job ran while the lane sat on "Starting…" forever. Liveness is now a ref kept by its own effect. Production mounts once and is unchanged. No spec had ever clicked a lane button (batch, specific sites, hub), which is how it went unseen. **Corrections to the plan** (also in its "As shipped" note): - The ops preview is `--json '{"preview":""}'`, not a `--preview` flag. - No `jobKinds.ts` entries: the hub's kinds have none, and neither does any publish kind. - `homepageOutDir` had to be exported. **How nothing in e2e builds or deploys.** There is no fake wrangler, `homepage/out` is the checkout's own directory (the primary's holds the live homepage's build), and the hub's buttons have no spec to copy. So: - **No spec clicks Deploy homepage.** None ticks Deploy after build and then clicks Build. - **Build homepage is clicked only with the `build` and `deploy` queues both held** by `/api/test/stuck-job`, which is never released in the spec. The `build-homepage` job only queues, and so would a `build-deploy-homepage` if the click regressed into one. The spec cancels it from its lane. Its `start()` never runs: no log file, no child, nothing written under `homepage/`. - **If that spec fails before its Cancel,** the next `resetData` cancels live jobs newest first (`registry.list()` sorts by `queuedAt` descending). The queued build is removed before the holder's slot is freed, so nothing is ever promoted. - **The deploy-homepage refusals run with both queues held the same way** (`holdBuildAndDeployQueues` in `ops-api.spec`). A build or deploy that a refusal failed to stop would only queue, and the spec's job-list check would fail. - **The missing-build refusal is asked only when the checkout has no `homepage/out/index.html`,** as `deploy-hub`'s is (`211d4666`), and it is asked as a preview. - **Checked after every run:** the worktree has no `homepage/out`, and `homepage/public` holds only its tracked `_headers`. Its gitignored data was not copied, because nothing here builds the homepage. | sha | what | |---|---| | `b047b1be` | `common:` `builtHomepageProblem` + `builtHomepageAt` (`builtExport.ts`), `homepageOutDir` (`publish/build.ts`); +3 unit tests | | `7ba41164` | `sites:` `homepageDeployActions.ts`, `HomepageBuildButtons.tsx`, the Homepage section; the Hub paragraph loses its homepage sentence | | `54021099` | `ops:` `/api/ops/build-homepage`, `/api/ops/deploy-homepage`; `archilyzer-ops.mjs` verbs + usage (+1 test) | | `ba4ff0bb` | `e2e:` `ops-api.spec` +1 (the refusals), `sites-homepage.spec` (3) | | `d52d017d` | `sites:` `JobLane` survives Strict Mode | | `d9218cb7` | `sites:` the group stamps `data-hydrated`; the specs wait for it | | `40b6a0d7` | `changelog:` `[Unreleased]` above `[0.9.4]` in `editor/CHANGELOG.md` | | `ff8b9382` | `plans:` this record; the plan's "As shipped" note | | `72175134` | `e2e:` review fix — the homepage refusal test, the Build homepage test and the hub's refusal test hold both the `build` and the `deploy` queue | | `275c0fa6` | `common:` review fix — `deployHomepage` ships `homepageOutDir(paths)` (no behaviour change) | | _this_ | `plans:` the review fixes in this record and the plan's note | **Gates** (logs `o4-*.log`): - **tsc** clean before every code commit (the last run on `d9218cb7`'s tree). - **common 2,041/2,041** (2,038 + 3), editor unit **85/85**, `test:scripts` **175 + 1 skipped** (174 + 1, +1), mcp **269/269**. - **Editor build** ok (59 s); `/api/ops/build-homepage` and `/api/ops/deploy-homepage` are in the route list. - **e2e:** - Run 1 on `ba4ff0bb` (`ops-api`, `sites-homepage`, `sites-crud`, `site-publish-preview`, `deploy-page`, `build`, `settings`): **63 passed, 1 failed** (2.8 min). The failure was `sites-homepage`'s Build homepage test: the lane stayed on "Starting…", which is the `JobLane` bug above. - `sites-homepage` ×3 with the old `JobLane`: **6 passed, 3 failed**. - The same ×3 on `d9218cb7`: **9 passed** (27 s). - The final list on `d9218cb7` (the same seven specs): **64 passed** (1.8 min). - After the review fixes, on `275c0fa6`: `ops-api` + `sites-homepage` **26 passed** (40.5 s). tsc was clean, common 2,041/2,041 and `build.test.ts` 10/10. The worktree still has no `homepage/out`. - No spec here greps `deploy-hub|build-hub|HubBuild` except `ops-api`, which is in the list. - **Numbers tool:** none. **They bite:** - **Unit, against `2162db92`'s source:** - `builtExport.test.ts` and `build.test.ts` fail to load (no `builtHomepageAt` / `homepageOutDir` export). - A mutation, `builtHomepageProblem` checking the directory instead of `index.html`, fails 1 of 8. - `archilyzer-ops.test.mjs` against the old script: 1 of 33 fails, the new test. - **e2e, against `2162db92`'s `page.tsx`, `builtExport.ts` and `build.ts`,** with the new action, component and routes moved out: **4 of 4 new tests fail.** `ops-api` hits a 404 page ("Unexpected token '<'"), and `sites-homepage` finds no Homepage group. The tree was restored by a trap afterwards. - **The `JobLane` fix:** the Build homepage test fails 3 of 3 with the old lane and passes 3 of 3 with the fix. **Review fixes** (review: SHIP AFTER FIXES, `o4-review.md`): - **Should-fix 1, one queue held where a regression could reach the other.** The deploy-homepage refusal test held only `deploy`. A regressed "preview needs deploy" guard would have run a real `build-homepage` there: in the primary checkout, compose writes fixture numbers into `homepage/public`, then `next build` runs. The Build homepage test held only `build`, so a click turned into build-and-deploy would have reached `deploy`. Both tests now hold both queues. So does the hub's `deploy-hub` / `build-hub` refusal test (the review's low 4). That test **predates this slice** (release 7) and held nothing. In `ops-api.spec` the holds sit in one helper, `holdBuildAndDeployQueues`, and `before` is taken after them. - **Low 2:** `deployHomepage` now takes its directory from `homepageOutDir(paths)`. This is a pure refactor, and what it ships is exactly the directory the editor judged "built". - **Low 3** (the "built " line does not refresh after an in-page build) is left as recorded below. **Found and left** - **A job cancelled while still queued keeps `"status":"queued"` in its `.meta.json`.** The `onCancel` path never rewrites the sidecar. The registry says cancelled, and the boot pass settles the sidecar. This is pre-existing and was not touched. - **The "built " line is read when `/sites` renders,** so a lane that finishes a build does not refresh it. The Hub's lanes do not refresh the page either. - **`/jobs` shows the three kinds by their raw names,** as it does the hub's. - **Nothing has been built or deployed through the new jobs:** no build and no deploy tonight, by rule. The proof is the rollout below. **Rollout (owed):** the section and the routes exist only on an editor built from this code, so it waits on the :3001 restart. After it: 1. **Build homepage** with Deploy after build unticked, or `pnpm ops build-homepage --wait`. 2. **Deploy homepage**, or `pnpm ops deploy-homepage --wait`. The job's log ends with `[deployed] https://.archilyzer.pages.dev`. ### Slice O3, as shipped — runner lows (2026-09-28) Branch `r11/runner-lows` off `main` `2162db92`, worktree `/home/user/Projects/r11-runner-lows`, one Opus implementer, beside O4 and O6. Four lows left by release 10: one from L2 ("New low"), one from slice N's review (L2) and its knock-ons, the `cookies-mode.spec:241` intermittent N's full suite found, and slice P's review lows L3 + L4. No settings, site or channel key; nothing on disk moves. `CutReleaseResult.written` and an always-set `CutReleasesOutcome.untouched` are additive. **1 — a failed confirm probe no longer clears "Missing?"** (`common/lib/availability-server.ts`). - `resolveMaybeMissingState` decides the published state of a video that fell out of its channel's listing. A probe made after the scan that came back `public` clears the flag; so did one that came back `error`, through `stateFromAvailability`'s `error` → `available` fold. An `error` probe (a 403, a network failure, the bot check, a blocked run's one rate-limited probe) never saw the video, so it is no evidence it is there. It now leaves the video `maybe_missing`, like one never probed. `needs_auth` after the scan still confirms: an age gate is the video answering. - **Display only, and the clean gate is untouched — proof.** `grep -rn resolveMaybeMissingState` over the whole repo (not `node_modules` or `.next`) finds one caller, `buildIndex.ts:1180` (the maybe-missing overlay → `stateByIndexKey` → a site's summaries pages and the `videoState` sub-DB the status chart reads), plus `videoState.test.ts`. The diff to `availability-server.ts` is the one line and its comment; `resolveEffectiveAvailability`, which `verifyBeforeClean` and the MtimeRecord read, is not in it. - **The export check** is `common/controller/maybeMissingBuild.test.ts` (new), which runs the REAL `buildIndex` over a temp corpus: one video per outcome of the overlay, and the `state` each gets on the site's `summaries/page-0000.json` — the field the export's badge and Availability filter read. `export/e2e/availability-state.spec.ts` was NOT extended: it fulfils the summaries with mocked `state`s, so a new case there could not see the fix; it was re-run (9/9) as the contract check that `state: "maybe_missing"` still draws "Missing?". The shared transcript pages carry no `state` (found while writing the test). - **The real count, read-only** (`o3-flip-count.ts` in the job scratch dir, `o3-flip-count.log`: reads `maybe-missing.json`, `availability.json`, `download-outcome.json`, `metadata.info.json`'s existence and `sites/*/site.json` through the build's own loaders; opens no LMDB, writes nothing): 1,827 maybe-missing ids in 31 channels; **5 videos across 2 channels move from available to maybe-missing at the next index build** — `rekietalaw-rumble/v7e07us` and `the-quartering-rumble/v4vriou v4x5o1l v4yqask v501kfc`. Per site: **jeralyzer 4** ("Missing?" 212 → 216), **rekietalyzer 1** (0 → 1), anilyzer, bonnellyzer, hasanalyzer and jasolyzer 0. All five are Rumble probes that got `HTTP Error 410: Gone` on 2026-09-25 and were stored as `error`, hours before `6d5cdbc3` taught the classifier that 410 is `deleted`; they are really deleted, and "Missing?" is closer than "available". A "Full-check unexpected" on those two channels would settle them as Deleted (the operator's call; nothing was run). *Done 2026-09-28 13:32Z, on the operator's word: all five read deleted — "Operator actions (2026-09-28 morning)", below.* **2 — a failed forced download keeps the subtitle pass's status** (`common/ytdlp/downloadOneManaged.ts`). - The attempt-3 `else` set `status = "failed"` and `lastSucceeded = false` for a `keepTranscript` pass too, so a "Persist source video", `fetch_clip` `full: true` or Persist kept now whose MEDIA failed put "Download failed" on a video whose transcript is fine. A `keepTranscript` failure now keeps the status (the subtitle pass's `ok` / `ok-with-cookies`) and `lastSucceeded`, so the archive line the subtitle pass printed is appended as on the success path; the failed n: 3 attempt is recorded with its error (no `failureClass`: the download did not fail). One log line: `forceMedia: the source download failed (yt-dlp exit N); the transcript on disk is untouched and the download stays ok. Left for a retry to resume: source-media.f137.mp4.part ().` - **Knock-on 1, fixed: the job reports it.** `sourceFetchFailure(record)` (exported) says whether a download asked for the source got it: the status for an outright failure, the last attempt for a failed forced pass. `archiveSourceVideo` (`videoActions.ts`, both "Persist source video" and `fetchFullSourceAction`) throws `The source video was not downloaded: ` after its revalidate, so the job ends `failed` and the poll's `error` tail carries the line; the MCP renders "Editor job … failed" with it instead of "finished but named no file". A download that failed outright ends the job `failed` too (before: `done`). - **Knock-on 2, left on purpose: the `.part`.** yt-dlp resumes a partial container on a retry, and deleting one that is several GB makes a retry after a network blip start over — more requests against the source, which the pacing rules want fewer of. Nothing else surfaces a `source-media.*.part` (the Partial downloads bucket matches `audio..part` only), so the log line names it and its size instead. **3 — the `cookies-mode.spec:241` intermittent** (`DownloadStage.tsx`, `RetryBucketControl.tsx`). - The mechanism is wider than the end-of-run refresh: `recordTaskDone` → `requestSnapshotOnFinish` regenerates the snapshot after EACH video a batch finishes, the pulse moves and `AutoRefresh` refreshes the page while the run still streams. The refreshed `NeedsCookiesList` got an empty bucket and returned null, and so did the `RetryBucketControl` in it — two unmounts, either enough to lose the log. - The FACTS shape, in both: `ranHere` set inside the trigger (`RetryBucketControl` also calls a new optional `onRun` so its card knows), null only while `ids.length === 0 && !ranHere`, and `disabled={ids.length === 0}` so the surviving panel is not a second Run button. Applied to all three bucket cards on the Download stage (`NoTranscriptList`, `PartialDownloadsList`, `NeedsCookiesList`, same hazard) and to `RetryBucketControl` itself. Beyond the Download stage it reaches one more control: the Transcribe stage's **Fetch audio** (its section stays mounted while `autoSubsCount > 0`, and a fetch moves videos from "Needs audio" to "Ready to transcribe"). It does NOT reach Diagnostics: both of its grids drop an emptied bucket's whole card (`DiagnosticsStage.tsx:156` `populated`, `:694` `listed`), control and log with it (corrected in review; see Review fixes). A reload drops an empty card as before (the reload check at the end of the same test still passes); labels and test ids unchanged. - **Made deterministic.** The spec now waits for the refreshed, EMPTY card (`needs cookies empty`, which renders only inside a card that survived the refresh) and asserts the log's last line and a disabled `Download with cookies (0)` in it. On the old code this fails every time, at that line (below). **4 — cut-release review lows L3 + L4** (`common/controller/cutRelease.ts`, `api/ops/cut-release`). - **L3:** `applyCut`'s commit failure returns `{ok: false, workspace, error, written: true}` — the file on disk has its new heading. The route revalidates whenever anything was written. - **L4:** `cutReleases` sets `untouched` on EVERY outcome (true = no changelog written, every refusal included; false once one was, a `written` failure included); the type keeps it optional only so the CLI's hand-built display tests compile, and absent reads as false. `describeCutFailure` builds the route's 400 `error`: the failure (prefixed `: ` for `all`), then `Before it, editor was already cut (## [0.9.5] - 2026-09-28, committed 1a2b3c4d).` Every 400 from the writer carries `untouched`. An `all` whose FIRST write fails wrote nothing and is `untouched: true`, so the CLI prints "not cut — all cuts both or neither, and nothing was written" for the export — true. The input refusals before the writer (a bad workspace, version, date or key) are the ops door's 400s and carry no `untouched`, as before. - The CLI (`common/bin/release.ts`) is unchanged: its per-line output already named the earlier cut and says "failed — Cut release X, but the commit failed" for L3. - **The `/sites` form** (`editor/app/sites/lib/cutReleaseAction.ts`) revalidates on `written: true` too. It is under `editor/app/sites/**`, which slice O4 owned tonight, so it landed after O4's merge (Review fixes). | sha | what | |---|---| | `877403db` | 1: `resolveMaybeMissingState` — `error` after the scan stays `maybe_missing`; `videoState.test.ts` +2, `maybeMissingBuild.test.ts` (new, 1) | | `8c72265c` | 2: the `keepTranscript` failure keeps the status; `sourceFetchFailure`; `archiveSourceVideo` fails the job; `forceMedia.test.ts` +2; the fake's `.fake-ytdlp-media-fail` knob; `fetch-window.spec.ts` +1, `persist-youtube-handling.spec.ts` +1 | | `86449b88` | 3: `ranHere` / `onRun` / disabled in `RetryBucketControl` and the three Download-stage bucket cards; `cookies-mode.spec.ts:241` made deterministic | | `edec55ca` | 4: `written`, always-set `untouched`, `describeCutFailure`, the route; `cutRelease.test.ts` +4 (1 changed); `ops-cut-release.spec.ts` pins `untouched` | | _this_ | `plans:` this record, FACTS (three amendments, one new section), the `[Unreleased]` bullets | **Gates**, from the worktree root; logs `o3-*.log` in `$T`. - **tsc** (`pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit`) clean before each code commit: `o3-tsc1` (item 1), `o3-tsc3` (items 2 + 3, item 4 stashed), `o3-tsc4` (item 4). - **common 2,047/2,047** (release 10's 2,038 + 9: videoState +2, maybeMissingBuild +1, forceMedia +2, cutRelease +4); **editor unit 85/85**; **`test:scripts` 174 + 1 skip of 175**; **mcp 269/269** (`o3-units1.log`, on `edec55ca`). - **Editor build** `pnpm --filter editor exec next build` ok, 55 s (`o3-build1.log`; the route table lists `ƒ /api/ops/cut-release`). **Export build not run**: nothing under `export/` changed but its CHANGELOG. - **e2e** (queued, detached, `export/public` linked per path from the primary with `sw.js` a plain copy, no dangling links; the lock was free each time): | run | specs | passed | failed | time | |---|---|---|---|---| | A | `cookies-mode maybe-missing persist-youtube-handling fetch-window saved-videos cut-release ops-cut-release retry-bucket partial-downloads-bucket auto-subs-replace bulk-actions no-subs-fallback storage-locations` (`o3-e2e-specs.txt`: the prompt's seven, the four that drive a retry-bucket control, and the two that drive the source-media branch) | **61** | **0** | 3.9 min | | B | `cookies-mode.spec.ts --repeat-each=3` | **18** | **0** | 1.8 min | | C | EXPORT `availability-state.spec.ts` | **9** | **0** | 30 s | `archives-off.spec.ts` draws no state badge, so it was not added. The primary's `export/public/sw.js` was not written (md5 `355d0d21…`, mtime 00:51:32, before and after). - **Numbers tool:** `o3-flip-count.ts` (item 1), above. **They bite.** - Unit, item 1 (`o3-bite1.log`): with the `error` line reverted, `maybeMissingBuild.test.ts` fails and `videoState.test.ts`'s "error, probed AFTER the scan" fails; 15 others pass. - Unit, items 2 + 4 (`o3-bite2.log`): with the `keepTranscript` branch removed, forceMedia's new behaviour test fails (the `sourceFetchFailure` table test is of a new function); with `written` and the always-set `untouched` reverted, 5 of 20 cutRelease tests fail (the four new and the changed half-way test). - **e2e** (`o3-e2e-bite.log`): the six source files at their `2162db92` versions, the new specs and fake kept; `fetch-window persist-youtube-handling cookies-mode ops-cut-release`: **16 passed, 4 failed, 2.5 min**, and the four are exactly the new and changed tests — `cookies-mode.spec.ts:241` at `getByLabel("needs cookies empty")` (the card is gone), `fetch-window.spec.ts:465` (`done`, not `failed`), `ops-cut-release.spec.ts:73` (`untouched` undefined), `persist-youtube-handling.spec.ts:182` (no failure line; the old run marked the download failed). Restored with `git checkout HEAD --`. **Found and left.** - ~~`persistKept` still counts a returned download as persisted~~: done in the review fixes. - **L3/L4 have no HTTP e2e for the half-way cases.** The fixture server's changelogs are gitignored copies, so a commit there fails only after the dirty-tree guard has run git in the WORKTREE, and a failed write needs a read-only directory the server's path cannot be pointed at. The controller's tests drive both in temp repos (a refusing pre-commit hook; a `0555` directory); the spec pins `untouched: true` on a refusal. - **A Diagnostics card after its run.** A Diagnostics retry that empties its bucket still loses its card and its log: `DiagnosticsStage.tsx:156` / `:694` filter emptied buckets out before the control renders. The follow-up is to lift a ran-bucket set into both sections and keep those cards in `populated` / `listed`. - **The five 410 videos** are published as available today and will read "Missing?" after the next build; a Full-check unexpected would make them Deleted. *Ruled and done 2026-09-28 13:32Z: the full check read all five deleted, so they publish as Deleted, not "Missing?".* **Review fixes** (review SHIP AFTER FIXES, `o3-review.md` in the job scratch dir: one should-fix, wording only; three lows; one nit; the three questions ruled — keep `archiveSourceVideo`'s failure broad, staying mounted is fine as a rule, keep `untouched` optional tonight). - **Merge.** `export/.next/dev` removed first (run C's export dev server had left stale types there, so `tsc` failed in `export/` — low 3, environment only), then `git merge main` at `baaa4b47` (O4 merged). Conflicts only in `editor/CHANGELOG.md` (one `[Unreleased]`: O4's two bullets, then O3's three) and this file's Record (O4's section, then O3's). - **The deferred `/sites` form change.** `cutReleaseAction` revalidates when the result is `written: true`, then returns the error as before. The reviewer read it as correct and complete; no e2e can make the fixture server's commit fail after the write, so tsc and the controller's tests cover it, and `cut-release.spec.ts` re-ran green. - **should-fix — the Diagnostics claim was false.** The record and the changelog said the Diagnostics buckets' retry controls keep their log; both Diagnostics grids drop an emptied bucket's card, so they do not. Item 3 above, the editor bullet ("The Transcribe stage's "Fetch audio" button does the same. (A Diagnostics card still disappears…)"), FACTS and found-and-left now say exactly what is covered: the Download stage's three cards and the Transcribe stage's Fetch audio. - **low 1 — `sourceFetchFailure`'s order.** A no-subs fallback that fetched and persisted the source and whose inline whisper then failed ends the download `failed`; reading the status first called that source "not downloaded". When the last attempt is the media pass, its exit code alone answers now; otherwise the status. Latent here (`inlineTranscribeOnFallback` is off on the primary). - **low 2 — `persistKept`.** It counted every returned download as `persisted`; it now reads `sourceFetchFailure` and counts a failed forced pass as `failed`, with the reason in its log. - **nit.** The inert `key="retry"` on the Download stage's three retry controls is gone. | sha | what | |---|---| | `0c770285` | merge `main` (`baaa4b47`, O4): the changelog and Record conflicts | | `f62d00f1` | the Cut release form revalidates on `written: true` | | `e9947421` | low 1: `sourceFetchFailure` reads a media-pass last attempt first; `forceMedia.test.ts`'s table +1 row | | `315c76e1` | low 2: `persistKept` counts a failed forced pass as failed; `persistKeptForceMedia.test.ts` +1 (setup made one helper with a process-wide settings file) | | `5c9987d2` | nit: the inert keys dropped | | _this_ | `plans:` these fixes, the corrected wording (record, changelog, FACTS) | **Gates on `5c9987d2`:** - tsc clean after the merge (`o3-tsc5.log`) and on the fixes (`o3-tsc6.log`); every fix commit is a disjoint set of files from that tree. - common **2,051/2,051** (2,041 on `main` after O4, + O3's 10), editor unit **85/85**, `test:scripts` **175 + 1 skip of 176**, mcp **269/269** (`o3-round2.log`). - e2e `cut-release ops-cut-release persist-youtube-handling fetch-window saved-videos cookies-mode` (`o3-e2e-specs2.txt`, `o3-e2e2-full.log`): **28 passed, 0 failed, 1.3 min**, no queue wait. - **They bite** (`o3-bite3.log`): with the old order in `sourceFetchFailure`, the table test fails on the new row; with `persistKept` counting every return, the new persistKept test fails; the other 8 in the two files pass. ### Slice O6, as shipped — one-core Phase 4 slice 3 (2026-09-28) — checkpoint A Branch `r11/phase-4-s3` off `main` `2162db92` (no later `main` to merge before the first commit), worktree `/home/user/Projects/r11-phase-4-s3`, port block #6, one Opus implementer. The spec is [`one-core.md`](one-core.md) "Phase 4", items 2 (the rest) and 3. Checkpoint A is everything but the `E2E_` prefix cleanup, which is checkpoint B, after O1–O5 land. **The CLI's last subcommands.** - **`archilyzer doctor [--json]`** (`common/bin/doctor.ts`). One read-only report over what was spread across `paths.ts`, umtool's doctor and `scripts/worktree.mjs`: - workspace: node against next's `>=20.9.0`, the checkout, `node_modules`, the path overrides set; - corpus: channel count, each channel's media through `inspectChannelMedia`, the LMDB index by `stat` only; - settings: `settings.json` present, a JSON object, loads (`settingsFromFile`, new in `settings.ts`: getSettings' body for a named file); - tools: every binary `paths.ts` names, plus each enabled local worker's engine, `parakeet-cli` and model (looked up on PATH, not run); - umtool's report-pipeline table (read from `umtool/lib/tools.mjs` by a runtime import of the file; common does not depend on umtool); - the port block (asked of `scripts/worktree.mjs ports`), each port free or in use by a TCP connect. - A FAIL exits 1. It is something the machine is configured to do and cannot: a settings file that is not a JSON object (every process silently reads defaults), an enabled worker's engine or model missing BESIDE a corpus, yt-dlp/ffmpeg/ffprobe missing beside a corpus, a binary an env override names explicitly, node too old, no `node_modules`. Everything else warns or notes. A clone with no corpus is not broken, and an engine missing there is a warning. - Strictly read-only: no LMDB open, no mkdir, no settings write, no bind. The one process-state change is a `chdir` around umtool's table (it resolves `facecrop.py` from the cwd), restored at once. - **The probe exists once.** `common/lib/toolProbe.mjs` is umtool's `probeOne` lifted out (plain ESM, exported); `umtool/lib/tools.mjs` keeps its table and calls it. `probeTools()` output was byte-identical before and after (`o6-umtool-probe-{before,after}.json`, `cmp`). - **`archilyzer run [ids…] [--lane local|remote]`** (`common/bin/run-operation.ts`) calls `runOperationChannelJob`, the body the /channels buttons, the stage cards and the lane runners call. So a run is a real `backfill-channel` / `digest-channel-*` job: its record and log under `.jobs/`, the same summary line, the channel snapshot flushed at the end, and `runManagedFunction`'s media guard (a refusal before any record exists). - **Runs:** diarization, attribution-diarized, attribution-text, digest (the four registry operations). - **Refused with a sentence read off the descriptor:** sync, metadata-scan, download (the per-platform download queue paces every request to one source inside the editor), and transcription (the editor's worker pool). - **Refused up front:** an unknown operation (with both lists), an unknown channel, an operation switched off in settings, a paused lane (the batch would idle-wait for a resume only the editor can give), and `--lane` off digest. Ids with no data dir are named. - Ctrl-C cancels the job. Exit 0 done, 1 failed or refused, 2 usage, 130 cancelled. - Different from the editor's run, and said in the file's header: it does not see the editor's transcription activity, so the backfill lane's yield-to-transcription does not apply. - To carry ids, `runDigestChannelJob` gains `ids` (as the backfill job has; into `spec.params`), `runOperationChannelJob` passes `ids` to both runners, and the editor's digest replay forwards them through `digestChannelAction`, so a replayed scoped run cannot widen to the channel. - **`archilyzer mcp [args…]`** (`common/bin/mcp.ts`) starts the MCP server as `pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts` does: mcp's tsx, cwd `mcp/`, env passed through. It is a child process, not an import. Nothing is printed on stdout. - **`pnpm archilyzer `** is a new root script for the long form. pnpm prints its `$ …` line on stderr, so `-- pnpm -C "$PWD" archilyzer mcp` is a clean MCP registration (initialize handshake checked through it). - **Every bin is a subcommand.** `_cli.ts` gains PASSTHROUGH rows. They are matched on the leading words only, and everything after the path is handed on verbatim. - In-process: `build stats|templates|archives`, `docs env|files`, and `brand media` (with its argv). - As children with their own flags (`_spawnBin.ts`): `duplicates` (with the 8 GB heap its script had), `posts fetch|check`, `diarize backfill`, `digest plan|validate`, `reconcile video-dirs`, `verify transcripts`, `transcribe` (`transform.ts`; deleted with its row in the review round, `9424d357`) and `migrate channel-priority`. - A test fails when a file in `common/bin/` has no row. - **package.json.** - Kept: the scripts the publish pipeline runs by name: export's `build:index`, `build:stats`, `build:templates`, `build:archives`, `compose:site` and `compose:hub` (`build.ts`'s steps and docker Phase A), and homepage's `build:index` and `compose`. They keep their names and heaps and now call `archilyzer `. Each was checked with `pnpm run