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>
218 lines
7.3 KiB
Python
218 lines
7.3 KiB
Python
#!/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())
|