Credibility pass: the accretion sanded smooth

Blind reviews over d2b40ec..HEAD (engine / web / server+deploy). Orphaned
doc comments rejoined their functions; the swap-meet tradables collapsed
from four pasted deriveds to one (killing the "the's treasure" label);
FEAR's aura and banner now share the engine's own dreadDistance; the
long-press peek got one home; rulebook.ts stopped wearing JSON quotes;
process-named tests and war-story comments now state the constraints they
pin; the protocol doc caught up to its handlers; verify-ledgers.sh can
actually count failures; the runbook learned about the determinism gate
and the nightly backups. No behavior changes — 255 tests and all 48
production ledgers replay identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-21 10:58:08 -04:00
co-authored by Claude Fable 5
parent 3445d12206
commit 6adcbe23b5
17 changed files with 209 additions and 204 deletions
+9 -5
View File
@@ -9,6 +9,8 @@
// {type:"claimTransfer", code} claim a seat on a new device
// {type:"catchUp", sinceSeq} replay of moves missed while away
// {type:"chat", text} table talk to the room
// {type:"rollDie"} the tabletop D4, published as talk
// {type:"addBot", style?, tier?} host seats an automaton
// {type:"watch", roomId} join the Peanut Gallery: nameless, read-only
// {type:"leave"} detach this socket from table or gallery
// {type:"myGames", seats} summaries for held seats
@@ -17,8 +19,8 @@
// server -> client:
// {type:"welcome"} on connect
// {type:"seat", playerId, token} your seat secret — keep it
// {type:"room", roomId, players, hostId, started, colors}
// {type:"events", events} redacted for this recipient
// {type:"room", roomId, players, hostId, started, audience, colors, bots}
// {type:"events", events, replayed?} redacted for this recipient
// {type:"state", view, seq} redacted full view (after every change)
// {type:"chat", player, text, at} one line of table talk
// {type:"watching", roomId} you are seated in the gallery
@@ -81,7 +83,7 @@ loadPersistedRooms();
// current actor is an automaton waits forever for a human to poke it.
setTimeout(() => {
for (const room of runningRooms()) runBots(room);
}, 2000);
}, 2000); // a beat for clients to reconnect before the clockwork stirs
// One process serves both the built client and the websocket, so production
// needs only a TLS proxy in front (or nothing, on a LAN).
@@ -150,7 +152,8 @@ interface Session {
socket: WebSocket;
playerId: PlayerId | null;
roomId: string | null;
/** In the Peanut Gallery: nameless, read-only, counted but never listed. */
/** In the Peanut Gallery: spectator implies playerId === null, so every
* handler that requires a seat refuses this session by construction. */
spectator: boolean;
/** The raw seat token this connection authenticated with (memory only). */
token: string | null;
@@ -380,7 +383,7 @@ wss.on("connection", (socket) => {
case "watch": {
// The Peanut Gallery: no name, no seat, no ledger line — a pure
// reader of the public broadcast, counted but never identified.
const roomId = String(msg.roomId ?? "").trim().toUpperCase().slice(0, 8);
const roomId = String(msg.roomId ?? "").trim().slice(0, 8);
if (!roomId) return send(socket, { type: "error", message: "roomId required" });
const room = getRoom(roomId);
if (!room) return send(socket, { type: "error", message: "no such room" });
@@ -444,6 +447,7 @@ wss.on("connection", (socket) => {
if ("error" in result) return send(socket, { type: "error", message: result.error });
broadcast(room, (playerId) => ({ type: "events", events: redactFor(result.events, playerId) }));
broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length }));
// The game's end unmasks the mystery machines in the roster.
if (room.state?.phase === "finished") broadcast(room, () => roomInfo(room));
botRemark(room, session.playerId, result.events as { type: string }[]);
runBots(room);