2026-07-31 01:43:31 +02:00
# Game Kernel — specification and acceptance metrics
Status: **v0.1 draft** — implements ADR-0002 (reimplement in Rust,
assimilate patterns). Governs crates `cb-kernel` , `cb-events` ,
`cb-game-runtime` , and the first game package `games/ground` .
Baselines referenced: research/CB-RES-0001-game-kernel.md.
Instruments: specs/MetricsAndScenarios.md. Rules content:
specs/GroundRules.md.
The kernel is the **authoritative semantic layer** of the Clay-Borg
architecture (Blueprint §1): headless, deterministic, replayable. No
rendering, no physics, no networking — those attach later through ports
and must never leak types into anything specified here (M-D4-LEAK = 0).
---
## 1. Crate boundaries
| Crate | Owns | Must not know about |
|---|---|---|
| `cb-kernel` | canonical IDs, RNG service, command/event/fold traits, clock-free tick model | any concrete game, serialization format details |
| `cb-events` | event envelope, append-only log, snapshots, canonical serialization, state hashing | game semantics |
| `cb-game-runtime` | round/phase machinery, simultaneous commit windows, per-player projections, scenario-runner API | GROUND specifics |
| `games/ground` | GROUND rules: state aggregate, commands, events, reducers per specs/GroundRules.md | anything below `cb-game-runtime` 's public API |
Dependency rule: `games/ground → cb-game-runtime → cb-events → cb-kernel` .
No cycle, no skip that bypasses a public API. External crates allowed in
the headless kernel workspace: serde (+format crate), a seedable RNG
AM-4: gate scenario YAML, retarget on audited source, re-measure
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>
2026-07-31 03:35:41 +02:00
(e.g. chacha), a hash (sha2), thiserror-class error derive. Weight is
budgeted as **third-party source under audit** , split by build
configuration — see AM-4.
**Scenario parsing is dev-only.** `cb-game-runtime` 's `scenarios` feature
carries the YAML dependency; a shipped game runtime builds with
`--no-default-features` and parses no YAML. Workspace dependencies on
`cb-game-runtime` and `games-ground` therefore set
`default-features = false` , and consumers that need scenarios (currently
`cb-sim` ) opt in explicitly.
2026-07-31 01:43:31 +02:00
## 2. Canonical model
### 2.1 Identifiers (`cb-kernel`)
Newtyped, copyable, ordered: `GameId` , `PlayerId` , `EntityId`
(problems, tokens, relations), `CommandId` , `EventId` (monotonic
`u64` sequence per game), `Seed` (`u64` ). No UUIDs inside the hot
path; string forms only at the boundary.
### 2.2 The mutation pipeline (assimilated: cqrs-es pattern)
```text
Command (actor-tagged intent)
→ validate(& State, & Command) → Result< Vec < Event > , Rejection>
→ fold(& mut State, & Event) # infallible, total
→ EventLog::append(events) # with per-event hash chaining
```
- **K1** All state change flows through events; there is no other mutator.
`fold` is infallible: every validation that could fail happens in
`validate` , so a logged event always applies.
- **K2** Commands carry: `CommandId` , issuing `PlayerId` (or `System` ),
round number, and payload. Duplicate `CommandId` within a game is
rejected (idempotency).
- **K3** Rejections are typed and testable (`Rejection::StressGate` ,
`::NotYourTarget` , `::SlotOccupied` , …); scenarios assert on them
(`expect.rejects` ).
- **K4** Events are closed enums per game package, serialized through a
versioned envelope: `{seq, game_id, round, kind, payload, schema_ver}` .
### 2.3 Determinism (assimilated: Rune's enforced discipline)
- **K5** The only randomness source is the kernel RNG service, seeded from
`Seed` ; RNG draws are themselves events (or deterministically derived
from the event sequence) so replay never re-rolls.
- **K6** No ambient time, no OS entropy, no pointer/hash iteration order:
all iterated collections in semantic state are ordered (`BTreeMap` /
`Vec` ); `HashMap` is forbidden in `games/*` state. Enforced by lint/deny
in CI, not convention (AM-8).
- **K7** State hash = SHA-256 over the canonical serialization of the
aggregate; computed on demand and recorded at round End events.
- **K8** Double-run invariant: the scenario runner executes every scenario
twice with the same seed and fails on any hash divergence
(MetricsAndScenarios §2).
### 2.4 Snapshots and replay (`cb-events`)
- **K9** A snapshot is the canonical serialization of the full aggregate +
the `EventId` it includes. `snapshot + remaining events → state` must be
hash-identical to a from-genesis fold (AM-7).
- **K10** A replay bundle (`.cbreplay` , MetricsAndScenarios §4) contains
manifest, command log, initial snapshot, and failed expectations; the
runner's `--replay` re-executes it bit-identically.
- **K11** Log format is append-only, length-prefixed, versioned; a
truncated tail is detected, not silently accepted.
### 2.5 Simultaneity and hidden information (`cb-game-runtime`)
- **K12** Commit window primitive (assimilated: GROUND's Select/Reveal,
boardgame.io `activePlayers` shape): the runtime opens a window naming
the players who must submit; submissions are recorded as **commitment
events** whose payload is hidden in projections until the window's
reveal event; late/duplicate submissions are Rejections.
- **K13** Projection (assimilated: OpenSpiel information states): for each
`PlayerId` (and `Spectator` ), a total function `project(& State) →
PlayerView` that structurally cannot include: other players' unrevealed
commitments, face-down problem identities, other players' hands.
Projections are derived views — never inputs to `validate` /`fold` .
CB-WP-0006 T07: implement K18, amend K14
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>
2026-08-01 12:47:16 +02:00
- **K14** *(amended 2026-08-01, CB-WP-0006 T07 — see §2.5a)* The GROUND
round (GR-R01..R09) **implements the commit-window contract** : one
window per Select, duplicate submission rejected, completeness required
before Reveal, then fixed-order resolution steps with Lead-order
iteration (GR-R06/R07) driven by system commands. It implements that
contract **in its own aggregate** ; `CommitWindow` in `cb-game-runtime`
is the extracted form, and is **provisional until a second game uses
it**.
2026-07-31 01:43:31 +02:00
### 2.6 GROUND aggregate (`games/ground`)
CB-WP-0006 T07: implement K18, amend K14
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>
2026-08-01 12:47:16 +02:00
#### 2.5a Why K14 was amended rather than implemented
The original K14 said the round *is expressed through* runtime
primitives. It was not: `CommitWindow` had **zero non-test users** and
`games/ground` did not import it. GROUND collects selections in its own
`BTreeMap` and enforces the same contract inline — duplicate submission
(`second_selection_is_a_duplicate` ), completeness before Reveal
(`GR-R04` ), and ordered reveal.
Two options were available and the choice is recorded because it could
reasonably have gone the other way.
**Wiring GROUND through `CommitWindow` was rejected.** It would change
the serialized shape of `selections` , which four scenario files assert by
dot-path (`selections.0.action` ) and which every state hash depends on —
a large, risky refactor whose only benefit is making a sentence literally
true. More importantly it inverts INTENT: *"abstractions are extracted
from working games... rather than invented in isolation"*, and *"No
concept becomes canonical merely because it looks general. It becomes
canonical after surviving a second concrete use."* `CommitWindow` was
invented before any game needed it and has survived **zero** uses.
Imposing it on GROUND now would manufacture the first use rather than
discover it.
**So the rule was amended to state what is actually guaranteed**, and the
primitive is kept and marked provisional. It costs ~60 lines, it documents
the seam stage 3 (networked sessions) and stage 4 (packaging) will need,
and re-extracting it from GROUND when a second game exists will be
better-informed than keeping it aligned by hand now.
**Open, with a date:** if no second game uses `CommitWindow` by
**2026-12-31**, it should be deleted rather than carried — a primitive
with one hypothetical user and a test that exercises only itself is the
AM-11 shape (a claim resting on a pair with no consumer), and this project
has now paid for that shape twice.
2026-07-31 01:43:31 +02:00
- **K15** State implements specs/GroundRules.md §1 exactly; every GR-rule
is realized in `validate` /`fold` and cross-referenced by rule ID in doc
comments, giving a greppable rule→code→scenario chain.
- **K16** U-item defaults (GroundRules §Underdetermined) are implemented
behind clearly named functions so a ground-game ruling is a localized
change; their scenarios carry `provisional: true` .
## 3. Scenario runner and CLI surface
- **K17** `cb-game-runtime` exposes the scenario runner as a library; a
thin binary (`cb-sim` , precursor of `cb sim` ) runs
`scenarios/ground/*.yaml` per MetricsAndScenarios §2: setup presets,
ordered actor-tagged commands, partial end-state assertions, `covers` ,
`rejects` , optional `state_hash` .
- **K18** Benchmarks are Criterion benches driving the same scenario
format at scale (MetricsAndScenarios §3); the synthetic workload
mirrors the CB-RES-0001 harness shape (3 players, commit/reveal
rounds) for same-shape comparison.
---
AM-4: gate scenario YAML, retarget on audited source, re-measure
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>
2026-07-31 03:35:41 +02:00
## 4. Acceptance metrics
ADR-0004: ratify AM-4a and AM-4b (maintainer decision)
Discharges the open item T07 raised. Values unchanged at 250,000 and
350,000; what was missing was a reviewed decision behind them, since
they had been set by the implementer in the commit that measured them
and that also changed the feature gating being measured.
The ADR supplies the argument T07's test requires -- why the targets
bind on FUTURE work rather than merely passing present work:
AM-4a leaves 3,750 lines of headroom (1.5%), about one small crate,
so any new shipped-runtime dependency breaches it almost immediately.
That is intended: the shipped runtime should be effectively frozen.
AM-4b leaves 32,979 lines (10.4%), deliberately looser -- dev tooling
should absorb one moderate dependency without a spec change, not two.
Both are ceilings on a quantity that only grows by choice. Nothing
drifts across them; only adding a dependency does.
Falsification condition stated: if a later pass raises AM-4a to
accommodate a dependency it wants, that is the failure the ceiling
exists to catch, and the answer is an ADR arguing for the dependency.
First ADR written under the correction/retarget test; sets the shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 09:45:35 +02:00
**On AM-4's retarget (2026-07-31).** *Ratified by
[ADR-0004 ](../decisions/ADR-0004-am4-ratification.md ) on 2026-07-31; the
headroom argument for why these ceilings bind on future work lives there.*
AM-4 originally read "≤20
AM-4: gate scenario YAML, retarget on audited source, re-measure
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>
2026-07-31 03:35:41 +02:00
transitive crates", set against boardgame.io's 120 npm packages. That
target was retired for two measured reasons. First, it was unreachable
without undoing this spec's own contracts: K5 (seeded ChaCha) and K7
(SHA-256) cost 12 crates between them, and the measured ladder showed
nothing reached 20 except reimplementing one of those primitives —
which trades an audited implementation for a scoreboard number. Second,
crate count does not compare across ecosystems: Rust splits crates far
more finely than npm, so the original 33-vs-120 comparison flattered us
while the ≤20 target punished us, both for the same reason.
Third-party source under audit is what the count was a proxy for, it is
comparable across ecosystems, and it cannot be gamed by crate
granularity. Splitting it by build configuration also makes the
dev/shipped distinction visible, which the single number hid. (the code loop's exit condition)
2026-07-31 01:43:31 +02:00
Per InnerLoop step 4/5: T08 iterates until every row meets its target;
evidence lands in `evidence/CB-EV-0001-game-kernel.md` with no
`unmeasured` . Baselines from CB-RES-0001; cited-only rows cap at parity.
| ID | Metric | Baseline (CB-RES-0001) | Target | Verdict basis |
|---|---|---|---|---|
| AM-1 | M-D1-COV: GR-rules covered by ≥1 passing scenario | no candidate has any (observation) | **100%** of GR + U rules | measured by runner report |
| AM-2 | M-D1-SPL: spec lines per rule in `games/ground` rules code (impl LOC ÷ rule count) | boardgame.io ~36 LOC for the 2-move synthetic game | ≤ 40 LOC/rule, paired with AM-1 (anti-gaming pair) | measured (tokei + rule count) |
| AM-3 | Synthetic-workload definition size: LOC to express the CB-RES-0001 synthetic game on our kernel | ~36 LOC (boardgame.io, measured) | ≤ 50 LOC | measured |
CB-WP-0013-T02/T03: retire SH-3 as a gate; correct AM-4a and its target
ADR-0008, tier M (survey and ADR merged).
D1 — SH-3 retired as a gate, kept as a diagnostic. Investigating it
found a third defect, deeper than the two this pass was declared on.
Re-deriving batching from the raw transcripts, independently of cb-cost:
CB-WP-0011 pass 54 with tools 0 batched 0.0%
gap -> next decl 16 with tools 6 batched 37.5%
CB-WP-0012 pass 86 with tools 0 batched 0.0%
gap -> next decl 10 with tools 1 batched 10.0%
CB-WP-0013 so far 10 with tools 0 batched 0.0%
Zero batched turns in 150 in-pass responses; 37.5% in one gap, above the
20% floor. Batching needs two calls whose inputs are known at once —
orientation work. Implementation consumes each step's result before the
next. SH-3's window is since the last commit, which during a pass is
always implementation. The metric could not read above ~0% in the window
it was gated on. A floor the window structurally excludes is not a
target.
This pass's own declaration was also wrong: it claimed batching "has got
worse" (7.8-8.6% vs 1.1-6.3%). Differently-placed windows, not different
behaviour. Withdrawn — the same class of error, in the pass written to
correct it.
Not retargeting to match the measurement: the floor was not moved to 6%,
the gate was removed on an argument about what the quantity is worth.
The number is still reported; only the verdict is gone.
D2/D3 — AM-4a counts --edges normal,no-proc-macro: 157,202, not 246,250.
The target moves down with it, 250,000 -> 161,000, so the correction
hands back essentially nothing (headroom 3,750 -> 3,798). Three controls:
the exclusion drops exactly the five expected crates, only removes and
never adds, and is not a no-op.
The DFD gate then caught the follow-on it exists for — three historical
documents carrying live fact tags for a number that had changed. Not
rewritten; untagged, with a supersession banner.
AM-4b is deliberately not corrected: its proc-macro share is unmeasured.
gate-review now reads 0 due, 0 silent, 0 drifted — GATE-REVIEW earns its
first caught entry by forcing SH-3's re-justification, and the registry
has no silent gates left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 07:30:14 +02:00
| AM-4a | M-D2-DEP: third-party LOC, **shipped runtime** (`--no-default-features --edges normal,no-proc-macro` ) | boardgame.io: 120 npm packages / 3.9M LOC | ** ≤ 161,000 lines** (ADR-0008 D3, was 250,000) | measured (`make dep-weight` ) |
CB-WP-0019 T01/T02: AM-4b asks what a contributor acquires
The two AM-4 budgets had the SAME scope -- one package, no dev edges --
while claiming to bound different things. AM-4b now measures the
workspace with dev edges: 57 crates / 725,258 lines where it read 29 /
317,021, having been blind to 28 crates and 408,237 lines, more source
than its own target.
Target 745,000, ~2.7% of room -- the same margin ADR-0008 D3 gave AM-4a,
applied to a number that grew because the instrument was repaired, not
because anything was added. The target moved to fit the measurement.
T02: proc-macros are COUNTED here and excluded from AM-4a, on purpose.
AM-4a asks what ships and a proc-macro never ships. AM-4b asks what is
acquired, and ADR-0007 D3's acquisition rule counts what the build
fetches -- 'it does not ship' is no answer to 'we downloaded it'. When
the rules disagree, the question each budget asks decides. Measured
share 109,585 lines / 15.1% against AM-4a's 36.2%, so ADR-0008 D2's
refusal to borrow the ratio was right by more than a factor of two.
Caught by this project's own earlier work twice: the mutation
find-string went stale and --self-test reported it BUILD-FREE (the check
CB-WP-0015 added after AM-4a's rotted for two passes), then the DFD gate
caught facts.toml carrying the old numbers.
CB-EV-0001 and ADR-0004 carried live fact: tags on historical readings.
A dated record asserting a CURRENT value is a category error, so those
occurrences are marked as-measured instead of retro-edited, and ADR-0004
gains a supersession note.
make all exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:04:54 +02:00
| AM-4b | M-D2-DEP: third-party LOC, **what a contributor acquires** (`--workspace --edges normal,dev` ) | as above | ** ≤ 745,000 lines** (CB-WP-0019; was 350,000 against a graph that measured 317,021 of the real 725,258) | measured (`make dep-weight` ) — see §5c |
CB-WP-0006 T04: withdraw AM-4c; and fix where AM-6 is measured
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>
2026-08-01 10:06:00 +02:00
| ~~AM-4c~~ | M-D2-DEP: own source per third-party 100k lines | — | **WITHDRAWN from the acceptance table 2026-08-01 (CB-WP-0006 T04)** — retained as a reported diagnostic in `make dep-weight` ; see §5a | diagnostic |
2026-07-31 01:43:31 +02:00
| AM-5 | M-D2-BLD: clean release build of headless workspace | n/a (npm install ~seconds; not comparable) | ≤ 60 s on bnt-lap001, recorded not gated | measured |
| AM-6 | M-D3-THR: applied events/s, synthetic workload, same machine | boardgame.io ~1,100– 1,900 moves/s (best config, degrading) | ** ≥ 100,000/s** (stipulated target, ADR-0002) | measured |
CB-WP-0015: the two inert clauses, AM-7 scaling and AM-8 N=10
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>
2026-08-02 14:07:08 +02:00
| AM-7 | M-D3 scaling: throughput @100k events vs @5k ; and snapshot+replay of 100k events | boardgame.io 0.45– 0.66× @20 – 40k, DNF @100k | ** ≥ 0.9× ** (flat), replay of 100k events ≤ 5 s, hash-identical | measured (`make am7` , `make test` ) |
| AM-8 | Determinism invariant: N=10 same-seed replays, bit-identical hashes; HashMap-in-state deny lint clean | Rune: enforced by tooling (cited) | zero divergence, lint clean in CI | measured (`make am8` , `make check` ) — see §5b |
2026-07-31 01:43:31 +02:00
| AM-9 | M-D3-MEM: peak RSS, 100k-event synthetic run | boardgame.io ~100→232 MB @5k →40k (indicative) | ≤ 64 MB, flat with history given snapshot interval | measured (indicative label, same method) |
CB-WP-0005 T03: correct the record, and defer Phase C
Four verdicts in evidence/CB-EV-0001 corrected in place with a dated
note, per ADR-0005 §4: AM-7 replay split (timing met, hash-identical
withdrawn), AM-10 withdrawn as written and restated as AM-10' (the K6
determinism lint it actually measured), AM-11 downgraded to unmet, and
AM-1b added to the scoreboard it was missing from.
The scoreboard gains an Enforced column carrying M-D1-MUT, because a row
can be measured and still enforce nothing and the table had no way to say
so. AM-6 now reads "met, 16.5x" alongside "not enforced — nothing
compares any number to 100,000".
A fifth correction surfaced that ADR-0005 did not list: AM-12 still read
$248.46, the figure CB-WP-0002 disproved and corrected to $93.15 four
workplans ago. It was stale in the evidence file ever since — untagged,
and therefore invisible to facts-check. Now tagged. A duplicated-fact
instance that survived the gate built to catch duplicated facts, because
that gate only checks copies that opted in. Recorded for T07.
GameKernel §5 carries the AM-10 withdrawal and AM-11 downgrade inline so
a reader of the spec cannot reach the old claim.
Phase C is deferred before starting, per the stop condition T02 wrote and
the maintainer's decision. It is scoped to five rules; the measurement
says eight acceptance rows have no instrument at all. Building it as
written would proceed on a diagnosis the instrument had just
contradicted. T04-T06 stay in the file with their analysis intact and
move to CB-WP-0006.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:54:29 +02:00
| AM-10 | M-D4-LEAK ** (withdrawn 2026-07-31, ADR-0005 §4 — no `cb-*-api` crate exists, so the population is empty; the clippy `HashMap` /`HashSet` deny that stood in for it cites K6 determinism and is now reported as AM-10′ )**: foreign types in canonical-interface signatures | boardgame.io: JS-ecosystem-locked | **0** | measured (grep/deny rule) |
CB-WP-0006 T05: K9's assertion, K11's format, and the AM-11 suites
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>
2026-08-01 10:50:52 +02:00
| AM-11 | M-D4-SWAP ** (met 2026-08-01 — `KernelRng` and `LogStore` each drive one shared `conformance()` ; CB-WP-0006 T05)**: null + reference impls passing one conformance suite | no candidate has the pattern | RNG and log storage each have ≥2 impls (real + test/null) under one suite | measured (bool) |
2026-07-31 01:43:31 +02:00
| AM-12 | M-D2-TOK / M-D2-CST: tokens and USD per completed task | n/a — first pass sets our own baseline | recorded per task in the evidence cost log (price sheet 2026-07-31) | recorded, not gated |
CB-WP-0006 T04: withdraw AM-4c; and fix where AM-6 is measured
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>
2026-08-01 10:06:00 +02:00
### 5a. Why AM-4c was withdrawn from the acceptance table
*(CB-WP-0006 T04, 2026-08-01. Tier S — amends one row, creates no
capability; chaos d4=2, no override. Follows the precedent ADR-0005 §4 set
for AM-10.)*
AM-4c was `reported, not targeted` , so nothing could fail and it counted
against M-D1-MUT. The task was to give it a threshold or drop it. It is
dropped, for a reason that a threshold cannot fix:
**The ratio has no monotone better direction.** INTENT's rule is *own the
semantics; assimilate the implementation*. A **rising** ratio can mean we
are properly owning semantics, or that we are reimplementing things we
should have assimilated. A **falling** ratio can mean good leverage, or
dependency bloat and implementation leaking into our semantics. Both
directions are ambiguous, and a target requires knowing which way is
better.
**It is also redundant.** AM-4a and AM-4b already bound the denominator
(third-party LOC ceilings, ratified in ADR-0004) and AM-2 bounds own-source
density per rule. AM-4c is the ratio of two quantities that are each
already targeted; any threshold on it would be implied by those two or
would contradict them.
Measured at withdrawal: **1,426** own lines per 100k third-party (shipped
runtime), **1,107** (dev toolchain).
**Decided before Phase B, deliberately.** ADR-0005 predicts own-source
growth from the kernel work, which will move this ratio. Setting a
threshold after seeing that movement would be the retarget InnerLoop
§Step 4 forbids — so the decision was taken while the number was still
unaffected by the work that will change it.
**M-D1-MUT keeps AM-4c in its denominator.** Withdrawing a row would
otherwise improve the metric from 7/14 to 7/13 without enforcing anything —
a score improved by deleting the question.
2026-07-31 01:43:31 +02:00
Comparisons against the event-sourcing 10⁵– 10⁶/s estimate stay **parity**
until a local Rust comparator is measured (open follow-up from the
adversarial review).
CB-WP-0019 T01/T02: AM-4b asks what a contributor acquires
The two AM-4 budgets had the SAME scope -- one package, no dev edges --
while claiming to bound different things. AM-4b now measures the
workspace with dev edges: 57 crates / 725,258 lines where it read 29 /
317,021, having been blind to 28 crates and 408,237 lines, more source
than its own target.
Target 745,000, ~2.7% of room -- the same margin ADR-0008 D3 gave AM-4a,
applied to a number that grew because the instrument was repaired, not
because anything was added. The target moved to fit the measurement.
T02: proc-macros are COUNTED here and excluded from AM-4a, on purpose.
AM-4a asks what ships and a proc-macro never ships. AM-4b asks what is
acquired, and ADR-0007 D3's acquisition rule counts what the build
fetches -- 'it does not ship' is no answer to 'we downloaded it'. When
the rules disagree, the question each budget asks decides. Measured
share 109,585 lines / 15.1% against AM-4a's 36.2%, so ADR-0008 D2's
refusal to borrow the ratio was right by more than a factor of two.
Caught by this project's own earlier work twice: the mutation
find-string went stale and --self-test reported it BUILD-FREE (the check
CB-WP-0015 added after AM-4a's rotted for two passes), then the DFD gate
caught facts.toml carrying the old numbers.
CB-EV-0001 and ADR-0004 carried live fact: tags on historical readings.
A dated record asserting a CURRENT value is a category error, so those
occurrences are marked as-measured instead of retro-edited, and ADR-0004
gains a supersession note.
make all exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:04:54 +02:00
### 5c. What each AM-4 budget asks, and why they differ
*(CB-WP-0019 T01/T02, 2026-08-03. Tier M.)*
The two budgets had the **same scope** — one package, no dev edges — while
claiming to bound different things. That left AM-4b blind to **28 crates
and 408,237 lines**, more source than its own target, and it is how
`quick-js` entered in CB-WP-0014 without moving the number that governs
dependencies (ADR-0009 withdrew its own cost argument over it).
| | the question it asks | scope | proc-macros |
|---|---|---|---|
| **AM-4a** | what does a game **ship** ? | `-p games-ground --no-default-features` | **excluded** |
| **AM-4b** | what does a contributor **acquire** ? | `--workspace --edges normal,dev` | **counted** |
**The proc-macro treatments are opposite on purpose.** AM-4a excludes them
because they run in the compiler and never reach a shipped binary —
counting them in *"what a game ships"* was simply false. AM-4b counts
them, because ADR-0007 D3's acquisition rule counts what the build causes
to be **fetched** , and a proc-macro is fetched, compiled and unaudited on
a contributor's machine like anything else. *"It does not ship"* is no
answer to *"we downloaded it"* .
**When the two rules disagree, the question each budget asks decides.**
That is the rule ADR-0008 D2 left open, and it is why that decision
refused to reuse AM-4a's measured 36.2% share for AM-4b: the real share is
**15.1%** (109,585 lines), so borrowing would have been wrong by more than
a factor of two.
**The target moved to fit the measurement, never the reverse.** 745,000
keeps ~2.7% of room on ADR-0008 D3's reasoning that ~1.5% fails on a
dependency's patch release — the same margin AM-4a received, applied to a
number that grew because the instrument was repaired rather than because
anything was added.
CB-WP-0015: the two inert clauses, AM-7 scaling and AM-8 N=10
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>
2026-08-02 14:07:08 +02:00
### 5b. Where AM-8's ten runs live, and why not everywhere
*(CB-WP-0015 T02, 2026-08-02. Tier S. The spec value N=10 is **not**
amended — this records where it is enforced.)*
For eight passes the runner executed each scenario **twice** (K8) while
this row said ten, and `mutation-check` reported the count inert every
run. Closing it needed an argument about what the extra runs buy, because
"the spec says ten" is not one.
**A deterministic divergence does not need ten runs.** A seed threaded
wrong or a fold that depends on insertion order diverges on run 2 exactly
as reliably as on run 10. For that class K8's double-run is sufficient and
the other eight are repetitions of an answered question — 47 s per build
across 25 scenarios.
**A late-onset or probabilistic divergence does.** Measured, on
`gr-r06-round-resolve` with the RNG perturbed only from its fourth
construction onward: `--runs 2` **passes** ; `--runs 10` fails with *"run 1
hash … != run 4 hash … (of 10)"*. That is a real class the double-run
structurally cannot see, and it is deterministic rather than flaky, so it
can be a control rather than a coin flip.
So both stay, at their own costs: **K8's two runs on every scenario**
(broad, cheap, `make sim` ) and **ten runs on one scenario** (deep, ~2 s,
`make am8` ). The clause is now measured by that mutation rather than
declared by its author.
The primary defence against the probabilistic class remains the
`HashMap` /`HashSet` deny lint under K6 — this row's other clause, already
live. The ten runs are defence in depth against that exclusion failing,
which is why one workload's worth is proportionate.
2026-07-31 01:43:31 +02:00
## 5. Out of scope for this pass
Networking/session protocol, WIT/Wasm game boundary, ECS world layer,
rendering, persistence beyond file snapshots, host migration, and any
second game. Each arrives through its own loop pass with its own survey.