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` .
- **K14** The GROUND round (GR-R01..R09) is expressed through runtime
primitives: one commit window per Select, reveal, then fixed-order
resolution steps with Lead-order iteration (GR-R06/R07) driven by
system commands.
### 2.6 GROUND aggregate (`games/ground`)
- **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 |
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
| AM-4a | M-D2-DEP: third-party LOC, **shipped runtime** (`--no-default-features` ) | boardgame.io: 120 npm packages / 3.9M LOC | ** ≤ 250,000 lines** | measured (`make dep-weight` ) |
| AM-4b | M-D2-DEP: third-party LOC, **dev toolchain** (default features) | as above | ** ≤ 350,000 lines** | measured (`make dep-weight` ) |
| AM-4c | M-D2-DEP: own source per third-party 100k lines | — | reported, not targeted | measured (`make dep-weight` ) |
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 |
| 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 |
| 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 (invariant, not a verdict row) |
| 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) |
| AM-10 | M-D4-LEAK: foreign types in `cb-*-api` -visible signatures | boardgame.io: JS-ecosystem-locked | **0** | measured (grep/deny rule) |
| AM-11 | M-D4-SWAP: 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) |
| 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 |
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).
## 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.