Skip to content

Commit e730fd5

Browse files
committed
Add roast/reporter.py
1 parent 4c9a3b8 commit e730fd5

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

roast/reporter.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Terminal and HTML reporting."""
2+
3+
from __future__ import annotations
4+
5+
from math import pi
6+
from pathlib import Path
7+
8+
from jinja2 import Environment, FileSystemLoader, select_autoescape
9+
from rich import box
10+
from rich.console import Console
11+
from rich.panel import Panel
12+
from rich.table import Table
13+
from rich.text import Text
14+
15+
from roast.analyzer import AnalysisReport, Issue
16+
from roast.roaster import RoastResult
17+
18+
HEADER_ART = r"""
19+
██████╗ ██████╗ █████╗ ███████╗████████╗
20+
██╔══██╗██╔═══██╗██╔══██╗██╔════╝╚══██╔══╝
21+
██████╔╝██║ ██║███████║███████╗ ██║
22+
██╔══██╗██║ ██║██╔══██║╚════██║ ██║
23+
██║ ██║╚██████╔╝██║ ██║███████║ ██║
24+
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝
25+
"""
26+
27+
28+
def _severity_order(issue: Issue) -> int:
29+
return {"high": 0, "medium": 1, "low": 2}.get(issue.severity, 3)
30+
31+
32+
def _score_color(score: int) -> str:
33+
if score >= 75:
34+
return "green"
35+
if score >= 40:
36+
return "yellow"
37+
return "red"
38+
39+
40+
def _score_bar(score: int, width: int = 24) -> str:
41+
filled = int((score / 100) * width)
42+
empty = width - filled
43+
color = _score_color(score)
44+
return f"[{color}]{'█' * filled}[/]{'░' * empty}"
45+
46+
47+
def render_terminal_report(
48+
report: AnalysisReport,
49+
roast: RoastResult,
50+
output_path: str | Path,
51+
console: Console | None = None,
52+
) -> None:
53+
"""Render the terminal scorecard with Rich."""
54+
console = console or Console()
55+
console.print(Panel.fit(Text(HEADER_ART, style="bold magenta"), title="🔥 ROAST-MY-CODE", border_style="red"))
56+
57+
score_table = Table(title="Score Card", box=box.SIMPLE_HEAVY)
58+
score_table.add_column("Category", style="bold")
59+
score_table.add_column("Score", justify="right")
60+
score_table.add_column("Bar Chart")
61+
for category in ("AI Slop", "Code Quality", "Style", "Overall"):
62+
score = report.scores.get(category, 0)
63+
score_table.add_row(
64+
category,
65+
f"[{_score_color(score)}]{score}[/]",
66+
_score_bar(score),
67+
)
68+
console.print(score_table)
69+
70+
issues_table = Table(title="Top 10 Issues", box=box.SIMPLE)
71+
issues_table.add_column("File", overflow="fold")
72+
issues_table.add_column("Line", justify="right", width=6)
73+
issues_table.add_column("Severity", width=8)
74+
issues_table.add_column("Description", overflow="fold")
75+
76+
top_issues = sorted(report.issues, key=lambda issue: (_severity_order(issue), issue.file, issue.line or 0))[:10]
77+
if not top_issues:
78+
issues_table.add_row("-", "-", "-", "No issues found. Miracles happen.")
79+
for issue in top_issues:
80+
row_style = {"high": "bold red", "medium": "yellow", "low": "white"}.get(issue.severity, "white")
81+
issues_table.add_row(
82+
issue.file,
83+
str(issue.line) if issue.line is not None else "-",
84+
issue.severity.upper(),
85+
issue.description,
86+
style=row_style,
87+
)
88+
console.print(issues_table)
89+
90+
console.print(Panel.fit(Text(roast.headline, style="bold yellow"), title="Roast Headline", border_style="yellow"))
91+
for line in roast.roast_lines:
92+
console.print(f"🔥 {line}")
93+
94+
verdict_color = {"SHIP IT": "green", "NEEDS WORK": "yellow", "BURN IT DOWN": "red"}.get(roast.verdict, "white")
95+
verdict_text = Text(f"{roast.verdict_emoji} {roast.verdict} {roast.verdict_emoji}", style=f"bold {verdict_color}")
96+
console.print(Panel.fit(verdict_text, title="Verdict", border_style=verdict_color))
97+
98+
console.print(f"\n[bold cyan]HTML report saved to: {Path(output_path)}[/]")
99+
100+
101+
def _badge_color(score: int) -> str:
102+
if score >= 75:
103+
return "#2ea043"
104+
if score >= 40:
105+
return "#d29922"
106+
return "#f85149"
107+
108+
109+
def export_html_report(
110+
report: AnalysisReport,
111+
roast: RoastResult,
112+
output_path: str | Path,
113+
) -> Path:
114+
"""Render a single self-contained HTML report."""
115+
template_dir = Path(__file__).resolve().parent / "templates"
116+
env = Environment(
117+
loader=FileSystemLoader(str(template_dir)),
118+
autoescape=select_autoescape(enabled_extensions=("html",)),
119+
)
120+
template = env.get_template("report.html")
121+
122+
overall_score = report.scores.get("Overall", 0)
123+
radius = 90
124+
circumference = 2 * pi * radius
125+
dash_offset = circumference * (1 - (overall_score / 100))
126+
issue_rows = sorted(report.issues, key=lambda issue: (_severity_order(issue), issue.file, issue.line or 0))
127+
score_items = [
128+
{
129+
"name": "AI Slop",
130+
"value": report.scores.get("AI Slop", 0),
131+
"color": _badge_color(report.scores.get("AI Slop", 0)),
132+
},
133+
{
134+
"name": "Code Quality",
135+
"value": report.scores.get("Code Quality", 0),
136+
"color": _badge_color(report.scores.get("Code Quality", 0)),
137+
},
138+
{
139+
"name": "Style",
140+
"value": report.scores.get("Style", 0),
141+
"color": _badge_color(report.scores.get("Style", 0)),
142+
},
143+
]
144+
145+
badge_markdown = f"![Roast Score](https://img.shields.io/badge/Roast_Score-{overall_score}-red)"
146+
rendered = template.render(
147+
report=report,
148+
roast=roast,
149+
overall_score=overall_score,
150+
score_items=score_items,
151+
issues=issue_rows,
152+
score_ring_circumference=f"{circumference:.2f}",
153+
score_ring_offset=f"{dash_offset:.2f}",
154+
overall_color=_badge_color(overall_score),
155+
badge_markdown=badge_markdown,
156+
)
157+
158+
output = Path(output_path).expanduser().resolve()
159+
output.write_text(rendered, encoding="utf-8")
160+
return output

0 commit comments

Comments
 (0)