Files
codemap-skill-Asixa/scripts/render.py
T

153 lines
5.7 KiB
Python

#!/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("</", "<\\/")
return template.replace("__ARCH_DATA__", blob)
def render_md(state):
meta = state.get("meta", {})
mods = [m for m in state.get("modules", []) if m.get("score") is not None]
bands = {b["id"]: b for b in state.get("bands", [])}
out = []
proj = meta.get("project", "Project")
out.append("<!--")
out.append(f" This file: {meta.get('mdPath', 'architecture-audit.md')} (written report)")
out.append(f" Interactive map: {meta.get('htmlPath', 'architecture-map.html')}")
out.append("-->\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()