|
| 1 | +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. |
| 2 | +# |
| 3 | +# Copyright (C) 2026 Tencent. All rights reserved. |
| 4 | +# |
| 5 | +# tRPC-Agent-Python is licensed under Apache-2.0. |
| 6 | +"""Report assembly and rendering.""" |
| 7 | +import json |
| 8 | +import os |
| 9 | +from datetime import datetime |
| 10 | + |
| 11 | +from .findings import severity_distribution |
| 12 | +from .redaction import redact_text |
| 13 | + |
| 14 | +_BLOCKING = {"critical", "high"} |
| 15 | + |
| 16 | + |
| 17 | +def _conclusion(reported, filter_events): |
| 18 | + if any(f.severity in _BLOCKING for f in reported): |
| 19 | + return "blocked" |
| 20 | + intercepted = any(e.decision != "allow" for e in filter_events) |
| 21 | + if reported or intercepted: |
| 22 | + return "needs_attention" |
| 23 | + return "pass" |
| 24 | + |
| 25 | + |
| 26 | +def build_report(task_id, input_ref, runtime, dry_run, diff_summary, reported, |
| 27 | + needs_review, deduped_count, filter_events, sandbox_outcomes, |
| 28 | + metrics, llm_summary, warnings) -> dict: |
| 29 | + """Assemble the full report dict (issue acceptance criterion #8).""" |
| 30 | + return { |
| 31 | + "task_id": task_id, |
| 32 | + "generated_at": datetime.now().isoformat(timespec="seconds"), |
| 33 | + "input": {"ref": input_ref, "runtime": runtime, "dry_run": dry_run, |
| 34 | + "diff_summary": diff_summary}, |
| 35 | + "conclusion": _conclusion(reported, filter_events), |
| 36 | + "summary": { |
| 37 | + "total_findings": len(reported), |
| 38 | + "by_severity": severity_distribution(reported), |
| 39 | + "needs_human_review_count": len(needs_review), |
| 40 | + "deduplicated": deduped_count, |
| 41 | + "intercepts": sum(1 for e in filter_events if e.decision != "allow"), |
| 42 | + }, |
| 43 | + "findings": [f.model_dump() for f in reported], |
| 44 | + "needs_human_review": [f.model_dump() for f in needs_review], |
| 45 | + "filter_intercepts": [ |
| 46 | + {"target": e.target, "decision": e.decision, "rule": e.rule, "reason": e.reason} |
| 47 | + for e in filter_events if e.decision != "allow"], |
| 48 | + "sandbox_runs": [ |
| 49 | + {"script": o.script, "status": o.status, "exit_code": o.exit_code, |
| 50 | + "duration_ms": o.duration_ms, "timed_out": o.timed_out, |
| 51 | + "error_type": o.error_type} for o in sandbox_outcomes], |
| 52 | + "metrics": metrics, |
| 53 | + "llm_summary": llm_summary, |
| 54 | + "warnings": list(warnings), |
| 55 | + } |
| 56 | + |
| 57 | + |
| 58 | +def _md_findings_table(findings): |
| 59 | + if not findings: |
| 60 | + return "_none_\n" |
| 61 | + lines = ["| Severity | Category | File | Line | Title | Confidence | Source |", |
| 62 | + "|---|---|---|---|---|---|---|"] |
| 63 | + for f in findings: |
| 64 | + lines.append(f"| {f['severity']} | {f['category']} | {f['file']} | {f['line']} " |
| 65 | + f"| {f['title']} | {f['confidence']} | {f['source']} |") |
| 66 | + return "\n".join(lines) + "\n" |
| 67 | + |
| 68 | + |
| 69 | +def render_markdown(report: dict) -> str: |
| 70 | + """Render the human-readable markdown report.""" |
| 71 | + s = report["summary"] |
| 72 | + parts = [ |
| 73 | + "# Code Review Report", |
| 74 | + f"- Task: `{report['task_id']}`", |
| 75 | + f"- Conclusion: **{report['conclusion']}**", |
| 76 | + f"- Input: {report['input']['ref']} (runtime={report['input']['runtime']}, " |
| 77 | + f"dry_run={report['input']['dry_run']})", |
| 78 | + f"- Findings: {s['total_findings']} (by severity: {s['by_severity']}), " |
| 79 | + f"needs human review: {s['needs_human_review_count']}, " |
| 80 | + f"deduplicated: {s['deduplicated']}, intercepts: {s['intercepts']}", |
| 81 | + "", |
| 82 | + "## Findings", |
| 83 | + _md_findings_table(report["findings"]), |
| 84 | + "### Recommendations", |
| 85 | + "\n".join(f"- `{f['file']}:{f['line']}` {f['recommendation']}" |
| 86 | + for f in report["findings"]) or "_none_", |
| 87 | + "", |
| 88 | + "## Needs Human Review", |
| 89 | + _md_findings_table(report["needs_human_review"]), |
| 90 | + "## Filter Intercepts", |
| 91 | + "\n".join(f"- [{e['decision']}] `{e['target']}` ({e['rule']}): {e['reason']}" |
| 92 | + for e in report["filter_intercepts"]) or "_none_", |
| 93 | + "", |
| 94 | + "## Sandbox Runs", |
| 95 | + "\n".join(f"- `{o['script']}`: {o['status']} (exit={o['exit_code']}, " |
| 96 | + f"{o['duration_ms']}ms, timed_out={o['timed_out']})" |
| 97 | + for o in report["sandbox_runs"]) or "_none_", |
| 98 | + "", |
| 99 | + "## Metrics", |
| 100 | + "```json", |
| 101 | + json.dumps(report["metrics"], indent=2), |
| 102 | + "```", |
| 103 | + "", |
| 104 | + f"## LLM Summary\n{report['llm_summary'] or '_none_'}", |
| 105 | + "", |
| 106 | + "## Warnings", |
| 107 | + "\n".join(f"- {w}" for w in report["warnings"]) or "_none_", |
| 108 | + ] |
| 109 | + return "\n".join(parts) + "\n" |
| 110 | + |
| 111 | + |
| 112 | +def write_reports(report: dict, output_dir: str): |
| 113 | + """Write review_report.json / review_report.md (final redaction pass).""" |
| 114 | + os.makedirs(output_dir, exist_ok=True) |
| 115 | + json_path = os.path.join(output_dir, "review_report.json") |
| 116 | + md_path = os.path.join(output_dir, "review_report.md") |
| 117 | + with open(json_path, "w", encoding="utf-8") as fh: |
| 118 | + fh.write(redact_text(json.dumps(report, indent=2, ensure_ascii=False, default=str))) |
| 119 | + with open(md_path, "w", encoding="utf-8") as fh: |
| 120 | + fh.write(redact_text(render_markdown(report))) |
| 121 | + return json_path, md_path |
0 commit comments