commit 005734a2dbae46ff2ec7ca8894c416fc6933960b
parent e1d09191132bc320e585be29739a356e3ff7c81d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 26 Aug 2026 16:40:22 -0400
plans: slice 3 chosen and planned
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat:
3 files changed, 366 insertions(+), 4 deletions(-)
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -2245,3 +2245,79 @@ catalog id as a worker tag, so "transcode" is offerable beside "download".
the new header), `"backfill-channel"` 14, `"backfill-sweep"` 9, `settings.backfill` 40 (+1
comment), `kindIds` 58, `sweepKinds` 24, `"worker-unit"` 1, `"auto-download-unit"` 4, and
every `backfill` under `editor/app/widget/` (34).
+
+## Verified 2026-08-26 — slice 3 seams (per-operation settings)
+
+Census taken before writing `editor-ia-slice-3.md`, so its plan rests on line numbers
+checked at `32950aa`. Everything here is read-only observation; nothing was changed.
+
+**`SettingsForm.tsx` (1,163 lines) is eight `<fieldset>`s**, and the four slice 3 moves are
+contiguous: Digest `:526-707`, Diarization `:708-888`, Speaker work lane `:889-981`, Speaker
+attribution `:982-1087` — **562 lines**. The other four stay on `/settings`: Transcription
+workers `:82-104`, Sync scheduler `:328-478`, Build pipeline `:479-525`, Social links
+`:1088-1099`. The file is `"use client"`, one `useActionState(saveSettingsAction)`, one
+"Save settings" button (`:1105`) and one `role="status"` (`:1109`). Its `Field` helper is
+file-local (`:1128`), not exported. The Digest and Attribution fieldsets read the
+`digestApps: DigestAppDescriptor[]` prop (`:551,650,683,1061`), built on the server in
+`settings/page.tsx` because `digestApps.ts` reaches `process.env` and imports execa.
+
+**`settings/actions.ts` (506 lines) already has the four blocks marked**: Digest `:264-343`,
+Diarization `:344-393`, Backfill lane `:394-411`, Speaker attribution `:412-448`; the `next`
+object is assembled at `:449` and the four blocks land on it at `:491-494`. Each block is
+gated on a hidden `<input name="*FormPresent" value="1">` — `digestFormPresent :289`,
+`diarizationFormPresent :351`, `backfillFormPresent :401`, `attributionFormPresent :417` —
+whose ONLY reason to exist is that one form saves everything: an unchecked checkbox is absent
+from a FormData, so a submit from a form without the block would read every switch as off.
+The comments name what each marker guards: an armed digest sweep, the diarization capture
+lane (whose loss lets Clean-audio delete held audio), `allowRedownload` + the backfill sweep
+flag/scope, and `textOnlyEnabled` (~194,000 calls). **Once each block has its own form and
+action, every marker is dead**, and the `...current` spread each block already does becomes
+the whole isolation story. Sanitizers are already per-block in `common/lib/settings.ts`:
+`sanitizeDigest :937`, `sanitizeBackfill :1111`, `sanitizeAttribution :1149`,
+`sanitizeDiarization :1211` (types at `:447/:289/:243/:342`, defaults at
+`:869/:1094/:1137/:1174`). `writeSettings` re-sanitizes every block regardless (`:1679`).
+
+**The lane fieldset is not per-operation.** "Speaker work lane" (`enabled`, `weight`,
+`concurrency`, `allowRedownload`) governs `BACKFILL_QUEUE`, which diarization,
+`attribution-text` and `attribution-diarized` share; its own comment (`:890-894`) says it is
+"THE ONE FIELDSET THAT STILL SAYS LANE, and it is the right one". So it cannot go on any one
+`/operations/<id>` as that operation's settings. Where the lane IS drawn: `SweepLane.tsx` is
+rendered from exactly one place, `OperationDetail.tsx:167` inside `SweepOperationView`
+(`:104-169`) — i.e. on every member page of a sweep lane and NOT on the `/operations` board
+(`OperationsBoard` draws the rail; `LaneHeader.tsx` is the RUNNER lanes' header for
+download/transcription, nothing to do with the backfill lane). `SweepOperationView` already
+carries `data-lane={laneId}` "deliberately the SAME value on every speaker operation's page,
+since they are one lane", the shared pause and the shared sweep. A lane-settings form beside
+`SweepLane` there is one definition rendered on three pages, matching how the pause already
+appears. Digest's lane has one member, so its lane facts (`sweepEnabled`, order/reach) are
+already in the Digest block.
+
+**How e2e sets these blocks:** `backfill.spec.ts` (17 calls), `attribution.spec.ts` (4) and
+`worker-unit.spec.ts:113` all go through `writeSettings()` in `e2e/helpers.ts:66`, which
+writes `test-settings.json` directly — they never drive the `/settings` form for these
+fields. The only specs that DO drive the form for the moving blocks are three in
+`settings.spec.ts`: `:179` "digest settings round-trip through the form" (`/settings`,
+`getByRole("group", { name: "Digest" })`, `Timestamp mode`, `prompt variant label`,
+`spend cap`, `long-tail cutoff`, `enable the metered`, `Context window (num_ctx) for
+ollama-direct`, then "Save settings" + status "Saved"); `:221` "an unrelated settings save
+does not reset the digest prompt shape" (sets digest fields, reloads, saves `admin title`,
+reads `test-settings.json`); `:265` "the sortformer engine settings round-trip" (targets
+`select[name="diarizationEngine"]` / `diarizationBackend` BY NAME because `getByLabel`
+substring-matches, plus `Sortformer engine binary`, `Sortformer model (GGUF)`). The
+attribution and lane fieldsets have **no form-driving spec at all**.
+
+**Copy that will point at the wrong page once the switches move.** Each says "in Settings"
+about a switch slice 3 relocates: `operations/components/OperationDetail.tsx:150` ("switched
+off in Settings, so nothing dispatches it"), `operations/components/SweepLane.tsx:218` ("turn
+one on in Settings"), `channels/[slug]/components/stages/SpeakersStage.tsx:144` ("Turn an
+operation on in Settings"), `channels/components/ChannelGroupLine.tsx:184` (lane "switched off
+in Settings" title), `channels/groupActions.ts:108` (skip reason), `stages/DigestStage.tsx:210`
+and `videos/[id]/components/DigestPanel.tsx:144` ("metered — disabled in Settings"). The
+disk-floor sentences ("lower the floor in Settings", four sites) stay true — the floor is not
+moving. `ChannelForm.tsx:812`, `scheduler/*`, `workers/*` link to `/settings` for blocks that
+stay.
+
+**`SettingsForm` has no per-fieldset test ids** — the digest spec scopes with
+`getByRole("group", { name: "Digest" })`, i.e. the `<legend>` text is the accessible name.
+Keeping the legends verbatim on the new pages keeps that selector valid after only the
+`goto` changes.
diff --git a/plans/STATE.md b/plans/STATE.md
@@ -3,7 +3,7 @@
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 (later) — **the vocabulary pass landed**: the registry is
+**Last updated:** 2026-08-26 (evening) — slice 3 chosen as next and planned (`editor-ia-slice-3.md`); no code changed since. Earlier: **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,
@@ -152,9 +152,15 @@ now **editor IA slice 3**:
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
sweep-sized commitment that was never priced.
-6. **Editor IA slice 3** (per-operation settings) — the vocabulary pass it waited on is
- done (below); the "Speaker work lane" fieldset moves to `/operations/<id>` on the renamed
- nouns.
+6. **Editor IA slice 3** (per-operation settings) — **CHOSEN 2026-08-26 as the next
+ slice, over #2**, because it is what the vocabulary pass paid for and it is self-contained
+ (four contiguous fieldsets, four already-marked action blocks, per-block sanitizers). The
+ detailed plan is [`editor-ia-slice-3.md`](editor-ia-slice-3.md); the census it rests on
+ is FACTS.md "Verified 2026-08-26 — slice 3 seams". One decision made while scoping: the
+ "Speaker work lane" fieldset is LANE settings shared by three operations, so it does not
+ become any one operation's block — it goes beside `SweepLane` in `SweepOperationView`,
+ rendered on every backfill-lane member page the way the shared pause already is. #2 comes
+ right after: it is what slice 4 and the transcode band both wait on.
---
diff --git a/plans/editor-ia-slice-3.md b/plans/editor-ia-slice-3.md
@@ -0,0 +1,280 @@
+# Editor IA slice 3 — per-operation settings live on the operation page
+
+## Context
+
+The vocabulary pass landed 2026-08-26 (`02ff048` → `32950aa`): the registry is `operations.ts`,
+says `Operation`, and every UI id space says "speakers". The user chose **slice 3 over
+unified-ops step 1** as the next piece (recorded in `plans/STATE.md` "Recommended next" #6 and
+in memory). Step 1 follows it.
+
+Today `/settings` (`SettingsForm.tsx`, 1,163 lines, one form, one `saveSettingsAction`) holds
+four fieldsets that configure *operations*: **Digest** (`:526-707`), **Diarization**
+(`:708-888`), **Speaker work lane** (`:889-981`), **Speaker attribution** (`:982-1087`) —
+562 lines. `/operations/<id>` (slice 1–2) is where an operator reads an operation's state,
+arms its sweep and pauses its lane, then has to go to a different page to switch it on. The
+IA doc's test is "a new operation is a registry entry, and it appears on the board, the channel
+page, the video page and the policy tree without a new page" — its settings should appear
+there too, and `/settings` should be the Machine page the end-state nav says it is.
+
+**Every fact below was verified at `32950aa` and is recorded in `plans/FACTS.md` "Verified
+2026-08-26 — slice 3 seams"** — read that section first. `plans/STATE.md` already links this
+plan as `plans/editor-ia-slice-3.md`; that file does not exist yet (see step 0).
+
+Three findings that decide the design:
+
+1. **The `*FormPresent` markers exist only because one form saves everything.** Each of the
+ four action blocks (`settings/actions.ts` Digest `:264-343`, Diarization `:344-393`,
+ Backfill `:394-411`, Attribution `:412-448`) is gated on a hidden input so a submit from a
+ form lacking the block cannot read every checkbox as off. Give each block its own form and
+ action and all four markers are dead — that is the deletion this slice makes. The
+ `...current` spread each block already does becomes the whole isolation story, and the
+ sanitizers are already per-block (`sanitizeDigest :937`, `sanitizeBackfill :1111`,
+ `sanitizeAttribution :1149`, `sanitizeDiarization :1211`; `writeSettings` re-sanitizes
+ everything at `:1679`).
+2. **The lane fieldset is not per-operation.** "Speaker work lane" (`enabled`, `weight`,
+ `concurrency`, `allowRedownload`) governs `BACKFILL_QUEUE`, shared by diarization,
+ `attribution-text` and `attribution-diarized`. It goes beside `SweepLane` inside
+ `SweepOperationView` (`OperationDetail.tsx:104-169`) — the one place `SweepLane` is
+ rendered, on every member page of the lane, already carrying the shared pause and sweep
+ under the same `data-lane="backfill"`. One definition, three renders. `LaneHeader.tsx` is
+ the RUNNER lanes' header and is not involved; the `/operations` board does not draw the
+ sweep lanes.
+3. **Nothing in the four fieldsets is stateful** — no `useState`, everything
+ `defaultValue`/`defaultChecked`. Outside reads are only `initial.<block>`, `digestApps`
+ (Digest and Attribution), the file-local `Field` (`:1128`), `DigestAppsField`
+ (`settings/components/DigestAppsField.tsx`), and one `Link` to `/operations` in the lane
+ fieldset. The precedent for a client form with its own scoped action is
+ `scheduler/components/SchedulerSettingsForm.tsx` (`useActionState`, `pending`, "Saving…",
+ `role="status"` "Saved.", `role="alert"`); the precedent for the action shape is
+ `saveLaneOrderAction` (`operations/actions.ts:205-235`: `{...getSettings(), block}`,
+ `writeSettings`, `revalidateOperations()` at `:30-33`).
+
+## Step 0 — the docs already in the working tree
+
+`plans/FACTS.md` and `plans/STATE.md` are modified and uncommitted (the census and the
+decision). First action: write this plan verbatim to `plans/editor-ia-slice-3.md` and commit
+the three files together: `plans: slice 3 chosen and planned`. Nothing else in that commit.
+
+## Order: three commits
+
+1. **The seams** — registry `settingsBlock`, `Field` promoted, the four per-block actions
+ written beside the old one (nothing rendered yet). tsc + unit-verified.
+2. **The move** — four form components, host slots in `OperationDetail`, fieldsets and blocks
+ deleted from `/settings`, markers gone, specs repointed. e2e-verified.
+3. **Copy, pointer, docs** — every "in Settings" sentence that now points at the wrong page,
+ a one-paragraph pointer on `/settings`, CHANGELOG, FACTS/STATE/IA doc.
+
+tsc in all six packages + `pnpm -C common test` + editor units after each; e2e once after
+commit 3 (detached — see memory `e2e-run-detached`; the queue lock is serial). Edit nothing
+while it runs.
+
+## Commit 1 — the seams
+
+### `settingsBlock` on the descriptor (`common/lib/operations.ts`)
+
+Off the descriptor, never off the id — the rule slice 2 set for `runner` and `sweepLaneIdFor`.
+Add to `Operation` (the registry-side type) and copy through to `OperationDescriptor` in
+`operationCatalog()`, exactly as `appliesTo` was added in `32950aa`:
+
+```ts
+// The settings.json block that configures this operation, when one does. Named
+// here so the operation page can render that block's form without a table
+// keyed by operation id. Digest owns `digest`; diarization owns `diarization`;
+// both attribution operations share `attribution` (one block, one form, drawn
+// on both pages). The backfill LANE's block (`settings.backfill`) is not an
+// operation's and is not named here — it is rendered per lane, not per operation.
+settingsBlock?: "digest" | "diarization" | "attribution";
+```
+
+Set it on `digest`, `diarization`, `attribution-text`, `attribution-diarized`. External
+operations and `transcode` have none. Test in `operations.test.ts`: the four carry the
+values above, everything else is `undefined`, and the two attribution ids share one.
+
+### `Field` promoted
+
+`SettingsForm.tsx:1128-1162` `Field` (the superset — has `type`/`step` and the load-bearing
+comment about `type="number"` implying `step=1`) → `editor/app/components/forms/Field.tsx`,
+exported. `SettingsForm` imports it. The three other file-local copies (`ChannelForm.tsx:948`,
+`SiteForm.tsx:664`, `OverviewPanel.tsx:168`) are **out of scope** — say so in a comment at the
+top of the new file so the next reader does not "finish" it here.
+
+### Four actions (`editor/app/operations/settingsActions.ts`, new, `"use server"`)
+
+`saveDigestSettingsAction`, `saveDiarizationSettingsAction`, `saveBackfillLaneSettingsAction`,
+`saveAttributionSettingsAction`, each `(prev: SaveResult | undefined, formData: FormData) =>
+Promise<SaveResult>` (reuse `SaveResult` from `settings/actions.ts:41`). Body of each is the
+existing block moved verbatim minus its marker:
+
+```ts
+const current = getSettings();
+const next: SiteSettings = { ...current, digest: { ...current.digest, /* fields */ } };
+try { await writeSettings(next); } catch (e) { return { ok: false, error: (e as Error).message }; }
+revalidateOperations();
+return { ok: true };
+```
+
+- `num(key, fallback)` is currently defined INSIDE the diarization block (`:353`) and reused by
+ backfill and attribution — it becomes a module-level helper in the new file.
+- Digest keeps its `digestAppsJson` parse and its `"Digest app config payload is malformed"`
+ error; keep `isDigestSectionKind` / `isDigestTimestampMode` imports.
+- Keep each block's existing head comment (what the spread protects: an armed sweep, the
+ capture lane, `allowRedownload`, `textOnlyEnabled`) — the marker paragraph is deleted, the
+ *why* is not. Add one line: the form that posts here renders every field of this block, and
+ is the only form that touches it.
+- No `getWorkerPool().reconfigure` (workers-only), no `revalidatePath("/workers")`.
+ `revalidateOperations()` covers `/operations` and `/operations/[id]`; add
+ `revalidatePath("/settings")` only if commit 3's pointer shows anything live (it will not).
+- Do NOT touch `saveSettingsAction` in this commit; it still works with the markers. tsc and
+ the existing units prove commit 1 alone changes nothing.
+
+## Commit 2 — the move
+
+### Four form components (`editor/app/operations/components/settings/`)
+
+`DigestSettingsForm.tsx`, `DiarizationSettingsForm.tsx`, `LaneSettingsForm.tsx` (the backfill
+lane), `AttributionSettingsForm.tsx`. Each is `"use client"`, takes its block as a prop
+(`initial: SiteSettings["digest"]`, plus `digestApps: DigestAppDescriptor[]` for Digest and
+Attribution), and is:
+
+```tsx
+<form action={formAction} data-settings-block="digest" className="flex flex-col gap-3">
+ <fieldset className="… border rounded p-3">
+ <legend className="px-1 text-sm font-medium">Digest</legend> {/* verbatim */}
+ …the fieldset's existing children, minus the hidden marker…
+ <button type="submit" disabled={pending}>{pending ? "Saving…" : "Save digest settings"}</button>
+ {state?.ok && <p role="status">Saved.</p>}
+ {state && !state.ok && <p role="alert">{state.error}</p>}
+ </fieldset>
+</form>
+```
+
+- **Legends verbatim** ("Digest", "Diarization", "Speaker work lane", "Speaker attribution"):
+ `settings.spec.ts:190` scopes with `getByRole("group", { name: "Digest" })` — the legend is
+ the fieldset's accessible name and keeps that selector valid after only a `goto` change.
+- **Field `name`s verbatim** (`digestTimestampMode`, `diarizationEngine`, …): the actions read
+ them and `settings.spec.ts:265` targets `select[name="diarizationEngine"]` by name.
+- **Button text is per block** ("Save digest settings", "Save diarization settings", "Save
+ lane settings", "Save attribution settings"): two forms share the attribution pages and the
+ backfill member pages, and an unscoped `/save settings/i` would be ambiguous.
+- `data-settings-block` is the spec hook. `role="status"` "Saved." is fine here: the SaveBar
+ comment (`SaveBar.tsx:66-68`, "the ONLY role=status on this page") is about RUNNER pages,
+ which get no settings form; `auto-subs-replace.spec.ts:415` (`getByRole("status").first()`)
+ runs on `/operations/transcription`. Write that boundary into the SaveBar comment.
+- `DigestAppsField.tsx` `git mv`s into the new directory — its only consumer moves.
+- The lane form's paragraph that links to `/operations` for "which operations a sweep runs is
+ not set here" is rewritten: the sweep scope is now **directly above this form** on the same
+ page. Keep the sentence about why scope is not persisted separately (the resurrected-sweep
+ bug); drop the `Link`.
+- Use the 3-tuple `useActionState` with `pending`, as `SchedulerSettingsForm` does.
+
+### Host slots
+
+`OperationDetail` gains two optional props: `operationSettings?: ReactNode` and
+`laneSettings?: ReactNode`. `SweepOperationView` renders them after `<SweepLane>` (`:167`):
+operation block first, lane block second, inside the same `section[data-lane]` — so the
+existing scoped selectors (`backfill.spec.ts:477`, `auto-queue.spec.ts:1210`) still find the
+lane's controls, and the new forms are inside the scope those specs use. Checked: nothing in
+the four fieldsets carries a label matching `Order`/`Reach` (exact), `/sweep$/i`, or the two
+sweep-scope checkbox labels.
+
+`/operations/[id]/page.tsx` (server) builds them:
+
+```ts
+const settings = getSettings();
+const digestApps = op.settingsBlock === "digest" || op.settingsBlock === "attribution" ? listDigestApps() : [];
+const operationSettings = formForBlock(op.settingsBlock, settings, digestApps); // small switch on the BLOCK, not the id
+const laneSettings = laneId === "backfill" ? <LaneSettingsForm initial={settings.backfill} /> : null;
+```
+
+`formForBlock` is a `switch` over the three block names — a table keyed by settings block,
+which is data the descriptor declared, not a table keyed by operation id. The digest lane's
+own lane facts (`sweepEnabled`, order/reach) are already in the Digest block and the
+`OrderReach` control, so `laneSettings` for `digest` is `null`.
+
+### `/settings` loses the four
+
+Delete `SettingsForm.tsx:526-1087` and the four blocks in `saveSettingsAction`
+(`:264-448`), the four `*FormPresent` reads, and the four keys at `:491-494` — the `next`
+object then carries `digest/diarization/backfill/attribution: getSettings().<block>` with the
+same one-line comment the `autoQueue` and `savedVideoBackup` keys already have ("edited on its
+own page; preserved here"). Drop `digestApps` from `SettingsForm`'s props and from
+`settings/page.tsx` if nothing else reads it (`workerTags` and `apps` stay). Remove now-unused
+imports (`DIGEST_SECTION_OPTIONS` etc., `isDigestSectionKind`, `isDigestTimestampMode`,
+`DigestAppsField`, `Link` if unused).
+
+### Specs
+
+- **`settings.spec.ts:179`** (digest round-trip) → `goto("/operations/digest")`, scope to
+ `page.locator('form[data-settings-block="digest"]')`, click `Save digest settings`, status
+ inside that form. Move it to a new `editor/e2e/operation-settings.spec.ts`.
+- **`:221`** (unrelated save does not reset the digest prompt shape) → becomes the genuine
+ cross-form test it always described: set the digest fields on `/operations/digest`, save,
+ then `goto("/settings")`, change `admin title`, save, read `test-settings.json`. Keep its
+ comment about React 19 resetting `<form action>` inputs — it still explains the reload.
+ Add a second half in the same file: save the **lane** form on `/operations/diarization`
+ with `Run the backfill lane` ticked, then save the **diarization** form on the same page,
+ and assert `backfill.enabled` survived — that is the property the markers used to protect,
+ now proven structurally.
+- **`:265`** (sortformer round-trip) → `goto("/operations/diarization")`, scope to
+ `form[data-settings-block="diarization"]`; the by-name selectors are unchanged.
+- **New**: attribution round-trip on `/operations/attribution-text` (tick
+ `attributionTextOnly`, set `attributionModel`, save, read back) and the same form present on
+ `/operations/attribution-diarized` (one assertion that the fieldset renders there). The
+ attribution and lane fieldsets had **no form-driving spec at all** before.
+- `settings.spec.ts` keeps everything else (`:8-178`); the `page.locator("form")` selectors at
+ `:124/:133` stay unambiguous — there is still exactly one form on `/settings`.
+- `backfill.spec.ts`, `attribution.spec.ts`, `worker-unit.spec.ts` set these blocks through
+ `writeSettings()` (`e2e/helpers.ts:66`), never the form — unaffected.
+
+## Commit 3 — copy, the pointer, docs
+
+**Copy that says "in Settings" about a switch that moved** (from FACTS; each becomes "on its
+operation page" with a link where the component can hold one):
+`OperationDetail.tsx:150` ("switched off in Settings" — it is now switched off *on this page*,
+below; say "below"), `SweepLane.tsx:218`, `SpeakersStage.tsx:144`, `ChannelGroupLine.tsx:184`
+(title), `groupActions.ts:108` (skip reason string — `channel-groups.spec.ts` does not assert
+it; check before changing), `DigestStage.tsx:210` and `DigestPanel.tsx:144` ("metered —
+disabled in Settings" → "on the Digest page"). The disk-floor sentences ("lower the floor in
+Settings", four sites) stay — the floor did not move.
+
+**`/settings` pointer**: one `<p>` at the top of `SettingsForm` (or `settings/page.tsx`):
+"Digest, diarization, the speaker work lane and speaker attribution are configured on their
+operation pages" with four links. Not a fieldset, no form. The IA rule "fold rather than
+add; every retired route redirects" does not apply — no route retires.
+
+**Docs**: `editor/CHANGELOG.md` (user-visible: the four blocks moved, the buttons are per
+block, `/settings` is machine + site settings); `plans/editor-operations-ia.md` slice 3 bullet
+→ "SHIPPED" with a "Slice 3, as shipped" section recording the two decisions (lane block per
+lane not per operation; `settingsBlock` on the descriptor rather than an id table);
+`plans/STATE.md` dated entry + "Recommended next" (#6 done, #2 next); `plans/FACTS.md` a short
+"as shipped" note under the seams section (the new file paths; markers gone; the SaveBar
+`role="status"` boundary).
+
+## Verification
+
+1. After each commit: `pnpm -C <pkg> exec tsc --noEmit` for `common editor export homepage
+ umtool mcp`; `pnpm -C common test` (828+ — the `settingsBlock` test); editor units
+ `pnpm -C editor exec tsx --test "app/**/*.test.ts"` (85+).
+2. Grep gates after commit 2, over `editor/`: `FormPresent` → 0; `Save settings` → exactly
+ the one on `/settings` (+ its spec regex); `saveSettingsAction` no longer reads
+ `digest|diarization|backfill|attribution` form fields (grep `formData.get("digest` etc. in
+ `settings/actions.ts` → 0); `DigestAppsField` imported only from the new directory.
+3. e2e once after commit 3, detached (`setsid nohup … pnpm e2e -- <specs>`, log monitor),
+ filenames only after `--`: `operation-settings.spec.ts settings.spec.ts auto-queue.spec.ts
+ auto-subs-replace.spec.ts backfill.spec.ts attribution.spec.ts channel-groups.spec.ts
+ digest.spec.ts` (whichever digest-stage spec asserts the "metered — disabled" copy; check
+ with grep first). Known flakes from the slice-2 run (`auto-queue.spec.ts:411` race; the
+ Videos-tab 5 s `toHaveURL` on a cold compile) are not this slice's — retry individually.
+4. Manual, on the e2e fixture, never `transcripts/`: `PORT=3021 pnpm dev:test`;
+ `/operations/digest` shows the Digest form under the lane and saving it persists to
+ `editor/test-settings.json`; `/operations/diarization` shows Diarization then Speaker work
+ lane; `/operations/attribution-text` and `/attribution-diarized` show Speaker attribution
+ then Speaker work lane; `/operations/download` and `/operations/transcode` show no form;
+ `/settings` has no operation fieldset and carries the pointer; kill the server, remove
+ `editor/test-transcripts` and `editor/test-settings.json`, `git status` clean.
+
+## Out of scope
+
+Unified-ops step 1 (next). The three other file-local `Field` copies. `WorkersField`
+(slice 8), the pause trio (slice 7), `/actionable` (slice 4). Persisted keys and field
+`name`s do not change. No redirect — no route retires.