Archilyzer · Source

archilyzer

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

commit 9a61334a986324dae398051625c8f92aed5d8122
parent abecb56d465dc943012863df725931e1a650d577
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu, 24 Sep 2026 21:46:49 -0400

plans: release 5 — record file, durable implementer rules, STATE pointer

Two parallel slices off abecb56d: R (Rumble impersonation + paced sweeps +
incomplete-not-failed) and X (per-site transcriptDownloads off), merged R → X,
one editor restart at the end. Where release-5.md corrects rumble-sweep-pacing.md
or site-exports-off.md, release-5.md wins.

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

Diffstat:
Mplans/STATE.md | 4++++
Aplans/release-5.md | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplans/tools/implementer-rules.md | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 234 insertions(+), 0 deletions(-)

diff --git a/plans/STATE.md b/plans/STATE.md @@ -9,6 +9,10 @@ prelude rolls it out. The one-sync md5 sweep from the release-3 rollout is still YouTube held a 429 cooldown across three attempts (18:43, 19:13, 19:41). See the [rollout record](one-core-phase-3.md#rollout-2026-09-24--9ab10d77-live-on-3001). +**Release 5 in flight (2026-09-24, late):** slices R (Rumble) and X (exports-off) cut off +`8b7f8924` as `one-core/r5-rumble` and `one-core/r5-exports`; plan, record and rollout in +[`release-5.md`](release-5.md); rules in [`tools/implementer-rules.md`](tools/implementer-rules.md). + **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 diff --git a/plans/release-5.md b/plans/release-5.md @@ -0,0 +1,143 @@ +# Release 5 — Rumble un-broken, visitor exports off per site, then ONE rollout + +Cut 2026-09-24 (late evening) off `main` `8b7f8924` (code `e172749b`, one-core Phase 3 complete). +Two parallel slices with no file overlap, merged **R → X**, then one editor restart at the END +(release 3 and 4 rolled the previous release out as a prelude; release 5 rolls out last because R +is what un-breaks Rumble syncing and the operator should not wait a release for it). The owed +one-sync md5 sweep rides along with that rollout. Site rebuilds (X) are separate from the restart. + +Where this file corrects `plans/rumble-sweep-pacing.md` or `plans/site-exports-off.md`, this file +wins and the slice's record commit updates that file. Implementer rules: +[`tools/implementer-rules.md`](tools/implementer-rules.md). + +## Context + +- The live editor on :3001 still serves the `9ab10d77` build (`BUILD_ID` `XsKaA_drqdAbTguxUGVsn`). + Release 4 is merged, not rolled out. `git diff --stat 9ab10d77..8b7f8924 -- umtool` is empty. +- Rumble is broken two ways. (1) Every Rumble download 403s at `https://rumble.com/embedJS/u3/` + (Cloudflare fingerprinting; upstream yt-dlp #17496, open since 2026-08-20); the editor never + passes `--impersonate`, and the pipx venv (editable install of `~/Projects/yt-dlp-patched`, the + binary the editor runs) has curl_cffi targets. The operator ran the probe 2026-09-24 — + `yt-dlp --impersonate chrome --skip-download --print title <rumble url>` printed a title. + (2) A full sweep of `the-quartering-rumble` (7,866 listed) 429s at listing page 155, yt-dlp exits + 1, the sync discards the partial listing (correctly) and never stamps `lastFullSweepAt`, so every + sync was a sweep → 44 days without sync. The operator set `fullSweepIntervalMinutes: 0` on that + channel as a stopgap. +- Exports-off: the operator's five published sites drop their visitor export surfaces; the + capability stays a per-site option, default ON, for the OSS release. +- Owed, unchanged: the one-sync md5 sweep; 188 orphan `*.tmp-*` under `transcripts/`; Anilyzer + production deploy; five sites to corpus spec 4; LM chat-only tier; `transcribeOne.ts:173`. + +## Slice R — Rumble: impersonation everywhere, paced sweeps, incomplete ≠ failed + +Branch `one-core/r5-rumble`. There is no single `configArgs`: four arg-builders drift today and +`probeChannelMeta` has none. + +| Builder | Where | Spawns | +|---|---|---| +| `configArgs` | `common/ytdlp/runYtdlp.ts:222-227` | `enumeratePlaylistUrls` :336 (sweep + paged walk + `fetchFlatPlaylistUrls`), `downloadSubsForUrl` :1097, `downloadOneAudio` :1269 | +| `channelExtraArgs` | `common/ytdlp/channelArgs.ts:13` | `downloadOneManaged.ts` :460,654,991,1035,1103,1240 (the per-video managed download), `fetchWindowManaged.ts:201` | +| inline | `common/ytdlp/metadataScan.ts:411-412` | metadata scan | +| inline | `common/controller/checkAvailability.ts:210-218` (`extraArgs` :120) | quick availability check | +| none | `common/ytdlp/runYtdlp.ts:401-448` `probeChannelMeta` | new-channel URL probe | + +1. **One table, one builder.** `common/ytdlp/channelArgs.ts`: `PLATFORM_ARGS: Partial<Record< + Platform, readonly string[]>>` = `{ rumble: ["--impersonate", "chrome", "--sleep-requests", "1"] }` + with a comment naming #17496 and the 2026-09-24 probe; `platformArgs(platform: Platform | null)`; + `channelExtraArgs(config)` prepends `platformArgs(config.platform ?? detectPlatform(config.url))` + (`common/lib/platform.ts:36-54`, `Platform` :5-22) before `ytdlpExtraArgs` so a channel override + still wins. `configArgs` delegates to it (cookies first, as now); the two inline copies call it; + `probeChannelMeta` gets `platformArgs(detectPlatform(url))` (a Rumble channel cannot even be + created today — the probe 403s). Precedent: `resolveDownloadFormatSelector` + (`common/ytdlp/downloadFormat.ts:41-58`). No new setting: the table is code; per-channel + `ytdlpExtraArgs` remains the tweak surface. +2. **A 429 mid-enumeration is "incomplete", not "failed" and not a listing.** `enumeratePlaylistUrls` + (`runYtdlp.ts:319-368`): on exit ≠ 0/101 classify the captured stderr with + `classifyDownloadFailure` (`common/lib/availability.ts:231-263`; `rate_limit` matches `http error + 429`); if `rate_limit` → throw a typed `EnumerationIncompleteError {platform, pagesReached, count}` + (page from the last `Downloading page N` line). `syncFullSweep` (`:1544-1727`; `:1557` is the + enumeration call) catches it BEFORE `acceptEnumeration`: records the backoff for the platform + via `recordDownloadBackoff` (`common/jobs/downloadBackoff.ts:31-42`; `platformBackoff` is + platform-keyed, `common/jobs/platformBackoff.ts`, set by the runner `autoRunner.ts:1936-1953`, + read by the Sync gate `pipelineActions.ts:126-142`), logs one line ("sweep incomplete: 429 at + page N of the listing, M entries — not a listing; next syncs are paged walks until the rumble + cooldown ends"), then RUNS `syncPaged` for this sync instead of failing, and never stamps + `lastFullSweepAt` (`touchLastFullSweep` is already inside `if (decision.accept)`). Exit-1-with- + other-stderr keeps throwing as today. Tolerating exit 1 generally stays out. +3. **No re-sweep while the platform cools down:** `fullSweepDue` (`runYtdlp.ts:1339-1350`) also + returns false when `platformCooldownRemainingMs` (`downloadBackoff.ts:19-27`) > 0 for the + channel's platform. +4. **403 classification, small:** `classifyDownloadFailure` has no 403 pattern (the 2026-09-24 job + read "aborted (network)" only because the traceback matched `/ssl/`). 403 from Cloudflare is a + fingerprint block, not a rate: add a `"blocked"` pattern group only if the type is cheap to + thread; otherwise record it and leave. +5. **Tests.** Unit: `channelArgs.test.ts` (rumble gets the four args before channel args; youtube + gets none; override order), `enumeratePlaylistUrls` incomplete classification, `fullSweepDue` + under cooldown. Fake yt-dlp (`editor/e2e/fixtures/bin/fake-ytdlp.mjs`; scenarios are URL + sentinels, `ratelimit` → 429 + exit 1; no "exit 1 after N pages" mode): add a `sweep429` + sentinel that prints N pages of urls then the 429 and exits 1, and have the fixture record + argv so a spec can assert `--impersonate chrome` for a `rumble.com` URL. e2e: a Rumble-URL + channel whose sweep hits `sweep429` → job succeeds as a paged walk, log has the "incomplete" + line, `.auto-queue/state.json` has `platformBackoff.rumble`, `lastFullSweepAt` absent, the Sync + button then reads "rumble is in a rate-limit cooldown" (`queues.spec.ts:38-73` pattern); + `fetch-window.spec.ts:273-296` (429 → cooldown) stays green. +6. **Rollout notes:** after R is live, remove `fullSweepIntervalMinutes: 0` from + `the-quartering-rumble` and watch one paced sweep complete; the first accepted listing after 44 + days may legitimately shrink — the two-observation guard handles it. + +Numbers: none (no file format changes). + +## Slice X — visitor exports off, per site + +Branch `one-core/r5-exports`. The editor does NOT mount `TranscriptModal`/`PlayerProvider` — the +gate is export-only. There is no `pnpm ops site-config`: `site.json` is written only by the +Settings form (`SiteForm.tsx` → `saveSiteAction` → `writeSite`). Five sites mount the modal. + +1. **Schema key `transcriptDownloads`** (boolean, absent = on), the `archives` four-site pattern in + `common/lib/siteSchema.ts` (type, docs, zod opt-out, write-if-non-default). Regenerate `SITE.md` + with `pnpm --filter yt-dlp-transcript-common exec tsx bin/file-schemas-docs.ts` (`--check` is + pinned by `fileSchemaDocs.test.ts`). +2. **Form:** `SiteForm.tsx` checkbox beside `archives` — label "Per-video transcript downloads + (Download menu, Copy Markdown, Copy download command)"; `actions.ts` reads it like `archives`. +3. **The gate:** `PlayerProvider({children, features?})` takes `features: { transcriptDownloads: + boolean }` (default all on) into its context; `TranscriptModal` hides the three controls when + off (Copy download command, the Download dropdown, Copy MD). Mount sites pass + `features={{ transcriptDownloads: currentSite().transcriptDownloads !== false }}`: + `export/app/(workspace)/SiteWorkspace.tsx`, `export/app/duplicates/page.tsx`; hub pages + `HubHome.tsx` and `AskHub.tsx` read the hub's own `_homepage` site.json the same way (the hub is a + published surface and follows the same key; default on). +4. **Tests.** Export e2e: a fixture site with `transcriptDownloads: false` → the three buttons + absent (by accessible name), default → present; an `archives: false` fixture → no `/downloads` + link, no zip manifest (new coverage); `phase3-files-numbers.ts` still reports zero unknown keys + on both sides. Editor e2e: the site form round-trips the new checkbox. +5. **Rollout (after merge and the editor restart):** for each of jeralyzer, rekietalyzer, + hasanalyzer, anilyzer, bonnellyzer: Settings form → untick archives and transcript downloads → + save → build-site + deploy-site. Verify per site: `/downloads` absent, transcript modal has no + Download/Copy MD/Copy download command, `/corpus.json` and one `page-0001.json` still 200. + `use-with-ai` and `llms.txt` already gate their archives prose. +6. Out of scope: gating the machine contract; "Copy share link"; umtool clip fetching; pruning the + archives R2 volume. + +Numbers: `plans/tools/phase3-files-numbers.ts` before/after — the new key must not appear as unknown. + +## Rollout at the end of release 5 (ONE restart) + +Template: `plans/one-core-phase-3.md` "Rollout 2026-09-24 — `9ab10d77` live on :3001". Steps: +(1) offline round-trips `phase3-settings-numbers.ts live=settings.json` and `phase3-files-numbers.ts` +over live sites/configs; (2) md5 baseline of `settings.json`, 6 `site.json`, 71 `config.json`; +(3) detached `pnpm --filter editor build` into the live `.next`; (4) ONE restart: TERM the `pnpm run +start` + `next-server` whose cwd is `<repo>/editor`, wait :3001 free, `setsid nohup pnpm run start +-H 0.0.0.0`, poll `/` and `/tags`; (5) smoke = the 8 old/new view pairs stripped of live fields, +`/api/view/<bogus>` + `invalidate-cache` 404, `/api/widget/presets` 200, `?rev=` `changed:false`, +pages 200 incl. `/channels` (rack + group Transcribe enabled with a count), `/jobs`, +`/operations/diarization`, one channel, one video, `BUILD_ID` changed, no `ZodError`; (6) md5 sweeps +after boot, after ONE sync of ONE channel (a Rumble channel proves R live), after one form save; +(7) `the-quartering-rumble`: remove the sweep override and watch one paced sweep; (8) record + +STATE. umtool: rebuild only if `git diff --stat <live>..<new> -- umtool` is non-empty. + +## Then — Phase 4 + +`plans/one-core.md:460-495` is the spec; deferred follow-ups are listed in +`one-core-phase-3.md` "Next — Phase 4". + +## Record diff --git a/plans/tools/implementer-rules.md b/plans/tools/implementer-rules.md @@ -0,0 +1,87 @@ +# Common rules for every release implementer (one-core, release 5 onward) + +You are one Opus implementer for ONE slice of one release in the repo `yt-dlp-transcript-browser`. +You work in your OWN git worktree (path given in the slice prompt), on your own branch, with your +own port block (`pnpm wt list` from the primary shows it). Other implementers work in sibling +worktrees at the same time on files that do not overlap yours; the shared files are only +`editor/CHANGELOG.md` and the release's record file (named in the prompt). + +Read `AGENTS.md` (the repo's instructions) FIRST, then `plans/STATE.md` and `plans/FACTS.md` +(verified facts, trust them over re-deriving), then the previous release's "as shipped" records +(the prompt names them) — your record must have the same shape. `node_modules/next/dist/docs/` is +the Next.js reference for this version. + +## Non-negotiables + +- **Scratch dir:** the job's `$CLAUDE_JOB_DIR/tmp` (`$T`, path in the prompt). Never `/tmp`. + Prefix your files with your slice letter. +- **Detached, then waited on in short foreground loops — never polled with `run_in_background`, + never parked on a Monitor.** Anything over a minute (builds, e2e, numbers tools) is launched + `setsid nohup sh script.sh > $T/<x>.log 2>&1 < /dev/null &` with the script ending in + `echo exit=$?`. Then wait INSIDE foreground Bash calls of at most ~100 s each: + `for i in $(seq 1 18); do grep -q 'exit=' $T/<x>.log && break; sleep 5; done; tail -3 $T/<x>.log` + — repeat the call until `exit=` appears. A subagent parked on the `Monitor` tool is never woken + (Monitor is for the parent session only), and `run_in_background` is killed at ~2 min here. + If your session is cut and resumed, re-read the logs and continue from where the logs say you + are — never redo a run whose result is on disk. +- **e2e is queued machine-wide** (`AGENTS.md`): `pnpm e2e` from YOUR WORKTREE ROOT waits for the + lock (`waiting for the e2e queue — held by …` is normal; another suite can hold it ~25–36 min). + Before launching, check no orphan holds your worktree's ports (`ss -ltnp | grep :<port>`). Write + the exact spec list to a file and pass it: `pnpm e2e $(cat $T/<x>-specs.txt)` — from the + worktree root, which runs the EDITOR suite; a spec name that does not exist is dropped and + recorded. The EXPORT suite is `node scripts/worktree.mjs run -- pnpm --filter export run e2e` + (also from the worktree root, also queued). Record `N passed, M failed, time` from the log for + every run. +- **Worktree e2e needs `export/public`:** the gitignored entries under `export/public` must be + symlinked from the primary checkout per path (under `sh`, not fish/zsh): + `for p in $(git -C /home/user/Projects/yt-dlp-transcript-browser/export status --ignored --short public | awk '{print $2}'); do p=${p%/}; ln -sfn "/home/user/Projects/yt-dlp-transcript-browser/export/$p" "<worktree>/export/$p"; done` + — check for dangling links (`find <worktree>/export/public -xtype l`) before every export build + or export-related e2e, because the live editor's build-deploy job regenerates the primary's. +- **Export build gate is `pnpm --filter export exec next build`** — NOT `run build`, which runs + `build:data` into a worktree-local `transcripts/`. +- **tsc per commit:** `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit` from the + worktree root must be clean before every commit (worktrees have no stale `.next/dev/types`). +- **Never boot a second editor against the real corpus.** Your worktree's e2e uses its own + fixture; never point anything at `<primary>/transcripts` except the read-only numbers tools, + which copy to scratch. Never hand-edit `transcripts/**`. Never restart the live :3001 editor. +- **Never stage** `settings.json.pre-priority-*` or anything under `test-results/`. +- **Commits** are small, each tsc-green, message in the repo's voice (`channels: …`, `common: …`, + `plans: …`), and every commit message ends with: + ``` + Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> + ``` + Commit incrementally (a session limit can cut you mid-slice; work on disk and in commits + survives, work in your context does not). Do NOT push. Do NOT merge into `main` — the parent + merges. A LATER slice merges `main` and re-gates once an earlier slice has landed. +- **Labels are contracts.** Every accessible name / test id the e2e specs use survives unchanged + unless the slice prompt says otherwise. Grep before renaming anything. +- **Tailwind v4, no config:** class strings must be complete literals (never `z-${n}`). +- **Do not touch files owned by another slice** (listed in your prompt). If you find you need to, + stop and say so in your report instead. +- **Record + changelog:** your last code commit is followed by a `plans:` commit adding a + "### Slice <X>, as shipped" section to the release's record file (before its "## Rollout" + heading) in the shape of the previous release's records: what/why, the commit table (sha → one + line), gates with numbers (tsc, common tests, editor unit, test:scripts, mcp, builds, e2e per + run with counts and minutes, numbers tool), what was found and left, and `[Unreleased]` bullets + in `editor/CHANGELOG.md`. If the slice's plan file was corrected by the release plan, the same + commit updates that plan file. +- **Report** to `$T/<x>-report.md` (path in the prompt): commit shas, gates with numbers, anything + not done and why, questions for the reviewer. End your final turn with `SLICE_DONE <branch tip>` + or `BLOCKED: <reason>`. + +## Gates, per slice (all from the worktree root) + +- `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit` — clean. +- `pnpm --filter yt-dlp-transcript-common test` (1738 at release 4; record the new count); + editor unit `pnpm exec tsx --test "app/**/*.test.ts"` run in `editor/` (72); `pnpm run + test:scripts` (156 + 1 skip); mcp `pnpm --filter yt-dlp-transcript-mcp test` (219) — check + `package.json` for the exact script names before running. +- `pnpm --filter editor exec next build` and `pnpm --filter export exec next build`. +- The slice's e2e spec list (in the prompt), detached and waited on as above. +- The numbers tool the prompt names, diff-empty (or "none", stated). + +## Reviews + +Reviews are a separate Opus agent, read-only, written to a file the parent names, with a verdict +line `SHIP | SHIP AFTER FIXES | BLOCK`. Fixes go back to the implementer by message; the reviewer +re-reads the fix commits.