Archilyzer · Source

archilyzer

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

commit 3784ea0529d7ff58772e35be746812e2a7bbc691
parent 26689d10b832efe9345baa20d62a27facfeb089e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 11 Sep 2026 11:52:31 -0400

editor: the lane status payload ships the tree the lane dispatches from

S4's first half of plans/channel-priority.md. The payload used to ship
`getSettings().autoQueue[kind]` verbatim — the STORED policy — and while a
priority model exists the runner does not dispatch from the stored tree. It
compiles one per tick from `settings.channelPriority` (S1). A ladder drawn from
the stored root would therefore name leaves the runner does not have and
attribute zero pending to every one of them, which breaks the one thing the
console's rungs are for: they ARE the dispatch order.

`channelPriorityView.ts` resolves the model ONCE per payload — the channel
listing, and for a `{kind:"site"}` focus the sites read — and hands the four
lanes the same view. `laneRootFor` then returns `compileLaneRoot(...)` in place
of `policy.root`, spreading the stored policy so `held`, `snoozeUntil`,
`enabled`, `order` and `maxWorkers` survive. An absent document skips all of it:
no compile, no channel listing, no sites read, and the stored tree ships byte
for byte, so a corpus with no priorities set is on exactly today's payload.

Two new fields ride along for the banner and the editor: `policyCompiled`, and
this lane's own `focus` summary plus the focus's resolved display name. The
summary is computed here rather than on the client because it is keyed by
COMPILED leaf id and is only meaningful against the counts of the tree the lane
dispatches from. `computeLeafPending` returns counts, not the pending arrays,
so they are re-presented as arrays of the right length rather than widening that
return type and putting every leaf's whole pending set on a 3 s poll's heap.

The three-line default gate is duplicated from the runner's `laneDispatchRoot`,
which S1 kept private. At merge, export it there and delete `laneRootFor`.

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

Diffstat:
Aeditor/app/operations/channelPriorityView.ts | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/operations/status.ts | 51++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 183 insertions(+), 3 deletions(-)

diff --git a/editor/app/operations/channelPriorityView.ts b/editor/app/operations/channelPriorityView.ts @@ -0,0 +1,135 @@ +import { getPaths, type Paths } from "yt-dlp-transcript-common/lib/paths"; +import { getSettings } from "yt-dlp-transcript-common/lib/settings"; +import { listSites } from "yt-dlp-transcript-common/lib/site"; +import { listChannelConfigs } from "yt-dlp-transcript-common/controller/channels"; +import type { + AutoQueueGroup, + AutoQueuePolicy, +} from "yt-dlp-transcript-common/jobs/autoQueuePolicy"; +import type { AutoQueueKind } from "yt-dlp-transcript-common/lib/autoQueueTypes"; +import { + type ChannelPriority, + type FocusSummary, + type PendingByLeaf, + type SiteChannelIndex, + compileLaneRoot, + focusSummary, + resolveFocusSlugs, +} from "yt-dlp-transcript-common/lib/channelPriority"; + +// THE STATUS PAYLOAD'S HALF OF CHANNEL PRIORITY — the tree the lane actually +// dispatches from, and the focus banner's numbers. +// +// WHY THIS EXISTS AT ALL. The payload used to ship `getSettings().autoQueue[kind]` +// verbatim, which is the STORED policy. While a priority model exists the runner +// does not dispatch from the stored tree — it compiles one from the model per +// tick (S1, `laneDispatchRoot` in controller/autoRunner.ts) — so a ladder drawn +// from the stored root would name leaves the runner does not have and attribute +// zero pending to every one of them. The console's whole contract is that its +// rungs ARE the dispatch order, so it has to compile the same tree. +// +// COMPILED FROM THE SAME CONTRACT, not from a second one: `compileLaneRoot` is +// the one compiler (common/lib/channelPriority.ts) and both sides call it with +// the same arguments. The duplication that remains is the three-line gate below +// (`isDefaultChannelPriority` + the call), because S1 kept `laneDispatchRoot` +// private to the runner. AT MERGE: export it there and delete `laneRootFor` +// here — that is the one fold S5 owes this file. +// +// THE ABSENT DOCUMENT COSTS NOTHING. No focus and no channel entry means the +// compiler never runs, the stored tree is shipped byte for byte, and neither +// `listChannelConfigs` nor `listSites` is read — which is what keeps a corpus +// with no priorities set on exactly today's payload and today's cost. + +export type PriorityView = { + model: ChannelPriority; + // The model says something, so the four trees are compiled rather than + // stored. The lane consoles read this to go read-only on the tree. + compiled: boolean; + // Every channel slug, the population `compileLaneRoot` filters per lane. + // Empty when nothing is compiled — it is never read in that case. + slugs: string[]; + focusSlugs: string[]; + // A display name for the focus: the site's title for a site focus, the slugs + // for a channel focus. Resolved here because the model stores a siteId and + // the banner does no I/O. + name: string | null; +}; + +// No focus and no per-channel entry: the document says nothing, so the compiler +// must not run. The same predicate the runner applies, spelled the same way. +function isDefaultChannelPriority(model: ChannelPriority): boolean { + return model.focus.kind === "none" && Object.keys(model.channels).length === 0; +} + +// "a, b and c" for a short channel focus; "a, b and 4 more" past three, because +// this lands mid-sentence in a banner and a 30-slug list is not a name. +function channelFocusName(slugs: readonly string[]): string { + if (slugs.length <= 3) { + if (slugs.length <= 1) return slugs[0] ?? ""; + return `${slugs.slice(0, -1).join(", ")} and ${slugs[slugs.length - 1]}`; + } + return `${slugs.slice(0, 2).join(", ")} and ${slugs.length - 2} more`; +} + +export async function readPriorityView( + paths: Paths = getPaths(), +): Promise<PriorityView> { + const model = getSettings().channelPriority; + if (isDefaultChannelPriority(model)) { + return { model, compiled: false, slugs: [], focusSlugs: [], name: null }; + } + const slugs = (await listChannelConfigs(paths)).map((row) => row.slug); + // ONLY A SITE FOCUS READS THE SITES DIRECTORY, as in the runner: the other + // two kinds resolve from the document alone. + const siteChannels: Record<string, string[]> = {}; + let name: string | null = null; + if (model.focus.kind === "site") { + for (const site of listSites(paths)) { + siteChannels[site.siteId] = site.channels.map((c) => c.slug); + if (site.siteId === model.focus.siteId) name = site.siteTitle; + } + name = name ?? model.focus.siteId; + } + const index: SiteChannelIndex = siteChannels; + const focusSlugs = resolveFocusSlugs(model, index, slugs); + if (model.focus.kind === "channels") name = channelFocusName(focusSlugs); + return { model, compiled: true, slugs, focusSlugs, name }; +} + +// THE ROOT THIS LANE DISPATCHES FROM, for the ladder to draw. Identical to the +// runner's answer by construction: same compiler, same model, same focus set, +// and `compileLaneRoot` re-applies the per-lane paused filter itself, so handing +// it the whole corpus and handing it a pre-filtered list give the same tree. +export function laneRootFor( + view: PriorityView, + lane: AutoQueueKind, + policy: AutoQueuePolicy, +): AutoQueueGroup { + if (!view.compiled) return policy.root; + return compileLaneRoot(lane, view.model, view.slugs, view.focusSlugs); +} + +// `focusSummary` reads nothing but each leaf's COUNT, and `computeLeafPending` +// throws the arrays away before this layer sees them (it returns counts plus a +// truncated head). Rather than widen that return type — which would put the +// whole pending set of every leaf on a three-second poll's heap — the counts are +// re-presented as arrays of the right length. `new Array(n)` allocates no +// elements; only `.length` is ever read. +export function pendingByLeafFromCounts( + counts: Record<string, number>, +): PendingByLeaf { + const out: Record<string, readonly string[]> = {}; + for (const [id, n] of Object.entries(counts)) out[id] = new Array<string>(n); + return out; +} + +export function laneFocusSummary( + view: PriorityView, + counts: Record<string, number>, +): FocusSummary { + return focusSummary( + view.model, + view.focusSlugs, + pendingByLeafFromCounts(counts), + ); +} diff --git a/editor/app/operations/status.ts b/editor/app/operations/status.ts @@ -16,7 +16,14 @@ import { LANES } from "yt-dlp-transcript-common/lib/autoQueueTypes"; import type { AutoQueuePolicy } from "yt-dlp-transcript-common/jobs/autoQueuePolicy"; import { getWorkerPool } from "yt-dlp-transcript-common/jobs/workerPool"; import { isGateHeld } from "yt-dlp-transcript-common/lib/pauseGates"; +import type { FocusSummary } from "yt-dlp-transcript-common/lib/channelPriority"; import { buildAutoQueueLanes, type AutoQueueLanesPayload } from "./lanes"; +import { + type PriorityView, + laneFocusSummary, + laneRootFor, + readPriorityView, +} from "./channelPriorityView"; // Read-only payload for the Auto-Queue panel: per-kind runner status (running?, // what's in flight, in-flight counts per tree node), the effective policy, the @@ -35,6 +42,11 @@ export type PlatformCooldownView = { export type AutoQueueKindStatus = { kind: AutoQueueKind; + // THE POLICY AS THE LANE DISPATCHES IT, not as it is stored. Every field is + // the stored one except `root`, which is the COMPILED tree whenever + // `settings.channelPriority` says anything (see channelPriorityView.ts). The + // claim ladder is drawn from this, so a rung is a rule the runner actually + // has — and with a priority model set the stored tree is not one. policy: AutoQueuePolicy; runner: AutoRunnerStatus; pendingByLeaf: Record<string, number>; @@ -62,6 +74,18 @@ export type AutoQueueKindStatus = { // settings wholesale between tests while the pool keeps its pausedSnapshot. // Download has no live counterpart: its flag IS the gate, read at dispatch. held: boolean; + // THE TREE ABOVE IS GENERATED, so the editor for it is read-only and the + // channel priorities on /channels are where it is edited. False for a corpus + // with no priorities set, which is every corpus until one is. + policyCompiled: boolean; + // THE FOCUS BANNER'S NUMBERS, FOR THIS LANE. Always present and inert when + // `active` is false, so the banner is one component with one early return + // rather than a conditional on the payload. The counts are this lane's own — + // `focusPending`/`otherPending` differ per lane by construction. + focus: FocusSummary; + // The focus's display name, resolved on the server (the model stores a + // siteId, and the banner does no I/O). Null when nothing is focused. + focusName: string | null; }; export type AutoQueueStatusPayload = Record< @@ -80,9 +104,20 @@ export type AutoQueueStatusPayload = Record< lanes: AutoQueueLanesPayload; }; -async function buildKind(kind: AutoQueueKind): Promise<AutoQueueKindStatus> { +async function buildKind( + kind: AutoQueueKind, + priority: PriorityView, +): Promise<AutoQueueKindStatus> { const paths = getPaths(); - const policy = getSettings().autoQueue[kind]; + const stored = getSettings().autoQueue[kind]; + // The stored policy, with the DISPATCHED root in place of the stored one. + // Spread rather than rebuilt, so `held`, `snoozeUntil`, `enabled`, `order` + // and `maxWorkers` come through untouched — the same rule every writer of a + // policy in this repo follows. + const policy: AutoQueuePolicy = { + ...stored, + root: laneRootFor(priority, kind, stored), + }; const runner = getAutoRunnerStatus(kind); const state = await readAutoQueueState(paths); const pending = await computeLeafPending(kind, paths); @@ -114,6 +149,12 @@ async function buildKind(kind: AutoQueueKind): Promise<AutoQueueKindStatus> { kind === "transcription" ? getWorkerPool().isPaused() : isGateHeld(getSettings(), kind), + policyCompiled: priority.compiled, + // Keyed by COMPILED leaf id (`prio-focus-<slug>`), which is why this is + // computed here and not on the client: it is only meaningful against the + // counts of the tree the lane dispatches from. + focus: laneFocusSummary(priority, pending.counts), + focusName: priority.name, }; } @@ -121,8 +162,12 @@ async function buildKind(kind: AutoQueueKind): Promise<AutoQueueKindStatus> { // lane added to the model appears on this payload with no edit here, which is // the whole point of the widened type. export async function buildAutoQueueStatusPayload(): Promise<AutoQueueStatusPayload> { + // ONE RESOLUTION FOR ALL FOUR LANES. The focus set costs a channel listing + // and, for a site focus, a sites read; the four lanes compile from the same + // one, so resolving per lane would pay for it four times on a 3 s poll. + const priority = await readPriorityView(); const [kinds, lanes] = await Promise.all([ - Promise.all(LANES.map((lane) => buildKind(lane))), + Promise.all(LANES.map((lane) => buildKind(lane, priority))), buildAutoQueueLanes(), ]); return {