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

223 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
```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.
---
## 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
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 | — | **WITHDRAWN from the acceptance table 2026-08-01 (CB-WP-0006 T04)** — retained as a reported diagnostic in `make dep-weight`; see §5a | diagnostic |
| 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 |
### 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.
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.