Archilyzer · Source

archilyzer

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

commit 26689d10b832efe9345baa20d62a27facfeb089e
parent 0186d2f4ab15cab7f3228b1dbc2292d0bb7e332d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 11 Sep 2026 11:24:38 -0400

plans: channel-priority gains per-operation overrides, and question 3 is answered

The operator's case: "I find the download on channels that sync to be an issue
(e.g. omnibased)" — a channel must be able to keep its playlist current while its
download lane is parked. channels[slug].overrides pins any of `sync` plus the four
lanes to a tier of its own, effectiveTier(model, slug, op) is what dispatch asks,
and the focus set stays one, corpus-wide.

The consequence worth the change: the migration becomes LOSSLESS. excludeFromSync
meant "stop syncing", not "stop everything", so it maps to {sync: "paused"} with
the base tier and the rank untouched — omnivods-odysee keeps downloading, no lane's
membership moves, and open question 3 (which of the 15 excluded channels are really
meant to be paused everywhere) needs no list from the operator: none of them are, by
construction. The order collapse is now the migration's only behaviour change.

Also recorded: the four decisions taken (Q1 collapse, Q2 a pause never stops a
manual run, Q3 above, Q5 a focus never raises a lane's enabled), S1/S2 reading the
effective tier per operation, S3's advanced disclosure plus a "Sync only" preset,
and where the sanitizer round-trip test has to live.

S0 is implemented as of this branch.

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

Diffstat:
Mplans/channel-priority.md | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 122 insertions(+), 41 deletions(-)

diff --git a/plans/channel-priority.md b/plans/channel-priority.md @@ -1,6 +1,15 @@ # Channel priority — one tier per channel, one focus, four compiled trees -**Status:** design only, nothing implemented. Anchors are at `61eae05` on `one-core/phase-1`. +**Status:** **S0 (the contract) is implemented** on branch `channel-priority/s0`, off `e74f005`. +S1-S5 are design only. Anchors are at `61eae05` on `one-core/phase-1`. + +Operator decisions taken, numbered as the Open questions section below: **Q1 — collapse** the +two live per-lane channel orders to ONE (the model has one rank per channel). **Q2 — no**, a +Paused channel still runs a MANUAL run; paused gates the sync scheduler, *Sync all* and the +four auto lanes, nothing else. **Q3 — resolved by design**, by the per-operation override map +(see the Model): all 15 migrate to a `sync` override and no lane's membership moves. **Q5 — +no**, activating a focus never changes any lane's `enabled`. Q4 is still open and does not +affect S0. The ask: *"focus on one group of channels (e.g. jeralyzer) and pause all others until the priority channels are done … rip out the current enable/disable feature on the channels page @@ -107,13 +116,17 @@ runner's channel list, not a tree shape.** 30 s TTL (`CHANNEL_LIST_TTL_MS`, `:141`). Filtering Paused there makes a paused channel invisible to all four lanes, catch-all included, in one predicate. -4. **`excludeFromSync` dissolves into the Paused tier; `excludeFromBuild` stays.** They are - different axes: one is scheduling, one is publishing, and the brief's own rule is that the - lowest tier must not gate export. The `/channels` row keeps its Build toggle and loses its - Sync toggle. +4. **`excludeFromSync` dissolves into a `sync` OVERRIDE, not into the Paused tier; + `excludeFromBuild` stays.** The three are different axes: one is the sync cadence, one is + dispatch, one is publishing, and the brief's own rule is that the lowest tier must not gate + export. A per-operation override says "stop syncing, keep everything else" exactly, so the + migration moves no lane's membership. The `/channels` row keeps its Build toggle and loses + its Sync toggle. -5. **No drag-and-drop, no per-lane overrides.** Tier is a `<select>`; intra-tier order is an - optional integer `rank` (migration seeds it; ties fall back to slug order). +5. **No drag-and-drop, and no per-lane RANK.** Tier is a `<select>`; intra-tier order is an + optional integer `rank` (migration seeds it; ties fall back to slug order), and there is + ONE rank per channel. Per-operation *tier* overrides do exist (above) — they change which + lane a channel is on, never its order within one. ## The model @@ -124,6 +137,13 @@ bars `lib → controller|jobs`). ```ts export const CHANNEL_TIERS = ["focus", "normal", "low", "paused"] as const; export type ChannelTier = (typeof CHANNEL_TIERS)[number]; +// What may be STORED on a channel — "focus" is a compiled POSITION, never a stored tier. +export const STORED_CHANNEL_TIERS = ["normal", "low", "paused"] as const; + +// The operations a tier can be pinned to: the four lanes plus `sync` (which is +// not a lane — it is a per-channel cadence, the answer pauseLaneFor already gives it). +export const PRIORITY_OPERATIONS = ["sync", ...LANES] as const; +export type PriorityOperation = (typeof PRIORITY_OPERATIONS)[number]; export type ChannelFocus = | { kind: "none" } @@ -133,10 +153,42 @@ export type ChannelFocus = export type ChannelPriority = { focus: ChannelFocus; // Only channels that differ from the default appear. Absent slug = normal, no rank. - channels: Record<string, { tier: ChannelTier; rank?: number }>; + channels: Record<string, { + tier: StoredChannelTier; + rank?: number; + // PER-OPERATION OVERRIDES of the base tier. Only operations that DIFFER appear. + overrides?: Partial<Record<PriorityOperation, StoredChannelTier>>; + }>; }; ``` +**Per-operation overrides are the advanced half, and they are what makes the migration +lossless.** The operator's case: *"I find the download on channels that sync to be an issue +(e.g. omnibased)"* — a channel must be able to keep its playlist and metadata current while +its download lane is parked, and in general any operation may sit at a different tier than +the channel's base. That is also precisely what `excludeFromSync` meant, read the other way +round, so the flag this model replaces becomes `{tier: "normal", overrides: {sync: "paused"}}` +and nothing else moves. + +- `effectiveTier(model, slug, op)` = `overrides?.[op] ?? tier`, and **every dispatch-side + question asks it**: the compiler per lane, `listChannelMeta` for the lane it is listing, + the sync scheduler for `"sync"`. `tierOf` stays the BASE tier and is the display answer. +- **The focus set stays ONE, corpus-wide.** A focused channel whose override for some + operation is `paused` is simply not drawn for that operation — focus wins over the stored + tier, paused wins over focus, per operation. +- **Two presets, each the other's inverse.** `{tier:"normal", overrides:{sync:"paused"}}` is + "everything but sync"; `{tier:"paused", overrides:{sync:"normal"}}` is "sync only". +- **Sanitizer normalisation:** an unknown operation key is dropped; an unknown tier VALUE is + dropped rather than coerced (coercing to `normal` would silently unpause an operation on a + paused channel — an override's job is to differ from the base, so a junk one falls back to + the base); an override equal to the base is normalised away, and an empty map with it; a + `rank` on a channel paused for *every* operation is dropped, while a "sync only" channel + keeps one because the sync scheduler still orders it. Keys are emitted in + `PRIORITY_OPERATIONS` order and slugs sorted, so the on-disk document has a stable diff. +- **The four compiled trees are identical unless an override moves a channel.** One rank per + channel means the only thing that can differ between lanes is membership, and the only + thing that changes membership is an override. + - `focus: {kind:"site"}` is the first-class answer to *"focus = the channels of site X"*, and it is resolved at compile time against `transcripts/sites/<id>/site.json`'s `channels[]`, so it tracks membership rather than freezing a list. `{kind:"channels"}` backs "Focus these". @@ -194,9 +246,11 @@ noise. `flattenLeaves`/`hasWork` walk ~69 nodes per pick. for the banner, and `channelPriorityFromLegacy(configs, autoQueue)` for the migration. 2. **`common/controller/autoRunner.ts:288-296`** — `listChannelMeta` drops - `tierOf(priority, slug) === "paused"`. One predicate; both the runner loop and - `computeLeafPending` inherit it, and the 30 s TTL (`:141`) is the re-evaluation clock. - *Nothing else in dispatch changes.* + `isChannelPaused(priority, slug, lane)` — the EFFECTIVE tier for the lane being listed, + so a channel paused for `download` and normal for `transcription` is absent from one and + present in the other. One predicate; both the runner loop and `computeLeafPending` + inherit it, and the 30 s TTL (`:141`) is the re-evaluation clock. *Nothing else in + dispatch changes.* 3. **A new idle reason is NOT added.** A lane whose focus group holds the rest is not idle — it is dispatching focus work. When the whole tree is empty the existing `no-pending` @@ -206,12 +260,15 @@ noise. `flattenLeaves`/`hasWork` walk ~69 nodes per pick. 4. **Sync** — `common/jobs/syncScheduler.ts`: - `selectDueChannels` takes the model. The `excludeFromSync` skip at `:99` becomes - `tierOf(priority, slug) === "paused"`, with the same silent `continue`. + `isChannelPaused(priority, slug, "sync")` — the effective tier for the `sync` + operation, which is exactly what the flag meant — with the same silent `continue`. - The sort at `:125` becomes **tier rank, then rank, then `overdueMs` descending**: - `focus < normal < low`. Most-overdue-first survives *within* a tier, so a focus channel + `focus < normal < low`, all read through `effectiveTier(·, "sync")`. Most-overdue-first + survives *within* a tier, so a focus channel due by a minute outranks a low channel due by a day, and the tick's cap (`editor/app/scheduler/runTick.ts:127-131`) therefore spends its slots on focus first. - `autoSyncEligible` (`:193-197`) follows the same predicate. + - `tierOrder(tier)` in `channelPriority.ts` is the one sortable form of the tier order. - `syncAllChannelsAction` (`editor/app/channels/actions.ts:466-472`) swaps its `excludeFromSync` skip for the same one and sorts the candidate list the same way. @@ -223,6 +280,10 @@ noise. `flattenLeaves`/`hasWork` walk ~69 nodes per pick. and `BulkCadenceBar.tsx:37,41` verbatim rather than inventing an idiom. Bulk actions: **Set tier**, **Focus these**, and a **Focus site: `<id>`** menu built from `listSites()` (no selection needed). + - **Advanced: per-operation selects behind a disclosure**, beside the tier control, plus a + **Sync only** preset (and its inverse, which is what every migrated `excludeFromSync` + channel already carries). The row shows the base tier and a marker when any operation is + pinned; the disclosure is where the five operations are set. - Sort key `sync` (`ChannelsTable.tsx:54-63,129-163`) becomes `tier` (tier order, then rank, then slug). Row dimming (`:406-411`) keys off `tier === "paused"`. - `channelGroupSections.ts:124`'s excluded-from-sync section becomes the Paused section. @@ -264,20 +325,28 @@ Live data: **15 of 68 channels carry `excludeFromSync: true`** — `community-no `exclusively-games`, `rcflightschool`, `rcspotlight`, `teamrcn`, `the-incredible-salt-mine`, `steven-crowder`, `midwestly`, `redbar` — and **zero** carry `excludeFromBuild`. One of the 15, `omnivods-odysee`, is also the 8th ranked leaf of the live -download tree: today it is excluded from sync yet still drawn by the download lane. **Paused -wins** — that is the whole point of a unified model — so migrating it is a real behaviour -change for that one channel and must be named in the commit. `community-notes` is a jeralyzer -channel, so a jeralyzer focus will not resurrect it either: paused is applied before the tree -is consulted. +download tree: today it is excluded from sync yet still drawn by the download lane. + +**The mapping is LOSSLESS, and that is what the override map is for.** `excludeFromSync` +meant "stop syncing", not "stop everything", so it becomes +`{tier: <base>, overrides: {sync: "paused"}}` — the base tier stays `normal` and the rank +(if the channel has one) stands. `omnivods-odysee` keeps downloading exactly as it does +today; **no lane's membership moves**, which makes the migration a settings rewrite rather +than a behaviour change. An operator who wants one of the 15 paused outright sets its base +tier afterwards, deliberately, on `/channels`. - **Pure function**, `channelPriorityFromLegacy(configs, autoQueue): ChannelPriority`, in `common/lib/channelPriority.ts`, in the `laneMigration.ts` style (no I/O, idempotent over its own output, asserted through `sanitizeChannelPriority`): - - `config.excludeFromSync === true` → `{tier: "paused"}`, checked **first**, so a paused - channel that also has a lane leaf (`omnivods-odysee`) is paused and keeps no rank. + - `config.excludeFromSync === true` → `overrides: {sync: "paused"}` on the channel's entry, + never a base tier. A channel that also has a lane leaf (`omnivods-odysee`) keeps its rank + and every lane it was drawn by. - A channel named by a bare channel leaf in **either** the download or the transcription root - → `{tier: "normal", rank: <the lower of its two leaf indices>}`. Channels ranked in one - lane only keep that lane's index. + → `{tier: "normal", rank: <the lower of its two indices>}`, where the index is its + position among that root's **bare channel leaves** (a dense order; identical to the leaf + index when every leaf is bare, which all 22 live leaves are — so a bucket or operation + leaf the model cannot express leaves no hole). Channels ranked in one lane only keep that + lane's index. - Everything else → absent (normal, unranked). - `focus: {kind: "none"}`. - **It is NOT run from `getSettings`.** That function is synchronous and reads one file; the @@ -285,9 +354,9 @@ is consulted. `common/bin/migrate-channel-priority.ts`, run offline with `tsx` (never a second editor), that reads the configs and `settings.json`, writes `channelPriority`, recompiles the four roots and prints a diff. Idempotent: a second run is a no-op because the document already exists. -- **The collapse is a behaviour change and must be measured.** Six channels are ranked in one - lane only, and the three Quartering channels are in different orders in the two lanes; after - the migration both lanes get one order. Gate it with `plans/tools/phase1-numbers.ts` +- **The order collapse is the migration's ONE behaviour change, and must be measured.** Six + channels are ranked in one lane only, and the three Quartering channels are in different + orders in the two lanes; after the migration both lanes get one order. Gate it with `plans/tools/phase1-numbers.ts` before/after over the live corpus, the way every Phase 1 slice was gated — the `*_leaves` lines will move by construction, so the number that must not move is each lane's total pending count. @@ -308,7 +377,9 @@ is consulted. corpus); channel focus keeps order and drops unknown slugs. 3. `compileLaneRoot`: group order focus → normal → low → catch-all-last; paused slugs never appear; rank ordering with unranked tail; ids are `prio-*`; an empty focus emits no focus - group; the output survives `sanitizeAutoQueue` unchanged. + group; a per-operation override moves a channel on **one** lane's tree and not the others. + The half that must import the engine — *the output survives `sanitizeAutoQueue` + unchanged*, with no id reassigned — is `jobs/channelPrioritySanitize.test.ts`. 4. **The focus/hold/done transition, asserted through the real engine**: build two `ChannelWork` objects, compile a tree with channel A focused, and drive `buildPendingByLeaf` + `selectNextWork` — every pick is A's while A has work; the first pick after A's list is @@ -317,7 +388,9 @@ is consulted. in `lib/`, because it must import `jobs/autoQueuePolicy` and `architecture.test.ts` forbids `lib/ → jobs/` — the same reason `laneMigration.test.ts` sits in `jobs/`. 5. `channelPriorityFromLegacy`: the live shape (9+9 leaves, 6 one-lane channels, 3 reordered); - an `excludeFromSync` config; a file with the document already present (untouched). + an `excludeFromSync` config becoming a `sync` override only; and **the 15-channel case + round-tripping with no lane change** — every channel still on every lane's compiled tree, + only the `sync` operation losing anyone. 6. Sync ordering: `selectDueChannels` with one focus channel 1 min overdue and one low channel 1 day overdue returns the focus channel first; a paused channel is never due. @@ -347,14 +420,20 @@ entry may be added**), and the named spec files. ## Slices **S0 — the contract (lands first, small, blocks everything).** Branch `channel-priority/s0`. -Files: `common/lib/channelPriority.ts` (all types, sanitizer, resolver, compiler, legacy -function — the compiler may return a placeholder only if fully typed), the sanitizer wired into +Files: `common/lib/channelPriority.ts` (all types including the per-operation overrides, the +sanitizer, `effectiveTier`/`isChannelPaused`/`channelsForOperation`, the focus resolver, the +compiler, `focusSummary` and the legacy function), the sanitizer wired into `getSettings` (`settings.ts:~1396`) and `saveSettings` (`:1613-1620`), `SiteSettings` gains -`channelPriority`, plus **empty-but-exported stubs** for the files S3/S4 import: -`editor/app/channels/components/ChannelTierSelect.tsx` and -`editor/app/channels/components/FocusBanner.tsx`. Tests 1-3 and 5. One commit. Nothing reads the +`channelPriority` (and `editor/app/settings/actions.ts` PRESERVES it, the way it preserves +`autoQueue` — rebuilding it would undo a focus), plus **empty-but-exported stubs** for the +files S3/S4 import: `editor/app/channels/components/ChannelTierSelect.tsx` and +`editor/app/channels/components/FocusBanner.tsx`. Tests 1-3 and 5. Nothing reads the model yet, so the live tree is untouched and no number moves. +The sanitizer round-trip half of test 3 lives in **`common/jobs/channelPrioritySanitize.test.ts`**, +not in `lib/`: `architecture.test.ts` scans test files like any other and forbids `lib/ -> jobs/`, +and its ALLOWED list may only shrink. Same reason `laneMigration.test.ts` sits in `jobs/`. + **S1 — dispatch.** Branch `channel-priority/s1`. Only `common/controller/autoRunner.ts` (`listChannelMeta` paused filter) + `common/jobs/channelPriorityCompile.test.ts` (test 4). @@ -389,26 +468,28 @@ plus `channels*.spec.ts`, and the full suite runs once at the merge. ## Out of scope -Per-lane priority overrides; drag-and-drop ordering; time-boxed focus ("focus until Friday"); +Per-lane *rank* (a channel has one order, not four); drag-and-drop ordering; time-boxed focus ("focus until Friday"); auto-ending a focus when its work hits zero (the banner reports it, the operator ends it); `excludeFromBuild` and `excludeFromCleanup`; the site-membership editor; any change to `operationBatch`, `laneLimit`, `pauseGates`, the `held` keys or `.auto-queue/state.json`. ## Open questions for the operator -1. **The two live trees disagree on six channels and on the Quartering ordering.** Collapse to - one order (recommended — it is the point of the feature), or keep a per-lane `rank`? -2. **Does Paused stop a *manual* run?** Recommended: **no**. Paused gates the sync scheduler, +1. ~~**The two live trees disagree on six channels and on the Quartering ordering.** Collapse to + one order, or keep a per-lane `rank`?~~ **DECIDED: collapse.** One rank per channel. +2. **Does Paused stop a *manual* run? DECIDED: no.** Paused gates the sync scheduler, *Sync all*, and all four auto lanes; the per-video and per-channel Run buttons (`runDigestChannelJob`, `runBackfillChannelJob`, the pipeline actions) still work, with a "Paused — automatic work is off for this channel" badge on the channel page. -3. **Are all 15 `excludeFromSync` channels really meant to be Paused everywhere?** Under the - new model they stop being downloaded and transcribed too, not just synced. If some of them - were only meant to stop *syncing* while still finishing a backlog, they want `low`, not - `paused`, and the migration needs that list. +3. ~~**Are all 15 `excludeFromSync` channels really meant to be Paused everywhere?**~~ + **RESOLVED BY DESIGN (per-operation overrides).** The question only existed because the + first model could not say "stop syncing, keep everything else". It can now: all 15 migrate + to `overrides: {sync: "paused"}` with their base tier and rank untouched, no lane's + membership moves, and no list from the operator is needed. Pausing one outright is a + deliberate later edit on `/channels`. 4. **Do the 5 channels in no site belong in a tier by default?** They are normal today; a site-scoped focus will hold them like any other non-focus channel. -5. **Should a Focus also raise the lane's `enabled`?** Recommended: no — focusing must not +5. **Should a Focus also raise the lane's `enabled`? DECIDED: no** — focusing must not start a stopped lane, for the same reason `saveAutoQueueAction` refuses to unhold one. ## Roadmap placement — a recommendation, not applied here