Archilyzer · Source

archilyzer

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

commit 06ea3aa2beb60d1132295a82aacbb73ea41cd247
parent 947f28fb8f960533864fa9abf4f225cfe89426c4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 11 Sep 2026 21:08:26 -0400

plans: the merge review's two fixes, and the one conflict with channel-priority/s5

The "As shipped" section gains a third review round — the shared relocation queue
and the nothing-to-move skip, and the settings-fixture merge that replaced the
one-key default — and the sha range moves to `1939ef9` → `947f28f`. FACTS.md
gains the queue key as a fact, since "do two moves run at once" is answered
entirely by whether their keys are equal and nothing in the caller says so.

THE MERGE MAP, because the two branches changed one file for the same reason.
`autoRunner.ts`'s `listChannelMeta` returns `{ meta, slugs }` on
channel-priority/s5, with paused channels filtered out of `meta` and every slug
kept in `slugs` — and it DROPS `config` from `ChannelMeta`. This branch adds
`config` precisely so the media guard can pass it to inspectChannelMedia rather
than re-read 68 config.json files on every tick of four lanes and every
three-second status poll. Resolution: keep s5's shape and its paused filter AND
keep `config` on each entry plus the media skip. The two are orthogonal — one
decides which channels are eligible, the other which of those can be reached.
Everything else is keep-both (ChannelsTable ×4, channels/page ×2, the three
records files); jobKinds.ts does not conflict; the remaining shared files touch
disjoint hunks.

Suite: 523/523 in 22.3 min at `947f28f`; common 971/971.

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

Diffstat:
Mplans/FACTS.md | 10+++++++++-
Mplans/STATE.md | 6+++---
Mplans/relocate-channel-media.md | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 75 insertions(+), 11 deletions(-)

diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -3634,7 +3634,15 @@ preview, the action and the job — `:387`, `:491`) refuses blank, relative, ins and a root whose `<slug>` level resolves back into the channel dir, comparing REAL paths and resolving the target separately from the root; without it `root = <transcriptsDir>/channels` makes the source its own target, `rsync -a src/ src/` succeeds, the verify compares the tree -with itself, and the reclaim deletes the only copy. `channels/<slug>/.relocating.json` +with itself, and the reclaim deletes the only copy. **Every relocation in the process runs on ONE queue**, `relocationQueueKey()` = +`"relocate"` (`common/lib/queueKeys.ts`), set by the single enqueue both the Storage panel and +the `/channels` bulk move share (`editor/app/channels/lib/relocationJob.ts`) — because +`registry.ts` caps a queue key at concurrency 1 and caps nothing across keys, so a per-channel +key would start every selected channel's rsync at once onto one destination volume and each +job's start-time space check would be credited room the others had already claimed. +Serializing a move against the channel's OWN jobs is done by refusal, not by the queue: both +actions reject a channel with running or queued jobs before they enqueue. +`channels/<slug>/.relocating.json` (`channelMedia.ts:40`) is the in-flight marker: present means "in transition" to every guard, its `phase` is what lets an interrupted move resume, `deleteChannel` and `renameChannel` refuse while it exists, and `clearRelocationMarker` (`:160`) removes it and nothing else. diff --git a/plans/STATE.md b/plans/STATE.md @@ -160,9 +160,9 @@ and Transcribe coverage, because that page passes the external ids into `buildOp and `addRegistryEntry` folded them on top of `addExternalBands`. Fixed in `d8754d2`. **2026-09-11 — [`relocate-channel-media.md`](relocate-channel-media.md) SHIPPED**, all three -slices, on branch `storage/relocate-media` (`4059dad` → `22a3b35` plus the suite fix, 21 -commits off `61eae05`, **unmerged**; the suite is **522/522 in 23.1 min** at `549dd2e`, and -`common` is 968/968). A channel's `data/` can be an absolute symlink to +slices, on branch `storage/relocate-media` (`4059dad` → `affe525`, 24 commits off `61eae05`, +**unmerged**; the suite is **523/523 in 22.3 min** at `affe525`, and +`common` is 971/971). A channel's `data/` can be an absolute symlink to another drive with `config.dataDir` recording the target, moved by a Storage panel on the channel page or in bulk from `/channels`; four guards plus a `needsMedia` flag on the job kind stand between an unmounted drive and a re-download, and the low-disk gate now measures the diff --git a/plans/relocate-channel-media.md b/plans/relocate-channel-media.md @@ -446,9 +446,9 @@ platter through the link, and the disk gate watches **that** volume for it. ## As shipped (2026-09-11) -Branch `storage/relocate-media`, **`4059dad` → `22a3b35` plus the fix below**, 21 commits off -`61eae05` (the plans commit that opened the branch, 19 of code and tests, and the suite fix), -**unmerged**. All three slices landed on the day they were planned. **No data +Branch `storage/relocate-media`, **`4059dad` → `affe525`** — 24 commits off `61eae05` to that +code tip (the plans commit that opened the branch, 19 of code and tests, the suite fix, the +records, and the two merge-review fixes), plus this record. **Unmerged.** All three slices landed on the day they were planned. **No data moved**: every byte in `transcripts/` is where it was, and the rollout below is still the operator's. @@ -458,7 +458,7 @@ operator's. | 2 — job + UI | `2cf7e37` `edbe6bd` `520a683` `a5dae31` `4c1b849` `1c5ef7b` `0d185a8` `0111a86` | the `relocate-channel-media` kind + `storageActions.ts`, the Storage panel and the badges, the ancestor walk, resume-observes-the-disk, two more `data/` readers, the shardActions bypass, the three docs, `editor/e2e/channel-storage.spec.ts` | | 3 — cold root + bulk | `bb92c00` `4039355` `00c1116` `45172e4` `96d2e25` `22a3b35` | `storage.mediaRoot` + `sanitizeStorage`, the two review rounds, per-row selection on `/channels`, `bulkStorageActions.ts`, the bulk e2e case | -### The two review rounds, and what closed them +### The three review rounds, and what closed them **Round one (`4039355`) — the two ways a move ended by deleting the only copy.** Both were reachable from the shipped panel and both ended in `rm -r` on media nothing else held. @@ -489,6 +489,61 @@ already in `data.incoming`; and three nits (`moveBack`'s swap refusing a `data/` "other", `sweepParked`'s unused `keep`, and the channel page reading the marker twice while telling the operator to delete it by hand above the button that does it). +### Round three (`7722acf`, `affe525`) — the merge review + +Both round-one blockers re-verified closed; two should-fixes landed before this became the +merge candidate. + +**Ten ticked rows started ten rsyncs onto one drive.** The queue key is the only thing that +decides whether two moves run at once — `registry.ts` submits every non-empty key at +concurrency 1 and caps NOTHING across keys — so `channelQueueKey(slug)` serialized a channel +against its own downloads and against no other channel. A bulk move therefore started one +rsync per selected channel, simultaneously, all writing to one destination volume: the worst +access pattern a platter has, and a broken space check, because each job runs its preflight +when it STARTS and jobs that start together are each credited room the others have already +claimed. Recoverable (ENOSPC aborts the copy, the source is untouched until a verify passes) +but hours of copying to learn what one queue slot knew. Both comments asserted the opposite, +and the bulk bar's no-preview-gate argument rests entirely on it. `relocationQueueKey()` takes +no slug and lives in `common/lib/queueKeys.ts` beside `BACKFILL_QUEUE`; the one enqueue both +actions share sets it, so there is no per-caller key to drift. Serializing against the +channel's own jobs is not lost — both actions refuse a channel with running or queued jobs +before they enqueue, which refuses rather than waits. `queueKeys.test.ts` states the claim as a +function of the slug on both sides so it still reads as the claim (and fails) if someone gives +the function a slug parameter; the e2e case asserts it where an operator sees it, two rows in +the `/jobs` table both on `relocate` and neither on `channel:`. **And a channel with nothing to +move is a skip, not a job**: no `data/` is `in-place` to `inspect()` — correctly — so the bulk +path used to queue a job whose only act was to throw from the copy phase, which on a fresh +corpus is most of a page. + +**A spec's settings are the fixture's plus what it names.** The suite fix below defaulted one +key; that was the symptom. `writeSettings` REPLACED `test-settings.json`, so every key in +`fixtures/test-settings.default.json` fell back not to something neutral but to the PRODUCT +defaults — a different fixture, chosen for operators. Four diverge: `minFreeDiskGB` 0 vs 5 GB, +`sleepBetweenDownloadsSeconds` 0 vs 10 s (ten seconds between every download in a fixture +batch), `verifyAvailabilityBeforeClean` false vs true, `syncScheduler.fullSweepIntervalMinutes` +0 vs 1440. Eighteen spec files write settings without naming the floor, fourteen without naming +the sleep. It merges now, one level deep for nested blocks, arrays and every other object +replaced rather than merged (`workers: []` has to mean no workers), explicit winning at every +level. The two other keys the fixture sets are inert: no spec asserts the admin title, and +8388608 IS `TRANSCRIPT_PAGE_DEFAULT_BYTES`. + +### Merging with `channel-priority/s5` + +One semantic conflict, and it is in the file both branches changed for the same reason. +`autoRunner.ts`'s `listChannelMeta` returns `{ meta, slugs }` on s5, with the paused channels +filtered out of `meta` and every slug kept in `slugs` — and it DROPS `config` from +`ChannelMeta`. This branch adds `config` precisely so the media guard can pass it: +`inspectChannelMedia(paths, m.slug, m.config)`, which is what keeps the guard from re-reading +68 `config.json` files on every tick of four lanes and every three-second status poll. +**Resolution: keep s5's return shape and its paused filter, AND keep `config` on each meta +entry plus the media skip in `buildChannelWork`.** The two are orthogonal — one decides which +channels are eligible, the other which of those can be reached. + +Everything else is keep-both: `ChannelsTable.tsx` (four hunks), `channels/page.tsx` (two), +`CHANGELOG.md`, `STATE.md`, `FACTS.md`. `jobKinds.ts` does not conflict. The other files the +two branches share — `backfillReacquire.ts`, `channelConfig.ts`, `settings.ts`, +`channels/actions.ts`, `settings/actions.ts` — touch disjoint hunks. + ### Divergences from the plan - **`needsMedia` is opt-in and absent means false** (`common/jobs/jobKinds.ts:457-462`). The @@ -538,9 +593,10 @@ telling the operator to delete it by hand above the button that does it). ### The suite -**522 passed, 0 failed of 522, 23.1 min** at the fix sha `549dd2e`, one worker behind the -machine-global queue lock, from a worktree with a composed fixture site in `export/public`. -`common` is 968/968 and `tsc --noEmit` is clean in both packages. The +**523 passed, 0 failed of 523, 22.3 min** at the merge-candidate tip `affe525`, one worker +behind the machine-global queue lock, from a worktree with a composed fixture site in +`export/public`. `common` is **971/971** and `tsc --noEmit` is clean in both packages. It was +522/522 at `549dd2e` before the two merge-review fixes, which added the queue-key case. The run before the fix, at `22a3b35`, was **519 passed / 2 failed** — `backfill.spec.ts:457` and `scheduler.spec.ts:29` — and the first run WITH it was 521/1, the one failure being `widget.spec.ts:591`, which wanted the disk gate armed and had been getting it by accident