CB-WP-0009-T02: gates.toml and make gate-review

ADR-0006 D3. Nine standing control gates now say what they check, what
they have caught with pointers, when their keep-or-kill argument is due,
and what would retire them. make gate-review reports what is overdue and
what has caught nothing; it never fails the build, for CB-RES-0005 §4's
reason.

Drift is checked in both directions and both are pinned by self-tests: a
dependency of `make all` that is neither a registered control gate nor
listed in not_control_gates is a loop-lint finding, so a new gate cannot
acquire permanence without a review date, and an entry naming a target
the Makefile lacks is a finding too.

First run: 0 due, 2 silent. The silent two are the chaos roll, whose
12-declaration window exists precisely to find out, and gate-review
itself, which is not exempt from its own rule — if it has retired,
tightened or forced the re-justification of nothing by 2026-12-31 it is
a ritual and goes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-01 15:42:13 +02:00
parent 38f237fc5c
commit cd2dc5380a
5 changed files with 436 additions and 2 deletions

View file

@ -162,12 +162,61 @@ def check_reporting_tools_self_test(root=REPO):
return out
def check_gate_registry(root=REPO):
"""ADR-0006 D3 — every control gate is in `gates.toml`, and every
entry names a real target.
The failure this prevents is drift in the direction nobody notices: a
gate added to `make all` with no registry entry never acquires a
review date, which is how five mechanisms accumulated with no way to
retire any of them.
"""
out = []
registry = os.path.join(root, "gates.toml")
makefile = os.path.join(root, "Makefile")
if not (os.path.exists(registry) and os.path.exists(makefile)):
return out
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
return out
with open(registry, "rb") as fh:
data = tomllib.load(fh)
gates = data.get("gate") or []
if not gates:
return [Finding("gates", "gates.toml", "registry contains no gates")]
registered = {g.get("target") for g in gates if g.get("target")}
exempt = set(data.get("not_control_gates") or [])
text = open(makefile).read()
m = re.search(r"^all:(.*)$", text, re.M)
deps = m.group(1).split() if m else []
targets = {ln.split(":", 1)[0].strip() for ln in text.splitlines()
if ln and not ln[0].isspace() and ":" in ln and not ln.startswith(".")}
for dep in deps:
if dep not in registered and dep not in exempt:
out.append(Finding(
"gates", "gates.toml",
f"`make all` runs {dep!r}, which is neither a registered "
f"control gate nor listed in not_control_gates — classify it, "
f"so it cannot acquire permanence without a review date"))
for target in sorted(registered):
if target not in targets:
out.append(Finding(
"gates", "gates.toml",
f"entry names target {target!r}, which the Makefile lacks"))
return out
CHECKS = (
check_loadability,
check_evidence_no_unmeasured,
check_survey_tier_and_chaos,
check_review_trail,
check_reporting_tools_self_test,
check_gate_registry,
)
@ -196,6 +245,29 @@ def self_test():
len(f) == 1 and "Big.md" in f[0].path,
f"{len(f)} finding(s)")
# gates: an unclassified `all:` dependency trips, and so does an
# entry naming a target the Makefile lacks.
with open(os.path.join(tmp, "Makefile"), "w") as fh:
fh.write("all: coverage newthing\ncoverage:\n\techo\n")
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
fh.write('not_control_gates = []\n\n[[gate]]\nid = "G"\n'
'name = "n"\ntarget = "coverage"\nchecks = "c"\n'
'added = "2026-01-01"\nreview_by = "2026-02-01"\n'
'retire_if = "r"\n')
f = check_gate_registry(tmp)
check("gate registry detects an unclassified all: dependency",
len(f) == 1 and "newthing" in f[0].detail, f"{len(f)} finding(s)")
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
fh.write('not_control_gates = ["newthing", "coverage"]\n\n[[gate]]\nid = "G"\n'
'name = "n"\ntarget = "ghost"\nchecks = "c"\n'
'added = "2026-01-01"\nreview_by = "2026-02-01"\n'
'retire_if = "r"\n')
f = check_gate_registry(tmp)
check("gate registry detects an entry naming a missing target",
len(f) == 1 and "ghost" in f[0].detail, f"{len(f)} finding(s)")
os.unlink(os.path.join(tmp, "gates.toml"))
os.unlink(os.path.join(tmp, "Makefile"))
# evidence: a table verdict trips; the word in prose does not.
with open(os.path.join(tmp, "evidence", "E.md"), "w") as fh:
fh.write("| AC-1 | x | unmeasured |\n"