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
@@ -75,9 +75,9 @@ Not items. Things that are true of every change, and that have gone wrong when s
|
||||
## Sections
|
||||
|
||||
1. **Play it at a table** — #39 #35 #42a #40
|
||||
2. **The common board, and watching play happen — Gitea#20** — #13 #15 #18 #91 #75
|
||||
2. **The common board, and watching play happen — Gitea#20** — #13 #15 #18 #75
|
||||
3. **Multiplayer, sessions and operations** — #8 #7 #76 #77 #79
|
||||
4. **The screen** — #94 #44 #81 #33 #36
|
||||
4. **The screen** — #44 #81 #33 #36
|
||||
5. **Replays and saved games** — #14 #47 #48 #49 #50 #51 #52
|
||||
6. **Rules** — #12 #80 #82 #83 #85
|
||||
7. **Play balance** — #61 #62 #63 #64 #67 #68 #69 #70 #71 #72 #73 #66 #65 #74
|
||||
@@ -149,17 +149,6 @@ cheaper.
|
||||
it needs the async stepped pump that Gitea#20 step 4 specifies**, which is why it lives here
|
||||
rather than under The screen. See **Reference · #18**.
|
||||
|
||||
- [ ] **#91** — **Narration is still outside the redaction net, and the two known leaks in it are
|
||||
fixed but the net is not.** `game.log` is one shared list that `linesSince` slices with no
|
||||
per-seat filter, so anything written into it reaches every player. The seed and the blind-draw
|
||||
card name were closed in v0.7.9.2; what has NOT been done is the systematic check the plan
|
||||
asks for — serialise the log alongside the Frame and search it for every opponent's card ids
|
||||
AND display names, objective names, `justDrawn` for the wrong seat, and private decision data,
|
||||
across a fresh game, a pending decision, the Superintendent acting, Employee Rotation, a
|
||||
reconnect and a finished game. **This is Gitea#20 step 1's starting point and its acceptance
|
||||
bar** — the plan is explicit that passing redaction tests alone is insufficient and that every
|
||||
public property needs an allow-list review. See **Reference · #91**.
|
||||
|
||||
- [ ] **#75** — Let the game join a call and talk to the table — the chat, audio and nudge half of the
|
||||
idea Gitea#20 took the visual half of. Long-term. See **Reference · #75**.
|
||||
|
||||
@@ -195,12 +184,6 @@ happening, and the turn structure that is still solitaire-shaped.
|
||||
What is drawn and where, for a player at the board. The three items settled on 2026-08-30 shipped in
|
||||
v0.7.9; what is left is the history panel and the end-of-game statistics.
|
||||
|
||||
- [ ] **#94** — **A Red Flag standing at an Office's Limits is drawn nowhere.** It is set out on the
|
||||
board, it holds the next train from that side, and after the one log line announcing it there is
|
||||
nothing on screen saying it is there. Confirmed in code rather than inferred. **The same class
|
||||
as Gitea#21 and #22 — the engine is right and the screen is silent — and it is already on
|
||||
Gitea#20 step 1's list, so doing it now is 0.8.0 groundwork.** See **Reference · #94**.
|
||||
|
||||
- [ ] **#44** — How much history the panel holds should be configurable. The cap is `slice(-60)` with
|
||||
no recorded reason anywhere. **Where the setting lives is an open question and the item exists
|
||||
to ask it** — a StartOS action, per game, or per browser. The replay viewer already answers the
|
||||
@@ -603,42 +586,6 @@ of enforced waiting per Stage, forty-eight per Day, and a player who has seen it
|
||||
will want it off. Whatever this becomes probably needs a speed control, or to scale with whether
|
||||
anything actually happened in the phase.
|
||||
|
||||
#### #91 — NARRATION IS OUTSIDE THE REDACTION NET.
|
||||
|
||||
**NARRATION IS OUTSIDE THE REDACTION NET.** `test/redaction.test.ts` serialises a seat's whole
|
||||
|
||||
`Frame` and asserts no other seat's card ids or deck order appear in it — and every one of those
|
||||
tests passes `[]` for the log. So the shared narration has never been checked at all, while
|
||||
`game.log` is ONE list and `linesSince(seat)` (`server/session.ts:181`) slices it with no per-seat
|
||||
filter whatsoever. Every line written there reaches every player.
|
||||
|
||||
**Two leaks found and closed in v0.7.9.2**, both discovered while planning Gitea#20 and both live in
|
||||
multiplayer with or without that display: the **seed**, announced in the opening line of every
|
||||
multiplayer game, and the **name of a card drawn blind** from the Home Office deck. The tests for
|
||||
them are in `redaction.test.ts` now, so narration is no longer entirely unchecked — but two specific
|
||||
strings are not a net.
|
||||
|
||||
**Solitaire deliberately keeps both**, and that is the rule to apply to anything found next: a
|
||||
one-seat table has nobody to leak to, the seed in the log is what a bug report quotes, and a solo
|
||||
player's own history naming their own draw is the record. The rule is "do not tell the OTHER seats",
|
||||
not "write less down".
|
||||
|
||||
**What is still owed** is the plan's own list (`docs/plans/jitsi-common-board.md`, Step 1 § Tests):
|
||||
serialise the public frame *and* the player pushes and search for every opponent hand-card id **and
|
||||
display name**, objective ids and names, `justDrawn` for the wrong player, seed values and seed
|
||||
narration, and private decision/menu data — across a newly created game, a blind draw, a pending
|
||||
decision, the Superintendent acting, Employee Rotation before and after ownership changes, a
|
||||
reconnect push, and a finished game. **And the plan's acceptance bar is not the tests**: it requires
|
||||
an allow-list review of every public property, on the grounds that passing redaction tests alone is
|
||||
insufficient. That is the right bar — the two leaks above would have passed any test nobody thought
|
||||
to write.
|
||||
|
||||
**Supersedes #78**, which said the exhaustive Frame check was "still missing… held for now,
|
||||
2026-08-20". It is not missing: `test/redaction.test.ts` exists and does exactly what #78 described
|
||||
— serialise a seat's Frame, assert no other seat's card ids and no deck order. #78 was written
|
||||
before that file and was never revisited, so it read as live work for two weeks after it was done.
|
||||
**The gap that is actually real is the log, which #78 never mentioned.**
|
||||
|
||||
#### #75 — Let the game join a call and talk to the table.
|
||||
|
||||
**Let the game join a call and talk to the table.** Long-term. If the game could join a Zoom,
|
||||
@@ -802,39 +749,6 @@ and whether they took it the moment their turn arrived.
|
||||
|
||||
### The screen
|
||||
|
||||
#### #94 — A RED FLAG STANDING AT THE LIMITS IS DRAWN NOWHERE.
|
||||
|
||||
**A RED FLAG STANDING AT AN OFFICE'S LIMITS IS DRAWN NOWHERE.** Found 2026-09-07 while checking
|
||||
|
||||
which of the Gitea#20 step 1 findings were still real. It is real, and it is a play bug now rather
|
||||
than a common-board one.
|
||||
|
||||
**What the engine does**, traced rather than assumed. `maneuver.redFlags` emits `redFlagsSet`, whose
|
||||
reducer sets `node.redFlag = side` on that Office's Division node (`apply.ts:2307`). From then on
|
||||
`redFlagStop` holds the next train arriving from that side — `dest.redFlag === from` → `spendFlag`,
|
||||
which removes the flag and emits `redFlagSpent` (`advance.ts:1216, 1173`). So it is a standing token
|
||||
on the board that stops a train, exactly as a flag on the table would be.
|
||||
|
||||
**What the screen does.** `narrate` announces it once — "RED FLAGS set out on the Eastern Limits" —
|
||||
and then it is gone with the scroll. The Office node in `DivisionView` carries `kind`, `label`,
|
||||
`trains`, `capacity`, `modifiers`, `gradeUp`, `seat`, `running` and `switching`, and **no `redFlag`
|
||||
field at all**; `redFlag` appears nowhere in `board-svg.ts` and nowhere in the web layer. The map
|
||||
draws the Office, its A/D tracks and the trains standing on it, and not the flag at its Limits.
|
||||
|
||||
**So a player who set a flag out three Stages ago has nothing telling them it is still there, and an
|
||||
opponent who missed the line never knew.** Then a train stops short, and the only explanation is a
|
||||
log entry that has scrolled away. That is the shape of Gitea#21 — a correct refusal with no visible
|
||||
reason — and of Gitea#22 — a board that does not show what the rules are acting on.
|
||||
|
||||
**Why it belongs before 0.8.0 rather than in it:** the plan already lists "add the Red Flag holder to
|
||||
the public player projection — it is public game state but is currently absent from `Frame`" as part
|
||||
of step 1. The field has to exist on the projection either way, so every hour spent on it is 0.8.0
|
||||
groundwork rather than a detour. **The drawing is the open question, not the data** — a flag at the
|
||||
Limits column is the obvious rendering, and `board-svg.ts` already draws those edges (`edge()`), so
|
||||
there is somewhere to hang it.
|
||||
|
||||
**Not fixed. Sized: small on the data, a judgement call on the picture.**
|
||||
|
||||
#### #44 — The history panel's 60-line cap is hard-coded, and how much history it…
|
||||
|
||||
**The history panel's 60-line cap is hard-coded, and how much history it holds should be
|
||||
@@ -1816,11 +1730,68 @@ where it belongs, and it is still open.
|
||||
Closed items, kept because several are the only record of a ruling or a lesson. Newest first within
|
||||
each group.
|
||||
|
||||
### Shipped through v0.7.9.3, from the queue
|
||||
### Shipped through v0.7.9.4, from the queue
|
||||
|
||||
Closed items, newest first. Kept because several of them are the only record of a ruling or a lesson;
|
||||
the numbers stay so cross-references above and below still resolve.
|
||||
|
||||
91. ~~**Narration was outside the redaction net, and so was everything else nobody had listed.**~~ —
|
||||
done 2026-09-07 in v0.7.9.4. The systematic pass Gitea#20 step 1 asks for: serialise a seat's
|
||||
Frame, the public board and the narration it receives, and search all three for every opponent
|
||||
card id, every card NAME that is 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 and a played-out game.
|
||||
**And the allow-list, which is the plan's actual acceptance bar:** every property of
|
||||
`publicSnapshot` is written down and compared, so adding a field fails the suite until somebody
|
||||
has said out loud that a spectator may see it. That is the check that would have caught both
|
||||
v0.7.9.2 leaks, since both were fields nobody had asked the question about.
|
||||
|
||||
**Worth knowing, and it cost two false failures to learn: a card NAME is a type, not an
|
||||
identity.** "right-hand curve" names a dozen cards and one is legitimately drawn on the board as
|
||||
a cell label the moment anybody lays track, so searching for it fails on correct code. A name
|
||||
counts as evidence only when EVERY card bearing it is in the one hand. Ids need no such care.
|
||||
**And a one-digit seed makes the seed check meaningless** — seed 7 matched "Train 7". The seeds
|
||||
in this file are nine digits deliberately.
|
||||
|
||||
Proved by mutation rather than by passing: restoring the seed line fails 6 tests, restoring the
|
||||
blind-draw 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. Said here so the next reader does not go looking for the gap.
|
||||
|
||||
94. ~~**A Red Flag standing at an Office's Limits was drawn nowhere.**~~ — done 2026-09-07 in
|
||||
v0.7.9.4. It is a token set out ON the board that holds the next train arriving from that side;
|
||||
it was announced once in the log and then existed only in the engine, so a train would stop
|
||||
short with its only explanation 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 — because which approach it covers is the whole of the information; a flag in
|
||||
the middle would say one is out and leave the reader to hover for the half that decides whether
|
||||
to run a train. **Worth knowing:** the same class as Gitea#21 and #22, and the third in a row —
|
||||
when the engine gains a thing that CHANGES what a train may do, ask where it is drawn before
|
||||
asking whether it works.
|
||||
|
||||
95. ~~**The public projection helpers — Gitea#20 step 1's foundation.**~~ — done 2026-09-07 in
|
||||
v0.7.9.4. `projectDistrict(state, seat)`, `projectDivision(state)`, `projectSharedTable(state)`,
|
||||
`publicSnapshot(state)` and `currentActorOfState(state)`, with `snapshot()` **rebuilt to compose
|
||||
from the same helpers** rather than keeping its own copy — so a player's frame and a spectator's
|
||||
cannot come to disagree about the clock, the phase or the score. Behaviour-neutral, and the
|
||||
existing 897 tests are the proof of that.
|
||||
|
||||
**Districts are keyed by SEAT and the player resolved through `playerAtSeat`**, because Employee
|
||||
Rotation moves players between districts; a public board that assumed seat and player index were
|
||||
interchangeable would relabel every district the first time anybody rotated. **And the public
|
||||
view is composed upward, never by calling `snapshot()` once per seat** — that shortcut builds
|
||||
every private field and then has to remember to strip it, and `snapshot` defaults its viewer to
|
||||
player zero, so a careless spectator call would have served seat 0's hand.
|
||||
|
||||
**One plan finding struck off rather than fixed:** it warns that a public display reading
|
||||
`clock.currentActor` could highlight the wrong district during a decision. Measured across six
|
||||
seeds and 3,600 decision points, that field 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.
|
||||
|
||||
32. ~~**Tell the 0.4.9 playtesters their saves are dead, before they find out.**~~ — done
|
||||
2026-09-07. `PLAYTEST-0.7.4.md` was written for exactly this and did its job; Jesse, 2026-09-07:
|
||||
"a temporary document to help some of the playtesters out on making the big jump, but that is no
|
||||
|
||||
Reference in New Issue
Block a user