Archilyzer · Source

archilyzer

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

commit a74bab1f0f5ab7c20c5d70aa19d1c2a686f73926
parent 596c29638db9d1b659999b6285133aaefd7167d4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu, 24 Sep 2026 20:26:20 -0400

plans: release 4 record — Phase 3 complete (main @ 596c2963)

one-core-phase-3.md: the release-4 record (prelude, P/W/3b merges, order, gates,
record corrections) and "Next — Phase 4" in place of the slice-3b inventory.
one-core.md: 3b and W "as shipped", Phase 3 COMPLETE, Phase 4 starting points.
FACTS.md: release-4 anchors (rack ladder, station rule, band divergence,
writeFileAtomic/copyFileAtomic, cards) and the anchors this release made stale.
STATE.md: head, phase table, decisions. Final full-suite numbers pending.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mplans/FACTS.md | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mplans/STATE.md | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mplans/one-core-phase-3.md | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mplans/one-core.md | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
4 files changed, 630 insertions(+), 114 deletions(-)

diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -2442,7 +2442,8 @@ operation on in Settings"), `channels/components/ChannelGroupLine.tsx:184` (lane in Settings" title), `channels/groupActions.ts:108` (skip reason), `stages/DigestStage.tsx:210` 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 +since slice 6, lives in `videos/[id]/components/DigestBody.tsx`. The group skip reason is +now "the lane is switched off on its operation page", at `groupActions.ts:186` on `e172749b`.)* 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. @@ -3672,7 +3673,8 @@ four instances, and **every known instance on the video page is closed as of `12 `savedVideo` was truthy, `IncompleteTranscriptBanner` and `ShortAudioBanner` were dropped by the parent when the run cleared the condition that drew them, and `PerFileTranscribeRow` swapped its panel for "Transcribe disabled — transcript already present." when its own run -wrote transcript.json (`hasTranscript` is server-derived at `VideoPanel.tsx:360`). +wrote transcript.json (`hasTranscript` is server-derived at `VideoPanel.tsx:360`; since +slice 3b, `VideoPanel.tsx:122` on `e172749b`). **THE SHAPE TO COPY** is the same in all four and is what a fifth should be written as: the parent renders the component unconditionally and passes the CONDITION down; the component holds @@ -3681,11 +3683,14 @@ a `ranHere` flag set inside the `trigger` it hands `StreamActionLog`, returns nu and keyed), and passes the now-cleared condition to `StreamActionLog`'s existing `disabled` prop — so the panel that stays for its log is not a second Run button (`VideoPanel.tsx:1085-1120` persist, `1550-1588` incomplete, `1638-1670` short-audio, -`729-756` the transcribe row, at `12d1778`). A banner's +`729-756` the transcribe row, at `12d1778`; since slice 3b each is its own file under +`videos/[id]/components/cards/`: `SourceVideoSection.tsx:38,64`, +`IncompleteTranscriptBanner.tsx:38-39`, `ShortAudioBanner.tsx:40-41`, +`PerFileTranscribeRow.tsx:39,62` on `e172749b`). A banner's `role="alert"` and warning aria-label are spread conditionally on that same condition, so the box that outlives the warning stops answering `getByLabel("incomplete transcript")` — `truncated-check.spec.ts:69` asserts exactly that. `PerFileTranscodeRow` (`:810` at `12d1778`) -is NOT an instance: its `return null` depends on the file's extension against a static format +(`cards/PerFileTranscodeRow.tsx:39` since 3b) is NOT an instance: its `return null` depends on the file's extension against a static format list, which a transcode cannot change, and the rows are keyed by file name. The persist case needed a fixture: `editor/e2e/fixtures/bin/fake-ytdlp.mjs:680-725` (at @@ -5858,7 +5863,9 @@ Not in the 14: - `metadataScanStore.ts:188-206` and `autoQueueState.ts:141-156`. They carry a module-level `writeSeq` (`${file}.tmp-${pid}-${seq}`), which is unique per module copy, not per process — the hazard review finding 1 fixed in `jsonFile-server` by pinning its counter on - `globalThis`. Owed: the same fix, or `writeJsonAtomic`. + `globalThis`. Owed: the same fix, or `writeJsonAtomic`. **Done by slice W (`9bfd15cd`, + merged `ddad13f4`):** both counters deleted, both writers on `writeJsonAtomic` + (`metadataScanStore.ts:200`, `autoQueueState.ts:155`); see the release-4 section below. - The non-JSON writers. **The numbers tool** is `plans/tools/phase3-files-numbers.ts` (317 lines). It is one process, @@ -5977,3 +5984,213 @@ Slice 4b (merged `ef88ac4d`) deleted or moved: - `:609` → `:602` - `:618-621` → `:611-614` - `:571` → `:564` + +## One-core Phase 3 release 4 — slices P, W, 3b (verified 2026-09-24, `main` @ `e172749b`) + +Records: `one-core-phase-3.md`, "Release 2026-09-24 (evening)", and the three "as shipped" +sections above it (P, W, 3b). Every `file:line` below was grepped at `e172749b`. Phase 3 is +complete with this release. + +### The /channels rack's layer ladder — slice P (merged `77a32de2`) + +- **One ladder, one module.** `editor/app/channels/components/rackLayout.ts` is plain data, + deliberately not `"use client"`. It holds: + - `RACK_LAYERS` `:37-43`: `popover` z-40 > `thead` z-30 > `groupHeader` `md:z-20` > + `identity` z-10. `deck` z-20 sits beside the ladder, not in its order. + - `RACK_IDENTITY` `:53-56`: `checkboxWidth` `w-9`, `slugLeft` `left-8`. + - `RACK_BRIDGE` `:59`. + + The header comment `:9-35` explains why the checkbox and Slug cells share **one** z. Two + sibling cells at one z-index paint in DOM order. The Slug cell comes later, so it covers + the 4 px the two cells overlap, and that overlap closes the seam `20ee34db` found. That + commit had closed the seam by lifting Slug to z-20, which TIED it with the group header: + scrolled down and right, every later row's slug band painted over the group header and + took its clicks. +- **The width invariant** is `slugLeft` (32 px) < `checkboxWidth` (36 px), stated at `:45-52`. + Equal values reopen the sub-pixel seam. `rackLayout.test.ts:13` parses both numbers. `:19` + checks that the ladder's order is strict. +- **Class sites read the tokens.** Every entry is a complete literal, because Tailwind v4 + scans source for them, so never build one as `z-${n}`. The readers: + - `ChannelsTable.tsx:277,389,553`; + - `channelColumns.tsx:123,127`; + - `ChannelGroupHeaderRow.tsx:53`; + - `ChannelTierSelect.tsx:232`, with `max-md:scroll-mb-48` so the panel stops above the deck; + - `ChannelSelectionDeck.tsx:130`. + + `grep -rn 'z-[0-9]' editor/app/channels/components --include='*.tsx'` is empty. +- **The region is `isolate`** (`ChannelsRack.tsx:221`, comment `:212-217`), so the ladder is + ordered inside the region and not against the page. It carries + `data-testid="channels-rack"`. `ChannelSelectionDeck` stays **outside** it (`:245`). +- A group header's name and stations pin left: `ChannelGroupHeaderRow.tsx:63`, + `sticky left-2 max-w-[calc(var(--rack-w,100%)-1rem)]`. The section rule is the th's inset + shadow (`:52`), not the `<tr>`'s border, because a collapsed border stays in the grid when + the th pins. + +### The transcribe station counts both buckets and queues by id — slice P + +- **The one fold** is `transcribeStationIds(snapshot)` (`common/views/channelGroupSections.ts:114-123`). + It returns `{missing, autoSubs}`: + - `missing` is `downloadedNoTranscript` minus `excludedDownloadIdSet`; + - `autoSubs` is `downloadedAutoSubsOnly` minus the same set. + + The two lists are disjoint. `stationWorkFor`'s transcribe branch (`:187-199`) returns + `missing.length + autoSubs.length` and never consults `handling`. `"social account"` + (`:172-174`) is now the only ineligible case. +- **The group action queues exactly those ids.** `transcribeChannel` is at + `editor/app/channels/groupActions.ts:93-127`: + - With a snapshot it runs `transcribeBucketAction(slug, ids.missing, …, + "downloadedNoTranscript")` (kind `whisper-bucket-downloaded-no-transcript`) and + `transcribeAutoSubsBucketAction(slug, ids.autoSubs)` (kind `whisper-bucket-auto-subs`). + - The scan, `transcribeMissingAction` (a `whisper-all` with no ids), runs **only** when there + is no snapshot (`:97`). + - Why by id: `downloadedNoTranscript` does not filter excluded ids, so a scan would also + transcribe a video downloaded before it went private. + - `jobIds` carries the first ok half's id (`:116`), and the other ok stream is cancelled. + A refused half is `console.warn`ed (`:122`), because `StreamActionResult`'s ok arm has no + message field. +- **The dedupe spans all three kinds.** `KIND_FOR.transcribe` (`groupActions.ts:61-65`) is + `whisper-all`, `whisper-bucket-downloaded-no-transcript` and `whisper-bucket-auto-subs`. + `activeSlugsForKinds(KIND_FOR[station])` is at `:209`. All three run on + `TRANSCRIPTION_QUEUE`. +- **The band/station divergence is on purpose.** The rack's transcription band counts + `downloadedNoTranscript` **alone**: `common/views/pipeline/buildBands.ts:168-172` + (`transcription.reachable += downloadedNoTranscript`, rule stated at `:104`). The stage + title lists auto-captions as informational only (`common/views/pipeline/stageStatus.ts:342-352`). + On a YouTube channel with many auto-caption-only videos, the station's figure is higher than + the band's. The station counts what its button queues; the band counts the default lane's + reachable work. `channelSnapshot.ts:581-587` warns that the two must not be merged; its last + line names the retired path `editor/app/components/pipelines/buildBands.ts`. + +### One write idiom — slice W (merged `ddad13f4`) + +`common/lib/jsonFile-server.ts` (268 lines): + +- The header `:1-5` says what is left on its own temp name: see + `one-core-phase-3.md`, "Slice W, as shipped". +- Header `:33-35`: **the chain serialises WRITES, not read-modify-write cycles.** A + read-modify-write takes `withJsonFileLock` (`:251`), and only `patchChannelConfig` does. +- `tmpPathFor` `:134-137` builds `${file}.tmp-${pid}-${seq}-${hex8}`. The state is on + `globalThis.__yttJsonFile__` (`:114-131`). +- `chained(key, op)` `:152-163` is the one per-absolute-path chain. `replaceNow` `:166-180` is + mkdir?, temp, fill, `rename`. On any failure it runs `rm(tmp, {force: true})` and rethrows + the original error. +- **`writeFileAtomic(file, data: string | Buffer, {mkdir, mode})` `:186-198`.** `mode` goes to + `writeFile(tmp, data, {mode})`, so the bytes are never more widely readable, even before + the rename (options doc `:139-147`). +- **`copyFileAtomic(src, dest, {mkdir})` `:203-212`** copies into a temp beside `dest` and + renames. It is on the same chain, keyed on `dest`. +- `writeJsonAtomic` `:217-223` is `jsonText` (`:107`) → `writeFileAtomic`. + `writeJsonAtomicSync` is at `:229`. +- **Tests**, in `jsonFile-server.test.ts`: + - `:72` two module copies share one chain; + - `:132` exact string and Buffer bytes; + - `:147` mode 0o600 on the file and on the temp. It intercepts `rename` via `node:fs/promises` + + `syncBuiltinESMExports`; `:169` asserts that exactly one temp was seen; + - `:173` `writeFileAtomic` and `writeJsonAtomic` share one chain on one path; + - `:190` copy; + - `:202` the remark literal. + + Also `controller/compactJsonWriters.test.ts` (the four compact writers' bytes) and + `jobs/autoQueueState.test.ts` (12 overlapping writes, the last one issued lands, no temp + left). +- **Call sites** (a sample): + - `rosterStore.ts:236` (mkdir); + - `metadataScanStore.ts:200`; + - `autoQueueState.ts:155` (mkdir; the old counter's comment is replaced at `:139`); + - `xSessionBroker.ts:218` (0o600); + - `savedVideo-server.ts:78` (`copyFileAtomic`); + - `normalizeTranscript.ts:141` (`{indent: 0, newline: false}`); + - `videos/[id]/videoActions.ts:499` (VTT promote) and `:609` (the remark, + `{transcription: []}` at `{indent: 0}`). +- **The two write counters are deleted.** `git grep -n 'writeSeq\|nextWriteSeq' -- common + editor` is empty. +- **What `git grep -n 'tmp-${process.pid}' -- common editor` still finds:** + - `buildIndex.ts:971` and `buildStats.ts:220`, the export page writers, out of scope; + - `transcode.ts:31` (ffmpeg's output, renamed at `:55`); + - `transcribeOne.ts:142` (the transcription app's `outputBase`); + - the shared writer's own comment `:9`, code `:136`, and test `:68`. + + `transcribeOne.ts:173` (`if (bytes) await writeFile(transcriptPath, bytes)`) has no temp + at all, so this grep cannot see it. +- **Temp-name readers** match either shape: `videos/[id]/page.tsx:63` and `videos/page.tsx:50` + use `includes(".tmp-")`, and `cards/videoFiles.ts:43` uses `startsWith("audio.tmp-")`. + +### The video page's chore cards — slice 3b (merged `e172749b`) + +- **`editor/app/channels/[slug]/videos/[id]/components/cards/`** holds 17 files: the 15 card + modules, `Heading.tsx` (shared by the two per-file rows) and `videoFiles.ts` (`VideoFile`, + `WHISPER_FILENAME`, `CANONICAL_VTT`, the extension predicates, `mediaUrl`). + - Children stay with their only caller: `PersistedSourceVideo` is in + `SourceVideoSection.tsx:96` and `DeleteFileButton` in `FilesList.tsx:67`. + - `VideoNavStrip.tsx` (60 lines) and `PipelineStatusStrip.tsx` (51) sit beside `VideoPanel`, + not in `cards/`. +- **`VideoPanel.tsx` is the assembly**: 455 lines (1,839 at `4130aca1`). `VideoPanel()` starts + at `:96`. `:49` re-exports `FetchedWindow` and `VideoFile`, so + `videos/[id]/page.tsx:33` and `channels/[slug]/videos/page.tsx:26` did not change. + `hasTranscript` is derived at `:122`. +- **`lib/videoChoreCards.ts`** (149 lines) holds `VideoChoreCard` `:46` and + `VIDEO_CHORE_CARDS` `:58`: fifteen rows `{id, component, shown, why}` in render order. + Nothing imports it at runtime. `videoChoreCards.test.ts` (`:23`, `:28`, `:36`) reads + `VideoPanel.tsx` as text and pins three things: fifteen distinct components, the rendered + order equal to the list, and the `./cards/` imports equal to the list. The registry + operations (digest, diarization, attribution) still render through `videoOperationPanels.ts` + + `OperationPanel`, after `<VideoPanel>` in `videos/[id]/page.tsx`. +- **The stage half needed no change.** `computeStageStatuses` + (`common/views/pipeline/stageStatus.ts:192`) and `computeChannelFlow` (`channelFlow.ts:220`) + are each called once, at `channels/[slug]/page.tsx:256` and `:308`. `stageOrder` is at + `:278-286`, around `GROUP_STAGES` (`stageStatus.ts:95`). +- **The ranHere banners, now one file each.** This is the shape the 2026-09-08 entry above + describes: + - `SourceVideoSection.tsx:38,64`; + - `IncompleteTranscriptBanner.tsx:38-39`; + - `ShortAudioBanner.tsx:40-41`; + - `PerFileTranscribeRow.tsx:39,62`. + + `PerFileTranscodeRow.tsx:39` is the static `return null`, which is not an instance. + +### Anchors this release made stale + +Line numbers are `plans/FACTS.md` lines at `e172749b`, before this record's in-place edits. + +**Corrected in place** (the grep the release-4 docs brief named: +`VideoPanel.tsx:`, `groupActions.ts:`, `channelGroupSections.ts:`, `metadataScanStore.ts:`, +`autoQueueState.ts:`): +- `:2442`: `channels/groupActions.ts:108`. This is historical: slice 3 fixed the copy. The + skip reason now reads "the lane is switched off on its operation page", at `:186`. A + pointer was added. +- `:3346`: `jobs/autoQueueState.ts:23-25`. **Holds**; W's edits are below it. +- `:3675`, `:3683`: the `VideoPanel.tsx` anchors "at `12d1778`". Historical. Their current + homes (the ranHere list above) were added in place. +- `:5573`: `channelGroupSections.ts:66-72` (`ChannelGroupSectionView`). **Holds**. P's edits + are below it, and the type is still at `:70`. +- `:5858`: "`metadataScanStore.ts:188-206` and `autoQueueState.ts:141-156` … Owed". Both + counters were deleted by W (`9bfd15cd`), and the entry now says so. + +**Moved by P** (the 3a entry's text still holds; only the numbers changed): +- `ChannelsRack.tsx` is 263 lines, not 250. The header `:23-28` holds. `--thead-h` is + `:136-142`, not `:132-138`. The deck is `:245`, not `:229`. `ChannelsTable` is drawn at + `:223-239`, not `:210-226`. +- `ChannelsTable.tsx`: + - the function is `:184`, not `:183`; + - the `colSpan` count is `:247-255`, not `:246-254`; + - the dim is `:533-536`, not `:527-530`, with its comment at `:519-532`, not `:513-526`. +- `channelColumns.tsx`: + - `CHANNEL_COLUMNS` is `:115`, not `:114`; + - the re-exports are `:20-29`, not `:19-28`; + - the registry-by-id comment is `:31-40`, not `:30-39`. + +**Moved by W** (the 4b entry for `jsonFile-server.ts`; the text holds unless noted): +- `readJsonFile` is `:74`, not `:72`. `readJsonFileSync` is `:86`, not `:84`. +- `writeJsonAtomic` is `:217`, not `:158`. The chain is `chained` `:152-163`, not `:165-176`. +- `writeJsonAtomicSync` is `:229`, not `:183`. `withJsonFileLock` is `:251`, not `:205`. +- `tmpPathFor` is `:134-137`, not `:132-135`. The state is `:114-131`, not `:112-129`. +- `jsonText` is `:107`, not `:105`. The bytes header is `:37-41`, not `:35-39`. The + per-process header is `:28-31`, not `:26-29`. +- The second-module-copy test is `jsonFile-server.test.ts:72`, not `:67`. +- The 4b entry "**Owed: JSON writers still on the per-pid temp name**" (16 sites in 13 + files) is **done**: W folded all of them. The two `failedTranscriptions` sites went onto + `writeFileAtomic` as text. +- The "Reusable helpers" row at `:166` ("tmp+rename write idiom | `normalizeTranscript.ts:139-141`") + is historical. `normalizeTranscript.ts:141` is now `writeJsonAtomic(cuesPath, out, + {indent: 0, newline: false})`. The idiom for any file is `writeFileAtomic` / + `writeJsonAtomic`. diff --git a/plans/STATE.md b/plans/STATE.md @@ -3,58 +3,89 @@ 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. -**Live on :3001 (2026-09-24, 18:42):** `9ab10d77` (release 3), `BUILD_ID` `XsKaA_drqdAbTguxUGVsn`; smoke clean, boot and a form save rewrote nothing but the saved channel's key order; the one-sync md5 sweep is owed — YouTube held a 429 cooldown across three attempts (18:43, 19:13, 19:41) — [rollout record](one-core-phase-3.md#rollout-2026-09-24--9ab10d77-live-on-3001). - -**Last updated:** 2026-09-24 — **release 3: one-core Phase 3 slices 3a and 4b are on `main` @ -`ef88ac4d`.** Rollout prelude first (`c0a90a37`, `1e27f7c3`: the backfill lane form's -re-acquire figures derived, release 2 live on :3001 — rollout record: -[`one-core-phase-3.md`](one-core-phase-3.md#rollout-2026-09-24--b836ea8a--two-prelude-commits-live-on-3001)), -then two worktrees off `1e27f7c3`, merged in the order `one-core/phase-3-s3a` → `3241fed2`, -then `one-core/phase-3-s4b` → `ef88ac4d`. Release record: -[`one-core-phase-3.md`](one-core-phase-3.md#release-2026-09-24--rollout-slice-3a-slice-4b), -with the two "as shipped" sections below it; anchors: -[`FACTS.md`](FACTS.md#one-core-phase-3-slices-3a--4b-verified-2026-09-24-main--ef88ac4d). -Previous: 2026-09-23 — slices 2 and 4a on `main` @ `ae2fa5a9`, gate B done -([record](one-core-phase-3.md#release-2026-09-23--slices-2-and-4a)); release 2 went live on -:3001 on 2026-09-24 as `1e27f7c3`. - -- **Slice 3a — one drawing per noun (channel row + job in flight).** One pure channel-row - builder (`common/views/channelRow.ts`, no config on the wire) and one per-channel counter - set (`actionableCounts.ts`); one `ChannelsTable` drawn by the `/channels` rack - (`ChannelsRack`), the dashboard and every operation page's work table, columns named by id - from a plain preset module. One `JobRow` (`table` | `card` | `compact`) + `JobRowActions` - drawn by `/jobs`, the card lists, the widget and the operations board's in-flight list - (via `fromInFlight`; the runner now records a download unit's `jobId`). `LaneStrip` keeps - its lane line. No number moved (`phase3-view-numbers.ts`, diff empty, 5,925 bytes). The - `stages/*` status logic and the `VideoPanel` split are slice 3b. -- **Slice 4b — the rest of the file schemas: `site.json`, channel `config.json`, sidecars.** - `siteSchema.ts` and `channelConfigSchema.ts` on 4a's `settingsField(coerce)` pattern (the - channel coercions stay zod-free in `channelConfig.ts`, which client code imports); one - config reader, a strict writer and one locked read-modify-write patcher - (`patchChannelConfig`, `updateConfigField` deleted); eight sidecar pairs each one - `sidecar(name, schema)` with the `transcript.<x>.<y>` guard thrown at declaration; one JSON - reader and one per-path-chained atomic writer (`jsonFile-server.ts`, state on - `globalThis`); `SITE.md` + `CHANNEL.md` generated. No number moved - (`phase3-files-numbers.ts`, frozen inputs, diff empty over 3,832 lines); `build:index` - +0.5 %. First form save or toggle per channel may show a one-time key-reorder diff in - `transcripts/` — expected. - -**Gates on `ef88ac4d`:** tsc clean; common 1723; editor unit 67; scripts 156 + 1 skip; mcp -219; editor and export builds green (on `7dfd7508`, code-identical). Full e2e: -`editor **621 passed, 0 failed, 36.3 min**; export **188 passed, 0 failed, 5.9 min** (run from a worktree detached at `ef88ac4d`, ports 3311/3310, log `final-e2e-r3.log`)` (3a's branch: 280/280 over 41 spec files; 4b's merged tip: the plan's -34 specs, 190/190). - -**Next:** the release-3 rollout to :3001 (editor only — `git diff 1e27f7c3..ef88ac4d -- umtool` -is empty and umtool's one `common` import, `lib/momentUrl`, is unchanged), then slice 3b -(`stages/*` status logic, `VideoPanel.tsx` → per-operation panels). - -**Still owed (operator):** the Anilyzer **production** deploy (preview is up, see below); the -other five sites to corpus spec 4; the LM chat-only tier (shipped, unconfigured). **Owed -(code):** slice 3b (the nine `stages/*` cards' status logic + the `VideoPanel.tsx` split); the -JSON writers still on the per-pid temp name — the record's 14 (16 write sites in 13 files, -recounted) plus the export build's two page writers (`buildIndex.ts:971`, `buildStats.ts:220`), -`maybeMissingStore` and `rosterStore` first (per-channel files written from several lanes); -the two per-module-copy write counters (`metadataScanStore.ts`, `autoQueueState.ts`). +**Live on :3001 (2026-09-24, 18:42):** still `9ab10d77` (release 3), `BUILD_ID` +`XsKaA_drqdAbTguxUGVsn`. **Release 4 is on `main` but NOT rolled out**; the next release's +prelude rolls it out. The one-sync md5 sweep from the release-3 rollout is still owed. +YouTube held a 429 cooldown across three attempts (18:43, 19:13, 19:41). See the +[rollout record](one-core-phase-3.md#rollout-2026-09-24--9ab10d77-live-on-3001). + +**Last updated:** 2026-09-24 (evening). **Release 4 is on `main` @ `e172749b`, and one-core +Phase 3 is COMPLETE. Phase 4 is next.** The prelude was plans only: `4130aca1` (the rollout +record) and `bbad0977` (the owed sync sweep). The three plan files `77f63356`, `42caf3cc` and +`cc89abfa` were filed during the release. Three worktrees were cut off `4130aca1` and merged +in this order: +- `one-core/phase-3-p` → `77a32de2`; +- `one-core/phase-3-w` → `ddad13f4`, which merged P and re-gated as `44dd843f`; +- `one-core/phase-3-s3b` → `e172749b`. + +Release record: +[`one-core-phase-3.md`](one-core-phase-3.md#release-2026-09-24-evening--rollout-prelude-slice-p-slice-w-slice-3b), +with the P, W and 3b "as shipped" sections above it. What is still open is in +[`one-core-phase-3.md`](one-core-phase-3.md#next--phase-4). Anchors are in +[`FACTS.md`](FACTS.md#one-core-phase-3-release-4--slices-p-w-3b-verified-2026-09-24-main--e172749b). +Previous: release 3, slices 3a and 4b, on `main` @ `ef88ac4d` +([record](one-core-phase-3.md#release-2026-09-24--rollout-slice-3a-slice-4b)), live on :3001 +as `9ab10d77`. + +- **Slice P: /channels rack polish.** + - One layer ladder (`channels/components/rackLayout.ts`: popover > thead > group header > + identity, plus the deck). Channel rows no longer paint over a group's station buttons. + - Four audit fixes: pinned group controls, an Advanced panel that scrolls into view, the + section rule as an inset shadow, and the region as `isolate`. + - The Transcribe station counts `downloadedNoTranscript` + `downloadedAutoSubsOnly`, minus + exclusions, for every handling, and queues exactly those ids as two bucket jobs. + - No number moved (`phase3-view-numbers.ts`, diff empty). +- **Slice W: one write idiom.** + - `writeFileAtomic` / `copyFileAtomic` sit under `writeJsonAtomic`, with one per-path chain + and one unique temp name. + - 26 sites were folded (19 JSON, 7 text/binary), and both per-module-copy write counters + are deleted. A failed write no longer leaves a temp file behind. + - No byte moved (`phase3-writers-numbers.ts`: 315 samples equal the old idiom's bytes, diff + empty). +- **Slice 3b: the video page's chore cards, one module each.** + - `VideoPanel.tsx` went from 1,839 to 455 lines, the assembly only. It now draws 15 modules + under `videos/[id]/components/cards/`, pinned by `lib/videoChoreCards.ts` and its test. + - The stage half needed nothing: the flow work had already made `computeStageStatuses` / + `computeChannelFlow` the one fold. + - Labels are identical (126 entries, same md5). + +**Gates.** +- On `e172749b`, re-run for the record: editor unit 72/72. +- On 3b's merged tip `2e322e37`, whose tree equals `e172749b`'s: tsc clean, common 1738, + scripts 156 + 1 skip, both builds green, the 19 video-page specs 108/108. +- On W's merged tip `44dd843f`: mcp 219, the 20 writer specs 140/140. + +Full e2e on `e172749b`: editor **<pending>**; export **<pending>** (worktree +`one-core-phase-3-s3b` detached at `e172749b`, ports 3311/3310, logs +`final-e2e-r4-editor.log` / `final-e2e-r4-export.log`) + +**Next.** +1. **The exports-off release**, [`site-exports-off.md`](site-exports-off.md): a per-site + option, off on the operator's five published sites, which need a rebuild and a deploy. +2. **The Rumble slice**, [`rumble-sweep-pacing.md`](rumble-sweep-pacing.md): step 0 is + `--impersonate chrome` through a per-platform args table (the embed endpoint 403s every + download, upstream #17496); step 2 is pacing the full sweep. The operator applied step 1 + on 2026-09-24. +3. **One-core Phase 4** (CLI, entry points, config, docs). `buildDeployCore.ts` imports + nothing from `editor/**`. `PUBLISH.md` does not exist yet. + +The release-4 rollout and the owed one-sync sweep ride in the prelude of whichever release +comes next. + +**Still owed (operator):** +- the Anilyzer **production** deploy (the preview is up, see below); +- the other five sites to corpus spec 4; +- the LM chat-only tier (shipped, not configured); +- the **188 orphan temp files** in `transcripts/`, listed by `find transcripts -name '*.tmp-*'`. + 175 of them are `.auto-queue/state.json.tmp-*` from the 2026-09-11 full-disk day. They are + safe to delete while nothing runs. + +**Owed (code):** +- `transcribeOne.ts:173` writes the remote transcript with no temp. It is the one real + `writeFileAtomic` candidate left. +- The export page writers `buildIndex.ts:971` / `buildStats.ts:220` need restructuring, + which is out of W's scope. +- The deferred follow-ups in the phase-3 "Next" section. **Previously:** 2026-09-22 — **the release *curated-tags follow-ups + debts sweep* is on `main` @ `766e0873`.** Base `4ac8ceda`; three branches merged in the order @@ -1446,8 +1477,9 @@ of THAT plan, not of PLAN.md's phases above): | 0 · Guardrails | **shipped** 2026-09-07 | | | 1 · One scheduler | **shipped**; gate A passed 2026-09-13, **gate B done 2026-09-23** | Records in `one-core-phase-1.md`. | | 2 · One `ArchiveReader` | **shipped** (on `main` since 2026-09-13) | | -| 3 · Views in the core | **slices 1, 2, 4a shipped** (2026-09-15, 2026-09-23, 2026-09-23) | Slice 3 and slice 4b are the next release. `one-core-phase-3.md`. | -| 4–6 | not started | Phase 4's `settings example` already exists (`common/bin/settings-example.ts`). | +| 3 · Views in the core | **COMPLETE 2026-09-24** (`main` @ `e172749b`) | Slices 1, 2, 4a, 3a, 4b, P, W, 3b. `one-core-phase-3.md`. Release 4 is not yet live. | +| 4 · CLI, entry points, config, docs | **next** (after exports-off and Rumble) | `buildDeployCore.ts` imports nothing from `editor/**`; `settings-example.ts` and `file-schemas-docs.ts` carry `--check`; no `PUBLISH.md` yet. | +| 5–6 | not started | | **Three corrections to this document's own claims, verified against the code 2026-07-29.** Exploration for Phase 2 found the planning docs describing more unbuilt work than exists: @@ -1523,6 +1555,60 @@ parallel task), Phase 0 transcription benchmark (a separate bottleneck). ## Decisions log +**One-core Phase 3 is closed (2026-09-24, evening, `main` @ `e172749b`).** Release 4 (P, W, +3b) ends it. Every item in `one-core.md` §Phase 3 is shipped or recorded as not needed. The +stage half of item 3 was already done, and item 4's mtime freshness has no reader. Phase 4 is +next, after the exports-off and Rumble releases. Release 4 was merged but not rolled out: the +next release's prelude does that, as release 3's did. + +**The rack's rows-over-controls bug was a z-index tie, and two commits made it (2026-09-24, +slice P).** `278d4463` shipped the rack plan's order as 30/20/10 (thead / group header / +identity). `20ee34db` (2026-09-13) then lifted the pinned Slug cell to z-20 to close a +sub-pixel seam against the checkbox cell. That put it on the group header's level, so every +later row's slug band painted over the header, in DOM order. The bump was never needed. The +Slug cell pins 4 px inside the checkbox cell (`left-8` < `w-9`), and at one z-index it already +covers the overlap in DOM order. The fix is one named ladder in `rackLayout.ts`, a unit test of +the width invariant and the order, and no `z-<n>` literal anywhere else under +`channels/components/`. A second, unstated tie (popover and thead both at z-30) went with it. + +**The Transcribe station counts both buckets and queues by id (2026-09-24, slice P, review +F1).** Buckets are decided by files, never by handling. `downloadedNoTranscript` is whisper +work for every channel, so the old `handling !== "transcribe"` refusal was wrong. The figure +is `downloadedNoTranscript` + `downloadedAutoSubsOnly`, minus the download exclusions. The +button queues exactly those ids as two bucket jobs. It does not run the `whisper-all` scan, +because `downloadedNoTranscript` does not filter excluded ids, and a scan would transcribe a +video downloaded before it went private: one more than the label said. The scan runs only for +a channel with no report. The dedupe spans all three kinds. The rack's band still counts +`downloadedNoTranscript` alone. That divergence is on purpose: the station counts what its +button queues. + +**Slice 3b was reduced to the VideoPanel split (2026-09-24).** The inventory's premise was +stale. The nine `stages/*` files hold no duplicated status logic, because the flow work had +already made `computeStageStatuses` / `computeChannelFlow` the one fold. The video page's +registry panels already existed (`videoOperationPanels.ts`). What remained was the chore +cards, which no registry entry owns. They were moved verbatim, one module per card, and +nothing on the page changed. The assembly is 455 lines, not ~300. The literal JSX was kept +over a list-driven render because every gate differs, and `videoChoreCards.ts` + its test pin +the two together. + +**Slice W left five temp names by name (2026-09-24).** The two export page writers +(`buildIndex.ts:971` streams, `buildStats.ts:220` joins by hand) need restructuring rather +than a fold. `transcode.ts:31` and `transcribeOne.ts:142` name an external process's output +file, so there is nothing to fold. `transcribeOne.ts:173` writes the remote transcript with no +temp at all, and it is the real candidate for later. The 188 orphan temps in `transcripts/` +are corpus files. They were left for the operator. + +**Exports-off and Rumble are the next two items (2026-09-24).** Both were filed while release +4 ran: [`site-exports-off.md`](site-exports-off.md) and +[`rumble-sweep-pacing.md`](rumble-sweep-pacing.md). Neither was folded into release 4. Its +slices were already cut, exports-off needs a five-site rebuild and deploy, and the Rumble fix +edits `runYtdlp.ts`, which W was touching. + +**The one-sync md5 sweep stays owed (2026-09-24).** It is the last step of the release-3 +rollout: one sync, then a sweep showing that only the synced channel's `config.json` moved. +All three attempts were refused, because auto-download kept re-arming a YouTube 429 cooldown. +No non-YouTube channel was substituted. It rides in the next rollout prelude. + **The rollout prelude went before the worktrees (2026-09-24).** Release 2 was put live on :3001 — and the two commits it needed (`c0a90a37`, `1e27f7c3`, both the backfill lane form's stale re-acquire sentence) landed on `main` — *before* `one-core/phase-3-s3a` and `-s4b` were diff --git a/plans/one-core-phase-3.md b/plans/one-core-phase-3.md @@ -1053,57 +1053,213 @@ detached from the worktree root: **108 passed, 0 failed, exit 0, 6.5 min**, firs Labels, `4130aca1`'s `VideoPanel.tsx` against the merged tree: 126 entries each, identical, md5 `3f89e5c1c9f79ad1a04ceea4568f0500` both. -## Next release — slice 3b and Phase 4 (inventory kept from 2026-09-23) - -Slices 3a and 4b shipped in the release above (2026-09-24); the slice 3b bullets and the -deferred follow-ups below are what remains of this inventory — except the first follow-up -(the lane form's "836" hint), which the prelude closed (`c0a90a37`, `1e27f7c3`). The facts -below were checked against `ae2fa5a9` on 2026-09-23. - -**Slice 3 — one drawing per noun.** - -- *Channel row.* Two `ChannelsTable`s — `editor/app/channels/components/ChannelsTable.tsx` - (966 lines, the rack) and `editor/app/components/dashboard/ChannelsTable.tsx` (192) — plus - `editor/app/components/channelWork/ChannelWorkTable.tsx` (178). One table with a column set. -- *Job in flight.* `jobs/components/RunningJobsList.tsx` (158), `jobs/components/LaneStrip.tsx` - (88), `operations/components/InFlightList.tsx` (85) and the widget's own - `ActiveJobsStrip`/`JobRow` (`widget/components/MonitorWidget.tsx:989,1033`) onto the - `JobRowView` renderer with a `variant`. **`InFlightList` does not draw `JobRowView` today**: - it draws `AutoQueueKindStatus.runner.inFlight` (`InFlightList.tsx:16,20`), so it needs a - mapping to the row, not just a re-skin. -- *Channel pipeline.* The status logic in the nine files under - `channels/[slug]/components/stages/` (eight `*Stage.tsx` cards plus `OperationWork.tsx`); - each stage keeps its body. `one-core.md` said eight. -- *Video page.* `channels/[slug]/videos/[id]/components/VideoPanel.tsx` is **1,839 lines** - (`one-core.md` said 1,586), with download, transcode and transcription still inline; - `videoOperationPanels.ts` (`…/videos/[id]/lib/`) is the enumeration to split along. - -**Slice 4b — the rest of the schemas.** `site.json`; channel `config.json`, with its mutable -sync state (`lastSyncedAt`, `lastFullSweepAt`, …) declared as such and the file NOT split; -the sidecar `X.ts` / `X-server.ts` pairs → one `sidecar(name, schema)` each (read, atomic -write, mtime freshness, the `transcript.<x>.<y>` naming guard). Slice 4a's pattern is the -template: `settingsField(coerce)` over the existing sanitizer, no `.default()`, no -`.passthrough()`, zod kept out of `"use client"` import graphs. - -**Deferred follow-ups, each checked still open on `ae2fa5a9`:** - -- The backfill lane form's hint text still says *"836 videos still have media on disk and - ~76,270 would need re-downloading"* (`operations/components/settings/LaneSettingsForm.tsx:101-102`); - the page measured 679 reachable / 74,046 no media at gate B. A stale measured figure in - copy — fix the copy or derive it, in a code slice. +## Release 2026-09-24 (evening) — rollout prelude, slice P, slice W, slice 3b + +`main` `9ab10d77` → **`e172749b`**. **Phase 3 is complete with this release.** A prelude on +`main`, then three branches, all off `4130aca1`, merged in this order (all three merges are +dated 2026-09-24, 19:50–20:14 local): + +0. **Prelude, on `main`, plans only.** `4130aca1` is the rollout record: release 3 + (`9ab10d77`) is live on :3001 (the "Rollout 2026-09-24 — `9ab10d77`" section above). + `bbad0977` records the owed one-sync md5 sweep. Three plan files were filed while the + slices ran: `77f63356` ([`site-exports-off.md`](site-exports-off.md)), and `42caf3cc` + `cc89abfa` + ([`rumble-sweep-pacing.md`](rumble-sweep-pacing.md)). **The one-sync sweep is still owed.** + `pnpm ops sync {"slug":"FearAnd"} --wait` was refused three times (18:43, 19:13, 19:41), + because auto-download kept re-arming a YouTube 429 cooldown. No other channel was + substituted. The sweep would show that a sync stamps `lastSyncedAt` through + `patchChannelConfig` on the synced channel's file and on nothing else. It is the one step + of the release-3 rollout left unmeasured. +1. **`one-core/phase-3-p` → `77a32de2`**: the /channels rack polish. Eleven commits: + - code: `5ac3e8ca`, `7a0b3d75`, `564f767d`, `99b06b40`, `53bbd482`, `476c8470`, `6ff63cbf`; + - the record, `0a717b79`; + - the review fixes, `e0f731fa` (F1/F2) and `4f951ecc` (F3/F5); + - the record update, `ca130aa6` (F4). + + Review verdict: **ship after fixes** (F1 medium, F2/F3 low, F4/F5 nits). All five were + taken. `main` had moved by plans only, so `77a32de2`'s code is `ca130aa6`'s. +2. **`one-core/phase-3-w` → `ddad13f4`**: one write idiom. Eight commits: + - code: `db9e3f44`, `9bfd15cd`, `ae6ed9d2`, `588fd7e9`; + - the record, numbers tool and changelog, `be4769de`; + - the review fixes, `71264112`; + - `main` (`77a32de2`) merged in as `44dd843f`; + - the merged-tip record, `ce8a4764`. + + Review verdict: **ship after fixes** (four nits, none blocking, all taken). + `ddad13f4`'s tree is `ce8a4764`'s. +3. **`one-core/phase-3-s3b` → `e172749b`**: the video page's chore cards, one module each. + Four commits: `315cec1f` (code), the record `9db2f291`, `main` (`ddad13f4`) merged in as + `2e322e37`, and the merged-tip record `aa6a76e7`. Review verdict: **ship**, with three + informational findings and nothing to fix. `e172749b`'s tree is `aa6a76e7`'s. + +**Why that order.** P went first because it is the UI slice with the largest e2e surface. +Its figures (the transcribe station) read through `common/views/channelGroupSections.ts`. +W had to prove its byte identity on a tree that already held P. So W merged `main` +(`77a32de2`) and ran its whole gate set again on `44dd843f`, the numbers tool included, with +inputs re-frozen and `77a32de2` as the before side. 3b came last and was mechanical. It +touches only `videos/[id]/components/**` and `lib/videoChoreCards*`. The one file it shares +with W is `videoActions.ts`, which 3b does not edit (W changed two write sites and one import +line). Every merge conflicted only in `editor/CHANGELOG.md` and this file, and each was +resolved by keeping both sides, in P, W, 3b order. + +**Scope.** Release 4 is P, W and 3b. **3b is only the `VideoPanel` split.** Its stage half +was already done by the flow work (`computeStageStatuses` `stageStatus.ts:192` and +`computeChannelFlow` `channelFlow.ts:220`, each called once, at `channels/[slug]/page.tsx:256` +and `:308`). No stage file changed. **Release 4 is not rolled out.** :3001 still serves +`9ab10d77` (`BUILD_ID` `XsKaA_drqdAbTguxUGVsn`). The next release's prelude does the rollout. + +**Gates, as the three records state them.** +- **P**, at the review-fix tip `ca130aa6`: + - tsc clean after every commit; + - common **1728**, editor unit **69**, `test:scripts` 156 + 1 skip; + - e2e after review (`channel-groups`, `channels-rack-layers`, `channel-priority`): **19/19**, + 1.1 min; + - before review: the 24-file list **146/146** in 11.8 min, and builds green at `6ff63cbf`; + - `phase3-view-numbers.ts`: diff empty, 5,119 bytes each side. +- **W**, on the merged tip `44dd843f` (W + P): + - tsc clean; + - common **1738**, editor unit **69**, `test:scripts` 156 + 1 skip, mcp **219**; + - editor and export `next build` exit 0, `ƒ /api/view/[name]` listed, no `ZodError|_zod` in + either `.next/static`; + - `phase3-writers-numbers.ts`: 323 lines each side, 315 `old-idiom=same`, 0 + DIFF/THREW/LEFT, diff empty; + - e2e, the 20 specs: **140/140**, 8.8 min. +- **3b**, on the merged tip `2e322e37` (all three): + - tsc clean; + - common **1738**, editor unit **72** (re-run on `e172749b` for this record: 72/72), + `test:scripts` 156 + 1 skip; + - both builds exit 0; + - e2e, the 19 specs: **108/108**, 6.5 min; + - labels: 126 entries each side, md5 `3f89e5c1…` both. + +Final full suites on `e172749b`: editor **<pending>**; export **<pending>** (run from the worktree `/home/user/Projects/one-core-phase-3-s3b`, detached at `e172749b`, ports 3311/3310, logs `final-e2e-r4-editor.log` / `final-e2e-r4-export.log`) + +**Record corrections made while checking the three records against the code** (2026-09-24; +where a record and the code disagreed, the code won): +- **Slice P**: + - The record said "not merged". It is merged, as `77a32de2`. + - It said "the ten below and this record, amended after review". The table lists **nine** + code commits. The record is **two** commits, `0a717b79` and `ca130aa6`; it was not + amended. The total of eleven stands. + - The pre-review paragraph says `jobIds` "carries the whisper-all id when both jobs were + queued". At `e0f731fa` it carries the id of the **first ok half**, which is the + no-transcript bucket job (`whisper-bucket-downloaded-no-transcript`) when both halves + queue (`groupActions.ts:116-126`). The whisper-all scan runs only for a channel with no + snapshot (`:97`). The record's "By id, after review" sentence already says this. The + earlier bullets describe the pre-review code. + - The band/station divergence anchor `channelSnapshot.ts:583-585` is a comment. The band's + own fold is `common/views/pipeline/buildBands.ts:168-172`, where `transcription.reachable + += downloadedNoTranscript` and nothing else. That comment (`channelSnapshot.ts:586-587`) + still names the retired path `editor/app/components/pipelines/buildBands.ts`. This + release changes plans only, so it was left as is. + - The audit table cites the mobile-thead trade-off at `ChannelsRack.tsx:152-155`. On + `main` that range is the `heldCount` comment, and the trade-off comment is at + **`:159-162`** (`channels/page.tsx:332-336` is right). +- **Slice W**: + - The record said "five commits, unmerged", with an unsha'd "(review fixes)" row. The + branch has eight commits: the review fix is `71264112`, then the merge `44dd843f` and + `ce8a4764`. It is merged, as `ddad13f4`. + - In the post-commit-4 grep block, `jsonFile-server.test.ts:65` is **`:68`** on `main`, + because the review fix added imports above it. + - "`git grep 'rename(tmp'` … finds only `buildIndex`, `buildStats`, `transcode`" leaves out + `jsonFile-server.ts:175`, the shared writer's own rename. +- **Slice 3b**: the record said "one code commit and this record". The branch has four commits + (see above), and it is merged, as `e172749b`. +- **This record's date.** It was asked for as "2026-09-25". The merge commits are dated + 2026-09-24 in local time (2026-09-25 in UTC), and the plans date by local time. + +## Next — Phase 4 + +Phase 3 is complete. Slices 1, 2, 3a, 3b, 4a and 4b have all shipped, and so have P and W, +the two slices this release added. Phase 4 (CLI, entry points, config, docs) is next; its +slices are in [`one-core.md`](one-core.md#phase-4--cli-entry-points-config-docs-3-slices), +with the starting points as inventoried on `e172749b`. Two plans filed during release 4 go +first, as their own releases, and each needs a code slice and a rollout: + +- **Visitor exports off on the published sites**, [`site-exports-off.md`](site-exports-off.md). + This is a per-site option, off on the operator's instances and on by default for the OSS + release. It needs a rebuild and a deploy of the five published sites. Its anchors were + taken on `4130aca1`; re-verify them before cutting the slice. +- **Rumble**, [`rumble-sweep-pacing.md`](rumble-sweep-pacing.md). Step 0 is a per-platform + yt-dlp args table carrying `--impersonate chrome`, because Rumble's embed endpoint answers + 403 to every download (upstream #17496). Step 2 paces a full sweep of a large channel. Step 1 + (`fullSweepIntervalMinutes: 0` on `the-quartering-rumble`) was applied by the operator on + 2026-09-24. Step 2 edits `runYtdlp.ts`, which W touched (only the playlist writer), so cut it + from `main`. + +**Rollout owed.** Release 4 is not live. :3001 still serves `9ab10d77`, and the next release's +prelude rolls it out. The one-sync md5 sweep owed since the release-3 rollout rides along with +it: one sync, then a sweep showing only the synced channel's `config.json` moved. + +**Left by slice W, by name** (`git grep 'tmp-${process.pid}' -- common editor` at `e172749b`): +- `common/controller/buildIndex.ts:971`, the streaming `createWriteStream` page writer; +- `common/controller/buildStats.ts:220`, a hand-joined array. These two are restructuring + work, not folds. +- `common/controller/transcode.ts:31` and `common/controller/transcribeOne.ts:142` name the + output of an external process (ffmpeg; the transcription app), which the code then renames. + There is nothing to fold. +- `common/controller/transcribeOne.ts:173`, + `if (bytes) await writeFile(transcriptPath, bytes)`, writes the remote transcript straight + to `transcript.json` with no temp and no rename. A crash mid-write can truncate it. **This + is the one real candidate** for `writeFileAtomic`. +- Also out of scope: `scripts/diarize.mjs` and everything under `umtool/`. + +**For the operator: 188 orphan temp files in `transcripts/`**, none of them touched: +- 175 `.auto-queue/state.json.tmp-2514131-NNNN`, all dated 2026-09-11 (the day `/home` was + full), 173 of them 0 bytes; +- seven `snapshot.json.tmp-<pid>`; +- three sidecar temps; +- three whisper `transcript.tmp-<pid>` output bases. + +`find transcripts -name '*.tmp-*'` lists them all. The new writer cannot leave more of the +first kind, so deleting them is safe once nothing is running. + +**Recorded by slice P, not fixed:** +- Below md the thead does not pin. This is the documented trade-off + (`ChannelsRack.tsx:159-162`, `channels/page.tsx:332-336`). +- An open Advanced panel lengthens the region's scroll extent: a blank band shows under the + last row until the panel closes. The alternative is opening upward near the bottom. +- The region's `-mx-4` edge shows the thead checkbox and the row edge in the 16 px gutter + beside the mobile deck. Cosmetic, and older than P. +- At 390 px, "Derived data off" wraps under the station line. +- `20ee34db`'s `min-w-52` on the tier cell is unchanged. +- `channels-rack-layers.spec.ts` does not exercise the popover-vs-thead order. Only + `rackLayout.test.ts` holds it. + +**Recorded by slice 3b:** +- `VideoNavStrip`'s `umtoolUrl` prop is never read. +- `pnpm --filter export run build` is the wrong gate in a worktree: it runs `build:data` into + a worktree-local `transcripts/` and then needs `SITE_ID`. Use + `pnpm --filter export exec next build`. + +**Deferred follow-ups from the 2026-09-23 inventory, still open on `e172749b`:** - `plans/tools/phase1-numbers.ts` sums `missingInput` straight from the snapshot - (`COUNT_FIELDS`, `:57-65`) and never filters `handling: "youtube"`, so its `missingInput` - is the whole bucket, not what a backfill pass could reach. -- `usePolledPayload`'s hang recovery is not unit-tested: the editor unit suite has no React - harness, so `lib/usePolledPayload.test.ts` pins `pollTimeoutMs` only (one test). -- The nested settings blocks (`DigestSettings`, `DiarizationSettings`, …) are each one - sanitizer-backed `settingsField`, not zod objects; their docs are the `*_FIELD_DOCS` - records. Converting them is optional work, not a bug. -- `editor/scripts/measure-nav.mjs:64-69` still requests the eight OLD API paths. They answer - through the rewrites, so it measures rewrite + view; say so if its numbers are compared - with an older run. + (`COUNT_FIELDS`, `:57`) and never filters `handling: "youtube"`. +- `usePolledPayload`'s hang recovery is not unit-tested (`lib/usePolledPayload.test.ts` has + one test, which pins `pollTimeoutMs`). +- The nested settings blocks are each one sanitizer-backed `settingsField`, not a zod object. + Converting them is optional. +- `editor/scripts/measure-nav.mjs:64-71` still requests the old API paths. These are **six** + paths (`/api/pulse`, `/api/widget/{cleanable,actionable,sync}`, `/api/jobs/active`, + `/api/workers`), not eight at `:64-69` as the inventory said, and they answer through the + rewrites. Its numbers therefore measure rewrite + view. - Accepted, not owed: nothing prunes stale per-app entries from the `transcriptionApps` - shadow any more (slice 4a, behaviour changes). + shadow. +- `common/controller/channelSnapshot.ts:586-587` names a retired path for `buildBands.ts`. It is + now `common/views/pipeline/buildBands.ts`. Comment only. + +**Phase 4 starting points, checked on `e172749b`:** +- `editor/app/sites/lib/buildDeployCore.ts` is 578 lines and imports nothing from `editor/**`: + only `node:*`, `@aws-sdk/*` and `yt-dlp-transcript-common/*`. It has two callers, + `sites/lib/buildAction.ts` and `sites/lib/deployAction.ts`. The move to + `common/publish/build.ts` has no editor import to cut. The two `@aws-sdk/*` dependencies + are declared in `editor/package.json` only, so they move with the file. +- `common/bin/settings-example.ts` and `common/bin/file-schemas-docs.ts` both carry + `--check`, so each becomes a one-line `archilyzer` subcommand. +- `PUBLISH.md` does not exist yet. Phase 4 slice 3 creates it (absorbing `DEPLOY_DOCKER.md` + and `DEPLOY_CLOUDFLARE.md`). + +**Owed outside one-core (operator):** the Anilyzer production deploy (the preview is up); the +other five sites to corpus spec 4; the LM chat-only tier (shipped, not configured). ## Slice 2, as shipped — one polling route (2026-09-23) diff --git a/plans/one-core.md b/plans/one-core.md @@ -311,12 +311,14 @@ https://jeralyzer.pages.dev/corpus.json` before and after, byte-identical output ### Phase 3 — Views in the core, editor as shell (4 slices) -> **Status 2026-09-24.** Slice 1 shipped 2026-09-15 (`838da4a`). Slice 2 shipped 2026-09-23, -> merged as `8d6e84f6`; slice 4a (settings.json only) shipped 2026-09-23, merged as `ae2fa5a9`. -> **Slice 3a (channel row + job in flight) shipped 2026-09-24, merged as `3241fed2`; slice 4b -> (`site.json`, channel `config.json`, sidecars) shipped 2026-09-24, merged as `ef88ac4d`.** -> Slice 3b (`stages/*` status logic, the `VideoPanel` split) is next. -> Records, gates and the next release's inventory: [`one-core-phase-3.md`](one-core-phase-3.md). +> **COMPLETE 2026-09-24, `main` @ `e172749b`.** Slice 1 shipped 2026-09-15 (`838da4a`). +> Slice 2 shipped 2026-09-23 (merged as `8d6e84f6`), and slice 4a (settings.json only) the same +> day (`ae2fa5a9`). Slice 3a (channel row + job in flight) and slice 4b (`site.json`, channel +> `config.json`, sidecars) shipped 2026-09-24 (`3241fed2`, `ef88ac4d`). Release 4 shipped the +> same evening: slice P (/channels rack polish, `77a32de2`), slice W (one write idiom, +> `ddad13f4`) and slice 3b (the `VideoPanel` split, `e172749b`). Release 4 is not yet live +> on :3001. Phase 4 is next. +> Records, gates and what is still open: [`one-core-phase-3.md`](one-core-phase-3.md). 1. **`common/views/`**: move `buildActiveJobs`, `buildWorkers`, `lanes.ts`, `channelFlow.ts` + `stageStatus.ts`, the widget sync builder and `pulse` out of @@ -358,6 +360,28 @@ https://jeralyzer.pages.dev/corpus.json` before and after, byte-identical output job, so only its runner job's buttons became `JobRowActions` — the "replaces … `LaneStrip` …" above was wrong. The `stages/*` status logic (nine files, not eight) and the `VideoPanel.tsx` split (1,839 lines, not 1,586) are slice 3b, not shipped.* + *As shipped for the channel pipeline and the video page (slice 3b, merged `e172749b`, + 2026-09-24):* + - ***The stage half was already done by the flow work, and nothing was changed.*** *The + nine `stages/*` files hold no duplicated status logic. `computeStageStatuses` + (`common/views/pipeline/stageStatus.ts:192`) and `computeChannelFlow` + (`channelFlow.ts:220`) are the one fold. Each is called once, in + `channels/[slug]/page.tsx:256-264` and `:308-315`. Every stage card takes derived id + lists and sums as props and keeps only its form state.* + - ***The video page's registry split already existed.*** *`videoOperationPanels.ts` and + `OperationPanel.tsx` render digest, diarization and attribution. What was still inline + in `VideoPanel.tsx` was the video CHORES, which no registry entry owns. They were split + verbatim, one module per card: 15 modules under `videos/[id]/components/cards/`, plus + `Heading.tsx` and `videoFiles.ts`, with `VideoNavStrip` and `PipelineStatusStrip` + placed beside the panel.* + - ***`VideoPanel.tsx` is the assembly, 1,839 → 455 lines, not ~300.*** *The `VideoPanel()` + function alone is 360 lines of gates, summaries and the fifteen `PipelineStageCard` + wrappers. The rule was to keep order and gating exactly. Every card's gate and props + differ, so the literal JSX was kept rather than driving it from a list. + `lib/videoChoreCards.ts` is that list: the fifteen chores in render order, each with + why it is a chore and not an operation. Its test reads `VideoPanel.tsx` as text and + pins the rendered order and the `./cards/` imports to the list. Labels are unchanged: 126 + entries, identical md5.* 4. **One settings writer and one schema.** Adopt one schema library (recommend `zod`; veto if unwanted) for `settings.json`, `site.json`, channel `config.json` and every sidecar. `lib/settings.ts`'s ten `sanitizeX` + eight `defaultX` + ~40 clamp constants @@ -405,15 +429,48 @@ https://jeralyzer.pages.dev/corpus.json` before and after, byte-identical output listed in the slice record. The chain is per process, so a CLI running beside the editor is not covered. `savedVideo`, `clipWindow`, `posts`, `transcripts` and `digestContext` stay outside `sidecar()` by name; only their JSON writes moved to the shared writer.* + *As shipped for the remaining writers (slice W, merged `ddad13f4`, 2026-09-24): + `common/lib/jsonFile-server.ts` gained `writeFileAtomic(file, string | Buffer, {mkdir, + mode})` and `copyFileAtomic(src, dest, {mkdir})`, and `writeJsonAtomic` is now `jsonText` + → `writeFileAtomic`. All three use the one per-path chain on `globalThis` and the one + unique temp name. **26 sites were folded**, byte for byte: 19 JSON write sites (14 of the + 16 above, including the roster and maybe-missing; the two failed-transcriptions writes are + text. Plus metadata-scan, `autoQueueState`, the two cue normalizers and the remark + literal) and 7 text and binary ones (failed + transcriptions ×2, the playlist, the 0o600 cookie jar, the saved-video cross-device + move, the release cut, the VTT promote). **Both per-module-copy write counters are + deleted** (`metadataScanStore`, `autoQueueState`). A failed write now removes its temp + file. Left by name: `buildIndex.ts:971` (the streaming page writer) and + `buildStats.ts:220`, which need restructuring rather than a fold; `transcode.ts:31` and + `transcribeOne.ts:142`, which name an external process's output file; and + `transcribeOne.ts:173`, which writes the remote transcript directly with no temp. + `phase3-writers-numbers.ts`: 315 samples, all equal to the old idiom's bytes, diff + empty.* Deleted: six payload builders from the app, eight API routes, two channel tables (the dashboard's, and `ChannelWorkTable`'s own `Row` — slice 3a), three job renderers (the widget's private `JobRow` and its bars, `RunningJobsList`'s card and `InFlightList`'s line now draw `JobRow`; `LaneStrip` keeps its lane line — slice 3a), nine stage status derivations (slice 3b), ~1,400 lines of hand sanitizers. +*As shipped: the nine stage derivations were already gone, because the flow work had moved +them into `computeStageStatuses`. 3b deleted nothing of that kind. What it removed from +`VideoPanel.tsx` was 1,424 lines, moved into card modules. Slice W deleted the two write +counters and the private tmp + rename code at 26 write sites.* ### Phase 4 — CLI, entry points, config, docs (3 slices) +> **Next** (after the exports-off and Rumble releases, see +> [`one-core-phase-3.md`](one-core-phase-3.md#next--phase-4)). Starting points, checked on +> `e172749b`: +> - `editor/app/sites/lib/buildDeployCore.ts` is 578 lines and imports nothing from +> `editor/**` (only `node:*`, `@aws-sdk/*`, `yt-dlp-transcript-common/*`). Its two callers +> are `sites/lib/buildAction.ts` and `deployAction.ts`. Item 1 has no editor import to cut. +> The `@aws-sdk/client-s3` and `@aws-sdk/lib-storage` dependencies are in +> `editor/package.json` only, so they move to `common/` with it. +> - `common/bin/settings-example.ts` and `common/bin/file-schemas-docs.ts` both already carry +> `--check`. +> - `PUBLISH.md` does not exist yet. + 1. **`buildSite` moves to the core.** `editor/app/sites/lib/buildDeployCore.ts` → `common/publish/build.ts` (`buildSite(id, opts)`, `deploySite`, `buildAll(mode)`, `composeHub`, `composeHomepage`). The editor actions call it as jobs; `docker/build-site.sh`