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>