diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index fca3e47..fae1353 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -421,24 +421,10 @@ fn pile_svg(out: &mut String, x: i32, label: &str, count: usize, face: &str, not ); } -/// The draw stack and the discard stack, as objects on the table. -/// -/// Both numbers come from the projection — `solution_deck_len` and -/// `solution_discard` — and are never recomputed here. -/// -/// **The reshuffle is real and is the U4 default**, confirmed by -/// ground-game on 2026-08-03: when the deck runs out, the discard is -/// reshuffled into it deterministically; if both are empty the draw is -/// skipped (`games/ground/src/lib.rs` `draw_solution`). So the pile shows -/// the state in which the *next* draw will trigger it. It does not claim a -/// reshuffle has happened — the view carries no such flag, and the event -/// already reads out in the log. -fn piles_svg(out: &mut String, view: &GroundView) { +fn piles_body(out: &mut String, view: &GroundView) { let deck = view.solution_deck_len; let discard = view.solution_discard.len(); let will_reshuffle = deck == 0 && discard > 0; - - out.push_str(""); pile_svg( out, 10, @@ -465,27 +451,34 @@ fn piles_svg(out: &mut String, view: &GroundView) { ", ); } - out.push_str(""); - - // The discard is public — it has been played (GR-S04 hides only the - // deck) — so its contents stay readable as text beside the picture. - let _ = write!( - out, - "
discard {}
", - esc(&cards(&view.solution_discard)), - ); } -fn relations_svg(view: &GroundView) -> String { +/// The table, seen from above (CB-WP-0028 T03). +/// +/// **One diagram, not three.** The seats were already placed on a circle +/// for the relationship graph, while the Problems were a row above it and +/// the piles a separate picture below — three views of one table, and the +/// player had to assemble them. Now: seats around the edge, the Problems +/// and the two stacks in the middle, each seat's played card in front of +/// it, relations drawn between seats. +/// +/// The existing `problem_svg` and `pile_svg` are reused rather than +/// reimplemented — they carry the tokens the coverage gate probes for, +/// and drawing the same thing twice is how the two drift. +fn table_svg(view: &GroundView) -> String { let n = view.players.len().max(1); - let (cx, cy, r) = (200.0f64, 150.0f64, 110.0f64); + let (cx, cy) = (380.0f64, 300.0f64); + let seat_r = 240.0f64; + let pos: Vec<(PlayerId, f64, f64)> = view .players .keys() .enumerate() .map(|(i, p)| { - let a = std::f64::consts::TAU * (i as f64) / (n as f64) - std::f64::consts::FRAC_PI_2; - (*p, cx + r * a.cos(), cy + r * a.sin()) + // Start at the bottom, not the top: the viewer sits nearest + // the reader, which is where you sit at a real table. + let a = std::f64::consts::TAU * (i as f64) / (n as f64) + std::f64::consts::FRAC_PI_2; + (*p, cx + seat_r * a.cos(), cy + seat_r * a.sin() * 0.72) }) .collect(); let find = |p: PlayerId| { @@ -495,8 +488,12 @@ fn relations_svg(view: &GroundView) -> String { }; let mut s = String::from( - "", + "\ + ", ); + + // Relations first, so seats draw over them. for (pair, rel) in &view.relations { if let (Some((x1, y1)), Some((x2, y2))) = (find(pair.0), find(pair.1)) { let colour = match rel { @@ -507,32 +504,70 @@ fn relations_svg(view: &GroundView) -> String { s, "\ - {rel:?}", + {rel:?}", mx = (x1 + x2) / 2.0, - my = (y1 + y2) / 2.0 - 3.0, + my = (y1 + y2) / 2.0 - 4.0, ); } } + + // The middle of the table: Problems, then the two stacks under them. + let span = (view.problems.len().max(1) as f64) * 130.0; + let _ = write!( + s, + "", + x = cx - span / 2.0, + ); + for (i, (priority, p)) in view.problems.iter().enumerate() { + problem_svg(&mut s, *priority, p, (i as i32) * 130); + } + s.push_str(""); + let _ = write!(s, "", x = cx - 150.0); + piles_body(&mut s, view); + s.push_str(""); + + // Seats, each with what it played in front of it. for (p, x, y) in &pos { let is_viewer = view.viewer == Some(*p); let focus = view .focus .get(p) - .map(|f| format!(" \u{2192}{}", seat_name(*f))); + .map(|f| format!(" \u{2192}{}", seat_name(*f))) + .unwrap_or_default(); + let pv = &view.players[p]; + // The played card sits between the seat and the centre. + let (dx, dy) = ((cx - x) * 0.30, (cy - y) * 0.30); + if let Some(sel) = view.selections.get(p) { + let _ = write!( + s, + "", + px = x + dx - 26.0, + py = y + dy - 17.0, + ); + played_inner(&mut s, sel); + s.push_str(""); + } let _ = write!( s, - "\ - {name}\ - {focus}", + "\ + \ + {name}{you}\ + stress {stress}{focus}", raw = p.0, + // The viewer's own seat is thicker and blue: a table where you + // cannot find yourself is worse than a list. stroke = if is_viewer { "#9cf" } else { "#5a6b7a" }, - ty = y + 2.0, - ty2 = y + 16.0, + sw = if is_viewer { 3 } else { 2 }, + t1 = y - 2.0, + t2 = y + 14.0, name = seat_name(*p), - focus = esc(focus.as_deref().unwrap_or("")), + you = if is_viewer { " (you)" } else { "" }, + stress = pv.stress, + focus = esc(&focus), ); } s.push_str(""); @@ -575,6 +610,11 @@ const CARD_BACK: &str = "\ "; +/// The played card without its own ``, for placing on the table. +fn played_inner(out: &mut String, sel: &SelectionView) { + played_svg(out, sel); +} + fn played_svg(out: &mut String, sel: &SelectionView) { let s = match sel { SelectionView::Hidden => { @@ -845,21 +885,15 @@ fn meta_section(s: &mut String, meta: &[String], note_to: &str) { /// than a second rendering of it — two renderings of one state is how /// they drift. fn body(s: &mut String, view: &GroundView) { - s.push_str( - "

problems

", - ); + // CB-WP-0028 T03: one overhead view — seats around the edge, the + // Problems and both stacks in the middle, each seat's played card in + // front of it. This replaces three separate diagrams the player had to + // assemble: a Problems row, a relationship circle, and a piles picture. + s.push_str("

the table

"); if view.problems.is_empty() { - s.push_str( - "no problems in play", - ); + s.push_str("
no problems in play
"); } - for (i, (priority, p)) in view.problems.iter().enumerate() { - problem_svg(s, *priority, p, 10 + (i as i32) * 130); - } - s.push_str(""); - - s.push_str("

relationships

"); - s.push_str(&relations_svg(view)); + s.push_str(&table_svg(view)); s.push_str("

seats

"); for (id, p) in &view.players { @@ -867,8 +901,13 @@ fn body(s: &mut String, view: &GroundView) { } s.push_str("
"); - s.push_str("

solutions

"); - piles_svg(s, view); + // The discard's contents stay as text: it is public and readable, + // and the stack on the table shows only how many. + let _ = write!( + s, + "
discard {}
", + esc(&cards(&view.solution_discard)), + ); if let Some(o) = &view.outcome { let personal = o diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs index 6fbb79c..662d39d 100644 --- a/crates/cb-render-html/src/lib.rs +++ b/crates/cb-render-html/src/lib.rs @@ -714,6 +714,119 @@ mod piles { } } +/// CB-WP-0028 T03 — one overhead table, not three diagrams. +#[cfg(test)] +mod overhead_table { + use cb_game_runtime::{Project, ScenarioGame, Setup, Viewer}; + use cb_kernel::PlayerId; + use games_ground::GroundState; + + fn view_of(players: u8) -> games_ground::view::GroundView { + GroundState::setup( + &Setup { + players, + preset: format!("standard-{players}p"), + patch: Default::default(), + }, + 7, + ) + .expect("preset") + .project(Viewer::Player(PlayerId(0))) + } + + /// **Every seat count lays out, and no two seats land on each other.** + /// Asserted per count rather than eyeballed at three, which is the + /// only count anyone ever looks at. + #[test] + fn two_through_six_seats_all_lay_out_without_overlap() { + for players in 2..=6u8 { + let v = view_of(players); + let html = crate::doc::document(&v, &[], "/command?t=x", Some(PlayerId(0)), false); + + let seats: Vec<(f64, f64)> = html + .match_indices(" 70.0, + "{players}p: two seats are {d:.0}px apart and the circles are r=34 — \ + they overlap" + ); + } + } + } + } + + /// A table where you cannot find yourself is worse than a list. + #[test] + fn the_viewers_own_seat_is_marked() { + let v = view_of(4); + let html = crate::doc::document(&v, &[], "/command?t=x", Some(PlayerId(0)), false); + assert!( + crate::text_of(&html).contains("P1 (you)"), + "the viewer's seat is not identifiable" + ); + assert!( + html.contains("stroke=\"#9cf\" stroke-width=\"3\""), + "the viewer's seat should also be visually distinct, not only labelled" + ); + // A spectator has no seat to mark, and must not claim one. + let spec = GroundState::setup( + &Setup { + players: 4, + preset: "standard-4p".into(), + patch: Default::default(), + }, + 7, + ) + .expect("preset") + .project(Viewer::Spectator); + assert!( + !crate::text_of(&crate::doc::document(&spec, &[], "/x", None, false)).contains("(you)"), + "a spectator was given a seat" + ); + } + + /// The three diagrams became one: the relationship circle and the + /// piles picture are gone as separate views, and their content is on + /// the table. + #[test] + fn the_stacks_and_the_relations_are_on_the_table() { + let v = view_of(3); + let html = crate::doc::document(&v, &[], "/command?t=x", Some(PlayerId(0)), false); + let table = html + .split("aria-label=\"the table, seen from above\"") + .nth(1) + .and_then(|s| s.split("").next()) + .expect("one table svg"); + assert!( + table.contains("draw pile:"), + "the draw stack is not on the table" + ); + assert!( + table.contains("discard pile:"), + "the discard is not on the table" + ); + assert!( + !html.contains("relationship graph"), + "the old separate relationship diagram is still being drawn" + ); + } +} + /// CB-WP-0028 T02 — the cards say what they do, in the edition's words. #[cfg(test)] mod card_words { @@ -936,7 +1049,7 @@ mod two_columns { .and_then(|s| s.split("class=\"cb-meta\"").next()) .expect("a game column followed by a meta column"); assert!( - game.contains("

problems

"), + game.contains("

the table

"), "the table must be in the game column" ); assert!( diff --git a/evidence/CB-EV-0026-the-table-you-sit-at.md b/evidence/CB-EV-0026-the-table-you-sit-at.md new file mode 100644 index 0000000..6d5c496 --- /dev/null +++ b/evidence/CB-EV-0026-the-table-you-sit-at.md @@ -0,0 +1,163 @@ +# CB-EV-0026 — the table you sit at + +CB-WP-0028 T08. Tier M (structural M — imports more of an external +dataset under AM-4's budgets; chaos d8=1 → no override). Declaration 11 of +chaos window 2. Closed 2026-08-06. + +**Delivered:** [ADR-0015](../decisions/ADR-0015-the-cards-own-words.md), +three vendored edition files, a generic reader, the cards' own words on +the page, one overhead table replacing three diagrams, controls and log in +the meta column, *"game solved"*, and rankings that cite their source. + +--- + +## 1. Nine observations, and where they actually came from + +**Seven of nine were engine defects. One was a design finding. One was +already true and nobody could tell.** + +| # | observation | what it turned out to be | +|---|---|---| +| 1 | no incentive to attack | a **design finding** (F17), and still a note | +| 2 | *"I don't understand the GROUND card"* | **a data-import gap** — the card explains itself and we never read it | +| 3 | overhead view | three diagrams the player had to assemble | +| 4 | click the deck to draw | **not a legal move**; built nothing | +| 5 | auto-draw option | **already automatic**, and always was | +| 6–7 | controls and log on the right | layout | +| 8 | *"Game Over"* on a win | wrong vocabulary for a co-operative game | +| 9 | rankings | a place where the game defines *not to rank* | + +**Observations 4 and 5 are the most useful pair in the set**, because both +dissolved. Clicking the deck is not a move — `GroundCommand` has no +standalone draw, and the edition's INVESTIGATE text says *"…reveal it. +**Then draw one Solution.**"* Drawing is a consequence. And auto-draw was +asked for a thing that has **never been manual**. + +**Both had one cause: nothing on the page said how drawing works.** The +INVESTIGATE card's own words were in a file we had not imported. So a +player invented a mental model to fill the gap, and reported the gap as +two feature requests. + +**No finding was raised.** An instinct differing from a *legible* rule is +a comprehension gap; an instinct differing from an *invisible* one is our +defect. Whether it recurs now that the card says it is a testable question +and was not before. + +## 2. The import gap was worse than "one file of nineteen" + +Measured before deciding: + +| | | +|---|---| +| edition files upstream | 19 | +| vendored | **1** | +| columns in that one file | 13 | +| columns the engine read | **5** | +| scenarios in the file | 4 | +| scenarios the engine deals | **1**, hardcoded | + +`title`, `problem_text`, `front_rules`, `reveal_effect` and +`unresolved_effect` were vendored on 2026-08-04 and **discarded at parse +time for eight days**. The page showed `Repair 2` for a card that reads +*"Missed Deadline"*. Fixing that cost no new bytes, no dependency and no +budget — it was the cheapest thing in the pass and the one nothing had +found. + +**Rule coverage was 59/59 throughout.** The gate measures whether rules +are exercised, and every rule was. **Nothing measures whether a player can +read the game**, and nothing cheaply could — which is why the person +playing it is the instrument. + +## 3. The dependency question was answered by measurement, not preference + +ADR-0011 refused the `csv` crate and named its own revisit condition: +*"nested quoting, embedded newlines, multiple dialects."* + +Across every candidate file — Actions, Solutions, Modes, Scenarios — +**zero doubled quotes and zero embedded newlines.** The hand reader's only +job is comma-in-quoted-field, which it already did. + +So the argument was not re-run, **and the reason is a measurement rather +than the inconvenience of re-running it.** `edition-check` now asserts +that condition on every vendored file, so the day it stops being true the +gate says so instead of a parser mangling a card. + +**One reader, four callers.** A per-file copy is how a parser acquires +four subtly different bugs. + +## 4. Two gates were written for a smaller world + +**`edition-check` compared the first recorded digest against +`Problems.csv` regardless of which file that digest described.** With one +vendored file it was correct; with four it was comparing across files. It +now checks both directions — a vendored file with no digest fails, and a +digest naming an absent file fails. + +**A `cb-play` test asserted the literal string `"game over"`** and went +red when a won game began saying *"solved"* — the feature working. Fixed +by asserting the heading **against the outcome** rather than against a +word, which also covers the no-outcome case the original never touched. + +**Both are the same shape:** an assertion that encoded a world with one +file, or one ending. Neither was wrong when written. + +## 5. What T07 decided not to do + +`Modes.csv` gives `scoring_tiebreak` per mode. For SHARED GROUND it is +**"Not applicable"** — the table succeeds or fails together. + +So the co-operative mode shows contributions and **refuses to order +them**, and says why on the page. A leaderboard would have been easy, +looked good, and invented scoring the game does not have — the same defect +class as canonising a provisional default, which this project has already +committed once. + +Where a mode *does* rank, the ordering is the game's own words. The single +derived superlative is labelled **"clay-borg's reading, not a rule."** + +**This is the first time the project has had the game's own tiebreak to +cite**, and it only had it because §2's import brought `Modes.csv` in. The +alternative was inventing one. + +## 6. The overhead view cost no coverage probe + +All existing probes passed through a restructure that **deleted two +renderers** (`relations_svg`, `piles_svg`) and merged three diagrams into +one. + +**Second confirmation of CB-WP-0027's finding**: a probe that names a +*fact* survives a reflow; one that names a *presentation* does not. +CB-WP-0024's `17 remaining` probe broke on a rendering change; +CB-WP-0027's and this pass's did not, and this reflow was far larger. + +The new control is per-seat-count: **2 through 6 seats all lay out with no +two seat circles closer than 70px**, asserted rather than eyeballed at +three — which is the only count anyone ever looks at. + +## 7. Chaos window 2 — closed + +**Declaration 11 of 12**, structural M, d8 = 1, no override. + +The window has now produced **zero overrides in eleven declarations at +d8**. Its retirement condition — *retire if an override changes nothing +twice running* — was never testable, and CB-EV-0024 and CB-EV-0025 both +said so. This is the third and final statement of it. + +**The window's verdict should be that d8 bought rarity by spending +evidence.** Window 1 at d4: two overrides, both changed the outcome. +Window 2 at d8: none. **A mechanism that produces no data across a full +window cannot be evaluated by that window**, which is a stronger +conclusion than "the rate is too low" and belongs in whatever closes it. + +## Open after this pass + +- **F17 needs an artifact.** Counting ATTACK selections across the policy + panel against hand quality is cheap and nobody has done it. +- **Three of four scenarios have never been dealt** (ADR-0015 D5). Now + visible, still true. +- **`Relations`, `DARVO`, `Tokens`, `Glossary` are deferred, not refused.** + If a seat's DARVO stage needs its own words, that is the trigger. +- **Nobody has written a trial note in anger** — carried forward from + CB-EV-0025, and the note channel only started working today. +- **Whether the card text changed what the maintainer understood** is this + pass's real acceptance test and has a person attached to it. diff --git a/tools/cb-play/src/hotseat.rs b/tools/cb-play/src/hotseat.rs index 3513935..0dd359c 100644 --- a/tools/cb-play/src/hotseat.rs +++ b/tools/cb-play/src/hotseat.rs @@ -752,7 +752,19 @@ mod tests { let replies = client.join().expect("client"); assert!(replies[0].contains("200 OK"), "{}", replies[0]); - assert!(replies[0].contains("game over"), "the page did not say so"); + // CB-WP-0028 T06: the heading follows the OUTCOME, so this asks + // the view rather than hardcoding a word. The old assertion said + // "game over" and went red when a won game started saying + // "solved" -- which was the feature working. + let want = match view.outcome.as_ref() { + Some(o) if o.group_success => "game solved", + Some(_) => "game over", + None => "the game stopped", + }; + assert!( + replies[0].contains(want), + "the heading did not match the outcome; expected {want:?}" + ); assert!( replies[0].contains("30 commands"), "the result did not reach the page" diff --git a/workplans/CB-WP-0028-the-table-you-sit-at.md b/workplans/CB-WP-0028-the-table-you-sit-at.md index 9b34a8c..f0da28f 100644 --- a/workplans/CB-WP-0028-the-table-you-sit-at.md +++ b/workplans/CB-WP-0028-the-table-you-sit-at.md @@ -2,7 +2,7 @@ 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: active +status: done state_hub_workstream_id: "d37c8671-54e4-447f-af64-56f482483282" --- @@ -39,29 +39,16 @@ split matters because they go to different places. ## 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.** +*"I don't understand the GROUND card"* reads as a design problem. It is +not: `Actions.csv` carries that card's tagline (*"Regulate. Restore the +frame. Decide."*) and its full rules text, and **we vendored one file of +nineteen**. Everything else the engine knows is a hand-transcription into +`GroundRules.md`'s 59 rules — enough to *play* the game, and nothing to +*read*. The page shows `Clarify` where the card says *"Ask What Happened."* -`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. +**Found by a player saying he did not understand something.** Rule +coverage is 59/59 and has been for weeks. See F18 and +[ADR-0015](../decisions/ADR-0015-the-cards-own-words.md). ## Task: decide what else to import, and what it costs @@ -103,23 +90,17 @@ Decide: [ADR-0015](../decisions/ADR-0015-the-cards-own-words.md), six decisions. **The gap is bigger than "one file of nineteen", and the measurement is -the decision.** Of the file we *did* vendor, the engine reads **5 of 13 -columns** — `title`, `problem_text`, `front_rules`, `reveal_effect` and -`unresolved_effect` were discarded at parse time. **The cheapest part of -this pass costs no new bytes** and was sitting in the repo for eight days. -And `SCN_01` is hardcoded at `lib.rs:1824`: the edition ships **four** -scenarios and the engine has never dealt three of them. +the decision.** Of the file we *did* vendor the engine reads **5 of 13 +columns**; the discarded ones are player-facing text that had been in the +repo for eight days. And `SCN_01` is hardcoded — the edition ships **four** +scenarios and the engine has never dealt three. -**ADR-0011's revisit condition is measurably absent**, so the dependency -argument does not get re-run. Across Actions, Solutions, Modes and -Scenarios: **zero doubled quotes, zero embedded newlines.** The hand -reader's only job is comma-in-quoted-field, which it already did. - -Vendored `Actions`, `Solutions`, `Modes` — the text a player reads. Not -the production artifacts. **Not `Extensions.csv`**, because it names -content the designer placed *outside* the core, and importing it would -break the as-printed claim — but it is now known to exist, which was the -real risk. +**ADR-0011's revisit condition is measurably absent** — zero doubled +quotes, zero embedded newlines across every candidate file — so the +dependency argument does not get re-run. Vendored `Actions`, `Solutions`, +`Modes`. Not the production artifacts, and **not `Extensions.csv`**, which +names deliberately non-core content; it is now known to exist, which was +the real risk. ## Task: the cards say what they do @@ -147,38 +128,26 @@ from the edition — not from a phrase we invented. - **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. -**Done 2026-08-06.** One `Table` reader with four callers (a per-file copy -is how a parser acquires four subtly different bugs), `CardText` for -Actions/Solutions/Modes, and `ProblemText` for the columns already -vendored. - -**The GROUND card now explains itself**: *"Regulate. Restore the frame. -Decide."* as the tagline, its GR/OU/ND text behind a disclosure. Problems -show their own titles where a priority number used to be. +**Done 2026-08-06.** One `Table` reader with four callers (a per-file +copy is how a parser acquires four subtly different bugs), `CardText` for +Actions/Solutions/Modes, `ProblemText` for the columns already vendored. +The GROUND card now explains itself; Problems show their own titles. **The load-bearing test asserts the text is a substring of the vendored -file**, not equal to a Rust literal — a test comparing against a -hardcoded expectation would pass for a hand-copied string, which is the -drift this ends. Plus counts (5 actions, 24 solutions, 3 modes), because a -reader returning one row would pass a "the text is real" test. +file**, not equal to a Rust literal — a hardcoded expectation would pass +for a hand-copied string, which is the drift this ends. -**`edition` came out from behind `#[cfg(feature = "scenarios")]`.** It was -gated because its only consumer was; but the edition is the game's own -data and the shipped runtime now reads it. **Test machinery and game -content are different things and only one of them is optional.** - -**`edition-check` was written for a single-file world** and compared the -first recorded digest against `Problems.csv` regardless of which file it -described. It now checks every file both ways — a vendored file with no -digest fails, and a digest naming an absent file fails — and asserts -ADR-0015 D3's falsifier directly: no doubled quotes, no embedded -newlines. +**`edition` came out from behind `#[cfg(feature = "scenarios")]`**: it was +gated because its only consumer was, but the edition is the game's own +data and the runtime now reads it. **Test machinery and game content are +different things and only one is optional.** `edition-check`, written for +a single-file world, now checks every file both ways. ## Task: an overhead view of a real table ```task id: CB-WP-0028-T03 -status: todo +status: done priority: high state_hub_task_id: "2ff677ac-80ff-4618-ad7c-05b62c8f80c8" ``` @@ -204,6 +173,25 @@ as a circle and once as a row. **One table, not two diagrams.** - 2 through 6 seats all lay out without overlap, asserted per seat count rather than eyeballed at 3. +**Done 2026-08-06.** One `table_svg`: seats around an elliptical table +starting at the **bottom** (the viewer sits nearest the reader, as at a +real table), Problems and both stacks in the middle, each seat's played +card between it and the centre, relations drawn between seats. + +**Two renderers were deleted** — `relations_svg` and `piles_svg`. The task +said *one table, not two diagrams*, and leaving the old ones would have +meant drawing the same thing twice and letting them drift. + +**No coverage probe cost**, through a restructure that merged three +diagrams and removed two functions. **Second confirmation of CB-WP-0027's +finding**: a probe naming a *fact* survives a reflow; one naming a +*presentation* does not. + +The new control is per seat count: **no two seat circles closer than 70px +at 2, 3, 4, 5 or 6 seats** — asserted rather than eyeballed at three, +which is the only count anyone ever looks at. Plus: the viewer's seat is +labelled *and* visually distinct, and a spectator is not given one. + ## Task: take cards from the stack, or let the table do it ```task @@ -369,7 +357,7 @@ are shown as ties. ```task id: CB-WP-0028-T08 -status: todo +status: done priority: medium state_hub_task_id: "a20a529e-0edf-49cc-9c80-7dba3b76cff5" ``` @@ -387,3 +375,25 @@ state_hub_task_id: "a20a529e-0edf-49cc-9c80-7dba3b76cff5" - **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.** + +**Done 2026-08-06.** +[CB-EV-0026](../evidence/CB-EV-0026-the-table-you-sit-at.md). + +- **Seven of nine observations were engine defects**, one was a design + finding, and one was already true and nobody could tell. +- **Observations 4 and 5 both dissolved**, and had one cause: nothing on + the page said how drawing works, so a player built a mental model to + fill the gap and reported the gap as two feature requests. +- **The import gap was worse than "one of nineteen"** — 5 of 13 columns + read from the file we *did* vendor, discarded at parse time for eight + days. Rule coverage was 59/59 throughout: the gate measures whether + rules are *exercised*, and **nothing measures whether a player can read + the game.** +- **Two gates were written for a smaller world** — `edition-check` + compared one digest across four files, and a `cb-play` test asserted the + literal `"game over"` and went red when a won game said *"solved"*. + Neither was wrong when written. +- **Chaos window 2 closes with zero overrides in eleven declarations.** + Third and final statement: d8 bought rarity by spending evidence, and a + mechanism producing no data across a full window cannot be evaluated by + it.