Keep review obligations visible and reconcile owner evidence (RISK-WP-0006, RISK-WP-0007)
check_all runs every check stage even when one fails; malformed dates are reported rather than aborting; accepted findings and closure evidence are shown; defer requires a valid future date. Adds SCOPE.md, the scope assessment, the open-findings source review and a unittest suite. Stops tracking __pycache__. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 6903@bnt-lap001 Assistant-Session: 8319e8a8-ffa6-4eb3-b8bf-b29945628f89
This commit is contained in:
parent
29f50d5143
commit
bbbede5f47
25 changed files with 1007 additions and 43 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
25
tools/check_all.py
Normal file
25
tools/check_all.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run all read-only checks, retaining failures without hiding later reports."""
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
TOOLS = pathlib.Path(__file__).resolve().parent
|
||||
STAGES = (("register_index.py", "--check"), ("register_check.py",), ("inbox_check.py",))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failed = False
|
||||
for stage in STAGES:
|
||||
try:
|
||||
result = subprocess.run([sys.executable, str(TOOLS / stage[0]), *stage[1:]])
|
||||
failed = result.returncode != 0 or failed
|
||||
except OSError as exc:
|
||||
print(f"{stage[0]}: could not run: {exc}", flush=True)
|
||||
failed = True
|
||||
print(flush=True)
|
||||
return int(failed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -68,6 +68,7 @@ def resolve(fix_tracking: str) -> list[dict]:
|
|||
"id": full,
|
||||
"repo": fm.get("repo", path.parent.parent.name),
|
||||
"state": (state or "unreadable") + (" [archived]" if archived else ""),
|
||||
"completed": str(state).lower() in ("done", "finished", "cancel"),
|
||||
"workplan_status": fm.get("status"),
|
||||
"moved": last_commit(path),
|
||||
})
|
||||
|
|
@ -81,6 +82,22 @@ def report() -> list[str]:
|
|||
continue
|
||||
tracking = f.get("fix_tracking")
|
||||
if not tracking or str(tracking).startswith("unset"):
|
||||
if f.get("status") == "accepted":
|
||||
missing = [key for key in ("accepted_by", "accepted_until", "determination")
|
||||
if not f.get(key) or str(f[key]) == "unset"]
|
||||
if lib.moment(f.get("next_check")) is None:
|
||||
missing.append("next_check")
|
||||
determinations = {r.get("id") for r in lib.regulatory()}
|
||||
if f.get("determination") and f["determination"] not in determinations:
|
||||
missing.append("resolvable determination")
|
||||
if not missing:
|
||||
lines.append(
|
||||
f"{f['id']} — RECORDED ACCEPTANCE by {f['accepted_by']}; "
|
||||
f"ends when: {f['accepted_until']}; basis {f['determination']}; "
|
||||
f"review {f['next_check']} (not a fix or renewed acceptance)"
|
||||
)
|
||||
continue
|
||||
lines.append(f"{f['id']} — ACCEPTANCE INCOMPLETE: {', '.join(missing)}")
|
||||
lines.append(f"{f['id']} ({f.get('severity')}) — NO FIX TRACKING; owner {f.get('fix_owner')}")
|
||||
continue
|
||||
resolved = resolve(tracking)
|
||||
|
|
@ -93,7 +110,7 @@ def report() -> list[str]:
|
|||
if r["moved"]:
|
||||
age = (now - r["moved"]).days
|
||||
window = STALL_DAYS.get(f.get("severity"), 30)
|
||||
done = str(r["state"]).lower() in ("done", "finished", "cancel")
|
||||
done = r.get("completed", False)
|
||||
flag = "" if done or age <= window else f" ← UNCHANGED {age}d (window {window}d)"
|
||||
lines.append(f"{f['id']} — {r['id']}: {r['state']}, last changed {r['moved']:%Y-%m-%d}{flag}")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ def main(argv: list[str]) -> int:
|
|||
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)
|
||||
if nxt is None or nxt <= now:
|
||||
fail("`defer` needs a valid future date")
|
||||
line = f"- **{now:%Y-%m-%d}** — deferred to {until} by explicit operator decision: {why}"
|
||||
defer = until
|
||||
else:
|
||||
|
|
@ -71,6 +73,8 @@ def main(argv: list[str]) -> int:
|
|||
}
|
||||
if defer:
|
||||
subs["deferred_to"] = defer
|
||||
else:
|
||||
text = re.sub(r"(?m)^deferred_to:.*\n", "", text)
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ NOW = lib.now()
|
|||
|
||||
|
||||
def main() -> int:
|
||||
fs = [f for f in lib.findings() if lib.watched(f.get("status"))]
|
||||
all_findings = lib.findings()
|
||||
fs = [f for f in all_findings if lib.watched(f.get("status"))]
|
||||
lines: list[str] = []
|
||||
|
||||
def section(title: str, rows: list[str], quiet: str) -> None:
|
||||
|
|
@ -50,9 +51,9 @@ def main() -> int:
|
|||
# reported by name, and never silently ignored.
|
||||
ids = {f["id"] for f in lib.findings()}
|
||||
malformed: list[str] = []
|
||||
for f in fs:
|
||||
for f in all_findings:
|
||||
fid = f["id"]
|
||||
for field in ("last_checked", "next_check", "embargo_since", "deferred_to"):
|
||||
for field in ("last_checked", "next_check", "embargo_since", "embargo_review", "deferred_to", "escalation_sent"):
|
||||
if f.get(field) and lib.moment(f[field]) is None:
|
||||
malformed.append(f"{fid} — {field} is not a date: {f[field]!r}")
|
||||
if (c := f.get("cadence")) and c not in lib.CADENCE_NAMES:
|
||||
|
|
@ -147,6 +148,10 @@ def main() -> int:
|
|||
# RISK-WP-0005-T01: the fix's own state, read from the owner's workplan
|
||||
# file rather than from our memory of what they told us.
|
||||
section("Fix state", fix_tracker.report(), "no finding claims a tracked fix")
|
||||
section("Closure evidence pending", [
|
||||
f"{f['id']} — {f['closure_condition']}"
|
||||
for f in fs if f.get("closure_condition")
|
||||
], "none recorded")
|
||||
if due_defaults:
|
||||
section("Defaults now due — apply them", due_defaults, "none")
|
||||
if deep:
|
||||
|
|
@ -154,11 +159,26 @@ def main() -> int:
|
|||
|
||||
embargo = [
|
||||
f"{f['id']} — lifts when: {f.get('embargo_condition')}"
|
||||
for f in fs
|
||||
for f in all_findings
|
||||
if f.get("disclosure") == "embargoed"
|
||||
]
|
||||
section("Embargoed", embargo, "none")
|
||||
|
||||
embargo_due = []
|
||||
for f in all_findings:
|
||||
if f.get("disclosure") != "embargoed":
|
||||
continue
|
||||
when = lib.moment(f.get("embargo_review"))
|
||||
if when is None:
|
||||
embargo_due.append(f"{f['id']} — missing or invalid embargo_review; re-decide the hold")
|
||||
elif NOW >= when:
|
||||
embargo_due.append(f"{f['id']} — embargo review due {f['embargo_review']}; re-decide the hold")
|
||||
section("Embargo reviews due", embargo_due, "none")
|
||||
section("Publication handovers pending", [
|
||||
f"{f['id']} ({f.get('status')}) — pending policy-nexus handover: {f.get('publication_id', 'no publication id')}"
|
||||
for f in all_findings if f.get("publication") == "pending-handover"
|
||||
], "none")
|
||||
|
||||
# T05 — an escalation nobody acknowledged is indistinguishable from one
|
||||
# never sent, which is the failure this register fixed for its own inbox
|
||||
# and not, until now, for the path that matters more.
|
||||
|
|
@ -184,6 +204,14 @@ def main() -> int:
|
|||
|
||||
reg = []
|
||||
for r in lib.regulatory():
|
||||
invalid = [field for field in ("last_checked", "next_check", "review_by", "deferred_to")
|
||||
if r.get(field) and lib.moment(r[field]) is None]
|
||||
if invalid:
|
||||
reg.append(f"{r.get('id')} — invalid date: {', '.join(invalid)}; review required")
|
||||
continue
|
||||
until = lib.moment(r.get("deferred_to"))
|
||||
if until and NOW < until:
|
||||
continue
|
||||
when = lib.moment(r.get("next_check")) or lib.moment(r.get("review_by"))
|
||||
if when is None:
|
||||
reg.append(f"{r.get('id')} — no next_check set")
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def regulatory() -> list[dict]:
|
|||
d = REPO / "docs" / "regulatory"
|
||||
if not d.exists():
|
||||
return []
|
||||
return sorted((load(p) for p in d.glob("*.md") if p.name != "README.md"),
|
||||
return sorted((load(p) for p in d.rglob("*.md") if p.name != "README.md"),
|
||||
key=lambda r: r.get("id", ""))
|
||||
|
||||
|
||||
|
|
@ -112,7 +112,12 @@ def moment(value) -> dt.datetime | None:
|
|||
if isinstance(value, dt.date):
|
||||
return dt.datetime.combine(value, dt.time(0, 0), tzinfo=dt.timezone.utc)
|
||||
if isinstance(value, str) and value not in ("", "unset"):
|
||||
return dt.datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(dt.timezone.utc)
|
||||
try:
|
||||
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
return (parsed.replace(tzinfo=dt.timezone.utc) if parsed.tzinfo is None
|
||||
else parsed.astimezone(dt.timezone.utc))
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue