clay-borg/specs/GameKernel.md
tegwick 145611e3b6 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

11 KiB
Raw Blame 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 (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)

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.

4. Acceptance metrics

On AM-4's retarget (2026-07-31). Ratified by ADR-0004 on 2026-07-31; the headroom argument for why these ceilings bind on future work lives there. AM-4 originally read "≤20 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-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 (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)
AM-11 M-D4-SWAP (unmet 2026-07-31 — the pair exists, the suite does not): 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.