,
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/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/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(),
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.**