Archilyzer · Source

archilyzer

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

commit 6767c0ec52989db16516f6972cc21f2afafaf369
parent ab1eb83ed36b007e2fcc8ddc037c9a1c04703177
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 29 Sep 2026 22:33:35 -0400

plans: slice UT as shipped — the bisect to the clip-audio route's join (the ternary in its template), cacheFile, the excludes measured alone, the ignoreIssue kept with its measured reason, the guard's post-build check and relative-import scan set; FACTS corrected (the opt-out is not documented for path or fs calls, an fs call on an opted-out join is not covered, an env or home-directory value is a dynamic part, a pattern walk does not enter symlinks); the editor changelog

Gates: tsc clean; test:scripts 194 + 1 skipped; common 2,220; umtool's capped
build with the corpus and the primary's fixture, 2,167 → 463 on the audio route
(0 under a dot-directory, the corpus or the planted link); the editor's capped
build 97 s / 1,576 MB, no corpus entry; umtool e2e find/triage/faces 6 passed,
29 skipped (no wav48/asr/media here).

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/FACTS.md | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mplans/release-15.md | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 246 insertions(+), 13 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -4,6 +4,7 @@ - **Transcripts that arrived after a video was first seen are counted.** The stats behind the homepage, the hub and every site's charts were cached per video and refreshed only when the video's metadata changed, so a transcript that came later — a Whisper run days after the download, or a video downloaded after the last index build — never reached them, and a video with YouTube captions alone had no transcription date. Counts and charts were low; the homepage could show a site with 0 transcripts, 0 channels and 0 hours while it served its videos. A stat is now also redone whenever the index re-reads the video, every transcript has a date, and a captioned video is dated by when its captions arrived rather than by a later Normalize run, so its place on "Transcribed over time" can move. **After updating, rebuild and restart the editor before anything else:** until then, **Build stats dataset** runs the old code and would undo the new stats, while a site, hub or homepage build already runs the new code — and the first stats build of any kind re-reads every video once (about 10–30 minutes on a large archive; it can be stopped and picks up where it stopped). Then build the index, the stats, the homepage, the hub, and the sites. - **A stats build keeps the stats of a channel whose drive is not mounted, and will not undo a newer version's stats.** A channel whose media is on a drive that is not mounted (or is being moved) is left as it was instead of being read as a channel with no videos; a stats rebuild that has to start over refuses until the drive is back. A stats build refuses to clear stats written by a newer version of the editor; set `ARCHILYZER_STATS_ALLOW_DOWNGRADE=1` to roll back on purpose. Its log also says apart how many videos were downloaded since the last index build (they catch up after the next one) and how many the index skipped (no upload date, or it failed on them). - **An index build keeps a channel whose drive is not mounted, instead of dropping it from the sites.** **Build index**, a site build's data phase and `archilyzer index` read a channel whose media is on a drive that is not mounted (or is being moved, or whose link and config disagree) as a channel with no videos: they removed its videos from the index, and the next site build published the channel as gone. Such a channel is now left as the last build had it — its videos stay in the index, its pages stay as they were, and the sites built next still list it — and the log names it, with its storage location: one line per channel, ` Held: N channel(s), K video(s) kept.` at the end of the `Diff:` line, and the channels again on the last line. A data folder that fails to read is held the same way, and a channel with no data folder at all is said in the log instead of passed over. An index rebuild that has to start over (after an update that changes the index's format, or with no index yet) refuses while any channel is held and says which; mount the drive first, or set `ARCHILYZER_INDEX_ALLOW_HELD=1` to rebuild without that channel until its drive is back and the index is built again — on the command for a command-line build (`ARCHILYZER_INDEX_ALLOW_HELD=1 pnpm archilyzer index`), or in the editor's own environment, with a restart, for **Build index** and the site builds started from the editor. +- **umtool's build no longer lists its e2e test data, the e2e server's build folder or `.env.local` among a route's files.** The clip-audio route named its cache files in a way the bundler read as a pattern reaching into umtool's hidden folders, so its list of files took in the e2e fixture (where the tests link the song data), the e2e dev server's build folder and the env file: 1,704 of its 2,167 entries. It now lists what the other routes list (463). Those folders and env files are also excluded from every route's list, and `pnpm test:scripts` reads the last umtool build's lists back and fails on any such entry, so a checkout whose umtool build predates this fails that test until umtool is rebuilt. Nothing changes when umtool runs. - **Building the homepage now publishes the source: a read-only git mirror, its raw tree and a fresh tarball, behind a gate.** `archilyzer build homepage`, the `/sites` Homepage jobs and `pnpm ops build-homepage` run `archilyzer source publish` between compose and `next build`. It makes a fresh clone of the private `main` (the repository itself is never rewritten), rewrites that copy with git-filter-repo using your scrub rules (file contents and commit messages; your home directory becomes `/home/user` without a rule), and publishes it under `homepage/public` for `git clone https://archilyzer.pages.dev/source/archilyzer.git`, beside `/source/tree/` and the Downloads tarball. Before anything is written, every object of the rewritten history and every file about to be published is searched for every string you have denied; **one hit refuses the build**, and its log names the string only by where you wrote it (`denylist line 3 (len 5)`) and each hit by its object, field and byte offset — never a byte of the object. **A refusal withdraws the source**: the last publish is removed from `homepage/public` and the last build's copy from `homepage/out`, and **Deploy homepage refuses** a build whose source was not audited under today's rules and today's `main` ("run `archilyzer build homepage`, then deploy"). The rules live outside the repo, in `~/.config/archilyzer/source-scrub.txt` and `source-denylist.txt` (`ARCHILYZER_CONFIG_DIR`, `SOURCE_SCRUB_FILE`, `SOURCE_DENYLIST_FILE`); **without them the build refuses**, naming the missing file. **Put everything private in the denylist before any deploy, a preview included**: previews are public, and every deployment stays reachable at its own address until you delete it. Install git-filter-repo once (`pipx install git-filter-repo`; the editor's process needs `~/.local/bin` on its `PATH` to find it) — without it the build fetches it through `pipx run`, which needs the network — and gitleaks if you want its secret scan too. An unchanged `main` with unchanged rules is skipped, so a rebuild costs about 20 seconds only when something moved. A checkout with no git repository (the docker image, a tarball install) builds with the /source page's empty state. `archilyzer source publish --check` audits without writing, `archilyzer source audit <clone>/.git` checks any clone, `archilyzer build homepage --no-source` removes the published source instead, and `archilyzer doctor` reports the tools, the two files (rule counts and permissions, never their contents) and the last publish. `create-archives.sh` is gone. See PUBLISH.md, "The source mirror (homepage)". - **umtool reads the corpus from its checkout (or `TRANSCRIPTS_DIR`), and the song project's data defaults to `~/.local/share/archilyzer/song`.** If yours is elsewhere, link it there before restarting umtool: `mkdir -p ~/.local/share/archilyzer && ln -s <where the data is> ~/.local/share/archilyzer/song` (the data stays where it is). With no `CHANNELS_DIR`, umtool reads the corpus at `$TRANSCRIPTS_DIR/channels`, else the checkout's own `transcripts/channels`; it used to fall back to an absolute path that existed on one machine only. The song project's videos default to `~/reports/quartering-uh-song/videos`; `SONG_DIR` and `VIDEO_ROOT` still win. The song project's tracked manifests record their paths relative to the song folders, and the twenty one-off `umtool/song/*.sh` run logs, which only ever ran on the machine that wrote them, are gone. - **umtool's production build no longer reads the corpus folder.** Since umtool began finding the corpus from its checkout (the bullet above), `next build` treated the checkout's whole `transcripts/channels` as files to bundle. On a real archive it ran out of memory and was killed, so umtool could not be rebuilt. The build now ignores that folder and finishes in about 25 s at under 1 GB, the same as a checkout with no corpus. Nothing changes when umtool runs. diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -7329,7 +7329,8 @@ Slices Q (`4855f70b`) and R (`ffdeb2cd`): [`release-12.md`](release-12.md), the `path.resolve` / fs call on the result becomes an asset reference: to a file, or, when the joined path is a directory, to every file under it (`DirAssetReference`). - **A worktree build will not show it.** A worktree has no `transcripts/`, so the reference is - empty. **Test with the corpus visible.** + empty. **Test with the corpus visible.** Nor does it have umtool's e2e fixture (`.e2e-song`, + `.next-e2e`), which is what slice UT's pattern reached (below). - **What happened:** slice Q wrote `path.join(REPO_ROOT, "transcripts", "channels")` with `REPO_ROOT = findRepoRoot(process.cwd())` in `umtool/lib/paths.mjs`. The walk's fallback, `path.resolve(start, "..")`, evaluates to the project root. @@ -7340,18 +7341,45 @@ Slices Q (`4855f70b`) and R (`ffdeb2cd`): [`release-12.md`](release-12.md), the ModuleReference>::resolve_reference failed … Symlink [project]/transcripts/channels/<slug>/archive is invalid, it points out of the filesystem root`. - A worktree built it in 30 s at 0.8 GB, which is how the gate passed. -- **The opt-out is per expression and documented:** `path.join(/* turbopackIgnore: true */ - process.cwd(), bar)`. That exact text is Turbopack's own advice in its "whole project was traced" - message; the table is in the Next docs, `03-api-reference/08-turbopack.md`, "Magic comments". It - goes before the FIRST argument of each path or fs call on such a value, and it changes nothing - at run time. Per call, not per value: +- **The opt-out is per expression, and it is not documented for path or fs calls** (corrected by + release 15, slice UT). `path.join(/* turbopackIgnore: true */ process.cwd(), bar)` is Turbopack's + own advice, in the text of its "Encountered unexpected file in NFT list" issue (the "whole project + was traced" warning; the 16.2.3 native binary carries it), and Next's own runtime uses the form + (`join(/* turbopackIgnore: true */ contextDir, …)`). The Next docs (`08-turbopack.md` and + `02-guides/lazy-loading.md`, "Magic Comments") list the comment only for `import()`, `require()`, + `require.resolve()` and `new Worker()`. It goes before the FIRST argument of each call, and it + changes nothing at run time. Measured in slice UT, it works on a `path.join` and on an fs call. + Per call, not per value: - a nested call needs its own marker (`path.dirname(/* turbopackIgnore: true */ fileURLToPath(import.meta.url))`); - - an outer fs call on an opted-out `path.join` is covered. -- **Not followed by the tracer:** `os.homedir()` and `process.env.*`. A build with `HOME` pointed at - a synthetic home inside the project, full of out-of-root symlinks under `reports/`, - `.local/share/archilyzer/song` and `.cache/`, succeeded. So `~/reports` and the XDG song path are - safe as `path.join(os.homedir(), …)`. + - **an fs call on an opted-out `path.join` is NOT covered**, nested or through a variable: it + traces the join's value. Slice UT, on umtool's clip-audio route. With 4 probe files in its dot + directories: the join opted out and the fs calls on its value kept, 4 traced; those calls + stubbed, 0. With the primary's fixture: one `existsSync(path.join(/* opt-out */ CACHE_DIR, …))`, + 1,704; the same with the `existsSync` opted out as well, 0; the join held in a variable and + only `existsSync(/* opt-out */ cached)` reading it, 0. On a cwd-derived value the join is a + known path, so the outer call traces the one file it names; the guard lets that through (its + comment says so). +- **A value the tracer cannot know is a dynamic part, not ignored** (corrected by release 15, slice + UT). `process.env.*`, `os.homedir()`, a parameter and an imported binding all make patterns over + the app's own directory (`umtool/`): + - 66 of umtool's 68 routes trace its whole tree outside dot-directories (361 files, + `next.config.ts` among them). Opting out every path op in `song/paths.mjs`, `lib/paths.mjs` + and `lib/paths.ts` (`path.resolve(process.env.X ?? path.join(os.homedir(), …))` and the like) + cleaned 31 routes; every path op in all 53 modules that have one (319 calls), 49. The rest + come through fs calls (`lib/report/snapshots.mjs` is the first the warning names). + - The clip-audio route's `path.join(CACHE_DIR, `${stamp}.${asMp3 ? "mp3" : "wav"}`)` (CACHE_DIR + imported, built on the env or home directory) also took in the dot-directories: 1,704 of its + 2,167 entries were the e2e fixture (`.e2e-song`), the e2e server's build directory + (`.next-e2e`) and `.env.local`. Hoisting the ternary into a `const` changed nothing. Without + the ternary (`${stamp}.wav`), as a ternary of two joins, or with the name handed to a function + from another module (the fix, `cacheFile` in `umtool/lib/paths.mjs`), it did not. The video + route's `.mp4` join did not reach the `.mp4` inside `.e2e-song`. + - **A pattern walk does not enter a symlinked directory**, in the root or out of it: a link at + `umtool/.e2e-song/data/planted` to `<primary>/transcripts/channels`, and one to the worktree's + `common/` (675 files), gave 0 entries, before and after the fix. That is what the synthetic-HOME + build showed, not that the env and home directory are unfollowed. A KNOWN directory (slice Q's + `<root>/transcripts/channels`) is different: it is walked through its symlinks. - **The guard is `scripts/next-build-trace.test.mjs`** (in `test:scripts`; it was `umtool-build-trace.test.mjs` until release 14 widened it). It scans umtool's app, components, lib, `report-to-video/*.mjs` and `song/paths.mjs`, and, since release 14 (F8), `homepage/app`, @@ -7360,14 +7388,28 @@ Slices Q (`4855f70b`) and R (`ffdeb2cd`): [`release-12.md`](release-12.md), the `process.cwd()`, `import.meta.url|dirname|filename`, `__dirname`, or a call to a function declared in the file whose body carries one, must open with the opt-out. - It is static and per module, as Turbopack's value analysis is: an imported binding is opaque to - it. + it (to Turbopack it is an unknown, which is a dynamic part: see above). - With the slice Q `paths.mjs` it fails on the defect's line. + - **Since release 15 (slice UT):** + - The scan set also follows every relative import out of those folders, which adds + `common/bin/_publicFile.ts`, `homepage/content/docs.ts` and seven `umtool/song` modules + (umtool 211 → 218 modules, the rest 895 → 897); a test pins them. + - The checked calls include `open`, `writeFile`, `appendFile`, `createWriteStream` (and their + Sync forms) and the `fs.promises.` / `fsPromises.` prefixes. No new finding. + - **It reads umtool's last build back** when `umtool/.next/server` exists (it skips, saying + so, when not). Every `.nft.json` under `umtool/.next` but `cache/` and `dev/` fails on an + entry outside the repo, under `transcripts/`, or through any name starting with a dot other + than the build's own directory and `node_modules/.pnpm`. On the old clip-audio route it + fails with 1,704 entries; on the fix it passes. It reads what is on disk: in a checkout whose + umtool build predates the fix, it fails until umtool is rebuilt. - **The build gate** is run with the corpus visible and under a memory cap (the command is in `plans/tools/implementer-rules.md`). Linking `<primary>/transcripts` into a worktree is for a BUILD only. Remove the link afterwards: never run an app, an index or a fixture builder through it. - **The other apps were safe by accident; since release 14 (F8) they are by rule:** - `common/lib/paths.ts` builds every path on the repo root through one opted-out `under()`, and - `findMonorepoRoot()`'s walk and its `process.cwd()` fallback are opted out. + `findMonorepoRoot()`'s walk and its `process.cwd()` fallback are opted out. Since release 15 + `under(first, ...rest)` puts the marker before a named first argument, not a spread (the + release 14 review's L3); `getPaths()` is unchanged. - `homepage/app/lib/source.ts`' directory join on `public` (the source mirror) and the homepage's and export's other cwd joins carry the opt-out. - The guard covers them. A homepage build with the published source measured the same with and diff --git a/plans/release-15.md b/plans/release-15.md @@ -216,4 +216,194 @@ checkout's code, so they hold from the moment `main` has this branch. The editor **Build index** button runs its built bundle, so it holds only after the editor is rebuilt and restarted. +### Slice UT, as shipped — umtool's build stops tracing its dot-directories (2026-09-29) + +Branch `r15/umtool-trace` off `main` `ccf90892`, worktree `~/Projects/r12-source-mirror` (block +#13: editor 4301, test 4311, export 4310), one Opus implementer. Scratch files `ut-*` in the job's +`tmp`. The ruling: find the one expression that widens the clip-audio route's trace and fix it +there; exclude the fixture, the e2e build and env files as a second line; narrow or drop the +`ignoreIssue`; make the trace guard read a build back and close the release 14 review's L1 and L3. + +**What was wrong.** With the primary's e2e fixture in place, umtool's +`app/api/clip/[key]/audio/route.js.nft.json` listed 2,167 files: the 463 its sibling routes list, +178 under `.e2e-song/` (the fixture, where `make-fixture.mjs` links the song data), 1,525 under +`.next-e2e/` (the e2e dev server's build directory, 1.1 GB) and `.env.local`. Turbopack's warning +for it ("Encountered unexpected file in NFT list", the "whole project was traced" text) was silenced +by the config's `ignoreIssue`. The traces are not consumed while `output: "standalone"` stays off, +so nothing broke; a fixture with more in it, or a standalone build, would have carried it. + +**The bisect.** One change per build, in this worktree with four probe files planted in +`.e2e-song/probe/` and `.next-e2e/probe/`; the audio route's trace, total / under dot-directories. +The route as on `main`: **467 / 4**. + +| Change (line on `main`) | Entries | +|---|---| +| `existsSync(file)` :51 stubbed | 467 / 4 | +| **the join `path.join(CACHE_DIR, `${stamp}.${asMp3 ? "mp3" : "wav"}`)` :58 written as a string concatenation** | **463 / 0** | +| `existsSync(cached)` :61, `readFile(cached)` :62, `mkdir(CACHE_DIR)` :78, `readFile(tmpMp3)` :89, `rename(tmpMp3, cached)` :90, `writeFile(tmp)` :93 or `rename(tmp, cached)` :94 stubbed, each alone | 467 / 4 each | +| the `tmpWav` / `tmpMp3` joins :82-83 as concatenations; `writeFile(tmpWav)` :84 stubbed; both `writeFile`s stubbed; either `writeFile` opted out | 467 / 4 each | +| the opt-out on the :58 join | 467 / 4 | +| the :58 ternary hoisted into a `const ext` | 467 / 4 | +| **:58 without the ternary** (`${stamp}.wav`) | **463 / 0** | +| :58 as `path.join(CACHE_DIR, asMp3 ? `${stamp}.mp3` : `${stamp}.wav`)` | 463 / 0 | +| :58 as a ternary of two joins | 463 / 0 | +| opt-outs on `existsSync(cached)` and `readFile(cached)`, or either alone; both stubbed | 467 / 4 each | +| all five readers and writers of `cached` stubbed | 467 / 4 | +| **all five stubbed, and the opt-out on the :58 join** | **463 / 0** | +| all five stubbed, and the join as a concatenation | 463 / 0 | + +With the primary's fixture (the table's 4 are 1,704 there), on top of the last-but-one row: + +| Change | Entries | +|---|---| +| one `existsSync(path.join(/* opt-out */ CACHE_DIR, …))` | 2,167 / 1,704 | +| the same `existsSync` opted out as well | 463 / 0 | +| `existsSync(/* opt-out */ cached)` as the only reader of the opted-out join | 463 / 0 | + +**The expression** is the :58 join, and in it the ternary inside the template literal. The join and +the fs calls on its value each trace the pattern (the join alone with every reader stubbed; the +readers alone with the join opted out; `existsSync` is one such reader), so no single opt-out or +stub cleared it. Without the ternary, the pattern stays out of the dot-directories. + +**The fix** (`a305b956`). `umtool/lib/paths.mjs` gains `cacheFile(name)`, `path.join(/* opt-out */ +CACHE_DIR, name)`, re-exported by `lib/paths.ts`. The audio route names all four of its cache files +through it (`cached`, `tmpWav`, `tmpMp3`, and `tmp`, now `cacheFile(`${stamp}.wav.tmp`)`, the same +path as `${cached}.tmp` in that branch). A value returned by a function from another module is +opaque to the tracer, so the call site traces nothing: the route lists 463, as its siblings do. The +video and face-frame routes join `CACHE_DIR` with a fixed extension; they were measured clean (the +video route's `.mp4` join did not reach the `.mp4` in `.e2e-song`) and name their cache files the +same way, so no route joins `CACHE_DIR` itself. The run-time paths are unchanged. + +**The second line** (`f18fa034`). `outputFileTracingExcludes: { "/*": ["./.e2e-song/**/*", +"./.next-e2e/**/*", "./.env*"] }` (Next 16.2.3's `05-config/01-next-config-js/output.md`: route +globs to globs from the project root; Turbopack reads the key natively, `collect-build-traces.js` +is the webpack path). Measured alone, with `main`'s route: 2,167 → 463. + +**The warning stays silenced, narrow as it was (path + title), with its measured reason.** Dropping +it shows one warning on every build, and after the fix it is still true: +- 66 of the 68 routes trace umtool's whole tree outside dot-directories: its 361 files, + `next.config.ts` (the file the warning names) among them. Only `_global-error` and `_not-found` + do not. +- Path and fs calls on env, home-directory and parameter values do it. Opting out every path op in + `song/paths.mjs`, `lib/paths.mjs` and `lib/paths.ts` cleaned 31 routes (the audio route 463 → + 102). Opting out all 319 path ops in the 53 modules that have one cleaned 49. The rest come + through fs calls; the next import trace the warning names is `lib/report/snapshots.mjs`. +- That walk skips dot-directories and does not enter symlinks. The warning names the same file for + it as for the audio route's, so it cannot tell the two apart; the second line and the guard below + cover the dot-directories instead. The config's comment says all of this. + +**A symlinked directory is not entered by these patterns.** The planted link +`umtool/.e2e-song/data/planted` → `<primary>/transcripts/channels` gave 0 entries before and after +the fix, and an in-root link to `common/` (675 files) gave 0 on `main`'s route. What the pattern +reached was the fixture's real files. + +**The guard** (`scripts/next-build-trace.test.mjs`, `9a375ecd`), 6 → 9 tests: +- **(a) It reads umtool's last build back.** When `umtool/.next/server` exists, every `.nft.json` + under `umtool/.next` (`cache/` and `dev/` aside) fails on an entry outside the repo, under + `transcripts/`, or through any name starting with a dot but the build's own directory and + `node_modules/.pnpm`. With no build it skips and says how to make one. On `main`'s route it fails + with 1,704 entries (the first 20 listed); on the fix it passes. A unit test pins + `forbiddenTrace`. +- **(b) The scan set follows relative imports** out of the listed folders, to any depth. It adds + the review's L1 modules and no others: `common/bin/_publicFile.ts`, `homepage/content/docs.ts` + (895 → 897) and seven `umtool/song` modules, `reasons`, `archive-url`, `pitch`, `flatness`, + `clipwindow`, `deplosive`, `orderfeat` (211 → 218; `song/paths.mjs` was listed by hand before and + is now reached). A test pins them, and that a song CLI and a common CLI stay out. The comments + that called them CLI-only are gone. +- **(c) The checked calls** add `open`, `writeFile`, `appendFile`, `createWriteStream` and their + Sync forms, and the `fs.promises.` / `fsPromises.` prefixes. No new finding. +- **(d)** `common/lib/paths.ts` `under(first, ...rest)` (`8b3409c2`): the opt-out sits before a + named first argument. `getPaths()` hashed identical, old module against new, from the repo root, + `editor/` and a directory outside the repo (56 keys). The guard passes; every caller type-checks. +- The header no longer says the env and home directory are unfollowed or that the opt-out is + documented. The nested-call exemption's comment says it is a simplification (below). + +**FACTS**, "A path joined from `process.cwd()` …", corrected: +- "documented": the Next docs list `turbopackIgnore` only for `import()`, `require()`, + `require.resolve()` and `new Worker()`. The path form is Turbopack's own advice, in the warning's + text in the 16.2.3 binary, and Next's own runtime uses it. +- "an outer fs call on an opted-out `path.join` is covered": it is not (the second bisect table). +- "Not followed by the tracer: `os.homedir()` and `process.env.*`": such values are dynamic parts, + and make patterns over the app directory. The synthetic-HOME build showed that a pattern walk + does not enter symlinks. +- The guard's entry, the worktree caveat (no fixture either) and `under()`'s shape. + +**Commits** + +| Commit | What | +|---|---| +| `a305b956` | `umtool:` `cacheFile`; the audio, video and face-frame routes name their cache files through it | +| `f18fa034` | `umtool:` `outputFileTracingExcludes`; the `ignoreIssue` kept, its comment the measured reason | +| `8b3409c2` | `common:` `under(first, ...rest)` | +| `9a375ecd` | `scripts:` the post-build check, the relative-import scan set, the opens and writes, the header | +| this commit | `plans:` this section; FACTS; the editor changelog | + +#### Gates (logs `$T/ut-*.log`) + +- **tsc** clean over the combined tree before the first commit, 241 s (load average ~26). The + commits are independent pieces of that tree; since then only a comment changed in a + type-checked file (`cacheFile`'s, in `umtool/lib/paths.mjs`). +- **`test:scripts`:** 194 passed, 1 skipped (195): `main`'s 191 + 1 and the guard's three new + tests. Before the final build it failed exactly the post-build test, on a build of `main`'s route. +- **common:** 2,220/2,220, 107 s. +- **umtool's build, capped at 5 GB with no swap, with the primary's `transcripts/` linked in, the + primary's `.e2e-song` and `.next-e2e` hard-linked in, an empty `.env.local`, and the planted + link** (all removed afterwards; none committed): + + | Tree | Wall | User | Max RSS | Audio route | `.e2e-song` | `.next-e2e` | `.env*` | `transcripts` / planted | + |---|---|---|---|---|---|---|---|---| + | `main`'s route and config | 23 s | 57 s | 809 MB | 2,167 | 178 | 1,525 | 1 | 0 / 0 | + | the branch | 34 s | 64 s | 765 MB | 463 | 0 | 0 | 0 | 0 / 0 | + + The wall times swing with the machine's load (another slice's e2e and builds ran alongside); + compile was 7.5 s and 10.7 s, TypeScript 12 s and 19 s. +- **The editor's build**, capped, with the corpus linked in (`paths.ts` changed): 97 s wall, 159 s + user, max RSS 1,576 MB (IG's: 64 s / 1,642 MB, under less load); 0 of its 81 traces' entries + under `transcripts/`, and none that `forbiddenTrace` refuses. +- **e2e** (umtool's own filter, `SONG_DIR=~/reports/quartering-uh-song/data`, queued): + `find.spec.ts` and `triage.spec.ts` fetch the audio route, `faces.spec.ts` the face-frame route, + and `find.spec.ts` names the video route: **6 passed, 29 skipped, 0 failed, 27 s** (6.3 min with + the queue). The skips are the fixture's: this machine has no `wav48/`, `asr/` or `media/`, so + every spec that fetches one of the three routes skipped, and they are not exercised at run time + here. What stands for that: calling `cacheFile` gives the same path as the old expression for + all six names (the four audio names, and the video and frame temporaries). +- **After the e2e run** (which built this worktree its own fixture and `.next-e2e`), a last capped + build with the corpus linked: the audio route 463, none under a dot-directory; `test:scripts` + 194 passed, 1 skipped. The first run of that `test:scripts` failed `queue-lock.test.mjs`'s FIFO + case once (`S1E1S3E3S2E2`) under a load average of about 26; it passed on the rerun, and this + slice does not touch the queue lock. +- **Numbers tool:** none. + +#### Found and left + +- **The whole-folder trace in 66 routes** (above). Bounded to umtool's own files; cleaning it means + opt-outs on hundreds of path and fs calls on unknown values, which no static check can find. +- **The guard's nested-call exemption.** Without it, four calls would need an outer opt-out: + `common/lib/paths.ts:311` (`existsSync` of one file), `export/app/changelog/page.tsx:9` and + `homepage/app/changelog/page.tsx:24` (one file each; other slices own them), and + `umtool/report-to-video/brand.mjs:82` (the brand kits, which a standalone build needs). Each + traces the file or files it reads. Left, and the comment says so. +- **Which fs calls Turbopack traces is not established per call.** `existsSync` does (the second + table); the writes were added to the guard without a measurement, since an extra opt-out costs + nothing. +- **The post-build check reads umtool only.** In this worktree the editor's 81 traces pass the same + rule. The review's Info saw `editor/.env` in the primary's; a worktree has none, so it was not + re-measured. +- **The gate command in `implementer-rules.md`, in a worktree that has a `transcripts/` directory,** + makes `transcripts/transcripts` and builds without the corpus where the paths point. This worktree + had one (an `index.mdb` from 2026-09-28), and my first two corpus-linked builds ran like that. The + numbers above are from builds that set it aside and put it back. `ln -sT` would refuse instead. +- **A checkout whose umtool build predates this fails `test:scripts`** until umtool is rebuilt: + the post-build check reads what is on disk. The primary's `umtool/.next` is from before it. + +#### Decisions the operator could overturn + +| What I did | The alternative | +|---|---| +| A `cacheFile` helper in `lib/paths.mjs`, used by all three routes that join `CACHE_DIR` | Opt-outs on the audio route's join and on every fs call on its value, in that route only | +| The `ignoreIssue` stays, narrow, with the measured reason in its comment | Drop it: one warning on every build, naming one route's import trace | +| The post-build check refuses any dot-named path but the build's own and `node_modules/.pnpm` | Refuse only `.e2e-song`, `.next-e2e`, `.env*` and `.git` | +| The post-build check covers umtool only | Also read the editor's, the export's and the homepage's builds | +| The static check keeps its nested-call exemption, documented as a simplification | Require the outer opt-out: four new findings, two in files other slices own | + ## Rollout