T02 — all chance derives from one root seed. Three chance points, all
reading it: the setup deck shuffle, the setup Lead draw, and the reshuffle
permutation. The Problems deal is not chance at all. So in extensive-form
terms the tree has a single chance node at the root.
That test was wrong first, and the mutation caught it. It compared state
hashes — and GroundState carries `seed` as a field, so "different seeds
differ" was true by construction. Mutating the shuffle away left it green.
It now compares the dealt configuration, and the same mutation fails it: a
wrong-subject error inside the control written for T02.
The reshuffle is a pure function of (seed, round) because K5 requires
deterministic replay, where a real table reshuffles independently. That is
a modelling restriction, not a defect, and it is now pinned.
T03 — commit/reveal checked in both directions: before Reveal each seat
sees its own selection and no other; after Reveal the information sets
merge, because an encoding that hides forever is not commit/reveal either.
T04 — ADR-0020 refuses the EFG port, and the blocker is T02 rather than
T01, which inverts what the workplan expected. Perfect recall looked like
the risk and is a constraint with a known answer: key on observation
histories. Making chance explicit is the expensive one — the reshuffle
would become a real chance node and break the K5 purity that every
recording, replay bundle and trial-note hash depends on. A port would
trade the property this project is built on for one it has never needed.
Track B's first move is therefore a question, not a build: take "is
exploitability meaningful for a co-operative game with a shared threshold"
to OpenSpiel on a toy model, where answering it costs nothing. D4 states
what being wrong looks like — OpenSpiel settling on a toy what three
rounds of policy sweeps could not — and makes watching for it the next
action.
Taxonomy §4.1 records the EFG correspondence with the test that checks
each row, so a later pass starts from a specification rather than a memory.
Chaos window 4 at three declarations. Window 3's verdict is now two
windows behind and should be evaluated rather than restated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The answer is "it depends what you call an information set", and the
distinction is the result. 44,938 decision points, random play, 2/3/4/6
seats.
Reading A — information set = the seat's current projection, which is what
project(Viewer::Player(seat)) returns and what the page renders: 22
violations. Reading B — information set = the seat's observation history,
every view seen and action taken in order: 0.
The Reading A witness is concrete. Two histories reach a byte-identical
view — round 3, Select step, same hand, same claimed Problem — where the
seat had played SOLVE then GROUND-OU(protect) in one and SUPPORT then
SOLVE in the other. The view does not tell the seat what it did, because
our state is a snapshot rather than a history: selections clear each round
and effects coincide, so a player cannot reconstruct their own past from
the present. In a real game the player's memory supplies it; in the state,
nothing does.
That is precisely OpenSpiel's ObservationString vs InformationStateString
split, arrived at here by measurement rather than read off. project() is
an observation, not an information state.
So Track B is not closed, it is constrained, and usefully: an
extensive-form game built from this engine must key information sets on
observation histories, never on project().
Both directions are asserted — Reading B empty AND Reading A non-empty —
because if the sample stops finding Reading A violations the conclusion is
unsupported and must be re-derived rather than quietly kept. And the check
samples, so it can falsify perfect recall and cannot establish it: Reading
B's zero means no counterexample was drawn, which is printed as such.
Wired into make panels, so it is re-derived by the gate rather than by
hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pattern is now measured over three rounds: 5 fatal, then 3 (2 from the
previous round's corrections), then 4 (3 from them). The corrections are
not getting safer.
FATAL 1: round 2's short-cell assertion went into regulation.rs only.
attack-value.rs — which produced every number in CB-EV-0030's DARVO table
— still just warned, and the gate registered to close the finding claimed
the property for both.
FATAL 2, the sharpest of the three rounds: counting games proves they
STARTED. Stopping the engine after one round gives 200 games, all-zero
columns and exit 0 — byte for byte the signature CB-EV-0030 says the
instrumentation distinguishes from a real result. Both harnesses now
require every counted game to have reached an outcome over five rounds.
FATAL 3: round 2's `.csv` filter was applied to all three loops, so
catalog.yaml and rules_delta.yaml — whose missing digests were round 1's
finding — were recorded and then never compared, and never checked against
upstream at all. Only the parser loop filters now.
FATAL 4: five of six tiebreak comparators had no coverage. GR-E04's
tiebreak never executes in any scenario. All four are now covered and
mutation-verified; the Blame key needed compensating claims to be
reachable at all, since Blame also lowers the coalition score.
SERIOUS: "peak held" computed the same number as "peak assigned" for every
possible input — the real gap was that START_STRESS was an unchecked
constant, now read off the dealt state; cadence="none" was a pure
loophole, removed; sibling discovery swapped a hand-written list for
hand-written globs and missed metadata.json and VARIANT.md, both named in
the package's own changed_files — now walked, and it found them
immediately; and "~72,000 games" was unsourced, make panels runs 17,600.
Also separated two kinds of number that were presented alike: seats×games
is invariant, 363 and 29 vary 7.1%-11.5% across samples.
Round 4 owed. The conclusion is not that the work is nearly right — it is
that author-made corrections to measurement work should be assumed
defective until a fresh reader has attacked them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three FATAL, five SERIOUS. The substance of round 1's corrections held —
Reactive is genuinely one arm different, the five replacement controls are
non-inert, the inert metric is right, the numbers reproduce. What failed
were the CLAIMS about them, and two defects the corrections introduced.
FATAL 1: the fix for round 1's #11 did not fix it. The assertion was
`games + setup_fails == 200`, and a refused setup increments setup_fails
while skipping games — so the sum is invariant under exactly the failure
it claimed to catch. Injecting setup failures gave exit 0 over 196-game
columns. Now asserts games == GAMES, verified to exit 101.
FATAL 2: the correction to the selective-column FATAL was itself
selective. "81-1000 per cell, baseline AND H1" and "31-1000" twelve lines
apart, both taken from the baseline row; under H1 rank-75 arms are
59/0/0/0. Every cell is now printed rather than summarised, and the
corrected verdict is the opposite of the one it replaced: under rank-75,
H1 REDUCES DARVO arms to zero at 3p and above.
FATAL 3: "DARVO arms 2 per seat per game" is 1 per seat per game, exactly,
at every band.
SERIOUS: the tiebreak oracle asserted only that the winner set CHANGED, so
reversing the tiebreak left it green; the #13 defect's impact was claimed
and never measured (72,000 games: zero divergences — real in principle,
witnessed only by a constructed board); a 29-of-363 citation pointed at a
file that did not contain it (round 1's reviewer did report it, and it was
never transcribed — the record was wrong, not the number); the harnesses
were run by NO GATE, so every published figure came from a manual run of
an ungated binary, including the assertion added for #1; and edition-check's
sibling handling — added by the last correction — was self-certifying,
crashed instead of failing, and counted Markdown lines as coverage. Now
discovered on disk, and it found a real gap on its first run: Rules_Text.csv
vendored with no digest.
Also: "peak Stress held" was dead code kept quiet by `let _ = held;` — the
numbers were right by coincidence.
make panels is now a registered gate. Round 3 is owed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
H1-B on the DARVO extra Attack: the delta says the extra Attack shares
the Attack resolution "so it can self-soothe too if Stress >= 4".
CB-WP-0038 asserted it because the code shares resolve_attack; nothing
tested it, and CB-EV-0031's withdrawn mechanism story ran through that
exact path. Now tested and mutation-verified.
Round-5 pressure did not reach the score, and this was a real defect
rather than a reporting one. end_round_events scored from `self` while
H1-A's pressure went into `work`, and score() reads Stress for the GR-E03
and GR-E04 tiebreaks — so the final round's pressure was invisible to the
two modes CB-EV-0030 reports on. Fixed. The test uses the case that bites:
uniform pressure preserves an ordering, so it takes the clamp at 5 to
collapse a gap and change who wins.
Inert arms reported separately: a DARVO arm at the End of Round 5 can
never advance a stage, and criterion 1 is about DARVO mattering. 29 of 363
at 2p, none above — matching the reviewer's independent figure, so
criterion 1 stands as met.
That fix produced one more wrong-subject error, caught before reporting:
the first inert-arm metric tested `g.rounds >= 5`, a property of the GAME
rather than the EVENT, so it marked every arm in every completed game
inert and briefly read as "criterion 1 fails after all". An arm is inert
when no RoundEnded follows it.
regulation.rs no longer skips setup failures silently: they are counted,
and a short cell fails an assertion rather than printing a number a reader
has to notice — which is the credit CB-EV-0030 §3 took and half earned.
All thirteen challenges closed. Re-review is owed before any of this
travels: the corrections were made by the author of the errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thirteen challenges, five FATAL, all five conceded. Nothing had reached
ground-game, which is the only reason this is a correction and not a
retraction.
The worst: `Reactive` was not "greedy with one preference changed". It
differed in five, including SpendFreedom — ranked 95 unconditionally
against greedy's `95 if gated else 0` — so the seat burned its Freedom
token in round one of every game. A second change to the exact mechanism
under study, and every number in CB-EV-0031 was measuring it. The pass
claimed ADR-0018's one-varying-parameter discipline in its own workplan
while violating it. GreedyPolicy::rank is now public and the policy
delegates, overriding one match arm, so the control is structurally true.
Withdrawn entirely: "H1-B suppresses DARVO in the attacker". Disabling
H1-B under the corrected policy changes the arm count by exactly zero.
The pass hedged the wrong variable — it disclaimed "the number 2" and
defended "the direction", and the direction is what failed. The
supporting inference was invalid anyway: final Stress cannot show who
armed, because DarvoEnded resets the stage and REVERSE gives its owner -2.
Corrected: criterion 1 was failed on the greedy column while the pass's
own printed table showed 31-1000 arms in the other columns — the
selective-column move, in the file that names it. "Peak Stress was 1" was
a maximum over StressSet payloads, not held state (true: 2); the baseline
game count was 1,600 not 3,200; and "a reckless policy plays identically
to a careful one" is refuted by this repo's own rank-95 policy.
Inert controls replaced, each verified red against the reviewer's own
mutation: the baseline hash test compared two identically-constructed
states (serde(skip) on variant left 57/57 green); the `unchanged:` test
checked 3 of 7 entries and passed with SOLVE made illegal; H1-A's ordering
and H1-B's OU-cancel path had no test at all.
edition-check now covers catalog.yaml and rules_delta.yaml, whose digests
CB-WP-0038 claimed and never recorded — the review found it and reported
it unverified rather than absent, which was the right call.
Still open: H1-B on the DARVO extra-Attack path is untested, regulation.rs
still skips setup failures silently, and round-5 arms are counted though
they can never act.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CB-EV-0030 concluded H1's DARVO arm rate was still 0. That was true of the
panel, and the panel was greedy-family throughout. GreedyPolicy ranks
`Ground if gated => 100`, so it grounds the instant the stress gate bites,
Stress plateaus at 3, and the arm at 5 is unreachable by construction. "H1
does nothing" was really "H1 does nothing to a seat that already manages
its Stress" — and H1 was written for the seat that does not.
`reactive` is greedy with exactly one preference changed: GROUND demoted
below ATTACK. Under it, H1's criteria 1 and 2 are MET — DARVO arms 400
times per cell, ATTACK is chosen 3 times per seat per game. Criterion 3
fails harder: reactive wins nothing at any seat count.
The larger finding is about the baseline. Greedy and reactive play
IDENTICALLY under baseline, and peak Stress across 3,200 baseline games
was 1 — against a starting value of 2. The gate at 4, the DARVO arm at 5
and the Freedom token are all unreachable, and a policy built to be
reckless with Stress is indistinguishable from one built to husband it.
That is a deeper account of F17 than F17 has. Not raised as a finding yet:
it wants the plural panel first.
A constant was investigated rather than reported: darvo was exactly 400 in
every cell while atk scaled with seats. Six-player final Stress is
[5,5,4,4,4,4] every seed — H1-B holds the attacker at 4, below the arm,
and pushes its targets to 5. The self-soothe suppresses DARVO in the
aggressor and concentrates it in the attacked. The direction follows from
H1-B's arithmetic; the number 2 is partly an artifact of reactive's
first-legal targeting, and is labelled as such.
Still unreviewed: tier L review outstanding on CB-WP-0038, and nothing
here reaches ground-game until it runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading Player_Mats.csv and Glossary.csv — which O4 had forbidden
declaring about until they were read — found a rule. Player_Mats'
choice_rule column IS GR-R03: at Stress 4-5 choose ATTACK or GROUND unless
you spend a ready Freedom token. The engine's stress_gated and
allowed_under_stress_gate match it exactly, and now have a tripwire.
So Ornamentation.md gains §1.3: the unit is a column, not a file.
Classifying Player_Mats as ornamentation would have thrown a rule away
with the colour swatches. A mixed artifact is normal — a player mat is a
rule printed on a decorated card in the box too. O4 is rewritten and
re-grounded from provisional to rule accordingly.
O5, O6, O7 declared with falsifiers. O7's is not theoretical:
back_design_id is unread in four vendored files, and hidden information is
what card backs are for.
CB-EV-0029 answers T04's question per rule rather than as a count — yes,
every checkable clause — and states the reading that result does NOT
support: it shows GroundRules.md was a faithful secondary source for the
clauses with behaviour to check, not that the engine is correct.
10 of 19 files vendored. Nine still unread, and Rules_Text.csv is the one
that matters: every clause checked here was checked against our own
transcription of it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vendored DARVO.csv, Relations.csv and Scenarios.csv — the three unread
files that carry mechanism. The engine has implemented the DARVO sequence
and relation formation/breaking from GroundRules.md since the beginning,
and had never read the game owner's statement of them.
Every checkable clause agrees, and agreement is recorded rather than
noted: a survey that finds nothing and leaves no trace cannot be told from
one never run. The two hardest clauses to notice were already right —
Focus placed "even if the Attack was cancelled", and Focus removed when
the sequence ends before REVERSE.
The tests are tripwires, not derivations. The match was made by a person
reading prose, and that reading goes stale in silence when the prose
changes; each behaviour pins the phrase it was read from, so a reworded
edition goes red and asks for a human. Mutation-proven by rewording the
cancelled-Attack clause.
F25 raised, and it is the real yield: Scenarios.csv carries
threshold_2_players/3_4/5_6, starting_stress and round_track, and the
engine hardcodes all three — a match returning 5/7/9, stress: 2 at setup,
five rounds. They agree on all four scenarios. These are the most
contested numbers in the project; the whole 4/6/9 vs 5/7/9 episode turned
on them, and the engine has been right by maintenance coincidence rather
than by reading the file that owns them.
Also pinned: Problems.csv and Scenarios.csv both state the deal and the
engine reads only the first. They agree; nothing was checking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RULED GROUND-WP-0004 (2026-08-03) asked for gr-e01 to be reinstated as an
import check: sum(point_value dealt) >= threshold FOR EVERY SEAT BAND.
Only the 2p half was done — gr-e01-threshold-reachable-2p covers the
tightest band, and a check at one band tests one value of the variable
that moves, which is the shape GameDesign §1.4 refuses.
every_seat_band_can_reach_its_threshold covers 2/3/4/5/6 and reports a
row-level table with Surface separated from each hidden priority, which
is engine ask 3 and §1.2's ruled shape — "12 in the file" is the wrong
premise that clause was written for.
Result matches ground-game's authoritative table: 6/9/12 available
against thresholds 5/7/9.
Mutation-proven, and the mutation is the original defect: count Surface as
one of the hidden slots and it fails with "2p: available 4 < threshold 5"
— the withdrawn 4/6/9 undercount, reproduced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F18 was the only open finding clay-borg owns, the only register row
lacking a reproduction, and the only off-target metric. It is also
understated: it reads as display data, but among the 14 unvendored files
are DARVO.csv (mandatory_effect, advance), Relations.csv (formation,
breaking) and Scenarios.csv — rules the engine already implements from a
secondary source and has never checked against the primary one.
The reproduction records column reads AT THE ACCESSOR rather than counting
them from the source: a list beside the code would be a second copy of a
fact the get calls already carry, and grepping would over-count because
six column names are shared between vendored files.
The first version was wrong in this repo's signature way — it watched
Table::at only, so it called visibility, required_solution and point_value
unread when the engine reads all three through problems_of's own index
lookups. Correct about the accessor, wrong about the engine: the ADR-0018
family, committed inside the artifact built to measure it. Problems.csv
went 7/13 to 10/13 once the manual reader was recorded too.
F24 raised: solution_deck() is a Rust literal that never opens
Solutions.csv. It agrees today, which is the point — the engine is right
by maintenance coincidence rather than by reading. Role `default`, with a
test that goes red the moment either side moves.
open, lacking a reproduction: 1 -> 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
says
Five games per seat count, 2-6 players. NO ANOMALIES: every game reaches
5 rounds with an outcome, no stalls, no stress above the cap, no
over-claimed Problems.
But the series showed something a crash never would. DARVO NEVER FIRED IN
25 GAMES and stress never exceeded 2. Measured wider: GreedyPolicy plays
ATTACK exactly ZERO times in 10,000 selections across 500 games.
THAT NUMBER IS ABOUT OUR BOT, NOT THE GAME. bot.rs ranks
`Action::Attack => 10`, below everything. Reporting "the game gives no
incentive to attack" from a policy we programmed to rank attack last would
have been CB-WP-0025's C4 error committed again -- a single policy's
behaviour presented as the game's.
So the artifact varies exactly one number: ATTACK's rank in an otherwise
identical policy, 200 games per cell.
rank 10 (below all): 132/165/190/200/200 wins, 0 attacks, 0 DARVO
rank 75 (above SUPPORT): 132/165/190/200/200 wins, 315-923, 13-218
rank 95 (above SOLVE): 0/0/0/0/0 wins, 1400-5170, 400-1000
THE MIDDLE ROW IS THE FINDING. Identical win counts at every seat count,
while attacking hundreds of times and arming DARVO repeatedly. Attacking
is not punished -- it is INERT with respect to the goal. Group success is
a function of SOLVE alone, and ATTACK costs anything only when it ranks
above SOLVE and displaces it.
The maintainer was right and the reason is sharper than his phrasing:
there is no incentive because there is no PATH. ATTACK's effects (Stress,
Rivalry, DARVO) feed nothing that decides group_success.
Bounded honestly to SHARED GROUND. Blame costs PERSONAL score, so ATTACK
may earn its place in GR-E03 and GR-E04 -- which have never been played to
the end (F14), and that is where to ask next. And this is NOT a claim the
game is broken: DARVO is the pattern the game is about not falling into,
so a self-destructive ATTACK may be the design. The question for
ground-game is whether the namesake mechanic being unreachable in
competent co-op play is intended.
F17 promoted from note to raised, with games/ground/examples/attack-value.rs
as its reproduction. Register: 18 findings, 8 with a resolving
reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
ADR-0015 and the import. F18's fix: "I don't understand the GROUND card"
was never a design gap -- the card explains itself in the edition and we
never imported the explanation.
THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of
nineteen". 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. Nobody had said so.
ADR-0011's revisit condition is measurably absent, so the dependency
argument does not get re-run: across Actions, Solutions, Modes and
Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The
hand reader's only job is comma-in-quoted-field, which it already did.
Refusing csv on a measurement rather than on a preference.
Vendored Actions, Solutions and Modes -- the text a player reads. Not the
production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT
Extensions.csv, which names content the designer placed outside the core;
importing it would break the claim that this engine plays the edition as
printed. It is now known to exist, which was the real risk.
One Table reader with four callers, because a per-file copy is how a
parser acquires four subtly different bugs. The GROUND card now shows
"Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand;
Problems show their own titles where a priority number used to be.
The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED
FILE rather than 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.
`edition` came out from behind #[cfg(feature = "scenarios")]. It was gated
because its only consumer was; 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.
And edition-check was written for a single-file world: it compared the
first recorded digest against Problems.csv regardless of which file that
digest described. It now checks every file both ways -- a vendored file
with no digest fails, a digest naming an absent file fails -- and asserts
ADR-0015 D3's falsifier directly rather than trusting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T06. games/ground/examples/difficulty.rs, make difficulty, wired into make
self-tests, and a report file in ground-game under GROUND-WP-0005 with a
hub message pointing at it.
THE REPORT OPENS WITH THE RETRACTION, because what this task was written
to send was withdrawn by T02 and GROUND-WP-0005 is blocked on exactly that
number. They are told, in the first section, that we nearly sent them "the
game is too easy at 5-6 seats" and why it was wrong.
seats winnable greedy random first-legal spread
2p 60% 60.0% 5.0% 76.7% 71.7
3p 93% 88.3% 6.7% 25.0% 81.7
4p 100% 93.3% 6.7% 30.0% 86.7
5p 100% 100.0% 3.3% 0.0% 100.0
6p 100% 100.0% 3.3% 0.0% 100.0
SPREAD justifies the whole redesign: 71.7 to 100.0 points between three
trivial policies. The table now shows why no single rate is a difficulty
rather than asserting it.
And the 5-6 rows point the OPPOSITE way from the withdrawn claim --
first-legal 0% against greedy 100% is the widest spread in the table,
which suggests play matters MORE there, not less. Neither reading is
established and the report says so.
The confound is stated in the tool's own output, not only in prose:
`winnable` is conditioned on greedy's play up to the final round, because
searching from round 1 is unaffordable. Presenting it as a property of the
deal would repeat this pass's error in a subtler form -- which is exactly
how a corrected project reintroduces a defect. NO THRESHOLD CHANGES ARE
PROPOSED.
The instrument can fail (spec §5): a witness must replay to a win, an
unwinnable position must report searched-out rather than a budget cut, a
one-node budget must not claim exhaustion, and the policy panel must
actually disagree. difficulty-baseline.rs marked superseded, kept as the
survey's dated snapshot. Registered as F16, inconsistent / withdrawn.
T07. evidence/CB-EV-0024. Five of nine defects came only from the review;
four from execution, and all four of those were in work written after it.
The wrong-denominator family now has five instances and still no control
-- facts-check catches copies that disagree, nothing catches a number
computed correctly against the wrong base. Tier L was an over-declaration
(no port, structurally M) and paid for itself anyway, because the review
is L-only. Chaos window 2 will close with zero overrides, making its
retirement condition untestable.
Named as open rather than implied done: the witness is NOT wired to the
ending page. The search works; the browser cannot ask it yet.
make all: exit 0. loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
affordability projection
games/ground/src/search.rs, five tests. It finds real winning lines and
replays them through validate/fold to group_success.
Two bugs in my own work, found and fixed here.
THE TRAVERSAL WAS WRONG. It branched on "the first seat with any legal
command" and stopped there, so a later seat never acted if an earlier one
was already selected but still had a legal move. Restructured around what
the rules oblige: a seat without a selection MUST select (GR-R02) and
nothing else can happen first; after Reveal the optional actions branch
freely and the aggregate rejects Resolve until the obligatory ones are
done -- so the search needs no phase logic of its own.
AND MY REWIND WAS OFF BY ONE ROUND, replaying the round it was meant to
search. That is why the first run reported 3 nodes and looked like a
working search.
Measured with the real search, rewinding real games to the start of their
last K rounds:
2p K=1 exhausted, 8,103 nodes, ~29 ms
2p K=2 budget cut at 2,000,000 nodes, ~5 s
3p K=2 win found, 41 nodes, ~157 us
The spec's own falsifier said "§3 fails if K=2 proves unaffordable at four
seats". IT FAILED AT TWO. The projection assumed a joint product per
round; the search explores sequential per-seat decisions, so orderings
multiply the tree far beyond width^seats. That is the second projection
this pass published in place of a measurement -- C1's timer was the first.
THE ASYMMETRY IS THE OPERATIVE FINDING. Finding a win is cheap: DFS
stumbles onto one in tens of nodes. Proving none exists needs exhaustion.
So the witness feature is affordable now at any K a player would ask
about, and the winnable fraction (ADR-0013 D4) is NOT, because its
negative half must exhaust every deal it counts. K=1 is the honest default
for exhaustive answers today; making K=2 exhaustible needs transposition
or move-ordering, neither of which this pass built. specs §3 and §3.1
corrected accordingly, and the K=2 default withdrawn.
The negative control that makes "winnable" falsifiable: 2p seed 7 over its
last round returns NoneFound with exhausted=true in ~8k nodes -- a real
negative, not a budget cut wearing a verdict's clothes. And the visible/
hidden marking is tested both ways, since a marking that can only say YES
is decoration.
make all: exit 0. loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
caught me repeating C1
specs/RetrospectiveAnalysis.md v1.0 plus games/ground/benches/search.rs,
which exists because ADR-0013 D7 refused to let the spec quote either
disputed figure.
THE BENCHMARK'S OWN FIRST FIXTURE WAS DEFECTIVE, and it is the same defect
class the review caught one layer up. Stopping at a fixed step 20 put 2p
and 4p in states where seat 0 had NO legal commands, so it timed an empty
Vec (~120 ns) and silently skipped validate_fold because there was nothing
to validate. It now advances until the seat has a real branch and ASSERTS
it. A clone benchmark was added too: a search must copy state per branch,
and iter_batched excludes setup from timing, so without it the budget
would again rest on an unmeasured span.
Measured at real decision points: legal_commands 4.06-4.76 us, clone
378-639 ns, validate+fold 0.5-3.8 us. Per-child cost is NOT uniform --
some commands resolve cascades -- so budgets use the upper end (~5
us/child).
That settles D3 with real numbers. Joint branching over the last two
rounds is ~5x10^2 / 1.6x10^5 / 5.7x10^5 at 2/3/4 seats, so K=2 costs
negligible / 0.8 s / 2.9 s and holds at two to four seats. It does NOT
hold at five or six, where the tool must reduce K and say that it did
rather than silently searching less.
§4.1 is a normative prohibition, not a preference: a single policy's win
rate MAY NOT be reported as a difficulty. The spec carries the measured
reason -- greedy 100% against first-legal 0% on identical deals -- because
this project already made that error and nearly exported it to a repo that
is blocked waiting on the number.
§2.3 makes the empty-result wording normative: "no winning line found in
the last K rounds", never "unwinnable". A bounded search cannot establish
unwinnability and that sentence is what a player who just lost reads.
Also corrected: the T01 completion record still asserted all three
withdrawn claims as fact. It now carries claimed / withdrawn / survives
explicitly rather than being rewritten -- a retraction that does not
propagate to every place the claim lives is how the earlier ones survived.
make all: exit 0. loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
premise never left the repo
Separate agent, second tier-L review in this project. Six of seven
challenges conceded. The survey's headline finding is WITHDRAWN, not
softened.
C4 kills it, and the reviewer ranked it fourth. A FirstLegal policy --
take legal[0], no heuristic at all -- scores 0% at five and six seats
where GreedyPolicy scores 100%, and 77.5% at two seats where greedy scores
66%. Two unsophisticated agents span the entire range at the same seat
count. "The game is too easy at 5-6 seats" is therefore a statement about
GreedyPolicy, not about GROUND. The rescue the reviewer offered -- greedy
hits the 12-point ceiling in 200/200 deals, so the 6p row is a rules claim
-- dies on the same data: FirstLegal reaches that ceiling never.
C1: the per-node cost was wrong by 30-50x. The timer started before the
seed loop, so "us/node" included two setups, an entire greedy game and a
full validate+fold replay, divided by player-decision count. The tell was
in my own published output and I did not look at it: the figure FELL
(161/139/112) as branching ROSE (4.7/7.4/9.1), which no per-enumeration
cost can do. Re-measured with the clock around legal_commands alone:
3.0/3.5/4.1 us, now rising with branching. The reviewer measured 15.6-20.4
by a different isolation; we disagree by ~5x and neither has established
which is right, so T04 must benchmark it with criterion rather than adopt
either number.
C6: "exhaustive search is out at any seat count" is false -- ~3 seconds
over the last two rounds at 3p. With C1's correction the budget is
~10^5-10^6 nodes and bounded endgame search fits, so ADR-0013 cannot open
with "exhaustive is impossible, therefore determinized sampling" --
especially as sampling carries strategy fusion that exhaustive search does
not.
C3: the finding failed the admissibility rule this project wrote nine
hours earlier. 6/9/12 are sums where GROUND-WP-0004 T02 requires
per-priority rows, and the harness has no assertions, no --self-test and
no make target, so nothing can turn it red -- a `default` artifact wearing
a `counterexample` label, by CB-WP-0022 T05's own distinction.
C2: the ratio story explains nothing; 3p and 4p share deal, threshold and
ratio and differ by 12.5 points of win rate. C5: "explains the
maintainer's report" is contradicted by lib.rs:2487, which records his
losses as 3-player games on the pre-ruling deal, arithmetically unwinnable
at 6 against 7.
T06 exists to report to GROUND-WP-0005, which is BLOCKED waiting on a
difficulty baseline. Had this proceeded they would have been invited to
move thresholds on the strength of one bot's behaviour. That is the fifth
wrong premise this project would have sent them, and the second stopped by
an adversarial review rather than by a control. Both tier-L reviews here
have now caught a false headline that every gate passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CB-RES-0008 with a runnable baseline
(games/ground/examples/difficulty-baseline.rs), and the measurement
produced a finding before any solver exists.
A GREEDY BOT WINS 200 OF 200 GAMES AT FIVE AND SIX SEATS. Median margin
+3, 11.8-12.0 points available against a threshold of 9. The curve is
66% / 82.5% / 95% / 100% / 100% across 2/3/4/5/6 seats.
Row-level, as GROUND-WP-0004 T02 requires: available points 6 / 9 / 12
against thresholds 5 / 7 / 9, so the ratio RISES with seat count (1.20,
1.29, 1.33) while the table also gains actions per round to clear it with.
Three multipliers pointing the same direction.
It also explains the maintainer's report without needing a solver at all:
"I felt it was too easy but then we lost" is two true statements about
different seat counts.
Cost measured and it rules out the obvious approach. Branching is small
(mean 4.7-9.1) but legal_commands costs 112-161 us per call because it
filters candidates through full validate. Exhaustive search is out at
every seat count; 10^4-10^5 nodes is 1.4-14 seconds, which is the budget
the ADR must design inside.
Prior art names the trap: determinized search (PIMC) suffers strategy
fusion (Frank, Basin & Matsubara 1998) -- the search picks different
actions in states a real player cannot distinguish, so the witness may
require knowing what was on top of the deck. Such a line still replays
green, so the checkability benchmark does not catch it. Honesty and
checkability are different properties; stated explicitly so T03 cannot
conflate them.
The survey states its own most likely killer up front (§6): a view-only
search cannot fold events, so making the information boundary structural
rather than a promise may not be affordable. Better found here than in
T05.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ground-game ruled all ten U-items on 2026-08-03, every one CONFIRMED as
the default clay-borg simulates, and confirmed five of six provisional
scenarios. clay-borg never collected the answers: CB-RES-0007 reported "0
of 10 ruled" the same day, and CB-WP-0022 built the finding register two
days later still recording them as `reported`. make design's first run is
what noticed -- not a human, not the adversarial review that found four
other things.
That is the unread-inbox failure running in the opposite direction, and it
appears nowhere in the declaration, survey, ADR or spec of the pass that
was built entirely around the forward version. It is arguably worse: an
unread message is visible as silence, while a collected-but-unapplied
ruling looks exactly like work in progress.
Ten rulings quoted into §Underdetermined (the three conditional ones
verbatim -- U1's designer note, U2's End-only trigger, U8's
consume-only-if-it-cancels). Five provisional flags lifted, replaced by
ruled/ruled_by/ruled_note so the flag went and the provenance stayed.
Register queue 9 -> 0.
T02's control came back clean: make sim is 26 passed, 59 rules covered,
nothing red. Had a scenario gone red it would have meant we described our
own behaviour incorrectly to ground-game.
I wrote two U-item mappings and both were wrong. gr-a04 -> U1 (it asserts
consent is REQUIRED; U1 asks WHEN the target accepts) and gr-d05 -> U5 (it
exercises the UNREJECTED Reverse; U5 is the rejected one). Both plausible
from covers:, neither survived reading the description. Third and fourth
instance of this defect; the first two reached ground-game. So encodes_u_item
is now a declaration and design.py asserts the file names what it claims --
and that check's own first version grepped for mentions and went red when
two files recorded why they do NOT encode U1 and U5. A mention is not a
claim, which is exactly the looseness that let "six of the ten have
provisional scenarios" stand.
Two positive controls went red for the best possible reason, both broken
the same way -- asserting against live repo data instead of constructing
their condition. rule-coverage.py required at least one provisional item
to EXIST; it now builds a fixture and reports the live count as a
diagnostic, because there is no number of provisional items this project
should have. design-baseline.py pinned "2 of 6" while recomputing one row
from a live glob, so the dated snapshot was never a snapshot; frozen to
its 2026-08-03 list and unwired from self-tests, since per ADR-0012 D8 it
is no longer a reporting tool.
ScenarioFile is deny_unknown_fields and refused the four new fields until
declared -- correct: a corpus accepting unknown metadata would let a typo'd
encodes_u_iem sit there claiming nothing.
DEVIATION: ADR-0012 D2 said "no new file". GroundRules.md crossed the
loadability limit, so the register moved to specs/FindingRegister.md. D2's
substance holds -- one register, same machinery, nothing competing -- but
the literal instruction did not, and it resolves an awkwardness D2 named
itself.
make all: exit 0. loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row folded a 5,000-event log against a 100,000-event log and compared
throughputs, which confounds 'does cost per event grow with history'
(the property it claims) with 'does streaming a 20x longer Vec cost more
per element' (a memory-hierarchy fact true of any program). It measured
the second and reported it as the first: importing the edition enlarged
the aggregate and the ratio fell to 0.845 with the state bounded.
Corrected to time the SAME 5,000 events on a state at depth 0 and on a
state at depth 100,000. Equal windows, equal event mix, so the only
difference left is history depth.
corrected: clean 1.004, mutated 0.589 (red)
old: clean 0.845 (red on healthy code), mutated 0.751
It also runs in 8.5s instead of timing out: the first version re-walked
the 100k prefix every repetition, 200M untimed folds per sample, which
under the mutation never finished. A control that cannot be run is not a
control. It now advances to depth once per sample and clones.
Two of my own measurements here were wrong and both were caught by
measuring again. A 2-minute timeout killed the shell line before its
restoring cp ran, so three readings were taken on MUTATED code -- I
diagnosed an event-mix confound that did not exist and 'fixed' it. The
fix is kept on its merits; the justification was fiction. And the probe
that proved state was bounded had checked four of eleven collections.
make all exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-0011 decided it: vendor the CSV with a checked digest, read it with
a ~50-line reader, and let the hashes move.
The declaration's constraint was measured against the WRONG BUDGET. It
said a CSV crate costs 21,613 against AM-4a's 3,798 of headroom, '5.7x
over, settled by measurement'. But setup and problem_priorities are
cfg(scenarios) and are not in the shipped runtime at all, so AM-4a never
sees them. Against AM-4b, csv costs 17,651 against 19,742 -- it FITS,
with 2,091 to spare. It is refused anyway, on proportion: 89% of the
budget's remaining capacity to read 20 rows. The revisit condition is
stated (nested quoting, embedded newlines, multiple dialects).
GR-S01 now deals Surface + hidden 1..=k as ruled, with edition values and
suits. Measured: 6/9/12 available against thresholds 5/7/9 -- the game is
winnable at every seat count, which is what the maintainer could not do.
gd0001 is INVERTED, not deleted, and now also asserts the 6/9/12 so a
deal that is reachable for the wrong reason still fails.
Blast radius was scenario expectations, exactly as the ADR predicted: no
scenario pinned a hash and no bundle is committed. Six scenarios and two
unit tests updated, each with a note. gr-e01-threshold-unreachable-2p is
RENAMED to -reachable- and rewritten as the non-provisional import check
ground-game asked for by name. gr-e03's setup was restructured, not just
renumbered: with values 2,2,2 its personal-edge test would have tied
three ways and asserted nothing.
BLOCKING: AM-7 fails at median 0.845 against its 0.9 floor. Isolated
across three runs -- 3 problems + stand-in 0.97, 3 problems + edition
0.909, 4 problems + edition 0.845. State is BOUNDED (proven: identical
after 5k and 100k events), so this is not the unbounded-growth defect
AM-7 exists to catch; it is a bigger working set streaming a long log.
Whether AM-7's floor is still right for a larger aggregate is a spec
question and lowering it requires an ADR, so it is not being tuned here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements ground-game's ruling of 2026-08-03. make all exits 0, 26
scenarios, rule coverage 59/59, and no scenario encoded the bug.
The rule ended up somewhere other than where I put it, and a gate moved
it. It went into legal_commands first; the AM-1 coverage gate then
demanded a scenario for the new GR-P05, and scenarios drive validate, not
the offer layer. A rule enforced only by the offer is enforced only for
clients that ask what is legal -- the browser would be filtered and a
scenario file would walk straight past it. Once GR-P05 moved into
validate, every condition in legal_commands was dead code, and the
layering test said so in those words.
And the reported case was not the one I reported. CB-WP-0018 and the
message to ground-game described SOLVE offered on a FACE-DOWN Problem.
Measured: validate already rejected face-down, so it never was offered.
Problem 1 is the Surface Problem, face-up from the deal -- the
maintainer's three inert SOLVEs were the HAND case, holding no Clarify
for a Clarify Problem. The ruling covers both so nothing is invalidated,
but the record was wrong.
Four conditions asserted separately, because one 'SOLVE is filtered' test
would pass with three of four implemented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The maintainer played several 3-player games on 2026-08-03 and could not
win any. This says why, from the engine's own constants rather than my
arithmetic: claim every Problem the deal puts in play, concede nothing,
and the total still falls short of GR-E01's threshold at 2, 3 and 4
seats.
2p: 2 problems worth 3 vs threshold 5 — UNREACHABLE
3p: 3 problems worth 6 vs threshold 7 — UNREACHABLE
4p: 3 problems worth 6 vs threshold 7 — UNREACHABLE
5p: 4 problems worth 10 vs threshold 9 — reachable
6p: 4 problems worth 10 vs threshold 9 — reachable
The test reads problem_priorities (GR-S01's deal) and threshold (GR-E01)
out of the engine, so it cannot drift from the rules it tests, and it
holds for either dataset -- the stand-in gives 3/6/10 and Problems.csv
gives 4/6/9 against the same 5/7/9.
It ASSERTS THE DEFECT and is expected to keep passing until ground-game
rules, then be inverted. This is CB-WP-0022's reproduction rule applied
before the register exists, because delivering feedback should not block
on building the tool that tracks it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot::Journal -- a shared list of Applied { actor, command, events } the
driver appends to via play_journaled; play delegates with None so
nothing existing changed. BotGame.events only appears after play
returns, which is no use to a page rendered mid-game.
Phrased with record::to_step, the recorder's vocabulary, so what the
player reads is what the scenario file will say, and all 29 GroundEvent
variants now render in words instead of Debug.
A command that produced no events says 'no effect'; the mutation
dropping that branch goes red. Honest limitation recorded: the reported
SOLVE case is resolved inside the system's resolve command, which does
produce events for other seats, so it shows as a selection with no claim
following rather than an explicit 'no effect'. Making it explicit would
mean the renderer deciding why a rule did nothing -- a second
implementation of the rules, which this task's control forbids.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Provenance (tier S, one paragraph in lieu of survey and ADR): the two
clauses mutation-check has reported inert since CB-WP-0005. AM-7's
scaling ratio was held up by a test literally named
replay_100k_events_is_linear_and_fast that computed both throughputs,
printed both, and never divided one by the other. AM-8's N=10 was held
up by a runner that does two.
Both are now red. AM-7 3/3, AM-8 2/2, M-D1-MUT 10/14, and ADR-0005's
>=10-of-14 prediction MET for the first time. Neither was closed by
amending the question away, which was the live risk: the denominator is
unchanged and the four unenforced rows are the four already
unenforceable.
AM-7 needed three estimators. Best-of-N per leg then divide (AM-6's,
correct for a floor on one number) gave 0.581-1.085 on an unchanged
binary; legs back-to-back gave medians 0.931-1.004; legs interleaved at
fold granularity give 0.987/0.991/0.989, and 0.989 under 8-way CPU
contention while absolute throughput fell 4x. The INDETERMINATE guard
demanded unanimity and failed a good measurement over one sample
0.001 under the floor; it now requires a two-thirds majority. The
control that matters: AM-6's constant-cost mutation halves throughput
and leaves this ratio at 0.999x green, so AM-7 is not a second AM-6.
AM-8 kept N=10 because the measurement said so. Perturbing the RNG only
from its fourth construction on: --runs 2 PASSES, --runs 10 fails. A
late-onset divergence is deterministic, not flaky, so it is a control
rather than a coin flip. Ten runs live on one scenario (make am8, ~2s)
rather than all 25 (47s a build). GameKernel 5b records it.
The full run also found AM-4a's own mutation stale since ADR-0008 D3
moved the target 250,000 -> 161,000 in CB-WP-0013 -- reported
HARNESS-BROKEN, no score published. The build-free half of that check
is now a --self-test assertion, so make all catches the next one.
mutation-check clauses may now carry their own verify and mutation, and
then the enforced flag is measured rather than declared; a declaration
disagreeing with its measurement is refused.
make all exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 0's ten items are met. Three qualifications are recorded rather
than hidden behind checkmarks: GR-E01's threshold is unreachable below
five seats with the placeholder fixture, GR-A13 admits a SOLVE that
resolves to nothing, and GR-E03 has no scenario at all — implemented,
unreferenced, and would not fail if deleted.
The second-consumer verdict INTENT was waiting for: every abstraction
with a consumer fits, and the one with none is still unused. Bots and
the CLI drove Aggregate, Project, parse_command and the replay bundles
unchanged. CommitWindow had its best chance at a second user and did not
get one; its delete-by date stands.
The retrospective answers with the curve: 0.123, 0.228, 0.362, 0.298,
0.123 dollars per response across the last five passes. Neither
"meta is expensive" nor "compaction did it" survives the data. What does:
cost per response tracks how far the work is from a runnable check. The
meta passes that shipped a command were cheap; the ones that argued about
what a number means were not.
The meta budget reads 61% OVER on a pass that is 100% product, because
it aggregates over every task ever closed — the same defect CB-RES-0005
found in SH-1/SH-2 and that CB-WP-0007 T01 fixed for session shape. Not
fixed here: v1.5 forbids opening meta work above the line, so the
budget's first real act was to stop me from improving the budget.
Also fixes a doc comment that claimed DARVO was unimplemented for weeks
after it shipped. facts-check gates duplicated numbers; nothing gates a
prose claim about code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.
Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.
GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Policy trait, a seeded random policy and a greedy one with a stated
heuristic, a legal-command generator that filters candidates through
validate, and a driver that runs a 3-player all-bot game to GameEnded.
Same-seed runs are hash-identical (K8), and a different policy seed
produces a different game — without that second assertion the first is
satisfied by a bot that ignores its RNG.
Every failure is loud, because the one a bot driver must not have is the
silent one: no legal move, passing where an action is required, an
out-of-range index (not clamped), a rejected command, and a stall guard.
What the second consumer found, none of it fixed here:
- GR-A13 admits SOLVE against an already-claimed Problem and resolution
then does nothing — the action is silently wasted. The policy avoids
it; the rule is left for a ruling.
- The 3-player standard fixture cannot reach GR-E01's threshold of 7:
three Problems valued 1,2,3 cap the total at 6. No scenario noticed
because none plays to scoring.
- K13's Project trait still has zero implementors. T02 is its first.
Mutation-checked by hand. The first mutation was a no-op and survived;
removing the Resolve call outright turned three tests red for the stated
reason. Third instance of the weak-mutation class.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two rules, two different answers, which is the point of a task phrased
"implement, or amend and say why".
K18 is implemented. "Criterion benches driving the same scenario format at
scale" was false — the bench hardcoded its commands and never touched
ScenarioFile, while MetricsAndScenarios §3 pointed at a benchmarks/
directory containing only baselines/. benchmarks/synthetic-3p.yaml now
holds the workload and both the bench and bench_shape read it: the
workload is data, not code.
A second defect surfaced while fixing the first. After the bench switched
to the file, bench_shape still hardcoded the same sequence, so the
workload existed twice — deleting end_round from the YAML broke bench-test
while bench_shape kept passing. Duplicated-fact drift in executable form.
Both now read the same include_str! and deleting a command breaks both.
Explicitly not claimed: this does not unblock AM-3. AM-3's baseline is a
declarative game object — moves, turn order, rules. synthetic-3p.yaml is a
command list; the rules live in games/ground. Marking it as AM-3's subject
would compare a script to a game definition, which is the category error
AM-3 is blocked on. The file says so in its own header, where the next
person will be tempted.
K14 is amended. CommitWindow had zero non-test users and GROUND enforces
the same contract inline. Wiring GROUND through it was rejected: it would
change the serialized shape of `selections`, which four scenario files
assert by dot-path and every state hash depends on, for the sole benefit
of making a sentence literally true.
The deciding argument is INTENT's, not convenience: abstractions are
extracted from working games rather than invented in isolation, and no
concept becomes canonical until it survives a second concrete use.
CommitWindow was invented before any game needed it and has survived none.
Imposing it on GROUND would manufacture the first use rather than discover
it. So K14 states what is actually guaranteed, CommitWindow is marked
provisional in the source, and it carries a delete-by date of 2026-12-31.
Kernel spec->code link 16/18 -> 18/18, stated with the caveat the gate
prints every run: that is about names, not assertions.
Two self-tests broke and both broke correctly. rule-coverage's gate test
hardcoded "unlinked rules exist today" and failed when the last one was
linked; it now computes that and asserts the gate fails iff rules are
unlinked. facts' text check rejected k_unlinked once it became
legitimately empty; empty now renders as "(none)" and the check
distinguishes absent from empty.
M-D1-MUT: 8 of 14, unchanged — K14 and K18 are kernel rules, not
acceptance rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
INTENT design decision 8 of 10, unimplemented for six passes. cb-sim had
no flag parsing at all, so --replay had nowhere to go.
The bundle is manifest + commands.log + initial.snapshot + expected.yaml,
dev-only behind the scenarios feature and charged to AM-4b. The command
stream goes through the K11 framing built in T05, so a truncated bundle is
detected rather than replayed short — the two tasks compose rather than
duplicating.
The reviewer's D2 correction was real: this was not "a directory of four
files". Pass carried only the end state, RunOutcome::Failed was a
formatted String, and scenario.rs created an EventLog, appended to it and
never read it. All three had to change.
The first round trip failed to reproduce, and the cause is worth keeping:
state_hash_hex over a serde_json::Value is a different canonical form than
over the typed aggregate — Value's map is key-sorted, a struct serializes
in declaration order. The bundle was written with one basis and verified
with the other. A round trip written to recompute its own comparison value
would have PASSED this bug; it failed because the recorded hash came from
the producing process, which is control 2's entire purpose.
make replay-test implements ADR-0005 §6's four controls, 14/14: a
committed deliberately-failing fixture outside the corpus with covers: []
so it neither fails `make sim` nor inflates AM-1; a tampered recorded hash
must fail; a log short by one byte and a corrupted length prefix must be
rejected; and a mutated manifest seed must fail — which bites only because
replay re-derives the initial state from seed+setup and checks it against
the recorded snapshot, since restoring from the snapshot alone would leave
the seed inert. Plus a control on the controls: the bundle must still
replay after every mutation is reverted.
AM-7's hash-identical clause is re-earned. The probe records a hash per
per-game segment and replays each from its own genesis; folding from the
wrong seed now fails. That is the clause ADR-0005 §4 withdrew as
mutation-proven inert. The scaling >= 0.9x clause is still unenforced, so
AM-7 stays PARTIAL — reported, not rounded up.
Kernel coverage 15/18 -> 16/18. facts-check immediately caught the spec's
copy of that number going stale, on a number that moved the same hour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K11 is implemented: crates/cb-events/src/store.rs, magic + version header,
4-byte little-endian length prefix, append-only. Reimplemented not
assimilated per ADR-0005 §2 — no new dependency, and AM-4a/AM-4b are
unchanged at 246,250 / 317,021 because nothing entered the graph.
The operative clause is "detected", so corruption is tested rather than
assumed: a tail short by one byte, a half-written length prefix, a length
prefix corrupted to claim more than the file holds, foreign magic, and a
future format version are each rejected with a distinct error. A reader
that accepts a truncated tail is worse than no format, because it silently
returns a short history that looks complete.
AM-11 is earned. LogStore has two impls — MemLogStore and FileLogStore —
driven through ONE conformance(). The trait carries raw/set_raw precisely
so the corruption controls live in the shared suite: a format contract
that only one impl enforces is not a contract. The same shape is
retro-fitted to KernelRng, which is what AM-11 actually names: ChaChaRng
and NullRng now pass one suite asserting bounds, draw(1) == 0, determinism
across fresh instances, and shuffle preserving the multiset. They were
previously exercised by two separate tests, which is why "met, narrow" was
never earned and ADR-0005 §4 downgraded it.
K9 gets the assertion it did not have: snapshot at seq N + events N+1..M
must equal the from-genesis fold, hash-compared, on GroundState,
single-seed on purpose — AM-7's probe folds a multi-seed log, which is not
a replay of anything, and that defect is not repeated. Two positive
controls: the log must exceed 50 events, and the mid-log snapshot must
differ from the end state or "apply the remainder" is vacuous.
Proof it works: the exact mutation that SURVIVED in CB-WP-0005 — making
Snapshot::take discard its EventSeq — now fails on the K9 assertion.
AM-11's mutation breaks NullRng::draw to return its bound and the shared
suite fails. That is what M-D4-SWAP claims — either impl substitutable —
and exactly what two separate per-impl tests could never demonstrate.
M-D1-MUT: 7 -> 8 of 14. CB-EV-0001's scoreboard is refreshed: AM-2, AM-5
and AM-9 added, AM-6 moved to enforced, and the headline total corrected
from 4 to 8 — it had gone stale inside the same workplan that produced it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AM-4c is withdrawn from the acceptance table and retained as a reported
diagnostic. GameKernel §5a carries the argument.
The ratio has no monotone better direction. INTENT's rule is "own the
semantics, assimilate the implementation": rising can mean owning
semantics properly or reimplementing what should have been assimilated;
falling can mean leverage or dependency bloat. A target requires knowing
which way is better. It is also redundant — AM-4a/AM-4b bound the
denominator and AM-2 bounds own-source density, so AM-4c is a ratio of two
already-targeted quantities.
Measured at withdrawal: 1,426 own lines per 100k third-party (shipped),
1,107 (dev). make dep-weight now prints both, labelled diagnostic — the
row was never actually reported before.
M-D1-MUT keeps AM-4c in its denominator on purpose and says so in the
output. Dropping it would move the score 7/14 -> 7/13 without enforcing
anything: a score improved by deleting the question.
Decided before Phase B deliberately, since ADR-0005 predicts own-source
growth that will move this ratio; deciding after would be the retarget
§Step 4 forbids.
A T01 correction found here. The AM-6 gate failed inside `make all` at
38,753 ev/s against 341,280 in isolation — a 9x drop, because cargo test
runs binaries and threads concurrently. A throughput assertion inside a
parallel harness measures contention, not throughput. T01's measurement
was valid; its gate placement was not.
Fixed by running it only where valid — #[ignore] plus `make am6` in
release with --test-threads=1, now 2.0M ev/s at 20.2x headroom — and not
by lowering the target, which T01 forbade. My first attempt did drift that
way, adding a debug "sanity floor" of 50,000, and was backed out: a second
threshold is still a second chance to tune.
The mutation then went SURVIVED on the first run after the move. 4,000
black_box iterations were calibrated against debug's 3.4x headroom and are
invisible against release's 20x. Raised to 100,000; back to red. A weak
mutation is not a fixed property of a row — it can become weak when the
row's measurement conditions change.
Tier S (amends one row, creates no capability), chaos d4=2, no override.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the workspace compared any number to 100,000 events/s while
the evidence file reported "AM-6 | met, 16.5x". Now a test does — a test,
not a bench, because Criterion reports throughput and asserts nothing,
which is why this row measured nothing for six passes.
Measured on bnt-lap001: 341,280 ev/s in debug (3.4x the target), ~2.4-3.1M
in release. The spec target holds even in an unoptimized build, so the
gate needs no cfg split and runs in the ordinary `make test`.
The trap this task named — loosening a flaky timing assertion until it
never fires — is avoided by construction. The threshold is the spec value,
untouched; the constant says lowering it requires an ADR; and the failure
message repeats that, states measured headroom, and names reference
figures, so an agent hitting a red AM-6 is told not to tune it in the
place they are actually reading. Robustness comes from best-of-N, not from
a lower bar: a throughput floor asks whether the machine is capable, so
transient load should not fail the build.
Two positive controls in the test: a run that applied fewer than 50,000
events, or measured zero elapsed time, fails rather than scoring as
infinite throughput.
Verified by a PROPERTY mutation — 4,000 black_box iterations injected into
GroundState::fold, the hot path — not a threshold tweak, which would only
prove the comparison runs.
And the FA class found last pass is now gated. mutation-check rows gained
an `expect` field: the mutant's output must contain the row's stated
failure string or the verdict is WRONG-REASON, not red. Without it a
mutation that merely failed to compile would credit its row with an
assertion it does not have. Verified by pointing expect at a string the
verifier never prints and watching the verdict flip. This is remedy (2)
from the CB-WP-0005 retrospective, built a task earlier than planned
because the class it guards is the newest and most dangerous.
M-D1-MUT: 4 -> 5 of 14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gates existed; CI ran half of them and tolerated the failure case.
- cb-sim no longer has a "tolerable" non-zero exit. An unregistered game
prefix is a failure, and a run in which nothing executed is a failure.
Previously CI carried `|| test $? -eq 2`, so renaming a scenario prefix
would have skipped every scenario while the pipeline stayed green.
Verified with a negative control.
- CI now runs make coverage (AM-1) and make dep-weight (AM-4), both
added after CI was written and neither enforced until now.
- dep-weight enforces its targets instead of only reporting them.
- CI lints the shipped-runtime configuration separately, so the feature
split cannot rot unnoticed.
- Dropped the stale `make deps` target, which still measured the retired
crate-count metric.
The positive-control rule is now executable: CI runs
`cargo bench -- --test`, which executes every benchmark once, so a
workload that stalls fails the build.
That step immediately found a fourth instance of the error class it was
written for. The committed replay benchmark was the broken version — an
earlier patch never applied, leaving a command sequence that omits
Resolve, so every round produced nothing and the log-building loop spun
forever. It had never run to completion; the reported AM-7 replay
numbers came from a probe test instead. Fixed, given the same positive
control as the round loop, and re-measured from the benchmark: 100k
events fold in 2.18ms (95% CI 2.14-2.23), against a 5s budget.
Evidence now reports confidence intervals rather than point estimates,
so the 3% regression rule in MetricsAndScenarios is enforceable.
The finding worth carrying: writing the positive-control rule into
InnerLoop v1.0 did not prevent the next instance. Making it a CI step
did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adopts both remediations from CB-EV-0001 §4 (maintainer decision).
Option A — serde_yaml is now optional behind cb-game-runtime's
`scenarios` feature. The scenario module, the ScenarioGame impl and the
string parsers behind it are cfg-gated; cb-sim opts in explicitly. Both
configurations compile and lint clean under -D warnings.
A trap worth recording: `default-features = false` on a *member*
dependency is silently ignored when the workspace dependency does not
specify it. The first attempt gated nothing while looking correct — the
build succeeded and cargo tree still showed all six YAML crates. Fixed
by setting it on the workspace dependency. This is the positive-control
failure mode in miniature: success was not evidence the change applied.
Retarget — AM-4 now measures third-party source under audit, split by
build configuration, replacing a crate count that was unreachable
without undoing K5/K7 and that does not compare across ecosystems.
Re-measured via the new `make dep-weight`, whose own positive control
refuses to report when any crate's source cannot be located:
shipped runtime 23 crates 246,250 lines target <=250,000 met
dev toolchain 29 crates 317,021 lines target <=350,000 met
own source 3,408 lines
Scenario tooling costs 70,771 lines a shipped game never compiles —
the split the single number was hiding.
Targets are set at current measurement plus headroom, so they bind on
future growth rather than retroactively passing what had failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
evidence/CB-EV-0001-game-kernel.md records the acceptance run against
the CB-RES-0001 baseline.
Met: AM-1 rule coverage 58/58; AM-6 throughput 1.65M events/s against a
100k target; AM-7 scaling 1.08x at 20x workload and a 100k-event replay
in 4.13ms against a 5s budget; AM-8 zero divergence over 10 full runs
with fmt and clippy clean; AM-10 zero foreign collection types.
Not met and reported as such: AM-4 at 33 transitive crates against a
<=20 target. Attribution is in the evidence file. The recommended fix
is making serde_yaml optional (-5, a test-only concern), after which
the remainder is sha2 and rand_chacha, which K5 and K7 require. We are
not hand-rolling crypto primitives to win a dependency count.
AM-12 is recorded as uncomputable: per-task token counts were never
instrumented, and inventing a USD figure would defeat the metric.
A measurement error was found and corrected before publication. The
first benchmark reported 9.3M events/s on a flat curve. The workload
had a player selecting SUPPORT while parked at Stress 4, so GR-R03
rejected it, rounds never completed, and throughput was computed for
rounds that never happened. The bench now asserts the per-round event
count and panics rather than measuring a stalled loop. The corrected
figure is 5.6x lower.
The evidence file states plainly what the boardgame.io comparison does
and does not support: the ~450x command-rate ratio is cross-runtime and
cross-feature-set, so it is a direction, not a verdict, per the
InnerLoop parity-cap rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- GR-R09: after Round 5's End the game ends and scoring runs instead of
the round advancing.
- GR-E01: claimed Problems sum their printed values against the
player-count threshold (2p 5, 3-4p 7, 5-6p 9 in dataset 0.1).
- GR-E02 SHARED GROUND: shared score, Mastery reduced per Blame token
and per Denied Problem.
- GR-E03 COMMON PROBLEM: personal score is claimed value less Blame,
tiebroken by lower Stress then more Bonds.
- GR-E04 BONDED COALITIONS: connected components over Bonds only, so
Rivalries do not connect and an unbonded player is a coalition of
one; tiebroken by lower combined Stress then fewer Blame.
Ties yield every tied candidate rather than an arbitrary pick, which is
what "shared victory" in GR-E03/E04 asks for.
AM-1 rule coverage is now 58/58 (100%), 21 scenarios, 17 tests.
Caveat recorded rather than papered over: GR-E02's "successes" is not
defined in dataset 0.1. It is implemented as the count of claimed
Problems and both scoring scenarios are marked provisional, so a
ground-game ruling flips a scenario rather than the kernel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolution step 3 (GR-R06), the last unimplemented step:
- GR-D02 binding: one stage per consecutive round, DENY then ATTACK
then REVERSE, regardless of the player's later Stress.
- GR-D03 DENY: turn one face-up, unsolved, unprotected Problem face
down and Deny it. Under the U3 default no legal target is a no-op and
the sequence still advances.
- GR-D04 ATTACK: one extra Attack under the normal relation rules, then
place the Focus token beside the target even if it was cancelled.
- GR-D05 REVERSE: flip Focus to Blame, +1 Stress to the holder, one
Protection to the owner, unless the holder's GROUND-ND rejects it.
Under the U5 default the owner takes -2 either way and the sequence
ends.
- GR-D06 early end: a Support through a Bond that predates this round's
Support step cancels the stage and ends the sequence, and the placed
Focus token is removed. GROUND-GR ends it after the stage resolves.
- GR-D07: the marker returns to OFF, so a later End can re-trigger.
The Attack rules are now one routine shared by the chosen ATTACK Action
and the DARVO extra Attack, so GR-A06..A09 cannot drift between them.
Stage targets are named by their own command during Reveal, validated
against what the stage admits: DENY needs an eligible Problem, ATTACK
another player, and only a player with a live sequence may choose.
18 scenarios pass; AM-1 coverage 47/58 (81%), up from 41/58.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the three rules deferred in iter 2 and 3 because each needed a
decision from a player rather than a default I would have invented:
- GR-A11 GROUND-OU: restore a Denied Problem, protect a face-up one
from Deny, or cancel an Attack aimed at the chooser.
- GR-A12 GROUND-ND: remove a Blame token, break a relation involving
the chooser, or reject a Reverse.
- GR-L02 / GR-A05: the Support target answers after Reveal — accept or
decline a Bond where no relation exists, flip or break an existing
Rivalry. A Bond can no longer form without consent.
Each choice is validated against the state it claims to act on: a
restore needs a Denied Problem, a cancel needs an Attack actually aimed
at the chooser, a Blame removal needs that token present, a break needs
that relation to exist. GR takes no sub-choice, and OU and ND may only
use choices from their own list.
GR-A09 ordering follows the U8 default: an OU cancellation is chosen at
step 1 and applies first, so a Protection token is spent only when it
is what actually cancels.
14 scenarios pass; AM-1 coverage 41/58 (70%), up from 34/58.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fills in resolution steps 1, 4 and 6 (GR-R06):
- GR-R05 mode choice: a player who revealed GROUND picks GR/OU/ND after
Reveal. Resolution refuses to start while any revealed GROUND lacks a
mode, and only that player may choose it.
- GR-A10 GROUND-GR: self -2 Stress and Freedom readied.
- GR-A01 INVESTIGATE: reveal the chosen hidden non-Denied Problem, then
draw one Solution; the draw still happens when nothing is revealable.
- GR-A02 SOLVE: spend a Solution of the Problem's suit and claim it; a
later resolver the same round spends nothing, per Lead order.
- GR-A13 tightened: INVESTIGATE must target a hidden Problem, SOLVE a
face-up non-Denied one. Previously any existing Problem was accepted.
Deck exhaustion (U4) reshuffles the discard, seeded from the game seed
and round so validate stays a pure function of state. The resulting
order travels inside DeckReshuffled, so replay never re-derives it.
Still pending, each because it needs its own decision command rather
than a default: GROUND-OU and GROUND-ND three-way choices (GR-A11/A12)
and the DARVO stage machine (GR-D02..D07). No scenario claims coverage
of them.
Filler picks in existing scenarios moved from GROUND to INVESTIGATE:
GROUND now has a real Stress effect, which was polluting the Support
and Attack assertions.
11 scenarios pass, 34 rules covered; 17 tests, fmt/clippy green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round machinery, system-driven (GR-R04/R06/R08):
- GR-R06 fixed step order with GR-R07 Lead-first ordering inside a step.
Steps 1 (GROUND), 3 (DARVO stages), 4 (INVESTIGATE) and 6 (SOLVE) are
not implemented yet; their Actions resolve as no-ops and no scenario
claims coverage of them.
- SUPPORT GR-A03/A04/A05 and ATTACK GR-A06..A09, with relation
formation, flip and break per GR-L01/L03/L04 and Protection
cancellation.
- GR-R08 End: DARVO trigger at Stress 5 in Lead order, Lead rotation,
Round advance, per-round flags cleared.
- Stress clamps 0-5 on every application, the U2 default, so a mid-round
spike that is reduced before End does not trigger DARVO.
Two consent-dependent rules are deliberately left out because they need
a decision command rather than a default: Bond formation (GR-L02) and
the target's flip-or-break choice on Support-through-Rivalry (GR-A05).
Both are noted in code and covered by a provisional scenario.
Fixes a defect in the T07 scaffold: relations were keyed by a tuple,
which JSON cannot use as an object key, so state_hash would have
panicked on any state holding a relation. Relation keys are now a Pair
newtype serialized as "a-b", with a regression test.
setup.patch may now create a final key so scenarios can seed open-ended
maps; a typo anywhere earlier in the path is still an error.
8 scenarios pass, 28 rules covered; 17 tests, fmt/clippy green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the RunOutcome::Unimplemented stub with a real runner:
- ScenarioGame trait: games own setup presets and the command
vocabulary, the runner owns execution, assertions, and determinism.
- K8 double-run: every scenario runs twice on the same seed and fails
on state-hash divergence.
- K4/K11: applied events go through Envelope into EventLog, so seq
monotonicity is enforced on the real path, not just in unit tests.
- setup.patch was parsed and silently dropped; the runner now applies
it generically and errors on a path that does not exist, so a typo
in a scenario can never pass as a no-op.
- Assertions: dot-path state lookup over objects and arrays, ordered
event subsequence matching by field subset, exact rejects-set match.
GROUND rules realized: GR-S01..S04 setup (seeded shuffle, deal, Lead,
Surface Problem face up), GR-R02 Select commit, GR-R03 stress gate and
Freedom spend, GR-A13 targeting legality.
cb-sim dispatches by the scenario's game prefix and reports rule
coverage. 3 scenarios pass, 7 rules covered; fmt/clippy/tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>