Files
James Dansereau 811e9a62cf Bug-fix pass for reported issues #114-#119 (D424-D434)
Recoil byte order, crouch-tank interactions, auto-fire cadence in ticks,
hipfire turret tracking, light-fixture Gfx index parsing, hit-type byte
reads. Rule-2 sign-offs recorded for D424 and D427. Documentation re-triaged
onto a single tracker (docs/ROADMAP.md).

Refs: D424 D425 D426 D427 D428 D430 D431 D432 D434 #114 #115 #116 #117 #118 #119
2026-10-06 18:35:38 -04:00

173 lines
5.5 KiB
Python
Raw Permalink 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"
r"|ROOT-CAUSED|GUARDED|ANALYZED|DIAGNOSTIC SHIPPED|NOT A BUG|CLOSED"
r"|DEFERRED|SUPERSEDED)",
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",
"ROOT-CAUSED": "RESOLVED",
"GUARDED": "PARTIAL",
"ANALYZED": "PARTIAL",
"DIAGNOSTIC SHIPPED": "PARTIAL",
"NOT A BUG": "RESOLVED",
"SUPERSEDED": "CLOSED",
}.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())