Archilyzer · Source

archilyzer

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

commit 6bcce6ed122ec024bab6ba93b1e857a68fa6f2c0
parent 9fdd996f96979e768ba3761405c68e9e6045585e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  7 Sep 2026 22:14:15 -0400

common: the sweeps' scope becomes a lane's tree, before anything is deleted

Slice 1.3 of plans/one-core-phase-1.md, first commit: the pure function that
carries ten retired settings fields onto the two lane trees, with its tests, and
nothing removed yet.

`migrateSweepsToLanes` runs on READ rather than as a one-shot script, because
the editor is not the only reader of settings.json — bin/ scripts, the MCP
server and the export build all call getSettings, and a migration that ran when
someone opened a page would give two readers two different answers about what is
armed. It applies only when `autoQueue.digest` / `.backfill` is ABSENT from the
file, which is what makes it idempotent by construction instead of by a version
marker nobody would maintain.

It never enables a lane the flag did not. That is the headline assertion of the
whole slice and it has its own test: the backfill lane as configured on this
corpus would re-fetch audio for ~66,540 videos, and an off-by-one in a migration
is not an acceptable way to find that out.

An empty channel list is ONE `{type:"all"}` leaf, not zero leaves and not a leaf
per channel, because that is what an empty `sweepChannels` meant:
`startDigestSweep` passes `scope.length > 0 ? scope : undefined` and an absent
scope is the whole corpus. A leaf per channel would have been the same thing
today and a different thing the day a channel is added.

`laneRootFromScope` is exported because the editor's arm action will call it
too: "arming a lane from the console produces the tree the migration would have
produced" is a property worth having, and the cheapest way to have it is for
there to be one leaf builder. A test asserts the equality so that a second
builder added later fails here rather than in production.

The module is in lib/ and its test is in jobs/ — architecture.test.ts forbids
lib/ importing jobs/, and the migration has to be asserted THROUGH
`sanitizeAutoQueue`, since it deliberately produces raw nodes for the sanitizer
to normalize rather than being a second implementation of it.

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

Diffstat:
Acommon/jobs/laneMigration.test.ts | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/laneMigration.ts | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 411 insertions(+), 0 deletions(-)

diff --git a/common/jobs/laneMigration.test.ts b/common/jobs/laneMigration.test.ts @@ -0,0 +1,251 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { laneRootFromScope, migrateSweepsToLanes } from "../lib/laneMigration"; +import { sanitizeAutoQueue } from "./autoQueuePolicy"; + +// The migration is asserted THROUGH THE SANITIZER, because that is how it is +// used: it produces raw nodes and `sanitizeAutoQueue` normalizes them. Testing +// the raw output alone would pass on a shape the sanitizer then coerced into +// something else. +// +// IT LIVES IN jobs/ THOUGH THE MODULE LIVES IN lib/, and that is the layering +// guard rather than an accident: architecture.test.ts forbids lib/ importing +// jobs/, so a test beside the module could not reach the sanitizer it has to +// assert through. jobs/ importing lib/ is the legal direction. +const migrate = (file: unknown) => sanitizeAutoQueue(migrateSweepsToLanes(file)); + +// The two lanes as the live settings.json carries them, minus the sweep fields +// under test. +const LIVE_LANES = { + transcription: { + enabled: true, + maxWorkers: null, + order: "listed", + root: { + id: "t-root", + mode: "strict", + children: [{ id: "t-all", match: { type: "all" } }], + }, + }, + download: { + enabled: true, + maxWorkers: null, + order: "listed", + root: { + id: "d-root", + mode: "strict", + children: [{ id: "d-all", match: { type: "all" } }], + }, + }, +}; + +test("the live shape: both sweeps disarmed, no channel scope", () => { + const out = migrate({ + autoQueue: LIVE_LANES, + digest: { + sweepEnabled: false, + sweepChannels: [], + recencyOrder: "newest", + recencyReach: "corpus", + }, + backfill: { + enabled: true, + sweepEnabled: false, + sweepKinds: [], + sweepChannels: [], + order: "newest", + reach: "corpus", + }, + }); + + // THE HEADLINE ASSERTION OF THE WHOLE SLICE. A migration that enabled a lane + // the flag did not would arm ~66,540 audio re-downloads on this corpus. + assert.equal(out.digest.enabled, false); + assert.equal(out.backfill.enabled, false); + + // The digest lane keeps `cheapest` — the composition — rather than taking + // `recencyOrder`, which is only its date half. + assert.equal(out.digest.order, "cheapest"); + // The backfill lane's order had no second term, so it is carried verbatim. + assert.equal(out.backfill.order, "newest"); + + // An empty channel list is one catch-all leaf, which is what an empty + // `sweepChannels` meant to `startDigestSweep`. + for (const lane of ["digest", "backfill"] as const) { + assert.equal(out[lane].root.mode, "strict"); + assert.deepEqual( + out[lane].root.children.map((c) => ("match" in c ? c.match : null)), + [{ type: "all" }], + ); + } + + // The two runner lanes are untouched by any of it. + assert.equal(out.transcription.enabled, true); + assert.equal(out.transcription.root.children.length, 1); + assert.equal(out.download.root.id, "d-root"); +}); + +test("an armed digest sweep with three channels becomes three channel leaves", () => { + const out = migrate({ + autoQueue: LIVE_LANES, + digest: { + sweepEnabled: true, + sweepChannels: ["teamrcn", "ObviousRises-rumble", "HasanAbiVODs3"], + }, + }); + + assert.equal(out.digest.enabled, true); + assert.deepEqual( + out.digest.root.children.map((c) => ("match" in c ? c.match : null)), + [ + { type: "channel", value: "teamrcn" }, + { type: "channel", value: "ObviousRises-rumble" }, + { type: "channel", value: "HasanAbiVODs3" }, + ], + ); + // Ids are derived from the scope, not generated, so re-arming the same scope + // keeps the fairness memory and pick log that leaf already had. + assert.deepEqual( + out.digest.root.children.map((c) => c.id), + ["digest-teamrcn", "digest-ObviousRises-rumble", "digest-HasanAbiVODs3"], + ); + // Scope ORDER is priority: a strict root walks its children in order, and the + // sweep visited its scope in the order it was given. + assert.equal(out.digest.root.mode, "strict"); +}); + +test("an armed backfill sweep with two kinds and one channel is a leaf per pair", () => { + const out = migrate({ + autoQueue: LIVE_LANES, + backfill: { + sweepEnabled: true, + sweepKinds: ["diarization", "attribution-diarized"], + sweepChannels: ["teamrcn"], + order: "oldest", + }, + }); + + assert.equal(out.backfill.enabled, true); + assert.equal(out.backfill.order, "oldest"); + assert.deepEqual( + out.backfill.root.children.map((c) => ("match" in c ? c.match : null)), + [ + { type: "channel", value: "teamrcn", operation: "diarization" }, + { type: "channel", value: "teamrcn", operation: "attribution-diarized" }, + ], + ); + assert.deepEqual( + out.backfill.root.children.map((c) => c.id), + ["backfill-teamrcn-diarization", "backfill-teamrcn-attribution-diarized"], + ); +}); + +test("an operation scope with no channel scope is a catch-all leaf per operation", () => { + const out = migrate({ + backfill: { sweepEnabled: true, sweepKinds: ["diarization"] }, + }); + assert.deepEqual( + out.backfill.root.children.map((c) => ("match" in c ? c.match : null)), + [{ type: "all", operation: "diarization" }], + ); +}); + +test("a file that already carries the blocks is untouched", () => { + const already = { + autoQueue: { + ...LIVE_LANES, + digest: { + enabled: true, + maxWorkers: 2, + order: "newest", + root: { + id: "kept", + mode: "round-robin", + children: [{ id: "kept-leaf", match: { type: "channel", value: "keep" } }], + }, + }, + backfill: { + enabled: false, + maxWorkers: null, + order: "listed", + root: { id: "kept-b", mode: "strict", children: [] }, + }, + }, + // Sweep fields still on disk — tolerated, and deliberately NOT allowed to + // overwrite a lane block that exists. A file mid-migration has both. + digest: { sweepEnabled: false, sweepChannels: ["ignored"] }, + backfill: { sweepEnabled: true, sweepChannels: ["ignored"], order: "newest" }, + }; + const out = migrate(already); + + assert.equal(out.digest.enabled, true); + assert.equal(out.digest.maxWorkers, 2); + assert.equal(out.digest.order, "newest"); + assert.equal(out.digest.root.mode, "round-robin"); + assert.deepEqual( + out.digest.root.children.map((c) => ("match" in c ? c.match : null)), + [{ type: "channel", value: "keep" }], + ); + // The armed sweep flag does NOT reach a lane that already has a block. + assert.equal(out.backfill.enabled, false); + assert.equal(out.backfill.order, "listed"); + assert.deepEqual(out.backfill.root.children, []); + + // Idempotent: running it over its own output changes nothing. + assert.deepEqual(migrate({ ...already, autoQueue: out }), out); +}); + +test("a settings file with no digest or backfill block at all migrates to the lane defaults", () => { + const out = migrate({}); + assert.equal(out.digest.enabled, false); + assert.equal(out.digest.order, "cheapest"); + assert.equal(out.backfill.enabled, false); + assert.equal(out.backfill.order, "listed"); + for (const lane of ["digest", "backfill"] as const) { + assert.deepEqual( + out[lane].root.children.map((c) => ("match" in c ? c.match : null)), + [{ type: "all" }], + ); + } +}); + +// THE CHECK THE SLICE PLAN ASKS FOR, as a unit test rather than as an e2e +// assertion: arming a lane from the console with channels X must produce the +// tree the migration would have produced from `sweepChannels: X`. It holds by +// 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("arming a lane with a scope equals migrating the same scope", () => { + const channels = ["teamrcn", "ObviousRises-rumble"]; + const armed = laneRootFromScope("digest", { channels }); + const migrated = migrateSweepsToLanes({ + digest: { sweepEnabled: true, sweepChannels: channels }, + }).digest as { root: unknown }; + assert.deepEqual(armed, migrated.root); + + const armedBackfill = laneRootFromScope("backfill", { + channels, + operations: ["diarization"], + }); + const migratedBackfill = migrateSweepsToLanes({ + backfill: { + sweepEnabled: true, + sweepChannels: channels, + sweepKinds: ["diarization"], + }, + }).backfill as { root: unknown }; + assert.deepEqual(armedBackfill, migratedBackfill.root); +}); + +test("blank and duplicate scope entries are dropped rather than becoming leaves", () => { + const root = laneRootFromScope("digest", { + channels: ["a", " ", "a", " b "], + }); + assert.deepEqual( + root.children.map((c) => ("match" in c ? c.match : null)), + [ + { type: "channel", value: "a" }, + { type: "channel", value: "b" }, + ], + ); +}); diff --git a/common/lib/laneMigration.ts b/common/lib/laneMigration.ts @@ -0,0 +1,160 @@ +// THE SWEEPS' LAST ACT: their persisted scope becomes a lane's tree. +// +// Two sweeps armed themselves through ten fields on `settings.digest` and +// `settings.backfill` — a flag, a channel list, an operation list and an +// order apiece. Slice 1.3 deletes all of that: a lane is armed by +// `autoQueue[lane].enabled` and scoped by `autoQueue[lane].root`, the same two +// things that have armed and scoped the transcription and download lanes since +// the auto-queue existed. +// +// So the fields have to land somewhere on the way out, and this is the pure +// function that lands them. It runs on READ (getSettings), not as a one-shot +// script, for the reason every migration in this file's neighbourhood does: the +// editor is not the only reader of settings.json — bin/ scripts, the MCP server +// and the export build all call getSettings — and a migration that only ran +// when someone opened a page would give two readers two different answers about +// what is armed. +// +// THREE RULES, each of which is a bug it is written to avoid: +// +// 1. IT NEVER ENABLES A LANE THE FLAG DID NOT. `sweepEnabled: false` becomes +// `enabled: false`. The backfill lane as configured on the live corpus +// would re-fetch audio for ~66,540 videos; an off-by-one in a migration is +// not an acceptable way to find that out. +// 2. IT ONLY APPLIES WHEN THE LANE BLOCK IS ABSENT FROM THE FILE. A file that +// already carries `autoQueue.digest` has been through here (or has been +// hand-edited, which is the same claim) and is returned untouched — so the +// function is idempotent by construction rather than by a version marker +// nobody would maintain. +// 3. AN EMPTY CHANNEL LIST IS ONE `{type:"all"}` LEAF, because that is what an +// empty `sweepChannels` meant: `startDigestSweep` passes +// `scope.length > 0 ? scope : undefined` and an absent scope is the whole +// corpus. A leaf per channel would have been the same thing today and a +// different thing the day a channel is added. +// +// It lives in lib/ and imports nothing from jobs/: it produces RAW nodes, which +// `sanitizeAutoQueue` then normalizes exactly as it normalizes a hand-edited +// file. That is deliberate — a migration that built already-sanitized objects +// would be a second implementation of the sanitizer, and the two would drift. + +import type { AutoQueueGroup, AutoQueueLeaf, AutoQueueMatch } from "./autoQueueTypes"; + +// A lane's scope in the terms the sweeps used: some channels, some operations. +// Both empty means "everything", which is what an unscoped sweep meant. +export type LaneScope = { + channels?: readonly string[] | undefined; + operations?: readonly string[] | undefined; +}; + +function slugs(value: unknown): string[] { + if (!Array.isArray(value)) return []; + const out: string[] = []; + for (const v of value) { + if (typeof v !== "string") continue; + const s = v.trim(); + if (s && !out.includes(s)) out.push(s); + } + return out; +} + +// THE ONE LEAF BUILDER, shared by the migration below and by the editor's +// `armLaneAction`. That sharing is the point: "arming a lane from the console +// produces the tree the migration would have produced" is a property worth +// having, and the cheapest way to have it is for there to be one function. +// +// Ids are DERIVED, not generated: `digest-teamrcn`, `backfill-all-diarization`. +// The sanitizer would happily assign `node-7`, but a lane's fairness memory and +// its pick log are keyed by leaf id, so a stable id means re-arming the same +// scope keeps the ledger it had. +export function laneRootFromScope( + lane: string, + scope: LaneScope, +): AutoQueueGroup { + const channels = slugs(scope.channels); + const operations = slugs(scope.operations); + const targets: AutoQueueMatch[] = + channels.length > 0 + ? channels.map((value) => ({ type: "channel" as const, value })) + : [{ type: "all" as const }]; + const leaf = (match: AutoQueueMatch): AutoQueueLeaf => ({ + id: [lane, match.value ?? "all", match.operation].filter(Boolean).join("-"), + match, + weight: 1, + maxWorkers: null, + }); + const children: AutoQueueLeaf[] = + operations.length > 0 + ? targets.flatMap((match) => + operations.map((operation) => leaf({ ...match, operation })), + ) + : targets.map((match) => leaf(match)); + return { + id: `${lane}-root`, + // STRICT, like every tree an operator has ever been handed: the sweeps + // walked their scope in order and never interleaved, and round-robin would + // be a dispatch change dressed up as a migration. + mode: "strict", + weight: 1, + maxWorkers: null, + children, + }; +} + +type RawRecord = Record<string, unknown>; + +function asRecord(value: unknown): RawRecord { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as RawRecord) + : {}; +} + +// The `autoQueue` object to hand `sanitizeAutoQueue`, with the two operation +// lanes filled in from the retired sweep fields when — and only when — the file +// does not already spell them. +// +// 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 { + const file = asRecord(parsed); + const autoQueue = { ...asRecord(file.autoQueue) }; + + if (autoQueue.digest === undefined) { + const digest = asRecord(file.digest); + autoQueue.digest = { + enabled: digest.sweepEnabled === true, + // NO `order`. The lane's default is `cheapest`, which is the composition + // the digest batch actually ran — duration inside a day, newest day + // first — and `digest.recencyOrder` was only ever the DATE HALF of it. + // Migrating it onto `order` would replace the composition with one of its + // two terms and silently drop the other; the runner fixes the date half + // at newest-first (the live value) instead. A digest lane that wants pure + // recency sets `order: "newest"` and gets no duration term at all. + root: laneRootFromScope("digest", { + channels: slugs(digest.sweepChannels), + }), + }; + } + + if (autoQueue.backfill === undefined) { + const backfill = asRecord(file.backfill); + const order = backfill.order; + autoQueue.backfill = { + enabled: backfill.sweepEnabled === true, + // Carried, unlike digest's: the backfill lane's order was a plain + // recency order with no second term to lose, and the runner reads the + // same enum. An unrecognised value is the sanitizer's problem. + ...(order === undefined ? {} : { order }), + root: laneRootFromScope("backfill", { + channels: slugs(backfill.sweepChannels), + // `sweepKinds` was the operation scope, and empty meant "every enabled + // operation on the lane" — which is exactly what a leaf naming no + // 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), + }), + }; + } + + return autoQueue; +}