#!/usr/bin/env python3 """gen_status.py — generate STATUS.md deterministically from frontmatter. Writes STATUS.md (no timestamps — output depends only on repo content, so reruns are diff-clean). With --check, regenerates in memory and fails if the committed STATUS.md is stale; CI uses this mode. Usage: python scripts/gen_status.py [repo-root] # write STATUS.md python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale """ from __future__ import annotations import re import sys from pathlib import Path try: import yaml except ImportError: # pragma: no cover sys.exit("gen_status.py needs PyYAML: pip install pyyaml") H1_RE = re.compile(r"^#\s+(.*)$", re.M) def parse(path: Path): lines = path.read_text(encoding="utf-8").splitlines() if not lines or lines[0].strip() != "---": return None, "" for j in range(1, len(lines)): if lines[j].strip() == "---": meta = yaml.safe_load("\n".join(lines[1:j])) or {} body = "\n".join(lines[j + 1:]) return meta, body return None, "" def title(body: str, fallback: str) -> str: match = H1_RE.search(body) return match.group(1).strip() if match else fallback def collect(root: Path, subdir: str, wanted_type: str): items = [] base = root / subdir if not base.is_dir(): return items for path in sorted(base.rglob("*.md")): if path.name == "template.md": continue meta, body = parse(path) if not isinstance(meta, dict) or meta.get("type") != wanted_type: continue rel = path.relative_to(root).as_posix() items.append((rel, meta, title(body, path.stem))) return items def render(root: Path) -> str: issues = collect(root, "docs/issues", "issue") designs = collect(root, "docs/design", "design") adrs = collect(root, "docs/adr", "adr") aars = collect(root, "docs/aar", "aar") out: list[str] = [] out.append("# STATUS") out.append("") out.append("") out.append("") open_issues = [i for i in issues if i[1].get("status") in ("open", "in-progress")] closed = len(issues) - len(open_issues) out.append(f"## Issues ({len(open_issues)} open, {closed} closed)") out.append("") if open_issues: out.append("| Issue | Status | Title |") out.append("|---|---|---|") for rel, meta, name in open_issues: out.append(f"| [{meta.get('id', '?')}]({rel}) " f"| {meta.get('status')} | {name} |") else: out.append("_none open_") out.append("") active = [d for d in designs if d[1].get("status") != "done"] out.append(f"## Active design docs ({len(active)})") out.append("") if active: out.append("| Design | Gate | Title |") out.append("|---|---|---|") for rel, meta, name in active: out.append(f"| [{Path(rel).stem}]({rel}) " f"| {meta.get('status')} | {name} |") else: out.append("_none active_") out.append("") out.append(f"## ADRs ({len(adrs)})") out.append("") if adrs: out.append("| ADR | Status | Title |") out.append("|---|---|---|") for rel, meta, name in adrs: out.append(f"| [{meta.get('id', '?')}]({rel}) " f"| {meta.get('status')} | {name} |") else: out.append("_none_") out.append("") open_aars = [a for a in aars if a[1].get("status") == "open"] out.append(f"## Open AARs ({len(open_aars)})") out.append("") if open_aars: for rel, _meta, name in open_aars: out.append(f"- [{name}]({rel})") else: out.append("_none — nothing awaiting harvest_") out.append("") return "\n".join(out) def main() -> int: args = [a for a in sys.argv[1:] if a != "--check"] check = "--check" in sys.argv[1:] root = Path(args[0]) if args else Path.cwd() content = render(root) status = root / "STATUS.md" if check: current = status.read_text(encoding="utf-8") if status.is_file() else "" if current != content: print("gen_status --check: STATUS.md is stale — " "run scripts/gen_status.py and commit the result") return 1 print("gen_status --check: STATUS.md is current") return 0 status.write_text(content, encoding="utf-8", newline="\n") print(f"wrote {status}") return 0 if __name__ == "__main__": sys.exit(main())