From d8f99ba781edab09e5c7870f7439e9d1fbf01b1c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 16 May 2026 19:18:18 -0400 Subject: [PATCH] docs(code-reviews): add regen-readme.py to generate the review index README.md is now generated from the per-module findings.md files by code-reviews/regen-readme.py (discovers modules, parses each finding's severity/status, rebuilds the Pending Findings and Module Status tables). Run with --check to fail when README.md is stale (CI-friendly). REVIEW-PROCESS.md section 5 now points to the script instead of describing a manual edit, and README.md carries a generated-file banner. --- code-reviews/README.md | 6 +- code-reviews/REVIEW-PROCESS.md | 20 ++-- code-reviews/regen-readme.py | 179 +++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 9 deletions(-) create mode 100755 code-reviews/regen-readme.py diff --git a/code-reviews/README.md b/code-reviews/README.md index ee2fb99..c0757ca 100644 --- a/code-reviews/README.md +++ b/code-reviews/README.md @@ -4,6 +4,9 @@ Comprehensive, per-module code reviews of the ScadaLink codebase. Each module (o buildable project under `src/`) has its own folder containing a `findings.md`. This README is the aggregated index — the single place to see all outstanding work. +> Generated by `regen-readme.py` from the per-module `findings.md` files. Do not +> edit by hand — edit the findings files and re-run the script. + ## How it works - Reviews are performed one module at a time against a fixed checklist. @@ -22,6 +25,7 @@ checklist, severity definitions, finding format, and how to mark items resolved. code-reviews/ ├── README.md # this file — process overview + pending findings ├── REVIEW-PROCESS.md # how to perform a review and track findings +├── regen-readme.py # regenerates this README from the findings files ├── _template/findings.md # copy-this template for a module review └── /findings.md # one folder per src/ project ``` @@ -45,8 +49,8 @@ module file and counted in **Total**. | Module | Last reviewed | Commit | Open (C/H/M/L) | Open | Total | |--------|---------------|--------|----------------|------|-------| -| [CentralUI](CentralUI/findings.md) | 2026-05-16 | `9c60592` | 0/3/10/5 | 18 | 19 | | [CLI](CLI/findings.md) | 2026-05-16 | `9c60592` | 0/1/6/6 | 13 | 13 | +| [CentralUI](CentralUI/findings.md) | 2026-05-16 | `9c60592` | 0/3/10/5 | 18 | 19 | | [ClusterInfrastructure](ClusterInfrastructure/findings.md) | 2026-05-16 | `9c60592` | 0/1/4/3 | 8 | 8 | | [Commons](Commons/findings.md) | 2026-05-16 | `9c60592` | 0/0/4/8 | 12 | 12 | | [Communication](Communication/findings.md) | 2026-05-16 | `9c60592` | 0/2/5/3 | 10 | 11 | diff --git a/code-reviews/REVIEW-PROCESS.md b/code-reviews/REVIEW-PROCESS.md index e2ee1a2..1578f87 100644 --- a/code-reviews/REVIEW-PROCESS.md +++ b/code-reviews/REVIEW-PROCESS.md @@ -91,16 +91,20 @@ drop off the base README's pending list. `Open` and `In Progress` are **pending* ## 5. Updating the base README -`code-reviews/README.md` holds the single cross-module view. After any review or -status change, update it: +`code-reviews/README.md` holds the single cross-module view (process overview, the +Pending Findings tables, and the Module Status table). It is **generated** from the +per-module `findings.md` files — do not edit it by hand. -1. **Pending Findings table** — add/remove rows so it lists exactly the `Open` and - `In Progress` findings across all modules, sorted by severity. -2. **Module Status table** — update the row for the reviewed module (last-reviewed - date, commit, open-finding count, review status). +After any review or status change, regenerate it: -The base README must always agree with the per-module `findings.md` files — they are -the source of truth; the README is the aggregated index. +``` +python3 code-reviews/regen-readme.py +``` + +`regen-readme.py --check` exits non-zero if `README.md` is stale, for use in CI. + +The per-module `findings.md` files are the source of truth; `README.md` is the +aggregated index and must always agree with them — which the script guarantees. ## 6. Re-reviewing a module diff --git a/code-reviews/regen-readme.py b/code-reviews/regen-readme.py new file mode 100755 index 0000000..87bd00c --- /dev/null +++ b/code-reviews/regen-readme.py @@ -0,0 +1,179 @@ +#!/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("└── /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()