Archilyzer · Source

archilyzer

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

commit 9cbed498b846120abc958645fef892d22b30f332
parent 70b97caa2789699c425922563771da20628f984d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 10:55:30 -0400

plans: release 7 — YouTube lane pacing + the archilyzer CLI, then one rollout (releases 6 + 7)

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

Diffstat:
Aplans/release-7.md | 317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 317 insertions(+), 0 deletions(-)

diff --git a/plans/release-7.md b/plans/release-7.md @@ -0,0 +1,317 @@ +# Release 7 — YouTube lane pacing + the archilyzer CLI, then ONE rollout (releases 6 + 7) + +Written 2026-09-25 morning on `main` `0032ed8a`. Verified by three Explore passes and one design +pass; anchors are on `0032ed8a`. Operator decisions (2026-09-25): release 6 is NOT rolled out on its +own; release 7 = two parallel slices + one rollout at the end that brings 6 + 7 live, editor restart +AND umtool rebuild + restart; scope = pacing slice + CLI slice (Phase 4 slice 2 + the rest of spec +item 1 + hub deploy path + the posts-only fix); Phase 4 slice 3 (config + docs, `doctor`, `run`, +`mcp`) waits. Rules: `plans/tools/implementer-rules.md`. Record file: this file. + +## Context — where things stand + +- **Live on :3001:** `93dcb532` (releases 4 + 5), `BUILD_ID` `FKE60BTWpUiUa94WCSxTO`, next-server + 888107. umtool on :3050: pid 5881, `BUILD_ID` `JKtTfoVrUbTosNENO3GMj` (Sep 22 build). +- **`main` = `0032ed8a`**: release 6 merged — follow-ups `4d97049f` (410 → deleted; umtool gets the + platform args through ONE table in `common/ytdlp/platformArgs.mjs`; atomic remote-transcript + write; measure-nav) and Phase 4 slice 1 `cda35622` (`buildDeployCore.ts` → `common/publish/build.ts`, + aws-sdk deps to common). Record `plans/release-6.md`. +- **Rollout hazard already behind us:** `pnpm install` ran in the primary at 01:12 — + `editor/node_modules/@aws-sdk` pruned, `common/node_modules/@aws-sdk/{client-s3,lib-storage}` + present, `umtool/report-to-video/node_modules/yt-dlp-transcript-common → ../../../common` present. + The live build's `editor/.next/node_modules/@aws-sdk/client-s3-3c41832e78b52e1d` resolves into + `node_modules/.pnpm/` (safe while the old build serves). Install MUST precede the editor build. + umtool changed in release 6 (4 files) → this rollout rebuilds + restarts umtool too; its jobs + already spawn the new `build-video.mjs` from disk through the symlink. +- **Why pacing:** `plans/youtube-lane-pacing.md` — the 2026-09-24 evening's three YouTube cooldowns + were ONE Short (`quarteringvlogs/ncdPaDSqt-c`) retried 12× from the head of the queue; with + `order: "listed"` the runner re-picks the same video after every cooldown and `fails` climbs to the + 30-min cap; a manual Sync reads the same cooldown and is refused. Every retained YouTube 429 is the + subtitle fetch, per video, not IP-wide. Also: `metadataScanStore.ts:250` never refreshes an + identical error's `at`, so 142 members-only `legal-mindset` videos are re-scanned (cookie-authed, + ~1 req/s) at EVERY runner start. +- **Why the CLI now:** `build:hub` has no deploy path (the hub's `transcriptDownloads: false` is set + but no hub was rebuilt); `export/package.json:26` `deploy` runs wrangler with NO `--project-name`; + the `build`/`build:nodata` twins are identical bodies differing only by npm's `prebuild` hook; + `docker/{build-site,publish-site}.sh` call `pnpm run build:nodata|build` directly. +- **Posts-only 404 root cause** (jeralyzer `thequartering-X`): a social channel (`sourceKind: + "social"`, `channelConfig.ts:25-28,116`) never enters the video scan so `count` stays 0; + `buildIndex.ts:1052-1113` still writes `transcripts/<slug>/manifest.json` with `pageCount: 0`; + `compose-site.ts:602-606` `reconcileChannelTree` computes `dirSignature(src, "manifest.json")`, + gets `""` for a manifest-only tree and `rm(dest)` — while `corpus.ts:248` advertises + `manifests.transcripts` unconditionally (and `llms.txt` at `corpus.ts:403`). +- `archilyzer.pages.dev` answers 522 because the `homepage` package (Pages project `archilyzer`, + `homepage/package.json:18`) has NEVER been deployed; the hub has never been deployed either. + Operator decision 2026-09-25: keep them two apps on two projects (`archilyzer` = homepage, + `archilyzer-hub` = the hub), cross-linked; both get their first deploy in this rollout. +- Not in this release: the LM chat-only tier; "hub absorbs homepage". + +## Slice Y — YouTube lane pacing (branch `one-core/r7-pacing`) + +**Decision: the per-video deferral is PERSISTED beside `platformBackoff`, not in-memory.** `fails` +already survives a restart (persisted at `autoRunner.ts:1965`, kept by `pruneExpired` on boot :1154); +an in-memory deferral would make every restart (rollouts restart the editor) re-hit the same video +at `fails+1`. The status view already reads `inputs.state[kind]` (`common/views/autoQueueStatus.ts:140`), +and e2e can seed it exactly like `queues.spec.ts:48-61` seeds `platformBackoff`. + +1. **Pure helpers, `common/jobs/platformBackoff.ts`** (append; `nextBackoff` :31-40 untouched): + `VideoDeferral = {until, channelSlug}`, `VideoDeferralState = Record<videoId, VideoDeferral>`, + `VIDEO_RATE_LIMIT_DEFER_MS = 6 h`, `deferVideo`, `isVideoDeferred`, `pruneDeferred`, + `coerceVideoDeferrals` (mirror `coercePlatformBackoff` :79). `platformBackoff.test.ts` +5. +2. **State, `common/jobs/autoQueueState.ts`**: `AutoQueueKindState` :36-44 gains `videoDeferrals`; + `emptyAutoQueueKindState` :56, `coerceKindState` :108-112, the trim in `writeAutoQueueState` + :147-151. Old state without the key → `{}`; an older build ignores the key (rollback-safe). + `autoQueueState.test.ts` +2. +3. **The one seam, new `common/jobs/unitOutcome.ts`**: `applyUnitOutcome(state, unit, now, rand?) → + {markCompleted, line}` — the body of `autoRunner.ts:1941-1963` moved and extended: `rate_limit` → + `nextBackoff` + `deferVideo` + line `Auto-download: <pf> rate_limit — backing off <s>s (attempt + <n>). <id> deferred 6h; next video after cooldown.`; `network` → backoff only (today's line); + `transcribed` → `clearBackoff`; every branch `pruneDeferred`. `autoRunner.ts:1941-1963` becomes + the call + `onLog(line)` + `markCompleted`; `persist()` :1965 unchanged. `unitOutcome.test.ts` + (~6 cases incl. `fails` climbing only across distinct ids). This is the testable seam the plan + file wanted (`runLoop` :1136 is not exported; `autoRunner.test.ts` covers only exports :6-14). +4. **Pick filter + idle reason, `autoRunner.ts`**: boot prune :1154 adds `pruneDeferred`; the + download branch :1648-1655 also drops ids with `isVideoDeferred` and sets `anyDeferred`; + :1740-1741 → `anyCooling ? "cooldown" : anyDeferred ? "deferred" : "capped"`; + `AutoRunnerIdleReason` :208-210 gains `"deferred"`; `editor/app/operations/components/dispatch.ts:152` + gains the sentence `every pending video was rate-limited recently and is deferred` (exhaustive + switch, tsc forces it). Manual Sync / download-missing do not consult deferrals (say so in the + record; a manual retry still runs and on success clears the platform cooldown). +5. **Scan-error `at` refresh, `metadataScanStore.ts:250`**: also rewrite when `Date.parse(now) - + Date.parse(prev.at) >= METADATA_SCAN_ERROR_COOLDOWN_MS` (`now` is the ISO arg :219; the + constant :334 is module-level). `metadataScanStore.test.ts` +2 (25 h later rewrites; 1 h leaves). +6. **View, `common/views/autoQueueStatus.ts`**: `VideoDeferralView {videoId, channelSlug, untilMs}`; + `AutoQueueKindStatus` :60 gains `deferred: VideoDeferralView[]`; `buildKind` :140-145 fills it. +7. **Strip, `RunnerOperationView.tsx:196-225`**: render when cooldowns OR deferred; `CooldownStrip` + becomes `<section role="region" aria-label="Rate-limit cooldown">` with the existing heading, + `<ul aria-label="Platforms in cooldown">` (items unchanged), and when deferred: heading + "Deferred videos — skipped by auto-download until:" + `<ul aria-label="Deferred videos">` with + `<li>` `alpha/a1 — 5h 59m left` (link like `RecentPicks` :167-169). `formatCooldown` + (`dispatch.ts:202-207`) gains an hours arm (grep callers first). **Accessible names, exact:** + region `Rate-limit cooldown`; lists `Platforms in cooldown`, `Deferred videos`. +8. **e2e, new `editor/e2e/pacing.spec.ts`** (copy `makeDownloadChannel` :110-149, `getStatus`/ + `pickOrder` :185-198, the settings block :772-779 from `auto-queue.spec.ts`): + - T1 seeded: alpha `["a1","a2"]`; `state.json` `download.videoDeferrals = {a1: {until: now+1h, + channelSlug: "alpha"}}`; start the runner; picks → exactly `["a2"]`; `idleReason === "deferred"`; + `/operations/download` has `getByRole("region", {name: "Rate-limit cooldown"})` and + `getByRole("list", {name: "Deferred videos"})` containing `alpha/a1`. + - T2 live 429: **new fake sentinel `dl429`** in `fake-ytdlp.mjs` `modeYoutubeSingleUrlManaged` + (:434-446): stderr `ERROR: [youtube] <url>: Unable to download video subtitles for 'en': HTTP + Error 429: Too Many Requests`, exit 1 (`availability.ts:246` → `rate_limit`); the prefetch branch + (:770-795, `urlSentinels` :143-157) still succeeds — the real two-spawn shape. alpha + `["dl429vid1","a2"]`; poll `state.json` for `videoDeferrals.dl429vid1` + `platformBackoff.youtube + .fails === 1`; then poll picks until `a2` (timeout 100 s; base cooldown 60 s ±10 %); + `pickOrder` = `["dl429vid1","a2"]`. `test.setTimeout(150_000)`. + - Keep green: `fetch-window queues rumble-sweep auto-queue lane-runner` (idleReason readers + `auto-queue.spec.ts:1105`, `lane-runner.spec.ts:247,518`). +9. **`sleepBetweenDownloadsSeconds` in the lane: OUT.** The failing attempts were already 93 s to + 30 min apart; it would prevent none of the observed 429s and adds a second timer every fixture + must neutralise. Ship the deferral alone so the first evening's log answers one question. +10. Numbers: none (`state.json` is outside both numbers tools); say so. `videoDeferrals: {}` appears + on all four lanes at the first persist after boot — the expected change outside the md5 baseline. + +Y owns: `common/jobs/{platformBackoff,autoQueueState,unitOutcome}.ts` (+tests), +`common/controller/{autoRunner,metadataScanStore}.ts` (+test), `common/views/autoQueueStatus.ts`, +`editor/app/operations/components/{RunnerOperationView.tsx,dispatch.ts}`, +`editor/e2e/fixtures/bin/fake-ytdlp.mjs`, `editor/e2e/pacing.spec.ts`. Gates: rules' list; e2e +`pacing fetch-window queues rumble-sweep auto-queue lane-runner`; numbers "none". + +## Slice C — the archilyzer CLI (branch `one-core/r7-cli`) + +1. **Parser: hand-rolled** (no `commander`/`yargs` in the workspace; `minimist` only transitively). + `common/bin/_parseFlags.ts` gains `parseArgv(argv) → {flags, positionals}` beside `parseFlags` :4. + New `common/bin/_cli.ts`: `Command = {path: string[], usage, run({positionals, flags, env}) → + Promise<number>}`, `resolveCommand` (longest-path match), `usage()`. `common/bin/archilyzer.ts` = + the table with lazy `await import("./compose-site")` per subcommand. `common/package.json` `test` + glob gains `bin`. `_cli.test.ts`. +2. **Bins become import-and-call.** `build-index`, `build-stats`, `build-chart-templates`, + `build-archives`, `compose-site`, `compose-hub`, `compose-homepage`, `sync-tick`, + `settings-example`, `file-schemas-docs` each `export async function main(opts)` and guard the + auto-run with the `scripts/worktree.mjs:350` idiom (`import.meta.url === pathToFileURL( + process.argv[1]).href`) so `tsx bin/x.ts` keeps working (`fileSchemaDocs.test.ts:25` names + `bin/file-schemas-docs.ts`). Env → argument where a subcommand names it: `compose site <id>` + passes `siteId` (`compose-site.ts:634` reads `SITE_ID` today; error if neither). Flag-driven bins + (`_parseFlags` users) are untouched — `run <operation>` material for slice 3. +3. **Named entry points, `common/publish/build.ts`** (thin, over the existing `run*`; keep every + `run*` export, `build.test.ts` pins three): `buildSite(siteId, {paths, onLog, signal, skipData, + skipArchives})`, `deploySite(siteId, {…, previewBranch})` (= `getSite` + `builtSiteProblem` + + `runArchiveUploadIntoLog` + `runDeployIntoLog`, the body of `deployAction.ts:24-80` minus the + pre-job refusals, which stay in the action), `buildAll({…, mode: "docker"|"basic", skipArchives})` + (docker → `runDockerBuildAllPhase` :456; basic → the serial loop lifted from + `buildAction.ts:328-360`), `composeHub`, `buildHub`, `deployHub`, `composeHomepage`. Editor + actions keep `runManagedFunction` (`streamCommand.ts:289`) and call these. +4. **Twins collapse + `prebuild`.** `runBuildPhase` :52-88 stops spawning `pnpm run build|build:nodata` + and runs the steps itself: `!skipData` → `runHostScript(…, "build:data")` (already used :456), + then `pnpm run compose:site` (env block :80-87), then `pnpm exec next build`, all in `export/`, + same `[notice]` lines. Extract `buildSiteSteps(opts) → [{command, args, env}]`, pin it in + `build.test.ts`. `export/package.json`: `build` → `tsx ../common/bin/archilyzer.ts build site` + (`SITE_ID` from env; `pnpm run build -- --nodata` forwards), **delete `build:nodata` :18 and + `prebuild` :16** (pnpm runs pre-scripts by default, no `.npmrc` override — `build.ts:59-63`), `build:hub` + :19 → `… build hub`, `deploy` :26 → `… deploy site` (refuses without `SITE_ID`/project). Root + `package.json`: `build:index` :8 → `archilyzer index`, `sync:tick` :9 → `archilyzer sync tick`; + `build`/`build:export` stay aliases. `homepage/package.json:11-13` twins stay (different app). +5. **Docker.** `docker/build-site.sh:40` → `pnpm --filter yt-dlp-transcript-common exec tsx + bin/archilyzer.ts build site "$SITE_ID" --nodata`; `publish-site.sh:38` → `… build site "$SITE_ID"`. + `Dockerfile.build` copies root/common/export `package.json` (tsx is in common devDeps :28); + ENTRYPOINT :44 unchanged. `Dockerfile:246-253` copies every package.json → the editor image has + the CLI. +6. **Hub build + deploy.** The hub is `export` under `INSTANCE_MODE=hub` (`export/app/lib/site.ts:29-44`), + out = `export/out` (shared with site builds, `resolveOutDir` :29). `buildHub` = `pnpm run + compose:hub` then `INSTANCE_MODE=hub pnpm exec next build` in `export/`; before composing + `rm -f public/site.json`, and symmetrically compose-site removes `public/hub-sites.json` (compose-hub + touches only `hub-sites.json`, `compose-hub.ts:61-62`) so `out/` says what it is. + `common/lib/builtExport.ts` gains `builtHubProblem(outDir)`. `deployHub` reads + `getHomepageConfig(paths).cloudflareProject` — **the key, form field and writer already exist** + (`common/lib/homepage.ts:33-34,83-86,144-147`, `HomepageConfigForm.tsx:56-62`, + `homepageActions.ts:33-43`); refusal `The hub has no Cloudflare Pages project configured — set it + on /sites under Hub.`; then `pagesDeployArgs({outDir, project, previewBranch})` (`pagesDeploy.ts:65`) + via `runChildIntoLog`, mirroring `runDeployIntoLog` :267-300 incl. `deploymentUrlIn`. Editor: + `editor/app/sites/lib/hubActions.ts` (`"use server"`): `buildHubAction`, `deployHubAction`, + `buildAndDeployHubAction` → `runManagedFunction` kinds `build-hub` / `deploy-hub` / + `build-deploy-hub`, queueKeys `"build"` / `"deploy"` (`buildAction.ts:38-39`). UI: + `HubBuildButtons.tsx` (copy `BuildAllSitesButton.tsx:18-70` + `JobLane`) in the Hub section + `editor/app/sites/page.tsx:181-197`. **Names, exact:** buttons `Build hub`, `Deploy hub`, checkbox + `Deploy after build`; `JobLane` titles `Build hub` / `Deploy hub` / `Build & deploy hub`. Ops + adapters: `editor/app/api/ops/{build-hub,deploy-hub}/route.ts`, `scripts/archilyzer-ops.mjs:95-100` + ACTIONS + its test, `ops-api.spec.ts` +1 (deploy-hub refuses when the fixture `homepage.json` has + no project). CLI: `build hub`, `deploy hub [--preview <b>]`. + **Hub Pages project: `archilyzer-hub`** (operator decision 2026-09-25: two projects, cross-linked; + the `homepage` package keeps `archilyzer` = https://archilyzer.pages.dev, never deployed yet). + Rollout sets on `/sites` → Hub: Cloudflare project `archilyzer-hub` (today the file says + `archilyzer` — the collision the design found) and `siteUrl` `https://archilyzer-hub.pages.dev` + (`compose-hub.ts:69-86` needs it for the hub's `corpus.json`/`llms.txt`/`robots.txt`). + **Cross-links (C owns):** the homepage hero (`homepage/app/page.tsx:79-85`, beside "Download the + source" / "Read the setup guide") gets a "Search all archives" button to `homepage.json.siteUrl` + (the hub's URL — the same field `hubSite()` reads; rendered only when set); the hub's footer + "Built with Archilyzer" (`export/app/components/Footer.tsx:12,85`) already points at `PROJECT_URL`. + **Docs:** `mcp/README.md:457,508` hub examples → `https://archilyzer-hub.pages.dev`; the hub-URL + hints in `SETTINGS.md:486` / `settingsSchema.ts:1518` / `HomepageConfigForm.tsx:52` / `SiteForm.tsx:251` + are checked and repointed if they name archilyzer.pages.dev as a hub. `README.md:13,474` and + `SETUP.md:8,175` keep pointing at the homepage (docs + tarball) — unchanged. CLI also gains + `build homepage` / `deploy homepage` (thin: `composeHomepage` + `next build` in `homepage/`; + deploy = `pagesDeployArgs` with the constant project `archilyzer`, replacing the hardcoded + `homepage/package.json:18` line) so the rollout deploys the homepage headlessly too. +7. **Posts-only 404 — fix the composer, keep the contract.** `compose-site.ts:602-606`: when the + signature is `""` but `src/manifest.json` exists, use a constant signature and copy the tree; a + `pageCount: 0` manifest is exactly what "0 transcripts" means; `corpus.ts:248` stays + unconditional, spec stays 4, readers untouched. New `common/bin/compose-site.test.ts` (export + `reconcileChannelTree`; possible once step 2 guards `main()` :916): manifest-only tree copied, + unchanged tree skipped, no manifest → removed. No export e2e can pin it (no spec reads + `corpus.json`; fixture site has only `test-youtube`) — say so; `editor/e2e/build.spec.ts:10` + pins the 0-transcript manifest on the index side. +8. **Cut line.** Ships: `index`, `compose site <id>|hub|homepage`, `build site <id> [--nodata] + [--skip-archives] | all [--skip-archives] | hub`, `deploy site <id> [--preview b] | hub [--preview b]`, + `sync tick`, `settings example [--check]`. **Deferred to slice 3:** `doctor` (inputs + `paths.ts:232-264`, `umtool/lib/tools.mjs probeTools` + `doctor.ts`, `scripts/worktree.mjs:16-28 + PORT_BASES`), `run <operation>`, `mcp`, "port defaults exist once". +9. **Gates (C)**: tsc; common tests (+`_cli`, `compose-site`, `build`); editor unit 72; `pnpm run + test:scripts` (159 + 1 → +N); `pnpm --filter editor exec next build`; `pnpm --filter export exec + next build` (NOT `run build`); export e2e in full (192) + `e2e:hub` (8); editor e2e `sites-crud + site-publish-preview build deploy-page cut-release channel-build-toggle ops-api`; + `phase3-files-numbers.ts` diff-empty. + +C owns: `common/bin/**`, `common/package.json`, `common/publish/build.ts` (+test), +`common/lib/builtExport.ts`, `export/package.json`, root `package.json`, `homepage/package.json`, +`homepage/app/page.tsx` (hero button) + its e2e `homepage/e2e/marketing.spec.ts`, +`docker/{build-site,publish-site}.sh`, `editor/app/sites/**`, +`editor/app/api/ops/{build-hub,deploy-hub}/`, `scripts/archilyzer-ops.{mjs,test.mjs}`, +`editor/e2e/ops-api.spec.ts`, `mcp/README.md` (hub URL examples), the hub-URL hint strings named +above. Gates add the homepage e2e (`pnpm --filter homepage run e2e`, 5 specs — check the script name). + +**Overlap Y ↔ C: none.** Shared only `editor/CHANGELOG.md`, `plans/release-7.md`. Merge order Y → C +(C merges `main` and re-gates). FACTS/STATE: parent, after both merge. Worktrees: `pnpm wt add +one-core/r7-pacing` and `one-core/r7-cli` off `main`; symlink `export/public` per path; adding +worktrees shifts port blocks. Reviews: Opus, read-only, `SHIP | SHIP AFTER FIXES | BLOCK`. + +## Rollout at the end (releases 6 + 7 together; ONE editor restart + umtool restart) + +Release-5 procedure (`plans/release-5.md` "## Rollout 2026-09-24 (night)") plus: +1. Final suites on the merge sha in a detached worktree: editor full, export full, `e2e:hub`. +2. Primary: `pnpm install --frozen-lockfile` (expected no-op; verify the three node_modules facts + above). MUST precede the editor build. +3. Numbers on the live corpus: settings (1,353 paths, diff empty), files (77 `unknown keys: []`, + 3,839 lines) — no key changed in 6 or 7 (`cloudflareProject` on `homepage.json` predates both). +4. md5 baseline: `settings.json` + 6 `site.json` + 71 `config.json` + `homepage.json` (79); + `jq '.download|keys' .auto-queue/state.json` before (no `videoDeferrals`). +5. Detached builds: editor into the live `.next` (`FKE60BTWpUiUa94WCSxTO` → new; then + `readlink -f editor/.next/node_modules/@aws-sdk/client-s3-*` resolves into `node_modules/.pnpm/`); + umtool (cwd `umtool/`, `JKtTfoVrUbTosNENO3GMj` → new). +6. ONE editor restart (TERM pnpm + next-server cwd `editor/`, `setsid nohup pnpm run start -H + 0.0.0.0`, `/` + `/tags` 200, no `ZodError`), then umtool restart (TERM 5881, its `start` on :3050, + `/` 200). +7. md5 sweep after boot: identical. `.download.videoDeferrals == {}` after the first unit. +8. Smoke = release-4 script (eight `/api/view/*` pairs, 404s, presets, `?rev=`, pages) plus: + `autoQueueStatus` `.download.deferred == []`; `/operations/download` 200 with NO `Rate-limit + cooldown` region while nothing cools; `/sites` shows `Build hub` + `Deploy hub`; umtool `/` 200; + **one site Build** `pnpm ops build-site {"siteId":"anilyzer","skipData":true,"skipArchives":true} + --wait` exit 0 (proves the SDK resolves from common AND the reshaped build: compose + `next + build`, no data phase, no `prebuild`). +9. Hub + homepage, first deploys ever. On `/sites` → Hub set Cloudflare project `archilyzer-hub` + and `siteUrl` `https://archilyzer-hub.pages.dev` (through the form; md5 of `homepage.json` moves + once, expected). `pnpm ops build-hub --wait` (first ever hub build: `export/out/hub-sites.json` + with five members, `corpus.json`), `pnpm ops deploy-hub --wait`; + `curl https://archilyzer-hub.pages.dev/{hub-sites.json,corpus.json}` 200, federated search + returns a hit from two different sites, the transcript modal has no export controls. Then the + homepage: `archilyzer build homepage` + `deploy homepage` (project `archilyzer`) → + `https://archilyzer.pages.dev/` 200 (no longer 522), `/docs/install/` 200, + `/downloads/archilyzer-source.tar.gz` 200, hero shows "Search all archives" → the hub. + The MCP quickstart `hub:https://archilyzer-hub.pages.dev#jeralyzer,…` then works — try one + `list_sources` through it. +10. Owed sync + md5: one `pnpm ops sync {"slug": <small youtube channel>} --wait`; only that + `config.json`'s stamps move. +11. 410 re-read: `rekietalaw-rumble/v7e07us` and the four quartering ids read `deleted` at their + next check (observe, do not force). +12. Pacing is live — a 429 cannot be provoked; proof = (i) the region + `deferred: []`, (ii) the + first real 429's line `… (attempt 1). <id> deferred 6h; next video after cooldown.`, (iii) the + next evening: `grep -h 'deferred 6h' transcripts/.jobs/*.log` — each id at most once per 6 h, + `attempt N` climbing only across distinct ids, the owed sync never refused for a single-video + cooldown. Scan-error refresh: the first runner start still bursts `legal-mindset`'s 142 cookie + requests ONCE; a second restart within 24 h logs no re-scan. +13. Record this file's "## Rollout", STATE head, FACTS section, memory; morning runbook + (`~/reports/release-7/make-runbook.py` → `RUNBOOK.html`, the release-5 generator as the model). + +## Verification (end to end) + +Per slice: the rules' gate list + the spec lists above; Y numbers "none", C `phase3-files-numbers` +diff-empty. Post-merge: step 1 suites; rollout steps 2-12 each with a recorded number. Y's live +proof is the evening watch (12); C's is the site build (8) and the hub (9). + +## Hazards + +- **Labels are contracts.** New: region `Rate-limit cooldown`, lists `Platforms in cooldown` / + `Deferred videos`, buttons `Build hub` / `Deploy hub`, lane titles above. Unchanged and asserted + elsewhere: `Sync notice` + the `rate-limit cooldown` sentence (`queues.spec.ts:66`, + `rumble-sweep.spec.ts:160`), heading `Build all sites` (`deploy-page.spec.ts:22,38`), `idleReason` + values (`auto-queue.spec.ts:1105`, `lane-runner.spec.ts:247,518`). +- **Runner state across restart**: deferrals persist by design; manual Sync/download-missing ignore + them; an older build drops the key on its next write (rollback-safe). +- **`Dockerfile.build`**: `runDockerBuildOne` mounts the host `build-site.sh` (:437-439) but the CLI + and the new `export/package.json` are BAKED — an old image running the new script fails (old + `build` still has `prebuild`). Check `ensureBuildImage`'s rebuild trigger; the first docker-mode + build after C may need one `docker build -f Dockerfile.build`. Live rollouts use host mode. +- **`export/package.json` `deploy` had no project name** — the CLI refuses without one. The hub + is `archilyzer-hub`; the homepage stays `archilyzer` — `homepage.json` currently says + `cloudflareProject: "archilyzer"` and MUST be changed on the form before `deploy hub`, or the + hub overwrites the project page. `deployHub` refuses the literal value `archilyzer` as a guard. +- **Hub vs homepage stay two apps** (decision 2026-09-25). The homepage owns the software's public + paths (`/docs/install/`, `/downloads/archilyzer-source.tar.gz`, linked from README/SETUP/AGENTS) + and `PROJECT_URL` is stamped into every footer and corpus generator string; the hub is one + operator's federation. "Hub absorbs homepage" is a possible later slice (~30 files, 5 e2e, the + docker `homepage` service), not this release. +- **`prebuild` semantics**: the collapse works only because the data phase becomes an explicit step + in `buildSite` and `prebuild` is deleted; `--nodata` still assumes a prior full build's staging. +- **Shared `export/out`**: a hub build and a site build overwrite each other; `builtHubProblem` / + `builtSiteProblem` refuse the wrong deploy — never rebuild silently. +- **e2e timing**: T2 waits a real 60 s ±10 % cooldown; expect ~2 min behind the machine-global + queue. `formatCooldown` needs the hours arm or a 6 h deferral reads `359m 58s`. +- Session budget (memory `session-limit-pacing`): Y and C in parallel = the two heavyweight jobs of + one window; reviews and the rollout in the next. + +## Then + +Phase 4 slice 3 (config + docs + `doctor`/`run`/`mcp`, `PUBLISH.md` absorbing `DEPLOY_DOCKER.md` + +`DEPLOY_CLOUDFLARE.md`); the-quartering-rumble retry-cadence stamp only if live sweeps come back +incomplete; the LM chat-only tier (operator config). + +## Record + +## Rollout