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:
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