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

218
tools/gate-review.py Normal file
View file

@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""Which control gates are due for a keep-or-kill argument? (ADR-0006 D3)
CB-WP-0009 T02. Six passes produced five standing control mechanisms and
no way to retire any of them. Gates accumulate monotonically because each
one was justified once, at the moment it was cheapest to justify.
This reads `gates.toml` and reports two things:
* gates past `review_by` the date someone said they would argue for
keeping it;
* gates whose `caught` list is **empty** which is not proof a gate is
useless (it may be preventing rather than missing), but is the
argument that has to be made out loud rather than never.
It **reports**. It does not fail the build, for CB-RES-0005 §4's reason:
a gate that blocks the remedy when the metric breaches is a trap. The
only failure exit here is a broken registry a file that cannot be read
would otherwise report "0 gates, all healthy".
Usage:
python3 tools/gate-review.py
python3 tools/gate-review.py --self-test
"""
import datetime
import os
import sys
from repo import ROOT, enter_root
REGISTRY = os.path.join(ROOT, "gates.toml")
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
import tomli as tomllib
class Fail(Exception):
pass
REQUIRED = ("id", "name", "checks", "added", "review_by", "retire_if")
def load(path=REGISTRY):
"""Every gate, validated. A registry that parses but says nothing is
the harness-does-nothing failure this project keeps finding."""
if not os.path.exists(path):
raise Fail(f"{os.path.relpath(path, ROOT)} is missing")
with open(path, "rb") as fh:
data = tomllib.load(fh)
gates = data.get("gate") or []
if not gates:
raise Fail("registry parsed but contains no gates")
for g in gates:
missing = [k for k in REQUIRED if not g.get(k)]
if missing:
raise Fail(f"gate {g.get('id', '?')!r} is missing {missing}")
for field in ("added", "review_by"):
try:
datetime.date.fromisoformat(g[field])
except ValueError as e:
raise Fail(f"gate {g['id']!r} has a bad {field}: {e}") from e
return gates
def make_targets(path=None):
"""Targets declared in the Makefile, so an entry cannot name a
command that does not exist."""
path = path or os.path.join(ROOT, "Makefile")
targets = set()
for line in open(path):
if line and not line[0].isspace() and ":" in line and not line.startswith("."):
name = line.split(":", 1)[0].strip()
if name and " " not in name:
targets.add(name)
return targets
def report(today=None):
today = today or datetime.date.today()
gates = load()
targets = make_targets()
overdue, quiet, broken = [], [], []
for g in gates:
if datetime.date.fromisoformat(g["review_by"]) <= today:
overdue.append(g)
if not g.get("caught"):
quiet.append(g)
target = g.get("target") or ""
if target and target not in targets:
broken.append((g, target))
print("gate review — every gate is an experiment (ADR-0006 D3)")
print(f" registry {len(gates)} gate(s), {REGISTRY.split('/')[-1]}")
print(f" today {today.isoformat()}")
print("\n gates")
for g in gates:
due = datetime.date.fromisoformat(g["review_by"])
days = (due - today).days
mark = "DUE " if days <= 0 else ("soon" if days <= 30 else "ok ")
print(f" [{mark}] {g['id']:<14} {g['name']:<28} "
f"review {g['review_by']} ({days:+}d) caught {len(g.get('caught', []))}")
if quiet:
print("\n caught nothing yet — not a verdict, an argument that is owed")
for g in quiet:
print(f" {g['id']:<14} retire if: {g['retire_if']}")
if overdue:
print("\n DUE for a keep-or-kill argument")
for g in overdue:
print(f" {g['id']:<14} {g['retire_if']}")
if broken:
print("\n registry drift — entry names a target the Makefile lacks")
for g, target in broken:
print(f" {g['id']:<14} target {target!r}")
print(f"\n {len(overdue)} due, {len(quiet)} silent, {len(broken)} drifted")
print(" reporting only — never fails the build (CB-RES-0005 §4)")
return 0
def self_test():
"""Each check pins a way this tool could report a comfortable lie."""
results = []
def check(name, ok, detail=""):
results.append((name, ok, detail))
gates = load()
check("the real registry loads", len(gates) >= 5, f"{len(gates)} gate(s)")
check("every gate names what would retire it",
all(len(g["retire_if"]) > 20 for g in gates))
# Registry drift is the failure this exists to prevent: a gate added
# to the Makefile with no entry, or an entry for a deleted target.
targets = make_targets()
named = [(g["id"], g["target"]) for g in gates if g.get("target")]
check("every named target exists in the Makefile",
all(t in targets for _i, t in named),
", ".join(f"{i}:{t}" for i, t in named if t not in targets) or "all present")
check("Makefile targets were actually parsed", len(targets) >= 15,
f"{len(targets)} target(s)")
import tempfile
def registry(text):
fh = tempfile.NamedTemporaryFile("w", suffix=".toml", delete=False)
fh.write(text)
fh.close()
return fh.name
# A registry that parses to nothing must abort, not report "0 due".
empty = registry("# no gates here\n")
try:
load(empty)
check("an empty registry aborts", False, "reported a clean bill")
except Fail:
check("an empty registry aborts", True)
finally:
os.unlink(empty)
# A missing required field must abort rather than be treated as absent
# evidence — `caught` is optional, the rest are not.
partial = registry('[[gate]]\nid = "X"\nname = "n"\nchecks = "c"\n'
'added = "2026-01-01"\nreview_by = "2026-02-01"\n')
try:
load(partial)
check("a gate with no retire_if aborts", False, "accepted")
except Fail:
check("a gate with no retire_if aborts", True)
finally:
os.unlink(partial)
# The overdue arithmetic must actually fire.
import io
from contextlib import redirect_stdout
buf = io.StringIO()
with redirect_stdout(buf):
report(today=datetime.date(2099, 1, 1))
late = buf.getvalue()
check("a far-future date marks every gate due",
late.count("[DUE ]") == len(gates),
f"{late.count('[DUE ]')} of {len(gates)}")
buf = io.StringIO()
with redirect_stdout(buf):
report(today=datetime.date(2020, 1, 1))
early = buf.getvalue()
check("a far-past date marks none due", "[DUE ]" not in early)
check("silent gates are named either way", "caught nothing yet" in early)
print("gate-review self-test (positive control)")
ok = True
for name, passed, det in results:
print(f" [{'ok ' if passed else 'FAIL'}] {name}" + (f"{det}" if det else ""))
ok &= passed
return 0 if ok else 1
def main():
enter_root()
try:
if "--self-test" in sys.argv:
return self_test()
return report()
except Fail as e:
print(f"gate-review: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

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"