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:
parent
c73848250c
commit
a1961a8f6f
3 changed files with 106 additions and 1 deletions
7
Makefile
7
Makefile
|
|
@ -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
90
tools/record_check.py
Normal 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:]))
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue