commit 06d636951d42f7961ee90a17f5c12c9483182707
parent 01c1b1eec4c24088dd0ddab556e2bcd208c5ce79
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 12:16:49 -0400
plans: slice Q recorded — the hardcoded paths fixed forward
release-12.md gains "Slice Q, as shipped": what shipped per step, what
went beyond the plan (SONG_REPORTS beside relTo, accept-thumb records
relative, the fixture and deck.spec pin), the corrections (common 2,114;
step 8 a no-op), the two one-off commands, the verification that every
thumb resolves to the same file with the same labels, the commit table,
the gates, the bite, what was left, the changelog note, and the
operator's symlink step. source-mirror.md gets an "As shipped" note
under Slice Q.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 219 insertions(+), 0 deletions(-)
diff --git a/plans/release-12.md b/plans/release-12.md
@@ -33,4 +33,210 @@ prompts give.
## Record
+### Slice Q, as shipped — fix-forward the hardcoded paths (2026-09-28)
+
+Branch `r12/paths-fix` off `main` `90c7f776`, worktree `/home/user/Projects/r12-paths-fix` (ports
+3901/3911, umtool e2e 3951/3952), one Opus implementer. Plan: [`source-mirror.md`](source-mirror.md),
+"Slice Q", steps 1–9. Why: slice R publishes the repo, and its gate refuses any denied literal. The
+Unix username was in code as machine paths (umtool's defaults, 20 run-log scripts, three tracked
+manifests) and in path examples (`/run/media/user`). Scrubbing the published copy is R's job. Q
+fixes the source, so the code no longer depends on one machine's home directory. Scratch files
+`q-*` in the job's `tmp`.
+
+**What shipped.**
+- **Step 1: `CHANNELS_DIR` comes from the checkout.** `umtool/lib/paths.mjs` gains
+ `findRepoRoot(start)` and `REPO_ROOT = findRepoRoot(process.cwd())`. The walk goes up to
+ `pnpm-workspace.yaml` and falls back to the cwd's parent. It starts from the cwd, not
+ `import.meta.url`, which is the `SONG_CODE` rule; the comment says why. `CHANNELS_DIR` is
+ `CHANNELS_DIR ?? $TRANSCRIPTS_DIR/channels ?? <REPO_ROOT>/transcripts/channels`, resolved.
+ `lib/projects/report.mjs`' `GLOBAL_CHANNELS_DIR` is `() => CHANNELS_DIR`, imported from
+ `../paths.mjs`, so there is one definition. `cues.mjs` is untouched.
+- **Step 2: `VIDEO_ROOT`.** `song/spec.mjs` and `song/video-dir.mjs` use `process.env.VIDEO_ROOT ??
+ path.join(os.homedir(), "reports", "quartering-uh-song", "videos")`, with `import os` added.
+- **Step 3: `SONG_DATA`.** `song/paths.mjs` resolves `SONG_DIR ?? ~/.local/share/archilyzer/song`
+ through `realpathSync`. A path that does not exist has no realpath, so it is **used as given**:
+ a missing default still gives a `SONG_DATA`, which every reader finds empty. The header comment
+ says the symlink is the supported way to keep the data where it is. It also says why the
+ realpath: `SONG_SCRATCH = dirname(SONG_DATA)` keeps pointing at the real job dir.
+- **Step 4: the 20 run logs are deleted** with `git rm`:
+ - `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`.
+
+ That was every `.sh` in `umtool/song/`. Nothing enumerated or ran them. `lib/jobs.ts`' header
+ and timeout doc, and `BuildChain.tsx`'s comment and on-screen note, now say it in the past tense:
+ "one-off shell run logs that hardcoded their paths … not in the tree, and not runnable from
+ here". No spec asserts that note's text.
+- **Step 5: `um-manifest.json` loses `vid`.** The one-off below stripped all 1,896 `vid` values
+ from 3,609 items. `build-um.mjs` now writes `[...merged.values()].map(({ vid: _v, ...rest }) =>
+ rest)`, with a comment. The page keeps `vid` in memory: `META` is `JSON.stringify(items)`. The
+ item push at `:224` is unchanged.
+- **Step 6: relative paths in the thumb manifests.** The one-off below made `out` relative to
+ `~/reports/quartering-uh-song` and `bg` relative to the song data dir: 18 paths in
+ `thumb-manifest.json` and 8 in `thumb-accepted.json`. `make-thumb.mjs` writes
+ `out: relTo(SONG_REPORTS, path.resolve(OUT))` and `bg: relTo(SONG_DATA, path.resolve(BG))`, with a
+ comment naming the two roots. `path.resolve` is there because both are CLI arguments and could
+ be cwd-relative.
+- **Step 7: path examples name no user.**
+ - `WORKTREES.md:119` → `cwd /path/to/checkout/editor`.
+ - `fit-hooks.mjs:93` → `home/<user>`.
+ - `/run/media/<user>/` in the comments of `storageLocations.ts`, `storageLocations.test.ts` and
+ `storageVolumes.ts`.
+ - `/run/media/operator/` in the fixtures of `storageVolumes.test.ts` and `views/storage.test.ts`,
+ on both sides of every assertion.
+ - The one `editor/CHANGELOG.md` line (see "Changelog").
+- **Step 8 had nothing to do.** `common/lib/envVars.ts` declares none of `SONG_DIR`, `VIDEO_ROOT`
+ or `CHANNELS_DIR`. Its header says "umtool's own knobs are NOT here", and `envVars.test.ts:17`
+ keeps umtool out of the scan. `ENVIRONMENT.md` is unchanged, and `--check` exits 0. The new
+ defaults are documented where umtool's knobs live: a table under "Environment" in
+ `umtool/docs/cli.md`.
+
+**Beyond the plan (all in `umtool/**`):**
+- **`SONG_REPORTS` moved into `song/paths.mjs`, beside `relTo`, and `lib/paths.mjs` re-exports
+ it.** `make-thumb.mjs` needs it, and the song scripts import only siblings. The e2e fixture copies
+ `song/*.mjs` into `.e2e-song/code/` and runs them from there, where `../lib` does not exist. There
+ is still one definition, and the default is unchanged.
+- **`relTo(root, p)`.** An absolute `p` inside `root` becomes relative to it. Anything else comes
+ back unchanged. Both sides are compared as given and through the realpath of their deepest
+ existing part, so a path spelled through the `~/.local/share` symlink counts as inside the
+ realpath'd `SONG_DATA`, even for a file not written yet.
+- **`accept-thumb.mjs` records `out` through `relTo(SONG_REPORTS, …)`.** It is the other writer of
+ `thumb-accepted.json`, and `/api/browse/thumbs` hands it an ABSOLUTE `file`. Without this, the
+ first accept from the page would have written a machine path back into the tracked file. The
+ route's comment is updated to match. A relative CLI argument is recorded as typed, as before.
+- **The e2e fixture and a new assertion.** The fixture's accepted `alpha-c` carries the relative
+ `out`, while its run log keeps the absolute form, so the suite reads both. deck.spec's
+ "serves the ACCEPTED cover" now resolves a relative `out`. The disjoint-accept test asserts the
+ CLI recorded `alpha-b` as `thumbs/alpha-b.jpg` (see "They bite").
+- **Comments that described the old default:**
+ - `song-capabilities.mjs` and `make-fixture.mjs` (twice) said "the job temp dir … which on most
+ machines no longer exists";
+ - `browse.ts` said "`out` is an ABSOLUTE path".
+
+**Corrections to the plan:**
+- **The common baseline is 2,114, not 2,112.** O6c added 2 after the plan's re-check
+ (`release-11.md`, O6c gates).
+- **Step 8 was a no-op**, as above.
+- **Step 4's `video-dir.mjs:137` is now `:139`**, because step 2 added two lines above it.
+
+**The one-off commands** (the scripts are in the job's `tmp`; what each does is stated here so it
+can be redone):
+- **Step 5:** `node $T/q-strip-vid.mjs umtool/song/um-manifest.json`. It runs `doc.items =
+ doc.items.map(({ vid, ...rest }) => rest)` and writes `JSON.stringify(doc, null, 1)` with no
+ trailing newline, which is the writer's format; a parse and re-stringify of the old file was
+ byte-identical. It printed `stripped vid from 1896 of 3609 items`.
+ - `git diff --numstat`: **0 added, 1,896 deleted**.
+ - Every deleted line is `"vid": "file://…"`.
+- **Step 6:** `node $T/q-rel-thumbs.mjs "$HOME/reports/quartering-uh-song"
+ "$HOME/.claude/jobs/efbe67a7/tmp/song" umtool/song/thumb-manifest.json
+ umtool/song/thumb-accepted.json`. For every entry, an absolute `out` becomes relative to the
+ first root and an absolute `bg` relative to the second. A path outside its root is refused, not
+ guessed. The output format is the same as step 5. It printed `18 paths made relative` and
+ `8 paths made relative`.
+ - `git diff --stat`: **26 insertions, 26 deletions**, the `out`/`bg` lines only.
+
+**Verified: the same files and the same labels.** Run from `umtool/` with
+`SONG_DIR=/home/user/.claude/jobs/efbe67a7/tmp/song`, before and after the rewrite:
+- **`q-thumbs-check.mjs`** prints `resolveInRoots(out)`, `labelFor` and `browse.thumbFor`'s
+ `SONG_REPORTS`-relative path for all 13 entries, plus the absolute `bg`. The two outputs are
+ identical except the `CHANNELS_DIR` line, which is step 1: the worktree's own
+ `transcripts/channels` instead of the primary's. Every `out` still resolves to
+ `/home/user/reports/quartering-uh-song/thumbs/<x>.jpg`, and all 13 exist.
+- **`q-thumbview.mts` runs the app's own readers** (`lib/thumbs.ts` `thumbView` and `lib/browse.ts`
+ `thumbFor`) for yoshi, mario-rpg, metal-slug, mortal-kombat and pokemon. For "before", it pointed
+ `SONG_CODE_DIR` at `90c7f776`'s two manifests. The output is **identical**: accepted names and
+ labels, every candidate's label, the clash strings, `check` and the poster path.
+- **The one visible difference:** a candidate's `file` is now `thumbs/<x>.jpg` instead of the
+ absolute path, which changes the bench's grey text. The accept route resolves it to the same
+ absolute file.
+- **The `SONG_DATA` default:** no env gives `/home/user/.local/share/archilyzer/song`, used as
+ given because the path is absent. `SONG_DIR=<a symlink in scratch to the job dir>` gives the job
+ dir, and `SONG_SCRATCH` = `…/efbe67a7/tmp`.
+- **Nothing was created** under `~/.local/share` or `~/.config`.
+
+| sha | what |
+|---|---|
+| `c3177eeb` | `plans:` the source mirror plan (verbatim) and this record file |
+| `de1ead5d` | `umtool:` `REPO_ROOT` + `CHANNELS_DIR` from the checkout; `GLOBAL_CHANNELS_DIR` returns it |
+| `d4d549c7` | `umtool:` `SONG_DATA` (XDG default, realpath, missing → as given) and `VIDEO_ROOT` defaults; fixture comments; `docs/cli.md` defaults table |
+| `292f7a96` | `umtool:` the 20 run-log scripts deleted; `jobs.ts` / `BuildChain.tsx` in the past tense |
+| `4d12652e` | `umtool:` `um-manifest.json` without `vid` (one-off) and `build-um.mjs` strips it on write |
+| `73c39376` | `umtool:` thumb manifests relative (one-off); `relTo`; `make-thumb` / `accept-thumb` write relative; `SONG_REPORTS` into `song/paths.mjs`; the fixture's relative accepted cover |
+| `0b28a7a4` | `common, docs:` `/run/media/<user>` / `operator`, `WORKTREES.md`, `fit-hooks.mjs` |
+| `29dc507c` | `changelog:` one `[Unreleased]` bullet; the released line's path example |
+| `1059befb` | `e2e:` deck.spec pins the relative `out` an accept records |
+| _this_ | `plans:` this record; `source-mirror.md`'s "As shipped" note under Slice Q |
+
+**Gates** (from the worktree root; logs `$T/q-*.log`):
+- **The grep gate** `git grep -c -i 'user' HEAD -- . ':!plans/'` is **empty** (exit 1) at
+ `29dc507c` and at `1059befb`.
+- **tsc** was clean before every commit (34–48 s; `q-tsc-{0..7}.log`, all seven packages).
+- **`node --check`** passed on all 11 changed `.mjs`.
+- **Unit:**
+ - common **2,114/2,114**, the same count, with tests edited and none added (the three edited
+ files 39/39);
+ - `test:scripts` **185 + 1 skipped**;
+ - editor unit **85/85**;
+ - mcp **269/269**.
+- `pnpm archilyzer docs env --check` exits **0**.
+- `pnpm --filter umtool exec next build` is **ok** (20 s).
+- **The `lib/paths.mjs` print** from `umtool/` gives `/home/user/Projects/r12-paths-fix/transcripts/channels
+ /home/user/.local/share/archilyzer/song`: this checkout's corpus and the XDG path.
+- **umtool e2e** (`faces.spec.ts deck.spec.ts clip-bench.spec.ts`, with
+ `SONG_DIR=~/reports/quartering-uh-song/data`; the queue was free each time):
+ - On `29dc507c`: **67 passed, 8 skipped, 0 failed** (2.0 min).
+ - deck.spec alone with the new assertion: **14 passed, 2 skipped** (20.5 s).
+ - On `1059befb`: **67 passed, 8 skipped, 0 failed** (1.6 min).
+ - The skips are the song-data specs. This machine's `data/` has `cand2/` but no `wav48/`, `asr/`
+ or `media/`, and `make-fixture` says so.
+ - The fixture sets `CHANNELS_DIR` and `SONG_DIR` on both servers, so the green run is also the
+ proof that env still wins.
+- **Numbers tool:** none.
+
+**They bite:**
+- deck.spec's disjoint-accept test fails with `90c7f776`'s `accept-thumb.mjs` swapped in: **1
+ failed**. It expected `thumbs/alpha-b.jpg` and received
+ `…/umtool/.e2e-song/reports/thumbs/alpha-b.jpg`. A trap restored the file, and the tree was clean
+ afterwards (`q-e2e-deck.log`).
+- The default changes have no unit tests; umtool has none for `lib/`. They are verified by the
+ prints above.
+
+**Found and left:**
+- **Historical mentions of the deleted scripts stay, as the plan says:**
+ - `debox-bg.mjs:24` (`pk3.sh`);
+ - `hush-head.mjs:47` and `pick-take.mjs:26` (`mk-fatal-finish3.sh`);
+ - `video-dir.mjs:5,139` (`pkmn-video.sh`).
+- **`cues.mjs`' `DEFAULT_CHANNELS_DIR` is evaluated inside the app bundle too.** `report.mjs`
+ imports `build-video` and `resolve-windows`, which import `cues.mjs`, and there its
+ `import.meta.url` walk points into `.next`. Only their CLI entry points read it, so nothing is
+ wrong today. It is left alone by the plan.
+- **umtool now reads `TRANSCRIPTS_DIR`,** a declared core variable. `envVars.ts`' `readBy` for it
+ does not name umtool, which is out of the registry's scope. I did not change it; it is not this
+ slice's file.
+- **`REPO_ROOT` outside any checkout** falls back to the cwd's parent, as the plan says. Started
+ from `/`, that is `/`.
+- **`bg` is now relative to the song data dir,** and nothing reads it yet.
+
+**Changelog.**
+- **One `[Unreleased]` bullet** in `editor/CHANGELOG.md`: umtool's corpus from `TRANSCRIPTS_DIR` or
+ the checkout, the two new defaults (and the symlink to make before restarting umtool), relative
+ manifests, and the run logs gone.
+- **The released `[0.9.0]` storage-locations entry now says `/run/media/<user>/<uuid>`.** A
+ released entry is normally left as written. This one was changed because it is a path example,
+ not a name a reader would search for.
+
+**For the operator** (the plan's Rollout 0, the slice Q step). Do this **before any umtool
+restart**:
+```
+mkdir -p ~/.local/share/archilyzer && ln -s /home/user/.claude/jobs/efbe67a7/tmp/song ~/.local/share/archilyzer/song
+```
+- **Why before a restart:** a restarted :3050 with no link finds nothing at the new default. It may
+ also create `~/.local/share/archilyzer/song/.cache/umtool` as a real directory, and `ln -s` would
+ then put the link *inside* it (`song/song`). If that happened, remove the directory first.
+- **Best at merge, not only before a restart.** The live :3050 is a `next start` build and is
+ unaffected until then, but it runs song scripts from disk. The song build chain passes `SONG_DIR`
+ explicitly (`lib/trim.ts:226`), and `accept-thumb` needs only `SONG_REPORTS`, whose default is
+ unchanged. A script run by hand with no `SONG_DIR` looks at the new default.
+
## Rollout
diff --git a/plans/source-mirror.md b/plans/source-mirror.md
@@ -178,6 +178,19 @@ Owns `umtool/**`, `WORKTREES.md`, `common/controller/storageLocations{,.test}.ts
fixture sets `CHANNELS_DIR`/`SONG_DIR`, proving env still wins). One `[Unreleased]` bullet in
`editor/CHANGELOG.md`.
+**As shipped (2026-09-28, `r12/paths-fix`; record: `release-12.md`, "Slice Q, as shipped"):**
+- **Steps 1–7 as written, with these additions:**
+ - `SONG_REPORTS` moved into `song/paths.mjs` beside a new `relTo(root, p)`, and `lib/paths.mjs`
+ re-exports it. The song scripts import only siblings, because the e2e fixture runs copies of
+ them.
+ - `accept-thumb.mjs`, the other writer of `thumb-accepted.json`, records `out` relative too.
+ - `make-thumb` passes `path.resolve(OUT)` / `path.resolve(BG)` to `relTo`.
+ - deck.spec pins the relative `out`.
+- **Step 3:** a missing `SONG_DIR` path is used as given, with no realpath.
+- **Step 8 was a no-op:** `envVars.ts` declares no umtool variable. The defaults are in
+ `umtool/docs/cli.md` instead.
+- **Step 9's common baseline is 2,114** (O6c's +2), unchanged by Q.
+
## Slice R — `archilyzer source publish` + `/source` (branch `r12/source-mirror`, after Q)
Owns `common/publish/{source,sourceAudit,sourceTree}.ts` + tests, `common/lib/sourceManifest.ts`,