Archilyzer · Source

archilyzer

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

commit 7563c14c9c8456b6ddd100ced0bbad6595f9c1e7
parent d45e1cc4cdfda5d3ba5008811da95e0f69cd1f41
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 20:56:12 -0400

plans: slice U1, as shipped — umtool's render scratch goes to a media root

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

Diffstat:
Mplans/release-17.md | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 175 insertions(+), 0 deletions(-)

diff --git a/plans/release-17.md b/plans/release-17.md @@ -323,4 +323,179 @@ hand; a dirent `isFile()` filter over a video dir hides it."** The `.relocating. ## Record +### Slice U1, as shipped — umtool's render scratch goes to a media root (2026-10-01) + +Branch `r17/umtool-media-root` off `main` `7f4901f1`, `main` `90bd8384` (the deck/posts-room merge) +merged in mid-slice, worktree `~/Projects/homepage-social-visible` (`pnpm wt list` block #11: editor +4101, test 4111, export 4110), one Opus implementer. Scratch files `U1-*` in the job's `tmp`. The +ruling is the plan's: render scratch (`out/`) goes to a media root by default; deliverables move per +project by a switch (slice U2); manifests, `revisions/`, the caches and the cue cache stay put. + +**What it does.** +- **Two roots, one new knob.** `umtool/lib/paths.mjs`: `MEDIA_ROOT = UMTOOL_MEDIA_DIR || REPORTS_ROOT`, + `MEDIA_TIERED` (they differ), `mediaMirror(abs, roots?)` (a path under `REPORTS_ROOT` → the same + relative path under `MEDIA_ROOT`, null outside; pure). `MEDIA_ROOT` joins `READ_ROOTS`, never + `WRITE_ROOTS`. Unset, nothing changes: `out/` is a directory in the project, and `READ_ROOTS` dedupes + it away. Every path op carries `turbopackIgnore`. +- **The cache leaves `SONG_DATA`.** `CACHE_DIR = UMTOOL_CACHE_DIR || $XDG_CACHE_HOME/archilyzer/umtool` + (an empty `XDG_CACHE_HOME` is unset, as `common/lib/paths.ts` reads it; default `~/.cache`). + `INDEX_DIR`, `MIX_CACHE`, the posters, loudness and clip audio follow it. `OLD_CACHE_DIR` + (`<SONG_DATA>/.cache/umtool`) is named only for the doctor. Nothing is migrated: the index is + rebuilt by `umtool index` and everything else is remade on demand. The cue cache + (`REPORT_CACHE_DIR`, `report-to-video/cues.mjs`) is untouched. +- **`umtool/lib/report/storage.mjs`** (new; modelled on `common/controller/relocateDir.ts`, not + importing it): + - `ensureOutDir(projectDir, roots?)`: a real `out/` → kept; a link to a directory → kept; a + **dangling link → refused** ("… is a link to …, which is not there — is the media drive mounted? + Nothing was written, and nothing was created in its place."); absent and tiered → + `mkdir -p <mirror>/out` and an absolute `symlink`; absent and not tiered → `mkdir` as before. The + media root itself is **stat'd and never created** (`mediaRootProblem`: missing, not a directory, or + inside/around `REPORTS_ROOT`). A project outside `REPORTS_ROOT` is never tiered. EEXIST from a + concurrent first writer is accepted when the winner resolves. + - `ensureWriteDir(dir)`: a directory a pipeline step writes into; when it is a project's `out` or up + to four levels under one, that `out` goes through `ensureOutDir` first, then `mkdir -p`. + - `moveDirToMedia(projectDir, name, opts)` / `moveDirToLocal(...)`: by NAME (`out` now; `clips`, + `share-*` for U2). Copy (`rsync -a --partial`), mirror toward the copy only (`-a --delete + --info=del`; refused when source and copy contain one another), verify (`--dry-run + --itemize-changes --delete` empty, one more mirror pass on a difference, a second refuses; equal + counts/bytes), then park (`<name>.moved-<ts>`), link, delete the parked copy. Space check on the + destination's volume (bytes + 1 GB). Every state is dispatched on the disk, so a cut run is + finished by running it again: a link to the mirror → `already` (a leftover parked copy removed); + absent with one parked copy → link and delete it; the reverse uses `<name>.incoming`, and after + the rename deletes the media copy and every directory above it the move left empty, never the + root. `dryRun` measures and changes nothing. + - `outDirState`, `pathState`, `measureTree` for readers and the CLI. +- **Call sites.** `build-video.mjs` (`outRoot`, before any fetch), `check-availability.mjs` (its one + write), `render-cards.mjs` (CLI `--out`), `compose-chrome.mjs` (its `out/<variant>` base), + `lib/report/onscreen.mjs` (`deckStill`'s scratch) all make `out/` through `ensureWriteDir`. + `lib/report/export.mjs` reads only: it now says "out/ is a link to …, which is not there — is the + media drive mounted?" instead of "no build" when the link dangles. tmp-then-rename sites (`cut.mjs`, + the clip route) are untouched. +- **The walk.** `kinds.mjs` `SKIP_DIRS` adds `clips` (`out` was already there) and `SKIP_PREFIXES = + ["share-"]`, read through `skipsDir(name)` by `walk.mjs`, so the project walk never stats a link + into a drive that is not there. The mix picker (`lib/media.ts`) follows a project's `out` link when + it points INTO the media root (so a tiered deliverable stays in the picker under its project) and + does not walk `MEDIA_ROOT` as a root of its own (it would list every tiered file twice). +- **CLI.** `umtool doctor` adds `roots` (JSON) / a "roots" block: reports, media (tiered or "= + reports"), cache (and whether an index exists), and the old cache with its size while it is there; + it exits 1 when the media root is set and missing (the tools' `ok` keeps its meaning). `umtool + storage [<project>]` lists every project's `out` (dir, link, DANGLING, none); `umtool storage + move-out|move-back <project>|--all [--dry-run] [--json]` runs the movers, one line per project and a + total; move-out without `UMTOOL_MEDIA_DIR` refuses once. +- **e2e env.** The app server and the specs' CLIs get `UMTOOL_CACHE_DIR=<fixture>/cache` + (`playwright.config.ts`, `projects.spec.ts`, `report-longform.spec.ts`; the index-deletion spec + now removes `cache/index`), so no run writes `~/.cache`. `make-fixture.mjs` adds + `storage-fixture` (a cached window, buildable offline), `storage-fresh-fixture` (no `out/`) and the + media root `umtool/.e2e-song-media/`, a sibling of the fixture (inside it would be inside + `REPORTS_ROOT`, which is refused), reset every run; `.gitignore` and umtool's trace excludes name + it. `storage.spec.ts` (new, 5): move-out (dry run first; the index's state and facts unchanged + through the link; again → already); **a build the app runs writes through the link and leaves it a + link** (the app has no `UMTOOL_MEDIA_DIR` at all); the root renamed away → `storage` says + dangling, `check-availability` refuses with the drive sentence on the moved project and with "is + not there" on the fresh one, no `out` made, the root not recreated, `doctor` exits 1; the first + writer of the fresh project makes the link; move-back → a real `out/`, the project's mirror gone, + the root and the other project's mirror kept. + +**Commits** + +| Commit | What | +|---|---| +| `65a3d146` | `umtool:` `MEDIA_ROOT`, `MEDIA_TIERED`, `mediaMirror`; `MEDIA_ROOT` in `READ_ROOTS`; `CACHE_DIR` from `UMTOOL_CACHE_DIR` / `XDG_CACHE_HOME`; `OLD_CACHE_DIR` | +| `47d6d1b4` | `umtool:` `lib/report/storage.mjs`; the five writers through `ensureWriteDir`; export's dangling sentence; `clips` + `share-*` skips; the picker follows `out` links into the media root; `doctor` roots; `umtool storage` | +| `4c2cd0c7` | merge of `main` `90bd8384` (the deck/posts-room branch: `build-video.mjs`, `make-fixture.mjs` and more) — one conflict, `export.mjs`'s imports, both kept | +| `cb08e57a` | `umtool:` `storage.test.mjs` (20); the e2e cache in the fixture; the storage fixtures, media root and `storage.spec.ts`; `umtool storage <project>` status | +| `efef56b3` | `umtool:` `docs/folders.md` (`MEDIA_ROOT`, `CACHE_DIR`), `docs/cli.md`; two `[Unreleased]` bullets in `editor/CHANGELOG.md` | +| this commit | `plans:` this section | + +#### Gates (logs `$T/U1-*`) + +- **tsc** (all workspaces) clean at `47d6d1b4`, at the merge `4c2cd0c7` and at `cb08e57a`. +- **common:** 2,484/2,484 (300 s, under load). **Editor unit:** 109/109. Neither touched; run on the + merged tree. +- **test:scripts:** 390 tests (the merged `main`'s 370 + `storage.test.mjs`'s 20): 386 passed, 1 + skipped, 3 failed, then 385/2/3 on a rerun — the three are `queue-lock.test.mjs` timing cases, a + different three each time, at a load average of 27–47 (other implementers' suites and builds); + `node --test scripts/queue-lock.test.mjs` alone: **11/11**. `storage.test.mjs` **20/20**. + `next-build-trace.test.mjs` is in it: **10/10** after each capped build below (its second skip on + the rerun is the staleness rule: the baseline run's `git checkout` of `main`'s umtool, below, gave + the modules new mtimes after the build). +- **The capped umtool build with the corpus linked** (`ln -sT <primary>/transcripts transcripts`, + 76 channels visible through it; `systemd-run --scope -p MemoryMax=5G -p MemorySwapMax=0`, `timeout + -s KILL 240`, the link removed after), at `cb08e57a`: **exit 0, 53 s, 0.83 GB peak**; and once more + with `UMTOOL_MEDIA_DIR` set to a scratch directory: **exit 0, 53 s, 0.83 GB**. The two builds' + `.nft.json` entries (39,658 each, every route) are **identical** (`diff` empty); none names + `transcripts`, the scratch media root or `.e2e-song`. (The worktree carries an old `transcripts/` + directory — an `index.mdb` — which `ln -sT` refuses to replace: the script sets it aside for the + build and puts it back. A first attempt that did not was stopped before it counted.) The capped + editor build was not run: no editor code changed (only `editor/CHANGELOG.md`). +- **Numbers tool:** none. +- **umtool e2e** (`SONG_DIR=~/reports/quartering-uh-song/data pnpm --filter umtool run e2e …` from + the worktree root; the fixture found song data, `cand2`, `wav48` and `media`, no `asr`, no face + detector, so `find.spec`'s 14 skip): + + | Run | At | Specs | Result | + |---|---|---|---| + | 1 | `efef56b3` | `storage`, `projects`, `report-longform`, `dashboard` | 37 passed, 6 failed, 10.3 min — `storage.spec` **5/5**; the six (`dashboard` ×3, `projects` ×3) are `page.goto: net::ERR_ABORTED` and 30 s timeouts at a load average of 47, and all six pass in run 2 | + | 2 | `efef56b3` | the full suite (21 files) | **214 passed**, 17 failed, 14 skipped, 13.8 min of tests (48 min with 34 min in the queue) — `faces` ×4 (`/api/face/detect` 503: no detector here), `triage` ×9 (no `asr` here), `browse:241`, `mix:166`, `mix:201`, `usage:112` | + | 3 | `main` `90bd8384`'s umtool, checked out into the worktree and restored after | `browse`, `faces`, `mix`, `triage`, `usage` | 48 passed, 15 failed, 7.2 min — the same `faces` ×4 and `triage` ×9, plus `browse:15`/`:34` (30 s timeouts) | + | 4 | `efef56b3` | the same five | 49 passed, 14 failed, 3.2 min — `faces` ×4 and `triage` ×9 as on `main`; `browse:241`, `mix:166`, `mix:201` pass; `usage:112` fails again | + | 5 | `efef56b3` | `usage` | **7 passed**, 0 failed, 19.6 s | + + So against `main` on this machine: the `faces` and `triage` failures are the machine's (both + missing capabilities fail rather than skip — on `main` too); `mix:166`/`:201` and `browse:241` + fail only after the whole suite (the corpus window an earlier spec fetched for `vid1` wins the + picker's lookup), and pass in isolation on both; `usage:112` ("confirming the drop writes it + through") failed twice when it ran right after the failing `triage` specs on this branch, passed + once in that position on `main`, and passes alone — the verdict path it drives reads no cache and + no `out/`. Left to the reviewer as an order/timing question, not changed. + +#### Found and left + +- **Open question 2 — the four `*.mp4` near the project roots** (measured in `~/reports`, depth ≤ 2, + outside any `out/`): `kirsche-pippa/latest-contact-2026-06-20.mp4` (7.4 MB) is a cited clip fetched + through the MCP's `fetch_clip`, with its `.provenance.json` beside it — the sweep report's evidence, + a deliverable of a project that has no `out/`; `quartering-uh-song/jer-metalslug-bg.mp4` (51.5 MB) + and `quartering-uh-song/pokemon-no-music-recording.mp4` (3.1 MB) are song-project INPUTS (a song + spec's `background.path` names such a file relative to a media root, `song/spec.mjs`); `clips/ + tim-pool-…mp4` (23.6 MB) is a loose cut at the reports root, in no project. None is render scratch: + all four are left untouched, and none is under U2's `clips/` or `share-*/`. +- **What move-out would move today:** `umtool storage move-out --all --dry-run` against `~/reports` + (a scratch media root): **10 projects, 10.6 GB** (quartering-diet 5.0 GB, ferret-rescue 1.5 GB, + quartering-employee-count 1.2 GB, elfpire-eva 1.1 GB, …) — less than the plan's "≈ 18 of the 20 + GB": the rest of `~/reports` is song data and loose files, not project `out/`s. +- **Rollout, once:** set `UMTOOL_MEDIA_DIR` (the live umtool's environment) to a directory that + exists on the media drive, outside `~/reports`; restart umtool; run `umtool index` (the index is + rebuilt under `~/.cache/archilyzer/umtool`; until then everything works, slower); `umtool doctor` + shows the roots and the old cache (8.7 MB here), which can then be deleted; `umtool storage + move-out --all` (when nothing is building) moves the existing `out/`s. +- **A dangling `out` reads as "no build" to the summary readers** (`lib/projects/report.mjs`'s + `stat0(out)`, `readAvailability`): only `export` and the writers say "is the media drive mounted?". + `umtool storage` and `umtool doctor` name it. `umtool check` learning it is U2's (plan: "`umtool + check` learns the two values"). +- **For U2:** `lib/report/deliver.mjs` `listBatches` filters `isDirectory()` on the project's dirents, + so a `share-*` that is a link would vanish from it; `sharedIdsIn`'s walk likewise does not follow a + link. The movers take any one-segment name and return `{ state, src, dest|from, bytes, files }`. +- **A CLI move cannot see the app's jobs** (they live in its memory): the verify refuses when the tree + keeps changing, but a write in the instant between the verify and the park would be deleted with + the parked copy. The CLI says "run when nothing is building"; U2's bench button runs in the app and + can check. +- **The worktree's stray `transcripts/`** (an `index.mdb` from 2026-09-28) is the trap the rules + describe; left in place. + +#### Deviations from the plan + +- `ensureOutDir` is not called in `driver.mjs`: its step builders are synchronous, are unit-tested with + a fake project directory, and only build argv; the call is in the scripts those steps run + (`build-video.mjs`, `check-availability.mjs`) through `ensureWriteDir`, which also covers a + hand-run script and the three other writers the plan did not list (`render-cards.mjs`, + `compose-chrome.mjs`, `onscreen.mjs`). +- `export.mjs` writes nothing under `out/`, so it does not create it; it reports a dangling link instead. +- `umtool storage move-back` and the plain `umtool storage [<project>]` listing were added beside + `move-out`: the e2e needs the way back, and an operator needs to see which projects moved. +- `lib/media.ts` (not in the plan) follows `out` links into the media root and skips the root as its + own: without it every moved deliverable fell out of the mix picker. + +`[Unreleased]` (`editor/CHANGELOG.md`): "umtool can keep each report's render folder on a media +drive." and "umtool's cache moves to `~/.cache/archilyzer/umtool`". + ## Rollout