commit a32f2f1ff14dda0787ef8cd3b0fa4c0991d01d3d
parent 6248309257a83e732b4e018f25bc582e54c1339b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 11 Sep 2026 12:23:23 -0400
priority: the migrated ranks are dense, and the migration is idempotent
Two review findings on `channelPriorityFromLegacy`, both about it reading a
tree it should not read that way.
DENSE RANKS (finding 4). Merging two lane orders by "the lower index wins"
produces COLLISIONS — four pairs on the live trees, because each lane ranked
one of the two Quartering channels second — and a collision falls through to
`orderWithin`'s slug fallback, which is NEITHER lane's order. So the merged
indices are renumbered 0..n-1 with a stated tie rule: the lane that ranked the
channel higher (the merged index itself), then the TRANSCRIPTION lane's own
order, then the slug. A channel transcription never ranked sorts after every
channel it did, within the same merged index. The 15-channel test now asserts
the ORDER — all fourteen ranked slugs, and the same order coming back out of
`compileLaneRoot`, which is the only reason a rank matters.
IDEMPOTENCE (finding 8). A compiled tree is made of BARE CHANNEL LEAVES, which
is exactly what rule 2 reads as a rank — so a second migration run would have
re-derived the hand-made order from the tree that order produced: dense,
alphabetical inside each tier, irrecoverable. `hasCompiledLaneRoots` is the
detector (`prio-*` ids are produced by the compiler and by nothing else) and
the stored document is returned whole rather than merged with a re-derivation.
The same detector is what the one writer's legacy seed gates on.
common 985/985.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 149 insertions(+), 17 deletions(-)
diff --git a/common/lib/channelPriority.test.ts b/common/lib/channelPriority.test.ts
@@ -9,6 +9,7 @@ import {
channelsForOperation,
compileLaneRoot,
compileLanes,
+ hasCompiledLaneRoots,
defaultChannelPriority,
effectiveTier,
focusSummary,
@@ -732,27 +733,91 @@ function liveConfigs() {
}));
}
+// THE MIGRATED ORDER, IN FULL — the thing the plan calls the migration's one
+// behaviour change, asserted as an ORDER and not as a set of memberships.
+//
+// Merged index first (the lane that ranked a channel higher wins), then the
+// transcription lane's own order, then the slug. Four merged indices carry two
+// channels each on the live trees, and that is exactly where a collision would
+// otherwise have been resolved alphabetically — by NEITHER lane's order.
+const EXPECTED_LIVE_ORDER = [
+ "quartering-live", // t0 d0 -> merged 0
+ "the-quartering-rumble", // t1 d2 -> merged 1, transcription first
+ "the-quartering", // t2 d1 -> merged 1, transcription second
+ "HasanAbiVODs3", // t3 -> merged 3, transcription 3
+ "nuxanor", // t7 d3 -> merged 3, transcription 7
+ "hasanabi", // t4 -> merged 4, transcription 4
+ "darlingstrawb", // d4 -> merged 4, unranked by transcription
+ "rekietalaw-rumble", // t5 -> merged 5
+ "chibi-reviews", // d5 -> merged 5, unranked by transcription
+ "nux-taku", // t6 -> merged 6
+ "destiny", // d6 -> merged 6, unranked by transcription
+ "omnivods-odysee", // d7 -> merged 7
+ "leaflit-rumble", // t8 -> merged 8
+ "piratesoftware", // d8 -> merged 8, unranked by transcription
+];
+
test("the legacy read collapses two lane orders into one rank per channel", () => {
const model = channelPriorityFromLegacy(liveConfigs(), LIVE_AUTO_QUEUE);
- // The three Quartering channels are in different orders in the two lanes; the
- // LOWER index wins, so the transcription order survives for the one it ranks
- // higher and the download order for the other.
- assert.equal(rankOf(model, "quartering-live"), 0);
- assert.equal(rankOf(model, "the-quartering-rumble"), 1); // t:1 vs d:2
- assert.equal(rankOf(model, "the-quartering"), 1); // t:2 vs d:1
- // Ranked in one lane only: that lane's index stands.
- assert.equal(rankOf(model, "HasanAbiVODs3"), 3);
- assert.equal(rankOf(model, "darlingstrawb"), 4);
- assert.equal(rankOf(model, "leaflit-rumble"), 8);
- assert.equal(rankOf(model, "piratesoftware"), 8);
- // nuxanor: t:7 vs d:3.
- assert.equal(rankOf(model, "nuxanor"), 3);
+ // DENSE, 0..n-1, in this order. The merge produces collisions (four pairs
+ // here); leaving them would hand the tie to `orderWithin`'s slug fallback,
+ // which is neither lane's order and is not what either list said.
+ const ranked = Object.entries(model.channels)
+ .filter(([, e]) => e.rank !== undefined)
+ .sort(([, a], [, b]) => (a.rank ?? 0) - (b.rank ?? 0))
+ .map(([slug]) => slug);
+ assert.deepEqual(ranked, EXPECTED_LIVE_ORDER);
+ assert.deepEqual(
+ EXPECTED_LIVE_ORDER.map((slug) => rankOf(model, slug)),
+ EXPECTED_LIVE_ORDER.map((_, i) => i),
+ );
+ // The order the COMPILER then produces is the same one — which is the only
+ // reason the rank matters at all.
+ const slugs = liveConfigs().map((c) => c.slug);
+ const normal = compileLaneRoot("download", model, slugs, []).children.find(
+ (c) => c.id === "prio-normal",
+ ) as AutoQueueGroup;
+ assert.deepEqual(
+ normal.children.slice(0, EXPECTED_LIVE_ORDER.length).map((c) => c.id),
+ EXPECTED_LIVE_ORDER.map((slug) => `prio-normal-${slug}`),
+ );
// Unranked and not excluded = absent entirely.
assert.equal(model.channels["unranked-one"], undefined);
assert.equal(tierOf(model, "unranked-one"), "normal");
assert.deepEqual(model.focus, { kind: "none" });
});
+test("the legacy read is a no-op on a tree it already compiled", () => {
+ const configs = liveConfigs();
+ const slugs = configs.map((c) => c.slug);
+ const model = channelPriorityFromLegacy(configs, LIVE_AUTO_QUEUE);
+ const lanes = compileLanes(model, slugs, []);
+ const compiled = Object.fromEntries(
+ LANES.map((lane) => [lane, { root: lanes[lane] }]),
+ );
+ assert.equal(hasCompiledLaneRoots(LIVE_AUTO_QUEUE), false);
+ assert.equal(hasCompiledLaneRoots(compiled), true);
+
+ // A SECOND RUN OVER ITS OWN OUTPUT. Compiled channel leaves are bare channel
+ // leaves, so without the detector rule 2 would read them and renumber the
+ // hand-made order into "focus group, then normal group, then low" — dense,
+ // alphabetical within each tier, and irrecoverable.
+ const second = channelPriorityFromLegacy(configs, compiled, model);
+ assert.deepEqual(second, model);
+
+ // Including when the stored document has since been EDITED: the stored
+ // document wins whole, it is not merged with a re-derivation.
+ const edited = sanitizeChannelPriority({
+ ...model,
+ focus: { kind: "channels", slugs: ["hasanabi"] },
+ channels: { ...model.channels, destiny: { tier: "paused" } },
+ });
+ assert.deepEqual(
+ channelPriorityFromLegacy(configs, compiled, edited),
+ edited,
+ );
+});
+
test("the legacy read is LOSSLESS: excludeFromSync becomes a sync override only", () => {
const model = channelPriorityFromLegacy(liveConfigs(), LIVE_AUTO_QUEUE);
for (const slug of LIVE_EXCLUDED) {
@@ -769,7 +834,10 @@ test("the legacy read is LOSSLESS: excludeFromSync becomes a sync override only"
assert.ok(LIVE_DOWNLOAD.includes("omnivods-odysee"));
assert.deepEqual(model.channels["omnivods-odysee"], {
tier: "normal",
- rank: 7,
+ // Dense rank 11, not the merged index 7 — see EXPECTED_LIVE_ORDER. Its
+ // POSITION among the ranked channels is what the download lane reads, and
+ // that is unchanged: still behind destiny, still ahead of piratesoftware.
+ rank: 11,
overrides: { sync: "paused" },
});
});
diff --git a/common/lib/channelPriority.ts b/common/lib/channelPriority.ts
@@ -658,6 +658,33 @@ function bareChannelSlugs(root: AutoQueueNode | undefined): string[] {
return out;
}
+// Does any lane's stored root already carry a COMPILED leaf?
+//
+// `prio-*` ids are produced by `compileLaneRoot` and by nothing else, which is
+// what makes them a reliable "this tree was written by the priority writer"
+// marker — and the marker two callers need before they read a tree as legacy:
+//
+// - `channelPriorityFromLegacy` (below), so a second migration run cannot
+// re-derive ranks from its own output;
+// - the one writer's legacy seed (editor/app/channels/actions.ts), so a
+// document that was cleared back to empty on a corpus whose trees are
+// already compiled is not re-seeded from those compiled trees.
+//
+// Both hazards are the same one, and it is not hypothetical: compiled channel
+// leaves ARE bare channel leaves, so `bareChannelSlugs` reads them happily and
+// would replace a hand-made 9+9 order with a reading of the tree that order
+// already produced — dense, alphabetical inside each tier, and irrecoverable.
+export function hasCompiledLaneRoots(autoQueue: LegacyLaneRoots): boolean {
+ const seen = (node: AutoQueueNode | undefined): boolean => {
+ if (!node) return false;
+ if (typeof node.id === "string" && node.id.startsWith(PRIO_ID_PREFIX)) {
+ return true;
+ }
+ return isGroup(node) ? node.children.some(seen) : false;
+ };
+ return LANES.some((lane) => seen(autoQueue[lane]?.root));
+}
+
// THE LEGACY READ: one exclusion flag and two hand-made lane orders become one
// document. Pure, no I/O, and asserted through `sanitizeChannelPriority` — the
// same shape `lib/laneMigration.ts` uses, for the same reason.
@@ -689,6 +716,25 @@ function bareChannelSlugs(root: AutoQueueNode | undefined): string[] {
// and "whichever lane ranked it higher" is the answer that loses no
// priority. This is the one thing the migration DOES change, and it is
// measured with plans/tools/phase1-numbers.ts.
+//
+// THE MERGED RANKS ARE THEN RENUMBERED DENSELY, 0..n-1, and that is not
+// cosmetic: the merge produces COLLISIONS (four pairs on the live trees —
+// `the-quartering` and `the-quartering-rumble` both land on 1, because
+// each lane ranked one of them second), and a collision is resolved by
+// `orderWithin`'s slug fallback, which is NEITHER lane's order. The tie
+// rule, in order: the lane that ranked the channel higher (that is the
+// merged index itself), then the TRANSCRIPTION lane's own order — the
+// longer-standing of the two hand-made lists, and the one that ranks the
+// Quartering channels the way the operator most recently arranged them —
+// then the slug. A channel the transcription lane never ranked sorts
+// after every channel it did, within the same merged index.
+//
+// 4. IT DOES NOT RE-DERIVE FROM ITS OWN OUTPUT. A compiled tree is made of
+// bare channel leaves, so rule 2 would read one perfectly happily and
+// collapse a hand-made order into a reading of the tree that order
+// produced. `hasCompiledLaneRoots` is the detector; `stored` is what is
+// returned instead, so a second migration run is a no-op rather than a
+// quiet rewrite.
// 3. EVERYTHING ELSE IS ABSENT — normal, unranked — and the focus starts at
// `none`. A migration does not start a focus.
//
@@ -697,21 +743,39 @@ function bareChannelSlugs(root: AutoQueueNode | undefined): string[] {
export function channelPriorityFromLegacy(
configs: readonly LegacyChannelRow[],
autoQueue: LegacyLaneRoots,
+ // The document already on disk. Returned unchanged when the trees are
+ // compiled — see rule 4. Defaults to the empty document, which is what a
+ // caller with nothing stored has anyway.
+ stored: ChannelPriority = defaultChannelPriority(),
): ChannelPriority {
+ if (hasCompiledLaneRoots(autoQueue)) return sanitizeChannelPriority(stored);
const syncPaused = new Set<string>();
for (const row of configs) {
const slug = typeof row?.slug === "string" ? row.slug.trim() : "";
if (!slug) continue;
if (row.config?.excludeFromSync === true) syncPaused.add(slug);
}
- const ranks = new Map<string, number>();
+ const merged = new Map<string, number>();
for (const lane of LEGACY_RANKED_LANES) {
const slugs = bareChannelSlugs(autoQueue[lane]?.root);
slugs.forEach((slug, index) => {
- const seen = ranks.get(slug);
- if (seen === undefined || index < seen) ranks.set(slug, index);
+ const seen = merged.get(slug);
+ if (seen === undefined || index < seen) merged.set(slug, index);
});
}
+ // The tie-break lane's own order, for the renumbering below.
+ const transcription = bareChannelSlugs(autoQueue.transcription?.root);
+ const tieIndex = (slug: string): number => {
+ const i = transcription.indexOf(slug);
+ return i === -1 ? Number.POSITIVE_INFINITY : i;
+ };
+ const ranks = new Map<string, number>();
+ [...merged.entries()]
+ .sort(
+ ([aSlug, a], [bSlug, b]) =>
+ a - b || tieIndex(aSlug) - tieIndex(bSlug) || aSlug.localeCompare(bSlug),
+ )
+ .forEach(([slug], index) => ranks.set(slug, index));
const channels: Record<string, ChannelPriorityEntry> = {};
const touch = (slug: string): ChannelPriorityEntry =>
(channels[slug] ??= { tier: DEFAULT_CHANNEL_TIER });