v0.7.9.5 — two answers to one question, and the copy nobody read
Both faults are in what 0.7.9.4 had just built, and both are the same shape: a second copy of an answer that agreed with the first until it didn't. #96 — the §3.3 vote has no actor, and the screen named one anyway. The vote is PARALLEL: every un-voted seat may vote at any moment, in any order, one refusal ends it, and `apply.ts` says where it accepts one that there is no actor to be. The turn chart named the last seat to move before the timetable ran out — no more claim on the vote than anybody else — directly above a tally correctly showing three seats outstanding. The cause is worth more than the symptom. `currentActor(game)` (`web/game.ts`) guarded on `status !== 'active'`; `currentActorOfState` (`sim/view.ts`), added the same day in #95 and the one the frame calls, did not, so it handed back whatever `clock.currentActor` was left holding. The view now carries the guard and `currentActor` delegates to it. That matters more than the tidiness: `currentActor` is what REFUSES an intent, so a screen answering differently tells the table to wait on a player the server would turn away. The fourth of this class after Gitea#21, #22 and #94 — but the first found by asking a view helper its question in a state the game is not `active` in, which is the generalisation and is cheaper than finding the fifth the same way. #97 — narration reaches a seat once, by one path. `Frame.lines` carried the whole log on every push to every seat, and nothing read it: `RemoteSession` accumulates from `push.lines` alone and its `lines()` returns that accumulator, so the log was serialised into every frame, grew all game, and was discarded on arrival while `linesSince` sent the same text correctly beside it. The duplicate was masking a bug rather than merely wasting bandwidth. `connect()` cleared `lastFrame` but not `sentLines`, so a reconnecting seat was told "nothing new since your last push" while the browser it answered had just reloaded from an EMPTY accumulator — the history panel came back blank, mid-game, with the server holding the whole log. So the two halves are one change, and the plan's instruction taken alone ("stop passing the full game log into `frameFor()`") would have deleted a real behaviour rather than a duplicate. Every remaining reader of `Frame.lines` was checked before the field was emptied: all of them are the solitaire and replay path, which builds Frames through `snapshot()` directly and never goes near a session. One test was wrong before the code was. The first draft of the reconnect test connected inside its own fixture, so both sides of the comparison were the empty array and it passed against the broken server. Each test now asserts its premise is non-empty before comparing. Also: `docs/plans/jitsi-common-board.md` is committed. It was never added — not ignored, just missed — while TODO.md cites it twice as the plan for all of v0.8.0 and the last two releases were built from it, so a clone got a TODO pointing at a file that did not exist. 917 tests pass, up from 909. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R
This commit is contained in:
co-authored by
Claude Opus 5
parent
ebd16983e2
commit
d5445badcc
@@ -19,6 +19,74 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.7.9.5 — 2026-09-07
|
||||
|
||||
Two faults in what 0.7.9.4 had just built, both of the same shape: a second copy of an answer that
|
||||
agreed with the first until it didn't.
|
||||
|
||||
### The §3.3 vote had no actor, and the screen named one anyway (#96)
|
||||
|
||||
Extended play's vote is **parallel**. Every un-voted seat may vote at any moment, in any order, and
|
||||
one refusal ends it — `apply.ts` says in as many words where it accepts a vote that there is no actor
|
||||
to be. So the honest answer to "whose turn is it" is nobody, and the honest answer to "who are we
|
||||
waiting on" is every un-voted seat, which is exactly what the tally beside the turn chart already
|
||||
drew.
|
||||
|
||||
The chart disagreed with the tally directly above it. It named the last seat to move before the
|
||||
timetable ran out — a seat with no more claim on the vote than anybody else — while the tally
|
||||
correctly showed three outstanding.
|
||||
|
||||
The cause is worth more than the symptom. `currentActor(game)` in `web/game.ts` guarded on
|
||||
`status !== 'active'` and returned null. `currentActorOfState(state)` in `sim/view.ts` — added the
|
||||
same day in #95, and the function the frame actually calls — had no such guard, so it handed back
|
||||
whatever `clock.currentActor` was left holding after the game stopped being `active`. Two functions
|
||||
answering one question, correct in every state anybody had looked at.
|
||||
|
||||
`currentActorOfState` carries the status guard now and `currentActor` delegates to it, so there is
|
||||
one answer. That matters more than the tidiness: **`currentActor` is what refuses an intent**, so a
|
||||
screen answering differently is telling the table to wait on a player the server would turn away.
|
||||
|
||||
This is the fourth of this class in a row after Gitea#21, #22 and #94. It is the first found by
|
||||
asking a view helper its question in a state the game is **not** `active` in — which is the
|
||||
generalisation, and cheaper than finding the fifth the same way.
|
||||
|
||||
### Narration reaches a seat once, by one path (#97)
|
||||
|
||||
`Frame.lines` carried the whole narration log on every push, to every seat, and nothing read it.
|
||||
`RemoteSession` (`web/session.ts`) accumulates `lines` from `push.lines` alone, and its `lines()`
|
||||
returns that accumulator — so the log was serialised into every frame, grew all game, and was
|
||||
discarded on arrival, while `linesSince` sent the same text correctly beside it.
|
||||
|
||||
**The duplicate was masking a bug rather than merely wasting bandwidth.** `connect()` cleared
|
||||
`lastFrame` but not `sentLines`, so a reconnecting seat was told "nothing new since your last push"
|
||||
— while the browser it was answering had just reloaded and started from an empty accumulator. The
|
||||
history panel came back blank, mid-game, with the server holding the whole log and shipping it in the
|
||||
one field nobody reads.
|
||||
|
||||
So the two halves are one change, and the plan's instruction taken alone — "stop passing the full
|
||||
game log into `frameFor()`" — would have deleted a real behaviour instead of a duplicate. A
|
||||
(re)connect now resets the seat's watermark, and `Push.lines` on a connect **is** the history, which
|
||||
is what lets the Frame stop carrying a second copy.
|
||||
|
||||
Every remaining reader of `Frame.lines` was checked before the field was emptied: all of them
|
||||
(`sim/replay.ts`, `web/replays.ts`, and the sim and replay tests) are the solitaire and replay path,
|
||||
which builds its Frames through `snapshot()` directly and never goes near a session.
|
||||
|
||||
**One test was wrong before the code was.** The first draft of the reconnect test connected inside
|
||||
its own fixture, so both sides of the comparison were the empty array and it passed against the
|
||||
broken server — two empty arrays are `deepEqual`. Each of these tests now asserts its premise is
|
||||
non-empty before comparing.
|
||||
|
||||
### Also
|
||||
|
||||
`docs/plans/jitsi-common-board.md` is committed. It was never added — not ignored, just missed —
|
||||
while `TODO.md` cites it twice as the plan for all of v0.8.0 and the last two releases were built
|
||||
from it, so a clone got a TODO pointing at a file that did not exist.
|
||||
|
||||
917 tests pass, up from 909.
|
||||
|
||||
---
|
||||
|
||||
## 0.7.9.4 — 2026-09-07
|
||||
|
||||
Gitea#20 step 1, done as its own release rather than as the first hour of 0.8.0 — and a Red Flag you
|
||||
|
||||
Reference in New Issue
Block a user