# render_report.py
"""Summarize a completed Execute run (C7) for in-chat display.
Two input modes (mutually exclusive, exactly one required):
render_report.py --phase-results
--repo [--date-suffix YYYY-MM-DD]
Reads rewrite.json + eval.json (+ delta-decisions.json) from the
phase-results directory — the canonical C7 path; no LLM-assembled
payload involved.
render_report.py
Legacy single-JSON mode ({rewrite, evalRes, repo, reportDateSuffix}),
retained for tests.
`--results-dir` keeps its original meaning in BOTH modes: the directory the
report file is copied to (default ~/saws-migrate-results).
summarize(payload) -> str is pure (testable).
"""
import json, re, sys, shutil, pathlib, argparse
def summarize(payload: dict) -> str:
rw = payload.get("rewrite") or {}
ev = payload.get("evalRes") or {}
rw = rw if isinstance(rw, dict) else {}
ev = ev if isinstance(ev, dict) else {}
files = rw.get("files_changed") or []
notes = ev.get("notes") or ""
# The evaluator emits pass_rate 1.0 with a `no_golden_cases: true` notes
# prefix when the golden dataset was empty — rendering that as "100%"
# would misrepresent a run with zero quality scoring.
if "no_golden_cases: true" in notes:
pass_line = "- Prompt eval pass rate: N/A (no golden cases — quality scoring skipped)"
else:
pass_rate = ev.get("pass_rate")
if isinstance(pass_rate, (int, float)):
pass_line = f"- Prompt eval pass rate: {round(pass_rate * 100)}% ({ev.get('total_cases', 0)} cases, {ev.get('failures', 0)} failures)"
m = re.search(r"partial_coverage: (\S+)", notes)
if m:
pass_line += f" — partial coverage {m.group(1)} (throttled)"
else:
pass_line = "- Prompt eval pass rate: (unavailable)"
# Test counts live only in the rewriter's free-text notes (e.g. "5 tests
# generated, 5/5 passing"); show them when parseable, omit otherwise.
m = re.search(r"(\d+)\s*/\s*(\d+)\s+passing", rw.get("notes") or "")
test_line = f"- Tests: {m.group(1)}/{m.group(2)} passing" if m else None
deltas = payload.get("deltaDecisions")
delta_line = (f"- Behavior-delta decisions applied: {len(deltas)}"
if isinstance(deltas, list) and deltas else None)
lines = [
"AI Migration Complete!",
f"- Branch: {rw.get('branch_name', '(none)')}",
pass_line,
f"- Files modified: {len(files)} files",
test_line,
delta_line,
f"- Report: {find_report_path(payload) or '(see repository root: MIGRATION_REPORT_*.md)'}",
]
return "\n".join(l for l in lines if l)
def find_report_path(payload: dict) -> str | None:
"""Locate MIGRATION_REPORT_.md from the payload's repo path."""
repo = payload.get("repo")
suffix = payload.get("reportDateSuffix")
if not repo:
return None
if suffix:
p = pathlib.Path(repo) / f"MIGRATION_REPORT_{suffix}.md"
if p.exists():
return str(p)
candidates = sorted(pathlib.Path(repo).glob("MIGRATION_REPORT_*.md"))
return str(candidates[-1]) if candidates else None
def load_phase_results(results_dir: str, repo: str, date_suffix: str | None) -> dict:
"""Assemble the summarize() payload from phase-result files. Missing or
control-state files degrade to empty dicts (summarize handles absence)."""
d = pathlib.Path(results_dir)
def read(name):
try:
data = json.loads((d / name).read_text())
except (OSError, json.JSONDecodeError):
return None
# A blocked/partial control-state file is not a payload.
if isinstance(data, dict) and ("blocked" in data or "partial" in data):
return None
return data
return {
"rewrite": read("rewrite.json") or {},
"evalRes": read("eval.json") or {},
"deltaDecisions": read("delta-decisions.json"),
"repo": repo,
"reportDateSuffix": date_suffix,
}
def main(argv=None) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("payload_json", nargs="?",
help="legacy single-JSON payload path (mutually exclusive with --phase-results)")
ap.add_argument("--phase-results", metavar="DIR",
help="phase-results directory (reads rewrite.json/eval.json/delta-decisions.json)")
ap.add_argument("--repo", help="repository root (required with --phase-results)")
ap.add_argument("--date-suffix", help="report date suffix YYYY-MM-DD (with --phase-results)")
ap.add_argument("--results-dir", default=str(pathlib.Path.home() / "saws-migrate-results"),
help="directory the report file is copied to (both modes)")
args = ap.parse_args(argv)
if bool(args.payload_json) == bool(args.phase_results):
ap.error("provide exactly one of: payload_json, --phase-results")
if args.phase_results:
if not args.repo:
ap.error("--phase-results requires --repo")
payload = load_phase_results(args.phase_results, args.repo, args.date_suffix)
else:
try:
payload = json.loads(pathlib.Path(args.payload_json).read_text())
except (OSError, json.JSONDecodeError) as e:
print(f"render_report: cannot read payload JSON: {e}", file=sys.stderr)
return 1
report_path = find_report_path(payload)
if report_path:
dest_dir = pathlib.Path(args.results_dir)
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(report_path, dest_dir / pathlib.Path(report_path).name)
print(summarize(payload))
return 0
if __name__ == "__main__":
sys.exit(main())