Public: anyone can read this; the crew can change it.
What you may do, and what it would pay you: `may`, `canStart` and the standing offer's worth, in the state itself #86
Check · Review · Land · Live done — Landed and live in production.
`tableState` now answers RULES.md §5 and §6 once, for the viewer's own seat, so no door has to re-derive a rule to draw a playable turn.
shipped production is serving it deploy 6ecafc0
tableState now answers §5 and §6 once, for the viewer's own seat, so no door has to re-derive a rule to draw a playable turn:
may—src/round.ts'sMayActcarried through in seat ids: may I offer, accept, pass, the refusal sentence for each, andcite, the aims §6 would let me swear on the standing offer, in charter order. It is the viewer's own seat and nobody else's: null for a guest, a watcher and a player at another table, and null before the table is dealt.your_payoff— what the standing accord would pay this seat if it bound,scoreinsrc/board.tsover the viewer's own charge and a public accord.canStart(who, table)besidecanJoinandcanLeave, agreeing exactly withstartTable's refusals: host or admin, open, four to six seated.
Nothing is stored and no rule is written twice. chargeOf is the one place a seat's stored charge becomes the charge the rules read, under the seat's own ordinal; dealtIn is the one place a read asks which seat is the viewer's, so mayAct is never handed another seat's charge.
main moved under this branch (#82, the ledger's write) and is merged in. Both sides had rewritten taking, and the resolution keeps each side's idea where it belongs: taking has #82's shape — the door's checks, then judged, asked again inside written's immediate transaction — and judged reads the charge through chargeOf, so the single place a stored charge becomes a rules charge now sits on both sides of the write. The README keeps both paragraphs, what a read answers and why a write re-decides; both sides' tests are kept whole and neither was edited to pass. bun test is 138 pass, 0 fail, including #82's eight-process race.
To try it in the preview: open a table, seat four, start it, and watch the offer/accept/pass buttons and the payoff follow the standing offer — then open the same table as a guest or another player and see may and your_payoff come back null.
🤖 Generated with Claude Code
tableStatenow answers RULES.md §5 and §6 once, for the viewer's own seat, so no door has to re-derive a rule to draw a playable turn.mayonTableViewissrc/round.ts'sMayActcarried through in the vocabulary a door speaks (seat ids, not ordinals): may I offer, may I accept, may I pass, the one refusal sentence for each, andcite— the aims §6 would let me swear on the standing offer, in charter order. It is the viewer's own seat and nobody else's:nullfor a guest, a stranger, an admin with no seat, andnullbefore the table is dealt, when there is no turn to take.your_payoffis what the standing accord would pay this seat if it bound —scoreinsrc/board.ts, over the viewer's own charge and a public accord, so it is arithmetic a player is entitled to and nobody else's sum.canStart(who, table)is exported besidecanJoinandcanLeaveand agrees exactly withstartTable's refusals: host or admin,open, betweenSEATS.minandSEATS.maxseated.Nothing new is stored and no rule is written twice:
mayismayActanswering over actsviewalready read,your_payoffisscore, and no extra query is run. Two small helpers keep the seams honest —chargeOfis the one place a storedcharge_jsonbecomes the charge the rules read (under the seat row's own ordinal, so a charge is always its holder's) andtaking()now uses it instead of its own copy;dealtInis the one place a read asks which seat is the viewer's, somayActcan never be handed another seat's charge and throw. The live payload is untouched:mayis per-viewer and the bus still carries addresses only.Worth a look: the redaction test in
src/actions.test.ts("mayis the viewer's own seat's, andcitenobody else's aims"), which asserts a guest and another player seemay: null,your_payoff: nulland no charge but their own, and that every aim in a player'sciteis an aim of their own charge —citecarries weights, so a stray aim would be a leaked charge. The §10 arithmetic is checked twice over in "your_payoffis what the standing accord would pay this seat": by hand (A₁ pays 9/0/4/7, A₂ pays 8/6/4/8, matching RULES.md §10's own numbers) and againstscore(). In the preview,GET /api/tables/:idas a seated player now carries both fields; as a guest both are null.I did not draw any button from these: the page, the terminal and the MCP tools are unchanged, since #33 is the issue that draws the start button and the turn controls.
bun testis green (136 tests), the app boots and/answers in ~8 ms.🤖 Generated with Claude Code
Closes #74