v0.8.5 — housekeeping from the audit, and the playtest line retired
The third release from the audit; nothing a player sees changes. CHANGELOG has the detail. The 0.4.9 playtest line is no longer maintained (Jesse, 2026-09-29): the deploy rule that existed for it is gone and #85 is moot. The table test (#39 #35 #42a #40) is closed — every line of the checklist was met at a table. #46 is done and cannot regrow: the 36 unused declarations are removed and `noUnusedLocals`/`noUnusedParameters` are on; two of them were dead bot functions from rejected candidates the round said it had deleted. The documents no longer teach `trainCapSlack` (a knob that throws), point at `as-built.md` (deleted in 0.8.2), model `officeType` (the engine says `tier`) or describe `collisionOccurred` (never emitted); the README's account of bot flags now matches the bot's. Five playtest saves committed in `docs/` against the repository's own rule are in the ignored `playtests/`. What the audit found and did not fix is written down as TODO #112-#117, each with its reason. #112 is `docs/plans/structure.md`, the proposal for `http.ts`, `main.ts` and `check`. #117 — `/api/save` hands a seat the seed mid-game — waits on a conversation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e47cd3d400
commit
04ca74c365
@@ -54,8 +54,13 @@ Not items. Things that are true of every change, and that have gone wrong when s
|
||||
- **Commits and tags are GPG-signed and I cannot make them.** Stage the work, write the message to
|
||||
a file, hand over a `!` command. Tags must be `git tag -s` — a bare `git tag` makes a lightweight
|
||||
tag and `git push --follow-tags` skips it *without any error*.
|
||||
- **Deploy from the right line.** `npm run deploy:web` defaults to the same destination on both
|
||||
lines, so deploying from `station-master/` silently replaces the playtesters' build with main's.
|
||||
- **There is one line now.** The 0.4.9 playtest line was retired on 2026-09-29 (Jesse: "no longer
|
||||
being maintained"), so `npm run deploy:web` from this directory is the deploy. The public site
|
||||
serves the last 0.4.9h build until that is run once from `main`.
|
||||
- **The save check is the LAST thing before the tag, not a thing done during the work.** 0.8.2
|
||||
replayed every server save, wrote "three resume, ten refuse" into its notes, and then put a card
|
||||
into the deck — and shipped with zero of thirteen resuming. Replay the server's saves against the
|
||||
exact tree being tagged (`tryResumeSession`, not `fromSave`).
|
||||
- **A failing test written before the fix is the only thing that proves a fix.** Three releases
|
||||
(v0.7.5 through v0.7.8) each reported the same bug fixed, and each fixed something real that was
|
||||
not the reported fault, because every verification read what the SERVER served rather than
|
||||
@@ -97,7 +102,7 @@ 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
|
||||
1. **Play it at a table** — CLOSED 2026-09-29: #39 #35 #42a #40 all confirmed at a table
|
||||
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** — #44 #81 #33 #36
|
||||
@@ -107,6 +112,7 @@ Not items. Things that are true of every change, and that have gone wrong when s
|
||||
8. **The bot** — #104 #105 #106 #41 #57 #59 #54 #58 #55 #56 #60
|
||||
9. **Code health and housekeeping** — #46 #45 #84 #87
|
||||
10. **Documentation and assets** — #15a #86 #88 #111
|
||||
11. **The 2026-09-29 audit — what it found and did not fix** — #112 #113 #114 #115 #116 #117
|
||||
|
||||
Then, at the back: **Reference** (the measurements, rulings and rejected approaches behind the
|
||||
items above) and **Done** (everything closed, kept because several of them are the only record of a
|
||||
@@ -116,6 +122,10 @@ ruling or a lesson).
|
||||
|
||||
## Play it at a table
|
||||
|
||||
**CLOSED 2026-09-29.** Jesse: "The table test was completed." Every item and every checklist line
|
||||
below was met at a table; the section is kept because the measurement in *Preparing the session*
|
||||
(which interruptions fire by themselves and which have to be set up) is the only record of it.
|
||||
|
||||
The largest gap in the project, and none of it is a coding gap. Features are shipped, packed,
|
||||
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.
|
||||
@@ -188,41 +198,41 @@ developer present; what it needs is somebody writing down what they saw.
|
||||
|
||||
**Game A — `days: 1`, two humans, two browsers.** Reaches the extension vote in one Day.
|
||||
|
||||
- [ ] The vote appears **in front of both players**, not underneath the results dialog (#35 — this is
|
||||
- [x] The vote appears **in front of both players**, not underneath the results dialog (#35 — this is
|
||||
the exact shape of the bug v0.7.9 fixed).
|
||||
- [ ] While one player has not voted, the other's turn chart says **who** it is waiting on (#35).
|
||||
- [ ] **Close the second laptop mid-vote.** Does the first player learn why nothing is happening, and
|
||||
- [x] While one player has not voted, the other's turn chart says **who** it is waiting on (#35).
|
||||
- [x] **Close the second laptop mid-vote.** Does the first player learn why nothing is happening, and
|
||||
does "waiting on Carol" still read once Carol is gone? (#35 — never tested.)
|
||||
- [ ] Reopen it. The history panel comes back **populated**, not empty, and the board is current
|
||||
- [x] Reopen it. The history panel comes back **populated**, not empty, and the board is current
|
||||
(the v0.7.9.5 reconnect fix, never seen by a person).
|
||||
- [ ] Vote yes. The extra Day begins and the official result is **unchanged** from when the
|
||||
- [x] Vote yes. The extra Day begins and the official result is **unchanged** from when the
|
||||
timetable ran out (#35).
|
||||
|
||||
**Game B — ordinary length, two humans, bots to fill.** Everything else.
|
||||
|
||||
- [ ] Somebody **builds a Yard Office** and lets a freight train (no coach) arrive at it. The offer
|
||||
- [x] Somebody **builds a Yard Office** and lets a freight train (no coach) arrive at it. The offer
|
||||
interrupts the Mainline Phase and asks a question mid-thought — is it legible, and does it say
|
||||
which train? (#39)
|
||||
- [ ] Somebody **holds the Red Flags card** while their A/D tracks are full, so an arrival would
|
||||
- [x] Somebody **holds the Red Flags card** while their A/D tracks are full, so an arrival would
|
||||
collide. The hold is offered out of phase (#39).
|
||||
- [ ] A **loaded Extra** is made up and run (#39 — the third of its three).
|
||||
- [ ] Watch a bot take a whole turn: does the district follow it, does the lit pile catch the eye,
|
||||
- [x] A **loaded Extra** is made up and run (#39 — the third of its three).
|
||||
- [x] Watch a bot take a whole turn: does the district follow it, does the lit pile catch the eye,
|
||||
does the caption say who and what? (v0.8.0)
|
||||
- [ ] Find the speed that suits you and say what it is — it becomes the committed default.
|
||||
- [ ] Let the board fall behind, then press **Skip**. Nothing is lost; the history has it all.
|
||||
- [ ] End a Day with a collision on it: the summary reads "N on Day D, N in all" and cannot
|
||||
- [x] Find the speed that suits you and say what it is — it becomes the committed default.
|
||||
- [x] Let the board fall behind, then press **Skip**. Nothing is lost; the history has it all.
|
||||
- [x] End a Day with a collision on it: the summary reads "N on Day D, N in all" and cannot
|
||||
contradict itself (v0.8.0.2).
|
||||
|
||||
**Solitaire, five minutes, alone.**
|
||||
|
||||
- [ ] Click through **every field** on the setup screen and confirm the dealt game matches what was
|
||||
- [x] Click through **every field** on the setup screen and confirm the dealt game matches what was
|
||||
chosen (#42a).
|
||||
|
||||
**Whatever else happens.** The two bugs that came out of the 0.7.4-0.7.9 runs were both things
|
||||
nobody set out to test. Write down anything that reads wrong, even where the rule underneath is
|
||||
right — most of this release's defects were legible-but-wrong rather than broken.
|
||||
|
||||
- [ ] **#39** — **None of v0.7.4 has been played by a human.** The Yard Office offer, the Red Flag
|
||||
- [x] **#39** — **CONFIRMED at a table, 2026-09-29.** Originally: **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,
|
||||
packed, and running on `phoenix.local` — and nobody has met any of them at a board. **Two are
|
||||
interruptions that stop the Mainline Phase and put a question in front of somebody
|
||||
@@ -230,7 +240,7 @@ right — most of this release's defects were legible-but-wrong rather than brok
|
||||
happens by itself — 0/10 games. See Preparing the session above for what to set up.** See
|
||||
**Reference · #39**.
|
||||
|
||||
- [ ] **#35** — **Extended play has never been played at a real table.** It was verified over the HTTP
|
||||
- [x] **#35** — **CONFIRMED at a table, 2026-09-29.** Originally: **Extended play has never been played at a real table.** It was verified over the HTTP
|
||||
API, which renders no dialog — and when a human first reached it in a browser it was unusable
|
||||
(fixed in v0.7.9). The multiplayer vote has still never been driven through two browsers: what a
|
||||
second player sees while waiting, and whether "waiting on Carol" reads once Carol has closed her
|
||||
@@ -426,8 +436,9 @@ need RAR or Jesse rather than code.**
|
||||
Gitea#14 closed with those ten listed on the issue so they do not vanish with it. See
|
||||
**Reference · #83**.
|
||||
|
||||
- [ ] **#85** — The 0.4.9 playtest line is behind on a rules ruling, and that was checked rather than
|
||||
assumed. See **Reference · #85**.
|
||||
- [x] **#85** — **MOOT 2026-09-29** — the 0.4.9 playtest line is retired, so it is behind on every ruling
|
||||
since and that no longer matters. Originally: behind on a rules ruling, checked rather than assumed.
|
||||
See **Reference · #85**.
|
||||
|
||||
- [x] **#107** — **May a Small Yard put cars on the NOSE of the engine? YES** — raised by Jesse
|
||||
2026-09-17, discussed the same day and built. Two sources disagreed: the v0.4.5 card text says
|
||||
@@ -580,9 +591,11 @@ non-player advantage — it reads the board, never the deck.
|
||||
Dead code, untrustworthy tests, and things carried but not used. Individually small; the reason they
|
||||
are one section is that each one found the next.
|
||||
|
||||
- [ ] **#46** — 29 unused declarations across 14 files, and the build does not run the flag that finds
|
||||
them. **The flag matters more than the 29** — two are Gitea#18 leftovers in one file, one found
|
||||
by hand and the other missed. Do #48 first; it settles ten of them. See **Reference · #46**.
|
||||
- [x] **#46** — **DONE 2026-09-29 (v0.8.5).** The 36 it had regrown to are gone and
|
||||
`noUnusedLocals` + `noUnusedParameters` are on in `tsconfig.json`, so the list cannot regrow.
|
||||
The ten in `sim/replay.ts` were unused IMPORTS, removed without deciding #48 — that question is
|
||||
untouched. Two dead bot functions (`wouldBuryTheEngine`, `strandedWantedCars`) were rejected
|
||||
candidates left behind; `isLegal` and `restoreRng` had no callers. See **Reference · #46**.
|
||||
|
||||
- [ ] **#84** — Five test fixtures pinned a seed and meant "a game like this". All five broke on
|
||||
Gitea#14 for that reason. See **Reference · #84**.
|
||||
@@ -613,6 +626,85 @@ What the project says about itself, and what it ships alongside the code.
|
||||
|
||||
---
|
||||
|
||||
## The 2026-09-29 audit — what it found and did not fix
|
||||
|
||||
Four reviewers read the engine, the server, the browser client and the sim/tests/hygiene, and every
|
||||
finding was re-verified against the code before anything was acted on. v0.8.3 (engine), v0.8.4
|
||||
(transport) and v0.8.5 (housekeeping) took the faults; these are the findings that were real and
|
||||
were NOT fixed, each with the reason, so nothing quietly evaporates.
|
||||
|
||||
- [ ] **#112** — **The structure proposal.** `docs/plans/structure.md`: a route table with auth
|
||||
wrappers for `http.ts`; five extractions and a `Selection` value for `main.ts`; `check` and
|
||||
`reduce` split per phase; one `carCategory`; one `Push` type. Each names the test it makes
|
||||
possible. Ordered by payoff; the first two are afternoons. Do the `http.ts` table before the
|
||||
next route (#20's display stream).
|
||||
|
||||
- [ ] **#113** — **Server faults left as found.** (a) A second SSE connection from the same seat
|
||||
shadows the first without ending it, and the old socket's close then broadcasts "disconnected"
|
||||
for a seat that is still there — end the old response on replace, and only announce a close
|
||||
when the closing response is the live one. (b) Seat tokens travel in URLs on `/api/intent`,
|
||||
`/api/save`, `/api/session`, and the JOIN SECRET on `/api/lobby/preview?secret=` — every
|
||||
reverse proxy's access log holds them; `lobby-and-sessions.md` §1 says keep them out. Move to a
|
||||
header or the body (EventSource forces the two stream routes). (c) `gameCodes` is not seeded
|
||||
from running games on boot, so a game that survived a restart finishes with `gameCode: ''` in
|
||||
the index and the admin listing, and `freshGameCode` can reissue its code. (d) `/api/lobby/start`
|
||||
mutates memory and tells every lobby watcher the game began BEFORE the writes; a failed write
|
||||
resurrects the lobby on restart with a fresh seed. (e) Secret comparisons are `!==`;
|
||||
`timingSafeEqual` costs nothing. (f) An admin can mint a claim for a lobby seat that `/api/claim`
|
||||
then cannot redeem. (g) `everConnected` is never pruned on delete. (h) `body.config` from the
|
||||
host is never shape-checked — a bad one wedges the lobby at Start with a 500 each time.
|
||||
|
||||
- [ ] **#114** — **Client faults left as found.** (a) Rules refusals and transport failures are
|
||||
invisible: every `void session.submit(...)` discards the `false`, and `lobby.ts`'s `postJson`
|
||||
has no catch, so a host who presses Start while the server restarts sits on "Starting…" until
|
||||
a reload. (b) `lobby.ts` reads `localStorage` bare (four sites) where `main.ts` guards every
|
||||
access — a browser with site storage blocked throws before any button is wired. (c) `claimSeat`
|
||||
awaits with no try: a 502 leaves the lobby doors drawn and dead. (d) `build-web.ts` stamps
|
||||
`sha-dirty` for every dirty build of one commit, so two dirty deploys publish byte-identical
|
||||
module URLs and a returning browser serves stale modules against new HTML. (e) "New game" on
|
||||
the results screen does `location.search = ''`, which the code elsewhere asserts is a no-op
|
||||
when the search is already empty — the save is wiped and the player stays on the finished
|
||||
board; `commitNewGame` has the `reload()` fallback, this button does not. (f) The animation
|
||||
loop outlives the session: `leavegame` does not reset the step queue, so rejoining another game
|
||||
runs the old game's steps against the new session until the first push. (g) Seven independent
|
||||
HTML-escape helpers with differing coverage, none escaping `'`; no test feeds a display name
|
||||
containing `<` or `"`. No XSS was found; the risk is the next helper.
|
||||
|
||||
- [ ] **#115** — **Engine drift left as found.** (a) `maneuver.flyingSwitch` is a weaker copy of
|
||||
`switch.dropCars` — no `switchingRefusal`, no `engineAt` clamp, no `standingWest` handling — latent
|
||||
at 0 copies, wrong the day the card is dealt. (b) Car category is spelled three times
|
||||
(`acceptsCar`, `newTrainPhase`, `isFreight`/`carriesLoad`). (c) `redFlag.play` emits a
|
||||
`phaseEnded` the reducer ignores — a no-op intent offered whenever the Emergency Toolbox is on;
|
||||
either the toolbox or the intent is vestigial. **Needs Jesse.** (d) The hand limit is enforced
|
||||
only on `draw.end`; `switch.end` and `freightAgent.end` let a `sixRandom` hand stay at six all
|
||||
game, and `card.discard` is ungated by option. (e) `freightAgent.unjam` from an inbound box
|
||||
returns the car `pooled()` but loaded — the same "coach that can never unload again" 0.8.1.0
|
||||
fixed for `clearInbound`. (f) `mainlinePhase` iterates a snapshot of trays after `collide`
|
||||
deletes some, so later per-train logic in that loop reads a dead tray; benign today. (g) A
|
||||
`Map`-order dependence in candidate ordering that would not survive deserialising state from
|
||||
JSON with a different key order — worth one comment in `legal.ts`.
|
||||
|
||||
- [ ] **#116** — **Test-suite faults left as found.** (a) `test/card-reference.test.ts` REWRITES
|
||||
`docs/home-deck.md` and `mainline-deck.md` and then compares — when they are stale the test is
|
||||
red AND the diff to inspect is already gone. Generate to a string and compare. (b)
|
||||
`test/track.test.ts` asserts on wall-clock elapsed time (`< 5000 ms`) in the default suite.
|
||||
(c) `test/sim.test.ts` pins 300 games to `seed: 1000 + i*7919` to reach the one where a tank
|
||||
car is first dropped (#84's shape; it is the seven-minute suite that breaks). (d)
|
||||
`test/web.test.ts` slices `src/server/http.ts`'s source text between two constant names. (e)
|
||||
`multiplayer.test.ts` still hard-codes seed 4242 where the playtest line had a seed search.
|
||||
(f) `package.json`'s `test/**/*.test.ts` only works because dash has no globstar and there is
|
||||
exactly one nesting level; spell it `test/*/*.test.ts`.
|
||||
|
||||
- [ ] **#117** — **`/api/save` hands every seat the seed mid-game — NEEDS JESSE.** The seat's own
|
||||
save download returns `seed` while the game is active; in Competitive that is every rival's
|
||||
hand and the deck order, the exact leak `game.ts` strips from the log. But a save without the
|
||||
seed cannot replay, which is the whole point of a save. Jesse (2026-09-29): "We should discuss
|
||||
before making changes on this one." Options on the table: serve the seat's save only once the
|
||||
game is finished; serve it mid-game without the seed (a receipt, not a replay) and with the seed
|
||||
once finished; or accept the leak in Co-op only. See **Reference · #117**.
|
||||
|
||||
---
|
||||
|
||||
## Reference — measurements, rulings and rejected approaches
|
||||
|
||||
The argument behind each open item, kept out of the work sections so those stay scannable. Moved
|
||||
@@ -2054,7 +2146,8 @@ same rule was applied to `content.ts`'s own comments on 2026-08-22 — see the p
|
||||
CHANGELOG for what came out and what was kept.
|
||||
|
||||
**BUILT 2026-09-07 in v0.7.9.2, completed in v0.7.9.3, as the first of the two options** — a build step writing Markdown,
|
||||
`scripts/build-card-reference.ts` → `docs/rules/as-built.md`, with `test/card-reference.test.ts`
|
||||
`scripts/build-card-reference.ts` → `docs/rules/as-built.md` (since 0.8.2 the tables are written
|
||||
into `docs/home-deck.md` and `docs/mainline-deck.md` instead), with `test/card-reference.test.ts`
|
||||
re-running the generator and failing when the checked-in file disagrees. All six sections are
|
||||
there, and so is the honesty column: every Enhancement carries its `live` / `dormantSolo` /
|
||||
`unbuilt` status, and the opponent-directed cards say plainly that none of them is dealt.
|
||||
@@ -2108,8 +2201,9 @@ deliberately did NOT rewrite them, on the grounds that changing a Laborer count
|
||||
decision rather than a documentation one. **That reasoning still stands and nothing was changed in
|
||||
the engine.**
|
||||
|
||||
What changed is that `card-reference.md` is no longer where anyone looks. `docs/rules/as-built.md`
|
||||
is generated from `content.ts` and carries the industry table the game actually runs; the old file
|
||||
What changed is that `card-reference.md` is no longer where anyone looks. The card tables (in
|
||||
`docs/home-deck.md` and `docs/mainline-deck.md` since 0.8.2; `docs/rules/as-built.md` before) are
|
||||
generated from `content.ts` and carries the industry table the game actually runs; the old file
|
||||
keeps its SUPERSEDED banner, now pointing forward, and its numbers are read as what the v0.4.5
|
||||
placeholder said. **The balance question the entry was really guarding is #70** (the rolling stock
|
||||
supply and the uniform 1/1/1 industry model, both marked provisional in `content.ts`) — that is
|
||||
@@ -2191,6 +2285,20 @@ distinction that matters:
|
||||
is the editorial half that generation was never going to do.
|
||||
|
||||
|
||||
### The 2026-09-29 audit
|
||||
|
||||
#### #117 — `/api/save` and the seed
|
||||
|
||||
Found by the server reviewer, verified: `http.ts`'s `/api/save` returns `session.exportSave()` —
|
||||
seed, config, names, full history — to any valid seat token while `status === 'active'`. The comment
|
||||
above the route says "every one of those moves is already on this player's screen", which is true
|
||||
of the moves and false of the seed. `game.ts` strips the seed from the shared log for exactly this
|
||||
reason ("handed each of them the whole future of the deal"), `test/redaction.test.ts` pins that a
|
||||
Frame never carries it, and `/api/lobby/preview` refuses to send it. Jesse's question, which is the
|
||||
right one: a save without the seed is not a save. The finished-game-only answer keeps the download
|
||||
button honest (it works; it just waits for the end); the receipt answer keeps a mid-game download
|
||||
possible but replays nothing; the Co-op answer accepts that a co-operative table has nothing to hide.
|
||||
|
||||
## Done
|
||||
|
||||
Closed items, kept because several are the only record of a ruling or a lesson. Newest first within
|
||||
@@ -2501,7 +2609,8 @@ the numbers stay so cross-references above and below still resolve.
|
||||
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
|
||||
`as-built.md` (folded into `docs/home-deck.md` and `docs/mainline-deck.md` in 0.8.2) 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
|
||||
|
||||
Reference in New Issue
Block a user