Skip to content

Commit 1e855bb

Browse files
committed
feat(examples): add review report builder and markdown renderer
1 parent 997a5bd commit 1e855bb

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
"""Tests for report building and rendering."""
7+
import json
8+
from pathlib import Path
9+
10+
from review.findings import Finding
11+
from review.governance import GovernanceDecision
12+
from review.report import build_report, render_markdown, write_reports
13+
from review.sandbox import SandboxRunOutcome
14+
15+
16+
def _report(reported=None, events=None):
17+
return build_report(
18+
task_id="t1", input_ref="x.diff", runtime="local", dry_run=True,
19+
diff_summary={"files_changed": 1},
20+
reported=reported or [],
21+
needs_review=[Finding(severity="low", category="db_lifecycle", file="b.py",
22+
line=2, title="cursor", confidence=0.5)],
23+
deduped_count=1,
24+
filter_events=events or [GovernanceDecision("curl x", "deny", "network_policy", "no net")],
25+
sandbox_outcomes=[SandboxRunOutcome(script="check_security.py", status="ok",
26+
exit_code=0, duration_ms=10, timed_out=False,
27+
stdout="{}", stderr="")],
28+
metrics={"total_duration_ms": 100, "sandbox_duration_ms": 50, "tool_calls": 6,
29+
"intercepts": 1, "findings_total": 1,
30+
"severity_distribution": {"high": 1}, "error_distribution": {}},
31+
llm_summary="fine", warnings=["w1"])
32+
33+
34+
def test_conclusion_blocked_on_high():
35+
rep = _report(reported=[Finding(severity="high", category="security", file="a.py",
36+
line=1, title="eval", confidence=0.9)])
37+
assert rep["conclusion"] == "blocked"
38+
39+
40+
def test_conclusion_needs_attention_on_intercepts_only():
41+
rep = _report()
42+
assert rep["conclusion"] == "needs_attention"
43+
44+
45+
def test_conclusion_pass_when_clean():
46+
rep = _report(events=[GovernanceDecision("ok", "allow")])
47+
assert rep["conclusion"] == "pass"
48+
49+
50+
def test_report_sections_present():
51+
rep = _report()
52+
for key in ("summary", "findings", "needs_human_review", "filter_intercepts",
53+
"sandbox_runs", "metrics", "llm_summary", "warnings"):
54+
assert key in rep
55+
assert rep["summary"]["needs_human_review_count"] == 1
56+
assert rep["summary"]["deduplicated"] == 1
57+
58+
59+
def test_markdown_contains_sections():
60+
md = render_markdown(_report())
61+
for heading in ("# Code Review Report", "## Findings", "## Needs Human Review",
62+
"## Filter Intercepts", "## Sandbox Runs", "## Metrics"):
63+
assert heading in md
64+
65+
66+
def test_write_reports_redacts(tmp_path):
67+
rep = _report(reported=[Finding(
68+
severity="critical", category="secret_leak", file="s.py", line=1,
69+
title="secret", evidence='k = "sk-abcdefghijklmnopqrstuvwxyz123456"',
70+
confidence=0.95)])
71+
json_path, md_path = write_reports(rep, str(tmp_path))
72+
text = Path(json_path).read_text() + Path(md_path).read_text()
73+
assert "sk-abcdefghijklmnopqrstuvwxyz123456" not in text
74+
assert json.loads(Path(json_path).read_text())["task_id"] == "t1"

0 commit comments

Comments
 (0)