#!/usr/bin/env python3 """render.py — regenerate architecture-map.html + architecture-audit.md from modules.json. modules.json is the single source of truth. The HTML and MD are pure projections of it and must never be hand-edited. Run scan.py --write before rendering so LoC / content hashes are current. Usage: python render.py --state modules.json \ --template assets/template.html \ --out-html architecture-map.html --out-md architecture-audit.md Stdlib only. """ import argparse, json, os def health_color(s): if s is None: return "#6b7280" if s < 50: return "#e0524b" if s < 65: return "#e0804a" if s < 75: return "#d9a441" if s < 85: return "#8f969d" return "#5d6b63" def band_order(state): return [b["id"] for b in state.get("bands", []) if not b.get("wire")] def render_html(state, template): data = { "meta": state.get("meta", {}), "bands": state.get("bands", []), "spine": state.get("spine", []), "reportThemes": state.get("reportThemes", []), "modules": [ {k: m.get(k) for k in ( "id", "label", "band", "path", "desc", "coupling", "deps", "loc", "score", "grade", "tags", "findings")} for m in state.get("modules", []) ], } blob = json.dumps(data, ensure_ascii=False).replace("\n") out.append(f"# {proj} — Functional Module Quality Audit\n") out.append(f"> **Interactive view:** [`{meta.get('htmlPath','architecture-map.html')}`]" f"({os.path.basename(meta.get('htmlPath','architecture-map.html'))}) — " "per-module scores, findings, LoC, and the dependency graph. This file is the written report.\n") gen = meta.get("generatedAt", "") loc_line = meta.get("locLine") or ( f"{meta.get('tracked_loc','?')} tracked LoC across {meta.get('tracked_files','?')} files") out.append(f"**Generated:** {gen} · **Modules:** {len(mods)} · **Size:** {loc_line}\n") # per-layer averages out.append("## Health by layer\n") out.append("| Layer | Modules | Avg score |") out.append("|---|--:|--:|") for b in state.get("bands", []): if b.get("wire"): continue grp = [m for m in mods if m["band"] == b["id"]] if not grp: continue avg = round(sum(m["score"] for m in grp) / len(grp)) out.append(f"| {b.get('t', b['id'])} | {len(grp)} | {avg} |") out.append("") # per-module LoC + score, grouped by band, sorted by loc desc out.append("## Per-module lines of code & score\n") out.append("_LoC is the representative file/folder per module; folder-level modules overlap " "and are not additive._\n") for b in state.get("bands", []): if b.get("wire"): continue grp = sorted([m for m in mods if m["band"] == b["id"]], key=lambda m: -(m.get("loc") or 0)) if not grp: continue out.append(f"### {b.get('t', b['id'])}\n") out.append("| Module | LoC | Score | Tags |") out.append("|---|--:|:--|:--|") for m in grp: tags = ", ".join(t for t in (m.get("tags") or []) if t != "clean") or "—" loc = f"{m.get('loc',0):,}" out.append(f"| {m['label']} | {loc} | {m['score']} {m['grade']} | {tags} |") out.append("") # worst offenders out.append("## Worst offenders\n") worst = sorted(mods, key=lambda m: m["score"])[:10] for m in worst: fnd = m.get("findings") or [] top = next((f for f in fnd if f["sev"] == "HIGH"), fnd[0] if fnd else None) ev = f" — {top['loc']}: {top['text']}" if top else "" out.append(f"- **{m['label']} ({m['score']}/{m['grade']})**{ev}") out.append("") # all findings, by severity out.append("## All findings\n") for sev in ("HIGH", "MED", "LOW"): rows = [(m, f) for m in mods for f in (m.get("findings") or []) if f["sev"] == sev] if not rows: continue out.append(f"### {sev} ({len(rows)})\n") for m, f in rows: out.append(f"- **{m['label']}** · `{f['loc']}` — {f['text']}") out.append("") # cross-cutting themes themes = state.get("reportThemes", []) if themes: out.append("## Cross-cutting themes\n") for h, body in themes: out.append(f"- **{h}.** {body}") out.append("") return "\n".join(out) + "\n" def main(): ap = argparse.ArgumentParser() ap.add_argument("--state", required=True) ap.add_argument("--template", required=True) ap.add_argument("--out-html", required=True) ap.add_argument("--out-md", required=True) args = ap.parse_args() state = json.load(open(args.state, encoding="utf-8")) template = open(args.template, encoding="utf-8").read() open(args.out_html, "w", encoding="utf-8").write(render_html(state, template)) open(args.out_md, "w", encoding="utf-8").write(render_md(state)) n = len(state.get("modules", [])) scored = sum(1 for m in state.get("modules", []) if m.get("score") is not None) print(f"rendered {n} modules ({scored} scored) -> {args.out_html} + {args.out_md}") if __name__ == "__main__": main()