4 Commits
Author SHA1 Message Date
Jesse.MarkowitzandClaude Opus 5 c10f52791e v0.8.0.2 — the speed control that was only ever a URL parameter, and a Day-end
contradiction

Two things found by playing v0.8.0.1, neither in the mechanism itself.

?pace= never worked. index.html's doors are play.html?lobby and
play.html?solitaire, so arriving through the splash replaces the query string and
the play page only ever saw ?lobby — a whole game was played at 1x while believing
it was at 7x. v0.8.0 shipped that parameter as the only way to change speed and the
game's own front door destroyed it. There is a control on the play screen now,
beside zoom, persisted per viewer; the doors carry pace through as well, so the URL
lever is honest for handing two playtesters different speeds. PACE_LEVELS moved to
sim/pacing.ts with DWELL and MAX_PACE — the tuning surface in one file, and
testable. The committed default is unchanged: what it should be is a question for a
game played at a speed that took effect.

And the Day-end dialog said "0 today, 2 in all". advance.ts increments the Day and
then zeroes collisionsToday, and noteDayEnd() fires when the Day goes up — so the
dialog reporting the Day that just finished was drawn from the very frame in which
that Day's count was reset. Reproduced on four of five seeds before changing
anything. The count is captured at the rollover now; it is not derivable on the
client, because in multiplayer the push announcing the new Day is the same push
that carries the reset. And "today" was the wrong word regardless: it names the Day
instead — "Collisions: 2 on Day 1, 2 in all".

Unrelated to v0.8.0 — that one has been wrong since the dialog was built for
Gitea#10, and needed somebody to play a Day with a collision in it and then read
the summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6
2026-09-09 20:08:39 -04:00
Jesse.MarkowitzandClaude Opus 5 02289e94b8 v0.8.0 — the board replays what everyone else did, instead of arriving rearranged
TODO #13, #15 and #18 — Gitea#20 steps 2-4 pointed at a seated player's own screen.
Every accepted intent, and every automatic phase that does anything, becomes an
ordered presentation step. A bot's whole switching turn used to land in one push;
now it arrives as a run of steps, the district panel follows whoever is acting,
and a [N behind] … [Skip] row says how far the board is from the game.

Solitaire runs the same path — one collector inside submit(), which both session
kinds already funnel through — which is where its automatic phases finally get a
visible beat.

Dwell is assigned by kind: switching holds the screen, turn bookkeeping costs
nothing, and the clock turning over earns the beat. Tunable per viewer without a
rebuild, and off entirely at pace 0.

Also: switching was the one class of action logging unattributed, and now names
its train. Reasoning, measurements and the three things that turned out wrong are
in CHANGELOG.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6
2026-09-09 15:31:46 -04:00
Jesse.MarkowitzandClaude Opus 5 312e0301e0 v0.7.9.8 — the test command did not typecheck, and the plan had gone stale
Housekeeping before v0.8.0: the answer to "anything else that should be
looked at first". One real hole, one stale document, and my own leavings.

#102 — `npm test` passed green on a type error. `pretest` ran
`scripts/build-web.ts`, which invokes `tsc --ignoreConfig` against three
web entry points, so it saw only what those three transitively import and
under a WEAKER configuration than tsconfig.json — no
`noUncheckedIndexedAccess`, no `exactOptionalPropertyTypes`,
`--types ''`. It never saw `src/server/` or a single file under `test/`.

Demonstrated rather than argued: a planted
`const DELIBERATE_TYPE_ERROR: number = 'not a number';` in
src/server/session.ts gives `npm run typecheck` a TS2322 and `npm test` a
clean `# fail 0`. `pretest` is `tsc --noEmit && node
scripts/build-web.ts` now, and the same error exits 1 with the tests
never running.

This mattered THIS week rather than generally: v0.8.0 is steps 2-7 of the
common board — display stream, credentials, persistence, Chromium
supervisor — which is almost entirely src/server/, exactly the half the
test command could not see.

#103 — the plan had drifted from the code it is the source for.
docs/plans/jitsi-common-board.md was written 2026-08-27, still said "No
implementation has been performed", and is what steps 2-7 get built from.
Step 1 shipped across four releases since, so every "current code
finding" under it described a fault that is now fixed — a document
reading as present tense and nine days stale sends the next reader to fix
things twice.

Measured: its PublicFrame sketch lists four properties never built
(protocolVersion, config, scoring, deckCounts) and omits 28 that exist,
and the shape is the real difference — the implementation is FLAT where
the plan grouped things into objects, so a renderer written from the
sketch would not compile. The plan now says so at the top and at step 1,
names src/sim/view.ts and the redaction allow-list as the authority,
keeps the original sketch for its reasoning, and calls out
`protocolVersion` as unbuilt rather than dropping it quietly — step 2 is
the reconnecting display stream and is the first thing that would want
one.

One step-1 item is STRUCK OFF rather than built: "add the Red Flag holder
to the public player projection". The premise does not hold here.
`decks.redFlags` is written once, in setup.ts, from
`optionalRules.emergencyToolbox`, and never again — `redFlag.play` emits
`phaseEnded` and does not spend it — so every player holds one or none
does, decided before the deal. A per-player `redFlagHeld` would be one
already-public option copied N times, while telling every reader of the
common board that it varies by player and might change mid-game. Worse
than the absence. Pinned by test so it is not re-raised from the plan.

Four dead imports removed, all mine: `HAND_LIMIT` left unused in
apply.ts, view.ts and web/game.ts when 0.7.9.6 consolidated the three
copies of the §6.2 test, and `actingPlayer` in web/game.ts, dead since
0.7.9.5 made `currentActor` delegate. Finding them re-measured #46:
`tsc --noUnusedLocals` now reports 40, up from 29 on 2026-08-30. That
entry's "without the flag this list simply regrows" is a measurement
rather than a forecast now. The other 36 and the flag stay open.

946 tests pass, up from 943. No behaviour changes: three new tests pin an
invariant, and the rest is a build command, dead imports and a document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R
2026-09-08 03:41:39 -04:00
Jesse.MarkowitzandClaude Opus 5 d5445badcc v0.7.9.5 — two answers to one question, and the copy nobody read
Both faults are in what 0.7.9.4 had just built, and both are the same
shape: a second copy of an answer that agreed with the first until it
didn't.

#96 — the §3.3 vote has no actor, and the screen named one anyway. The
vote is PARALLEL: every un-voted seat may vote at any moment, in any
order, one refusal ends it, and `apply.ts` says where it accepts one that
there is no actor to be. The turn chart named the last seat to move
before the timetable ran out — no more claim on the vote than anybody
else — directly above a tally correctly showing three seats outstanding.

The cause is worth more than the symptom. `currentActor(game)`
(`web/game.ts`) guarded on `status !== 'active'`; `currentActorOfState`
(`sim/view.ts`), added the same day in #95 and the one the frame calls,
did not, so it handed back whatever `clock.currentActor` was left
holding. The view now carries the guard and `currentActor` delegates to
it. That matters more than the tidiness: `currentActor` is what REFUSES
an intent, so a screen answering differently tells the table to wait on a
player the server would turn away.

The fourth of this class after Gitea#21, #22 and #94 — but the first
found by asking a view helper its question in a state the game is not
`active` in, which is the generalisation and is cheaper than finding the
fifth the same way.

#97 — narration reaches a seat once, by one path. `Frame.lines` carried
the whole log on every push to every seat, and nothing read it:
`RemoteSession` accumulates from `push.lines` alone and its `lines()`
returns that accumulator, so the log was serialised into every frame,
grew all game, and was discarded on arrival while `linesSince` sent the
same text correctly beside it.

The duplicate was masking a bug rather than merely wasting bandwidth.
`connect()` cleared `lastFrame` but not `sentLines`, so a reconnecting
seat was told "nothing new since your last push" while the browser it
answered had just reloaded from an EMPTY accumulator — the history panel
came back blank, mid-game, with the server holding the whole log. So the
two halves are one change, and the plan's instruction taken alone ("stop
passing the full game log into `frameFor()`") would have deleted a real
behaviour rather than a duplicate.

Every remaining reader of `Frame.lines` was checked before the field was
emptied: all of them are the solitaire and replay path, which builds
Frames through `snapshot()` directly and never goes near a session.

One test was wrong before the code was. The first draft of the reconnect
test connected inside its own fixture, so both sides of the comparison
were the empty array and it passed against the broken server. Each test
now asserts its premise is non-empty before comparing.

Also: `docs/plans/jitsi-common-board.md` is committed. It was never added
— not ignored, just missed — while TODO.md cites it twice as the plan for
all of v0.8.0 and the last two releases were built from it, so a clone
got a TODO pointing at a file that did not exist.

917 tests pass, up from 909.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R
2026-09-07 20:35:19 -04:00