Archilyzer · Source

archilyzer

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

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:
Mplans/FACTS.md | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/STATE.md | 14++++++++++----
Aplans/editor-ia-slice-3.md | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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.