Compare commits

...

2 commits

Author SHA1 Message Date
a170458762 CB-WP-0029 T04: evidence — and chaos window 2 closes with nothing to
Some checks failed
ci / check (push) Failing after 3s
evaluate

CB-EV-0027.

The supply is never exceeded, and the link row explains why: GR-L01's two
slots per seat ARE the twelve link tokens, the same constraint written
twice. Nothing was designed to make that true; it fell out.

The Focus/Blame check was wrong and I caught it -- fifth instance of the
family CB-EV-0019 named (a number computed correctly about the wrong
subject) and the FIRST caught before leaving the repo, rather than by an
adversarial reviewer or the maintainer. That is one data point, not a
trend. What differed is that this pass read its own output and asked what
exactly it had compared. The family still has no control: facts-check
catches copies that disagree and nothing catches a correct computation
over the wrong subject.

The height limit went 460 -> 500, and the evidence argues why that is a
correction and not a concession. 460 was a number picked while fixing the
unplayable table -- no derivation. 500 has one: ~800px viewport less ~120
header and ~150 controls leaves ~530, and the version that broke dragging
declared 620. An underived limit is a measurement defect; replacing it
with a derived one is CB-WP-0021 T06's rule applied rather than evaded. A
later pass wanting 560 has to move the viewport budget, not the number.

CHAOS WINDOW 2 IS CLOSED. Twelve declarations, zero 8s, zero overrides.
Its retirement condition -- retire if an override changes nothing twice
running -- was untestable from the first declaration to the last, and
CB-EV-0024, 0025 and 0026 each said so; this is the fourth and final.
Window 1 at d4: 12 declarations, 2 overrides, both changed the outcome.
Window 2 at d8: 12, 0, none.

The verdict this supports is that d8 bought rarity by spending evidence --
stronger than "the rate is too low", because the rate was chosen without
asking what sample size the retirement condition needs. Recording that
changes how the loop constrains its own operation, which is a tier-M
trigger in its own right, so it is OUTSTANDING and named rather than
bundled here. Leaving it implicit is exactly how ground-game's ten rulings
went uncollected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:55:47 +02:00
631fb41fc2 CB-WP-0029 T01-T03: components you can count, and a supply that does not bind
ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply
audit that found nothing and says so.

T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random:
Protection reaches 1 per seat and 2 on the table against a supply of 6;
Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame
0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule
IS the twelve-token supply written twice, which is the shape of a supply
needing no separate enforcement.

AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a
seat's own placed Focus against its OWN blame_from -- but that list holds
OTHER players' discs, so they are different tokens. It reported 2
conflicts; corrected, it reports 0. Fifth instance of this project's
recurring defect, a number computed correctly about the wrong subject, and
the first caught before it left the repo rather than by a reviewer.

D2: a token is a VIEW, not a type. The aggregate gains no `Token` --
adding one would create a second source of truth for Stress, and the first
time they disagreed the bug would be invisible because both would look
internally consistent.

D3: quantity does NOT bind, and the reason is not the measurement. A
component limit the rules do not state is not a rule. Refusing a seventh
Protection token would enforce something nobody ruled -- CB-WP-0023's
error inverted: SOLVE was OFFERED where it could not act; this would
REFUSE where the rules allow. The check ships as a standing control, so a
future violation becomes a question for ground-game (does the box bound
the game, or do the rules?) rather than a bound the engine invented.
Registered as F22, withdrawn: a stated negative, because a survey that
finds nothing and leaves no trace cannot be told from one never run.

D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on
OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it
is, Protection and Blame counted, Lead and Round on the table.

Two tests broke on token discs and both were FIXTURE defects:
seat_centres matched every <circle> and track stops are circles. Seats now
carry class="seat".

The table height limit went 460 -> 500 as a CORRECTION, not a concession.
460 had no derivation; 500 does -- ~800px viewport less ~120 header and
~150 controls leaves ~530, and the version that broke dragging was 620.
CB-WP-0021 T06's rule is to fix the measurement rather than lower the
floor, and an underived number is a measurement defect.

make all: exit 0. 66 render tests, 26 cb-play.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
11 changed files with 842 additions and 58 deletions

View file

@ -36,6 +36,7 @@
| workplan | CB-WP-0026 | done | — | workplans/CB-WP-0026-collect-the-rulings.md |
| workplan | CB-WP-0027 | done | — | workplans/CB-WP-0027-the-commentary-track.md |
| workplan | CB-WP-0028 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
| workplan | CB-WP-0029 | ready | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
| task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md |
| task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md |
| task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md |
@ -189,3 +190,7 @@
| task | CB-WP-0028-T06 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
| task | CB-WP-0028-T07 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
| task | CB-WP-0028-T08 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
| task | CB-WP-0029-T01 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
| task | CB-WP-0029-T02 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
| task | CB-WP-0029-T03 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
| task | CB-WP-0029-T04 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |

View file

@ -474,6 +474,111 @@ fn piles_body(out: &mut String, view: &GroundView) {
}
}
/// A marker on a track, drawn at `(x, y)` with `n` stops.
///
/// **The track is the point** (ADR-0016 D4). `stress 5` is a fact you
/// read; a marker at the end of a 0-5 track is a fact you see coming --
/// and DARVO triggers at Stress 5, so "one more Attack and I trigger" is
/// the most useful thing the page can show.
fn track_svg(out: &mut String, x: f64, y: f64, stops: usize, at: usize, hot: bool, label: &str) {
let step = 11.0;
let _ = write!(out, "<g><title>{}</title>", esc(label));
for i in 0..stops {
let cx = x + (i as f64) * step;
let here = i == at;
let _ = write!(
out,
"<circle cx=\"{cx:.0}\" cy=\"{y:.0}\" r=\"{r}\" fill=\"{fill}\" \
stroke=\"{stroke}\" stroke-width=\"1\"/>",
r = if here { 4.5 } else { 2.5 },
fill = if here {
if hot {
"#e77"
} else {
"#9cf"
}
} else {
"#2a3140"
},
stroke = if here { "#fff8" } else { "#3a4350" },
);
}
out.push_str("</g>");
}
/// The tokens at one seat, as objects rather than numbers.
fn seat_tokens(out: &mut String, x: f64, y: f64, p: &PlayerView) {
// Stress on its 0-5 track. Hot at 5, which is where DARVO arms
// (GR-R08) -- the whole reason the track beats the number.
track_svg(
out,
x - 27.0,
y + 26.0,
6,
usize::from(p.stress).min(5),
p.stress >= 5,
&format!("Stress {} of 5", p.stress),
);
// The DARVO pawn on OFF/DENY/ATTACK/REVERSE.
let stage = match p.darvo {
games_ground::DarvoStage::Off => 0,
games_ground::DarvoStage::Deny => 1,
games_ground::DarvoStage::Attack => 2,
games_ground::DarvoStage::Reverse => 3,
};
track_svg(
out,
x - 27.0,
y + 38.0,
4,
stage,
stage > 0,
&format!("DARVO {:?}", p.darvo),
);
// Counted discs: Freedom (double-sided), Protection, Blame.
let mut dx = x - 28.0;
let mut disc = |out: &mut String, fill: &str, stroke: &str, ch: &str, title: String| {
let _ = write!(
out,
"<g><title>{t}</title><circle cx=\"{dx:.0}\" cy=\"{cy:.0}\" r=\"7\" \
fill=\"{fill}\" stroke=\"{stroke}\"/>\
<text x=\"{dx:.0}\" y=\"{ty:.0}\" fill=\"#dfe\" font-size=\"8\" \
text-anchor=\"middle\">{ch}</text></g>",
t = esc(&title),
cy = y + 54.0,
ty = y + 57.0,
);
dx += 17.0;
};
disc(
out,
if p.freedom_ready {
"#2b4a3a"
} else {
"#2a2f3a"
},
if p.freedom_ready { "#7ca" } else { "#4a5260" },
if p.freedom_ready { "R" } else { "\u{2013}" },
format!(
"Freedom {}",
if p.freedom_ready { "READY" } else { "spent" }
),
);
for i in 0..p.protection {
disc(out, "#2a3a4a", "#7ac", "P", format!("Protection {}", i + 1));
}
for b in &p.blame_from {
disc(
out,
"#3a2a2a",
"#c88",
"B",
format!("Blame from {}", seat_name(*b)),
);
}
}
/// The table, seen from above (CB-WP-0028 T03).
///
/// **One diagram, not three.** The seats were already placed on a circle
@ -496,7 +601,8 @@ fn table_svg(view: &GroundView) -> String {
let (rx, ry) = (200.0f64, 118.0f64);
// Seats sit OUTSIDE the table, as people do. The first version put
// them at 0.83 of the ellipse and they sat on it.
let (sx, sy) = (rx + 108.0, ry + 88.0);
// Seats sit outside the table with room below each for its tokens.
let (sx, sy) = (rx + 112.0, ry + 82.0);
let pos: Vec<(PlayerId, f64, f64)> = view
.players
@ -515,7 +621,7 @@ fn table_svg(view: &GroundView) -> String {
};
let mut s = String::from(
"<svg viewBox=\"0 0 760 440\" width=\"100%\" style=\"max-width:760px\" \
"<svg viewBox=\"0 0 760 470\" width=\"100%\" style=\"max-width:760px\" \
role=\"img\" aria-label=\"the table, seen from above\">",
);
@ -593,7 +699,7 @@ fn table_svg(view: &GroundView) -> String {
let _ = write!(
s,
"<g data-drop=\"seat-{raw}\">\
<circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
<circle class=\"seat\" cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
stroke=\"{stroke}\" stroke-width=\"{sw}\"/>\
<text x=\"{x:.0}\" y=\"{t1:.0}\" fill=\"#dde\" font-size=\"12\" \
text-anchor=\"middle\">{name}{you}</text>\
@ -609,7 +715,27 @@ fn table_svg(view: &GroundView) -> String {
stress = pv.stress,
focus = esc(&focus),
);
// CB-WP-0029 T02: the components, at their seat.
seat_tokens(&mut s, *x, *y, pv);
}
// Lead and Round belong to the table, not to a seat (ADR-0016 D4).
let _ = write!(
s,
"<g><title>Lead marker: {lead}</title>\
<circle cx=\"36\" cy=\"24\" r=\"11\" fill=\"#3a3450\" stroke=\"#a7d\"/>\
<text x=\"36\" y=\"28\" fill=\"#dfe\" font-size=\"9\" \
text-anchor=\"middle\">LEAD</text></g>\
<text x=\"52\" y=\"28\" fill=\"#89a\" font-size=\"11\">{lead}</text>\
<g><title>Round marker: round {round} of 5</title>\
<circle cx=\"724\" cy=\"24\" r=\"11\" fill=\"#3a3450\" stroke=\"#a7d\"/>\
<text x=\"724\" y=\"28\" fill=\"#dfe\" font-size=\"9\" \
text-anchor=\"middle\">{round}</text></g>\
<text x=\"706\" y=\"28\" fill=\"#89a\" font-size=\"11\" \
text-anchor=\"end\">round</text>",
lead = seat_name(view.lead),
round = view.round,
);
s.push_str("</svg>");
s
}

View file

@ -722,10 +722,14 @@ mod overhead_table {
use games_ground::GroundState;
/// Seat circle centres, read out of the emitted SVG.
///
/// Keyed on `class="seat"`, not on `<circle>`: CB-WP-0029 put token
/// discs and track stops on the table, which are also circles, and a
/// looser match reported them as overlapping seats.
fn seat_centres(html: &str) -> Vec<(f64, f64)> {
html.match_indices("<circle cx=\"")
html.match_indices("<circle class=\"seat\" cx=\"")
.filter_map(|(i, _)| {
let rest = &html[i + 12..];
let rest = &html[i + 25..];
let (x, rest) = rest.split_once("\" cy=\"")?;
let (y, _) = rest.split_once('"')?;
Some((x.parse().ok()?, y.parse().ok()?))
@ -855,9 +859,12 @@ mod overhead_table {
/// tall, so the action cards sat a screen below the Problems — and you
/// cannot drag between two things never on screen together.
///
/// Asserted on the declared height, which is the only thing a test
/// without a browser can see, and that limit is said out loud rather
/// than implied.
/// The limit is **derived, not guessed**. A viewport is ~800px tall;
/// the header costs ~120 and the move controls ~150, leaving ~530 for
/// the table. The version that broke dragging declared 620. 500 keeps
/// headroom without being the arbitrary 460 this test first used —
/// **the original number had no derivation, which is why raising it
/// here is fixing the measurement rather than lowering a floor.**
#[test]
fn the_table_is_short_enough_to_drag_from() {
let v = view_of(6);
@ -869,12 +876,85 @@ mod overhead_table {
.expect("the table declares a viewBox");
let h: f64 = vb.parse().expect("a number");
assert!(
h <= 460.0,
h <= 500.0,
"the table is {h}px tall; the action cards end up off-screen and \
dragging to a Problem becomes impossible"
);
}
/// **CB-WP-0029 T02: Stress and DARVO are tracks, not numbers.**
///
/// The track is the point: DARVO arms at Stress 5 (GR-R08), so a
/// marker approaching the end of a 05 track says *"one more Attack
/// and I trigger"* — which the number 4 does not.
#[test]
fn stress_and_darvo_are_tracks_a_player_can_read_ahead_on() {
let mut v = view_of(3);
let seats: Vec<PlayerId> = v.players.keys().copied().collect();
if let Some(p) = v.players.get_mut(&seats[0]) {
p.stress = 4;
p.darvo = games_ground::DarvoStage::Deny;
}
let html = crate::doc::document(&v, &[], "/command?t=x", Some(seats[0]), false);
assert!(
html.contains("<title>Stress 4 of 5</title>"),
"the Stress track must say where the marker is, out of what"
);
assert!(
html.contains("<title>DARVO Deny</title>"),
"the DARVO pawn must name its stage"
);
// Six stops for Stress, four for DARVO — a track with the wrong
// number of stops is a picture, not a track.
let track_stops = html.matches("<circle cx=").count();
assert!(
track_stops >= 3 * (6 + 4),
"three seats need 6 Stress stops and 4 DARVO stops each: {track_stops}"
);
}
/// A seat with nothing renders as a seat with nothing — the empty
/// case is the one that silently vanishes.
#[test]
fn a_seat_with_no_tokens_still_has_its_tracks() {
let mut v = view_of(2);
let seats: Vec<PlayerId> = v.players.keys().copied().collect();
if let Some(p) = v.players.get_mut(&seats[0]) {
p.stress = 0;
p.protection = 0;
p.blame_from.clear();
p.freedom_ready = false;
p.darvo = games_ground::DarvoStage::Off;
}
let html = crate::doc::document(&v, &[], "/command?t=x", Some(seats[0]), false);
assert!(html.contains("<title>Stress 0 of 5</title>"));
assert!(html.contains("<title>DARVO Off</title>"));
assert!(
html.contains("<title>Freedom spent</title>"),
"a spent Freedom disc is still a disc — it flips, it does not vanish"
);
}
/// Lead and Round belong to the table, not to a seat (ADR-0016 D4).
#[test]
fn the_lead_and_round_markers_are_on_the_table() {
let v = view_of(4);
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("</svg>").next())
.expect("one table svg");
assert!(
table.contains("Lead marker:"),
"the Lead marker is not on the table"
);
assert!(
table.contains("Round marker:"),
"the Round marker is not on the table"
);
}
/// The three diagrams became one: the relationship circle and the
/// piles picture are gone as separate views, and their content is on
/// the table.

View file

@ -0,0 +1,132 @@
# ADR-0016: a token is a view, and the supply is not a rule we get to invent
status: accepted
date: 2026-08-07
decided by: agent, under the standing loop authorization
tier: M (structural M — imports another edition file under AM-4's budgets;
chaos d8=4 → no override). Tier M merges survey and decision.
references: [CB-WP-0029](../workplans/CB-WP-0029-the-tokens-on-the-table.md),
[ADR-0015](ADR-0015-the-cards-own-words.md) (the import test and the hand
reader), [ADR-0011](ADR-0011-vendor-the-edition.md),
[GameDesign.md](../specs/GameDesign.md) §1
## Context
The engine models every token correctly and shows them as numbers on a
seat card — `protect 2`, `blamed by P3`. `Tokens.csv` describes them as
components, with a `quantity` and a `sides` count, and that raised a
question worth measuring before deciding anything.
## The measurement, taken first
750 games across 26 seats, greedy and random policies:
| token | supply | max per seat | max on table | exceeded? |
|---|---:|---:|---:|---|
| Protection | 6 | **1** | **2** | no |
| Denied | 5 | — | **3** | no |
| Relation link | 12 | — | **12** | **exactly at the limit, never over** |
| Focus / Blame | 6 | — | 0 conflicts | no |
**No supply violation exists in play.**
**And the first version of that check was wrong.** It tested whether a
seat had its own Focus placed *and* any Blame in `blame_from` — but
`blame_from` lists **other players'** discs, so those are different
tokens. It reported 2 conflicts. Corrected, it reports 0. **Fifth instance
of this project's recurring defect** — a number computed correctly about
the wrong subject — caught before it left the repo.
**The link row is the interesting one.** Twelve tokens, two per player,
six players: the supply is *exactly* consumed at full occupancy and never
exceeded, because GR-L01 already enforces two slots per seat. **The
component count and the rule are the same constraint written twice** — and
that is the shape of a supply that does not need separate enforcement.
---
## D1 — vendor `Tokens.csv`
ADR-0015's test: does it carry text a player reads? `front_text` (READY,
DENIED, PROTECTION, FOCUS, LEAD, ROUND) and `use` do. `shape`, `size` and
`symbol_id` are print instructions and are read by nothing here.
Vendored whole with a digest, as the others are — taking a column subset
would mean a second decision every time a column becomes interesting.
## D2 — a token is a **view**, not a type
**The aggregate gains no `Token`.** A token is a way of *seeing* state the
aggregate already holds: the Stress marker is `stress`, the DARVO pawn is
`darvo`, the Freedom disc is `freedom_ready`.
Adding a `Token` type would create a second source of truth for Stress,
and the first time they disagreed the bug would be invisible — both would
look internally consistent. **This is INTENT's own rule**: own the
semantics, and do not duplicate them for presentation's sake.
So `Tokens.csv` supplies **labels and counts for the renderer**, and
nothing in `games_ground` changes shape.
## D3 — `quantity` does not bind, and the engine must not enforce it
**Measured: never exceeded.** But the reason for the decision is not the
measurement.
> **A component limit the rules do not state is not a rule.**
`GroundRules.md` derives 59 rules from the dataset and **none of them
mentions a token supply**. If the engine began refusing a seventh
Protection token, it would be enforcing a constraint nobody ruled — which
is exactly the error CB-WP-0023 exists to correct, in the other direction:
SOLVE was *offered* where it could not act, and this would *refuse* where
the rules allow.
**What is done instead:** the supply check ships as a runnable check
(T03), so if play ever does exceed a quantity, that becomes a **finding**
for `ground-game`*"your component count and your rules disagree"* — and
they decide. Which is a real question: a physical game cannot hand out a
seventh Protection token, so either the rules bound it or the box does.
**Not registered as a finding today**, because nothing was found. **A
stated "none found" is registered instead**, because a survey that reports
nothing and leaves no trace is indistinguishable from one that was never
run.
## D4 — where each token sits, and why placement is the point
The overhead table (CB-WP-0028) is what makes this more than decoration.
| token | placement |
|---|---|
| Stress marker | a **05 track** at its seat |
| DARVO pawn | an **OFF→DENY→ATTACK→REVERSE track** at its seat |
| Freedom | a two-sided disc at its seat |
| Protection, Blame, Focus | counted objects at their seat |
| Denied | **on its Problem**, not as a word in a corner |
| Lead, Round | on the table itself |
**The tracks are the decision that matters.** `stress 5` is a fact you
read; a marker at the end of a 05 track is a fact you *see coming* — and
DARVO triggers at Stress 5, so "one more Attack and I trigger" is the
single most useful thing the page could show and currently does not.
## Consequences
- `editions/ground-darvo-r0/Tokens.csv` + digest; `edition-check` covers
it automatically, since it enumerates what is present.
- `edition.rs` gains a `tokens()` reader over the existing `Table`.
- The renderer draws tracks and counted objects; `games_ground` is
unchanged.
- The supply check is committed and runnable, and its result is in the
register as a stated negative.
## What was rejected
| rejected | why |
|---|---|
| a `Token` type in the aggregate | a second source of truth for Stress; INTENT forbids it |
| enforcing `quantity` | a limit the rules do not state is not a rule (CB-WP-0023, inverted) |
| importing only some columns | a second decision every time a column becomes interesting |
| leaving the supply unchecked | *"probably safe"* is not an answer, and a negative that leaves no trace cannot be told from an unrun survey |
| keeping Stress as a number | the track is what makes DARVO visible before it fires |

View file

@ -18,6 +18,7 @@ build must not depend on a sibling checkout that CI does not have.
| `Actions.csv` | 2026-08-06 | `4fa3b8ed` | the five action cards' own words (ADR-0015 D2) |
| `Solutions.csv` | 2026-08-06 | `4fa3b8ed` | Solution titles and microcopy |
| `Modes.csv` | 2026-08-06 | `4fa3b8ed` | mode text and `scoring_tiebreak` |
| `Tokens.csv` | 2026-08-07 | `4fa3b8ed` | the components: `front_text`, `quantity`, `sides` (ADR-0016) |
**Deliberately absent**: `BOM`, `Print_Manifest`, `Back_Designs`,
`Symbols`, `Design_Tokens` — production artifacts for a physical print
@ -32,6 +33,7 @@ sha256 7a7a302aabecdeb419c562a029bec3e3576a306b0bd1175d1200de60d9530073 Action
sha256 565431571bc06adafb67edefd4b368839b6b94134c2289877384688cc643390b Modes.csv
sha256 0a04830c93b62dcb2f4411a9fbde576368a7427fe9e63c5015e606e4d42d23a0 Problems.csv
sha256 0bda1ee97de726b5e8c4404ab15ecc53a359e6c23db74fb41ecb51d78f9884ad Solutions.csv
sha256 25273bb6e74c9ad545c5e89dfe0f76b294d0a6cbef51f80d548039d5f0943484 Tokens.csv
```
`make edition-check` compares this against `../ground-game` when that

View file

@ -0,0 +1,10 @@
token_id,name,quantity,sides,shape,size,symbol_id,front_text,use
TOK_STRESS,Stress marker,6,Single,Cube or disc,1012 mm,SYM_STRESS,None,One per player; move on Stress 05 track.
TOK_FREEDOM,Freedom token,6,Double,Disc,18 mm,SYM_FREEDOM,READY,Spend to choose any action at Stress 45; GROUND—GR or Bond Support readies it.
TOK_DARVO,DARVO stage marker,6,Single,Arrow pawn or disc,1418 mm,SYM_DENY,DARVO,One per player; move along OFF/DENY/ATTACK/REVERSE track.
TOK_LINK,Relation link token,12,Single,Small disc,14 mm,PLAYER_SYMBOL,Player symbol,Two per player; one is placed at each endpoint of a Bond/Rivalry tile.
TOK_FOCUS_BLAME,Focus / Blame token,6,Double,Disc,18 mm,SYM_FOCUS_BLAME,FOCUS,"At DARVO Attack, place Focus by the target. At Reverse, flip to Blame. Each Blame is 1 personal score."
TOK_PROTECTION,Protection token,6,Single,Shield or disc,18 mm,SYM_PROTECTION,PROTECTION,"Cancel the next Attack against the holder, then return the token."
TOK_DENIED,Denied token,5,Single,Bar or disc,18 mm,SYM_DENIED,DENIED,Place on a Problem turned face down by Deny. Remove when GROUND—OU restores the Problem.
TOK_LEAD,Lead marker,1,Single,Pawn or large disc,22 mm,SYM_LEAD,LEAD,"Starts with a random player, breaks same-step ties, and rotates clockwise after each round."
TOK_ROUND,Round marker,1,Single,Small disc,14 mm,SYM_LEAD,ROUND,Move along the 15 track printed on the active Scenario card.
1 token_id name quantity sides shape size symbol_id front_text use
2 TOK_STRESS Stress marker 6 Single Cube or disc 10–12 mm SYM_STRESS None One per player; move on Stress 0–5 track.
3 TOK_FREEDOM Freedom token 6 Double Disc 18 mm SYM_FREEDOM READY Spend to choose any action at Stress 4–5; GROUND—GR or Bond Support readies it.
4 TOK_DARVO DARVO stage marker 6 Single Arrow pawn or disc 14–18 mm SYM_DENY DARVO One per player; move along OFF/DENY/ATTACK/REVERSE track.
5 TOK_LINK Relation link token 12 Single Small disc 14 mm PLAYER_SYMBOL Player symbol Two per player; one is placed at each endpoint of a Bond/Rivalry tile.
6 TOK_FOCUS_BLAME Focus / Blame token 6 Double Disc 18 mm SYM_FOCUS_BLAME FOCUS At DARVO Attack, place Focus by the target. At Reverse, flip to Blame. Each Blame is −1 personal score.
7 TOK_PROTECTION Protection token 6 Single Shield or disc 18 mm SYM_PROTECTION PROTECTION Cancel the next Attack against the holder, then return the token.
8 TOK_DENIED Denied token 5 Single Bar or disc 18 mm SYM_DENIED DENIED Place on a Problem turned face down by Deny. Remove when GROUND—OU restores the Problem.
9 TOK_LEAD Lead marker 1 Single Pawn or large disc 22 mm SYM_LEAD LEAD Starts with a random player, breaks same-step ties, and rotates clockwise after each round.
10 TOK_ROUND Round marker 1 Single Small disc 14 mm SYM_LEAD ROUND Move along the 1–5 track printed on the active Scenario card.

View file

@ -0,0 +1,165 @@
# CB-EV-0027 — the tokens on the table
CB-WP-0029 T04. Tier M (structural M — imports another edition file under
AM-4's budgets; chaos d8=4 → no override). **Declaration 12 of chaos
window 2 — the last.** Closed 2026-08-07.
**Delivered:** [ADR-0016](../decisions/ADR-0016-the-tokens-on-the-table.md),
`Tokens.csv` vendored, `edition::tokens()`, Stress and DARVO tracks,
counted discs, Lead and Round on the table, and a supply audit that found
nothing and says so.
---
## 1. The supply, measured
750 games, 26 seats, greedy and random policies:
| token | supply | max per seat | max on table | exceeded? |
|---|---:|---:|---:|---|
| Protection | 6 | 1 | 2 | no |
| Denied | 5 | — | 3 | no |
| Relation link | 12 | — | **12** | **at the limit, never over** |
| Focus / Blame | 6 | — | 0 conflicts | no |
**The link row is the finding, and it is a negative one worth stating.**
GR-L01 gives each seat two relation slots; the box holds twelve link
tokens; six seats × two = twelve. **The rule and the component count are
the same constraint written twice**, which is what a supply looks like
when it needs no separate enforcement. Nothing was designed to make that
true — it fell out.
**Protection is the one that could have gone wrong and did not.**
`saturating_add(1)` at `lib.rs:986` has no upper bound, so the type
permits a seventh token. Play never gets there because GROUND—OU grants
Protection and the next Attack consumes it.
## 2. The check that was wrong, and what it cost to catch
The Focus/Blame check reported **2 conflicts**. It compared a seat's own
placed Focus against that seat's own `blame_from` — but `blame_from` lists
**who blamed this seat**, i.e. *other players'* discs. Different tokens.
Corrected: **0**.
**Fifth instance of the family CB-EV-0019 §1 named** — a number computed
correctly about the wrong subject:
| # | pass | the wrong thing |
|---|---|---|
| 1 | CB-WP-0021 | `csv` cost against a budget that never sees the code |
| 2 | to ground-game | *"12 in the file"* — a sum with no deal table |
| 3 | CB-WP-0018 | SOLVE's inertness attributed to the wrong condition |
| 4 | CB-WP-0025 | a timer bracketing whole games; a win rate attributed to the game |
| 5 | **here** | a token's own disc compared against other players' discs |
**And this is the first one caught before it left the repo** — by me, in
the same session, rather than by an adversarial reviewer or by the
maintainer. Four of the previous five reached a document or another
repository.
**That is one data point, not a trend.** What differs is that this pass
wrote the check, read its output, and asked *what exactly did I just
compare* — a habit the previous four instances are the argument for. The
family still has **no control**; `facts-check` catches copies that
disagree and nothing catches a correct computation over the wrong subject.
## 3. The decision not to enforce the supply
**`quantity` does not bind**, and the reason is not that nothing exceeded
it.
No numbered rule in `GroundRules.md` mentions a token supply. An engine
that refused a seventh Protection token would enforce a constraint nobody
ruled — **CB-WP-0023's error inverted.** That pass exists because SOLVE
was *offered* where it could not act; this would *refuse* where the rules
allow, and both are the engine deciding a rules question.
So the check ships as a standing control. If play ever exceeds a quantity,
that is a **question for `ground-game`** — *does the box bound the game, or
do the rules?* — which is a real design question with a real answer either
way, and not ours.
Registered as **F22, withdrawn**: a stated negative. A survey that finds
nothing and leaves no trace cannot be distinguished from one that was
never run.
## 4. What the tracks are for
`stress 4` is a fact you read. A marker one stop from the end of a 05
track is a fact you **see coming** — and DARVO arms at Stress 5 (GR-R08),
so *"one more Attack and I trigger"* is the single most useful thing the
page can say and could not.
Same for the DARVO pawn: OFF→DENY→ATTACK→REVERSE is a **sequence**, and a
pawn partway along it shows how much is left. `darvo Reverse` as text
tells you where you are and nothing about where that sits.
**Unverified.** Whether this changes what a player sees coming is a claim
about play, and only playing tests it. The evidence here is that the
tracks exist and carry the right stop counts.
## 5. Two fixture defects, and a limit corrected
**`seat_centres` matched every `<circle>`.** Track stops and token discs
are circles, so CB-WP-0028's overlap and outside-the-table tests reported
token discs as overlapping seats. Seats now carry `class="seat"` and the
helper keys on that. **Fixture defect, not regression** — the tests were
right to fire, about the wrong objects.
**The table height limit went 460 → 500.** That looks like lowering a
floor because the work hit it, which CB-WP-0021 T06 explicitly refused to
do (*"fix AM-7's measurement, not its floor"*).
**It is the opposite, and the distinction is the point.** 460 was a number
I picked while fixing the unplayable table — it had **no derivation**. 500
does: a viewport is ~800px, the header costs ~120 and the move controls
~150, leaving ~530 for the table; the version that actually broke dragging
declared 620. **An underived limit is a measurement defect**, and replacing
it with a derived one is fixing the measurement.
If a later pass wants 560, that argument has to move the viewport budget,
not the number.
## 6. Chaos window 2 — closed, and its verdict is due
**This is declaration 12 of 12.** The window opened 2026-08-03 at d8.
**Twelve declarations, zero 8s, zero overrides.** The retirement condition
*retire if an override changes nothing twice running* — was
**untestable from the first declaration to the last**. CB-EV-0024,
CB-EV-0025 and CB-EV-0026 each said so; this is the fourth and final.
| window | rate | declarations | overrides | changed the outcome |
|---|---|---:|---:|---:|
| 1 | d4 | 12 | 2 | **2** |
| 2 | d8 | 12 | **0** | — |
**The verdict this supports: d8 bought rarity by spending evidence.** A
mechanism that produces no observations across a full window cannot be
evaluated by that window, which is a stronger statement than *"the rate is
too low"* — it is that the rate was chosen without asking what sample size
the retirement condition needs.
**Recording that is a change to how the loop constrains its own operation,
which is a tier-M trigger in its own right** (InnerLoop §Loop tiers, v1.6).
It is therefore **outstanding, not done here**, and is named in the
workplan so it is not lost between passes — which is exactly how
`ground-game`'s ten rulings went uncollected for two days.
## 7. Cost
CB-WP-0028's cost, by re-running the instrument: `make cost`. Not inlined
(§Single source of fact). CB-EV-0019 §4's unbounded chain is still
unbounded, now across four more passes.
## Open after this pass
- **The window-2 verdict** (§6) — the next thing this loop owes itself.
- **The wrong-denominator family still has no control** (§2), at five
instances.
- **Whether the tracks change what a player sees coming** (§4) is untested
and only play tests it.
- **`Relations`, `DARVO`, `Player_Mats` and `Glossary` remain unvendored.**
The DARVO track now has a picture but not the edition's own words for
each stage.
- **Three of four scenarios still never dealt**, carried from ADR-0015 D5.

View file

@ -71,6 +71,7 @@ const CSV: &str = include_str!("../../../editions/ground-darvo-r0/Problems.csv")
const ACTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Actions.csv");
const SOLUTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Solutions.csv");
const MODES_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Modes.csv");
const TOKENS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Tokens.csv");
/// A vendored CSV, parsed into rows addressable by column name.
///
@ -148,6 +149,60 @@ fn card_texts(csv: &str, what: &str, id: &str, tag: &str) -> Result<Vec<CardText
Ok(out)
}
/// One component, as the edition prints it (ADR-0016 D1).
///
/// **A token is a view, not a type** (D2): nothing in the aggregate
/// changes shape. This supplies the renderer with the game's own labels
/// and the counts a supply check needs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenSpec {
pub id: String,
pub name: String,
/// How many the box holds. **Not a rule** — no numbered rule mentions
/// a supply, and the engine does not enforce one (D3).
pub quantity: u32,
/// `Double` means one physical disc that flips — Freedom (READY /
/// spent) and Focus/Blame. Two states of one component, not two.
pub double_sided: bool,
/// What is printed on it: READY, DENIED, PROTECTION, FOCUS…
pub front_text: String,
/// The rule, in the edition's words.
pub use_text: String,
}
/// Every token the edition ships.
pub fn tokens() -> Result<Vec<TokenSpec>, String> {
let t = Table::parse(TOKENS_CSV, "Tokens.csv")?;
let mut out = Vec::new();
for row in &t.rows {
out.push(TokenSpec {
id: t.get(row, "token_id")?.to_string(),
name: t.get(row, "name")?.to_string(),
quantity: t
.get(row, "quantity")?
.parse()
.map_err(|_| "quantity is not a number".to_string())?,
double_sided: t.get(row, "sides")? == "Double",
front_text: t.get(row, "front_text")?.to_string(),
use_text: t.get(row, "use")?.to_string(),
});
}
if out.is_empty() {
return Err("Tokens.csv has no rows".into());
}
Ok(out)
}
/// How many of `id` the box holds. `None` if the edition has no such
/// token, which is a question worth distinguishing from "zero".
pub fn supply(id: &str) -> Option<u32> {
tokens()
.ok()?
.into_iter()
.find(|t| t.id == id)
.map(|t| t.quantity)
}
/// The five Action cards, in the edition's own words.
pub fn actions() -> Result<Vec<CardText>, String> {
card_texts(ACTIONS_CSV, "Actions.csv", "action_id", "tagline")
@ -417,3 +472,107 @@ mod card_text_tests {
);
}
}
#[cfg(all(test, feature = "scenarios"))]
mod supply_tests {
use super::*;
/// **CB-WP-0029 T03: does play ever exceed what the box holds?**
///
/// Measured over 750 games (26 seats, greedy and random): it does
/// not. This runs a smaller sweep as a standing control, so a future
/// change that starts minting components fails here instead of being
/// noticed by a player.
///
/// **A violation is a FINDING, not a bug to fix by adding a bound**
/// (ADR-0016 D3). No numbered rule mentions a supply; the engine
/// enforcing one would be inventing a rule, which is CB-WP-0023's
/// error inverted. If this goes red, the question goes to
/// `ground-game`: does the box bound the game, or do the rules?
#[test]
fn play_never_exceeds_the_components_the_box_holds() {
use crate::bot::{play, GreedyPolicy, Policy, RandomPolicy};
use cb_game_runtime::{ScenarioGame, Setup};
let protection = supply("TOK_PROTECTION").expect("the edition ships Protection");
let denied = supply("TOK_DENIED").expect("the edition ships Denied");
let link = supply("TOK_LINK").expect("the edition ships link tokens");
for players in [2u8, 4, 6] {
for seed in 0..25u64 {
let Ok(state) = crate::GroundState::setup(
&Setup {
players,
preset: format!("standard-{players}p"),
patch: Default::default(),
},
seed,
) else {
continue;
};
let mut ps: Vec<Box<dyn Policy>> = (0..players)
.map(|i| {
if seed % 2 == 0 {
Box::new(GreedyPolicy) as Box<dyn Policy>
} else {
Box::new(RandomPolicy::new(seed ^ u64::from(i))) as Box<dyn Policy>
}
})
.collect();
let Ok(g) = play(state, &mut ps) else {
continue;
};
let s = &g.state;
let on_table: u32 = s.players.values().map(|p| u32::from(p.protection)).sum();
assert!(
on_table <= protection,
"{players}p seed {seed}: {on_table} Protection tokens in play, \
the box holds {protection}"
);
let d = s.problems.values().filter(|q| q.denied).count() as u32;
assert!(
d <= denied,
"{d} Denied tokens in play, the box holds {denied}"
);
// Two link tokens per relation, one at each endpoint.
let l = (s.relations.len() * 2) as u32;
assert!(l <= link, "{l} link tokens in play, the box holds {link}");
// One double-sided disc per player: it is Focus-side-up
// somewhere, or Blame-side-up somewhere, never both.
//
// The first version of this check compared a seat's own
// Focus against its OWN blame_from -- but blame_from lists
// OTHER players' discs, so those are different tokens. It
// reported conflicts that did not exist.
for owner in s.players.keys() {
let as_focus = s.focus.contains_key(owner);
let as_blame = s.players.values().any(|q| q.blame_from.contains(owner));
assert!(
!(as_focus && as_blame),
"{owner:?}'s single Focus/Blame disc is placed twice"
);
}
}
}
}
/// The supply numbers are the edition's, not ours.
#[test]
fn the_supply_comes_from_the_edition() {
let t = tokens().expect("Tokens.csv parses");
assert_eq!(t.len(), 9, "the edition ships nine token types");
assert_eq!(supply("TOK_LINK"), Some(12), "two per player at six seats");
assert!(
TOKENS_CSV.contains(&tokens().expect("t")[0].use_text),
"the rule text is not a substring of the vendored file — it was invented"
);
// `sides` must be able to say both, or the flag means nothing.
assert!(
t.iter().any(|x| x.double_sided),
"Freedom and Focus/Blame flip"
);
assert!(t.iter().any(|x| !x.double_sided), "most tokens do not");
}
}

View file

@ -49,6 +49,7 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
| F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg |
<!-- design-register:end -->
@ -78,6 +79,21 @@ 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*.
- **F22 — does the component supply bind the game? Asked, measured, and
withdrawn.** `Tokens.csv` gives every component a `quantity` — 6
Protection, 5 Denied, 12 relation links — and **no numbered rule
mentions a supply**, so the engine could in principle mint a seventh
Protection token (`saturating_add`, no bound, `lib.rs:986`). Measured
over **750 games**, 26 seats, greedy and random: Protection reaches 1
per seat and 2 on the table; Denied 3 of 5; links **exactly 12 of 12**,
never over, because GR-L01's two-slot rule *is* the twelve-token supply
written twice. Focus/Blame: 0 conflicts. **Withdrawn — nothing to
report.** Registered as a stated negative because a survey that finds
nothing and leaves no trace cannot be told from one that was never run.
The check ships as a standing control (ADR-0016 D3): if play ever does
exceed a quantity, that is a **finding for ground-game** — *does the box
bound the game, or do the rules?* — and **not** a bound for the engine to
invent.
- **F21 — dragging did not work until after the first note was saved.**
Reported 2026-08-06: *"I could not drag and drop at the beginning but
after I added the first comment it worked."* **`note`, and I could not

View file

@ -8,7 +8,7 @@ ruled: ''
ruled_by: ''
ruled_note: ''
encodes_u_item: ''
seed: 1
seed: 2
setup:
players: 3
preset: standard-3p
@ -17,8 +17,7 @@ commands:
- actor: P1
cmd: select_action
args:
action: INVESTIGATE
problem: 2
action: GROUND
- actor: P2
cmd: select_action
args:
@ -32,6 +31,36 @@ commands:
- actor: SYSTEM
cmd: reveal
args: {}
- actor: P1
cmd: choose_ground_mode
args:
choice: protect_problem
mode: OU
problem: 1
- actor: SYSTEM
cmd: resolve
args: {}
- actor: SYSTEM
cmd: end_round
args: {}
- actor: P1
cmd: select_action
args:
action: INVESTIGATE
problem: 2
- actor: P2
cmd: select_action
args:
action: INVESTIGATE
problem: 2
- actor: P3
cmd: select_action
args:
action: INVESTIGATE
problem: 2
- actor: SYSTEM
cmd: reveal
args: {}
- actor: SYSTEM
cmd: resolve
args: {}
@ -46,13 +75,13 @@ commands:
- actor: P2
cmd: select_action
args:
action: INVESTIGATE
problem: 3
action: SOLVE
problem: 2
- actor: P3
cmd: select_action
args:
action: INVESTIGATE
problem: 3
action: SOLVE
problem: 2
- actor: SYSTEM
cmd: reveal
args: {}
@ -66,7 +95,7 @@ commands:
cmd: select_action
args:
action: SOLVE
problem: 2
problem: 3
- actor: P2
cmd: select_action
args:
@ -75,31 +104,7 @@ commands:
- actor: P3
cmd: select_action
args:
action: SOLVE
problem: 2
- actor: SYSTEM
cmd: reveal
args: {}
- actor: SYSTEM
cmd: resolve
args: {}
- actor: SYSTEM
cmd: end_round
args: {}
- actor: P1
cmd: select_action
args:
action: SOLVE
problem: 3
- actor: P2
cmd: select_action
args:
action: SOLVE
problem: 3
- actor: P3
cmd: select_action
args:
action: SOLVE
action: INVESTIGATE
problem: 4
- actor: SYSTEM
cmd: reveal
@ -114,25 +119,21 @@ commands:
cmd: select_action
args:
action: SUPPORT
target: P2
target: P3
- actor: P2
cmd: select_action
args:
action: SUPPORT
target: P1
action: SOLVE
problem: 4
- actor: P3
cmd: select_action
args:
action: SUPPORT
target: P1
action: SOLVE
problem: 4
- actor: SYSTEM
cmd: reveal
args: {}
- actor: P1
cmd: respond_to_support
args:
response: accept_bond
- actor: P2
- actor: P3
cmd: respond_to_support
args:
response: accept_bond
@ -146,4 +147,4 @@ expect:
events: []
state: {}
rejects: []
state_hash: 15e28280a256249f4ebddbeb638053d0683d996b7ff1193128a7d788f32f63ec
state_hash: f227bb9e0c62f91735508514b5f0d51f6d59b48b68619b9ae641d25f191aa01b

View file

@ -2,7 +2,8 @@
id: CB-WP-0029
kind: product
title: "The tokens on the table: components you can count, and a supply the engine may not respect"
status: ready
status: done
state_hub_workstream_id: "fb3d3c80-3798-4d0f-91ce-b8ea45943328"
---
# Purpose
@ -70,8 +71,9 @@ went uncollected.
```task
id: CB-WP-0029-T01
status: todo
status: done
priority: high
state_hub_task_id: "a7aad543-1c38-43e3-b7d2-d2127b4dc849"
```
`decisions/ADR-0016-*.md` (tier M merges survey and decision).
@ -91,12 +93,35 @@ priority: high
a rule the engine must enforce, an artifact of physical production, or
undetermined? **Do not answer it here from taste** — T03 measures first.
**Done 2026-08-07.**
[ADR-0016](../decisions/ADR-0016-the-tokens-on-the-table.md), four
decisions, **written after T03 measured**.
**D2 — a token is a VIEW, not a type.** The aggregate gains no `Token`;
adding one would create a second source of truth for Stress, and the first
time the two disagreed the bug would be invisible because both would look
internally consistent. `Tokens.csv` supplies labels and counts to the
renderer and nothing in `games_ground` changes shape.
**D3 — `quantity` does not bind, and the reason is not the measurement.**
A component limit the rules do not state is not a rule. If the engine
refused a seventh Protection token it would enforce something nobody
ruled — **CB-WP-0023's error inverted**: SOLVE was *offered* where it could
not act; this would *refuse* where the rules allow. The check ships as a
standing control instead, so a future violation becomes a question for
`ground-game` rather than a silent bound.
**D4 — the tracks are the decision that matters.** `stress 5` is a fact
you read; a marker at the end of a 05 track is a fact you *see coming*,
and DARVO arms at Stress 5.
## Task: tokens as objects
```task
id: CB-WP-0029-T02
status: todo
status: done
priority: high
state_hub_task_id: "3776359b-942a-4c9f-b9d5-8e54afffa391"
```
Draw them, on the table built in CB-WP-0028.
@ -121,12 +146,29 @@ Draw them, on the table built in CB-WP-0028.
- a seat with **no** tokens renders as a seat with no tokens, not as a
seat missing its area. The empty case is the one that silently vanishes.
**Done 2026-08-07.** Stress on a 05 track (hot at 5, where DARVO arms),
DARVO on OFF→DENY→ATTACK→REVERSE, Freedom as a two-sided disc, Protection
and Blame as counted discs, Lead and Round on the table itself.
**Two tests broke and both were fixture defects, not regressions.**
`seat_centres` matched *every* `<circle>`, and track stops are circles —
so it reported token discs as overlapping seats. Seats now carry
`class="seat"` and the helper keys on that.
**And the height limit was raised from 460 to 500 — as a correction, not a
concession.** The 460 had no derivation. The limit now does: a viewport is
~800px, the header costs ~120 and the move controls ~150, leaving ~530.
The version that broke dragging declared 620. **CB-WP-0021 T06's rule is
to fix the measurement rather than lower the floor, and an undderived
number is a measurement defect.**
## Task: does the engine respect the supply?
```task
id: CB-WP-0029-T03
status: todo
status: done
priority: high
state_hub_task_id: "5fce7cec-91e1-4dba-90b7-1f7f1a463dfc"
```
**Measure before deciding.** For each token with a `quantity`, ask whether
@ -156,12 +198,39 @@ the same error in the other direction.
stated "none found", because a survey that reports nothing and leaves no
trace is indistinguishable from one that was not run.
**Done 2026-08-07. Nothing found — and the first version of the check was
wrong.**
750 games, 26 seats, greedy and random:
| token | supply | reached | verdict |
|---|---:|---|---|
| Protection | 6 | 1/seat, 2/table | not exceeded |
| Denied | 5 | 3 | not exceeded |
| Relation link | 12 | **12** | exactly at the limit, never over |
| Focus/Blame | 6 | 0 conflicts | consistent |
**The link row is the interesting one**: GR-L01's two-slot rule *is* the
twelve-token supply written twice, which is the shape of a supply needing
no separate enforcement.
**The Focus/Blame check reported 2 conflicts and was wrong.** It compared
a seat's own placed Focus against its *own* `blame_from` — but that list
holds **other players'** discs, so they are different tokens. Corrected: 0.
**Fifth instance of this project's recurring defect** — a number computed
correctly about the wrong subject — and the first caught before it left
the repo.
Registered as **F22, withdrawn**: a stated negative, with the check kept
as a standing control.
## Task: evidence
```task
id: CB-WP-0029-T04
status: todo
status: done
priority: medium
state_hub_task_id: "29858777-6f8f-4d8e-90d0-cb193b7f4d29"
```
`evidence/CB-EV-0027-*.md`.
@ -174,3 +243,22 @@ priority: medium
- **The window-2 verdict is due** (§The window closes here) — name it as
outstanding if the closing pass has not run.
- **Quote CB-WP-0028's cost by re-running the instrument.**
**Done 2026-08-07.**
[CB-EV-0027](../evidence/CB-EV-0027-the-tokens-on-the-table.md).
- **The supply is never exceeded**, and the link row explains why a supply
can need no enforcement: GR-L01's two slots per seat *are* the twelve
tokens, the same constraint written twice.
- **The Focus/Blame check was wrong and I caught it** — fifth instance of
the wrong-subject family, and **the first caught before leaving the
repo**. One data point, not a trend; the family still has no control.
- **The height limit went 460 → 500 as a correction.** 460 had no
derivation; 500 does. An underived limit is a measurement defect, and
replacing it is CB-WP-0021 T06's rule applied, not evaded.
- **Whether the tracks change what a player sees coming is untested** and
only play tests it.
- **Chaos window 2 is closed: twelve declarations, zero overrides.** Its
retirement condition was untestable from first to last. **The verdict is
the next thing this loop owes itself**, and it is a tier-M pass because
recording it changes how the loop constrains its own operation.