Archilyzer · Source

archilyzer

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

commit becadcda1ec1de85ae019f90ab59253a8b4b57bc
parent f15e4c9c051b697dca3568fc3aeeab65a410cf61
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 26 Aug 2026 17:05:00 -0400

editor: the copy points at the operation page, and /settings says so

Nine sentences told an operator to change something "in Settings" that slice 3
had just moved. Each now names where the switch actually is: the lane panel and
the operation banner say "below" (the form is on that same page), the channel
card links to Operations, the group line's title and the group action's skip
reason say "on its operation page", the two metered-lane options say "on the
Digest page", and diarization's deferredHint sends the operator to the
Diarization operation's page for Max audio hours.

The four disk-floor sentences are deliberately untouched — the floor did not
move — and neither is SiteForm's R2 hint, for the same reason.

/settings gains a POINTER, not a control: one paragraph naming the four pages
with links. A fieldset here would be the second place to edit those blocks,
which is the thing being undone. No route retired, so nothing redirects.

Docs: the CHANGELOG entry is written for an operator (what moved, that the lane
settings appear on all three speaker pages because the queue really is shared,
that the Save buttons are per block, that nothing on disk changed);
editor-operations-ia.md marks slice 3 SHIPPED and records the two decisions that
could have gone the other way (the lane block is rendered per lane, and
settingsBlock lives on the descriptor rather than in an id table); FACTS.md gets
an "as shipped" note under the seams census so its line numbers do not send the
next reader to the wrong file; STATE.md gets the dated entry and moves
unified-ops step 1 to the front of "Recommended next".

tsc in six packages, common 828/828, editor units 85/85. The pointer and every
operation page checked against the fixture.

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

Diffstat:
Mcommon/lib/operations.ts | 2+-
Meditor/CHANGELOG.md | 1+
Meditor/app/channels/[slug]/components/stages/DigestStage.tsx | 2+-
Meditor/app/channels/[slug]/components/stages/SpeakersStage.tsx | 9++++++++-
Meditor/app/channels/[slug]/videos/[id]/components/DigestPanel.tsx | 3++-
Meditor/app/channels/components/ChannelGroupLine.tsx | 2+-
Meditor/app/channels/groupActions.ts | 7++++++-
Meditor/app/jobs/actions.ts | 3++-
Meditor/app/operations/components/OperationDetail.tsx | 7++++---
Meditor/app/operations/components/SweepLane.tsx | 5+++--
Meditor/app/settings/page.tsx | 32++++++++++++++++++++++++++++++++
Mplans/FACTS.md | 40++++++++++++++++++++++++++++++++++++++++
Mplans/STATE.md | 57++++++++++++++++++++++++++++++++++++++++++++-------------
Mplans/editor-operations-ia.md | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
14 files changed, 204 insertions(+), 32 deletions(-)

diff --git a/common/lib/operations.ts b/common/lib/operations.ts @@ -635,7 +635,7 @@ const diarization: Operation = { costBasis: "one pass over the audio per video", // The wording SpeakersStage used to hardcode for every kind at once. deferredHint: - "too long to diarize under the current limit — raise or clear Max audio hours in Settings to include them", + "too long to diarize under the current limit — raise or clear Max audio hours on the Diarization operation's page to include them", tier: "lane", // CPU, on the shared backfill queue. Serialized against the other backfill // kinds on purpose — two channels' worth of diarization at once just thrashes diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **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. - **The channel page's "Backfill" card is the *Speakers* card, and it says what each operation costs.** "Backfill" is the name of a *queue*, and on this install that one word was standing in for three operations with different inputs, different costs and different reasons to be switched on — nobody can arm, pause or run "a backfill". The card is now named after the work it holds, one row per operation, each row carrying what one unit of it costs. Old links keep working: **`?stage=backfill` still opens the Speakers panel** rather than silently dropping you on the channel overview. diff --git a/editor/app/channels/[slug]/components/stages/DigestStage.tsx b/editor/app/channels/[slug]/components/stages/DigestStage.tsx @@ -207,7 +207,7 @@ export function DigestStage({ > <option value="local">local (ollama)</option> <option value="remote" disabled={!remoteEnabled}> - metered{remoteEnabled ? "" : " — disabled in Settings"} + metered{remoteEnabled ? "" : " — disabled on the Digest page"} </option> </select> </label> diff --git a/editor/app/channels/[slug]/components/stages/SpeakersStage.tsx b/editor/app/channels/[slug]/components/stages/SpeakersStage.tsx @@ -19,6 +19,7 @@ // single "77,105 remaining" here would make the card useless the day it shipped // — the same failure /api/widget/actionable already documents for `noDigest`. +import Link from "next/link"; import { useState } from "react"; import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog"; import { QueueControl } from "../../../../components/QueueControl"; @@ -141,7 +142,13 @@ export function SpeakersStage({ it twice costs nothing the second time. </> ) : ( - "Nothing here is enabled. Turn an operation on in Settings and this card will report what the existing corpus is missing." + <> + Nothing here is enabled. Turn an operation on from its page under{" "} + <Link href="/operations" className="underline underline-offset-2"> + Operations + </Link>{" "} + and this card will report what the existing corpus is missing. + </> ), }, ...(missingInput > 0 diff --git a/editor/app/channels/[slug]/videos/[id]/components/DigestPanel.tsx b/editor/app/channels/[slug]/videos/[id]/components/DigestPanel.tsx @@ -141,7 +141,8 @@ export function DigestPanel({ > <option value="local">local (ollama)</option> <option value="remote" disabled={!data.remoteEnabled}> - metered{data.remoteEnabled ? "" : " — disabled in Settings"} + metered + {data.remoteEnabled ? "" : " — disabled on the Digest page"} </option> </select> </label> diff --git a/editor/app/channels/components/ChannelGroupLine.tsx b/editor/app/channels/components/ChannelGroupLine.tsx @@ -181,7 +181,7 @@ export function ChannelGroupLine({ type="button" disabled aria-label={`${station.id} group ${name}`} - title={`The ${label.toLowerCase()} lane is switched off in Settings. Its channels may still carry recorded work — an off lane is not a finished one.`} + title={`The ${label.toLowerCase()} lane is switched off on its operation page. Its channels may still carry recorded work — an off lane is not a finished one.`} className={CHIP + " opacity-50 cursor-not-allowed"} > {label} off diff --git a/editor/app/channels/groupActions.ts b/editor/app/channels/groupActions.ts @@ -105,7 +105,12 @@ async function runGroupStation( return { group, queued: [], - skipped: [{ slug: groupId, reason: "the lane is switched off in Settings" }], + skipped: [ + { + slug: groupId, + reason: "the lane is switched off on its operation page", + }, + ], }; } if (station === "download" && settings.downloadsPaused) { diff --git a/editor/app/jobs/actions.ts b/editor/app/jobs/actions.ts @@ -336,7 +336,8 @@ export async function stopBackfillSweepAction(): Promise<BackfillSweepResult> { // stays alive, logs "Backfill lane disabled in settings — holding", and resumes // within one 3s poll with nothing re-derived. Adding a second field would give // the same hold two writers that could disagree, and would silently divorce -// this button from the "Run the backfill lane" checkbox in Settings. +// this button from the "Run the backfill lane" checkbox on the speaker +// operations' pages. // // So: one field, two places to set it, and they cannot drift. // diff --git a/editor/app/operations/components/OperationDetail.tsx b/editor/app/operations/components/OperationDetail.tsx @@ -167,9 +167,10 @@ function SweepOperationView({ {!switchedOn && ( <p className="text-sm text-warning"> - This operation is switched off in Settings, so nothing dispatches it — - not the sweep below, and not the arbiter. That is not the same as - being finished, which is what an empty lane would otherwise imply. + This operation is switched off in its settings below, so nothing + dispatches it — not the sweep, and not the arbiter. That is not the + same as being finished, which is what an empty lane would otherwise + imply. </p> )} diff --git a/editor/app/operations/components/SweepLane.tsx b/editor/app/operations/components/SweepLane.tsx @@ -214,8 +214,9 @@ export function SweepLane({ {!lane.available && ( <p className="text-sm text-muted-foreground"> - No operation on the backfill lane is switched on, so there is no lane to run. That - is not the same as being finished — turn one on in Settings. + No operation on the backfill lane is switched on, so there is no lane + to run. That is not the same as being finished — turn one on from its + operation page; this operation&apos;s own settings are below. </p> )} diff --git a/editor/app/settings/page.tsx b/editor/app/settings/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import Link from "next/link"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { listTranscriptionApps } from "yt-dlp-transcript-common/lib/transcriptionApps"; @@ -12,6 +13,8 @@ export const dynamic = "force-dynamic"; export const metadata: Metadata = { title: "Settings" }; +const POINTER_LINK = "underline underline-offset-2 hover:text-foreground"; + export default async function SettingsPage() { const paths = getPaths(); const settings = getSettings(); @@ -43,6 +46,35 @@ export default async function SettingsPage() { Persisted in <code>settings.json</code> at the monorepo root. Used by the export build to title the static site. </p> + {/* A POINTER, NOT A CONTROL. Slice 3 moved four fieldsets that + configured operations onto the operation pages, so this page is + the machine and the site. Deliberately a paragraph rather than a + fieldset: a form here would be the second place to edit those + blocks, which is the thing being undone. No route retired, so + there is nothing to redirect. */} + <p className="text-sm text-muted-foreground mt-2"> + An operation&apos;s own settings live on its page, beside its + backlog and its sweep:{" "} + <Link href="/operations/digest" className={POINTER_LINK}> + Digest + </Link> + ,{" "} + <Link href="/operations/diarization" className={POINTER_LINK}> + Diarization + </Link>{" "} + (with the shared speaker work lane),{" "} + <Link href="/operations/attribution-text" className={POINTER_LINK}> + Speaker names (from the transcript) + </Link>{" "} + and{" "} + <Link + href="/operations/attribution-diarized" + className={POINTER_LINK} + > + Speaker names (from the audio) + </Link> + . + </p> </div> <SettingsForm initial={settings} diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -2321,3 +2321,43 @@ stay. `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. + +### As shipped (`f14ceb5`, `43bb519`) + +The census above is now history — here is what replaced it, so the line numbers do not send +the next reader to the wrong file. + +- **`Operation.settingsBlock`** (`common/lib/operations.ts`), a closed union + `OperationSettingsBlock = "digest" | "diarization" | "attribution"`, copied through to + `OperationDescriptor` in `operationCatalog()`. Set on `digest`, `diarization` and BOTH + attribution entries (one block, one form, two pages). `settings.backfill` is deliberately + NOT named here — it is the lane's, not an operation's. +- **The four forms** are `editor/app/operations/components/settings/{Digest,Diarization, + Lane,Attribution}SettingsForm.tsx`, each `"use client"`, each a `<form + data-settings-block="…">` with its own `useActionState`. `DigestAppsField.tsx` moved into + that directory with them. `Field` is `editor/app/components/forms/Field.tsx` (exported; the + three other file-local copies are explicitly out of scope and the file says so). +- **The four actions** are `editor/app/operations/settingsActions.ts` + (`saveDigestSettingsAction`, `saveDiarizationSettingsAction`, + `saveBackfillLaneSettingsAction`, `saveAttributionSettingsAction`), each the old block + verbatim minus its marker; `num()` is module-level there now. `revalidateOperations()` is + duplicated rather than imported — a `"use server"` module may only export async functions. +- **THE `*FormPresent` MARKERS ARE GONE**, all four, and nothing emits or reads one. That was + the point of the split: the isolation is now structural (a form cannot submit a block it + does not render), not a hidden input. `saveSettingsAction` carries the four blocks as + `getSettings().<block>`. +- **Which form a page draws** is `settingsFormFor(op.settingsBlock, …)` in + `operations/[id]/page.tsx` — an exhaustive switch over the union, never over the id. The + LANE form is `laneId === "backfill" ? <LaneSettingsForm …> : null`, passed to + `OperationDetail` as a second slot (`operationSettings`, `laneSettings`) and rendered by + `SweepOperationView` after `<SweepLane>`, INSIDE `section[data-lane]` so every existing + scoped selector still reaches the lane's controls. +- **The `role="status"` boundary.** `SaveBar.tsx`'s "the ONLY role=status on this page" means + RUNNER pages — `auto-subs-replace.spec.ts:415` asserts `getByRole("status").first()` on + `/operations/transcription`. Sweep-fed pages now carry settings forms with their own status + regions; they have no policy tree, so the two never meet. The comment now says this, and + the settings specs scope by `form[data-settings-block]` rather than by role. +- **Specs**: `editor/e2e/operation-settings.spec.ts` (7 tests) holds the three that moved out + 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. 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 (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 +**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 `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,28 +142,59 @@ 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**, unchanged from the 08-25 list except that #6 is done and #6 below is -now **editor IA slice 3**: +**Recommended next**, with slice 3 (the old #6) now shipped, so **#2 is the one to take**: 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.** + 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 sweep-sized commitment that was never priced. -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. +6. ~~**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 (evening) — editor IA slice 3: an operation's settings are on its page + +Three commits: `9515083` (the plan, with the two uncommitted doc files), `f14ceb5` (the +seams), `43bb519` (the move). The as-shipped record is in `editor-operations-ia.md` +("Slice 3, as shipped") and the new file paths are in `FACTS.md` under the seams census; +this entry is the decisions. + +1. **The markers were the deletion.** Four hidden `*FormPresent` inputs gated the four + settings blocks, and every one existed because ONE form saved everything: an unchecked + checkbox is absent from a FormData, so a submit from a form lacking a block would read + that block's every switch as off — a disarmed corpus sweep, a dropped diarization capture + lane, `allowRedownload` on against a full disk, ~194,000 model calls armed. One form per + block makes that structural. 562 lines out of `SettingsForm`, 171 out of its action. +2. **The lane block is per LANE, not per operation** (the decision recorded while scoping, + and it held). `settings.backfill` governs `BACKFILL_QUEUE`, shared by diarization and both + attribution operations, so `LaneSettingsForm` renders beside `SweepLane` in + `SweepOperationView` — one definition, three renders, exactly where the shared pause and + sweep already are. +3. **`settingsBlock` is on the descriptor**, a closed union, switched over exhaustively — + never a table keyed by operation id. Both attribution operations get one form because they + declare one block. Same rule slice 2 set for `runner` and `sweepLaneIdFor`. +4. **The `role="status"` boundary is now written down.** `SaveBar`'s "only role=status on + this page" means RUNNER pages (`auto-subs-replace.spec.ts:415` depends on it); sweep-fed + pages have settings forms with their own status regions and no policy tree. +5. **Two blocks gained their first form-driving spec.** Attribution and the lane had none — + which is how the most expensive switch in the console went untested from a browser. The + new `operation-settings.spec.ts` also proves structurally what the markers used to buy: + saving the diarization form beside the lane form leaves `backfill.enabled` alone. 7/7. + +Verified: tsc in all six packages, `common` 828/828, editor units 85/85, the new spec 7/7, +and a fixture check of every operation page (digest → one form; diarization and both +attribution pages → operation form then lane form; download/transcode/transcription → none; +`/settings` down to its four remaining fieldsets and one `<form>`). + +Deliberately not done: the three other file-local `Field` copies; `WorkersField` (slice 8); +the pause trio (slice 7); `/actionable` (slice 4). No persisted key or field `name` changed, +and no route retired. + ### 2026-08-26 (later) — the vocabulary pass: the registry says "operation" Three commits after `e0703b4`, sized so the 60-file identifier sweep is reviewable on its own. diff --git a/plans/editor-operations-ia.md b/plans/editor-operations-ia.md @@ -1,6 +1,6 @@ # The editor: one noun, not a pile -**Status: the model is decided; slice 1 has shipped.** This is the UI half of +**Status: the model is decided; slices 1, 2 and 3 have shipped.** This is the UI half of [`unified-operations-model.md`](unified-operations-model.md), which states the backend half ("four schedulers for what is really one kind of work"; steps 1, 5, 6 open). Read that one first if you are touching a dispatcher; read this one if you are touching a page. @@ -126,12 +126,11 @@ dependencies allow. Sizes are S/M/L. - **The `/channels` backfill group button does NOT go through `runOperationChannelJob`.** That station is the LANE, not one operation on it — `KIND_FOR` says so by mapping it to the lane's job kind — so it still runs every enabled lane kind. -3. **Per-operation settings on the operation page.** Move the Digest - (`SettingsForm.tsx:526`), Diarization (`:708`), Speaker-work lane (`:889`) and Speaker - attribution (`:982`) fieldsets to `/operations/<id>`; split `saveSettingsAction` - (`settings/actions.ts:286-420`) along its existing per-block markers — the sanitizers are - already per-block (`common/lib/settings.ts:1096-1241`). Deletes ~560 lines of - `SettingsForm`. e2e: `settings.spec.ts:190`. **M.** +3. **Per-operation settings on the operation page — SHIPPED** (`9515083` → `43bb519`; see + "Slice 3, as shipped" below). The Digest, Diarization, Speaker-work-lane and Speaker + 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 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 @@ -250,3 +249,56 @@ written into the file: if it ever branches on an operation id, stop. or the `/actionable` section id; touch `snapshot.backfill`, `BACKFILL_QUEUE`, or the widget's persisted `SectionId`; or fold `digestBucketAction` into the shared runner (it takes an explicit id set the runner has no parameter for). + +## Slice 3, as shipped + +Three commits, `9515083` (the plan) → `f14ceb5` (the seams) → `43bb519` (the move), plus the +copy and doc pass. The plan is [`editor-ia-slice-3.md`](editor-ia-slice-3.md). + +**The markers were the whole deletion.** Each of the four blocks in `saveSettingsAction` was +gated on a hidden `<input name="*FormPresent">`, and every one existed for a single reason: an +unchecked checkbox is ABSENT from a FormData, so one form saving everything meant a submit +from a form lacking a block would read that block's every switch as off. What that would have +cost is written in the old comments and is not small — a disarmed corpus sweep, a dropped +diarization capture lane (which lets the next Clean-audio sweep delete audio being held), +`allowRedownload` flipped on against a 97%-full disk, ~194,000 model calls armed. One form per +block retires all four, and the `...current` spread each block already did becomes the whole +isolation story. The four keys in `next` now read `getSettings().<block>` with the same +"edited on its own page; preserved here" comment `autoQueue` and `savedVideoBackup` carry. + +**Two decisions worth recording, because both could plausibly have gone the other way:** + +- **The lane's block is rendered per LANE, not per operation.** `settings.backfill` + (`enabled`, `weight`, `concurrency`, `allowRedownload`) governs `BACKFILL_QUEUE`, which + diarization, `attribution-text` and `attribution-diarized` share — so it is not any one + operation's setting and could not go on any one operation's page as such. It is rendered + inside `SweepOperationView` beside `SweepLane`, which is exactly where the shared pause and + the shared sweep already are: one definition, three renders, on every member page of the + lane. The digest lane has no equivalent form — one member, and its lane facts + (`sweepEnabled`, order and reach) are already in the digest block and the `OrderReach` + control. +- **`settingsBlock` is on the descriptor, not in a table keyed by id.** Same rule slice 2 set + for `runner` and `sweepLaneIdFor`. `settingsFormFor` in `/operations/[id]/page.tsx` + switches over the closed union `"digest" | "diarization" | "attribution"`, so a new block is + a type error rather than a page that quietly renders nothing — and both attribution + operations get one form because they declare one block, not because a table lists them + together. + +**A `role="status"` boundary now has to be stated.** `SaveBar`'s comment claimed the only +`role="status"` on "this page"; the truth is RUNNER pages, which is what +`auto-subs-replace.spec.ts:415` (`getByRole("status").first()` on `/operations/transcription`) +depends on. Sweep-fed pages now carry settings forms with their own status regions, they have +no policy tree, and the two never meet. The comment says so. + +**Specs.** The three form-driving tests moved out of `settings.spec.ts` into +`operation-settings.spec.ts` and repointed; the "unrelated save" one became the genuinely +cross-form test it always described (digest on its page, admin title on `/settings`). Two +blocks that had **no form-driving spec at all** — attribution and the lane — have one now, +including the one that proves what the markers used to buy: saving the diarization form beside +the lane form leaves `backfill.enabled` alone. 7/7. + +**What slice 3 deliberately did not do:** unify the three other file-local `Field` copies +(`ChannelForm`, `SiteForm`, `OverviewPanel` — they differ in props and markup, and the new +`components/forms/Field.tsx` says so at the top); touch `WorkersField` (slice 8), the pause +trio (slice 7) or `/actionable` (slice 4); change any persisted key or field `name`; or retire +a route — nothing redirects, because nothing moved off a URL.