Refresh what a switch changes, not what a turn changes

A branch switch does not change the length of the story. It changes which
story it is. Four panels keyed on actions.length and so could not tell the
difference: the Branches panel drew one branch while the reader was already
on a second, Insights showed the prompt built for the path just left, the
script-state drawer kept the other line's numbers, and the Memory Bank did
not notice a deleted branch taking its memories with it.

Only the world-state drawer was right, and only because it happened to
carry stateKey already. They all key on the pair now, and deleting a branch
bumps it too — that is the one operation that changes what is stored
without a turn being played and without the story on the current path
moving by a single action.

tools/branch_fixture.py is the thing that could show it. The stress
fixture's world state is empty, so it cannot answer whether a switch puts
the scoreboard back, and its story is one branch. This builds a small
bootable adventure with a stat schema, a gold script, two takes on one turn
that differ by 35 hit points, a fork, and a memory on each side — with both
branches the same length on purpose, because equal length is precisely the
case a length-based key cannot see.

Verified in a browser with the drawer open: hp 60 to 95 and back, the bar
redrawn, the story swapped to the other take, Insights carrying the scratch
and not the beating. The Memory Bank deliberately does not change on a
switch: the drawer is adventure-wide so a memory is always findable to
delete, and retrieval is the path-scoped half.

396 tests, build clean, no new lint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015H5qiyiR7gtFQaoDphHZ3g
This commit is contained in:
parththakkar106
2026-08-18 19:14:07 +05:30
committed by Parth
co-authored by Claude Opus 5
parent 84827f0f37
commit 811368d048
4 changed files with 232 additions and 17 deletions
+26 -6
View File
@@ -745,12 +745,32 @@ Five things worth not rediscovering:
group renumbers whenever an attempt is added, so an ordinal held across that points at
a different take — the same reason SP4's note called the pager's index match "one line,
and SP7 removes the pager anyway".
- **The panel reloaded on the wrong thing, and only a browser could say so.** Its refresh
key was `actions.length`. A fork taken from the story column swaps one 60-action window
for another 60-action window, so the length never changes, and the panel went on
drawing a one-branch tree while the story was already being read on a second. The
server was correct throughout; nothing in 396 tests could see it. It keys off the
counter `adoptWindow` bumps now.
- **Four panels reloaded on the wrong thing, and only a browser could say so.**
`actions.length` was the refresh key for the Branches panel, the Status drawer
(script state), Insights and the Memory Bank. **A branch switch does not change the
length of the story** — it changes which story it is. So the Branches panel drew a
one-branch tree while the reader was already on a second, Insights showed the prompt
for the path just left, and the scoreboard kept the other line's numbers. The server
was correct throughout, and nothing in 396 tests could see any of it. All four now key
on `${actions.length}:${stateKey}`, and `stateKey` is bumped by `adoptWindow` — the two
operations that move the head — plus branch deletion, which removes that branch's
memories without a turn being played.
`tools/branch_fixture.py` exists because of this: it builds two branches of **equal
path length**, which is the case `actions.length` cannot distinguish at all. The
`--keep` fixture could not have found it, and neither could a fixture whose branches
happened to differ in length.
**What a switch puts back was checked end to end, not just server-side.** SP5 already
proved `restore_state` in `test_switching_restores_the_script_and_world_state`; what had
never been looked at is whether the *screen* re-reads it. On `tools/branch_fixture.py`,
switching between the two branches moves the World State drawer from **hp 60 to hp 95**
live, redraws the bar, swaps the story to the other take (`hp -5`, not `hp -40`) and
repoints Insights at the other path — "History: 5 of 5 actions", carrying the scratch and
not the beating. The Memory Bank deliberately does *not* change: the drawer is
adventure-wide so a memory can always be found and deleted, and it is retrieval that is
path-scoped (`test_memory_nodes.py`). That split is worth stating out loud, because
"memories did not change when I switched" reads as a bug and is the design.
**The scroll path was driven, and it holds.** Three prepends on the 602-action fixture,
60 actions and ~16,200 px each. The same DOM node stayed at viewport top 792 → 787 — a
+16 -7
View File
@@ -149,13 +149,22 @@ read the same `GET /branches`.
`rename` had no column and no route; `delete` had no route. Migration 61 adds
`branches.name`, and the two endpoints came with it.
**One bug, and only a browser could have found it.** The panel refreshed on
`actions.length`. Forking from the story column swaps a 60-action window for another
60-action window, so the length never changes — the panel kept drawing a one-branch tree
while the story was already being read on the second branch. The server was right the
whole time and no test could see it. That is now three bugs on this frontend found by
exercising it rather than by testing it, and the second found in a path that had just
shipped.
**One bug class, in four places, and only a browser could have found it.** The Branches
panel, the Status drawer, Insights and the Memory Bank all refreshed on `actions.length`
— and **a branch switch does not change the length of the story, it changes which story
it is.** So the tree showed one branch while the reader was on a second, Insights showed
the prompt for the path just left, and the scoreboard kept the other line's numbers. The
server was right the whole time and no test could see any of it. All four key on
`${actions.length}:${stateKey}` now.
`backend/tools/branch_fixture.py` was written to catch exactly this and is worth keeping:
a small bootable adventure with real stats whose **two branches have equal path length**,
which is the case a length-based key cannot distinguish. `--keep` could not have found
it. Run it, switch branches, and watch hp go 60 ↔ 95 with the drawer open.
That is now four bugs on this frontend found by exercising it rather than by testing it,
two of them in paths that had just shipped. The pattern is not subtle any more: **this
frontend has no test runner, so anything not driven by hand is unverified.**
**The scroll path is finally driven.** 602-action fixture, three prepends of ~16,200 px
each: the same DOM node held viewport top 792 → 787, and the view stayed 48,174 px from