Archilyzer · Source

archilyzer

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

commit da2945be8a1d46eee108dad789237db4ded722ac
parent 6bfa76f2f05ca01440363deda0edc12bb72d14dc
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  7 Sep 2026 23:40:37 -0400

editor+plans: the arbiter's buttons are asserted gone, and the migration collapses a full kinds list the way arm does

Slice 1.3 review fix-up. Four things, none of them behaviour on a live lane.

THE RETIRED-ROUTES RULE HAD A HOLE THE SIZE OF THE ARBITER. `ArbiterBar` carried
`aria-label="Start the arbiter"` / `"Stop the arbiter"`, and its three tests were
deleted with it — so `grep -rn -i arbiter editor/e2e/` came back EMPTY and
nothing in the suite would have noticed a live Start button over a dispatcher
that never ran a unit in production coming back. The same hole was open on every
DISARM half: only `start digest sweep` / `start backfill sweep` were pinned gone,
so their stop twins and "Stop sweeping" could have survived over actions that no
longer exist. Both halves are asserted `toHaveCount(0)` now, on the board and on
the dashboard.

ONE COLLAPSE RULE, NOT TWO. `armLaneAction` collapsed a scope naming every
operation to `[]` — a leaf naming none draws the union, so it tracks the registry
rather than freezing today's three — and the migration carried `sweepKinds`
verbatim. Two answers to one question, through one leaf builder, which is
exactly what having one builder was supposed to prevent. It is `laneRootFromScope`'s
rule now, keyed on a new optional `available`.

`available` is OPTIONAL because `getSettings` cannot ask the registry:
`lib/operations.ts` imports the controller layer, and pulling that into every
reader of settings.json — the MCP server, the export build, every bin/ script —
to tidy one list would cost far more than the tidying is worth. Absent means no
collapse, which is a no-op on every settings file that exists: the live
`sweepKinds` is `[]`, and empty already meant the union. The numbers diff over
the live corpus is byte-identical to the post-1.3 run.

`LaneDeck`'s backfill card still explained itself in terms of "the corpus-wide
backfill sweep" and "at the default weight the lane is ALREADY idle-only". Both
retired — and the second was about to become misleading in a way that matters:
the yield is the operation's declared `contendsFor` now, which is NARROWER, so a
network-bound attribution run does not stand aside at all.

And the record: the trap about which writers must gain `held` in 1.4 was WRONG.
`armLaneAction` spreads `...policy` and overrides two keys, so it carries `held`
for free; `saveAutoQueueAction` is the one writer that spells every field out and
the one that must be edited. The as-shipped note also now says plainly that
folding the /jobs lane strip onto `deriveLaneState` changes RENDERED TEXT on a
live lane — a held transcription row reads Holding where it read Idle, and this
corpus has `transcriptionsPaused: true` — kept because the strip and the rail
disagreed until now, but not filed as a no-op. Plus what is dead and 1.4 deletes:
`AutoQueueReach`, its sanitizer, `AUTO_QUEUE_REACHES` and `OrderReach.tsx`'s
whole `reach !== null` branch.

common 890 tests (888 + two collapse cases), tsc clean in common and editor,
numbers diff unchanged.

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

Diffstat:
Mcommon/jobs/laneMigration.test.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/laneMigration.ts | 44++++++++++++++++++++++++++++++++++++++++++--
Meditor/app/components/lanes/LaneDeck.tsx | 53+++++++++++++++++++++++++++--------------------------
Meditor/app/jobs/actions.ts | 13++++++-------
Meditor/e2e/auto-queue.spec.ts | 21++++++++++++++++++++-
Meditor/e2e/backfill.spec.ts | 19+++++++++++++++----
Mplans/one-core-phase-1.md | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
7 files changed, 209 insertions(+), 50 deletions(-)

diff --git a/common/jobs/laneMigration.test.ts b/common/jobs/laneMigration.test.ts @@ -215,6 +215,51 @@ test("a settings file with no digest or backfill block at all migrates to the la // construction — both sides call `laneRootFromScope` — and this is what says so, // so that a second leaf builder added later fails here rather than in // production. +test("a scope naming EVERY operation collapses to a leaf naming none", () => { + // The rule that makes a scope track the registry: a leaf naming no operation + // draws the lane's whole union, so "all three ticked" must not freeze today's + // three into the tree and silently exclude the fourth when it is enabled. + const all = ["diarization", "attribution-diarized", "attribution-text"]; + assert.deepEqual( + laneRootFromScope("backfill", { + operations: all, + available: all, + }).children.map((c) => ("match" in c ? c.match : null)), + [{ type: "all" }], + ); + // A STRICT subset still reaches the leaves. + assert.deepEqual( + laneRootFromScope("backfill", { + operations: ["diarization"], + available: all, + }).children.map((c) => ("match" in c ? c.match : null)), + [{ type: "all", operation: "diarization" }], + ); + // And the migration collapses on the same rule, through the same builder — + // which is the point of it being one function rather than two. + const out = migrateSweepsToLanes( + { backfill: { sweepEnabled: true, sweepKinds: all } }, + { backfill: all }, + ).backfill as { root: { children: { match?: unknown }[] } }; + assert.deepEqual( + out.root.children.map((c) => c.match), + [{ type: "all" }], + ); +}); + +test("without an `available` list the migration carries the scope verbatim", () => { + // getSettings cannot ask the registry, so it passes none — and that is a + // no-op on every settings file that exists, because the live `sweepKinds` is + // empty and an empty list already means the union. + const out = migrateSweepsToLanes({ + backfill: { sweepEnabled: true, sweepKinds: ["diarization"] }, + }).backfill as { root: { children: { match?: unknown }[] } }; + assert.deepEqual( + out.root.children.map((c) => c.match), + [{ type: "all", operation: "diarization" }], + ); +}); + test("arming a lane with a scope equals migrating the same scope", () => { const channels = ["teamrcn", "ObviousRises-rumble"]; const armed = laneRootFromScope("digest", { channels }); diff --git a/common/lib/laneMigration.ts b/common/lib/laneMigration.ts @@ -44,6 +44,21 @@ import type { AutoQueueGroup, AutoQueueLeaf, AutoQueueMatch } from "./autoQueueT export type LaneScope = { channels?: readonly string[] | undefined; operations?: readonly string[] | undefined; + // Every operation the lane currently dispatches, when the caller knows it. + // + // NAMING ALL OF THEM IS NOT THE SAME AS NAMING TODAY'S THREE: a leaf that + // names none draws the lane's whole union, so it TRACKS THE REGISTRY and an + // operation enabled later is picked up rather than silently excluded forever. + // So a scope covering everything collapses to no operation at all, and only a + // strict subset reaches the leaves. + // + // Optional because the settings migration's caller (getSettings) cannot ask + // the registry — `lib/operations.ts` imports the controller layer, and pulling + // that into every reader of settings.json to tidy one list would be a much + // larger change than the tidying is worth. Absent means "no collapse", which + // is a no-op on every settings file in existence: the live `sweepKinds` is + // empty, and an empty list already means the union. + available?: readonly string[] | undefined; }; function slugs(value: unknown): string[] { @@ -71,7 +86,10 @@ export function laneRootFromScope( scope: LaneScope, ): AutoQueueGroup { const channels = slugs(scope.channels); - const operations = slugs(scope.operations); + const operations = collapseFullScope( + slugs(scope.operations), + slugs(scope.available), + ); const targets: AutoQueueMatch[] = channels.length > 0 ? channels.map((value) => ({ type: "channel" as const, value })) @@ -100,6 +118,21 @@ export function laneRootFromScope( }; } +// A scope that names every operation the lane has means the same thing as a +// scope that names none — see LaneScope.available. ONE definition, called by the +// migration and by the editor's arm action through this one builder, so the two +// cannot answer it differently. +function collapseFullScope( + operations: readonly string[], + available: readonly string[], +): string[] { + if (operations.length === 0 || available.length === 0) return [...operations]; + const covers = + operations.length >= available.length && + available.every((id) => operations.includes(id)); + return covers ? [] : [...operations]; +} + type RawRecord = Record<string, unknown>; function asRecord(value: unknown): RawRecord { @@ -115,7 +148,13 @@ function asRecord(value: unknown): RawRecord { // Takes the PARSED FILE, not the merged settings object, because "absent from // the file" is the whole trigger and a merged object has already had the // defaults folded in. Returns a new object; the input is never mutated. -export function migrateSweepsToLanes(parsed: unknown): RawRecord { +export function migrateSweepsToLanes( + parsed: unknown, + // The operations each lane dispatches, when the caller can ask the registry. + // See LaneScope.available for why this is optional and why absent is a no-op + // on every settings file that exists. + laneOperations: Partial<Record<"digest" | "backfill", readonly string[]>> = {}, +): RawRecord { const file = asRecord(parsed); const autoQueue = { ...asRecord(file.autoQueue) }; @@ -152,6 +191,7 @@ export function migrateSweepsToLanes(parsed: unknown): RawRecord { // operation draws. So an empty list produces plain channel leaves and a // non-empty one produces a leaf per (channel, operation). operations: slugs(backfill.sweepKinds), + available: laneOperations.backfill, }), }; } diff --git a/editor/app/components/lanes/LaneDeck.tsx b/editor/app/components/lanes/LaneDeck.tsx @@ -15,7 +15,7 @@ import { armLaneAction, disarmLaneAction } from "../../jobs/actions"; // The four lanes, and the ONLY place lane → server-action wiring lives. Rendered // identically by the dashboard band and the monitor widget's controls section, // which is what stops the two surfaces from growing different ideas about what -// a lane can do (the widget could not start or stop a sweep at all, and had no +// a lane can do (the widget could not arm or disarm a lane at all, and had no // digest control whatsoever). // // Every lane's gate reaches the wire as `held`, computed once by isGateHeld; no @@ -82,10 +82,10 @@ export function LaneDeck({ // THE DENOMINATOR IS ELIGIBLE VIDEOS, NOT EVERY VIDEO DIRECTORY. `videos` // counts every directory in the corpus, ~1,700 of which have no transcript or // are marked untranscribable and so can never carry a digest — against that, - // this figure could not reach 100% however long the sweep ran. `eligible` + // this figure could not reach 100% however long the lane ran. `eligible` // comes from the digest operation's own work list (transcribed, not // untranscribable, not waiting on transcription), so it is the same population - // the sweep actually walks. + // the lane actually walks. // // It falls back to `videos` while null, which is the state until every // channel's snapshot has been regenerated — see the sync payload. @@ -213,12 +213,12 @@ export function LaneDeck({ // // Sweep on/off — is there a corpus-wide backfill at all. Persisted, so a // server restart resumes it (editor/instrumentation.ts). - // Pause/resume — hold a running sweep at zero throughput without ending it. + // Pause/resume — hold a running lane at zero throughput without ending it. // The batch's limit() returns 0, which makes the pool idle-WAIT rather // than finish, so resuming costs nothing and re-derives nothing. // - // Stopping the sweep drains rather than cancels: the channel in flight - // finishes instead of losing a part-generated video. + // Stopping the lane drains rather than cancels: the video in flight finishes + // instead of being thrown away part-generated. const digestArmed = digest?.armed ?? false; const digestPaused = digest?.held ?? false; const digestControls: LaneControl[] = [ @@ -283,7 +283,7 @@ export function LaneDeck({ <> {/* The coverage percentage stays on the FIGURE rather than being restated here — a card that says a thing twice is the accessory - to remove. What the detail adds is the reach (a sweep can be a + to remove. What the detail adds is the reach (a lane can be a third of the way through the corpus and have touched every channel, or the reverse) and the two reasons a video is not in the numerator at all. */} @@ -316,17 +316,19 @@ export function LaneDeck({ ); // ── Backfill ────────────────────────────────────────────────────────────── - // Arm / disarm the corpus-wide backfill sweep, and hold it without ending it. + // Arm / disarm the backfill lane, and hold it without switching it off. // - // TWO CONTROLS, AND THIS REVISED AN EARLIER DECISION. The backfill lane used - // to carry only the sweep, on the reasoning that at the default weight the - // lane is ALREADY idle-only — standing aside whenever transcription works — so - // a manual pause looked redundant. + // TWO CONTROLS, AND THIS REVISED AN EARLIER DECISION. The lane used to carry + // only the arm switch, on the reasoning that it stands aside whenever + // transcription works anyway — so a manual pause looked redundant. // - // That covered the wrong hazard. Idle-only yielding handles "get out of the - // transcription lane's way"; it does nothing for "this is a desktop someone is - // sitting at, and diarization pins four cores for hours." Backfill work is - // CPU-bound and long — a diarization pass over the corpus runs for days — so + // That covered the wrong hazard, and the yield is narrower than it looked: it + // applies to a run that would CONTEND for the GPU (the operation's declared + // `contendsFor`, since slice 1.3), so a network-bound attribution run does not + // stand aside at all. Even where it does, standing aside handles "get out of + // the transcription lane's way"; it does nothing for "this is a desktop + // someone is sitting at, and diarization pins four cores for hours." Backfill + // work is CPU-bound and long — a diarization pass over the corpus runs for days — so // the operator needs a hold for reasons the scheduler cannot see. // // The pause writes THE SAME FIELD the Settings checkbox writes @@ -335,15 +337,14 @@ export function LaneDeck({ // backfill.spec asserts the settings field through this button's label rather // than just watching the label flip. // - // Stopping the SWEEP, by contrast, drains rather than cancels: the video in - // flight finishes instead of being thrown away, and the stop reaches the - // per-channel job the sweep is waiting on rather than meaning "after this - // channel". Pause when you want it back; stop when you don't. + // Stopping the LANE, by contrast, drains rather than cancels: the video in + // flight finishes instead of being thrown away. Pause when you want it back; + // stop when you don't. // - // THE TWO NOW USE DIFFERENT VERBS, which is the cheapest fix for the hazard - // this comment has been describing in prose. "Start sweep" and "Pause - // Backfill" are the same shape of phrase for two acts whose costs to undo - // differ by a week of GPU time. The feed SWEEPS; the gate HOLDS. The + // THE TWO USE DIFFERENT VERBS, which is the cheapest fix for the hazard this + // comment has been describing in prose. "Run the lane" and "Pause Backfill" + // would otherwise be the same shape of phrase for two acts whose costs to undo + // differ by a week of GPU time. The switch RUNS; the gate HOLDS. The // aria-labels are untouched — they are internal addressing that confuses // nobody, and the e2e suite finds these buttons by them. const backfillArmed = backfill?.armed ?? false; @@ -456,14 +457,14 @@ export function LaneDeck({ ) : undefined } // TWO NOTES, NOT ONE SENTENCE. "Here is what this lane holds" and "the - // sweep is armed while the lane is off" are different conditions, and + // lane is armed but held" are different conditions, and // folding them together loses the one that is a problem. note={ <> {backfillAvailable && laneOperations.length > 1 && ( // THE ONE PLACE THE LANE IS STILL NAMED AS A LANE, and it earns it: // that these operations share a queue and a pause is a true, - // load-bearing fact — the sweep and the pause are two controls, and + // load-bearing fact — the arm switch and the pause are two controls, and // conflating them is how an operator loses a week of GPU time. So // it is said once, here, where both controls are, and the members // are LISTED rather than hidden behind the queue key. Everywhere an diff --git a/editor/app/jobs/actions.ts b/editor/app/jobs/actions.ts @@ -196,12 +196,6 @@ export async function armLaneAction( `A lane armed on it would run forever without doing anything.`, }; } - // TICKING EVERY OPERATION IS NOT THE SAME AS NAMING TODAY'S THREE. A leaf - // that names none draws the lane's whole union, so it tracks the registry: - // an operation enabled later is picked up rather than silently excluded - // forever. Only a STRICT SUBSET goes onto the leaves. - const scoped = - operations.length > 0 && operations.length < known.size ? operations : []; const policy = settings.autoQueue[lane]; await writeSettings({ ...settings, @@ -210,10 +204,15 @@ export async function armLaneAction( [lane]: { ...policy, enabled: true, + // TICKING EVERY OPERATION IS NOT THE SAME AS NAMING TODAY'S THREE, + // and `available` is what says so — the collapse lives in the leaf + // builder, with the settings migration, rather than being written + // once here and once there. root: scope ? laneRootFromScope(lane, { channels: scope.channels, - operations: scoped, + operations, + available: [...known], }) : policy.root, }, diff --git a/editor/e2e/auto-queue.spec.ts b/editor/e2e/auto-queue.spec.ts @@ -1213,6 +1213,17 @@ test("UI: each operation page carries its own lane, and the rail as context", as const digest = page.locator('section[data-lane="digest"]'); await awaitHydration(digest); await expect(page.locator("section[data-sweep-lane]")).toHaveCount(0); + // THE ARBITER'S BUTTONS, ASSERTED GONE. Its three tests were deleted with it, + // so without this nothing in the suite would notice `ArbiterBar` coming back — + // and it was a live Start button over a dispatcher that never ran a unit in + // production. The board is where it sat, so the board is where it is missed. + await page.goto("/operations"); + await awaitHydration(page.locator('[data-board="operations"]')); + await expect( + page.getByRole("button", { name: /the arbiter/i }), + ).toHaveCount(0); + await page.goto("/operations/digest"); + await awaitHydration(digest); await expect( digest.getByRole("heading", { name: "Auto-digest" }), ).toBeVisible(); @@ -1224,10 +1235,18 @@ test("UI: each operation page carries its own lane, and the rail as context", as await expect( digest.getByRole("button", { name: "Pause Digests" }), ).toBeVisible(); - // The sweep's arm button is gone from this page and from everywhere else. + // The sweep's own controls are gone from this page and from everywhere else — + // BOTH halves. Asserting only the arm half would let a disarm button survive + // over an action that no longer exists. await expect( page.getByRole("button", { name: "Start Digest sweep" }), ).toHaveCount(0); + await expect( + page.getByRole("button", { name: "Stop Digest sweep" }), + ).toHaveCount(0); + await expect(page.getByRole("button", { name: /Stop sweeping/i })).toHaveCount( + 0, + ); }); test("the digest lane offers Shortest first, and has no Reach axis", async ({ diff --git a/editor/e2e/backfill.spec.ts b/editor/e2e/backfill.spec.ts @@ -425,10 +425,21 @@ test("the lane arms from the dashboard and disarms without losing its rules", as ) .toEqual({ enabled: false, rules: 1 }); - // The sweep's own button is gone from this surface, not merely relabelled. - await expect( - page.getByRole("button", { name: "start backfill sweep" }), - ).toHaveCount(0); + // The sweep's own controls are gone from this surface, not merely relabelled + // — BOTH halves, and both aria-labels, because the deck drew whichever half + // matched the state and a test that only names the arm half would miss a + // disarm button left over an action that no longer exists. + for (const name of [ + "start backfill sweep", + "stop backfill sweep", + "start digest sweep", + "stop digest sweep", + ]) { + await expect(page.getByRole("button", { name })).toHaveCount(0); + } + await expect(page.getByRole("button", { name: /Stop sweeping/i })).toHaveCount( + 0, + ); }); // (8c) THE SCOPE IS SET FROM A SCREEN, AND IT IS A TREE. diff --git a/plans/one-core-phase-1.md b/plans/one-core-phase-1.md @@ -910,11 +910,16 @@ would make every later slice's evidence unreadable. - **`buildActiveJobs.buildLanes` takes no argument now.** It counted in-flight work on the sweep lanes by scanning the job rows for their queue keys; four runner rows read `getAutoRunnerStatus(lane).inFlight`, which is in-memory. - **A held transcription or download lane reads `Holding` on the /jobs strip - now**, where the two runner rows could only say running / idle / unavailable — - the strip and the operations rail (which has derived it that way since slice 7) - finally agree. One fold, one answer; that is what folding the two halves - together buys. + + **THIS CHANGES RENDERED TEXT ON A LIVE LANE, and the plan did not ask for it.** + The four rows now fold `gateHeld` through `deriveLaneState`, so a held + transcription or download row on the /jobs strip reads **Holding** where it + read *Idle* — and the live `settings.json` has `transcriptionsPaused: true`, so + this is what the operator will actually see. It is kept rather than reverted + because the strip and the operations rail (which has derived it that way since + slice 7) disagreed about the same lane until now, and one fold cannot answer + twice. Said plainly here rather than filed as a no-op, because "no rendered + number moved" is a claim this slice otherwise makes and this is the exception. - **The sweep panel's surviving prose moved rather than died.** The band, the "this operation is switched off" warning and the "shares one lane, one runner and one pause with …" sentence are a `LaneOperationContext` **div** above the @@ -984,6 +989,41 @@ first spec — the exact failure `plans/FACTS.md` documents for the primary checkout, arriving by a route that entry did not cover. The two commits were rewritten to drop it and FACTS.md now says so. Add by path in this repo. +### The fix-up, after review + +The commit carrying this section, and so unable to name its own sha — the same +knot the 1.2 note hit. Four things, none of them behaviour on a live lane: + +- **The arbiter's buttons are asserted GONE.** `ArbiterBar` carried + `aria-label="Start the arbiter"` / `"Stop the arbiter"` and its three tests + were deleted with it, so `grep -rn -i arbiter editor/e2e/` was EMPTY — nothing + would have noticed a live Start button over a dispatcher that never ran a unit + coming back. Same hole on the DISARM halves: only `start … sweep` was pinned + gone, so `stop digest sweep`, `stop backfill sweep` and *Stop sweeping* could + have survived over actions that no longer exist. Both halves, both surfaces, + now `toHaveCount(0)`. +- **The full-scope collapse moved into `laneRootFromScope`.** `armLaneAction` + collapsed a scope naming every operation to `[]` (so the leaf tracks the + registry) and the migration did not, which is two answers to one question + through one builder. It is one rule now, keyed on a new optional + `available` — passed by the arm action, absent from `getSettings`, which cannot + ask the registry without pulling the controller layer into every reader of + settings.json. Absent is a no-op on every settings file that exists: the live + `sweepKinds` is `[]`, and empty already meant the union. **Numbers diff + unchanged.** +- `LaneDeck`'s backfill card still explained itself in terms of "the corpus-wide + backfill sweep" and "at the default weight the lane is ALREADY idle-only". + Both retired; the yield is the operation's declared `contendsFor`, which is + NARROWER than the old sentence claimed — a network-bound attribution run does + not stand aside at all. +- The trap below about which writers must gain `held` was wrong. Corrected. + +**Dead after this slice, and 1.4 deletes them:** `AutoQueueReach`, +`sanitizeAutoQueueReach` and `AUTO_QUEUE_REACHES` in +`jobs/autoQueuePolicy.ts`/`lib/autoQueueTypes.ts` have no reader left, and +`OrderReach.tsx`'s whole `reach !== null` branch (the select, `REACH_LABEL` and +two of its four sentences) is unreachable — every caller passes `null`. + ### Traps for the 1.4 implementer - **Every GATE read of the four legacy fields is inside `lib/pauseGates.ts`** — @@ -1004,11 +1044,15 @@ rewritten to drop it and FACTS.md now says so. Add by path in this repo. `backfill.spec.ts` (`backfill.enabled`, three tests, via its `laneEnabled` helper) and `widget.spec.ts`. - **`held` should land on `AutoQueuePolicy`, beside `enabled` and `snoozeUntil`, - and `sanitizePolicy` is the one place to default it.** `armLaneAction` and - `saveAutoQueueAction` both spell every policy field EXPLICITLY when they write - — the note on `saveAutoQueueAction` says why — so both must gain the key or a - save will silently unhold a lane. That is two call sites, and they are the whole - list. + and `sanitizePolicy` is the one place to default it.** **ONE writer has to gain + the key: `saveAutoQueueAction`** (`editor/app/operations/actions.ts:~47`), + which spells every policy field EXPLICITLY — its own comment says why, and that + is exactly the hazard: a field it forgets is dropped on every save. Its + neighbours are safe by construction — `armLaneAction` and `disarmLaneAction` + (`editor/app/jobs/actions.ts:~203`) SPREAD `...policy` and override only + `enabled` and `root`, so they carry `held` for free — and so does + `snoozeAutoQueueAction`. An earlier draft of this note said all of them spell + the fields out; it was wrong, and the difference is the whole trap. - **The numbers script already prints `held <lane>` for all four lanes** through `isGateHeld`, so 1.4's before/after diff over the live corpus is a direct test of the fallback: it must stay `true/false/false/false` while the file carries