review-repo: add stale-deferred check for ADR Deferred entries
repo-scan.py now enumerates open ADR "Deferred/Open" items and flags any that another file describes as resolved but which isn't marked resolved in place (the recurring miss in docs/FRICTION.md). review-repo.md's Phase 2 reviewer confirms each open item against later ADRs/STATUS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
66d11cc352
commit
f566fd17eb
2 changed files with 83 additions and 0 deletions
|
|
@ -27,6 +27,11 @@ Run `python3 scripts/repo-scan.py > /tmp/repo-scan.json`. It returns the **inven
|
|||
(roles, ADRs, runbooks, playbooks, scripts — your shard list) and **exact findings**
|
||||
(markers, broken refs, unencrypted vaults). Fold these into the report verbatim.
|
||||
|
||||
It also emits two deferral checks (see Phase 2): `open-deferred-item` (every still-open
|
||||
ADR "Deferred/Open" entry — a checklist to confirm) and `stale-deferred` (an entry
|
||||
another file describes as resolved but which isn't marked resolved in place —
|
||||
high-confidence, usually auto-fixable by marking the source ADR's entry RESOLVED).
|
||||
|
||||
### Phase 1 — fan-out judgement review
|
||||
Scale to repo size:
|
||||
- **Small** (≤ ~10 roles, like boma today): a few sub-agents, or one pass per area.
|
||||
|
|
@ -42,6 +47,13 @@ location (file:line), description, suggested_fix, auto_fixable (bool)}`.
|
|||
- Merge and dedupe all findings (deterministic + reviewer).
|
||||
- Run **one cross-cutting reviewer** over the full ADR set + `STATUS.md` + `CLAUDE.md`
|
||||
to catch contradictions that span files (per-shard agents can't see these).
|
||||
- **Resolve the deferral checklist.** For every `open-deferred-item` from Phase 0,
|
||||
judge whether it is *genuinely* still open: search later ADRs / `STATUS.md` for a
|
||||
decision on that subject (a deferred item often resolves silently when a later ADR
|
||||
lands). If it has been decided, it is a stale-deferred finding — the fix is to mark
|
||||
that entry RESOLVED in its **source ADR's** Deferred list (the spot the resolving
|
||||
ADR's own change won't have touched). Treat every `stale-deferred` finding as
|
||||
high-confidence. This is the recurring miss logged in `docs/FRICTION.md`.
|
||||
- Diff against the previous run's `docs/reviews/<prev>-findings.json` and tag each
|
||||
finding **new / recurring / resolved**.
|
||||
- Prioritise by severity; split into auto-fixable vs report-only.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,67 @@ ADR_REF_RE = re.compile(r"\bADR-(\d{3})\b")
|
|||
PATH_REF_RE = re.compile(r"(?:docs|scripts|roles|inventories|terraform|playbooks)/[\w./-]+")
|
||||
PLACEHOLDER = set("<>*${}")
|
||||
|
||||
# Stale-deferred detection: ADR "Deferred/Open" entries that another file describes
|
||||
# as resolved, but which aren't marked resolved in place. (See docs/FRICTION.md.)
|
||||
RESOLVE_MARK_RE = re.compile(r"\b(?:RESOLVED|DECIDED)\b", re.I)
|
||||
LIST_ITEM_RE = re.compile(r"^\s*(\d+\.|[-*+])\s+(.*)")
|
||||
# An external "this resolves ADR-NNN deferred #K" style reference.
|
||||
DEFER_REF_RE = re.compile(r"ADR-(\d{3})\D{0,40}?deferred\D{0,12}?(\d+)", re.I)
|
||||
RESOLVE_WORD_RE = re.compile(r"\b(?:resolv\w*|decid\w*|address\w*|complet\w*|done)\b", re.I)
|
||||
|
||||
|
||||
def _is_defer_heading(text):
|
||||
t = text.strip().lower()
|
||||
return (t.startswith("deferred") or t.startswith("unresolved")
|
||||
or "open question" in t or "open issue" in t)
|
||||
|
||||
|
||||
def _defer_subject(item_text):
|
||||
m = re.search(r"\*\*(.+?)\*\*", item_text)
|
||||
s = m.group(1) if m else re.split(r"\s+[—–-]\s+|:", item_text, maxsplit=1)[0]
|
||||
return re.sub(r"\s+", " ", s).strip(" *_`~—–-:.")
|
||||
|
||||
|
||||
def deferred_findings(adr_files, defer_refs):
|
||||
"""adr_files: {rel_path: [lines]} for docs/decisions/*.md.
|
||||
defer_refs: [(adr, ordinal, path, line, has_resolve_word)] gathered repo-wide.
|
||||
Emits one informational `open-deferred-item` per open entry, and a `stale-deferred`
|
||||
contradiction when another file describes that entry as resolved."""
|
||||
out = []
|
||||
for rpath, lines in sorted(adr_files.items()):
|
||||
madr = re.match(r"(\d{3})-", os.path.basename(rpath))
|
||||
adr_num = madr.group(1) if madr else None
|
||||
in_defer = False
|
||||
for i, raw in enumerate(lines, 1):
|
||||
hm = re.match(r"#{1,6}\s+(.*)", raw)
|
||||
if hm:
|
||||
in_defer = _is_defer_heading(hm.group(1))
|
||||
continue
|
||||
if not in_defer:
|
||||
continue
|
||||
im = LIST_ITEM_RE.match(raw)
|
||||
if not im:
|
||||
continue
|
||||
marker, item_text = im.group(1), im.group(2)
|
||||
# self-marked resolved (inline RESOLVED/DECIDED or ~~strikethrough~~) → fine
|
||||
if RESOLVE_MARK_RE.search(raw) or item_text.lstrip().startswith("~~"):
|
||||
continue
|
||||
ordinal = int(marker[:-1]) if marker[:-1].isdigit() else None
|
||||
subject = _defer_subject(item_text)
|
||||
tag = f" #{ordinal}" if ordinal else ""
|
||||
out.append({"check": "open-deferred-item", "severity": "low", "path": rpath,
|
||||
"line": i, "detail": f"open deferred item{tag} in ADR-{adr_num}: "
|
||||
f"'{subject[:80]}' — confirm not resolved by a later ADR/STATUS"})
|
||||
if adr_num and ordinal:
|
||||
for ra, rk, rp, rl, has_res in defer_refs:
|
||||
if ra == adr_num and rk == ordinal and rp != rpath and has_res:
|
||||
out.append({"check": "stale-deferred", "severity": "medium",
|
||||
"path": rpath, "line": i,
|
||||
"detail": f"ADR-{adr_num} deferred #{ordinal} "
|
||||
f"('{subject[:60]}') is described as resolved at "
|
||||
f"{rp}:{rl}, but is not marked RESOLVED in place"})
|
||||
return out
|
||||
|
||||
|
||||
def walk_files():
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
|
|
@ -81,6 +142,9 @@ def adr_numbers():
|
|||
def scan():
|
||||
findings = []
|
||||
adrs = adr_numbers()
|
||||
adr_files = {} # docs/decisions/*.md → lines, for deferred-section parsing
|
||||
defer_refs = [] # repo-wide "resolves ADR-NNN deferred #K" references
|
||||
decisions_dir = os.path.join("docs", "decisions")
|
||||
for path in walk_files():
|
||||
rpath = rel(path)
|
||||
if rpath.startswith(SKIP_PREFIX):
|
||||
|
|
@ -108,7 +172,13 @@ def scan():
|
|||
except OSError:
|
||||
continue
|
||||
|
||||
if rpath.startswith(decisions_dir) and rpath.endswith(".md"):
|
||||
adr_files[rpath] = lines
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
for m in DEFER_REF_RE.finditer(line):
|
||||
defer_refs.append((m.group(1), int(m.group(2)), rpath, i,
|
||||
bool(RESOLVE_WORD_RE.search(line))))
|
||||
markers = sorted(set(m.group(1) for m in MARKER_RE.finditer(line)))
|
||||
if markers:
|
||||
findings.append({"check": "marker", "severity": "low", "path": rpath,
|
||||
|
|
@ -131,6 +201,7 @@ def scan():
|
|||
if not os.path.exists(os.path.join(ROOT, ref)):
|
||||
findings.append({"check": "broken-path-ref", "severity": "medium", "path": rpath,
|
||||
"line": i, "detail": f"references '{ref}' which does not exist"})
|
||||
findings.extend(deferred_findings(adr_files, defer_refs))
|
||||
return findings
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue