Archilyzer · Source

archilyzer

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

commit 41a5d202d5a2b12b91ccfe86c6c9cff3b0b5db1a
parent ac2e1e09a4c95e9466c5cdfc71fad1fec11e85bf
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  7 Sep 2026 19:44:58 -0400

common: cheapest is not a recency order, and an empty projection reads no snapshot

Three review fix-ups on slice 1.1.

`makeRecencyComparator` returned a comparator for `"cheapest"`: the `order ===
"listed"` guard did not cover it, so it fell through to the `olderFirst = -1`
arm and the digest lane's own default order silently meant OLDEST-FIRST — a
real ordering nobody asked for, and the exact opposite of the shortest-first it
is named for. It returns null now, like "listed", so the lane keeps today's
order until its runner supplies the duration comparator.

`buildChannelWork` returns early when the lane projects neither buckets nor
operations. The status payload folds four lanes on a three-second poll and the
two new ones would otherwise read and JSON.parse all 68 snapshots (~6.5 MB)
twice a tick to build a map every leaf draws [] from. It is marked for deletion
in 1.2, when a lane's default draw is its operation union.

And a test for the slice's headline invariant, which nothing pinned: an
operation leaf FIRST in a runner lane's tree draws [] while the bucket leaves
below it keep everything — the zero retainLeaves("buckets") used to write. Its
mirror image is asserted too, so it reads as a statement about the projection
rather than about operation leaves being inert.

common 890 tests, tsc clean in common and editor, numbers diff empty.

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

Diffstat:
Mcommon/controller/autoRunner.ts | 12++++++++++++
Mcommon/controller/recencyIndex.test.ts | 15+++++++++++++++
Mcommon/controller/recencyIndex.ts | 9++++++++-
Mcommon/jobs/autoQueuePolicy.test.ts | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/one-core-phase-1.md | 15+++++++++++++++
5 files changed, 109 insertions(+), 1 deletion(-)

diff --git a/common/controller/autoRunner.ts b/common/controller/autoRunner.ts @@ -219,6 +219,18 @@ async function buildChannelWork( // none, so this is empty and the projection is byte-identical to today's. operations: ReadonlyArray<string> = [], ): Promise<{ channels: ChannelWork[]; owner: Map<string, string> }> { + // NOTHING TO PROJECT MEANS NOTHING TO READ. The status payload folds all four + // lanes on a three-second poll, and the digest and backfill lanes have no + // buckets and — while their tree is the default catch-all leaf — no + // operations either, so without this they would read and JSON.parse all 68 + // snapshots (~6.5 MB) twice a tick to build a map every leaf draws [] from. + // + // DELETE THIS the moment a lane's default draw is its operation union (slice + // 1.2): from then on `operations` is non-empty for every live lane and the + // guard only fires for a lane that is genuinely switched off. + if (selectableBucketsForKind(kind).length === 0 && operations.length === 0) { + return { channels: [], owner: new Map() }; + } const channels: ChannelWork[] = []; const owner = new Map<string, string>(); // Read every snapshot concurrently, then fold sequentially — `owner` is diff --git a/common/controller/recencyIndex.test.ts b/common/controller/recencyIndex.test.ts @@ -320,3 +320,18 @@ test("layer 1 still serves an id that IS its channel's metadata id", async () => await rm(dir, { recursive: true, force: true }); } }); + +test('"cheapest" is not a recency order: no comparator, so no silent oldest-first', () => { + // It is keyed by DURATION and its comparator comes from the lane's runner + // (slice 1.2). Falling through to the date sort would have made it mean + // oldest-first — a real ordering nobody asked for. + const keys = new Map<string, RecencyKey>([ + ["a", { key: "20260101", estimated: false }], + ["b", { key: "20261231", estimated: false }], + ]); + assert.equal(makeRecencyComparator(keys, "cheapest"), null); + assert.equal(makeRecencyComparator(keys, "listed"), null); + // And the two orders this module DOES answer still do. + assert.ok(makeRecencyComparator(keys, "newest")); + assert.ok(makeRecencyComparator(keys, "oldest")); +}); diff --git a/common/controller/recencyIndex.ts b/common/controller/recencyIndex.ts @@ -507,6 +507,13 @@ export async function buildRecencyKeys({ // absent comparator — so the historical order is reproduced by not sorting at // all, not by sorting with an identity comparator. // +// AND NULL FOR "cheapest", which is NOT a recency order at all: it is keyed by +// DURATION, and the comparator for it is supplied by the lane's runner. Falling +// through to the date sort below would have made it silently mean oldest-first +// (the `olderFirst = -1` arm), which is a real ordering an operator did not ask +// for — so an order this module cannot answer returns "don't sort" and the lane +// keeps today's behaviour until its runner supplies one. +// // Equal keys compare 0 deliberately: Array#sort is stable, so videos sharing an // upload date keep their "listed" order (playlist order for undownloadedIds, // id order elsewhere) instead of being shuffled by an arbitrary tiebreak. @@ -514,7 +521,7 @@ export function makeRecencyComparator( keys: ReadonlyMap<string, RecencyKey>, order: AutoQueueOrder, ): ((a: string, b: string) => number) | null { - if (order === "listed") return null; + if (order === "listed" || order === "cheapest") return null; // Keys are YYYYMMDD strings, so lexicographic order IS chronological order. // "newest" therefore sorts DESCENDING: a smaller (older) key must come later, // which is a positive comparator result. diff --git a/common/jobs/autoQueuePolicy.test.ts b/common/jobs/autoQueuePolicy.test.ts @@ -602,6 +602,65 @@ test("bucketsForKind: the operation lanes have no buckets", () => { } }); +test("an operation leaf in a runner lane's tree draws NOTHING", () => { + // THE SLICE'S HEADLINE INVARIANT, and the one retainLeaves("buckets") used to + // write by zeroing the leaf after the fact. A leaf naming `digest` inside the + // auto-transcribe tree is legal to write and always has been; getting this + // wrong hands a digest candidate to whisper. + // + // It now holds BY CONSTRUCTION: the transcription lane projects no `digest` + // list (operationsForLane("transcription") is empty, so autoRunner's + // buildChannelWork never puts one in ChannelWork.operations), and a leaf with + // no list to draw from comes back empty on its own. + const root: AutoQueueGroup = { + id: "root", + mode: "strict", + children: [ + // FIRST in priority order, so if it could claim anything it would claim + // it out from under the bucket leaves below. + { id: "digest-leaf", match: { type: "all", operation: "digest" } }, + { id: "cornbread", match: { type: "channel", value: "cornbreadman" } }, + { id: "rest", match: { type: "all" } }, + ], + }; + const channels: ChannelWork[] = [ + { + slug: "cornbreadman", + platform: "youtube", + buckets: { downloadedNoTranscript: ["c1"], failedListed: ["c2"] }, + // Exactly what the transcription lane projects: no operations map at all. + }, + { + slug: "hasanabi", + platform: "youtube", + buckets: { downloadedNoTranscript: ["h1"], failedListed: [] }, + }, + ]; + const pending = buildPendingByLeaf( + root, + channels, + bucketsForKind("transcription"), + ); + assert.deepEqual(pending["digest-leaf"], []); + // ... and the bucket leaves are untouched: the operation leaf stole nothing. + assert.deepEqual(pending.cornbread, ["c1", "c2"]); + assert.deepEqual(pending.rest, ["h1"]); + + // The mirror image, so this is a statement about the PROJECTION and not about + // operation leaves being inert: hand the same tree a lane that DOES project + // `digest`, and the same leaf claims it. + const withDigest: ChannelWork[] = channels.map((c) => ({ + ...c, + operations: { digest: [`${c.slug}-d`] }, + })); + const pending2 = buildPendingByLeaf( + root, + withDigest, + bucketsForKind("transcription"), + ); + assert.deepEqual(pending2["digest-leaf"], ["cornbreadman-d", "hasanabi-d"]); +}); + test("sanitizeAutoQueue defaults replaceAutoSubs to false", () => { assert.equal(defaultAutoQueue().transcription.replaceAutoSubs, false); assert.equal(sanitizeAutoQueue({}).download.replaceAutoSubs, false); diff --git a/plans/one-core-phase-1.md b/plans/one-core-phase-1.md @@ -303,6 +303,21 @@ true: `download`.** Widening it would expose a Start button for a lane the runner cannot execute. It widens in 1.2 with `run()`. +**Review fix-up, the commit after `621ffa6`.** Three things the slice shipped with: `makeRecencyComparator` +did not cover `"cheapest"` and fell through to the `olderFirst = -1` arm, so the digest +lane's default order silently meant OLDEST-FIRST (it returns null now); `buildChannelWork` +read and parsed all 68 snapshots for the two lanes whose projection is empty today (early +return, deleted in 1.2 when a lane's default draw is its operation union); and nothing +pinned the slice's headline invariant — an operation leaf inside a runner lane's tree draws +`[]` while the bucket leaves are untouched. common 890 tests, numbers diff still empty. + +**Open design point 1.2 must settle first: the real digest order is duration THEN recency.** +`digestBatch.ts:242-252` composes both, and the live `digest.recencyOrder` is `"newest"` — +but `AutoQueuePolicy.order` is one field holding one of them. So 1.2 has to NAME the +composition (the obvious reading: `cheapest` = duration ascending, ties broken by the lane's +`reach`/recency order) before 1.3 migrates `recencyOrder` into the tree, or the migration +silently drops half of today's ordering. + **For 1.2.** `buildChannelWork`'s `operations` argument is now `laneOperationIds(kind, root)` — an INTERSECTION, so the digest lane projects nothing while its tree is the default catch-all leaf. `next()` drawing the lane's default operation union is the change