clay-borg/tools/gate-review.py
tegwick cd2dc5380a 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>
2026-08-01 15:42:13 +02:00

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())