Some checks failed
ci / check (push) Failing after 3s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
382 lines
16 KiB
Markdown
382 lines
16 KiB
Markdown
---
|
||
id: CB-WP-0006
|
||
title: "Instrument the acceptance table, then implement what it exposes"
|
||
status: in_progress
|
||
state_hub_workstream_id: "8a6327cc-fd5c-4e2c-a29b-b437c27d1e71"
|
||
---
|
||
|
||
# Purpose
|
||
|
||
CB-WP-0005 T02 measured M-D1-MUT for the first time:
|
||
|
||
```text
|
||
M-D1-MUT: 4/14 rows enforced
|
||
PARTIAL 2 (AM-7, AM-8 — some clauses live, some inert)
|
||
unmutatable 8 (no property to invert)
|
||
```
|
||
|
||
**4 of 14, against a predicted 10 of 14.** The prediction was wrong by
|
||
2.5×, and wrong about *what* as well as *how much*: CB-RES-0004 diagnosed
|
||
three absent kernel rules, and the measurement found that **more than half
|
||
the acceptance table has no instrument behind it at all**.
|
||
|
||
CB-WP-0005 Phase C was deferred before starting on that basis (T02's stop
|
||
condition, maintainer decision 2026-07-31). It was scoped to five rules —
|
||
K9, K10, K11, K14, K18 — which does not address the eight.
|
||
|
||
This workplan is scoped to what was measured rather than what was
|
||
predicted. **Phase A instruments the unmeasured rows; Phase B implements
|
||
the kernel work inherited from CB-WP-0005 T04–T06.** That order is the
|
||
whole point: the previous pass built a fix before its instrument existed,
|
||
and the instrument then contradicted the fix's premise.
|
||
|
||
Per InnerLoop §Step 4, no target moves in the commit that measures it.
|
||
|
||
## Phase A — instrument the rows that measure nothing
|
||
|
||
## Task: AM-6 — assert the throughput target
|
||
|
||
```task
|
||
id: CB-WP-0006-T01
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "a4513794-2f5f-480c-be30-56658711796f"
|
||
```
|
||
|
||
The sharpest single finding of CB-WP-0005. **Nothing in the workspace
|
||
compares any number to 100,000 events/s** — the project's headline
|
||
throughput claim, reported as `met, 16.5×`. The Criterion bench emits
|
||
throughput; the only asserts in `synthetic.rs` are the stress-gate shape
|
||
and the events-per-round pin.
|
||
|
||
Deliver a standing assertion — a test, not a bench — that measures applied
|
||
events/s on the synthetic workload and fails below the AM-6 target.
|
||
|
||
**The trap, stated up front:** a timing assertion in CI is flaky by
|
||
nature, and the usual response is to loosen it until it never fires, which
|
||
would reproduce exactly the defect being fixed. So the threshold is
|
||
**pinned well below the measured 1.65M/s** (AM-6 asks for 100k), the
|
||
assertion states its own headroom in the failure message, and a loosening
|
||
requires an ADR. A gate tuned until it cannot fire is a gate that counts
|
||
names.
|
||
|
||
**Verified by:** `make mutation-check --row AM-6` goes from `unmutatable`
|
||
to `red`.
|
||
|
||
**Delivered.** `am6_throughput_clears_the_spec_target` in
|
||
`games/ground/src/lib.rs` — a test, not a bench. Best of 3 samples of
|
||
50,000 applied events each.
|
||
|
||
Measured on bnt-lap001 2026-07-31: **341,280 ev/s in debug (3.4× the
|
||
target)**, ~2.4–3.1M in release (~24–30×). **The spec target holds even in
|
||
an unoptimized build**, so the gate needed no `cfg` split and runs in the
|
||
ordinary `make test`.
|
||
|
||
**The trap was avoided by construction, not by intention.** The threshold
|
||
is the spec value `100_000`, untouched; the constant carries a comment
|
||
saying lowering it requires an ADR; and the failure message repeats that,
|
||
states the measured headroom, and names the reference figures — so a
|
||
future agent hitting a red AM-6 is told not to tune it, in the place they
|
||
will actually be reading. Robustness comes from **best-of-N**, not from a
|
||
lower bar: a throughput *floor* asks "is this machine capable", so
|
||
transient load should not fail the build.
|
||
|
||
Two positive controls in the test itself: a run that applied fewer than
|
||
50,000 events, or measured zero elapsed time, fails rather than scoring as
|
||
infinite throughput.
|
||
|
||
**Verified:** `make mutation-check --row AM-6` → **red**, via a *property*
|
||
mutation (4,000 `black_box` iterations injected into `GroundState::fold`,
|
||
the hot path) rather than a threshold tweak — raising the target would only
|
||
prove the comparison runs.
|
||
|
||
**And the FA class is now gated.** `mutation-check` rows gained an
|
||
`expect` field: the mutant's output must contain the row's stated failure
|
||
string, or the verdict is **`WRONG-REASON`**, not `red`. Without it, a
|
||
mutation that merely failed to compile would credit its row with an
|
||
assertion it does not have. Verified by pointing `expect` at a string the
|
||
verifier never prints and confirming the verdict flips. This is
|
||
remedy (2) from the CB-WP-0005 retrospective, built one task earlier than
|
||
T08 planned because the class it guards is the newest and the most
|
||
dangerous.
|
||
|
||
**M-D1-MUT: 4 → 5 of 14.**
|
||
|
||
## Task: AM-2, AM-3 — instrument the size metrics
|
||
|
||
```task
|
||
id: CB-WP-0006-T02
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: "d1572a4f-46e5-49dd-b1ec-7c371a780bb7"
|
||
```
|
||
|
||
AM-2 (≤40 spec lines per rule) and AM-3 (≤50 LOC for the synthetic
|
||
workload) are both `unmutatable` for the same reason: `make loc` prints
|
||
line counts and nothing divides, compares, or gates.
|
||
|
||
AM-2 is the **anti-gaming pair** for AM-1 — 100% rule coverage means
|
||
nothing if the rules are trivially small — so an uninstrumented AM-2
|
||
leaves AM-1 gameable, which is the specific risk MetricsAndScenarios §1
|
||
names when it introduces M-D1-SPL.
|
||
|
||
AM-3 depends on K18 (benches driven from scenario files); if K18 resolves
|
||
toward amending the spec rather than implementing it, AM-3 must be
|
||
restated or withdrawn with an argument rather than left unmeasured.
|
||
|
||
**Delivered — but the two rows resolved differently, and the difference is
|
||
the point.**
|
||
|
||
**AM-2 is instrumented and enforced.** `tools/size-metrics.py` +
|
||
`make size-metrics`, in `make all`:
|
||
|
||
```text
|
||
AM-2: 27.2 LOC/rule [ok target <= 40] (1.47x headroom)
|
||
1,575 code lines before the first #[cfg(test)] / 58 numbered rules
|
||
```
|
||
|
||
Tests are excluded because AM-2 asks what a rule *costs*, not how much it
|
||
is exercised — `lib.rs` is ~18% test code and including it would have
|
||
flattered the number. Verified red by a **property** mutation: ~800 lines
|
||
of filler injected into the impl, pushing the ratio past 40. `expect` is
|
||
the precise failure signature `FAIL target <= 40`, not the row name, which
|
||
would have matched passing output too.
|
||
|
||
**AM-3 is BLOCKED, not uninstrumented — and this is a finding, not a
|
||
deferral.** The row measures "LOC to express the CB-RES-0001 synthetic
|
||
game on our kernel" against a boardgame.io baseline of ~36 LOC for a
|
||
declarative **3p commit/reveal game object**. That artifact has never been
|
||
built: `games/` contains only `ground`, and `benches/synthetic.rs`
|
||
*drives* GROUND rather than *defining* a synthetic game.
|
||
|
||
Measuring GROUND's 1,575 impl lines against a 36-line synthetic game
|
||
object would compare **two different games** and call the difference a D1
|
||
result. So the tool ships the measurement mechanism — a marker-delimited
|
||
`// AM-3:BEGIN` / `// AM-3:END` region, self-tested — and **reports the row
|
||
blocked, naming the missing artifact**. A number here would have been
|
||
worse than a blank.
|
||
|
||
It therefore stays `unmutatable` and **still counts against M-D1-MUT**, per
|
||
ADR-0005 §1: a row that cannot fail asserts nothing, however good the
|
||
reason. Resolving it needs an artifact, not a metric tweak — carried
|
||
forward, not silently dropped.
|
||
|
||
**M-D1-MUT: 5 → 6 of 14.**
|
||
|
||
## Task: AM-5, AM-9 — measure or withdraw, but stop leaving them blank
|
||
|
||
```task
|
||
id: CB-WP-0006-T03
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: "6c84a220-cab3-40f1-b35f-b8df79106f6e"
|
||
```
|
||
|
||
AM-5 (clean build ≤60 s) is `recorded not gated` — and not recorded
|
||
either. AM-9 (peak RSS ≤64 MB) is declared "very unlikely to bind", which
|
||
CB-EV-0001 itself calls out as an unmeasured judgment call.
|
||
|
||
Both are cheap to measure: one `time` around a clean build, one
|
||
`/usr/bin/time -v` around the 100k-event run. **Or withdraw them with an
|
||
argument**, which is a legitimate outcome and possibly the right one for
|
||
AM-5 on a machine-dependent number. What is not legitimate is a third pass
|
||
leaving them blank while the scoreboard reports elsewhere.
|
||
|
||
**Delivered — `tools/runtime-metrics.py`. One passes, one breaches.**
|
||
|
||
**AM-9: met, and comfortably.** 13.4 MB peak RSS against a ≤64 MB target,
|
||
**4.8× headroom**. Gated and in `make all` (`--fast`, ~1 s). The row that
|
||
CB-EV-0001 called "very unlikely to bind" was right — but it is now
|
||
*measured* rather than assumed, and verified red by a property mutation
|
||
(a 300 MB allocation in the workload).
|
||
|
||
**AM-5: BREACHED, on both readings, on the machine the spec names.**
|
||
|
||
```text
|
||
dev toolchain (default features) 87.0 s [FAIL target <= 60 s]
|
||
shipped runtime (--no-default-features) 61.3 s [FAIL target <= 60 s]
|
||
```
|
||
|
||
Measured on `bnt-lap001`, 8 cores — the machine `specs/GameKernel.md` §5
|
||
names, so this is a direct comparison, not a directional one. A row
|
||
declared `recorded not gated` and never recorded turns out, on first
|
||
measurement, to **fail its own target by 45%**.
|
||
|
||
The tool **reports and exits 0**, because the spec says the row is not
|
||
gated. Gating it is a spec change and needs an ADR; a tool that promotes
|
||
itself is how a target starts binding without anyone deciding it should.
|
||
So AM-5 stays `unmutatable` — for the accurate reason now — and the breach
|
||
is **raised as a maintainer decision**, not resolved here. Three honest
|
||
options: speed the build, move the target by ADR (arguing why 60 s was
|
||
wrong rather than why 87 s is convenient), or withdraw the row.
|
||
|
||
**The measurement had a real bug, found by cross-validation.**
|
||
`getrusage(RUSAGE_CHILDREN)` is a high-water mark across *every* reaped
|
||
child, so it attributed `cargo`'s memory to the workload and reported
|
||
**38.2 MB for a run that used 12.3 MB** — a 3× over-report. Fixed with
|
||
`os.wait4`, which returns that specific child's rusage. The self-test now
|
||
**cross-checks against `/usr/bin/time -v`** (13.4 vs 12.4 MB), which is
|
||
the only reason the bug was visible at all: the wrong number was
|
||
plausible, passed its target, and would have been published.
|
||
|
||
That is FA in the measurement layer rather than the mutation layer — an
|
||
instrument confidently reporting a number it had not earned.
|
||
|
||
**M-D1-MUT: 6 → 7 of 14.**
|
||
|
||
## Task: AM-4c — target it or drop it
|
||
|
||
```task
|
||
id: CB-WP-0006-T04
|
||
status: todo
|
||
priority: low
|
||
state_hub_task_id: "a2571314-4c02-41cf-9473-96d3ae2d7b2b"
|
||
```
|
||
|
||
`reported, not targeted`, so there is no property to invert and it counts
|
||
against M-D1-MUT by ADR-0005 §1.
|
||
|
||
Two honest options, and the task must pick one with an argument: give it a
|
||
threshold (making it enforceable), or **remove it from the acceptance
|
||
table** and keep it as a reported diagnostic. A row in an acceptance table
|
||
that cannot fail is decoration, and this pass has learned to say so.
|
||
|
||
Note the incentive: ADR-0005 already predicts own-source growth from the
|
||
kernel work in Phase B, so AM-4c will move against us. Setting its
|
||
threshold *after* seeing that movement would be the retarget InnerLoop
|
||
§Step 4 forbids — so it is set here, before Phase B, or not at all.
|
||
|
||
## Phase B — the kernel work inherited from CB-WP-0005
|
||
|
||
> These three were written in [CB-WP-0005](CB-WP-0005-assertion-coverage.md)
|
||
> Phase C and cancelled unstarted. Their analysis lives **here**, next to
|
||
> the work — CB-WP-0005 keeps only a forward pointer, so there is one copy.
|
||
> The normative controls are in
|
||
> [ADR-0005](../decisions/ADR-0005-assertion-coverage-and-replay.md) §6.
|
||
|
||
## Task: K9 and K11 — durable log, LogStore port, real conformance suite
|
||
|
||
```task
|
||
id: CB-WP-0006-T05
|
||
status: todo
|
||
priority: high
|
||
state_hub_task_id: "2a357dbf-3861-499d-a12e-f7682b7332f6"
|
||
```
|
||
|
||
**K9** — the acceptance property is *snapshot at seq N + events N+1..M ≡
|
||
genesis fold*, hash-compared, on `GroundState`. What exists instead is one
|
||
test round-tripping a `BTreeMap<String, u8>` with `EventSeq(17)` as a
|
||
literal: no game aggregate, no events applied, no from-genesis comparison.
|
||
Mutation-proven — making `Snapshot::take` discard its `EventSeq` leaves the
|
||
test green, so the half of K9 that says "+ the EventId it includes" is
|
||
unverified. `Snapshot` has no callers outside its own test.
|
||
|
||
**K11** — append-only, length-prefixed, versioned framing, with a truncated
|
||
tail **detected**; the operative clause is *detection*, so truncate-by-one-
|
||
byte and corrupt-length-prefix rejection are required, not optional.
|
||
Reimplemented rather than assimilated (ADR-0005 §2): ~100 lines against a
|
||
format Kafka and EventStore converged on independently, adding no
|
||
dependency, charged to **AM-4a** (shipped runtime, 1.5% headroom). The `LogStore` port with a shared
|
||
`fn conformance<S: LogStore>(…)`, retro-fitted to `KernelRng`, is what
|
||
finally earns **AM-11** — currently downgraded to unmet because the pair
|
||
exists and the suite does not.
|
||
|
||
**Verified by:** `make mutation-check --row AM-11` goes from
|
||
`unmutatable` to `red`.
|
||
|
||
## Task: K10 — the replay bundle and `--replay`
|
||
|
||
```task
|
||
id: CB-WP-0006-T06
|
||
status: todo
|
||
priority: high
|
||
state_hub_task_id: "d459444d-41be-40c5-9101-7a8420b42fb6"
|
||
```
|
||
|
||
INTENT design decision 8 of 10, unimplemented. `cb-sim` has no flag
|
||
parsing at all, so `--replay` has nowhere to go yet.
|
||
|
||
**This is not "a directory of four files"** — the D2 correction the
|
||
adversarial review forced. `scenario.rs` creates an `EventLog`, appends to
|
||
it and never reads it: the one production instantiation of the K11 log is a
|
||
write-only sink. `Pass` carries the *end* state, not an initial snapshot,
|
||
and failures are a formatted `String`, not structured expected-vs-actual.
|
||
Plumb the log out of `execute`, capture an initial snapshot, restructure
|
||
`RunOutcome::Failed`. The writer is **dev-only** behind the `scenarios`
|
||
feature and is charged to **AM-4b** (9.4% headroom), not AM-4a.
|
||
|
||
All four controls from ADR-0005 §6 are required verbatim, including the two
|
||
the reviewer supplied: truncate-by-one-byte rejection, and a mutated-seed
|
||
control so the round-trip is capable of failing.
|
||
|
||
Fixing this is also what lets **AM-7's withdrawn `hash-identical`
|
||
clause** be re-earned, since a real replay is what the clause was always
|
||
about.
|
||
|
||
## Task: K14 and K18 — implement, or amend the spec and say why
|
||
|
||
```task
|
||
id: CB-WP-0006-T07
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "d76ad893-520b-44bd-9b77-07dbfb651ce3"
|
||
```
|
||
|
||
Inherited from CB-WP-0005 T06. `CommitWindow` has zero non-test users;
|
||
the bench never touches `ScenarioFile`. Either wire them up or amend the
|
||
rules with a recorded argument. Deleting a rule to make a gate green is
|
||
forbidden; deleting one *with an argument* is legitimate and probably
|
||
right for K18.
|
||
|
||
## Phase C — measure, and answer the question this pass inherited
|
||
|
||
## Task: control loop — M-D1-MUT before and after
|
||
|
||
```task
|
||
id: CB-WP-0006-T08
|
||
status: todo
|
||
priority: high
|
||
state_hub_task_id: "1d4455c3-c642-47d8-916b-ab35bc512207"
|
||
```
|
||
|
||
Commit `evidence/CB-EV-0004`. The baseline is **4 of 14**, committed and
|
||
reproducible via `make mutation-check`.
|
||
|
||
Three tests, all reported:
|
||
|
||
1. **Did the enforced count rise, per row?** Against the per-task
|
||
predictions above, unmet reported as unmet.
|
||
2. **Did any row regress from `red` to `SURVIVED`?** The four currently
|
||
enforced rows are the ones with something to lose.
|
||
3. **Were the new mutations strong?** CB-WP-0005 T02 wrote two no-op
|
||
mutations on its first attempt, either of which would have published a
|
||
false accusation against working code. Every mutation added by this
|
||
pass must be shown to fail for the *stated* reason — record how that
|
||
was checked.
|
||
|
||
## Task: retrospective — is a weak mutation the new grep?
|
||
|
||
```task
|
||
id: CB-WP-0006-T09
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "4f291d5a-90f0-4f1f-bcaf-7ec28856bfcf"
|
||
```
|
||
|
||
The question CB-WP-0005 T08 raised and this pass is positioned to answer.
|
||
|
||
CB-WP-0004 concluded that tooling recovers capacity only where it removes
|
||
the manual path. M-D1-MUT does **not** remove a manual path — writing a
|
||
weak mutation is exactly as easy as writing a strong one, and the harness
|
||
cannot tell the difference. CB-WP-0005 produced two no-op mutations on its
|
||
first attempt, so the failure mode is demonstrated, not hypothetical.
|
||
|
||
**And it fails in the more dangerous direction:** a weak mutation does not
|
||
merely miss a defect, it *indicts working code*. A false `SURVIVED` sends
|
||
the next pass to fix something that is not broken.
|
||
|
||
So: is M-D1-MUT a real instrument, or a name-counter with extra steps? If
|
||
the latter, say so and propose what would actually bind — the candidate
|
||
being that the mutation must be written by someone other than the author
|
||
of the assertion, which is the adversarial-review principle applied one
|
||
level down.
|