ADR-090
Structural doctrine enforcement — gates project the SSOT
accepted · 2026-06-10 · L0 L0.5 L1 L2 L3 L4 L5 · cites 6
0ADR-090: Structural doctrine enforcement — gates project the SSOT #
Context #
ADR-003's thesis is "enforcement must be mechanical, not social". Yet several load-bearing invariants were still honor-system — recorded in prose (an ADR, an audit table, an ownership matrix) but with no gate that fails when the prose is violated. A self-audit (2026-06-10) found four such gaps:
- Gate 5 (mutation ≥90%) — checked only that the crate-spec mentioned
"Mutation". No
cargo-mutantswas actually run/floored at admission. - ADR-081 computer-use guards — "MANDATORY-at-admission" for the L1 crates that touch screen capture / accessibility secure-fields, but nothing failed if a guard-owner crate shipped without its guard.
- Supply-chain policy —
deny.toml+cargo-denywere configured but unwired; onlycargo audit(advisories) ran, not bans/licenses/sources. - Error one-voice — every error enum was migrated to the canonical
NikaErrorCodetrait (recorded in a completeness audit table), but a new error enum could skip the trait silently.
The common shape: a doctrine with a written SSOT (audit table · ownership
matrix · deny.toml policy · the 12-gate list) but no projection of that
SSOT into a pass/fail gate.
Decision #
Every doctrine invariant that can be mechanically checked gets a structural gate, and the gate PROJECTS an existing SSOT rather than hardcoding a parallel list. This makes the doctrine evolutive (edit the SSOT, the gate follows) and antifragile (a regression that violates the doctrine turns a gate RED).
Four gates landed under this principle (2026-06-10):
- Gate · SSOT it projects · Invariant
- `scripts/ci/check-mutation-floor.sh` · crate-spec Gate-5 row · real Gate 5 — `cargo-mutants` kill-ratio ≥ floor; BUDGET mode reads a `<!-- GATE5-EXEMPT: N -->` spec marker (survivors ≤ N)
- `scripts/hygiene/check-adr-081-guards.sh` (vector 35) · the ADR-081 ownership matrix · every MANDATORY guard whose owner-crate is a workspace member has an impl-binding (`scripts/ci/adr-081-guard-manifest.tsv`) + impl/test markers
- `scripts/hygiene/check-cargo-deny.sh` (vector 34) · `deny.toml` · full `cargo deny check` (advisories + bans + licenses + sources)
- `scripts/hygiene/check-error-one-voice.sh` (vector 37) · `error-trait-completeness-2026-06-10.md` → `error-one-voice-allowlist.tsv` · every thiserror enum in admitted-crate src impls `NikaErrorCode` or is a documented exemption
Each gate carries an orphan/parity check so its projected SSOT can't rot
(a manifest/allowlist row must map to a real matrix row / real enum). Vector 36
(check-unused-deps.sh, cargo-machete) shipped in the same arc.
Consequences #
Positive #
- The four doctrines are now structurally enforced — a future under-pressure dev can't let them rot silently (ADR-003's thesis, completed).
- Evolutive: adding a guard / error enum / banned dep updates one SSOT; the gate auto-extends. Zero parallel hardcoded lists.
- The ADR-081 guard gate is a forcing-function for the next crate: when
nika-input(M2.4) is admitted, its Guards 1+2 become owed-and-present and the gate goes RED until they ship + test.
Negative #
check-mutation-floor.shandcargo deny checkare minutes-slow → admission/CI-tier, not pre-commit. The 34-vectorcheck-all.shstays fast (the slow gates are invoked per-crate at admission).- BUDGET mode is not reproducible for cross-platform L1 crates: a naive
cargo mutants -- --libmutates the cfg'd-out other-OS arms too, so the spec's reachable exempt-count (e.g. nika-a11y's 7) does not match the raw macOS survivor count (~31). Calibration needs a per-cratemutants.tomlexclude_re — a deferred-with-trigger follow-up. Until then those crates carry no exempt marker and are not FLOOR-gated.
Neutral #
- The self-audit that found these gaps (and two bugs in the mutation gate as it was first shipped) is itself the antifragile pattern: gates are audited like any other code, and a gate that would false-RED a legit admission is a bug caught before it blocks.
Evidence #
scripts/hygiene/check-all.sh— 34 live vectors (34-37 added this arc)scripts/hygiene/README.md— vector catalog 34-38docs/adr/adr-003-12-gate-admission.md— amendment recording the Gate-5 + ADR-081 closuredocs/architecture/error-trait-completeness-2026-06-10.md— the error one-voice SSOTdocs/adr/adr-081-l1-effect-crate-guard-contract.md— the guard ownership matrixscripts/ci/adr-081-guard-manifest.tsv— the projected guard bindingsscripts/ci/error-one-voice-allowlist.tsv— the projected error-voice allowlist
Alternatives considered #
- Leave it social (memory + reminders) — rejected: ADR-003 already proved the legacy codebase rotted under social enforcement (29 broken MCP aliases lived for months).
- Hardcode the lists in each gate — rejected: a parallel list drifts from the SSOT. Projection (gate reads the matrix / table / policy file) keeps one source.
- One mega-gate — rejected: four orthogonal drift classes, four scripts, single responsibility (mirrors the existing vector design).
Notes #
This ADR records a principle, not a frozen list. As new mechanically-checkable
doctrines appear (e.g. #[must_use] discipline, public-API surface lock via
cargo-public-api, semver-checks), they fall under the same rule: find the
SSOT, project it into a gate, add an orphan check. Gate count is not sacred —
the projection-from-SSOT discipline is.
read at v0.107.0 · the decision record ships with the engine