v0.7.9.2 — two things the table could hear that only one seat should

Both leaks were found while planning the common board (Gitea#20 step 1),
and both are live multiplayer bugs with or without that display, so they
are fixed now rather than with 0.8.0.

`game.log` is one shared list and `linesSince(seat)` slices it with no
per-seat filter, so every line reaches every player. It carried the SEED
in the opening line of each multiplayer game — the whole future of the
deal — and the NAME OF A CARD DRAWN BLIND from the face-down Home Office
deck. Solitaire deliberately keeps both: a one-seat table has nobody to
leak to, the seed is what a bug report quotes, and a player's own history
naming their own draw is the record. A Department slot is face up and
stays named. The drawer still learns their card through `justDrawn`,
which already goes to that seat alone.

Neither was found by a test. Every test in `redaction.test.ts` passes an
empty log, so the whole of narration has sat outside the redaction net
since the net was built. Both now have tests there; TODO #91 carries what
is still owed and supersedes #78, which described a gap that had already
been closed and never mentioned this one.

`docs/rules/` had no current description of the game, and `content.ts`
named `card-reference.md` as the file that carries what the cards say —
a file whose own banner says not to use its numbers, describing the
v0.4.5 deck where 3/4 is a Mail-Express with three coaches. Every file in
that directory is a deliberate historical record, so none of them is
rewritten. `as-built.md` is new and GENERATED from the same catalogues
the engine instantiates from, with a test that re-runs the generator and
fails when the checked-in file disagrees. A hand-written replacement
would have drifted the same way, for the same reason.

TODO.md: #32 closed — the playtest migration note did its job and the
jump is made; the durable fact it carried is kept. #78 retired in favour
of #91. The "play it at a table" section now records that 0.7.4-0.7.9
were test-run without change requests, and that more testing comes at the
end of the 0.7.9 series.

897 tests pass, up from 891.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg
This commit is contained in:
Jesse.Markowitz
2026-09-07 12:09:25 -04:00
co-authored by Claude Opus 5
parent 7ade60e21f
commit 819996faa2
10 changed files with 717 additions and 41 deletions
+86 -34
View File
@@ -74,8 +74,8 @@ 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 #32
2. **The common board, and watching play happen — Gitea#20** — #13 #15 #18 #78 #75
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
3. **Multiplayer, sessions and operations** — #8 #7 #76 #77 #79
4. **The screen** — #44 #81 #33 #36
5. **Replays and saved games** — #14 #47 #48 #49 #50 #51 #52
@@ -94,8 +94,14 @@ ruling or a lesson).
## Play it at a table
The largest gap in the project, and none of it is a coding gap. Features are shipped, packed,
running on `phoenix.local` — and no person has met them at a board. Everything else in this file
waits behind a release; this waits behind an afternoon.
running on `phoenix.local` — and the items below name the ones no person has met at a board.
Everything else in this file waits behind a release; this waits behind an afternoon.
**Test runs WERE made across 0.7.4 through 0.7.9** (Jesse, 2026-09-07) and produced no change
requests — the two bugs that did come out of them are Gitea#21 and #22, fixed in v0.7.9.1. So this
section is not "nobody has touched it since 0.7.4"; it is the narrower and still-true claim that the
specific paths below have not been exercised at a table. **More testing is planned at the end of the
0.7.9 series, before 0.8.0 starts** — that is the moment to close these, not a separate errand.
- [ ] **#39** — **None of v0.7.4 has been played by a human.** The Yard Office offer, the Red Flag
hold and its out-of-phase prompt, and the loaded-Extra make-up rules are tested end to end,
@@ -118,11 +124,6 @@ waits behind a release; this waits behind an afternoon.
in a release note rather than code, and worth knowing when a bug report arrives with a save that
will not load. See **Reference · #40**.
- [ ] **#32** — **Tell the 0.4.9 playtesters their saves are dead, before they find out.** The same
shape as #40 for the playtest line; the saves attached to Gitea#15 and #17 are among them.
`PLAYTEST-0.7.4.md` at the repo root is the note drafted for this and leads with it. See
**Reference · #32**.
---
## The common board, and watching play happen — Gitea#20
@@ -148,8 +149,16 @@ 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**.
- [ ] **#78** — The redaction test is more done than the plan suggests, but the remaining gap is real
and is Gitea#20 step 1's starting point. See **Reference · #78**.
- [ ] **#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**.
@@ -453,14 +462,6 @@ the move and leaves the file untouched — and `WHISTLE-4086` did survive on `ph
is "may not" rather than "will not". Worth a line wherever the build is announced, and worth
knowing when a bug report arrives with a save that will not load.
#### #32 — Tell the 0.4.9 playtesters their saves are dead, before they find out.
**Tell the 0.4.9 playtesters their saves are dead, before they find out.** The same shape as #40
but for the playtest line: the deck change shipped as v0.4.9h, so every save filed before it —
including the ones attached to Gitea#15 and #17 — stops replaying at its first `card.play`. They
fail safe and the files are kept, but nobody has been told. `PLAYTEST-0.7.4.md` (untracked, at
the repo root) is the note drafted for this and leads with it.
### The common board, and watching play happen — Gitea#20
#### #13 — I CANNOT SEE WHAT THE OTHER PLAYERS DID — BOTS INCLUDED.
@@ -582,22 +583,41 @@ 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.
#### #78 — The redaction test (multiplayer.md §7) is more done than the plan sugg…
#### #91 — NARRATION IS OUTSIDE THE REDACTION NET.
**The redaction test (multiplayer.md §7) is more done than the plan suggests, but the
**NARRATION IS OUTSIDE THE REDACTION NET.** `test/redaction.test.ts` serialises a seat's whole
exhaustive check is still missing.** `test/multiplayer.test.ts`'s "the view shows one seat at
a time" section (added earlier) already proves `snapshot(s, ..., viewer)` gives each seat its
own hand, board, Revenue and impediments — traced `snapshot()` itself
(`src/sim/view.ts:1180-1219`): `hand` reads only `s.decks.hands.get(viewer)`, `deck` is a
count, other seats' hands appear only as `.length`, and `Frame`'s type has no `seed`,
`rngState` or card-id-dictionary field for anything to leak through by accident. What exists
is all spot-checks, though — "this seat's Frame has the right hand length." What's still
missing is the exhaustive one §7 actually calls for: serialize a seat's `Frame` and assert it
contains none of another seat's actual card ids and no deck order, so a future careless edit
is caught rather than assumed safe. Doesn't need a server — buildable now against `snapshot()`
and the existing `game()`/`playGame` harness already in `multiplayer.test.ts`. Held for now,
2026-08-20.
`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.
@@ -1729,11 +1749,43 @@ re-verified turn out to have been re-verified against a premise rather than agai
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.1, from the queue
### Shipped through v0.7.9.2, 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.
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
longer needed." The jump is made, so the note is retired rather than committed. **The durable
fact, which is why this entry stays:** a save written by v0.4.9h does not replay on 0.7.x — the
deck changed, so it stops at its first `card.play` — and that includes the saves attached to
Gitea#15 and #17. It fails safe, naming the move and leaving the file untouched, so a bug report
arriving with a save that will not load is this and not a new fault. #40 is the same shape on the
main line and is still open.
92. ~~**Two multiplayer information leaks in the shared narration log.**~~ — done 2026-09-07 in
v0.7.9.2, found while planning Gitea#20 step 1. The **seed** was announced in the opening line of
every multiplayer game and a **blind Home Office draw named the card** — and `linesSince(seat)`
slices one shared `game.log` with no per-seat filter, so both went to every player. **Ruling:
solitaire keeps both**, because a one-seat table has nobody to leak to, the seed is what a bug
report quotes, and a solo player's history naming their own draw is the record. A Department
slot is face up and stays named for the same reason. **Worth knowing:** these were not found by
a test, they were found by reading the plan — every redaction test passes `[]` for the log, so
the whole of narration was unchecked. #91 is what remains.
93. ~~**`docs/rules/` had no current description of the game, and the code pointed at a superseded
one.**~~ — done 2026-09-07 in v0.7.9.2. `content.ts` named `card-reference.md` as "the place
that now carries what the cards say" while that file's own banner said not to use its numbers;
it describes the v0.4.5 deck, where 3/4 is a Mail-Express with three coaches against a `content.ts`
whose train 3 is the Express with two freight cars. **The fix is not a rewritten table** — every
file in `docs/rules/` is a deliberate historical record and worth more intact than patched. A new
`as-built.md` is GENERATED from the same catalogues the engine instantiates from, by
`scripts/build-card-reference.ts` (`npm run build:cards`), and `test/card-reference.test.ts`
re-runs the generator and fails if the checked-in file disagrees. **Worth knowing:** a
hand-written replacement would have drifted the same way and for the same reason — nothing fails
when a table falls behind a constant. Generate it or check it; do not retype it.
89. ~~**The map drew westbound trains in the wrong half of a Mainline card.**~~ — done 2026-09-07 in
v0.7.9.1, Gitea#22. `regionOfTransit` counts from the end a train ENTERED, which is what the
collision rules want; the map wanted "which printed box, left to right" and used the same number,