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>
88 lines
4.1 KiB
Markdown
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.
|