ADR-0100: ADR conformance as a measured contract¶
Status: Accepted Date: 2026-06-30 Enforcement: enforced Enforced by: pytest:tests/test_adr_conformance_coverage.py
Precedent: Design by Contract (Meyer, Object-Oriented Software Construction, 1988/1997) and executable-specification / fitness-function conformance
Divergence: Design by Contract checks a contract at runtime against a running program, but here the "program" is a corpus of decision records that generative agents read as ground truth, so conformance is a side-effect-free check resolved at merge (the coverage ratchet) and re-executed post-merge (AdrConformanceLoop), and a recurring failure flips interpretation from "code drifted" to "the decision itself should be superseded" (receipt: ADR-0093, ADR-0046)
Context¶
HydraFlow has a dependency link between an ADR and the code it governs: adr_drift.py cites modules and trips a tripwire when they change (ADR-0056). It does not have a conformance link: a way to ask "does the decision still hold?" and get a machine answer. ADR-0093 pioneered a one-off **Enforced by:** frontmatter line pointing at a test, but that line was unparsed, unvalidated, and rendered nowhere — a convention by convention, not by construction.
This matters because agents read Accepted ADRs as ground truth. adr_index.render_full injects ADR prose directly into plan-phase prompts. An ADR whose invariant has silently drifted is worse than no ADR at all: it is a confidently wrong instruction. The fitness-function pattern is already proven three times over in this repo (wiki json:entry blocks, ADR source-citations, the loop_fitness ratchet from ADR-0093). The one architectural layer without a conformance contract is the ADRs themselves, the decisions everything else defers to.
This ADR supersedes the pre-existing "P3" **Enforced by:** convention introduced by PR #8398: a bare **Enforced by:** line (values like (none), (process), (historical)) validated only by tests/test_adr_enforcement.py, with no typed grammar and no resolution check. That test is deleted; tests/test_adr_conformance_coverage.py is the single validator going forward, and the **Enforced by:** line is now paired with a required **Enforcement:** kind (enforced / manual / decision-of-record) instead of standing alone.
Decision¶
The contract surface¶
Every Accepted ADR (outside a shrinking grandfather list) declares an **Enforcement:** kind and, where required, an **Enforced by:** list of typed-prefix checks (pytest:, make:). The three kinds map onto ADR-0093's FitnessKind: enforced (asserts a runnable invariant) is the SCORED analogue, decision-of-record (a choice with no runtime predicate) is the HOUSEKEEPING analogue, and manual (a real guardrail that is human-verified, not machine-run) sits as a new middle kind with no equivalent in the loop-fitness lattice. An unrecognized or absent value normalizes to unknown, which fails the ratchet. Executed checks must be side-effect-free: a check that mutates the repo would have the very act of measuring conformance rewrite files, which violates the purity spirit this ADR inherits from ADR-0093.
Two seams, two lifecycle points¶
The parsed Enforcement / Enforced by fields are read by two independent seams that never depend on each other's output:
- The coverage ratchet (
tests/test_adr_conformance_coverage.py) is a pre-merge, CI-blocking PR gate. It never executes a check; it resolves each one by AST/file inspection — apytest:node's file exists and defines the function or class, amake:target is present in theMakefileand is not on the mutating-target denylist. A green PR proves every declared link resolves and is safe to run. AdrConformanceLoop(ADR-0029 caretaker shape) is a post-merge drift watch onmain. On a slow cadence it actually executes eachenforcedADR's checks, persists PASS/FAIL/UNRESOLVED results to.hydraflow/metrics/{repo_slug}/adr_conformance.jsonl, regeneratesdocs/arch/generated/adr-conformance.md, and emitsADR_CONFORMANCE_UPDATE.
The ratchet proves the wiring is sound at merge time; the loop proves the decision still holds over time. Neither one can substitute for the other: a check that resolves can still fail once the code around it drifts, and a check that fails once can still resolve.
Split-class remediation¶
A red check has more than one plausible cause, and the loop does not guess which:
UNRESOLVEDwith a confirmed rename (the named check exists under a new identity: an identical test node moved, a target renamed) is handled by filing a dedup'd issue that proposes the re-point — the loop computes the newEnforced by:identity but does not edit the ADR itself; a human or the pipeline applies it. No confident match means the guardrail was deleted, not moved, and the outcome is treated asFAIL.FAILon first occurrence files one dedup'd remediation issue with pipeline labels, the same way any other pipeline defect enters the system (ADR-0002).FAILthat recurs past a per-ADR attempt budget (default 3 attempts, reusingConvergenceLedgeroscillation semantics) flips interpretation: the pipeline's repeated inability to conform the code is evidence that the decision itself, not the code, is the problem. That case escalates toadr_revieweras a supersession proposal.
The ambiguity between "code drifted" and "decision moved on" is real and the loop does not resolve it by classification — a FAIL exit code cannot distinguish the two causes. It resolves it by recurrence: assume conform-the-code on the first failure, and only promote to supersession-candidate once that assumption has been tested and has failed repeatedly. This is the same posture ADR-0046 takes toward "who decides the watcher is wrong": termination by an external, bounded process, not by an internal guess.
The no-rubber-stamping guardrail (load-bearing)¶
A red conformance check has exactly two legitimate ways to go green: the code is fixed, or a human/reviewer supersedes the decision. There is an illegitimate third way — weaken the check or quietly relax the ADR's own fields — and this ADR makes that path structurally unavailable rather than merely discouraged.
AdrConformanceLoop's entire write surface is three kinds of GitHub issue: an issue proposing a re-point of Enforced by: on a confirmed rename, a remediation issue (filed or updated), and an adr_reviewer supersession proposal. It never edits repo files: no code path touches a check's assertion body, an ADR's Enforced by: line, or an ADR's Enforcement: / decision-of-record fields, and a unit test asserts the loop cannot mutate any file under src/, tests/, or docs/. The jsonl it persists is gitignored scratch state, not a decision artifact. Nothing in this design lets the loop mark itself green by softening what green means.
AdrConformanceLoop is on the ADR-0046 recursion ladder¶
FitnessScorecardLoop (ADR-0093) is read-only: it observes and mutates nothing, so ADR-0046's "who watches the watcher" question does not apply to it — there is no behavior change for a meta-observer to bound. AdrConformanceLoop is different in kind: it mutates by filing GitHub issues (never by editing repo files). That makes it a producer in the ADR-0046 sense, and it sits on the recursion ladder rather than beside it. It is bounded the same way every other mutating caretaker loop is bounded: the state rollup (get_adr_conformance_rollup / set_adr_conformance_rollup) caps it at one open remediation issue per ADR by gating create-vs-update on the recorded issue number, a per-ADR attempt budget caps how long it keeps re-filing before escalating to supersession, and enabled_cb("adr_conformance") (ADR-0049) is the kill switch that stops it outright. A DedupStore entry is also written alongside the rollup as a secondary dedup marker, consistent with the sibling AdrTouchpointAuditorLoop, but it is not the operative suppression gate. No new recursion-termination mechanism was invented for this loop; it reuses the ladder's existing rungs.
Consequences¶
- Every Accepted ADR outside the grandfather list must declare a resolving
Enforcement. The grandfather list is committed and monotonically shrinking (test_grandfather_only_shrinksfails CI on any growth), so backfill is incremental and cannot be dodged by widening the exemption. enforcedchecks must be side-effect-free. A mutating make target (e.g.lint-ul,arch-regen) cannot be cited; a--check-mode variant must exist or be written.- The dependency tripwire (ADR-0056,
adr_drift.py) and the conformance contract (this ADR) are complementary, not redundant: one flags that governed code changed, the other proves the governing decision still holds. AdrConformanceLoopis a mutator on the ADR-0046 ladder, so any future audit of "which loops need dedup + attempt-budget + kill-switch bounding" must include it alongside the other producer loops, not alongside the read-only observers.- This ADR is itself Exhibit A: it is
enforced, citestests/test_adr_conformance_coverage.py, and its own coverage entry is proved the same way as every other ADR the ratchet checks — no ADR earns a manual exemption from the mechanism it defines. - The P3
**Enforced by:**-only convention (PR #8398) and its validatortests/test_adr_enforcement.pyare retired.docs/adr/README.md's Enforcement section and every future Accepted ADR use the**Enforcement:**+**Enforced by:**pairing this ADR defines.
References¶
- ADR-0093 — Loop fitness as a measured contract. Sibling ADR and structural template;
FitnessKindis the latticeEnforcementkinds map onto. - ADR-0056 — ADR touchpoint gate. The dependency tripwire (
adr_drift.py) this conformance contract complements: one watches for code change, the other watches for decision drift. - ADR-0029 — Caretaker Background Loop Pattern.
AdrConformanceLoopfollows this shape:BaseBackgroundLoopextension, gated_do_work(), stats-dict tick. - ADR-0049 — Trust-loop kill-switch convention.
AdrConformanceLoop._do_work()gates onenabled_cb("adr_conformance"). - ADR-0046 — Meta-observability with bounded recursion.
AdrConformanceLoopmutates, so unlikeFitnessScorecardLoopit sits on the recursion ladder, bounded by dedup, attempt budget, and the kill switch. - ADR-0002 — Labels as state machine. Remediation issues enter the pipeline the same way any other defect does: through labels.
src/adr_index.py— parse host forEnforcement/Enforced by.tests/test_adr_conformance_coverage.py— the CI-blocking coverage ratchet (this ADR's own enforcement).src/adr_conformance.py/src/adr_conformance_loop.py— pure result model and producer loop.