Archilyzer · Source

archilyzer

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

commit e65a37bbe606ea0ba994d168ea77c3b259a5946e
parent 7ae37867a19834c79055cd8c6c6dac8e6885673c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 30 Sep 2026 09:26:05 -0400

plans: slice DT after review — the parent's rulings (the half-interval floor stays; a lowered cap is reached as calls return; doctor's line is slice SG's); the review's findings to their commits; the cap at most 8 in the record and FACTS; the re-gates

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

Diffstat:
Mplans/FACTS.md | 7++++---
Mplans/release-15.md | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
2 files changed, 57 insertions(+), 25 deletions(-)

diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -7682,7 +7682,8 @@ shipped". Anchors are at slice DT's tip (`r15/drive-timings`). detect it reliably: the root's inode is in the kernel's cache whenever the drive was used lately. - **The timings are settings: `settings.storage.health`** (slice DT). `budgetMs` (default 3000, 500–60000), `passIntervalMs` (15000, 5000–300000), `probeTimeoutMs` (3000, 500–30000), - `clearAfterCleanPasses` (2, 1–10), `inFlightPerLocation` (4, 1–16); the defaults, ranges, sanitizer + `clearAfterCleanPasses` (2, 1–10), `inFlightPerLocation` (4, 1–8: at most half the editor's 16 + file-access threads, review M1); the defaults, ranges, sanitizer and words are `common/lib/storageHealthTimings.ts` (pure; the /storage form imports it). A read clamps into the range and keeps only a value that differs from its default (an untuned file has no `health` key); the /storage form refuses out of range with a sentence. Every number is read through @@ -7690,7 +7691,7 @@ shipped". Anchors are at slice DT's tip (`r15/drive-timings`). `globalThis` — no file read per call. `applyHealthTimings(stored)` (`:193`) sets them: the health pass on every pass (from the settings it reads), /storage's save at once (`saveHealthTimingsAction`), and the `index` and `build stats` bins once at start. Any other process - with no pass (a CLI, `archilyzer doctor`) runs on the defaults. A changed interval is told to + with no pass (a CLI, `archilyzer doctor` until slice SG adds its line) runs on the defaults. A changed interval is told to `onPassIntervalChange` (`:214`) subscribers, which re-arms the armed pass's timer; a raised cap admits waiting calls, a lowered one is reached as calls return. `resetStorageHealth` keeps the timings (configuration, not health); `setDriveCallBudget` is a test seam below the 500 ms floor and @@ -7706,7 +7707,7 @@ shipped". Anchors are at slice DT's tip (`r15/drive-timings`). `lib/storageVolumes.ts:607`). The root's device from the last pass (findmnt `-J -T <root> -o SOURCE,UUID`, raced against `probeTimeoutMs` (3 s), only when there is none or its `/sys` entry stops reading; another volume's UUID names none; `[…]` stripped, `/dev/mapper` resolved, basename); then - `/sys/class/block/<dev>/stat` (`parseBlockStat`, `storageHealth.ts:826`): completed = fields 1 + 5 + `/sys/class/block/<dev>/stat` (`parseBlockStat`, `storageHealth.ts:831`): completed = fields 1 + 5 + 12 + 16 (reads, writes, discards, flushes), in flight = field 9. Stalled ⇔ in flight at both samples AND nothing completed between; samples at least `minCounterIntervalMs()` apart (`storageVolumes.ts:472`: min(10 s, interval − 5 s), floored at half the interval — 10 s at the diff --git a/plans/release-15.md b/plans/release-15.md @@ -1112,7 +1112,7 @@ location (`DRIVE_CALLS_IN_FLIGHT`, 4). The constants are gone; tsc named every r | `passIntervalMs` | 15000 | 5000–300000 | at once on a save from `/storage` (the armed pass re-arms its timer); a hand edit, at the next pass | | `probeTimeoutMs` | 3000 | 500–30000 | the next pass or Refresh (child `stat` and findmnt) | | `clearAfterCleanPasses` | 2 | 1–10 | the next answer | - | `inFlightPerLocation` | 4 | 1–16 | the next slot taken; a raised cap admits waiting calls at once, a lowered one is reached as calls return | + | `inFlightPerLocation` | 4 | 1–8 (review M1: at most half the editor's 16 file-access threads) | the next slot taken; a raised cap admits waiting calls at once, a lowered one is reached as calls return | - The type, defaults, ranges, sanitizer, words and `SETTINGS.md` docs are one pure module, `common/lib/storageHealthTimings.ts` (the `/storage` form, a client file, imports it). @@ -1177,18 +1177,23 @@ location (`DRIVE_CALLS_IN_FLIGHT`, 4). The constants are gone; tsc named every r | `41ad2f65` | `editor:` the Drive health timing form, its action and parse (+ unit test), the page; the Refresh note, the stalled line, the media notice, the volume chip's title; the comments. | | `b92df1fe` | `editor(e2e):` `storage-locations.spec.ts`: the drive health timing case. | | `4372abaf` | `common:` the cap's hint says to keep it well under the editor's 16 file-access threads. | -| this commit | `plans:` this section and the slices row; FACTS; the changelog. | +| `fd2b8d63` | `plans:` the first version of this section and the slices row; FACTS; the changelog. | +| `85c2407b` | `common:` review M1: `inFlightPerLocation` at most 8; the hint, the docs string, `SETTINGS.md`, the test values (the form's too). | +| `6a24ac4e` | `common:` review L4: a timeout on no known location names the budget the call ran against. Test. | +| `e7a525f3` | `editor:` review L2: a storage patch of the locations keeps `storage.health` (a `saveSettings` merge case). | +| this commit | `plans:` the rulings and the review in this section; FACTS; the report. | **Tests** (unit; no test stalls a real drive) | File | What it pins | |---|---| -| `lib/storageHealthTimings.test.ts` (7, new) | The defaults are the constants they replace, each in its range. Absent, empty, an array, a string, a number: every default, and no block (`getSettings()` with no file, `defaultSiteSettings()`). A read clamps (200 → 500, 900000 → 300000), rounds (7.6 → 8), drops a string and an unknown key, and drops a value equal to its default (also one that rounds onto it). **The settings.json round trip** through `writeSettings`/`getSettings`: `{budgetMs: 4000, clearAfterCleanPasses: 2}` is written as `{budgetMs: 4000}` and read back; a save of the default removes the key; a hand-edited 99 reads as 16. The block survives the mediaRoot migration. The spacing (15 s → 10 s, 300 s → 10 s, 12 s → 7 s, 10 s → 5 s, 8 s → 4 s, 5 s → 2.5 s). The words. | -| `lib/storageHealth.test.ts` (+6) | **The accessor feeds the watchdog:** a stored `budgetMs: 200` applies as 500 ms (the floor), and a 700 ms unit is refused and marks the location ("within 0.5 s"); on the defaults the same unit answers. **The cap:** with `inFlightPerLocation: 2` the third call waits, and runs when a slot frees. A cap raised from 1 to 3 admits the two waiting calls at once; lowered to 1 with three in flight, two returns bring it to one and the fourth call still waits, and runs on the third return. **The clear count:** with 3, two clean answers do not clear and the third does; with 1, one does. A changed interval is told to the subscribers once, an unchanged one and other keys are not, and an unsubscribed one hears nothing. The test seam's budget wins, and a reset keeps the timings. The existing constants' assertions read `HEALTH_TIMING_DEFAULTS`. | +| `lib/storageHealthTimings.test.ts` (7, new) | The defaults are the constants they replace, each in its range. Absent, empty, an array, a string, a number: every default, and no block (`getSettings()` with no file, `defaultSiteSettings()`). A read clamps (200 → 500, 900000 → 300000), rounds (7.6 → 8), drops a string and an unknown key, and drops a value equal to its default (also one that rounds onto it). **The settings.json round trip** through `writeSettings`/`getSettings`: `{budgetMs: 4000, clearAfterCleanPasses: 2}` is written as `{budgetMs: 4000}` and read back; a save of the default removes the key; a hand-edited 99 reads as 8 (16 before review M1). The block survives the mediaRoot migration. The spacing (15 s → 10 s, 300 s → 10 s, 12 s → 7 s, 10 s → 5 s, 8 s → 4 s, 5 s → 2.5 s). The words. | +| `lib/storageHealth.test.ts` (+7) | **The accessor feeds the watchdog:** a stored `budgetMs: 200` applies as 500 ms (the floor), and a 700 ms unit is refused and marks the location ("within 0.5 s"); on the defaults the same unit answers. **The cap:** with `inFlightPerLocation: 2` the third call waits, and runs when a slot frees. A cap raised from 1 to 3 admits the two waiting calls at once; lowered to 1 with three in flight, two returns bring it to one and the fourth call still waits, and runs on the third return. **The clear count:** with 3, two clean answers do not clear and the third does; with 1, one does. A changed interval is told to the subscribers once, an unchanged one and other keys are not, and an unsubscribed one hears nothing. The test seam's budget wins, and a reset keeps the timings. The existing constants' assertions read `HEALTH_TIMING_DEFAULTS`. After review L4: a call on a hand-typed root whose budget changes while it is out (80 ms, then 5 s) is refused naming 0.08 s (the pre-fix code named 5 s). | | `lib/storageHealthCounters.test.ts` (+1) | The spacing follows the applied interval: at 8 s, samples 3,999 ms apart give no verdict and 4,000 ms apart compare (stalled); at 300 s it is 10 s. The existing case reads `minCounterIntervalMs()` (10 s). | | `lib/storageHealthProbe.test.ts` (+1) | With `probeTimeoutMs: 500` applied and no `timeoutMs` passed, a child that sleeps 20 s is `stalled` after 0.5–2.5 s. | | `controller/storageWatch.test.ts` (+2) | **Every pass applies what it reads:** `clearAfterCleanPasses: 3` and `budgetMs: 5000` in the settings are in force after the first pass; its stall line says "until it answers 3 times in a row"; two clean passes do not clear and the third does; a pass handed its locations reads no settings and leaves the timings. **The re-arm:** the armed watch, told `passIntervalMs: 5000` (the save's apply), logs the re-arm and runs its second pass 4.5–7 s later (15 s at the default); a stopped watch re-arms nothing; one armed with an explicit interval does not follow. | -| `editor/app/storage/lib/healthTimingsForm.test.ts` (5, new) | One field per key, in order, with accessible names none of which contains another. Empty and blank fields write nothing. A value in range is kept and trimmed; one equal to its default is not written. Out of range is refused with the field, range and value, for a millisecond field and both counts. "3.5", "-1", "3e3", "abc", "0x10" and "two" are refused as not whole numbers. | +| `editor/app/storage/lib/healthTimingsForm.test.ts` (5, new) | One field per key, in order, with accessible names none of which contains another. Empty and blank fields write nothing. A value in range is kept and trimmed; one equal to its default is not written. Out of range is refused with the field, range and value, for a millisecond field and both counts (the cap: 8 kept, 9 refused, after review M1). "3.5", "-1", "3e3", "abc", "0x10" and "two" are refused as not whole numbers. | +| `editor/app/settings/saveSettings.test.ts` (+1, review L2) | A storage patch of `{ locations, defaultLocationId }` (what the /storage location actions write) keeps `storage.health` and `savedVideosLocationId`. | **e2e** (`storage-locations.spec.ts`, new case "the drive health timing saves to settings.json and reads back"): after hydration, the block is collapsed and says "defaults"; opened, `read budget` is @@ -1201,19 +1206,20 @@ refused with the sentence and the file is unchanged; emptied and saved, "A read #### Gates (logs `$T/dt-*.log`) - **tsc** (all workspaces): clean before every commit — 58 s on the tree of the first three code - commits, 51 s after the hint's. + commits, 51 s after the hint's; after the review, 44 s (M1) and 58 s (L4, L2). - **Unit:** | Suite | Result | |---|---| - | common | **2,318/2,318**, 50 s (`main`'s 2,301 + 17) | - | editor unit | **100/100** (95 + 5) | + | common | **2,318/2,318**, 50 s (`main`'s 2,301 + 17); after the review **2,319/2,319**, 50 s (+1, L4) | + | editor unit | **100/100** (95 + 5); after the review **101/101** (+1, L2) | | `test:scripts` | 194 passed, 2 skipped (196), as at DS | | mcp | not run: no mcp file and nothing it imports changed | -- **Docs:** `settings example --check`, `docs env --check` and `docs files --check` all exit **0** - (`SETTINGS.md` regenerated in `13411edc`: the `health` row and the `storage.health` table; - `settings.json.example` unchanged, the default block has no `health`). +- **Docs:** `settings example --check`, `docs env --check` and `docs files --check` all exit **0**, + before and after the review (`SETTINGS.md` regenerated in `13411edc`: the `health` row and the + `storage.health` table; and in `85c2407b` for the cap's range; `settings.json.example` unchanged, + the default block has no `health`). - **Build:** the editor's `next build`, with the primary's `transcripts/` linked in (`ln -sT`) and capped at 5 GB with no swap: **34 s, max RSS 1,648,128 KB**, exit 0. The link was removed after the build, and nothing ran through it. @@ -1226,20 +1232,20 @@ refused with the sentence and the file is unchanged; emptied and saved, "A read | 2 | `4372abaf` (the code as shipped) | **39 passed, 0 failed, 12 skipped, 2.5 min** | DS's 38 plus the new case. The 12 skips are `channels-rack-audit`, which needs `E2E_RACK_SHOTS`. - Neither run waited in the queue. + Neither run waited in the queue. Not re-run after the review, as the parent directed: the fixes + change a range, a message and a unit test, and the review's own run of `storage-locations` at + `fd2b8d63` passed 9/9. - **Numbers tool:** none. #### Found and left - **Other CLI processes run on the defaults.** The `index` and `build stats` bins apply the settings; - `archilyzer doctor` (`common/bin/doctor.ts`, another slice's file) and any other command whose - inspects go through `onDrive` race them against the default 3 s. A one-line - `applyHealthTimings(settingsFromFile(paths.settingsFile).storage.health)` at its start is all it - needs. -- **`inFlightPerLocation` may be set to 16, the editor's whole thread pool** (`UV_THREADPOOL_SIZE`, - 16 in `start`). At 16, one drive that stops answering can hold every file-access thread, which is - what DS exists to prevent; two drives at 8 can too. The range is the ruling's; the form's hint and - `SETTINGS.md` say to keep it well under 16. + `archilyzer doctor` and any other command whose inspects go through `onDrive` race them against the + default 3 s. **Doctor's line belongs to slice SG**, which owns `common/bin/doctor.ts` (ruled below): + `applyHealthTimings(settingsFromFile(paths.settingsFile).storage.health)` at its start. +- **Two drives at the cap's maximum can still hold every thread.** Review M1 lowered the maximum to + 8, half of `UV_THREADPOOL_SIZE` (16), so one drive that stops answering cannot hold them all; two + such drives at 8 each can, as two at 4 hold half. The cap is per location, not per process. - **A hand edit of `passIntervalMs`** re-arms at the next pass, so it can wait up to the old interval (at most 5 minutes). A save from `/storage` re-arms at once. - **A refused save clears the typed value.** React resets a form after its action returns, so the @@ -1251,13 +1257,14 @@ refused with the sentence and the file is unchanged; emptied and saved, "A read | What I assumed | The alternative | |---|---| -| The counters' spacing is the ruling's min(10 s, interval − 5 s), **floored at half the interval**. Without the floor it is 0 at the 5 s minimum interval (1 s at 6 s), so a Refresh just after a pass would compare two samples milliseconds apart, the case DS's review kept the spacing for. From 10 s up the two agree. | The formula as ruled, 0 at 5 s. Or a higher minimum interval (10 s) | +| The counters' spacing is the ruling's min(10 s, interval − 5 s), **floored at half the interval**. Without the floor it is 0 at the 5 s minimum interval (1 s at 6 s), so a Refresh just after a pass would compare two samples milliseconds apart, the case DS's review kept the spacing for. From 10 s up the two agree. **Ruled at review: the floor stays.** | The formula as ruled, 0 at 5 s. Or a higher minimum interval (10 s) | | A read clamps an out-of-range value (the schema's convention); the `/storage` form refuses it with a sentence and writes nothing. | The form clamps too, and says what it stored | | Only a value that differs from its default is written, so a save of 3000 for the budget writes nothing, and a later release's new default reaches it. | Write what the operator saved, pinning the default of the day | | The slice's test as specified (`budgetMs: 200` → a 300 ms unit refused) is below the ruled 500 ms floor, so the test stores 200, shows it clamped to 500, and refuses a 700 ms unit; the default passes the same unit. | Lower the floor so 200 applies | | The timings are applied into the health state (the pass on every pass, the save at once, the two bins), not read from `settings.json` on each call: lib has no settings memo, and `onDrive` is on every page's hot path. | A time-limited memo of `getSettings()` inside the accessor (a file read at most every few seconds, and lib/storageHealth.ts no longer free of I/O) | | A save re-arms the pass's timer at once, through a subscription on `globalThis`. | Leave the running timer; the new interval at the next restart | -| A lowered cap is reached as calls return; calls already in flight are not refused. A raised cap admits waiting calls at once. | Refuse the calls over the new cap | +| A lowered cap is reached as calls return; calls already in flight are not refused. A raised cap admits waiting calls at once. **Ruled at review: it stays.** | Refuse the calls over the new cap | +| **After review M1:** the cap's maximum is 8, half the editor's 16 file-access threads (the ruling said 16). | 16, with a warning on the form and in `SETTINGS.md` (as first shipped) | | The form's inputs are text with a numeric keypad, so the action's sentence is the only validation. | `type="number"` with `min`/`max`: the browser's own bubble, and "3.5" blocked before the action | | `resetStorageHealth` (a test seam and the e2e `invalidate-cache` route) keeps the applied timings. | Reset them to the defaults until the next pass | @@ -1267,6 +1274,30 @@ is rebuilt and restarted. Nothing is written until the operator saves the form; the editor runs on today's numbers. A CLI `archilyzer index` or `build stats` reads the settings itself. +#### Rulings (parent, 2026-09-30) + +| Question | Ruling | Where | +|---|---|---| +| The counters' spacing: the ruled min(10 s, interval − 5 s) is 0 at a 5 s interval | The half-interval floor stays | as built (`13411edc`) | +| A lowered cap: refuse the calls in flight over it, or reach it as they return | Reached as calls return; nothing in flight is refused | as built (`13411edc`) | +| `archilyzer doctor` runs on the default timings | Its one `applyHealthTimings` line goes to slice SG, which owns `common/bin/doctor.ts` | "Found and left" | + +#### Review + +**Verdict: SHIP AFTER FIXES** (`dt-review.md` in the job's scratch). No High. The review re-ran every +gate (tsc, common 2,318, editor unit 100, the three docs checks, `storage-locations` 9/9, a clean +`merge-tree` against `main`), checked the writer paths, the migration, the accessor's `globalThis` +home, the re-arm, the live cap's arithmetic and 20 FACTS anchors, and found no accessible-name +collision in the three specs that open `/storage`. + +| Finding | Ruling | Where | +|---|---|---| +| M1: `inFlightPerLocation` could be 16, the editor's whole thread pool, so one drive that stops answering could hold every thread; the hint only warned | The maximum is 8; the hint, the docs string, `SETTINGS.md`, the test values and these records | `85c2407b`, this commit | +| L1: the parent's rulings were not in the record | The block above; the doctor bullet names SG | this commit | +| L2: no test pinned that a location write keeps `storage.health` | One `mergeSettingsPatch` case | `e7a525f3` | +| L3: a refused save clears the typed value (React's form reset) | As recorded in "Found and left" | as built | +| L4: the no-location timeout's words read the budget at throw time, not the one the call ran against | The detail carries `secondsText(budget)`; a test that changes the budget mid-call | `6a24ac4e` | + ## Rollout Release 15 is slices IG (`r15/index-hold`, merged `ccf90892`), UT (`r15/umtool-trace`, `07c991be`),