commit 990e1cd3ad3d97fedeebb8b6c5c69f3479cb15ec
parent 56e1a4b41ea39c16667f6ec19da18088650d071d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 26 Aug 2026 18:57:58 -0400
plans: unified-ops step 1 shipped
- unified-operations-model.md: step 1 DONE, with what actually shipped (a deletion,
not a rewiring — the migration was already complete on disk, 68/68, so no rendered
number moved and the coverage percentage the step warned about had already changed).
The header no longer says "designed, not built": four of six steps are done.
- FACTS.md: a dated section with the measurements (68/68; bucket 59,159 vs entry
47,382; the delta is `deferred` on 65/68 and the three exceptions; corpus `deferred`
16,156 against 1 on 08-10), what each surface reads now, `DigestWork` without
`source`, the four deleted branches, and the "no snapshot schema version" convention.
The "44 of 66 snapshots" line is marked superseded.
- STATE.md: header, a dated entry with the decisions (no regen, no CLI, view-model names
kept, transcode band deliberately not taken), and a Recommended-next list where step 1
is done, slice 4 is unblocked, and the `deferred` finding is the new #2. The
"STEP 4 IS NOT DONE AND CANNOT BE DONE FROM HERE" paragraph is corrected in place with
a dated note rather than deleted — it got done on its own.
- editor-operations-ia.md: slice 4's dependency satisfied; the Transcode row records that
its "with step 1" coupling is void and its band waits on its own snapshot-shape change.
- editor/CHANGELOG.md: operator-facing — the report no longer carries the second digest
list, every screen reads the registry's entry, and no count on any page changes.
- Memory: slice-3-chosen-next updated to step-1-shipped, with the `deferred` finding.
Diffstat:
5 files changed, 184 insertions(+), 22 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **"How many videos still need a digest" now has exactly one answer.** A channel's report used to carry two: a plain list of videos with no current digest, and the operation registry's own entry — the one the digest run itself dispatches from. They were built from different rules and disagreed by **11,777 videos** across this archive, because only the registry asks whether a video has a transcript at all and whether its normalized cue file is current. The plain list is gone from the report; every screen that shows a digest figure — the dashboard's *needs work* card, the `/channels` Digest column, a channel's Digest stage and its transit line, the pipelines band and the monitor widget — reads the registry's entry, which is what all of them were already doing. **No count on any page changes**, and nothing published changes: an exported site never reads a channel report. Reports written before this keep an unused key until the next time they are regenerated; nothing needs to be re-run.
- **An operation's settings are on its own page now.** Digest, Diarization and Speaker attribution each moved off **Settings** and onto that operation's page under **Operations**, next to its backlog, its sweep and its pause — so switching an operation on and seeing what it would do are no longer two different screens. The **Speaker work lane** settings (whether the lane runs, its resource share, its concurrency, and re-acquiring deleted media) moved too, and appear on *all three* speaker operations' pages, because those operations genuinely share one queue: change it in one place and you have changed it for the others. Each block has its own **Save** button naming what it saves — *Save digest settings*, *Save lane settings*, and so on — so saving one can no longer touch another. Nothing on disk changed and no setting was renamed; the values you had are where you left them. **Settings** keeps the machine and the site: transcription workers, the sync scheduler, the build pipeline, social links and system paths, plus a line pointing at the four pages.
- **The word "backfill" now means one thing: the shared queue.** It had been doing double duty — naming the queue that diarization, speaker attribution and the digest share, *and* standing in for each of those operations wherever a screen had no better word. The `/channels` group button that read **Backfill** is named after the operations it runs: **Speakers** once a speaker operation is switched on, **Derived data** while none is — the same derived label the channel page's stage already carried, so the button never claims work its lane is not doing. The `/actionable` section and every row of the channel page's Speakers card are labelled by the operations, not the queue, and the count column there says *reachable* rather than *to backfill*. *Pause backfill*, *Resume backfill* and *Start backfill sweep* keep their names, because those act on the queue itself. Under the hood the operation registry is `operations.ts` and its types say *Operation*, not *BackfillKind*; nothing stored on disk changed.
- **Transcode is in the operation registry, and `/operations/transcode` exists.** It had a state per video, a lane and a dependency, and was the one media operation the catalog did not know. It is registered as an operation nothing in the auto-queue drives — its page says so, names Download as what has to happen first, and points at the channel page's Transcode stage and the video page as what does dispatch it. The registry also now records *which channels an operation applies to*: a `youtube`-handling channel or one with no audio format never has transcode work, and the three screens that each spelled that rule for themselves read it from the registry instead. The Transcode stage and the `/channels` columns are unchanged; a transcode band on the pipelines rail waits until a channel's report can carry it.
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -1985,7 +1985,7 @@ binary, and why the scope comparison at `backfillSweep.ts:382` was repeatedly mi
| new reachable | **75,199** |
| old `noDigest` (54 channels that have the bucket) | 75,613 |
| `present` (digested at the current identity) | 122 |
-| snapshots with a `backfill` block | 44 of 66 |
+| snapshots with a `backfill` block | 44 of 66 — **SUPERSEDED 2026-08-26: 68 of 68**, see the step-1 section at the end of this file |
| channels predating the `noDigest` bucket | 11 (~1,516 videos) |
**CORRECTED 2026-08-10 — `deferred` was NEVER mostly "stale cues.json".** A per-video walk of
@@ -2361,3 +2361,86 @@ the next reader to the wrong file.
of `settings.spec.ts` plus four new ones — attribution and the lane had no form-driving
spec at all before. `settings.spec.ts` is 168 lines and still has exactly one `<form>` on
the page, so its `page.locator("form")` selectors stay unambiguous.
+
+---
+
+## Verified 2026-08-26 — unified-ops step 1 (`buckets.noDigest` deleted)
+
+Measured read-only over `transcripts/channels/*/snapshot.json` with `node -e`, no editor
+booted, nothing written under `transcripts/`.
+
+**The migration the fallback existed for was already over.**
+
+| measure | value |
+| --- | --- |
+| snapshots on disk | **68** |
+| carrying `backfill.digest` | **68 of 68** (and all 68 carry a numeric `eligible`) |
+| `generatedAt` range | 2026-08-11T22:18Z → 2026-08-26T20:39Z |
+| Σ `buckets.noDigest` | **59,159** |
+| Σ `backfill.digest.ids` | **47,382** |
+| Σ `backfill.digest.deferred` | **16,156** |
+| Σ `backfill.digest.blocked` | **1,685** |
+| Σ `backfill.digest.partial` | **0** |
+
+So the `source: "bucket"` branch in `digestWorkOf()` was dead against production, and no
+rendered number moved when it was deleted. **This supersedes "snapshots with a `backfill`
+block | 44 of 66"** in the 2026-08-09 table above, and STATE.md's "STEP 4 IS NOT DONE AND
+CANNOT BE DONE FROM HERE" (dated note added there): the live editor was restarted on the new
+build some time after 08-10 and the snapshot scheduler regenerated the rest.
+
+**The two counters disagreed by 11,777 videos and could not be reconciled.** On 65 of 68
+channels `noDigest − digest.ids` is EXACTLY `deferred` — the registry gates on a current
+`cues.json`, the bucket did not. The three exceptions are videos with a fresh digest but no
+current `cues.json`, which the registry calls `deferred` and the bucket called done:
+
+| channel | `noDigest − ids` | `deferred` |
+| --- | --- | --- |
+| `HasanAbiVODs3` | 260 | 348 |
+| `chibi-reviews` | 34 | 2,699 |
+| `destiny` | 2,243 | 3,869 |
+
+The bucket also had no transcript gate (it silently omitted untranscribed videos — the
+regression `stageStatus.ts` records as "how a channel of untranscribed videos read as fully
+digested") and no `partial`.
+
+**What each surface reads now** — all of them already did, through `digestWorkOf`, which is
+now a pure read of `snapshot.backfill.digest`:
+
+- dashboard / `NeedsWorkPanel` / `ChannelsTable` — `actionableNoDigestCount`
+ (`editor/app/actionable/lib/loadActionable.ts`), `.reachable`.
+- `/api/widget/sync` — `eligible − blocked` summed corpus-wide, `blocked` and `deferred`
+ reported separately, all `number | null`.
+- `/api/widget/actionable` — reports `noDigest` (the wire field name is unchanged) and still
+ REFUSES to filter on it: during the backfill it is ~99.87% of the corpus.
+- the channel station (`channelFlow.ts`), the Digest stage card (`stageStatus.ts`,
+ `[slug]/page.tsx`), the `/channels` digest column (`channelGroupSections.ts`), the
+ pipelines bands (`components/pipelines/buildBands.ts`).
+
+**`DigestWork` no longer has `source`.** Its shape is `ids / reachable / blocked / deferred /
+partial / present / eligible`; the no-entry return is `present: null, eligible: null` and
+everything else 0/[] — "unknown, not zero coverage", which
+`editor/app/api/widget/sync/route.ts` builds its null rule on. A snapshot with no entry is now
+a channel whose FIRST snapshot has not been written, not an old generation of one.
+
+**Four "digest is special" branches deleted**, each of which existed only to cover a snapshot
+with no entry: `components/pipelines/buildBands.ts`, `controller/sweepPreview.ts`
+(`reachableIdsFor` and `countsFor`), `controller/sweepRecency.ts`. Digest is now a plain
+registry entry to every reader, exactly as diarization is: no entry → no work known, coverage
+unknown. `digestWorkOf` has **no callers in `common/`** and stays in `channelSnapshot.ts` as
+the editor's adapter.
+
+**No snapshot schema version exists**, and none was added: per-field optionality is the
+convention (`presentOperationWork` returning `null` for a snapshot predating `eligible` is the
+precedent). `normalizeBuckets` picks only the keys it knows, so an old snapshot's stray
+`noDigest` key is ignored until its next routine regen. No regeneration was run and no regen
+tooling was written: an offline writer would race the live editor's `snapshotScheduler` on the
+same files.
+
+**An operator finding, not fixed here: corpus-wide `deferred` is 16,156.** It was measured at
+**1** on 2026-08-10, right after the normalize pass ("Phase D step 1" above: 1,987 → 1). The
+largest holders are `destiny` 3,869, `the-quartering` 3,762, `chibi-reviews` 2,699,
+`nux-taku` 1,802, `leaflit` 1,612, `kirsche` 1,197. Either new `handling: "youtube"`
+downloads have accumulated with no `cues.json` (nothing automatic runs `normalizeTranscript`
+for a subtitle-downloading channel) or the cues gate regressed. It is exactly what the Digest
+card's **Normalize transcripts** button exists for, and it was invisible to a reader of the
+old bucket.
diff --git a/plans/STATE.md b/plans/STATE.md
@@ -3,7 +3,11 @@
The working memory for the local-AI derived-corpus work. Rewritten at the end of every
session, before context is cleared. See [`README.md`](README.md) for the protocol.
-**Last updated:** 2026-08-26 (evening) — **editor IA slice 3 landed**: an operation's settings live on its operation page, and the four `*FormPresent` markers are gone. See the dated section below. Earlier the same day: **the vocabulary pass landed**: the registry is
+**Last updated:** 2026-08-26 (late) — **unified-ops step 1 landed**: `buckets.noDigest` is
+deleted and `snapshot.backfill.digest` is the one digest work list. No rendered number moved
+(the fallback's migration was already complete on disk, 68/68), and **IA slice 4 is now
+unblocked**. One finding for the operator: corpus-wide `deferred` is **16,156**, against 1 on
+08-10 — see the dated section below. Earlier the same day: **editor IA slice 3 landed**: an operation's settings live on its operation page, and the four `*FormPresent` markers are gone. See the dated section below. Earlier the same day: **the vocabulary pass landed**: the registry is
`operations.ts` and says `Operation`, the three id spaces slice 2 handed it say "speakers",
"unit" has one meaning, and `transcode` is a registry entry. See the dated section below.
Earlier the same day: editor IA slice 2 (speakers are a real stage; one runner path) landed,
@@ -142,21 +146,79 @@ nothing renders.
the vocabulary pass to keep them out of this slice's e2e risk. *(Done later the same day —
they say "speakers"; see the entry above.)*
-**Recommended next**, with slice 3 (the old #6) now shipped, so **#2 is the one to take**:
+**Recommended next**, with unified-ops step 1 (the old #2) now shipped:
1. **GPU yield on a quiet box** — still the gate on arming the sweep, still unmeasured.
-2. **Unified-ops step 1**: collapse `noDigest` into `snapshot.backfill.digest`. **Slice 4
- needs this**, and slice 3's plan named it as what follows. This is the next piece.
-3. **Relocate `omnimirror`'s media** once the platter is mounted (131 GB off a 94%-full SSD).
-4. **Phase 6 Ollama `/ask`** — genuinely independent; a good parallel task.
-5. Decide `attribution-text`'s fate on that one channel: ~194,000 chunk-level calls is a
+2. **Run Normalize transcripts over the 16,156 `deferred` videos** — NEW, and the one thing
+ this session found rather than built. Corpus `deferred` was **1** on 2026-08-10 after the
+ normalize pass and is **16,156** today (`destiny` 3,869, `the-quartering` 3,762,
+ `chibi-reviews` 2,699, `nux-taku` 1,802, `leaflit` 1,612, `kirsche` 1,197). Those videos
+ have a transcript and no current `cues.json`, so the digest lane will not touch them and
+ nothing automatic will fix it: `transcribeOne.ts` is the only automatic caller of
+ `normalizeTranscript` and a `handling: "youtube"` channel downloads subtitles with
+ `--skip-download`. **Decide whether this is accumulation or a regression before running
+ anything** — if it is accumulation, the missing piece is a normalize step on the
+ subtitle-download path, and re-running normalize by hand only defers the question.
+3. **Editor IA slice 4** (`/actionable` dissolves) — **UNBLOCKED**: it wanted the per-state
+ split (`blocked`, `deferred`, `partial`) and the coverage pair (`eligible`, `present`),
+ which is what `snapshot.backfill.digest` carries and the deleted bucket did not.
+4. **Relocate `omnimirror`'s media** once the platter is mounted (131 GB off a 94%-full SSD).
+5. **Phase 6 Ollama `/ask`** — genuinely independent; a good parallel task.
+6. Decide `attribution-text`'s fate on that one channel: ~194,000 chunk-level calls is a
sweep-sized commitment that was never priced.
-6. ~~**Editor IA slice 3** (per-operation settings)~~ — **DONE 2026-08-26**, `9515083` →
+7. ~~**Unified-ops step 1**~~ — **DONE 2026-08-26**, `efb0cf9` → `00b1c8a`. Plan:
+ [`unified-ops-step-1.md`](unified-ops-step-1.md); outcome: the dated entry below and the
+ FACTS section "Verified 2026-08-26 — unified-ops step 1".
+8. ~~**Editor IA slice 3** (per-operation settings)~~ — **DONE 2026-08-26**, `9515083` →
`43bb519`. Plan: [`editor-ia-slice-3.md`](editor-ia-slice-3.md); outcome: the dated entry
below and "Slice 3, as shipped" in `editor-operations-ia.md`.
---
+### 2026-08-26 (late) — unified-ops step 1: the digest work list has one definition
+
+Three commits: `1db30c3` (the plan), `efb0cf9` (the deletion), `00b1c8a` (de-specialising the
+readers), plus this docs commit. Plan: [`unified-ops-step-1.md`](unified-ops-step-1.md);
+measurements in [`FACTS.md`](FACTS.md) under "Verified 2026-08-26 — unified-ops step 1".
+
+1. **The migration was already over, so the step was a deletion, not a rewiring.** All **68 of
+ 68** snapshots on disk carry `backfill.digest` with a numeric `eligible` (`generatedAt`
+ 08-11 → 08-26), so `digestWorkOf`'s `"bucket"` branch was reading nothing. **No rendered
+ number moved.** The "published coverage percentage will change" warning in the step's own
+ text had already happened, silently, between 08-11 and 08-26.
+2. **The two counters disagreed by 11,777 videos** (bucket 59,159 vs entry 47,382) and could
+ not have been reconciled: on 65 of 68 channels the delta is exactly `deferred`, because the
+ registry gates on a current `cues.json` and the bucket did not. Deleting the bucket is what
+ makes there be one definition — the registry's `state()`, which is also what the runner
+ dispatches from.
+3. **No regeneration, and no regen CLI.** Old snapshots keep a stray `noDigest` key until
+ their next routine regen; `normalizeBuckets` picks only the keys it knows and there is no
+ snapshot schema version. An offline writer would race the live editor's
+ `snapshotScheduler` on the same files, and there is nothing it would need to do.
+4. **The `noDigest` view-model names were deliberately kept** — `DashboardChannel.noDigest`,
+ the `/api/widget/actionable` wire field, `actionableNoDigestCount`, `DigestStage`'s
+ `noDigestIds`. They mean "videos with no current digest", which is still what they hold;
+ renaming a wire field is its own change.
+5. **The transcode band was NOT taken with this step.** Three docs filed it "with step 1" only
+ because both were thought to need one regeneration of the snapshots; step 1 needs none, so
+ the coupling is void. It is its own snapshot-shape change (the writer recording a transcode
+ population, `appliesTo(config)` and a kept-media denominator) and its own plan.
+6. **Digest is no longer special to any reader.** Four `id === DIGEST_OPERATION_ID` fallbacks
+ went (`buildBands.ts`, `sweepPreview.ts` ×2, `sweepRecency.ts`). No entry now means for
+ digest what it means for diarization: no work known, coverage unknown — an unfilled band
+ outline, never a filled 0 %.
+
+**THE FINDING TO ACT ON: corpus-wide `deferred` is 16,156.** It was **1** on 2026-08-10, right
+after the normalize pass. `destiny` 3,869, `the-quartering` 3,762, `chibi-reviews` 2,699,
+`nux-taku` 1,802, `leaflit` 1,612, `kirsche` 1,197. Either `handling: "youtube"` downloads
+have accumulated with no `cues.json` (nothing automatic runs `normalizeTranscript` for a
+subtitle-downloading channel) or the cues gate regressed. That is 16,156 videos the digest
+lane will not touch, and it was invisible to a reader of the old bucket. The fix is the Digest
+card's **Normalize transcripts** button, run deliberately — deliberately NOT run from this
+slice.
+
+---
+
### 2026-08-26 (evening) — editor IA slice 3: an operation's settings are on its page
Five commits: `9515083` (the plan, with the two uncommitted doc files), `f14ceb5` (the
@@ -476,6 +538,12 @@ the new build, then a regen. The `noDigest` fallback in `digestWorkOf()` is what
interim safe: an un-regenerated channel reports its old bucket rather than reading as fully
digested.
+> **STALE — corrected 2026-08-26.** It got done on its own. The live editor was restarted on
+> the new build some time after 08-10 and the snapshot scheduler regenerated the rest: **68 of
+> 68** snapshots carry `backfill.digest` (and a numeric `eligible`), `generatedAt` spanning
+> 08-11 → 08-26. So the fallback was reading nothing, and unified-ops step 1 deleted it along
+> with the bucket — see the 2026-08-26 (late) entry.
+
**ALSO SHIPPED, ASKED FOR MID-SESSION: a diarization progress bar.** A `backfill` task got the
DOWNLOAD parser, which matches nothing in diarization output, so a 40-minute windowed
diarization showed an indeterminate bar while the job bar sat at "0/9 videos". `diarize-sherpa.py`
diff --git a/plans/editor-operations-ia.md b/plans/editor-operations-ia.md
@@ -57,7 +57,7 @@ and concludes the model was wrong.
| **Sync** | An operation, but channel-scoped and cadence-triggered rather than per-video and backlog-driven. Add `scope: "video" \| "channel"` and `trigger: "backlog" \| "cadence"` to `OperationDescriptor`; it gets a board row and `/operations/sync` (today's `/scheduler`) and keeps its own runner. The keep-latest checks and the saved-video backup that ride the same heartbeat (`editor/app/scheduler/runTick.ts:25-27`) are **Storage chores**, not sync, and should be labelled as such. |
| **Build / deploy** | **Not** operations. Per-site, no per-video state, no lane. They are the Site's *publish* verb → `/sites/[siteId]`. |
| **Cleanup / saved-videos / relocate** | **Not** operations: they consume outputs rather than producing derived artifacts. Third noun, **Storage**, filed under Machine. The channel `cleanup` stage stays where it is. |
-| **Transcode** | **Registered 2026-08-26** (the vocabulary pass, commit 3): an external descriptor, group `media`, `lane: { TRANSCRIPTION_QUEUE, cpu }`, `dependsOn: ["download"]`, no runner, and the registry's first `appliesTo(config)` — `handling === "transcribe" && !!audioFormat`, exposed as `operationApplies(id, config)` and replacing the three verbatim copies of that gate. `/operations/transcode` is slice 2's no-console panel. **Its BAND waits**: `buildOperationBands` is snapshot-only and pure, and `ChannelSnapshot` carries no `handling`/`audioFormat`, so a band could not tell "never transcodes" from "finished". The honest route is the snapshot writer recording a transcode population per channel — a snapshot-shape change that belongs with unified-ops step 1. `EXTERNAL_BAND_IDS` stays two until then. |
+| **Transcode** | **Registered 2026-08-26** (the vocabulary pass, commit 3): an external descriptor, group `media`, `lane: { TRANSCRIPTION_QUEUE, cpu }`, `dependsOn: ["download"]`, no runner, and the registry's first `appliesTo(config)` — `handling === "transcribe" && !!audioFormat`, exposed as `operationApplies(id, config)` and replacing the three verbatim copies of that gate. `/operations/transcode` is slice 2's no-console panel. **Its BAND waits**: `buildOperationBands` is snapshot-only and pure, and `ChannelSnapshot` carries no `handling`/`audioFormat`, so a band could not tell "never transcodes" from "finished". The honest route is the snapshot writer recording a transcode population per channel — a snapshot-shape change. **It was filed "with unified-ops step 1" and that coupling is void as of 2026-08-26**: both were thought to need one regeneration of the snapshots, and step 1 shipped needing none. This is now its own change and its own plan — the writer recording a transcode population (`appliesTo(config)` plus a kept-media denominator beside `buckets.untranscoded`) so `EXTERNAL_BAND_IDS` can go to three. It stays two until that lands. |
| **Social channels** | A channel whose operation set is `{fetch-posts}`. An explicit **non-goal** here: leave the short-circuit at `editor/app/channels/[slug]/page.tsx:156` alone. |
| **The channel stage list** | Half-derived and that is correct. The middle (`download … backfill`) derives from `OPERATION_GROUP_ORDER`; the bookends (`configure`, `playlist` / `cleanup`, `diagnostics`, `danger`) are **channel chores**, not operations, and stay hand-listed. Say so in the code so the next reader does not "finish" the derivation. |
| **Digest's two lanes** | A **registry defect**, not a noun problem. `common/controller/arbiter.ts` special-cased `DIGEST_KIND_ID` because digest runs on its own queue. **Resolved in slice 2** by giving digest its own `laneFor(settings)` and resolving every operation through `laneForOperation` — but NOT by "giving every kind a `laneFor` defaulting to `lane`": `laneFor` is optional and its PRESENCE is what `backfillBatch.ts` keys the GPU idle-only rule off, so a default would enrol every operation in that rule (FACTS.md, "laneFor? is OPTIONAL"). Add one only where the lane genuinely varies. |
@@ -131,7 +131,11 @@ dependencies allow. Sizes are S/M/L.
attribution fieldsets moved to `/operations/<id>`, each with its own form and action;
`saveSettingsAction` lost the four blocks and the four hidden `*FormPresent` markers with
them. 562 lines out of `SettingsForm`, 171 out of its action. **M.**
-4. **`/actionable` dissolves.** Depends on unified-ops step 1. The per-operation sections
+4. **`/actionable` dissolves.** ~~Depends on unified-ops step 1~~ — **SATISFIED 2026-08-26**
+ (`efb0cf9` → `00b1c8a`): `snapshot.backfill.digest` is now the only digest work list, and it
+ carries what this slice needs and the deleted `noDigest` bucket did not — the per-state
+ split (`blocked`, `deferred`, `partial`) and the coverage pair (`eligible`, `present`).
+ The per-operation sections
become the "channels with reachable work" table on `/operations/<id>` — which is what
`SweepPlan.tsx` already draws; `NeedsWorkPanel.tsx:61-76` drops `noDigest` in favour of
band `reachable` (`buildBands.ts:163`); the non-operation sections go to `/cleanup`, Sites
diff --git a/plans/unified-operations-model.md b/plans/unified-operations-model.md
@@ -1,9 +1,10 @@
# The unified rule model for media-derived work
-**Status: designed, not built.** Phases A–C shipped on `feat/diarization-oom-wall`
-(`1f82296`, `67d2ff2`, `f9c15d5`). This file is the target they aim at, written down so the
-next person does not have to re-derive it, and so the shortcuts taken in A–C are legible as
-shortcuts rather than as decisions.
+**Status: partly built — steps 1, 2, 3 and 4 are DONE; 5 and 6 are open.** Phases A–C shipped
+on `feat/diarization-oom-wall` (`1f82296`, `67d2ff2`, `f9c15d5`). This file is the target they
+aim at, written down so the next person does not have to re-derive it, and so the shortcuts
+taken in A–C are legible as shortcuts rather than as decisions. The step list below is the
+current record; each DONE step says what actually shipped.
## The problem, stated once
@@ -85,13 +86,18 @@ type AutoQueueNode = {
Each step is independently shippable and independently revertable. **Do not do them in one
change** — every one of them moves live numbers on a 78,000-video corpus.
-1. **Collapse the counters.** `snapshot.backfill.digest` replaces `noDigest`. This is the
- step Phase C deliberately skipped: the digest kind's `state()` already computes it
- (including the shared-from-duplicate rule), so this is a rewiring, not new logic. Watch
- for: `/api/widget/actionable` refuses to filter on `noDigest` because during the backfill
- it is 99.87% of the corpus — the replacement must keep that refusal, and note that
- `blocked` now removes untranscribed videos from the count, which will change the
- published coverage percentage.
+1. **Collapse the counters.** — **DONE 2026-08-26** (`efb0cf9`, `00b1c8a`; plan
+ [`unified-ops-step-1.md`](unified-ops-step-1.md)). `snapshot.backfill.digest` is the only
+ digest work list: `buckets.noDigest` is no longer written, typed, defaulted or read, and
+ `DigestWork.source` and the four `id === DIGEST_OPERATION_ID` fallbacks
+ (`buildBands.ts`, `sweepPreview.ts` ×2, `sweepRecency.ts`) went with it. It was a
+ **deletion, not a rewiring**: the fallback's migration had already completed on disk —
+ 68 of 68 snapshots carried `backfill.digest` when this was measured — so the bucket branch
+ was reading nothing and **no rendered number moved**. The coverage percentage this step
+ warned would change had already changed, silently, between 08-11 and 08-26; the two
+ counters were 11,777 videos apart (bucket 59,159 vs entry 47,382), the delta being
+ `deferred` on 65 of 68 channels. `/api/widget/actionable`'s refusal to filter on the count
+ survives, unchanged: it filters on `undownloaded || untranscribed` only.
2. **A leaf can name an operation.** — **DONE.** `AutoQueueMatch.operation` beside `bucket`,
drawing from a separate `ChannelWork.operations` id space, with the claim key widened to
`${operation}\0${id}` so digest and diarization cannot steal each other's work. The tree