Skip to content

Commit 98ead74

Browse files
committed
feat: upgrade remote scanning and reporting (roast/reporter.py)
1 parent 2e877fc commit 98ead74

1 file changed

Lines changed: 105 additions & 4 deletions

File tree

roast/reporter.py

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
"""Terminal and HTML reporting."""
1+
"""Terminal, HTML, and JSON reporting."""
22

33
from __future__ import annotations
44

5+
from collections import Counter
6+
import json
57
from math import pi
68
from pathlib import Path
79

@@ -37,13 +39,79 @@ def _score_color(score: int) -> str:
3739
return "red"
3840

3941

42+
def _badge_markdown_color(score: int) -> str:
43+
if score >= 75:
44+
return "brightgreen"
45+
if score >= 40:
46+
return "yellow"
47+
return "red"
48+
49+
4050
def _score_bar(score: int, width: int = 24) -> str:
4151
filled = int((score / 100) * width)
4252
empty = width - filled
4353
color = _score_color(score)
4454
return f"[{color}]{'█' * filled}[/]{'░' * empty}"
4555

4656

57+
def _issue_counts_by_severity(issues: list[Issue]) -> dict[str, int]:
58+
counts = {"high": 0, "medium": 0, "low": 0}
59+
for issue in issues:
60+
counts[issue.severity] = counts.get(issue.severity, 0) + 1
61+
return counts
62+
63+
64+
def _issue_counts_by_category(issues: list[Issue]) -> dict[str, int]:
65+
ordered = Counter(issue.category for issue in issues)
66+
return dict(sorted(ordered.items(), key=lambda item: (-item[1], item[0].lower())))
67+
68+
69+
def _hotspot_files(issues: list[Issue], limit: int = 5) -> list[dict[str, int | str]]:
70+
ranked = Counter(issue.file for issue in issues).most_common(limit)
71+
return [{"file": file_path, "issue_count": count} for file_path, count in ranked]
72+
73+
74+
def build_report_payload(report: AnalysisReport, roast: RoastResult) -> dict[str, object]:
75+
overall_score = report.scores.get("Overall", 0)
76+
severity_counts = _issue_counts_by_severity(report.issues)
77+
category_counts = _issue_counts_by_category(report.issues)
78+
hotspots = _hotspot_files(report.issues)
79+
badge_color = _badge_markdown_color(overall_score)
80+
81+
return {
82+
"summary": {
83+
"total_files": report.total_files,
84+
"total_lines": report.total_lines,
85+
"total_issues": len(report.issues),
86+
"scores": report.scores,
87+
},
88+
"counts": {
89+
"by_severity": severity_counts,
90+
"by_category": category_counts,
91+
},
92+
"hotspots": hotspots,
93+
"roast": {
94+
"headline": roast.headline,
95+
"roast_lines": roast.roast_lines,
96+
"verdict": roast.verdict,
97+
"verdict_emoji": roast.verdict_emoji,
98+
},
99+
"share": {
100+
"badge_markdown": f"![Roast Score](https://img.shields.io/badge/Roast_Score-{overall_score}-{badge_color})",
101+
},
102+
"issues": [
103+
{
104+
"file": issue.file,
105+
"line": issue.line,
106+
"category": issue.category,
107+
"severity": issue.severity,
108+
"description": issue.description,
109+
}
110+
for issue in sorted(report.issues, key=lambda item: (_severity_order(item), item.file, item.line or 0))
111+
],
112+
}
113+
114+
47115
def render_terminal_report(
48116
report: AnalysisReport,
49117
roast: RoastResult,
@@ -67,6 +135,23 @@ def render_terminal_report(
67135
)
68136
console.print(score_table)
69137

138+
severity_counts = _issue_counts_by_severity(report.issues)
139+
category_counts = _issue_counts_by_category(report.issues)
140+
hotspots = _hotspot_files(report.issues, limit=3)
141+
category_summary = ", \".join(f"{name}: {count}" for name, count in category_counts.items()) or "No categories to summarize."
142+
hotspot_summary = ", \".join(f"{item['file']} ({item['issue_count']})" for item in hotspots) or "No hotspots found."
143+
console.print(
144+
Panel.fit(
145+
Text.from_markup(
146+
f"[bold red]High:[/] {severity_counts['high']} [bold yellow]Medium:[/] {severity_counts['medium']} [bold green]Low:[/] {severity_counts['low']}\n"
147+
f"[bold]Categories:[/] {category_summary}\n"
148+
f"[bold]Hotspots:[/] {hotspot_summary}"
149+
),
150+
title="Summary",
151+
border_style="cyan",
152+
)
153+
)
154+
70155
issues_table = Table(title="Top 10 Issues", box=box.SIMPLE)
71156
issues_table.add_column("File", overflow="fold")
72157
issues_table.add_column("Line", justify="right", width=6)
@@ -124,11 +209,12 @@ def export_html_report(
124209
)
125210
template = env.get_template("report.html")
126211

212+
payload = build_report_payload(report, roast)
127213
overall_score = report.scores.get("Overall", 0)
128214
radius = 90
129215
circumference = 2 * pi * radius
130216
dash_offset = circumference * (1 - (overall_score / 100))
131-
issue_rows = sorted(report.issues, key=lambda issue: (_severity_order(issue), issue.file, issue.line or 0))
217+
issue_rows = payload["issues"]
132218
score_items = [
133219
{
134220
"name": "AI Slop",
@@ -147,19 +233,34 @@ def export_html_report(
147233
},
148234
]
149235

150-
badge_markdown = f"![Roast Score](https://img.shields.io/badge/Roast_Score-{overall_score}-red)"
151236
rendered = template.render(
152237
report=report,
153238
roast=roast,
154239
overall_score=overall_score,
155240
score_items=score_items,
156241
issues=issue_rows,
242+
severity_counts=payload["counts"]["by_severity"],
243+
category_counts=payload["counts"]["by_category"],
244+
hotspots=payload["hotspots"],
245+
total_issues=payload["summary"]["total_issues"],
157246
score_ring_circumference=f"{circumference:.2f}",
158247
score_ring_offset=f"{dash_offset:.2f}",
159248
overall_color=_badge_color(overall_score),
160-
badge_markdown=badge_markdown,
249+
badge_markdown=payload["share"]["badge_markdown"],
161250
)
162251

163252
output = Path(output_path).expanduser().resolve()
164253
output.write_text(rendered, encoding="utf-8")
165254
return output
255+
256+
257+
def export_json_report(
258+
report: AnalysisReport,
259+
roast: RoastResult,
260+
output_path: str | Path,
261+
) -> Path:
262+
"""Export a machine-readable JSON report."""
263+
payload = build_report_payload(report, roast)
264+
output = Path(output_path).expanduser().resolve()
265+
output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
266+
return output

0 commit comments

Comments
 (0)