#!/usr/bin/env python3 """Regenerate code-reviews/README.md from the per-module findings.md files. The per-module findings.md files are the source of truth. This script aggregates them into the single cross-module README.md (module status + pending/closed finding tables). Usage: python code-reviews/regen-readme.py # rewrite README.md python code-reviews/regen-readme.py --check # exit 1 if stale or inconsistent `--check` fails when README.md is out of date OR when a module's header `Open findings` count disagrees with its finding statuses, or a finding carries an unrecognised Status value. """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent README = ROOT / "README.md" PENDING_STATUSES = {"Open", "In Progress"} KNOWN_STATUSES = {"Open", "In Progress", "Resolved", "Won't Fix", "Deferred"} SEVERITY_ORDER = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3} GENERATED_NOTE = ( "" ) def cell(value: str) -> str: """Escape a value for safe inclusion in a markdown table cell.""" return value.replace("|", "\\|").strip() def summarize(value: str, limit: int = 240) -> str: """Trim a long description to a single-cell-friendly summary.""" value = value.strip() if len(value) <= limit: return value return value[: limit - 1].rstrip() + "…" def first_table(text: str) -> dict[str, str]: """Parse the first contiguous block of '| key | value |' rows into a dict.""" rows: dict[str, str] = {} started = False for line in text.splitlines(): stripped = line.strip() if stripped.startswith("|"): started = True cells = [c.strip() for c in stripped.strip("|").split("|")] if len(cells) >= 2: key, value = cells[0], cells[1] if key and not set(key) <= {"-", ":"} and key != "Field": rows[key] = value elif started: break return rows def parse_module(findings_path: Path) -> dict: """Parse one module's findings.md into its header and finding list.""" text = findings_path.read_text(encoding="utf-8") module = findings_path.parent.name parts = re.split(r"^##\s+Findings\s*$", text, maxsplit=1, flags=re.M) header = first_table(parts[0]) findings: list[dict] = [] if len(parts) > 1: for chunk in re.split(r"^###\s+", parts[1], flags=re.M)[1:]: fid = chunk.splitlines()[0].strip() tbl = first_table(chunk) desc_m = re.search( r"\*\*Description:\*\*\s*(.*?)(?=\n\*\*|\Z)", chunk, re.S ) desc = re.sub(r"\s+", " ", desc_m.group(1)).strip() if desc_m else "" findings.append( { "id": fid, "severity": tbl.get("Severity", ""), "category": tbl.get("Category", ""), "location": tbl.get("Location", ""), "status": tbl.get("Status", ""), "description": desc, } ) return {"module": module, "header": header, "findings": findings} def build_readme(modules: list[dict]) -> str: modules = sorted(modules, key=lambda m: m["module"]) all_findings = [ dict(f, module=m["module"]) for m in modules for f in m["findings"] ] pending = [f for f in all_findings if f["status"] in PENDING_STATUSES] closed = [ f for f in all_findings if f["status"] and f["status"] not in PENDING_STATUSES ] def sev_key(f: dict) -> tuple: return (SEVERITY_ORDER.get(f["severity"], 9), f["id"]) pending.sort(key=sev_key) closed.sort(key=sev_key) out: list[str] = [ "# Code Reviews", "", GENERATED_NOTE, "", "Cross-module code review index for the OtOpcUa server codebase " "(`lmxopcua`). The review process is defined in " "[../REVIEW-PROCESS.md](../REVIEW-PROCESS.md).", "", "Each module's `findings.md` is the source of truth; this file is generated " "from them by `regen-readme.py` and must not be edited by hand.", "", "## Module status", "", "| Module | Reviewer | Date | Commit | Status | Open | Total |", "|---|---|---|---|---|---|---|", ] if not modules: out.append( "| _no modules reviewed yet_ | | | | | | |" ) for m in modules: h = m["header"] open_n = sum( 1 for f in m["findings"] if f["status"] in PENDING_STATUSES ) out.append( f"| [{m['module']}]({m['module']}/findings.md) " f"| {cell(h.get('Reviewer', ''))} " f"| {cell(h.get('Review date', ''))} " f"| {cell(h.get('Commit reviewed', ''))} " f"| {cell(h.get('Status', ''))} " f"| {open_n} | {len(m['findings'])} |" ) out += ["", "## Pending findings", ""] out.append( "Findings with status `Open` or `In Progress`, ordered by severity." ) out.append("") if pending: out.append("| ID | Severity | Category | Location | Description |") out.append("|---|---|---|---|---|") for f in pending: out.append( f"| {cell(f['id'])} | {cell(f['severity'])} " f"| {cell(f['category'])} | {cell(f['location'])} " f"| {cell(summarize(f['description']))} |" ) else: out.append("_No pending findings._") out += ["", "## Closed findings", ""] out.append("Findings with status `Resolved`, `Won't Fix`, or `Deferred`.") out.append("") if closed: out.append("| ID | Severity | Status | Category | Location |") out.append("|---|---|---|---|---|") for f in closed: out.append( f"| {cell(f['id'])} | {cell(f['severity'])} " f"| {cell(f['status'])} | {cell(f['category'])} " f"| {cell(f['location'])} |" ) else: out.append("_No closed findings._") return "\n".join(out) + "\n" def find_inconsistencies(modules: list[dict]) -> list[str]: """Return human-readable problems in the per-module findings.md files. Checks that each module header's `Open findings` count agrees with its finding statuses, and that every finding carries a known Status value. """ issues: list[str] = [] for m in modules: open_n = sum( 1 for f in m["findings"] if f["status"] in PENDING_STATUSES ) declared = m["header"].get("Open findings", "").strip() if declared != str(open_n): issues.append( f"{m['module']}: header 'Open findings' = '{declared}' but " f"{open_n} finding(s) are Open/In Progress" ) for f in m["findings"]: if f["status"] not in KNOWN_STATUSES: issues.append( f"{m['module']}: finding {f['id']} has unrecognised " f"Status '{f['status']}'" ) return issues def main(argv: list[str]) -> int: check = "--check" in argv[1:] module_dirs = sorted( d for d in ROOT.iterdir() if d.is_dir() and d.name != "_template" and (d / "findings.md").is_file() ) modules = [parse_module(d / "findings.md") for d in module_dirs] content = build_readme(modules) issues = find_inconsistencies(modules) if check: stale = ( README.read_text(encoding="utf-8") if README.exists() else "" ) != content for issue in issues: print(f"inconsistent: {issue}", file=sys.stderr) if stale: print( "code-reviews/README.md is stale - run regen-readme.py", file=sys.stderr, ) if stale or issues: return 1 print("code-reviews/README.md is up to date and consistent.") return 0 for issue in issues: print(f"warning: {issue}", file=sys.stderr) README.write_text(content, encoding="utf-8", newline="\n") print(f"Wrote {README} ({len(modules)} modules).") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))