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.
This commit is contained in:
@@ -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
|
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.
|
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
|
## How it works
|
||||||
|
|
||||||
- Reviews are performed one module at a time against a fixed checklist.
|
- 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/
|
code-reviews/
|
||||||
├── README.md # this file — process overview + pending findings
|
├── README.md # this file — process overview + pending findings
|
||||||
├── REVIEW-PROCESS.md # how to perform a review and track 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
|
├── _template/findings.md # copy-this template for a module review
|
||||||
└── <Module>/findings.md # one folder per src/ project
|
└── <Module>/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 |
|
| 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 |
|
| [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 |
|
| [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 |
|
| [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 |
|
| [Communication](Communication/findings.md) | 2026-05-16 | `9c60592` | 0/2/5/3 | 10 | 11 |
|
||||||
|
|||||||
@@ -91,16 +91,20 @@ drop off the base README's pending list. `Open` and `In Progress` are **pending*
|
|||||||
|
|
||||||
## 5. Updating the base README
|
## 5. Updating the base README
|
||||||
|
|
||||||
`code-reviews/README.md` holds the single cross-module view. After any review or
|
`code-reviews/README.md` holds the single cross-module view (process overview, the
|
||||||
status change, update it:
|
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
|
After any review or status change, regenerate it:
|
||||||
`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).
|
|
||||||
|
|
||||||
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
|
## 6. Re-reviewing a module
|
||||||
|
|
||||||
|
|||||||
179
code-reviews/regen-readme.py
Executable file
179
code-reviews/regen-readme.py
Executable file
@@ -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("└── <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()
|
||||||
Reference in New Issue
Block a user