From 5816d334ad370645da98871400dd2cc7f8a17fdc Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 6 Aug 2026 15:32:09 +0200 Subject: [PATCH 1/2] fix: the note form carried no session token, so every note was refused Tier S (a fix inside a boundary; chaos d8=5, no override). Reported by the maintainer: "I can't save notes, I get 'refused: no session token'." The form posted to a bare `/note`. Control 1 requires the token on EVERY request, so the guard refused all of them. WHY THE TESTS MISSED IT IS THE PART WORTH RECORDING. I verified the note channel over real HTTP and got 303 -- but I appended the token to the URL by hand. I tested the ENDPOINT and not the PATH A PLAYER TAKES, so the one thing standing between the feature and the user was the one thing not exercised. Same family as timing the wrong span and counting the wrong denominator: a correct measurement of the wrong subject. Fixed with Guard::note_endpoint(), so the form's action carries the token like every other request. The assertion now pins the token's PRESENCE rather than the bare path, so reverting the fix turns it red. Verified the way it should have been done first: read the form's `action` out of the SERVED page and POST to exactly that, nothing added by hand. 303. Clippy then flagged document_with_log at 8 arguments. It was right -- the signature had grown across three passes -- so the two endpoints are now one `Endpoints` struct rather than an #[allow]. They are one concept: the guarded surface this page may talk to, one channel that becomes commands and one that provably cannot. Co-Authored-By: Claude Opus 5 --- crates/cb-render-html/src/doc.rs | 39 +++++++++++++++++++++++++----- crates/cb-render-html/src/lib.rs | 24 ++++++++++++------ crates/cb-render-html/src/serve.rs | 11 +++++++++ tools/cb-play/src/hotseat.rs | 5 +++- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index 7995e81..cb3f9ca 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -698,19 +698,44 @@ pub fn document( seat: Option, may_pass: bool, ) -> String { - document_with_log(view, legal, endpoint, seat, may_pass, &[], &[]) + document_with_log( + view, + legal, + Endpoints { + command: endpoint, + note: "/note", + }, + seat, + may_pass, + &[], + &[], + ) } /// The table, plus the game log (CB-WP-0018 T02). +/// Where the page posts, both channels (ADR-0014 D1). +/// +/// One struct rather than two `&str` parameters because they are one +/// concept — the guarded surface this page may talk to — and because +/// clippy was right that the signature had grown across three passes. +#[derive(Debug, Clone, Copy)] +pub struct Endpoints<'a> { + /// Pointer facts. `resolve` turns these into commands. + pub command: &'a str, + /// Free text. Nothing turns these into commands. + pub note: &'a str, +} + pub fn document_with_log( view: &GroundView, legal: &[games_ground::GroundCommand], - endpoint: &str, + to: Endpoints<'_>, seat: Option, may_pass: bool, log: &[LogLine], meta: &[String], ) -> String { + let (endpoint, note_to) = (to.command, to.note); let mut s = String::with_capacity(8192); let _ = write!( s, @@ -743,7 +768,7 @@ pub fn document_with_log( body(&mut s, view); move_section(&mut s, legal, seat, may_pass); s.push_str("
"); - meta_section(&mut s, meta); + meta_section(&mut s, meta, note_to); log_section(&mut s, log); s.push_str("
"); let _ = write!( @@ -761,7 +786,7 @@ pub fn document_with_log( /// /// Empty is a legitimate state — a first game has no tally and may have no /// notes — and renders as nothing rather than as an empty heading. -fn meta_section(s: &mut String, meta: &[String]) { +fn meta_section(s: &mut String, meta: &[String], note_to: &str) { // CB-WP-0027 T03: the comment box. Always present — the panel's // purpose is that a player can say something at any moment, and a box // that appears only sometimes trains them not to look for it. @@ -770,12 +795,14 @@ fn meta_section(s: &mut String, meta: &[String]) { // The command channel needs JavaScript because a drag is not a form // submission; a comment is, and making it depend on the script would // add a failure mode for no gain. - s.push_str( + let _ = write!( + s, "

what are you thinking?

\ -
\ + \ \
", + note_to = esc(note_to), ); if meta.is_empty() { return; diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs index a460ab4..2885d15 100644 --- a/crates/cb-render-html/src/lib.rs +++ b/crates/cb-render-html/src/lib.rs @@ -47,7 +47,14 @@ pub mod input; pub mod jsrun; pub mod serve; -pub use doc::{document, text_of}; +pub use doc::{document, text_of, Endpoints}; + +/// The endpoint pair every test in this crate posts to. +#[cfg(test)] +const TEST_ENDPOINTS: Endpoints<'static> = Endpoints { + command: "/command?t=x", + note: "/note?t=x", +}; pub use input::{resolve, Note, PointerFact}; pub use serve::{Guard, Refusal, Request}; @@ -186,7 +193,7 @@ mod coverage { text_of(&document( view, &[], - "/command?t=x", + crate::TEST_ENDPOINTS.command, Some(PlayerId(0)), false, )) @@ -595,7 +602,7 @@ mod gamelog { document_with_log( &crate::testfix::view(Some(PlayerId(0))), &[], - "/command?t=x", + crate::TEST_ENDPOINTS, Some(PlayerId(0)), false, log, @@ -779,7 +786,7 @@ mod notes { let html = document_with_log( &crate::testfix::view(Some(PlayerId(0))), &[], - "/command?t=x", + crate::TEST_ENDPOINTS, Some(PlayerId(0)), false, &[], @@ -807,13 +814,16 @@ mod notes { let html = document_with_log( &crate::testfix::view(Some(PlayerId(0))), &[], - "/command?t=x", + crate::TEST_ENDPOINTS, Some(PlayerId(0)), false, &[], &[], ); - assert!(html.contains("action=\"/note\""), "no comment box"); + assert!( + html.contains("action=\"/note?t=x\""), + "the form must carry the session token" + ); assert!( html.contains("method=\"post\""), "a plain form, so it works with the script disabled" @@ -832,7 +842,7 @@ mod two_columns { document_with_log( &crate::testfix::view(Some(PlayerId(0))), &[], - "/command?t=x", + crate::TEST_ENDPOINTS, Some(PlayerId(0)), false, &[], diff --git a/crates/cb-render-html/src/serve.rs b/crates/cb-render-html/src/serve.rs index dfe464d..e4f3169 100644 --- a/crates/cb-render-html/src/serve.rs +++ b/crates/cb-render-html/src/serve.rs @@ -158,6 +158,17 @@ impl Guard { format!("/command?t={}", self.token) } + /// The note channel's endpoint, token and all (CB-WP-0027). + /// + /// **A form's `action` must carry the token like every other + /// request.** The first version posted to a bare `/note` and was + /// refused with "no session token" — the endpoint had been tested + /// with a token appended by hand, so the test exercised the mechanism + /// and not the path a player takes. + pub fn note_endpoint(&self) -> String { + format!("/note?t={}", self.token) + } + pub fn page_url(&self) -> String { format!("{}/?t={}", self.origin, self.token) } diff --git a/tools/cb-play/src/hotseat.rs b/tools/cb-play/src/hotseat.rs index d853c3f..3513935 100644 --- a/tools/cb-play/src/hotseat.rs +++ b/tools/cb-play/src/hotseat.rs @@ -243,7 +243,10 @@ impl Server { let page = cb_render_html::doc::document_with_log( &view, legal, - &self.guard.endpoint(), + cb_render_html::Endpoints { + command: &self.guard.endpoint(), + note: &self.guard.note_endpoint(), + }, Some(seat), may_pass, &self.log_lines(), From 02402c36b2e85bcab6588225ec963461ea82e79b Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 6 Aug 2026 15:34:58 +0200 Subject: [PATCH 2/2] CB-WP-0028 declared, and two findings from play registered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine observations from the maintainer's session. Two are about the game and go to the register; seven are about the engine and are this workplan. F18 IS THE ONE THAT REFRAMES THE PASS. "I don't understand the GROUND card" reads as a design problem. It is not: Actions.csv carries that card's own tagline -- "Regulate. Restore the frame. Decide." -- and its full rules text, and clay-borg never imported it. We vendored ONE OF NINETEEN edition files. Everything else the engine knows is a hand-transcription into GroundRules.md's 59 numbered rules, which is enough to PLAY the game and gives a player nothing to READ. The page shows `Clarify` where the card says "Ask What Happened -- Invite a concrete account before judging." Registered as `inert`: the data exists and cannot fire, because nothing reads it. Found by a player saying he did not understand something. Rule coverage is 59/59 and has been for weeks. F17: no incentive to ATTACK while holding useful Solutions. Registered as a NOTE, not a finding -- no artifact demonstrates it, and under GameDesign §3.1 it may not go to ground-game until one exists. One is cheap (count ATTACK selections across the policy panel against hand quality). Owner is ground-game if it survives, since it would be a design finding. The workplan (M, chaos d8=1, no override, declaration 11 of window 2) carries the seven engine observations. Two tasks are deliberately shaped against past mistakes: T01 must decide whether ADR-0011's hand-rolled CSV reader survives Solutions.csv, whose microcopy and rules_text are prose with commas and quotes -- ADR-0011 named exactly that as its revisit condition, so if the reader cannot parse them the dependency argument gets re-run rather than a fragile parser written. T04 must first establish whether "click the deck to draw" is a legal move at all. GR-A01 draws as part of INVESTIGATE; the deck is not a thing a player may take from. If it is not legal, that is a FINDING for ground-game -- the maintainer expected an interaction the rules do not offer -- and not a feature. CB-WP-0023 exists because SOLVE was offered where it could not act. Co-Authored-By: Claude Opus 5 --- specs/FindingRegister.md | 20 ++ workplans/CB-WP-0028-the-table-you-sit-at.md | 277 +++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 workplans/CB-WP-0028-the-table-you-sit-at.md diff --git a/specs/FindingRegister.md b/specs/FindingRegister.md index 1490bbf..2b84b41 100644 --- a/specs/FindingRegister.md +++ b/specs/FindingRegister.md @@ -44,6 +44,8 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by | F14 | unplayed | note | — | — | 2026-08-01 | clay-borg | | F15 | underdetermined | note | — | — | 2026-08-05 | clay-borg | | F16 | inconsistent | withdrawn | games/ground/examples/difficulty.rs | counterexample | 2026-08-05 | clay-borg | +| F17 | degenerate | note | — | — | 2026-08-06 | ground-game | +| F18 | inert | raised | — | — | 2026-08-06 | clay-borg | @@ -73,6 +75,24 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by since GROUND-WP-0005 is blocked on exactly this number. The withdrawal was reported (ADR-0012 D5). Its reproduction is `difficulty.rs`, whose policy panel is plural *because of this finding*. +- **F17 — no incentive to ATTACK while holding useful Solutions.** + Reported from play, 2026-08-06: *"there is no incentive to play attacks + as long as I have positive cards."* If true, ATTACK is a dead branch for + most of a game, which would make GR-A06/A07/A08 and the whole Rivalry + half of the relation system reachable only when a player is out of good + options. **`note`, not a finding**: no artifact demonstrates it yet. One + is cheap — count ATTACK selections across the policy panel and compare + against hand quality — and until it exists this may not go to + `ground-game` (GameDesign §3.1). **Owner is ground-game if it survives**; + it would be a design finding, not an engine one. +- **F18 — the engine imports one of nineteen edition files, so the cards + cannot say what they do.** Reported as *"I don't understand the GROUND + card"* — which is not a design gap: `Actions.csv` carries that card's + tagline (*"Regulate. Restore the frame. Decide."*) and full rules text, + and clay-borg never imported it. Every other card is the same: a player + sees `Clarify` where the card reads *"Ask What Happened — Invite a + concrete account before judging."* **`inert`**: the data exists and + cannot fire, because nothing reads it. **Ours, and CB-WP-0028 fixes it.** - **F15 — the rules define one game, not a series.** `OutcomeView` gives `personal` (per seat), `group_success` (per table) and `winners`. Summing the first and counting the third answer different questions, and GROUND diff --git a/workplans/CB-WP-0028-the-table-you-sit-at.md b/workplans/CB-WP-0028-the-table-you-sit-at.md new file mode 100644 index 0000000..d7d1649 --- /dev/null +++ b/workplans/CB-WP-0028-the-table-you-sit-at.md @@ -0,0 +1,277 @@ +--- +id: CB-WP-0028 +kind: product +title: "The table you sit at: the cards' own words, an overhead view, and a game you solve rather than survive" +status: ready +--- + +# Purpose + +``` +structural tier M (imports more of the authoritative edition, which is + an external dataset under AM-4's budgets, and touches + the rendering the coverage gate is written against) +chaos d8 = 1 → no override +declared tier M +``` + +Declaration 11 of chaos window 2. Tier M: survey and decision merged; +adversarial review optional. + +## Nine observations from play + +The maintainer played with the new meta panel and reported nine things. +**Two are findings about the game, seven are about the engine**, and the +split matters because they go to different places. + +| # | observation | where it goes | +|---|---|---| +| 1 | no incentive to attack while holding positive cards | **register** — a game-design finding | +| 2 | *"I don't understand the GROUND card"* | **here** — and it is not a design gap (below) | +| 3 | overhead view: players around a table, stacks on it | T03 | +| 4 | click the draw stack to take cards | T04 | +| 5 | optional auto-draw | T04 | +| 6 | play again / stop belong in the meta column | T05 | +| 7 | so does the full log | T05 | +| 8 | *"Game over"* is wrong when you won | T06 | +| 9 | rankings — MVP, most problems solved | T07 | + +## Observation 2 is a data-import gap, and it reframes the pass + +*"I don't understand the GROUND card or why other cards should be played +to the table"* reads as a design problem. It is not. **The card explains +itself in the dataset and we never imported the explanation.** + +`ground-game/editions/*/Actions.csv`, the GROUND row: + +> **tagline:** *"Regulate. Restore the frame. Decide."* +> **rules_text:** *"After all actions are revealed, choose one mode: +> GR—Ground & Restate: −2 Stress, ready Freedom… OU—Observe & Uphold…"* + +**We vendored one file of nineteen.** `editions/ground-darvo-r0/` holds +`Problems.csv` and nothing else; `Actions`, `Solutions`, `Modes`, +`Scenarios`, `Relations`, `DARVO`, `Tokens`, `Glossary` and the rest live +only in `ground-game`. Everything the engine knows about them is a +**hand-transcription into `GroundRules.md`'s 59 numbered rules** — which +is enough to *play* the game and gives a player nothing to *read*. + +So the page shows `Clarify` where the card says **"Ask What Happened — +Invite a concrete account before judging."** + +**This is the most valuable thing in the pass**, and it was found by a +player saying he did not understand something rather than by any gate. +Rule coverage is 59/59 and has been for weeks. + +## Task: decide what else to import, and what it costs + +```task +id: CB-WP-0028-T01 +status: todo +priority: high +``` + +`decisions/ADR-0015-*.md` (tier M merges survey and decision). + +**ADR-0011 is the precedent and it constrains this.** It vendored +`Problems.csv` with a checked digest and a ~50-line hand reader, and +**refused the `csv` crate on proportion** — 17,651 lines against AM-4b's +19,742 of remaining headroom, 89% of the budget to read 20 rows. + +Decide: + +- **which files**, and the answer is not "all of them". `Actions`, + `Solutions` and `Modes` carry text a player reads. `BOM`, `Print_Manifest`, + `Back_Designs`, `Symbols` are production artifacts for a physical print + run and have no business here. +- **whether the hand reader survives contact.** `Solutions.csv` has + `microcopy` and `rules_text` — prose fields with commas, quotes and + possibly embedded newlines. ADR-0011 named exactly this as the revisit + condition: *"nested quoting, embedded newlines, multiple dialects."* + **If the reader cannot parse them correctly, say so and re-run the + dependency argument rather than writing a fragile parser.** +- **what happens when text and rules disagree.** `GroundRules.md` was + derived by hand from these files. If a `rules_text` contradicts a + numbered rule, that is a **finding**, and it goes to the register — not + a quiet edit of either. +- **`Extensions.csv` exists** and names content deliberately outside the + core. Decide whether it is imported at all; the answer is probably no, + but *"we did not know it existed"* must not be the reason. + +## Task: the cards say what they do + +```task +id: CB-WP-0028-T02 +status: todo +priority: high +``` + +Every card a player can see carries its own title, tagline and rules text, +from the edition — not from a phrase we invented. + +- action cards: GROUND, SOLVE, INVESTIGATE, SUPPORT, ATTACK; +- Solution cards: the title and microcopy, not just the suit; +- the mode in play, with its own description. + +**Controls:** +- **a test asserts the text comes from the dataset**, not from a Rust + literal — the whole point is that the game's own words reach the player, + and a hand-copied string drifts from the source it copies; +- the drag affordance still works: the explanation is *additional*, and + CB-WP-0020 T02 already found that replacing a label with an explanation + loses the label; +- **the page stays readable.** Five action cards with full rules text is a + wall; the tagline is the default and the rules text is on demand. + +## Task: an overhead view of a real table + +```task +id: CB-WP-0028-T03 +status: todo +priority: high +``` + +> *"Players sitting around the table, the draw and discard stacks on it. +> An overhead view of a game table would instantly help with understanding +> what is going on."* + +The seats become positions around a table rather than a row of cards; the +Problems, draw and discard sit in the middle; each seat's played card +appears in front of that seat. + +**This subsumes `relations_svg`**, which already places seats on a circle +for the relationship graph — so the layout exists and is drawn twice, once +as a circle and once as a row. **One table, not two diagrams.** + +**Controls:** +- the coverage gate still passes: every view field appears in the parsed + document. CB-WP-0027 showed a reflow costs nothing when probes name + facts; this is a bigger reflow and the same rule applies; +- **the viewer's own seat is identifiable at a glance** — a table where + you cannot find yourself is worse than a list; +- 2 through 6 seats all lay out without overlap, asserted per seat count + rather than eyeballed at 3. + +## Task: take cards from the stack, or let the table do it + +```task +id: CB-WP-0028-T04 +status: todo +priority: medium +``` + +Observations 4 and 5. Clicking the draw stack to take a card is the +natural gesture; auto-draw is for when that stops being interesting. + +**Both are subject to a rule the engine already has.** Drawing is not a +free action — GR-A01 draws a Solution as part of INVESTIGATE, and the deck +is not a thing a player may simply take from. **So this task must first +establish whether "click to draw" is a legal move at all**, and if it is +not, it is a **finding for `ground-game`**, not a feature: the maintainer +expected an interaction the rules do not offer, and that gap is the +signal. + +**Do not implement a draw the rules do not have.** CB-WP-0023 exists +because SOLVE was offered where it could not act. + +**Controls:** +- the answer to *"is clicking the deck a legal move?"* is in the task + record with the rule that settles it; +- auto-draw changes no outcome — a game played with it and without it from + the same seed produces the same end-state hash, or it is not automation + but a rules change. + +## Task: the meta column takes the controls and the log + +```task +id: CB-WP-0028-T05 +status: todo +priority: medium +``` + +Observations 6 and 7. `play again` and `end session` move to the meta +column, and the ending page gets the same two-column shape as the table — +it currently has none. + +The full log belongs there too, which CB-WP-0027 did for the live page and +not for the ending page. + +**Controls:** +- CB-WP-0024 T01's seal still works: acknowledging the end must still + remove every control, wherever they now live; +- the ending page's existing tests pass unchanged or the change is a + regression. + +## Task: a game you solve + +```task +id: CB-WP-0028-T06 +status: todo +priority: medium +``` + +> *"'Game Over' is negative and should only be used for lost games. If the +> players won, let's use 'Game solved'."* + +**Right, and it is more than tone.** GROUND is a co-operative game about +repairing a situation; `group_success` means the table *solved* something. +"Game over" is arcade vocabulary for a failure state, and using it for a +win tells the player the wrong thing about what they just did. + +**Controls:** +- the heading is a function of `outcome.group_success`, asserted **both + ways** — a test that only checks the win case passes for a page that + always says "solved"; +- the no-outcome case (a game that ended badly) keeps its own wording and + claims neither. + +## Task: who did what — rankings that are not invented + +```task +id: CB-WP-0028-T07 +status: todo +priority: medium +``` + +> *"A ranking of players in the final stats. 'Most valuable player', 'most +> problems solved' and other conditions that might apply."* + +`OutcomeView` already carries `personal`, `winners`, `coalitions` and +`mastery`, and the aggregate knows who claimed each Problem. + +**The risk is inventing scoring the game does not have.** *"Most problems +solved"* is countable from `claimed_by` and is a fact. *"Most valuable +player"* is a **judgement**, and any formula for it is a rule we made up. + +So: **report what the game counts, and mark anything derived as ours.** +A superlative computed from real data is fine; a superlative presented as +if the rules defined it is the same defect as a provisional default +silently canonised. + +**Controls:** +- every ranking names its source: a rule id, or *"clay-borg's reading"*; +- ties are handled and shown as ties, not broken arbitrarily — `Modes.csv` + has a `scoring_tiebreak` column, so the game may already say how; +- a ranking that no seat leads (nobody solved anything) renders as that, + not as an empty list. + +## Task: evidence + +```task +id: CB-WP-0028-T08 +status: todo +priority: medium +``` + +`evidence/CB-EV-0026-*.md`. + +- **Whether importing the card text changed what the maintainer + understood** — observation 2 is the acceptance test and it has a person + attached to it. +- **Whether any `rules_text` contradicted `GroundRules.md`.** Nineteen + files were hand-transcribed into 59 rules; if the import surfaces a + disagreement, that is the most valuable output of this pass. +- **Whether "click the deck to draw" was legal**, and if not, what went to + `ground-game`. +- **What the overhead view cost the coverage gate**, against CB-WP-0027's + finding that a probe naming a fact survives a reflow. +- **Quote CB-WP-0027's cost by re-running the instrument.**