""" report.py - Stage 4: assemble the final report. Produces a single JSON object (also the web API payload) and a human-readable Markdown summary grouped by severity. """ from typing import List, Dict from datetime import datetime, timezone def build_report(conflicts: List[Dict], sheets: List[Dict], clusters: List[Dict], source: str = "") -> Dict: """Assemble the structured report object.""" by_sev = {"high": 0, "medium": 0, "low": 0} by_cat: Dict[str, int] = {} for c in conflicts: by_sev[c["severity"]] = by_sev.get(c["severity"], 0) + 1 by_cat[c["category"]] = by_cat.get(c["category"], 0) + 1 disciplines = sorted({s["discipline"] for s in sheets if s.get("discipline")}) return { "source": source, "generated_at": datetime.now(timezone.utc).isoformat(), "summary": { "sheets_analyzed": len(sheets), "disciplines": disciplines, "assertions_extracted": sum(len(s.get("assertions", [])) for s in sheets), "clusters_checked": len(clusters), "conflicts_found": len(conflicts), "by_severity": by_sev, "by_category": by_cat, }, "conflicts": conflicts, "sheets": [ { "page_number": s.get("page_number"), "sheet_number": s.get("sheet_number"), "discipline": s.get("discipline"), "sheet_title": s.get("sheet_title"), "assertion_count": len(s.get("assertions", [])), } for s in sheets ], } def to_markdown(report: Dict) -> str: s = report["summary"] lines = [ f"# Conflict Check Report", "", f"- Source: `{report.get('source','')}`", f"- Generated: {report.get('generated_at','')}", f"- Sheets analyzed: {s['sheets_analyzed']} ({', '.join(s['disciplines']) or 'none'})", f"- Assertions extracted: {s['assertions_extracted']}", f"- Clusters checked: {s['clusters_checked']}", f"- **Conflicts found: {s['conflicts_found']}** " f"(high {s['by_severity']['high']}, medium {s['by_severity']['medium']}, low {s['by_severity']['low']})", "", ] if not report["conflicts"]: lines += ["No cross-discipline conflicts detected.", ""] return "\n".join(lines) for sev in ("high", "medium", "low"): group = [c for c in report["conflicts"] if c["severity"] == sev] if not group: continue lines += [f"## {sev.capitalize()} severity ({len(group)})", ""] for i, c in enumerate(group, 1): lines += [ f"### {i}. [{c['category']}] {c['location']}", f"- Disciplines: {', '.join(c.get('disciplines', []))}", f"- Sheets: {', '.join(c.get('sheets', []))}", f"- {c['description']}", ] for ev in c.get("evidence", []): lines.append( f" - {ev.get('discipline','?')} ({ev.get('sheet','?')}): " f"\"{ev.get('source_text','')}\"" ) if c.get("recommended_resolution"): lines.append(f"- Resolution: {c['recommended_resolution']}") lines.append(f"- Confidence: {c.get('confidence','')}") lines.append("") return "\n".join(lines)