record_check.py moves a finding along the cadence ladder and writes the dated line into the finding at the same time — a check that is not written down did not happen, which is the rule the register applies to everyone else. make checked ARGS="RISK-F-0002 clean". make check now also reports duplicate finding ids. The RISK-F-0004 collision was resolved by hand yesterday; the next one gets caught by the tooling instead of by someone noticing a file listed twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
3.3 KiB
Python
90 lines
3.3 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 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() if f["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 = {
|
|
"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:]))
|