Ad hoc: make the ladder executable, and make the collision class loud

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>
This commit is contained in:
tegwick 2026-08-20 08:05:42 +02:00
parent c73848250c
commit a1961a8f6f
3 changed files with 106 additions and 1 deletions

View file

@ -1,11 +1,12 @@
PY := python3
TOOLS := tools
.PHONY: help register check
.PHONY: help register check checked
help:
@echo "make register - rebuild REGISTER.md from findings/"
@echo "make check - verify the index is current, then report what is going quiet"
@echo "make checked - record a check outcome: make checked ARGS=\"RISK-F-0002 clean\""
register:
@$(PY) $(TOOLS)/register_index.py
@ -14,3 +15,7 @@ check:
@$(PY) $(TOOLS)/register_index.py --check
@echo
@$(PY) $(TOOLS)/register_check.py
checked:
@$(PY) $(TOOLS)/record_check.py $(ARGS)
@$(PY) $(TOOLS)/register_index.py

90
tools/record_check.py Normal file
View file

@ -0,0 +1,90 @@
#!/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:]))

View file

@ -26,6 +26,16 @@ def main() -> int:
lines.append(f" {quiet}")
lines.append("")
seen: dict[str, str] = {}
dupes = []
for f in lib.findings():
prior = seen.get(f["id"])
if prior:
dupes.append(f"{f['id']} — filed twice: {prior} and {f['_path'].name}; renumber the later commit")
seen[f["id"]] = f["_path"].name
if dupes:
section("Duplicate ids", dupes, "none")
unknown = [
f"{f['id']} — status '{f.get('status')}' is not one of {', '.join(lib.KNOWN_STATUSES)}; watched anyway"
for f in fs