clay-borg/decisions/ADR-0004-am4-ratification.md
tegwick 53c460c992 CB-WP-0004 T04: fact registry and make facts-check — DFD gets a gate
Duplicated-fact drift is the fourth error class and the only one with no
executable rule. No positive control catches it (both copies are
internally consistent) and re-derivation does not either (the copy
reproduces whatever it was copied from). It is caught only by reading a
copy against its source, which nothing in the loop required.

facts.toml holds 15 facts and is GENERATED by `make facts-gen` from
cb-cost, dep-weight and rule-coverage. The trap this task named — a
hand-maintained registry that becomes another drifting copy — is closed
by facts-check re-running the instruments and failing when the committed
registry disagrees with them. A stale registry cannot certify stale
artifacts.

An artifact quoting a fact tags it: **$93.15** <!-- fact --> with the key.
17 occurrences across 5 artifacts are now checked.

Falsified before being believed: changing CostAccounting.md line 158 from
$93.15 to $92.87 — the exact historical drift — produced exit 1 naming
the file, the line and the expected value. Tested against the class it
exists to catch, on a real artifact, not only in its self-test.

It then caught a live tag inside its own documentation example in
InnerLoop.md within the hour. Third time a gate has failed on its own
pass's work.

What it does not close is stated rather than implied: 22 untagged literal
copies remain and are reported, not failed. Tagging is opt-in, a number
can legitimately recur, and a gate that fires on coincidence gets routed
around. Naming the uncovered surface beats claiming the class is closed.

InnerLoop single-source-of-fact moves from prose to executable — v1.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 10:24:39 +02:00

88 lines
4.1 KiB
Markdown

# ADR-0004: ratify AM-4a and AM-4b
status: accepted
date: 2026-07-31
decided by: maintainer (Bernd Worsch), 2026-07-31
tier: S (structural S — ratifies an existing target, creates no capability; chaos d4=3)
supersedes: nothing; discharges the open item raised by CB-WP-0003 T07
references: [GameKernel.md](../specs/GameKernel.md) §4,
[InnerLoop.md](../specs/InnerLoop.md) §Step 4 (correction vs retarget),
`history/260731-inner-loop-rule-audit.md`
## Why this ADR exists
CB-WP-0003 T07 added a mechanical test separating a **correction** (the
instrument disproved the target; the implementation did not change) from a
**retarget** (the same commit moved both the target and the code it
measures). AM-4a/AM-4b failed that test: they were set at 250,000 and
350,000 in commit `4be6e02`, by the implementer, after seeing the measured
246,250 — and that commit also changed the feature gating the metric
measures.
The reasoning was recorded at the time and is defensible. The *structure*
was not, and the audit flagged both targets as unratified: `make
dep-weight` has been enforcing thresholds no reviewed decision stood
behind.
## Decision
**AM-4a (≤ 250,000 lines, shipped runtime) and AM-4b (≤ 350,000 lines,
dev toolchain) are ratified as written.** No values change.
## The old target, and why it was abandoned
AM-4 originally read **≤ 20 transitive crates**, set against
boardgame.io's 120 npm packages. Retired for two measured reasons:
1. **Unreachable without undoing this spec's own contracts.** K5 (seeded
ChaCha) and K7 (SHA-256) cost 12 crates between them. The measured
ladder showed nothing reached 20 except reimplementing one of those
primitives — trading an audited cryptographic implementation for a
scoreboard number.
2. **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, for the same reason.
## The measurement that motivated the change
At the time of the retarget, `make dep-weight`:
| configuration | crates | third-party LOC |
|---|---|---|
| shipped-runtime (`--no-default-features`) | 23 | **246,250** | <!-- fact:am4a_loc -->
| dev-toolchain (default features) | 29 | **317,021** | <!-- fact:am4b_loc -->
| own source | — | 3,443 |
## Why these targets bind on future work rather than merely passing present work
This is the question T07 requires an ADR to answer, and it is the reason
ratification is defensible rather than a rubber stamp.
- **AM-4a leaves 3,750 lines of headroom — 1.5%.** That is roughly one
small crate. Any dependency added to the shipped runtime breaches it
almost immediately, which is the intended behaviour: the shipped runtime
is meant to be effectively frozen, and the target enforces that a new
runtime dependency is a decision someone must argue for, not a default.
- **AM-4b leaves 32,979 lines — 10.4%.** Deliberately looser. The dev
toolchain is where scenario YAML, benchmarking, and future tooling land,
and it does not ship to a player. It should be able to absorb one
moderate dependency without a spec change, and not two.
- **Both are ceilings on a quantity that only grows by choice.** Nothing
drifts a project across these thresholds; only adding a dependency does.
A target that can only be breached deliberately is a target that binds.
**What would falsify this ratification:** if a future pass finds itself
raising AM-4a to accommodate a dependency it wanted, that is the failure
mode the ceiling exists to catch, and the answer is a new ADR arguing for
the dependency — not a quiet retarget.
## Consequences
- The open item in `history/260731-inner-loop-rule-audit.md` is discharged.
- `make dep-weight` continues to fail the build on breach, now backed by a
reviewed decision.
- AM-4c (own source per 100k third-party lines) remains **reported, not
targeted**, and is unaffected.
- Precedent: this is the first ADR written under the correction/retarget
test. Future retargets follow this shape — old target, the measurement,
and an argument about *future* binding rather than present passing.