Archilyzer · Source

archilyzer

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

commit 30f1621da1144c19431f08dd6c76efa5be4e7c08
parent 119ae2cde9ea19000406bf0fa5ffbe363d553800
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 28 Aug 2026 15:52:48 -0400

plans: slice 6 shipped, and the docs say so

CHANGELOG gets the operator-facing bullet: one panel per operation, the same
state words the channel counts use, a Run for one video, and a record whose
feature is switched off still shown and marked as such.

The IA bullet becomes SHIPPED with its hashes, and "Slice 6, as shipped"
records the six places the bullet was wrong — hasDigest belonged to the channel
count and not the page, hasAttribution was already dead, hasDiarization is a
live cleanup guard, the page was duplicating digest.state(), readVideoFiles had
never been called from editor/, and ids existed on the batch but not the job.

STATE gets the dated entry and a DONE line. FACTS gets the census and an "As
shipped" list, and the stale note about DigestPanel.tsx:144 ("metered —
disabled in Settings") is corrected: slice 3 fixed that copy and slice 6 moved
the file to DigestBody.tsx.

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/FACTS.md | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mplans/STATE.md | 21++++++++++++++++++++-
Mplans/editor-operations-ia.md | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
4 files changed, 161 insertions(+), 8 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **A video's page shows one panel per operation, not just the digest's.** Speaker diarization and both *Speaker names* operations wrote records that no per-video screen could show — and for diarization that record is often the only surviving evidence of audio the cleanup sweep has since deleted. Each operation now has its own panel: what produced its record (engine, models, threshold, which transcript the names were read from, how many chunks survived), and the **same state word the channel's own counts use** — *current*, *stale*, *partly stale*, *not generated*, *held — …* with the reason spelled out, or *waiting on …* naming what has to happen first. Each speaker panel carries a **Run** button that runs **that one video** on the speaker lane, in front of the sweep's work; the channel's Speakers stage still runs the whole channel. A record whose feature has since been switched off is still shown, marked *switched off*, with no Run button — hiding it is how a record that cost audio nobody has any more becomes invisible. Both *Speaker names* operations read the one `attribution.json` they share, so their panels differ in the state word and not in the record; the `method` line says which lane wrote it. The digest panel is unchanged apart from its heading, which is now **Digest** rather than *AI digest* — the name the rest of the editor already uses for it — and the page no longer works out the digest's freshness a second way of its own, so a video's panel and its channel's count cannot disagree. A re-run of a per-video run from **Jobs** stays per-video. **Nothing on disk changes.** - **One pause, drawn wherever a lane is — and the runner pages have one now too.** Four lanes could be held, and each of them had grown its own button: three components (one of which nothing imported), four inline descriptors on the dashboard, and a fifth on the sweep panels with the opposite emphasis and an aria-label of its own. They are one control now, over one pair of server actions, keyed by LANE rather than by operation — three speaker operations share one backfill queue and therefore one pause, so an operation page is not a per-operation switch. **Every button name is unchanged.** New: `/operations/transcription` and `/operations/download` have their pause beside **Start**, **Drain** and **Stop** — those three act on the runner, the pause holds the lane, and a held lane outlives any runner. A held runner lane now shows as *Holding* on the operations rail, which it could never do before. The transcription page also stops contradicting itself: pausing disables every worker, so it used to say "no enabled worker to run it" directly beside its own *Resume Transcriptions* button, and now says "transcriptions are paused". **Resume is always clickable** — on a lane with no workers, or one switched off, the pause is disabled and the resume is not, because a paused pool with zero workers has to be releasable. The operation page's hold button also adopts the dashboard's emphasis: filled while the lane is HELD, where it used to tint itself while the lane was running fine. **Nothing on disk changes** — the same four settings fields, and the Speaker lane's *Run the backfill lane* checkbox and the pause button still write the same one, on purpose. The monitor widget's own data feed renames `digest.paused` and `backfill.enabled` to `held`; a pinned widget tab shows that lane as not held until it is reloaded. - **"What needs doing" is answered on the page that does it, and `/actionable` is gone.** One page listed ten kinds of pending work, and it was the fourth place in the editor that answered the same question — so a channel with undownloaded videos appeared on the dashboard, on `/channels`, on `/operations` and there, four times, in four vocabularies. Each of its sections now sits with the work: **Download** and **Transcription** each carry the channels with that operation's backlog and its attention items (videos lost before they were ever fetched, truncated downloads, truncated transcripts — the ones a runner cannot simply retry), **Digest** carries the passes that recorded warnings, `/cleanup` carries the two reclaimable-audio tables, and `/channels` carries how fresh each channel's report is, with *Refresh report* on the row and *Update all reports* in the header — the report is the caveat on every count in that row, so it belongs beside them. Duplicate clusters and media-integrity findings get a new **Review** page under *Corpus*: both are flag-only, both a human decides, and neither is an operation or a pile of bytes. `/actionable` redirects to `/operations`, so every bookmark still lands. **Nothing on disk changes** — no report field, no setting, no widget section. The dashboard's *No digest* badge and column are now **Digest to do**: it is the same number under its right name, the figure the digest lane could act on today, which is what it has always been. The monitor widget's own data feed renames its `noDigest` field to `digestReachable` to match; a pinned widget keeps working and shows the same figure. - **Re-acquired audio is handed to auto-transcribe instead of being deleted under it.** On a channel that only downloads subtitles, the audio the speaker lane fetches sits next to YouTube's own auto-captions — which is precisely the shape the *replace auto-captions* transcription runner looks for, and the channel's report is rebuilt about a second after any work on it finishes. Nothing coordinates the two, so the runner could start on a file the speaker lane was about to delete: the transcription then fails, and the video is written to the channel's permanently-honoured failed list. The lane now checks, at the moment it would delete, whether the transcription policy would take this video — and if it would, leaves the audio for it, saying so per video and in the run's summary line. That audio then behaves like any other download: it stays until you run *Clean audio from transcribed*. Audio is still deleted immediately in every other case, still kept for a video marked *do not clean*, and the hand-off is refused when free disk is below the mark the download runner itself would need — the lane never keeps a file the runner would have refused to fetch. diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -2313,7 +2313,9 @@ off in Settings, so nothing dispatches it"), `operations/components/SweepLane.ts 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 +and `videos/[id]/components/DigestPanel.tsx:144` ("metered — disabled in Settings"). *(Both +were fixed by slice 3; the digest one now reads "metered — disabled on the Digest page" and, +since slice 6, lives in `videos/[id]/components/DigestBody.tsx`.)* 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. @@ -2742,3 +2744,74 @@ Worse, `/operations/transcription` said **"no enabled worker to run it"** while - New idle reason `"workers-paused"`, checked before the `no-workers` branch in `autoRunner.ts`'s `limit()`, mapped in `dispatch.ts` ("transcriptions are paused") and `buildActiveJobs.ts` ("transcriptions paused globally"). + +## Verified 2026-08-28 — editor IA slice 6 seams (the video page, one panel per operation) + +Census taken before writing `editor-ia-slice-6.md`, against `ce2ebd9`. Read-only observation +at census time; the "As shipped" notes were added with the docs commit. + +**`hasDigest` had ONE caller and it was not the video page.** `controller/channels.ts:94`, +inside `countDataFiles` (`:67`) under `listChannelStatsFromDisk` — the batch ground-truth +walk. Identity-blind on purpose (`digest-server.ts:84-90`: "is there a digest at all", never +"is it current"), and `channelProjection.test.ts:66-67` pins that a record with a non-empty +section counts. Defined at `digest-server.ts:91-98`, not `:89` as the IA bullet said. + +**`hasAttribution` was already dead** — `attribution-server.ts:36-38`, zero callers across +`common editor export homepage umtool mcp`. **`hasDiarization` is NOT dead**: +`cleanAudioFromTranscribed.ts:125` (the audio-deletion guard), `diarizeOne.ts:185`, +`diarization.test.ts:57-92`. `files.hasDiarization` on `VideoFiles` is a separate listing flag. + +**The page duplicated the digest operation's `state()`.** `page.tsx:155-184` called +`resolveDigestTarget` with `lane: "local"` and folded `isSectionFresh` + the `derivedFrom` rule +per section; `digest.state()` (`operations.ts:1104-1185`) folds the same rule at `:1153` and +`:1176-1178`, and its `resolveTarget` (`:1096-1103`) calls the same resolver with no lane, +which `resolveDigestChunking` (`digestTarget.ts:69-79`) resolves to `localAppId` — the same +identity. Dropping the copy changed no displayed value. + +**`readVideoFiles` had never been called from `editor/`.** The page's own `loadVideoDir` +(`page.tsx:57-77`) is a `readdir` + per-file `stat` for size/mtime, which the Files card needs, +so it stays; `readVideoFiles` is a second `readdir` and is the registry `state()`'s probe. + +**`ids` existed on the batch and not on the job.** `runBackfillBatch` took `ids?: string[]` +(`backfillBatch.ts:99`, intersected with disk at `:290-291`); `BackfillChannelJobOptions` +(`operationJobs.ts:60-75`) did not, and `countBackfillWork` (`:869-926`) walked every dir with +no filter. `jobReplayRegistry.ts:281-284` read `p.kindIds` only. `digestBucketAction` +(`digestActions.ts:83-135`) is the precedent for an id-scoped job on the other lane. + +**Client-graph hazard, carried through.** `lib/operations.ts` imports `digest-server`, +`diarization-server`, `attribution-server` and `videoStatus` — all `node:fs`. Every client file +in this slice imports from it as `import type` only. `lib/digest.ts`, `lib/diarization.ts` and +`lib/attribution.ts` have no imports at all and are safe for types and pure values. + +### As shipped + +- `common/controller/videoOperations.ts`: `inspectVideoOperations` (one `readVideoFiles`, one + `resolveTarget` + `state()` per entry, in `OPERATIONS` order, filtering nothing), + `shownOnVideoPage` (`enabled || any output present`), `orderForVideoPage` + (`OPERATION_GROUP_ORDER` then registry order — a stable sort, so digest comes first and the + three speaker operations keep their registry sequence). `VideoOperationView.target` is the + erased `unknown` and never crosses to a client. +- `digestSectionStates(record, sections, target)` in `lib/digest.ts` is the ONE fold; + `operations.ts:1176` counts through it. The `derivedFrom` early return at `:1153` stays, so + behaviour is identical. +- `hasDigest` → private `hasDigestWithItems` in `channels.ts`; `hasAttribution` deleted. +- `ids?: string[]` on `BackfillChannelJobOptions`, through to `countBackfillWork` (which now + intersects the dir list with it before the walk) and `runBackfillBatch`; in `spec.params`; + parsed in `jobReplayRegistry.ts` exactly as `kindIds` is; a fourth argument on + `backfillChannelAction`. `backfillSweep.ts:168` (three args) is unaffected. +- `editor/app/channels/[slug]/videos/[id]/`: `components/operationState.ts` (the state→words + table + `stateTone`, pure, no directive, `import type` from `lib/operations`), + `components/OperationPanel.tsx` (the frame: `${id} panel`, `${id} freshness`, `${id} off`, + and an exhaustive body switch whose `undefined` arm renders `${id} outputs`), + `components/DigestBody.tsx` (the renamed `DigestPanel`, frame removed, everything else + byte-for-byte), `components/SpeakerBodies.tsx`, `lib/videoOperationPanels.ts` (server-only; + loads one body per `settingsBlock`, reads `attribution.json` ONCE for both attribution + views, strips `target`), `operationActions.ts` (`"use server"`, functions only). +- The digest heading is `view.label` — **"Digest"**, not "AI digest". No spec pinned the old + string; `sections.tsx:154` is the only remaining "AI digest" in `editor/`, and it is prose. +- `runOperationForVideoAction` is the SPEAKER lane only. Digest keeps `digestBucketAction` + (its own queue, its own guards). No `queueKey`, no `background`. +- Left alone deliberately: `readVideoDigestAction` (`digestActions.ts:149`) still has **zero + callers**; the two-pane `/channels/[slug]/videos?video=` route is still `VideoPanel`-only; + `common/components/TranscriptModal.tsx` has its own unrelated local `DigestPanel` (the + published player's), which is why a repo-wide `DigestPanel\b` grep is not zero. diff --git a/plans/STATE.md b/plans/STATE.md @@ -3,7 +3,21 @@ 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-28 — **editor IA slice 7 shipped** (`c924f73` → `685cba3`): one +**Last updated:** 2026-08-28 — **editor IA slice 6 shipped** (`6986efc` → `d5ca90a`): the +video page shows one panel per registry operation. `common/controller/videoOperations.ts` is +the reader — one `readVideoFiles`, one `resolveTarget` and one `state()` per entry, never +re-derived — and `digestSectionStates` in `lib/digest.ts` is now the ONE per-section fold the +digest operation's `state()` and the panel both count through, so the page's duplicate of it +is gone. `DigestPanel` is the digest's BODY inside a generic `OperationPanel`; diarization and +both attribution operations get bodies of their own, each with a **Run** that runs that one +video on the speaker lane (`ids` now threads through `runBackfillChannelJob` → +`countBackfillWork` → the batch, and rides in `spec.params` so a replay stays scoped). A +record whose feature is switched off is still shown, marked *switched off*. `hasDigest` was +the channel COUNT's helper, not the page's — it is now private to `channels.ts`; +`hasAttribution` was dead and is deleted; `hasDiarization` is the live cleanup guard and +stays. Every digest aria-label is byte-identical; the heading is "Digest" rather than "AI +digest"; nothing on disk changed. See "Slice 6, as shipped" in `editor-operations-ia.md`. +Previously: 2026-08-28 — **editor IA slice 7 shipped** (`c924f73` → `685cba3`): one pause. `common/lib/pauseGates.ts` is the only place the four gates' polarity is known (`backfill.enabled` is inverted), `pauseLaneAction` / `resumeLaneAction` replace eight actions in two files, and `components/lanes/pauseControl.tsx` is the one control every lane @@ -243,6 +257,11 @@ nothing renders. seams". It is also **half of unified-ops step 5**: one definition and one writer among the controls, but the four settings fields are still four settings fields — step 6 moves the storage behind `isGateHeld`/`withGateHeld`. +10. ~~**Editor IA slice 6** (the video page: one panel per operation)~~ — **DONE 2026-08-28**, + `6986efc` → `d5ca90a`. Plan: [`editor-ia-slice-6.md`](editor-ia-slice-6.md); outcome: + "Slice 6, as shipped" in `editor-operations-ia.md` and the FACTS section "Verified + 2026-08-28 — editor IA slice 6 seams". The registry's per-video reader is + `common/controller/videoOperations.ts`, and a fifth operation now needs no page. --- diff --git a/plans/editor-operations-ia.md b/plans/editor-operations-ia.md @@ -148,12 +148,15 @@ dependencies allow. Sizes are S/M/L. aliases,publish}`; `?site=` stays for Dashboard and Channels. Deletes five pages. e2e: `build.spec.ts`, `deploy-page.spec.ts`, `site-scope.spec.ts:63-87`, `aliases.spec.ts`, `channel-build-toggle.spec.ts:17`. **M.** -6. **The video page: one panel per operation.** `DigestPanel.tsx` generalizes to - `OperationPanel {state, target, provenance, sections?}`, built per enabled kind from - `readVideoFiles()`; deletes `hasDigest()` (`digest-server.ts:89`) and `hasAttribution()` - (`attribution-server.ts:36`). Attribution and diarization get a per-video view for free. - The **"Open in umtool" link** (`VideoPanel.tsx`, `data-umtool-link`, landed in `818bfc7`) - stays above the panels, unchanged: it is a Corpus→umtool bridge, not an operation. **M.** +6. **The video page: one panel per operation — SHIPPED** (`6986efc` → `d5ca90a`; see + "Slice 6, as shipped" below). `DigestPanel.tsx` is the digest's BODY inside a generic + `OperationPanel`, built per registry entry from `inspectVideoOperations`; attribution and + diarization get a per-video view, and a Run for one video with them. The **"Open in umtool" + link** (`VideoPanel.tsx`, `data-umtool-link`, landed in `818bfc7`) stays above the panels, + unchanged: it is a Corpus→umtool bridge, not an operation. The bullet was wrong in six + places and the section below records each — most consequentially, `hasDigest()` was the + channel COUNT's helper and not the page's, `hasAttribution()` was already dead, and the + page was duplicating the digest operation's own `state()`. **M.** 7. **One pause — SHIPPED** (`c924f73` → `685cba3`; see "Slice 7, as shipped" below). unified-ops step 5 as reconciled above. The bullet was wrong in four places and the section below records each: there were EIGHT actions in two files over FOUR gates with @@ -376,6 +379,63 @@ cleanup tables were not merged into `ChannelCleanupCard`. "Uncertain attribution attention section: no bucket or confidence field exists yet, so it would be net-new rather than a move. +## Slice 6, as shipped + +Five commits: `6986efc` (the plan) → `731f9fa` (`common/controller/videoOperations.ts` + its +test, `digestSectionStates`, `hasDigest` moved, `hasAttribution` deleted) → `1eae26e` (`ids` +through the backfill channel job) → `6966239` (the panels) → `d5ca90a` (the per-video Run and +the e2e). The plan is [`editor-ia-slice-6.md`](editor-ia-slice-6.md). + +**`hasDigest` was the CHANNEL COUNT's, not the page's, and the bullet above had it backwards.** +Its one caller was `countDataFiles` in `controller/channels.ts` — the batch ground-truth walk +under `listChannelStatsFromDisk`. It is identity-blind on purpose ("is there a digest at all", +never "is it current"), and `channelProjection.test.ts` pins that a record with a non-empty +section counts. So "deletes `hasDigest()`" is a MOVE: it is now the private +`hasDigestWithItems` in `channels.ts`, body and comment verbatim, and the export is gone. What +being exported cost was legibility — it read as a general "is this video digested", which is +the second definition of digested the registry's `state()` exists to be the only one of. + +**`hasAttribution` was already dead** (zero callers across `common editor export homepage +umtool mcp`) and is deleted. **`hasDiarization` is NOT dead and stays** — it is the +audio-deletion guard in `cleanAudioFromTranscribed.ts`, plus `diarizeOne.ts` and its test. The +`files.hasDiarization` flag on `VideoFiles` is a different thing (a listing flag) and is +untouched. + +**The page was duplicating the digest operation's `state()`.** It called `resolveDigestTarget` +itself with `lane: "local"` and folded `isSectionFresh` plus the `derivedFrom` rule per +section, beside the registry's own fold of exactly the same rule — and the registry's +`resolveTarget` calls the same resolver with NO lane, which resolves to `localAppId`, the same +identity. `digestSectionStates(record, sections, target)` in `lib/digest.ts` is now the one +fold: `digest.state()` counts through it and the panel's per-section rows are its output, so a +video's panel and its channel's work list cannot disagree about one section. No displayed +value moved. + +**The external operations get no panel, deliberately.** Download, transcode and transcription +already have a per-video surface — the `PipelineStageCard`s inside `VideoPanel.tsx`, which +carry their own actions. `inspectVideoOperations` is over `OPERATIONS` (the derived-data +registry) and the asymmetry is stated in its header rather than left to be rediscovered. + +**`ids` on the channel job is what makes a per-video Run possible.** `runBackfillBatch` +already took `ids` and intersected them with disk; `BackfillChannelJobOptions` did not, and +`countBackfillWork` walked every dir with no filter — so a per-video job would have sized its +progress bar to the whole channel. `ids` now goes through the job to both, and it is in +`spec.params` beside `kindIds`, so a replayed per-video run stays per-video. +`runOperationChannelJob` is unchanged; the dispatcher stays channel-scoped. + +**A shown-but-disabled operation says "switched off".** `shownOnVideoPage` is `enabled || +any output present`, so a sidecar captured before its feature was switched off stays visible — +and without the extra word its pill would read "stale — regenerating would replace this" +beside nothing that can regenerate it. The Run button is not drawn for it, and +`runOperationForVideoAction` refuses on the same condition by name. + +**The vision test now holds for the video page.** `OperationPanel`'s body switch has an arm +for an operation with NO settings block, which renders its outputs list — so a fifth registry +entry gets a panel, a state word and its outputs with nobody editing a component. The state +words are one table (`components/operationState.ts`); `digest.spec.ts`'s pill contract +("not generated", "current", and a stale string containing "stale") survives verbatim, and +`partial` deliberately contains "stale" so a config change that leaves a video part-done still +satisfies it. + ## Slice 7, as shipped Five commits: `c924f73` (the plan) → `fdef74c` (`common/lib/pauseGates.ts` + its test, and the