Files
goldeneye-pc-port/tools_pc/gen_findings_index.py
T
James Dansereau dbea5a30fe chore(agentic): speed-ups plan Steps 1, 4 (N2/R8/R9), 6
Static/tooling slice of docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md — no build or
runtime environment needed to verify any of it.

- Step 6 / N7: add a VERIFY: line to the investigation-brief template
  (docs/dev-process.md) and the CLAUDE.md dispatch-preflight checklist — the
  brief must now name the exact probe/verify invocation that proves the task
  done, or state that runtime verification is impossible here.
- Step 4 / N2: tools_pc/README.md — indexes the ~60 scripts with a
  living vs artifact marker so closed-finding artifacts can be archived.
- Step 4 / R8: tools_pc/gen_findings_index.py -> docs/dev/findings-index.csv
  (label, bucket, one_liner, status_raw) — grep-before-you-read aid for the
  ~200 KB finding log. Heuristic parser of the §F/§H tables.
- Step 4 / R9: tools_pc/gen_env_probes.py — re-greps live getenv("GE_*")
  sites and reports NEW/GONE drift vs docs/dev/GE-ENV-PROBES.md (keeps the
  curated prose, only checks the site map). Doc reconciled: 7 new probes
  added, GE_D54 kept as a tombstone.
- CI: validate job now runs both generators with --check.

Step 1 / N1 (HANDOFF preflight) applied in the local-only docs/HANDOFF.md.
Step 2 (verify.sh) and the HANDOFF/HANDOFF-ARCHIVE trim are left for a
session with a build environment.
2026-09-04 19:21:35 -04:00

158 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate docs/dev/findings-index.csv from the §F/§H tables in findings.md.
Agentic speed-ups plan Step 4 / R8: findings.md is ~200 KB. Agents should grep
this CSV (label, status, one-liner) to decide *which* Dxx entries to actually
read, instead of linear-reading the log.
Heuristic parser: every markdown table row whose first cell matches a Dxx
label (`D12`, `D88.4`, `D75 · D77`, `D24-D30`, ...). The topic cell is
truncated to a one-liner; the status cell is normalised to a short bucket
(RESOLVED / OPEN / FIXED / PARTIAL / PROPOSED / ?) with the raw text kept.
Usage:
python tools_pc/gen_findings_index.py # write the CSV
python tools_pc/gen_findings_index.py --check # exit 1 if stale
python tools_pc/gen_findings_index.py --stdout # print, don't write
"""
from __future__ import annotations
import csv
import io
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "docs" / "dev" / "findings.md"
OUT = ROOT / "docs" / "dev" / "findings-index.csv"
LABEL_RE = re.compile(r"^D\d+(?:\.\d+)?(?:[-–]D\d+)?(?:\s*[·,]\s*D\d+(?:\.\d+)?)*\+?$")
STATUS_BUCKETS = [
("FIXED", re.compile(r"\bfixed\b", re.I)),
("RESOLVED", re.compile(r"\bresolved\b", re.I)),
("PARTIAL", re.compile(r"\bpartial|clamp landed|landed\b|safety-net", re.I)),
("PROPOSED", re.compile(r"\bproposed|not applied|design\b", re.I)),
("OPEN", re.compile(r"\bopen\b|not started|awaiting|owed|pending", re.I)),
]
def one_liner(cell: str, limit: int = 160) -> str:
"""Strip markdown emphasis/code and collapse to a single short line."""
txt = cell.replace("**", "").replace("`", "")
txt = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", txt) # [t](u) -> t
txt = re.sub(r"\s+", " ", txt).strip()
# first sentence-ish
m = re.match(r"(.{20,}?[.—:])\s", txt)
if m and len(m.group(1)) <= limit:
txt = m.group(1)
if len(txt) > limit:
txt = txt[: limit - 1].rstrip() + "…"
return txt
LEAD_RE = re.compile(r"^\**\s*(OPEN|RESOLVED|FIXED|PROPOSED|PARTIAL|LANDED|CLAMP|DISPROVEN)", re.I)
def bucket(status_cell: str) -> str:
# An explicit leading verdict token wins over prose keywords deeper in the
# cell (e.g. "OPEN (parked). ... D168 fixed ..." is OPEN, not FIXED).
lead = LEAD_RE.match(status_cell.strip())
if lead:
tok = lead.group(1).upper()
return {"LANDED": "PARTIAL", "CLAMP": "PARTIAL", "DISPROVEN": "RESOLVED"}.get(tok, tok)
for name, rx in STATUS_BUCKETS:
if rx.search(status_cell):
return name
return "?"
def split_row(line: str) -> list[str] | None:
line = line.strip()
if not line.startswith("|"):
return None
# split on '|' that is not inside backticks
cells, buf, tick = [], [], False
for ch in line:
if ch == "`":
tick = not tick
buf.append(ch)
elif ch == "|" and not tick:
cells.append("".join(buf))
buf = []
else:
buf.append(ch)
cells.append("".join(buf))
cells = [c.strip() for c in cells]
# leading/trailing empty from outer pipes
if cells and cells[0] == "":
cells = cells[1:]
if cells and cells[-1] == "":
cells = cells[:-1]
return cells or None
def rows():
section = None
for raw in SRC.read_text(encoding="utf-8").splitlines():
h = re.match(r"^#+\s+([A-Z])\.\s", raw) or re.match(r"^#+\s.*§?([FH])\b", raw)
if raw.startswith("### ") and re.search(r"\b([FH])\.", raw):
section = re.search(r"\b([FH])\.", raw).group(1)
cells = split_row(raw)
if not cells or len(cells) < 2:
continue
label = cells[0].replace("–", "-")
if not LABEL_RE.match(cells[0]):
continue
topic = cells[1]
status = cells[2] if len(cells) > 2 else ""
yield {
"label": label,
"bucket": bucket(status),
"one_liner": one_liner(topic),
"status_raw": one_liner(status, 200) if status else "",
}
def build_csv() -> str:
buf = io.StringIO()
w = csv.DictWriter(
buf, fieldnames=["label", "bucket", "one_liner", "status_raw"], lineterminator="\n"
)
w.writeheader()
seen = set()
for r in rows():
key = r["label"]
if key in seen:
continue
seen.add(key)
w.writerow(r)
return buf.getvalue()
def main() -> int:
args = set(sys.argv[1:])
text = (
"# GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.\n"
"# Grep this to pick which findings.md entries to read; it is not authoritative.\n"
+ build_csv()
)
if "--stdout" in args:
sys.stdout.write(text)
return 0
want = text.encode("utf-8")
if "--check" in args:
cur = OUT.read_bytes() if OUT.exists() else b""
if cur != want:
sys.stderr.write("findings-index.csv is stale: re-run gen_findings_index.py\n")
return 1
return 0
OUT.write_bytes(want)
print(f"wrote {OUT.relative_to(ROOT)} ({text.count(chr(10))} lines)")
return 0
if __name__ == "__main__":
raise SystemExit(main())