Archilyzer · Source

archilyzer

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

commit 7c2f2b705aceeeedbedc598d680b036a60b474ea
parent 697378098fbc49c58d00b98952baa263d5aa72f5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue,  8 Sep 2026 16:53:06 -0400

plans: a keyed move keeps its state, so name what actually unmounted the panel

Five corrections from the review of `a6e6d46`. No code changes.

- The React rule was stated wrong in all three files. "A moved child is
  unmounted and mounted again" is FALSE — React preserves state across a KEYED
  move. Static JSX fragment children compile to one array, so
  reconcileChildrenArray runs, its index fast-loop breaks at 0 (p vs dl), and
  the map pass finds ul/p at index 1 where the UNKEYED RunOne was: a positional
  TYPE MISMATCH is what destroyed it. A key alone would have matched the old
  fiber at its new index; a fixed position alone would never have reached the
  map pass. Each was sufficient — the commit ships both, belt and brace.
- deflake-e2e.md quoted one shape for both bodies. DiarizationBody was
  [p, RunOne] vs [dl, p, RunOne] (index 1 → 2); AttributionBody was [p, RunOne]
  vs [dl, ul, p, RunOne] (1 → 3). Both are stated now, as a table.
- "H1 held" was too generous. H1 as written — an early SSE teardown, fixed by
  polling the log file until terminal — was killed by the trace too: zero
  stream.cancel in 240 instrumented tests. Only its DISPOSITION held (server
  innocent, failure client-side); the cause is a sixth mechanism outside the
  table, and the fix H1 prescribed was not shipped and would not have worked.
- attribution-batch-hang.md had no outcome. It has an "## Outcome" section now.
- The durable rule in FACTS.md was too narrow. "Must not change position" misses
  the same effect by a different mechanism: a parent that stops rendering the
  panel at all. Three pre-existing instances, none under a spec, so none flakes,
  all three known and unfixed and out of scope here — VideoPanel.tsx:1060
  (SourceVideoSection early-returns the persisted view once savedVideo is
  truthy, destroying the "Persist source video" log at :1133) and the two
  banners at :1494 and :1557, dropped by their parent at :237-255 once the run
  clears the condition that drew them. The rule is now: a component carrying a
  StreamActionLog must not be UNMOUNTED when the record its run produced lands.

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

Diffstat:
Mplans/FACTS.md | 51++++++++++++++++++++++++++++++++++-----------------
Mplans/STATE.md | 30++++++++++++++++++++----------
Mplans/attribution-batch-hang.md | 15+++++++++++++++
Mplans/deflake-e2e.md | 52++++++++++++++++++++++++++++++++++------------------
4 files changed, 103 insertions(+), 45 deletions(-)

diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -3526,23 +3526,40 @@ entry calls `missingInput`; its `eligible`/`present` are the playlist and ### A run log lives in the panel's React state, so the panel must not move (`fae99f1`) -`StreamActionLog` keeps the streamed log in `useState` and calls `router.refresh()` itself the -moment a run ends (`common/components/StreamActionLog.tsx:156-163`). That refresh re-renders -the SERVER component that draws the panel — with the sidecar the run just wrote now on disk. -So any panel that changes the SHAPE of its child list when its record appears destroys the log -it was showing: React reconciles a fragment's children by POSITION, a moved child is unmounted -and mounted again, and with `log` empty and `running` false `StreamActionLog` renders no -`<pre role="log">` at all — the element the operator (and the spec) is reading is not stale, -it is gone. `SpeakerBodies.tsx` had exactly that: `[empty state, RunOne]` before a record, -`[dl, ul, p, RunOne]` after. Both bodies are one two-child list now, the record body a sibling -of the button, with `<RunOne key="run-one" …>` last in both branches -(`editor/app/channels/[slug]/videos/[id]/components/SpeakerBodies.tsx:86-99,166-179`) — the -position is the fix, the key is the brace behind it. **This is the shape to copy for any new -panel that carries a `StreamActionLog`** — `DigestBody.tsx:71-131` was already written that -way (one return, one ternary, the log at a fixed index), which is why only the speaker panels -flaked — and it is why `attribution.spec.ts:346` flaked 1-2 runs in 10 for a batch that had -finished in 65 ms with every line written. The full trace and the four hypotheses it killed -are in `plans/deflake-e2e.md`'s "4, as shipped". +**THE RULE: a component carrying a `StreamActionLog` must not be UNMOUNTED when the record its +own run produced lands.** `StreamActionLog` keeps the streamed log in `useState` and calls +`router.refresh()` itself the moment a run ends (`common/components/StreamActionLog.tsx:156-163`). +That refresh re-renders the SERVER component that draws the panel — with the sidecar the run +just wrote now on disk. Unmount it in that render and the log is not stale, it is GONE: with +`log` empty and `running` false the component renders no `<pre role="log">` at all. Two ways +that happens, and the rule has to cover both. + +**Moved unkeyed across a differing sibling shape.** `SpeakerBodies.tsx` returned +`[empty state, RunOne]` before a record and `[dl, (ul,) p, RunOne]` after. Static JSX fragment +children compile to ONE array, so `reconcileChildrenArray` runs: the index fast-loop breaks at +0 (`p` vs `dl`), and the map pass then looks up index 1 and finds `ul`/`p` where `RunOne` used +to be — **a positional TYPE MISMATCH is what destroyed it, not the move itself**. React +preserves state across a keyed move: a `key` alone would have matched the old `RunOne` fiber at +its new index with state intact, and a fixed position alone would never have reached the map +pass. Either was sufficient; the fix ships both — the record body is a sibling of the button +and `<RunOne key="run-one" …>` is last in both branches +(`editor/app/channels/[slug]/videos/[id]/components/SpeakerBodies.tsx:86-99,166-179`). +`DigestBody.tsx:71-131` was already written that way (one return, one ternary, the log at a +fixed index); only the speaker panels were under a spec that could see it, which is why only +they flaked — `attribution.spec.ts:346`, 1-2 runs in 10, for a batch that had finished in 65 ms +with every line written. + +**Dropped by a parent branch swap** — same effect, different mechanism, and `VideoPanel.tsx` +has three instances of it, all pre-existing, none under a spec, so none of them flakes and all +three are KNOWN AND UNFIXED (out of scope for `fae99f1`; the operator decides whether they are +worth a commit). `SourceVideoSection` early-returns the persisted view once `savedVideo` is +truthy (`:1060`), so the *Persist source video* log (`:1133`) is destroyed by the +`router.refresh()` its own run fires. `IncompleteTranscriptBanner` (`:1494`) and +`ShortAudioBanner` (`:1557`) are the same shape from the other side: the parent stops rendering +them (`:237-255`) once the run clears the condition that drew them. + +The full trace, and why five of the plan's hypotheses — H1's mechanism included — were all +killed by it, is in `plans/deflake-e2e.md`'s "4, as shipped". ### The `editor/content` symlink still blocks e2e in the primary checkout diff --git a/plans/STATE.md b/plans/STATE.md @@ -106,21 +106,31 @@ Error: locator.getAttribute: Test timeout of 120000ms exceeded. ``` The job finished in **65 ms**, wrote all five lines including "1 done", enqueued every one of -them with `closed=false`, and finalized `done`. The log panel was **gone** — not stale, not -short, absent: the failure snapshot shows the record body and the Run button and no -`<pre role="log">` at all. `AttributionBody` returned two fragments of different SHAPES -(`[empty, RunOne]` with nothing on disk, `[dl, ul, p, RunOne]` with a record), React reconciles -a fragment's children by POSITION, so the record landing moved `<RunOne>` down the list — and a -moved child is unmounted and re-mounted, taking `StreamActionLog`'s `log` state with it. The +them with `closed=false`, and finalized `done`. **All five hypotheses the plan ranked are dead, +H1's mechanism included** — no `stream.cancel` fired in 240 instrumented tests, so the early +SSE teardown H1 described (and the log-file polling it prescribed) was never happening; all +that held of H1 was its disposition, server innocent and failure client-side. The real cause is +a sixth mechanism outside the table: the log panel was **gone** — not stale, not short, absent. +The failure snapshot shows the record body and the Run button and no `<pre role="log">` at all. +`AttributionBody` returned two fragments of different SHAPES (`[p, RunOne]` with nothing on +disk, `[dl, ul, p, RunOne]` with a record; `DiarizationBody` the same at `[dl, p, RunOne]`), so +`reconcileChildrenArray`'s index pass broke at 0 and found `ul`/`p` where the UNKEYED `RunOne` +had been — a positional type mismatch, which is what unmounts a child and takes +`StreamActionLog`'s `log` state with it. React preserves state across a KEYED move, so a key +alone or a fixed position alone would each have sufficed; the fix ships both. The refresh that lands the record is the one `StreamActionLog` fires itself when the run ends, so the panel is wiped at the instant the summary line reaches it, and the spec could only pass in the ~200 ms window before that. **The earlier note here — "the job never reaches a terminal state", "the hang is in the BATCH" — was the artifact, and so was "the log FILE has no more than the panel": the file had everything, and no reconcile could have helped a component whose -state had been destroyed.** Do not trust either claim again. Both bodies are one two-child list -now, RunOne last and keyed; the spec asserts the log still says "1 done" AFTER the pill reads -"current", which is red on every run at `fea2996` (5/5) and green at the fix. -`attribution.spec.ts --repeat-each 20`: **20/20**. +state had been destroyed** — nor would the plan's own H1 fix have helped, for the same reason. +Do not trust either claim again. Both bodies are one two-child list now, RunOne last and keyed; +the spec asserts the log still says "1 done" AFTER the pill reads "current", which is red on +every run at `fea2996` (5/5) and green at the fix. `attribution.spec.ts --repeat-each 20`: +**20/20**. The durable rule — **a component carrying a `StreamActionLog` must not be UNMOUNTED +when the record its run produced lands**, whether by an unkeyed move or by a parent branch +swap — is in `FACTS.md`, with three pre-existing unfixed instances in `VideoPanel.tsx` that no +spec covers. **The last full suite: 514 passed, 1 failed of 515, 25.1 min** from a worktree of `2133d94`. The red was slice 1.5's own new band assertion and it was right — writing the two new diff --git a/plans/attribution-batch-hang.md b/plans/attribution-batch-hang.md @@ -166,3 +166,18 @@ no-boot-against-`transcripts/`, detached-e2e and DO-NOT-POLL (Monitor) instructi report contract = commit shas, exact gate outputs, the decisive trace lines, which hypothesis held and which were killed, every divergence. A second Opus agent reviews the diff before the final full e2e. + +## Outcome (2026-09-08, `fae99f1`) + +The cause was none of H1–H5 but a sixth mechanism outside the table: the job finished normally +in 65 ms with every line written and `done` recorded, and React then UNMOUNTED the log panel — +`SpeakerBodies.tsx`'s two branches put the unkeyed `<RunOne>` at different child indices, so +the record landing (on the `router.refresh()` `StreamActionLog` fires itself) hit a positional +type mismatch and destroyed `StreamActionLog`'s state, `<pre role="log">` and all. H1's +disposition held (server innocent, failure client-side) but its mechanism did not, and the +log-file-polling fix prescribed for it was NOT shipped and would not have worked. + +Fixed by giving both bodies one two-child list with `RunOne` last and keyed; the spec now +asserts the log still reads "1 done" after the freshness pill reads "current" (red 5/5 at +`fea2996`, `--repeat-each 20` green at the fix). Full record, trace and the durable rule: +[`deflake-e2e.md`](deflake-e2e.md)'s "### 4, as shipped" and `FACTS.md`. diff --git a/plans/deflake-e2e.md b/plans/deflake-e2e.md @@ -135,32 +135,48 @@ Error: locator.getAttribute: Test timeout of 120000ms exceeded. ``` The whole job took **65 ms**. Every line reached `onLog` with `closed=false`, the summary -included, and the record finalized `done`. So of the five hypotheses the plan ranked, **H1 -held and H2-H5 are dead**: no `stream.cancel` fired in 240 instrumented tests (40 of this -spec), `limit()` never read 0 -(`pool.wait-capacity target=1` throughout), `runOne`'s `finally` timestamps are all present, -finalize ran, and no signal aborted. - -What the panel was doing is the answer, and it is not a stale log — the log ELEMENT was gone. -`AttributionBody` (and `DiarizationBody`) returned two fragments of different SHAPES: -`[empty state, RunOne]` with no record on disk, `[dl, ul, p, RunOne]` with one. React -reconciles a fragment's children by POSITION, so the record appearing moved `<RunOne>` from -index 1 to index 3 — and a moved child is unmounted and mounted again, taking -`StreamActionLog`'s `log` state with it. With `log` empty and `running` false the component -renders no `<pre role="log">` at all, which is exactly what the failure snapshot shows: the -record body, the Run button, and no log. The refresh that lands the record is the one -`StreamActionLog` fires itself at the end of a run (`:163`), so the panel is wiped at the +included, and the record finalized `done`. **The trace killed all five hypotheses the plan +ranked, H1's mechanism included.** H2: `limit()` never read 0 (`pool.wait-capacity target=1` +throughout). H3: `runOne`'s `finally` timestamps are all present. H4: finalize ran. H5: no +signal aborted. And H1 as WRITTEN — an early SSE teardown, to be fixed by polling the job's +log file until terminal — is dead too: `stream.cancel` fired **zero** times in 240 +instrumented tests (40 of this spec) and every line enqueued `closed=false`. All that held of +H1 was its DISPOSITION: the server is innocent and the failure is client-side. The real cause +is a sixth mechanism, outside the plan's table, and **the fix the plan prescribed for H1 was +not shipped and would not have worked** — see below. + +**The sixth mechanism: React unmounted the panel.** The log was not stale — the log ELEMENT +was gone. Both bodies returned two fragments of different SHAPES, each with `RunOne` at a +different index in the two branches: + +| body | no record on disk | with a record | RunOne moves | +|---|---|---|---| +| `DiarizationBody` | `[p, RunOne]` | `[dl, p, RunOne]` | index 1 → 2 | +| `AttributionBody` | `[p, RunOne]` | `[dl, ul, p, RunOne]` | index 1 → 3 | + +Static JSX fragment children compile to one array, so `reconcileChildrenArray` runs: the index +fast-loop breaks at 0 (`p` vs `dl`) and the map pass then finds `p`/`ul` at index 1 where +`RunOne` used to be. **A positional TYPE MISMATCH on an UNKEYED child is what destroyed it** — +not the move as such: React preserves state across a KEYED move, so a `key` alone would have +matched the old fiber at its new index, and a fixed position alone would never have reached +the map pass. Either was sufficient. With `log` empty and `running` false the remounted +component renders no `<pre role="log">` at all, which is exactly what the failure snapshot +shows: the record body, the Run button, and no log. The refresh that lands the record is the +one `StreamActionLog` fires itself at the end of a run (`:163`), so the panel is wiped at the instant the summary line reaches it, and the spec could only pass in the ~200 ms window before the refresh landed. **This retires two claims made here on 2026-09-08 and both were measurement artifacts**: "the job never reaches a terminal state" (it reached it in 65 ms) and "the log FILE has no more than the panel does" (the file had everything). It also explains why the two panel-side -reconciles measured no change — no amount of reconciling helps a component React has just -destroyed and rebuilt with empty state. +reconciles measured no change, and why the plan's prescribed H1 fix would have measured no +change either — no amount of reconciling helps a component React has just destroyed and +rebuilt with empty state. Fix: both bodies are ONE two-child list, the record body a sibling of the button, `RunOne` -last and keyed in both branches. No DOM change, no server change, `StreamActionLog` untouched. +last AND keyed in both branches — belt and brace, since each alone would have done. No DOM +change, no server change, `StreamActionLog` untouched. The durable rule this generalises to +(and three pre-existing, unfixed instances of it in `VideoPanel.tsx`) is in `plans/FACTS.md`. Test: the spec asserts the panel STILL contains "1 done" AFTER the freshness pill reads "current" — the pill is the proof the refresh landed, which makes it deterministic rather than a second race. At `fea2996` that assertion is red **5/5** (`element(s) not found`); at