Archilyzer · Source

archilyzer

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

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:
Mcommon/lib/channelPriority.test.ts | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcommon/lib/channelPriority.ts | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
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 });