v0.7.9.4 — Gitea#20 step 1, and a Red Flag you can see
Step 1 of the common board done as its own release rather than as the first hour of 0.8.0, since both halves of it are worth having whether or not anything is ever published to a call. #95 — the public projection helpers. `projectDistrict(state, seat)`, `projectDivision(state)`, `projectSharedTable(state)`, `publicSnapshot(state)` and `currentActorOfState(state)`, with `snapshot()` REBUILT to compose from the same helpers rather than keeping a second copy of the shared table, so a player's frame and a spectator's cannot come to disagree about the clock, the phase, whose turn it is or the score. Behaviour-neutral; the 897 existing tests passing unchanged is the proof. The public view is composed UPWARD, never by calling `snapshot()` once per seat. That shortcut is the trap the plan names: `snapshot` assembles one player's view, so a public view made of player views builds every private field and then has to remember to strip it — and it defaults its viewer to player zero, so a careless spectator call would have served seat 0's hand. Districts are keyed by SEAT with the player resolved through `playerAtSeat`, because Employee Rotation moves players between districts and a board that treated seat and player index as interchangeable would relabel every district the first time anybody rotated. One plan finding is struck off rather than fixed: it warns a display reading `clock.currentActor` could highlight the wrong district during a decision. Measured over six seeds and 3,600 decision points, that field and `actingPlayer` never disagreed. `currentActorOfState` exists anyway, as one place for the next reader to ask. #91 — the redaction net, systematically. v0.7.9.2's two leaks were found by reading a plan, not by a test, which is the whole argument for this: a suite made of the leaks somebody happened to notice proves nothing about the next one. Serialise a seat's Frame, the PublicFrame a spectator gets and the narration they receive, then search all three for every opponent card id, every card name unique to one opponent's hand, the seed and any private decision or menu data — across a fresh game, a blind draw, mid-game, a pending decision, Employee Rotation before and after the seating moves, a reconnect push (a full Frame, and its own opportunity to leak) and a played-out game. And the allow-list, which is the plan's stated acceptance bar rather than the tests: every property of `publicSnapshot` is written down with its reason and compared on every run, so adding a field fails the suite until somebody has said out loud that a spectator may see it. Both v0.7.9.2 leaks were fields nobody had ever asked that question about. Proved by mutation rather than by passing: restoring the seed line fails 6 tests, restoring the blind-draw card name fails 1, adding a private field to the public projection fails 7, and making `players[]` carry hand contents instead of a count fails 5. Two false failures were worth the lesson. A card NAME is a type, not an identity — "right-hand curve" names a dozen cards and one is legitimately a cell label the moment anybody lays track, so searching for it fails on correct code, which is worse than not searching; a name is evidence only when every card bearing it is in the one hand. And a one-digit seed makes the seed check meaningless: seed 7 matched "Train 7". One item on the plan's list has no test because it has no referent — there is no secret objective in this game, `objectiveOf` deriving from `config.minCombinedRevenue` and the player's own Revenue, both public. #94 — a Red Flag standing at an Office's Limits is on the map. It is a token set out ON the board that holds the next train arriving from that side, and it was announced once in the log and drawn nowhere, so a train stops short three Stages later with its only explanation scrolled out of the panel. `DivisionView`'s office node carries `redFlag` and the map draws a staff and pennant AT THE END IT GUARDS — west on the left, east on the right — because which approach it covers is the whole of the information; a flag in the middle of the cell would say one is out and leave the reader to hover for the half that decides whether to run a train. The tooltip leads with it, ahead of everything that merely describes the cell. The third of these in a row after Gitea#21 and #22: when the engine gains something that changes what a train may do, the question to ask is where it is drawn, not whether it works. 909 tests pass, up from 897. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ss2y7FyhxkHjGj7xnUPCgY
This commit is contained in:
co-authored by
Claude Opus 5
parent
e734481d65
commit
ebd16983e2
@@ -19,6 +19,88 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
can now see.
|
||||
|
||||
### A Red Flag standing at the Limits is on the map (#94)
|
||||
|
||||
`maneuver.redFlags` sets a flag on an Office's Division node, and from then on the next train
|
||||
arriving from that side is held short until the flag is spent. It is a token standing on the board —
|
||||
the same kind of object as a train — and it was announced once in the log and drawn nowhere. Three
|
||||
Stages later a train stops and the only explanation has scrolled out of the panel.
|
||||
|
||||
`DivisionView`'s office node carries `redFlag` now, and the map draws a staff and pennant **at the
|
||||
end it guards** — west on the left, east on the right, since east is right on this map. Which
|
||||
approach it covers is the whole of the information: a flag in the middle of the cell would say a
|
||||
flag is out and leave the reader to hover for the half that decides whether to run a train. The
|
||||
tooltip leads with it, ahead of everything that merely describes the cell.
|
||||
|
||||
**The third of these in a row**, after Gitea#21 and #22. When the engine gains something that
|
||||
changes what a train may do, the question to ask is where it is drawn, not whether it works.
|
||||
|
||||
### The public projection helpers (#95)
|
||||
|
||||
`projectDistrict(state, seat)`, `projectDivision(state)`, `projectSharedTable(state)`,
|
||||
`publicSnapshot(state)` and `currentActorOfState(state)` — and **`snapshot()` rebuilt to compose
|
||||
from the same helpers** rather than keeping a second copy of the shared table. A player's frame and
|
||||
a spectator's now cannot come to disagree about the clock, the phase, whose turn it is or the score.
|
||||
Behaviour-neutral: the existing 897 tests passing unchanged is the proof.
|
||||
|
||||
**The public view is composed upward, never by calling `snapshot()` once per seat.** That shortcut
|
||||
is the trap the plan warns about: `snapshot` exists to assemble one player's view, so a public view
|
||||
made of player views starts by building every private field and then has to remember to strip it —
|
||||
and it defaults its viewer to player zero, so a careless spectator call today would have served seat
|
||||
0's hand. Composing upward means a private field cannot arrive by accident; it would have to be
|
||||
added to a projection that has no business holding one.
|
||||
|
||||
**Districts are keyed by seat, with the player resolved through `playerAtSeat`.** Employee Rotation
|
||||
moves players between districts, so seat and player index are not interchangeable — a board that
|
||||
assumed they were would relabel every district the first time anybody rotated.
|
||||
|
||||
One finding from the plan is struck off rather than fixed: it warns that a display reading
|
||||
`clock.currentActor` could highlight the wrong district during a decision, since that field is null
|
||||
while an interruption stands. Measured across six seeds and 3,600 decision points, it and
|
||||
`actingPlayer` never disagreed — both are only consulted when somebody is genuinely acting.
|
||||
`currentActorOfState` exists anyway, as one place for the next reader to ask.
|
||||
|
||||
### The redaction net, systematically (#91)
|
||||
|
||||
v0.7.9.2 closed two leaks. Both were found by reading a plan rather than by a test, which is the
|
||||
whole argument for this: a suite made of the leaks somebody happened to notice proves nothing about
|
||||
the next one.
|
||||
|
||||
Serialise a seat's `Frame`, the `PublicFrame` a spectator gets, and the narration they receive, then
|
||||
search all three for every opponent card id, every card name unique to one opponent's hand, the
|
||||
seed, and any private decision or menu data — across a fresh game, a blind draw, mid-game, a pending
|
||||
decision, Employee Rotation before and after the seating moves, a reconnect push (a full Frame, not
|
||||
a delta, and its own opportunity to leak) and a played-out game.
|
||||
|
||||
**And the allow-list, which is the plan's stated acceptance bar rather than the tests.** Every
|
||||
property of `publicSnapshot` is written down with the reason it is public and compared on every run,
|
||||
so adding a field fails the suite until somebody has said out loud that a spectator may see it. Both
|
||||
v0.7.9.2 leaks were fields nobody had ever asked that question about.
|
||||
|
||||
**Two false failures were worth the lesson. A card NAME is a type, not an identity:** "right-hand
|
||||
curve" names a dozen cards and one is legitimately drawn as a cell label the moment anybody lays
|
||||
track, so searching for it fails on correct code — which is worse than not searching. A name counts
|
||||
as evidence only when every card bearing it is in the one hand. **And a one-digit seed makes the
|
||||
seed check meaningless**: seed 7 matched "Train 7" and reported a leak that was not one. The seeds
|
||||
here are nine digits deliberately.
|
||||
|
||||
Proved by mutation rather than by passing: restoring the seed line fails 6 tests, restoring the
|
||||
blind-draw card name fails 1, adding a private field to the public projection fails 7, and making
|
||||
`players[]` carry hand contents instead of a count fails 5.
|
||||
|
||||
One item on the plan's list has no test because it has no referent: **there is no secret objective in
|
||||
this game.** `objectiveOf` derives from `config.minCombinedRevenue` and the player's own Revenue,
|
||||
both public. Recorded so the next reader does not go looking for the gap.
|
||||
|
||||
909 tests pass, up from 897.
|
||||
|
||||
---
|
||||
|
||||
## 0.7.9.3 — 2026-09-07
|
||||
|
||||
Documentation and the build script behind it. No engine change; 897 tests pass, unchanged.
|
||||
|
||||
Reference in New Issue
Block a user