#!/usr/bin/env python3 """Record the outcome of a check and move the finding along the ladder. python3 tools/record_check.py RISK-F-0002 clean python3 tools/record_check.py RISK-F-0002 moved "flex-auth shipped the fix" python3 tools/record_check.py RISK-F-0002 defer 2026-09-01 "operator: after the migration" `clean` climbs one rung, `moved` resets to `instant`, `defer` parks it until a date by explicit operator decision. Writes the front-matter and appends a dated line to the finding's Reviews section — the check is not recorded until it is written down, which is the same rule the register applies to everyone else. """ from __future__ import annotations import datetime as dt import os import re import sys import register_lib as lib def fail(msg: str) -> None: print(msg) raise SystemExit(2) def main(argv: list[str]) -> int: if len(argv) < 2: fail(__doc__) fid, outcome, rest = argv[0], argv[1], argv[2:] matches = [f for f in lib.findings() + lib.regulatory() if f.get("id") == fid] if not matches: fail(f"no finding with id {fid}") f = matches[0] path = f["_path"] text = path.read_text(encoding="utf-8") now = lib.now() stamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") rung = f.get("cadence", "instant") if outcome == "clean": new_rung = lib.climb(rung) streak = int(f.get("clean_streak", 0)) + 1 nxt = now + lib.interval(new_rung) note = rest[0] if rest else "checked, nothing moved" line = f"- **{now:%Y-%m-%d}** — clean check: {note}. Cadence {rung} → {new_rung} ({streak} clean in a row); next check {nxt:%Y-%m-%d %H:%MZ}." defer = "" elif outcome == "moved": if not rest: fail("`moved` needs a reason: what changed") new_rung, streak, nxt = lib.reset(), 0, now line = f"- **{now:%Y-%m-%d}** — not clean: {rest[0]} Cadence {rung} → instant; checked again immediately." defer = "" elif outcome == "defer": if len(rest) < 2: fail("`defer` needs a date and the operator's reason") until, why = rest[0], rest[1] new_rung, streak, nxt = rung, int(f.get("clean_streak", 0)), lib.moment(until) line = f"- **{now:%Y-%m-%d}** — deferred to {until} by explicit operator decision: {why}" defer = until else: fail("outcome must be one of: clean, moved, defer") subs = { "checked_by": os.environ.get("RISK_CHECKED_BY", os.environ.get("USER", "unknown")), "last_checked": stamp, "next_check": nxt.strftime("%Y-%m-%dT%H:%M:%SZ"), "cadence": new_rung, "clean_streak": str(streak), } if defer: subs["deferred_to"] = defer for key, value in subs.items(): quoted = f'"{value}"' if key not in ("cadence", "clean_streak") else value if re.search(rf"(?m)^{key}:", text): text = re.sub(rf"(?m)^{key}:.*$", f"{key}: {quoted}", text) else: text = text.replace("\n---\n", f"\n{key}: {quoted}\n---\n", 1) if "\n## Reviews\n" in text: text = text.rstrip("\n") + "\n" + line + "\n" else: text = text.rstrip("\n") + "\n\n## Reviews\n\n" + line + "\n" path.write_text(text, encoding="utf-8") print(f"{fid}: {outcome} — cadence {rung} → {new_rung}, next check {subs['next_check']}") print("REGISTER.md is stale; run `make register`.") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))