clay-borg/specs/GameKernel.md

187 lines
11 KiB
Markdown
Raw Normal View History

# 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.
## 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
**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)
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`) |
| 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,1001,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.450.66× @2040k, 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.