Files
scadalink-design/code-reviews/regen-readme.py

180 lines
7.3 KiB
Python
Executable File
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
"""Regenerate code-reviews/README.md from the per-module findings.md files.
The findings files are the source of truth; README.md is a generated index.
Run this after resolving or re-triaging a finding so the aggregated tables stay
in sync (see REVIEW-PROCESS.md section 5).
Usage:
python3 regen-readme.py # rewrite README.md
python3 regen-readme.py --check # exit 1 if README.md is stale (for CI)
Works from any directory — paths are resolved relative to this script.
"""
import os
import re
import sys
BASE = os.path.dirname(os.path.abspath(__file__))
SEVERITY_ORDER = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
PENDING_STATUSES = {"Open", "In Progress"}
def discover_modules():
"""Module folders are every subdirectory of code-reviews/ holding a findings.md,
excluding the _template folder. Returned sorted for a stable README order."""
modules = []
for name in sorted(os.listdir(BASE)):
if name.startswith(("_", ".")):
continue
if os.path.isfile(os.path.join(BASE, name, "findings.md")):
modules.append(name)
return modules
def parse_findings(module):
"""Parse one module's findings.md into (module, id, severity, title, status) tuples."""
text = open(os.path.join(BASE, module, "findings.md")).read()
findings = []
for block in re.split(r"^### ", text, flags=re.M)[1:]:
head = block.splitlines()[0].strip()
m = re.match(r"([A-Za-z][A-Za-z0-9]*-\d+)\b(.*)", head)
if not m:
raise SystemExit(f"{module}/findings.md: unparseable finding heading: {head!r}")
fid = m.group(1).strip()
title = m.group(2).strip().lstrip("—–-").strip().replace("|", "\\|")
sev = re.search(r"\|\s*Severity\s*\|\s*([A-Za-z]+)", block)
status = re.search(r"\|\s*Status\s*\|\s*([A-Za-z' ]+?)\s*\|", block)
if not sev or not status:
raise SystemExit(f"{module}/findings.md: {fid} is missing a Severity or Status field")
findings.append((module, fid, sev.group(1), title, status.group(1).strip()))
return findings
def finding_number(finding):
return int(re.search(r"-(\d+)$", finding[1]).group(1))
def build_readme(modules, per_module):
pending = sorted(
(f for fs in per_module.values() for f in fs if f[4] in PENDING_STATUSES),
key=lambda f: (SEVERITY_ORDER.get(f[2], 9), f[0], finding_number(f)),
)
def severity_total(sev):
return sum(1 for f in pending if f[2] == sev)
def open_count(module, sev):
return sum(1 for f in per_module[module]
if f[2] == sev and f[4] in PENDING_STATUSES)
lines = []
add = lines.append
add("# Code Reviews")
add("")
add("Comprehensive, per-module code reviews of the ScadaLink codebase. Each module (one")
add("buildable project under `src/`) has its own folder containing a `findings.md`. This")
add("README is the aggregated index — the single place to see all outstanding work.")
add("")
add("> Generated by `regen-readme.py` from the per-module `findings.md` files. Do not")
add("> edit by hand — edit the findings files and re-run the script.")
add("")
add("## How it works")
add("")
add("- Reviews are performed one module at a time against a fixed checklist.")
add("- Every finding is recorded in the module's `findings.md` with a severity and status.")
add("- Findings are **never deleted** — they are closed by changing their status, keeping")
add(" a full audit trail.")
add("- This README aggregates every **pending** finding (`Open` / `In Progress`) across all")
add(" modules.")
add("")
add("See **[REVIEW-PROCESS.md](REVIEW-PROCESS.md)** for the full procedure: the review")
add("checklist, severity definitions, finding format, and how to mark items resolved.")
add("")
add("## Layout")
add("")
add("```")
add("code-reviews/")
add("├── README.md # this file — process overview + pending findings")
add("├── REVIEW-PROCESS.md # how to perform a review and track findings")
add("├── regen-readme.py # regenerates this README from the findings files")
add("├── _template/findings.md # copy-this template for a module review")
add("└── <Module>/findings.md # one folder per src/ project")
add("```")
add("")
add("## Baseline review — 2026-05-16")
add("")
add("All 19 modules were reviewed at commit `9c60592` (241 findings: 6 Critical, 46 High,")
add("100 Medium, 89 Low). The tables below track what remains **open** as findings are")
add("resolved and re-triaged; findings discovered after the baseline are appended to their")
add("module file and counted in **Total**.")
add("")
add("| Severity | Open findings |")
add("|----------|---------------|")
for sev in ("Critical", "High", "Medium", "Low"):
add(f"| {sev} | {severity_total(sev)} |")
add(f"| **Total** | **{len(pending)}** |")
add("")
add("## Module Status")
add("")
add("| Module | Last reviewed | Commit | Open (C/H/M/L) | Open | Total |")
add("|--------|---------------|--------|----------------|------|-------|")
for module in modules:
counts = [open_count(module, s) for s in ("Critical", "High", "Medium", "Low")]
add(f"| [{module}]({module}/findings.md) | 2026-05-16 | `9c60592` "
f"| {counts[0]}/{counts[1]}/{counts[2]}/{counts[3]} "
f"| {sum(counts)} | {len(per_module[module])} |")
add("")
add("## Pending Findings")
add("")
add("Every `Open` / `In Progress` finding across all modules, highest severity first.")
add("Resolved findings drop off this list but remain recorded in their module's")
add("`findings.md` (see [REVIEW-PROCESS.md](REVIEW-PROCESS.md) §4§5). Full detail —")
add("description, location, recommendation — lives in the module's `findings.md`.")
add("")
for sev in ("Critical", "High", "Medium", "Low"):
rows = [f for f in pending if f[2] == sev]
add(f"### {sev} ({len(rows)})")
add("")
if not rows:
add("_None open._")
add("")
continue
add("| ID | Module | Title |")
add("|----|--------|-------|")
for module, fid, _, title, _ in rows:
add(f"| {fid} | [{module}]({module}/findings.md) | {title} |")
add("")
return "\n".join(lines)
def main():
check = "--check" in sys.argv[1:]
modules = discover_modules()
per_module = {m: parse_findings(m) for m in modules}
content = build_readme(modules, per_module)
readme_path = os.path.join(BASE, "README.md")
pending = sum(1 for fs in per_module.values()
for f in fs if f[4] in PENDING_STATUSES)
total = sum(len(fs) for fs in per_module.values())
if check:
current = open(readme_path).read() if os.path.exists(readme_path) else ""
if current != content:
print("README.md is stale — run: python3 code-reviews/regen-readme.py")
sys.exit(1)
print(f"README.md is up to date ({pending} pending / {total} total).")
return
open(readme_path, "w").write(content)
print(f"README.md regenerated — {pending} pending, {total} total findings "
f"across {len(modules)} modules.")
if __name__ == "__main__":
main()