Intent. Twice now the pure half has gone first and the wiring has been easy — deal/score in #6, validAccord/satisfied/citable in #26 — so do the same for the rest of the game: one module that reads a table's public facts as plain data and says whose turn it is, what stands, who has signed, whether it binds, and what the settlement is. Then #8, #9 and #10 are a schema, a transaction and a boundary rather than a rules engine invented inside an action.
Done looks like
[ ] src/round.ts (new), pure, no bun:sqlite, no Date.now(), no Math.random(). Its input is plain data: the charter (Clause[] from src/board.ts), the seats in order, the acts newest last ({ seat, kind: "offer" | "accept" | "pass", accord?, cite?, at }), and now. Declare that act shape here — #8 stores rows that match it rather than the other way round.
[ ] whoseTurn(round): the seat to act, from seat order and the act count, and the rule for a seat that has left or a settled table. Say in the pull why the turn is derived rather than stored.
[ ] standing(round): the latest offer and the acceptances that came after it, in order — a new offer clears them, and an acceptance before any offer counts for nothing.
[ ] quorum(seats) = ⌈n/2⌉+1 (§2), and binds(round): whether the standing offer has reached it, and on which act.
[ ] settlement(round, charges): the binding accord, every seat's score from score() whether they signed or not, the ranking with ties sharing a rank and no tie-break, and each said claim's verdict with §7's none | small | large | dread mapping. The impound case is the same function with no binding accord: every score zero.
[ ] mayAct(round, seat) answers the one question every door asks before it draws a button: may this seat offer, accept (using citable from board.ts), or pass — and when not, the sentence to show.
[ ] src/round.test.ts replays §10's nine acts end to end: the standing offer after each act, the turn after each act, the third acceptance being the one that binds, the scores 8/6/4/8, the ranking, and the said claim that is false at the reveal. Plus an impound, an acceptance before any offer, a second offer clearing acceptances, and a seat accepting its own offer.
[ ] Nothing here writes, reads a database or touches a door. bun test stays green and the preview is unchanged.
Filessrc/round.ts (new), src/round.test.ts (new), README.md and AGENTS.md (one line each, as #26 did for the sworn arithmetic).
Risks The act shape declared here is the one #8 stores; get it wrong and the ledger and the rules disagree on the first migration. Keep it the smallest shape the rules need — a seat, a kind, an accord, a citation, a timestamp — and leave ids, authorship and storage to #8. src/board.ts is merged and free; this is a new file, so it runs beside every open pull.
Brief that fits @vesper — the rules engine of a game, as small typed modules with tests. (A person assigns.)
Intent. Twice now the pure half has gone first and the wiring has been easy —
deal/scorein #6,validAccord/satisfied/citablein #26 — so do the same for the rest of the game: one module that reads a table's public facts as plain data and says whose turn it is, what stands, who has signed, whether it binds, and what the settlement is. Then #8, #9 and #10 are a schema, a transaction and a boundary rather than a rules engine invented inside an action.Done looks like
src/round.ts(new), pure, nobun:sqlite, noDate.now(), noMath.random(). Its input is plain data: the charter (Clause[]fromsrc/board.ts), the seats in order, the acts newest last ({ seat, kind: "offer" | "accept" | "pass", accord?, cite?, at }), andnow. Declare that act shape here — #8 stores rows that match it rather than the other way round.whoseTurn(round): the seat to act, from seat order and the act count, and the rule for a seat that has left or a settled table. Say in the pull why the turn is derived rather than stored.standing(round): the latest offer and the acceptances that came after it, in order — a new offer clears them, and an acceptance before any offer counts for nothing.quorum(seats)= ⌈n/2⌉+1 (§2), andbinds(round): whether the standing offer has reached it, and on which act.settlement(round, charges): the binding accord, every seat's score fromscore()whether they signed or not, the ranking with ties sharing a rank and no tie-break, and each said claim's verdict with §7'snone | small | large | dreadmapping. The impound case is the same function with no binding accord: every score zero.mayAct(round, seat)answers the one question every door asks before it draws a button: may this seat offer, accept (usingcitablefromboard.ts), or pass — and when not, the sentence to show.src/round.test.tsreplays §10's nine acts end to end: the standing offer after each act, the turn after each act, the third acceptance being the one that binds, the scores 8/6/4/8, the ranking, and the said claim that is false at the reveal. Plus an impound, an acceptance before any offer, a second offer clearing acceptances, and a seat accepting its own offer.bun teststays green and the preview is unchanged.Files
src/round.ts(new),src/round.test.ts(new),README.mdandAGENTS.md(one line each, as #26 did for the sworn arithmetic).Risks The act shape declared here is the one #8 stores; get it wrong and the ledger and the rules disagree on the first migration. Keep it the smallest shape the rules need — a seat, a kind, an accord, a citation, a timestamp — and leave ids, authorship and storage to #8.
src/board.tsis merged and free; this is a new file, so it runs beside every open pull.Brief that fits @vesper — the rules engine of a game, as small typed modules with tests. (A person assigns.)