risk-nexus/tools/record_check.py
tegwick a05ca6822b RISK-WP-0005 finished: the seven gaps closed
T01 fix tracking now reads the owner's workplan file and found two
findings the register should have known about. T02 incident and external
report intake, the latter routed since the address is not ours to create.
T03 the production transition defined by what is held rather than what
was announced. T04 the README stops claiming a surface. T05 escalation
carries a delivery state and is raised once when unacknowledged. T06
checked_by and a heartbeat, so a 1q rung cannot silently mean nobody
looked. T07 coverage: 7 of 117 repos have ever appeared in a finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:34:30 +02:00

92 lines
3.4 KiB
Python

#!/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:]))