Archilyzer · Source

archilyzer

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

commit 8487b6f5a8c51929f064aafd4fd030cbd7b5946c
parent 7490472bdd8784aad64435f6937baae47bbaf442
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 11:49:19 -0400

plans: release 12 — the source mirror plan and the record file

The plan (written 2026-09-27, re-checked 2026-09-28 against 7490472b)
enters the repo on slice Q's branch as plans/source-mirror.md, verbatim;
plans/release-12.md takes release 11's shape (header, the slices, Record,
Rollout).

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

Diffstat:
Aplans/release-12.md | 36++++++++++++++++++++++++++++++++++++
Aplans/source-mirror.md | 464+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 500 insertions(+), 0 deletions(-)

diff --git a/plans/release-12.md b/plans/release-12.md @@ -0,0 +1,36 @@ +# Release 12 — the source mirror: a read-only git clone on the project site + +`main` at `90c7f776` (release 11 merged in full and NOT rolled out: the cut, the :3001 restart, a +settings save and the deploys are still owed by the operator, `release-11.md` "Rollout"). Release +12 is the operator's ask of 2026-09-27: the repo mirrored on the project site +(https://archilyzer.pages.dev) as static, read-only files — `git clone +https://archilyzer.pages.dev/source/archilyzer.git` over git's dumb-HTTP protocol, a raw tree at +`/source/tree/`, the tarball regenerated per deploy, and a gate that refuses to publish a denied +literal. Plan: [`source-mirror.md`](source-mirror.md) (written 2026-09-27 in plan mode, re-checked +2026-09-28 against `90c7f776`; it entered the repo as slice Q's first commit, never as a direct +commit to `main`). Rules: `plans/tools/implementer-rules.md`, with the commit trailer this release's +prompts give. + +**The operator's standing choices** (`source-mirror.md`, "Decisions"; not re-opened): +- **Mirror = `main` only.** `/home/user → /home/user` is scrubbed in file contents AND commit + messages; the `Co-Authored-By` and ` +- **The private repo's history is never rewritten.** The mirror is generated by `git-filter-repo` + on a fresh bare clone at every homepage build; its commit ids differ from the private repo's. +- **Operator-private inputs live outside the repo**, in `${ARCHILYZER_CONFIG_DIR ?? + ~/.config/archilyzer}/`. Nothing carrying the username is in code. +- **Working alongside the parallel session:** nothing is edited in the primary checkout; each slice + has its own worktree, and the parent merges with `git merge --no-ff` only on a clean tree. + +## The slices + +| Slice | Branch | What | Owns | +|---|---|---|---| +| Q | `r12/paths-fix` | Fix-forward the hardcoded paths: umtool's `CHANNELS_DIR` from the repo root, `VIDEO_ROOT` and `SONG_DATA` from the home dir (XDG for the song data), the 20 one-off `umtool/song/*.sh` run logs deleted, the machine paths stripped from three tracked umtool manifests, `/run/media/user` out of `common/` | `umtool/**`, `WORKTREES.md`, `common/controller/storageLocations{,.test}.ts`, `common/lib/storageVolumes{,.test}.ts`, `common/views/storage.test.ts`, `common/lib/envVars.ts` (umtool defaults only) + `ENVIRONMENT.md` | +| R | `r12/source-mirror` | `archilyzer source publish` (filter-repo scrub, repack, dumb-HTTP files, the audit gate, the raw tree, the tarball) run by `buildHomepage`; the `/source/` page; doctor's source block; the docs; `create-archives.sh` deleted | `common/publish/{source,sourceAudit,sourceTree}.ts` + tests, `common/lib/sourceManifest.ts`, `buildHomepage`, `common/bin/{archilyzer,doctor}.ts` + tests, `common/lib/{paths,envVars}.ts` (new variables), `homepage/**`, `.gitignore`, the docs | + +**Order:** Q lands first; R branches from `main` after Q merges. The shared files are +`editor/CHANGELOG.md` (`[Unreleased]`) and this record. + +## Record + +## Rollout diff --git a/plans/source-mirror.md b/plans/source-mirror.md @@ -0,0 +1,464 @@ +# Release 12 — the source mirror: a read-only git clone on the project site + +Written 2026-09-27 in plan mode, RE-CHECKED 2026-09-28 against `main` `90c7f776` (128 commits later; +see "What changed since the plan was written"). Self-contained for a fresh session. The design was +verified by three Explore passes and one Plan pass; the dumb-HTTP clone, the filter-repo scrub and +wrangler's upload filter were PROVED in scratch. Rules: `plans/tools/implementer-rules.md` (one Opus +implementer per slice in a worktree, one read-only Opus review, the parent merges). Two slices, Q +then R. Record file: `plans/release-12.md` (NEW, the shape of `plans/release-11.md`). Memory to read +first: `release-11-overnight`, `fetch-clip-shipped`, and the audit at +`~/reports/repo-mirror-audit/AUDIT.html`. + +## What changed since the plan was written (re-check 2026-09-28) + +- **"Release 11" is taken**: the overnight of 2026-09-28 merged slices O1–O6 (+ O2b, O1c, O6c) to + `main`, NOT rolled out (owed by the operator: cut, :3001 restart, a settings save, the deploys; + runbook `~/reports/overnight-2026-09-28/MORNING.html`). This work is **release 12**, slices Q and R. +- `DEPLOY_CLOUDFLARE.md` is GONE, absorbed into **`PUBLISH.md`** (`## Cloudflare Pages` :70, + `## Download archives and R2` :137). Every doc edit this plan aimed at it goes to `PUBLISH.md`. +- **Env vars are a registry**: `common/lib/envVars.ts` declares every variable the code reads, a + test fails on an undeclared read, `ENVIRONMENT.md` is GENERATED (`pnpm archilyzer docs env`, + `--check` in the gates), and path overrides are read by `getPaths()` (`common/lib/paths.ts`) and + nowhere else. Test-only variables carry an `E2E_` prefix and are declared in the playwright config. +- **`archilyzer doctor`** exists (`common/bin/doctor.ts` + test) — slice R adds its checks there. +- **The homepage builds from `/sites` now** (release 11 O4): `editor/app/sites/lib/ + homepageDeployActions.ts:51,95` call `buildHomepage` IN-PROCESS, and `pnpm ops build-homepage` + reaches them. So the source step also runs inside the editor's process and its PATH. +- Anchors that moved: `buildHomepage` is `common/publish/build.ts:973`; `WORKTREES.md:119`; + `umtool/components/BuildChain.tsx:147-149`. The umtool path literals are where they were. +- Baselines now: common **2,112**, editor unit 85, `test:scripts` **185 + 1 skip**, mcp 269, + homepage unit 2, homepage e2e **31** (specs: brand, docs, downloads, instance-colours, marketing, + no-data, stats, theme). +- Still true: 1,985 lines with `/home/user` outside `plans/`, 20 `umtool/song/*.sh`, `/run/media/user` + in five `common/` files AND one line of `editor/CHANGELOG.md`; `create-archives.sh` exists and + nothing calls it; pipx and gitleaks installed, git-filter-repo not; `~/.config/archilyzer/` absent. + +## Working alongside the parallel session (operator's instruction, 2026-09-28) + +- **Nothing is edited in the primary checkout.** Each slice gets its own worktree from `pnpm wt + add`: `r12/paths-fix` → `/home/user/Projects/r12-paths-fix`, then `r12/source-mirror` → + `/home/user/Projects/r12-source-mirror` (branched after Q merges). Seven `r11-*` worktrees exist + and are the operator's to remove; do not touch them. +- **This plan enters the repo on the slice branch**, as `plans/source-mirror.md` in Q's first + commit — never as a direct commit to `main`. +- **Merging into `main`** happens in the primary with one command, only when `git status --short` + is empty and no merge is in progress: `git merge --no-ff r12/<slug>`. If the tree is dirty (the + parallel session is mid-change), wait and re-check; never stash or touch its files. Expected + conflicts: `editor/CHANGELOG.md` `[Unreleased]` and nothing else (the record file is new). +- Scratch: `$CLAUDE_JOB_DIR/tmp`, files prefixed `q-` / `r-`. e2e is queued machine-wide. + +## Context + +The operator wants the repo mirrored on the project site (https://archilyzer.pages.dev, the +`homepage/` Next.js 16.2.3 static-export app on Cloudflare Pages) as static, read-only files. The +2026-09-26 pre-publication audit found no secret and no personal identifier in the 1,648-commit +history except the Unix username in `/home/user` paths (a first name) — plus, found during design, +`/run/media/user` in a few `common/` tests and two backticked `user` mentions in `plans/`. Today the +site ships only a hand-run, stale (2026-08-12) `git archive` tarball (`create-archives.sh`, whose +header says "no public git remote, by choice" — the policy this release reverses on the operator's +word). Outcome: `git clone https://archilyzer.pages.dev/source/archilyzer.git` works with nothing but +static files (git's dumb-HTTP protocol), a raw browsable tree sits at `/source/tree/`, the tarball is +regenerated per deploy, and a gate refuses to publish anything that still carries a denied literal. + +## Decisions (made with the operator 2026-09-27; do not re-open) + +- **Mirror = `main` only**, no other refs (no tags exist). **Scrub** `/home/user → /home/user` in + file contents AND commit messages; **keep** `Co-Authored-By` and ` + **`plans/` ships as-is.** The 20 one-off `umtool/song/*.sh` run-log scripts are **deleted**. +- The private repo's history is never rewritten. The public mirror is generated by + `git-filter-repo` (deterministic: two runs gave identical ids) on a fresh bare clone at every + homepage build; its commit ids differ from the private repo's and the page says so. +- The mirror directory is `/source/archilyzer.git/` — NEVER a path segment literally named `.git`: + wrangler's upload `IGNORE_LIST` (`**/.git`, `**/node_modules`, …) drops it silently, while + `archilyzer.git` passes (verified in wrangler 4.107 source). Extensionless files (`HEAD`, + `info/refs`) upload fine. +- Operator-private inputs live OUTSIDE the repo in `${ARCHILYZER_CONFIG_DIR ?? ~/.config/archilyzer}/`: + `source-scrub.txt` (filter-repo `lhs==>rhs` lines; the step always prepends `${os.homedir()}==>/home/user`) + and `source-denylist.txt` (one literal per line, `i:` prefix = case-insensitive; every scrub lhs is + implicitly denied). Missing file → refusal naming the path. Nothing with the username is in code. +- The step is one CLI verb, `archilyzer source publish`, and `buildHomepage` runs it between + `composeHomepage` and `next build` (`common/publish/build.ts:973-985`), so `archilyzer build + homepage`, the runbooks' home scripts and the `/sites` homepage jobs all produce it. It + writes only under gitignored `homepage/public/{source,downloads}` with the link-safe writers in + `common/bin/_publicFile.ts` (`ownDir`, `writePublicFile`, `copyPublicFile`). + +## Verified facts the implementer must not re-derive + +- `git clone --no-local --bare --single-branch --branch main` is load-bearing: a same-host + `--mirror` copies ~52 MB of loose garbage (`.git/objects/ac/tmp_obj_*`). After filter-repo the + pack grows 13.1 → 21.5 MB; `git repack -a -d --max-pack-size=20m` splits it (a 21-pack layout at + 2 MB each cloned fine over `python3 -m http.server`). Pages caps: 25 MiB per file, 20,000 files + per deployment (`src/pages/constants.ts`); the step refuses at 24 MiB / 15,000. +- `--replace-text` rewrites blobs only; one commit MESSAGE carries the path → also + `--replace-message` with the same file. filter-repo is not installed; `pipx run + --spec git-filter-repo==2.47.0 git-filter-repo` works without sudo (pipx 1.15.0). `git 2.55.0`. + gitleaks is installed and clean on the primary (1,541 commits, "no leaks found"). +- wrangler's mime map serves `.ts` as `video/mp2t`, `.mjs` as `application/javascript`, `.md` as + `text/markdown` → `_headers` must override to `text/plain` under `/source/tree/*` (a NEW pattern + here; today `homepage/public/_headers` only sets Cache-Control on `/downloads/*`). Only the ROOT + `_headers` is parsed; copies inside the tree are served as text. +- `main` today: 1,556 commits, 2,017 tracked files (25.9 MB), 333 directories, 123 paths with + `[`/`]` (Next dynamic-route dirs + two variable fonts) → hrefs must be URL-encoded; 0 spaces, + 0 non-ASCII, 0 symlinks, 0 tracked `.html`; largest blob 1.57 MB; largest dir 204 entries. + Expected public file count ≈ 2,360. +- Next static export copies `public/` into `out/` verbatim; `next dev` does NOT serve + `public/<dir>/index.html` at `/<dir>/` (Pages does) — e2e requests `index.html` explicitly, and + NO `public/source/index.html` may exist (it would collide with the `/source/` app route). +- **Tailwind v4 scans every file `.gitignore` does not exclude** and pack files are binary (the + umtool `.next-*` incident, `.gitignore:151-157`); `homepage/tsconfig.json` includes `**/*.ts`. + So `/homepage/public/source` goes into `.gitignore` and `"public"` into the tsconfig `exclude` + in R's FIRST commit, before any build with a mirror under `public/`. +- `umtool/lib/paths.mjs` and `umtool/lib/projects/report.mjs` are imported by the umtool Next app + (11 API routes), where Turbopack rewrites `import.meta.url` into `.next/server/chunks` + (`umtool/lib/paths.ts:36-43`, the `SONG_CODE` rule): use a cwd walk to `pnpm-workspace.yaml`, not + the `cues.mjs:55-64` `import.meta` pattern (CLI-only). +- `umtool/song/um-manifest.json`'s 1,896 `vid` values are used only IN MEMORY by + `umtool/song/build-um.mjs:375,624,643` for an untracked review HTML; nothing reads them back from + the JSON (`umtool/lib/clips.ts:16` has no `vid`). `thumb-manifest.json`/`thumb-accepted.json` `out` + IS read (`umtool/lib/thumbs.ts:165-203` via `resolveInRoots`, `umtool/lib/paths.mjs:120-128`, which + binds a relative path to the FIRST root, `SONG_REPORTS`); `bg` is write-only and lives under + `SONG_DATA`. The live umtool on :3050 is started with only `UMTOOL_PORT` + (`~/reports/release-10/scripts/r10-umtool.sh:45`) and its data is at + `/home/user/.claude/jobs/efbe67a7/tmp/song` — a changed `SONG_DATA` default needs an operator + symlink before the next umtool restart. +- Worktrees do NOT symlink `homepage/public` entries today (only `_headers` is tracked there); + `ownDir` covers both cases anyway. filter-repo leaves `<bare>/filter-repo/{commit-map,ref-map}` + with the PRIVATE shas and `config` holds `remote.origin.url` = the local path → publish from an + ALLOWLIST of files, never the bare dir whole. + +## Slice Q — fix-forward the hardcoded paths (branch `r12/paths-fix`, lands first) + +Owns `umtool/**`, `WORKTREES.md`, `common/controller/storageLocations{,.test}.ts`, +`common/lib/storageVolumes{,.test}.ts`, `common/views/storage.test.ts`, `common/lib/envVars.ts` +(only the defaults of the umtool variables it changes) + the regenerated `ENVIRONMENT.md`; shared: +`editor/CHANGELOG.md`, `plans/release-12.md` (created by Q with the release-11 shape), +`plans/source-mirror.md` (this plan, Q's first commit). + +1. `umtool/lib/paths.mjs:86`: add `export const REPO_ROOT = findRepoRoot(process.cwd())` (walk up + until `pnpm-workspace.yaml` exists; fall back to `path.resolve(cwd, "..")`; comment why cwd, not + `import.meta.url`), and `CHANNELS_DIR = path.resolve(process.env.CHANNELS_DIR ?? + (process.env.TRANSCRIPTS_DIR ? join(TRANSCRIPTS_DIR, "channels") : join(REPO_ROOT, "transcripts", + "channels")))`. `umtool/lib/projects/report.mjs:39-41`: `GLOBAL_CHANNELS_DIR = () => CHANNELS_DIR` + imported from `../paths.mjs` (one definition). Leave `cues.mjs` alone. +2. `umtool/song/spec.mjs:31`, `umtool/song/video-dir.mjs:53`: `process.env.VIDEO_ROOT ?? + path.join(os.homedir(), "reports", "quartering-uh-song", "videos")` (add `import os`). +3. `umtool/song/paths.mjs:19`: `SONG_DATA` = `fs.realpathSync` of `process.env.SONG_DIR ?? + path.join(os.homedir(), ".local", "share", "archilyzer", "song")` (realpath so `SONG_SCRATCH = + dirname(SONG_DATA)` keeps pointing at the real job dir through a symlink); update the header + comment (the symlink is the supported way to keep the data where it is). +4. `git rm` the 20 run-log scripts (`backfill-build, mk-fatal-finish{,2,3}, mk-rebuild, ms2-{bg3, + drums,full,jerbg,play-rebuild,triangle}, pk2, pk3, pk-v12, pkmn-{rebuild,video}, rpg-remake-v5, + rpg-short, vshort, yoshi-rebuild}.sh`). Reword `umtool/lib/jobs.ts:8-17,71-72` and the static prose + in `umtool/components/BuildChain.tsx:144-150` to past tense ("were one-off shell run logs that + hardcoded their paths; not in the tree, not runnable from here"). Historical mentions in + `debox-bg.mjs:24`, `hush-head.mjs:47`, `pick-take.mjs:26`, `video-dir.mjs:5,137` stay (record as left). +5. `um-manifest.json`: one-off strip of `vid` (record the command; indent 1 as the writer uses; + `git diff --stat` shows only removed `"vid"` lines); `build-um.mjs:250` writes + `items.map(({ vid: _v, ...rest }) => rest)` with a comment (the page needs the `file://` URL, the + tracked manifest must not carry a machine path); `:224` unchanged. +6. `thumb-manifest.json` + `thumb-accepted.json`: one-off rewrite `out` relative to + `~/reports/quartering-uh-song` and `bg` relative to the song data dir; writer + `umtool/song/make-thumb.mjs:258` → `out: relTo(SONG_REPORTS, OUT), bg: relTo(SONG_DATA, BG)` + with a comment naming the two roots. Verify and record: from `umtool/`, `resolveInRoots + ("thumbs/<x>.jpg")` prints the same absolute file as before, and `thumbs.ts:165,199` yield the + same labels. +7. Text: `WORKTREES.md:119` → `cwd /path/to/checkout/editor`; `umtool/song/fit-hooks.mjs:93` → + `<user>`; the `/run/media/user/` literals in the five `common/` files → `/run/media/<user>/` in + comments and `/run/media/operator/` in test fixtures (change both sides of each assertion); the + one `editor/CHANGELOG.md` line → `/run/media/<user>/` (a released entry is normally left as + written; this is a path example, not a name a reader searches for — say so in the record). +8. If `common/lib/envVars.ts` states a default for `SONG_DIR`, `VIDEO_ROOT` or `CHANNELS_DIR`, + update it to the new default and regenerate `ENVIRONMENT.md` (`pnpm archilyzer docs env`). +9. Gate: `git grep -c -i 'user' HEAD -- . ':!plans/'` → empty; tsc; `node --check` on every + changed `.mjs`; common 2,112 (edited, not added); `test:scripts` 185 + 1; editor unit 85; mcp 269; + `pnpm archilyzer docs env --check` clean; `pnpm --filter umtool exec next build`; `cd umtool && + node -e 'import("./lib/paths.mjs").then(p=>console.log(p.CHANNELS_DIR,p.SONG_DATA))'` prints the + repo's `transcripts/channels` and the XDG path; umtool e2e `faces.spec.ts deck.spec.ts + clip-bench.spec.ts` via `node scripts/worktree.mjs run -- pnpm --filter umtool run e2e …` (the + fixture sets `CHANNELS_DIR`/`SONG_DIR`, proving env still wins). One `[Unreleased]` bullet in + `editor/CHANGELOG.md`. + +## Slice R — `archilyzer source publish` + `/source` (branch `r12/source-mirror`, after Q) + +Owns `common/publish/{source,sourceAudit,sourceTree}.ts` + tests, `common/lib/sourceManifest.ts`, +`common/publish/build.ts` (`buildHomepage` only), `common/bin/archilyzer.ts` + `_cli.test.ts`, +`common/bin/doctor.ts` + test, `common/lib/paths.ts` + `common/lib/envVars.ts` (the new variables) ++ the regenerated `ENVIRONMENT.md`, `homepage/**`, `.gitignore`, `create-archives.sh` (deleted), +`README.md`, `SETUP.md`, `PUBLISH.md`, `homepage/content/docs/install.md`; shared: both changelogs, +`plans/release-12.md`. + +**New variables, declared in `common/lib/envVars.ts` and read through `getPaths()`**: +`ARCHILYZER_CONFIG_DIR` (default `~/.config/archilyzer`), `SOURCE_SCRUB_FILE`, +`SOURCE_DENYLIST_FILE` (defaults inside the config dir), `ARCHILYZER_SOURCE_SCRATCH` (default +`os.tmpdir()`). None is test-only, so none carries `E2E_`. + +**`archilyzer doctor`** gains a "source publish" block: which filter-repo command resolves (or the +install line), whether gitleaks is on PATH, whether the two operator files exist, and the last +published `mirrorHead` if there is a manifest. Read-only, like the rest of doctor. + +**The `/sites` homepage jobs run the step too** (they call `buildHomepage` in-process). A refusal +fails the job with the redacted audit report in its log. `--no-source` stays CLI-only. + +### R1. `common/publish/source.ts` — `publishSource(opts): Promise<number>` (0 ok, 1 refused) + +`SourcePublishOpts = PublishOpts & { force?, check?, keepScratch?, branch = "main", sourceRepo? +(default: git -C monorepoRoot rev-parse --path-format=absolute --git-common-dir — a worktree build +mirrors the PRIMARY's main), publicDir? (default homepage/public, HOMEPAGE_PUBLIC_DIR honoured), +scrubFile?, denylistFile?, filterRepo?: string[]|null, gitleaks?: string|null (null = skip, tests), +scratchRoot? (ARCHILYZER_SOURCE_SCRATCH ?? os.tmpdir()), now? }`. `class SourceRefusal extends Error`. +Every child through the existing `runChildIntoLog` with `AbortSignal.any([signal, +AbortSignal.timeout(ms)])`; scratch = `mkdtemp(<scratchRoot>/archilyzer-source-)`, removed in +`finally` unless `keepScratch`. Order: + +1. `sourceCommit = git rev-parse refs/heads/main` in the common dir. +2. `loadScrubRules` / `loadDenylist` (R1 files); `rulesHash = sha256(rules + denylist)`. +3. **Skip** unless `force`/`check`: existing `public/source/manifest.json` with equal `sourceCommit` + + `rulesHash` AND `public/source/archilyzer.git/info/refs` AND the tarball present → log + `[source] up to date at <sha>; skipping (--force to rebuild)`, return 0. +4. `resolveFilterRepo()`: `git filter-repo --version` → `["git","filter-repo"]`; else pipx → + `["pipx","run","--spec","git-filter-repo==2.47.0","git-filter-repo"]`; else `SourceRefusal` with + the install line (`pipx install git-filter-repo`). Log the choice and version. +5. `git clone --no-local --bare --single-branch --branch main --quiet <common-dir> <scratch>/bare` + (120 s); `git -C bare remote remove origin`. +6. Write `replace.txt`; run `<filterRepo> --force --replace-text replace.txt --replace-message + replace.txt` in `bare` (900 s; `--force` because origin was removed); `rm -rf bare/filter-repo`. +7. `git repack -a -d -q --max-pack-size=20m` (300 s), `git prune-packed`, `git pack-refs --all`, + `git update-server-info`; refuse if `git count-objects -v` `count` ≠ 0 or `objects/info/packs` + is empty. +8. `mirrorHead = git rev-parse refs/heads/main`, `subject = git log -1 --format=%s` (in bare). +9. **Gate**: `auditBare` (R2). Any hit → print the redacted report, return 1, write nothing. +10. Tree: `git archive --format=tar -o scratch/tree.tar refs/heads/main` → `tar -xf … -C + scratch/stage/source/tree`; `writeTreeIndexes` (R3). +11. Tarball: `git archive --format=tar.gz -9 --prefix=archilyzer/ -o + scratch/stage/downloads/archilyzer-source.tar.gz refs/heads/main`; sha256 by stream; + `snapshot.json` in today's shape `{generatedAt, commit: mirrorHead, subject, bytes, sha256}` + (the `Snapshot` type in `homepage/app/lib/snapshot.ts` stays). +12. Stage the mirror from an ALLOWLIST only: `HEAD`, `packed-refs`, `info/refs`, + `objects/info/packs`, `objects/pack/*.pack`, `objects/pack/*.idx` → `stage/source/archilyzer.git/` + (no `config`, `hooks/`, `description`, `logs/`, `*.rev`, `*.bitmap`). +13. `auditFiles(stage)` — a byte sweep of every staged file except `*.pack`/`*.idx` (the object + walk covered those). Hit → return 1. +14. Limits: total files > 15,000 or any file > 24 MiB → refusal. +15. `--check` stops here (prints the report + counts, returns 0, writes nothing). +16. Install, link-safe: `ownDir(public/source)`; `rm public/source/manifest.json` FIRST (a crash + mid-copy leaves the page in its empty state, never a stale manifest over a half tree); + `rm -rf public/source/{archilyzer.git,tree}`; `cp -r stage/source/*`; `ownDir(public/downloads)`; + `copyPublicFile(tarball)`; `writePublicFile(snapshot.json)`; LAST `writePublicFile(manifest.json)`. +17. One summary line: `[source] published main <src12> as <mirror12>: N files, M MB (mirror P + packs, tree D dirs), tarball K MB sha256 <sha12>`. + +`common/lib/sourceManifest.ts` (leaf): `SOURCE_MANIFEST_VERSION = 1`, `MIRROR_DIR = "archilyzer.git"`, +`CLONE_URL = ${PROJECT_URL}/source/archilyzer.git`, `TREE_HREF = "/source/tree/"`, and +`SourceManifest = { version: 1; generatedAt; branch: "main"; sourceCommit; mirrorHead; subject; +files; bytes; mirror: {files, bytes, packs}; tree: {files, dirs, bytes}; tarball: {href, bytes, +sha256}; cloneUrl; treeHref; audit: {literals, objects, commits, gitleaks: "clean"|"skipped"}; +rulesHash; tools: {git, filterRepo} }`. + +`buildHomepage` becomes compose → `if (!opts.skipSource) { const c = await (opts.publishSource ?? +publishSource)({paths,onLog,signal}); if (c !== 0) return c; }` → `next build`. `r10-home.sh` is +unchanged and fails closed (a refusal = `HOME_BUILD_FAILED`, no deploy). CLI table rows: `build +homepage [--no-source]`; `source publish [--force] [--check] [--keep-scratch]`; `source audit +[<git dir>]` (default: the published `homepage/public/source/archilyzer.git`; the rollout runs it on +the LIVE clone). `SOURCE_DENYLIST_FILE` / `SOURCE_SCRUB_FILE` env override the file paths (the gate +uses it to plant a literal). + +### R2. `common/publish/sourceAudit.ts` + +`parseDenylist(text)` → `{bytes, ci}[]`; `scanBuffer(buf, literals)`; `redactHit` (±24 bytes, the +literal → `[REDACTED]`, non-printables `.`); `auditObjects(bare, literals)` = ONE stream `git -C bare +cat-file --batch-all-objects --unordered --batch` with a framing parser scanning EVERY object — +blobs, commits (message + identity lines), trees (entry NAMES), tags; `auditFiles(dir, literals, +skip=/\.(pack|idx)$/)`; `runGitleaks(bare)` = `gitleaks git --no-banner --redact --exit-code 3 +--report-format json --report-path <scratch>/gitleaks.json <bare>` (300 s; 0 → "clean", 3 → parse +`RuleID`/`File`/`Commit` into hits, other → `SourceRefusal`; ENOENT → "skipped" + a WARNING line); +`auditBare` combines; `formatAuditReport` never prints a literal (shows `#n (x…, len L)`). Refusal +output ends with `[source] add a rule to ~/.config/archilyzer/source-scrub.txt or drop the file from +history, then re-run.` + +### R3. `common/publish/sourceTree.ts` and `_headers` + +`hrefFor(entry)` = `encodeURIComponent(name)` + `/` for dirs (`[slug]` → `%5Bslug%5D/`); +`escapeHtml`; `renderTreeIndex({relDir, entries, mirrorHead, generatedAt})` — one dependency-free +template (~40 lines inline CSS honouring `prefers-color-scheme`, `<meta name=robots content=noindex>`, +breadcrumb with relative `../` hops, `..` row except at root, dirs before files sorted by name, +sizes via `formatBytes` with raw bytes in `title`, footer `main @ <mirrorHead12> · generated <at> · +clone`); `writeTreeIndexes(treeDir, meta)` walks with `readdir({withFileTypes:true})`, REFUSES if a +directory already has an `index.html`, returns `{files, dirs, bytes}`. + +Append to the tracked `homepage/public/_headers` (exact text; later rules win for the same header): + +``` +# The published source (`archilyzer source publish`, common/publish/source.ts). +/source/manifest.json + Cache-Control: public, max-age=300, must-revalidate +/source/archilyzer.git/* + Cache-Control: public, max-age=300, must-revalidate +# The raw tree is PLAIN TEXT whatever the extension: wrangler's mime map would +# send .ts as video/mp2t, .mjs as application/javascript, .md as text/markdown +# and .html as a page on this origin. +/source/tree/* + Content-Type: text/plain; charset=utf-8 + X-Content-Type-Options: nosniff + X-Robots-Tag: noindex + Cache-Control: public, max-age=300, must-revalidate +# ...except the generated directory indexes and the binary files. +/source/tree/ + Content-Type: text/html; charset=utf-8 +/source/tree/*/ + Content-Type: text/html; charset=utf-8 +/source/tree/*.ttf + Content-Type: font/ttf +/source/tree/*.m4a + Content-Type: audio/mp4 +/source/tree/*.zip + Content-Type: application/zip +/source/tree/*.onnx + Content-Type: application/octet-stream +/source/tree/*.svg + Content-Type: image/svg+xml + Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline' +``` + +(`.md` deliberately text/plain so it renders raw. The binary list is what the tree holds today: +3 `.ttf`, 3 `.m4a`, 1 `.zip`, 1 `.onnx`, 6 `.svg`.) + +### R4. Homepage + +- `homepage/app/lib/source.ts`: `loadSourceManifest()` — `public/source/manifest.json` with + `version === 1`, 40-hex `mirrorHead`, 64-hex sha, AND `public/source/archilyzer.git/info/refs` and + the tarball present (the `snapshot.ts:25-46` rule), else null. +- `homepage/app/lib/nav.ts`: `{ href: "/source/", label: "Source" }` after Docs. +- `homepage/app/source/page.tsx` (`PageShell`/`PageHeading`/`Fact` rows as `downloads/page.tsx`): + title "Source"; standfirst "The canonical public copy: a read-only git mirror, its raw tree, and a + tarball."; one paragraph in the operator's voice: "There is no GitHub, by choice. This site is where + the code lives in public: a read-only mirror of the private main branch, regenerated with every + deploy of this site. Commit ids differ from the private repository because paths were scrubbed on + the way out — same history, different hashes. Nothing here takes a push or a pull request."; the + clone command in `<pre data-testid="source-clone">`; Facts (`source-mirror-head`, reflects private + main, subject, generated, files/size); links "Browse the tree" (`source-tree-link`) and the tarball + with bytes + sha256 (`source-tarball-link`); sections "What is mirrored" and "License" (MIT). Empty + state `data-testid="source-empty"`: "No source published in this build." + how it is produced. +- `homepage/app/downloads/page.tsx`: keep every phrase `downloads.spec.ts` asserts; add one sentence + pointing at `/source/` for history; the empty-state sentence names `archilyzer source publish` + instead of `./create-archives.sh`. +- `homepage/tsconfig.json` `exclude: ["node_modules", "public"]`; `.gitignore` adds + `/homepage/public/source` with the Tailwind note — BOTH in R's first commit. +- `homepage/e2e/marketing.spec.ts:42-47` nav list gains `["Source", "/source/"]`. + +### R5. Docs + +Delete `create-archives.sh` and every reference: `README.md:485`, `SETUP.md:162,182`, +`homepage/app/downloads/page.tsx:102`, `homepage/app/lib/snapshot.ts:4,23`, `.gitignore:49,93` +(grep again; lines move). `README.md`: the clone command as the canonical public copy (`main` only; +ids differ because paths are scrubbed), the tree at `/source/tree/`, the tarball at `/downloads/`, +regenerated by `archilyzer source publish` inside `archilyzer build homepage`. `SETUP.md` and +`homepage/content/docs/install.md` "Get the code": clone first, tarball as the no-git alternative. +**`PUBLISH.md`**: a new section `## The source mirror (homepage)` after `## Cloudflare Pages`, with +the 20,000-file / 25 MiB caps (the step refuses at 15,000 / 24 MiB), the `.git` segment trap, the +dumb-HTTP file set, the `_headers` block, the two operator files, and that only `wrangler pages dev` +honours `_headers` locally. `ENVIRONMENT.md` regenerated. `[Unreleased]` bullets in +`editor/CHANGELOG.md` and `homepage/CHANGELOG.md` (the homepage's keeps its own convention). + +### R6. Tests + +- `common/publish/source.test.ts` (~9): scrub-rule loading (built-in homedir rule first, comments, + missing file → refusal naming the path); denylist (`i:`, implied lhs); skip logic; limits; the + allowlist (a planted `config`/`hooks/`/`filter-repo/` are never copied); manifest shape; a **round + trip** in a temp repo (`git init -b main`, identity set, the `GIT_*` env vars deleted as + `common/controller/cutRelease.test.ts` does): three commits with a planted literal in one blob and + one message, rule `/srv/plantedhome==>/home/user`, `gitleaks: null`, `publicDir` temp → `git clone + file://<public>/source/archilyzer.git` succeeds, HEAD = `manifest.mirrorHead` ≠ `sourceCommit`, + the message reads `/home/user`, `git grep -F plantedhome $(git rev-list --all)` empty, no `config` + in the published dir, `info/refs` names `refs/heads/main`, `objects/info/packs` ≥ 1, snapshot sha + = fresh sha256 of the tarball, `tree/index.html` and one subdir index exist, no + `source/index.html`; a **planted-literal refusal** (denylist without a rule → 1, no manifest, + downloads untouched, report free of the literal); `--check` writes nothing. The two filter-repo + tests `t.skip` when `resolveFilterRepo()` throws — the gate requires they RAN. +- `common/publish/sourceAudit.test.ts` (~6): blob / commit / tree-name hits, clean repo, redaction, + gitleaks skipped. `common/publish/sourceTree.test.ts` (~5): encoding, escaping, ordering, + breadcrumbs, the existing-index refusal. `_cli.test.ts` (+3 usage rows). `build.test.ts` (+1: + `skipSource` never calls the injected fake; a fake returning 1 fails the build). +- `homepage/e2e/source.spec.ts` (5; the suite runs against `next dev` + `homepage/public` on disk, + two-state like `downloads.spec.ts`; the R gate runs `source publish` in the worktree BEFORE the + suite so the manifest branch is exercised — record which): the page's fixed copy; with a manifest + the clone command, tarball href (200 via `page.request`), and the sha on the page = `snapshot.json` + = `manifest.json`, else the empty state; the mirror files (`/source/archilyzer.git/HEAD` = `ref: + refs/heads/main`, `info/refs` contains `<mirrorHead>\trefs/heads/main`, `objects/info/packs` has a + `P pack-` line); tree indexes (`/source/tree/index.html` 200, one dir href + `index.html` 200, one + file href 200, the fonts dir index contains `%5B` and that href resolves). Content-Type cannot be + asserted under `next dev` (`_headers` is Pages-only) — the preview deploy checks it. + +### R7. Gates (worktree root, after merging `main` with Q) + +tsc (must stay fast — proves the tsconfig exclude); common 2,112 + ~26 (doctor included); +`test:scripts` 185 + 1; editor unit 85; mcp 269; homepage unit 2; `pnpm archilyzer docs env --check` +clean; `pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts build +homepage` in the worktree (the parent creates `~/.config/archilyzer/{source-scrub,source-denylist}.txt` +BEFORE the slice starts; the implementer never reads them aloud and never commits them) → `homepage/out/source/archilyzer.git/ +info/refs`, `manifest.json`, the tarball; record `find homepage/out -type f | wc -l` and the build's +extra wall time; `git clone file://$WT/homepage/out/source/archilyzer.git` AND, with `python3 -m +http.server 8765` in `homepage/out`, `git clone http://127.0.0.1:8765/source/archilyzer.git` — both +HEADs = `manifest.mirrorHead`; `git -C <clone> grep -c -i user $(git rev-list --all)` empty; `source +audit <clone>/.git` clean; the gate EXERCISED: `SOURCE_DENYLIST_FILE` pointing at a copy with a literal +that IS in history (e.g. `Co-Authored-By`) → `source publish --check` exits 1 with redacted hits and +`public/source/manifest.json`'s mtime unchanged; `build homepage --no-source` → the `/source/` page's +empty state; `PATH` without pipx → the install-line refusal; `archilyzer doctor` prints the new +block; homepage e2e 31 → 36 with the manifest present, `downloads.spec.ts` unchanged. Record the +resolved filter-repo command and version. + +## Rollout (parent; nothing here edits the primary's tracked files) + +Release 11 is merged and NOT rolled out. A production homepage deploy of release 12 also ships +release 11's homepage changes, so production waits for the operator's release 11 rollout (or is +done as part of it, on the operator's word). The PREVIEW deploy below is safe at any time. + +0. Before slice R's gate, the parent creates `~/.config/archilyzer/` (mode 700) with + `source-scrub.txt` (`/run/media/user==>/run/media/user`, `` `user`==>`user` ``) and + `source-denylist.txt` (mode 600: `user`, the hostname from `hostname`, the Gmail address the + session already knows). **The operator adds what only they know** — real name, other handles, + anything else that must never appear — before the first production publish. `pipx install + git-filter-repo` (so a build needs no network and the editor's jobs find it on PATH). Q's + operator step, BEFORE any umtool restart: `mkdir -p ~/.local/share/archilyzer && ln -s + /home/user/.claude/jobs/efbe67a7/tmp/song ~/.local/share/archilyzer/song`. +1. `archilyzer source publish --check`, then `archilyzer build homepage` (r10-home.sh's first line); + check `homepage/out/source/archilyzer.git/info/refs` and the file count. +2. **Preview first**: `archilyzer deploy homepage --preview source` → `https://source.archilyzer.pages.dev`. + Live: `git clone https://source.archilyzer.pages.dev/source/archilyzer.git` (the edge-side unknown: + extensionless `HEAD`/`info/refs`, `.git` inside a segment past Cloudflare's managed rules, the dumb + fallback after the `?service=` probe); clone HEAD = `manifest.mirrorHead`; `curl -sI + …/source/tree/common/lib/paths.ts` → `text/plain; charset=utf-8` + `nosniff`; `…/source/tree/common/` + → text/html; `…/source/tree/umtool/report-to-video/fonts/Archivo%5Bwdth,wght%5D.ttf` → 200 font/ttf; + `…/source/tree/homepage/app/docs/%5Bslug%5D/page.tsx` → 200; the tarball's sha256 = `snapshot.json` + = the page; `archilyzer source audit <clone>/.git` clean. +3. Production, after release 11's rollout: `archilyzer deploy homepage` (or `pnpm ops + build-homepage --json '{"deploy":true}'`, which also proves the step inside the editor's job), + the same checks on `archilyzer.pages.dev`; the record's `## Rollout`, STATE ("Now"), FACTS (the + `.git` segment trap, Tailwind/tsconfig scanning of `public/`, dumb HTTP on Pages), memory, and an + HTML runbook at `~/reports/release-12/RUNBOOK.html` (handoffs are HTML). +4. If the edge refuses the dumb clone: the tree and tarball still stand; the mirror needs another host + (R2 behind a custom domain, out of scope) — record, do not improvise. + +## Verification summary + +Unit: the round trip (clone → scrub → repack → `update-server-info` → clone back), the planted-literal +refusal, the audit's three hit kinds, the index encoding. Build: `archilyzer build homepage` in the +worktree, then a dumb-HTTP clone from `homepage/out` over `python3 -m http.server`. Live: the preview +deploy's clone, content types, bracket paths, tarball sha, and `source audit` on the live clone. Docs: +`grep -rn 'create-archives' README.md SETUP.md homepage/` shows nothing. + +## Risks (flagged, not solved here) + +- Edge-side serving of `HEAD`/`info/refs`, the `.git`-inside-a-segment path and bracket paths are + proved upload-side only; the preview deploy is the test. Cloudflare's managed "git exposure" rules + match `/.git/`, ours is `/archilyzer.git/`. +- `pipx run` needs network on first use and every 14 days; the editor's `/sites` homepage jobs run in + the editor's environment where `pipx`/`gitleaks` may be off PATH → the build refuses (fail closed) + or skips gitleaks with a warning. `pipx install git-filter-repo` once removes both. +- filter-repo determinism holds for a fixed version; an upgrade that changes rewriting defaults + changes `mirrorHead` (readers see a non-fast-forward; the page says ids may change); the manifest + records the version. Object ids are stable across git versions; pack bytes are not (irrelevant). +- The denylist `user` is exact-case; a future transcript quote "user that" trips the gate — + intended (add a rule or reword). +- About a minute more per homepage build (filter-repo ~20-60 s); an unchanged `main` skips it. +- The first publish replaces the 2026-08-12 tarball and `Snapshot.commit` becomes a mirror sha; only + the `/source` page's "reflects private main" row links the two — deliberate.