Skip to content

Commit a23ac84

Browse files
committed
feat(v0.3.0): major update with custom rules, CI/CD action, AI remediation, and trend reporting
1 parent 62ceda8 commit a23ac84

9 files changed

Lines changed: 162 additions & 23 deletions

File tree

action.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: 'Roast My Code'
2+
description: 'Brutally honest, AI-powered code quality governance.'
3+
author: 'Rohan'
4+
inputs:
5+
path:
6+
description: 'Path to the directory to roast'
7+
required: false
8+
default: '.'
9+
fail_under:
10+
description: 'Exit with error if the roast score is below this value'
11+
required: false
12+
default: '0'
13+
github_token:
14+
description: 'GitHub token for private repository access'
15+
required: false
16+
default: ${{ github.token }}
17+
runs:
18+
using: 'composite'
19+
steps:
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.11'
24+
- name: Install roast-my-code
25+
shell: bash
26+
run: pip install .
27+
- name: Run Roast
28+
shell: bash
29+
env:
30+
GITHUB_TOKEN: ${{ inputs.github_token }}
31+
run: |
32+
roast ${{ inputs.path }} --no-llm --fail-under ${{ inputs.fail_under }} --json-output roast-report.json
33+
branding:
34+
icon: 'zap'
35+
color: 'red'

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"rich",
1414
"openai",
1515
"jinja2",
16+
"pyyaml",
1617
]
1718

1819
[project.scripts]

roast/analyzer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Literal
99

1010
from roast.scanner import FileResult
11+
from roast.custom_rules import load_custom_rules, CustomRule
1112

1213
Severity = Literal["low", "medium", "high"]
1314

@@ -279,8 +280,13 @@ def _detect_python_medium_severity(file: FileResult, issues: list[Issue], tree:
279280
)
280281

281282
lines = file.content.splitlines()
283+
custom_rules = load_custom_rules()
282284
if not _is_test_file(file.path):
283285
for idx, line in enumerate(lines, start=1):
286+
for rule in custom_rules:
287+
if re.search(rule.pattern, line):
288+
_add_issue(issues, file.path, idx, rule.category, rule.severity, rule.message)
289+
284290
if re.search(r"\bprint\s*\(", line):
285291
_add_issue(
286292
issues,

roast/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from urllib.error import HTTPError, URLError
1212
from urllib.parse import urlparse
1313
from urllib.request import Request, urlopen
14+
from datetime import datetime
1415
import zipfile
1516

1617
import typer
@@ -22,6 +23,7 @@
2223
from roast.reporter import export_html_report, export_json_report, render_terminal_report
2324
from roast.roaster import DEFAULT_GROQ_MODEL, DEFAULT_NIM_MODEL, generate_roast
2425
from roast.scanner import scan_repo
26+
from roast.history import save_history
2527

2628
app = typer.Typer(
2729
help="Brutally honest AI-powered code quality roaster.",
@@ -293,6 +295,15 @@ def roast(
293295
if json_output:
294296
export_json_report(report, roast_result, output_path=json_output)
295297
render_terminal_report(report, roast_result, output_path=output, console=console)
298+
299+
# Save to history for trend tracking
300+
save_history({
301+
"timestamp": datetime.now().isoformat(),
302+
"scores": report.scores,
303+
"overall_score": report.scores.get("Overall", 0),
304+
"verdict": roast_result.verdict,
305+
})
306+
296307
if json_output:
297308
console.print(f"[bold cyan]JSON report saved to: {Path(json_output).expanduser()}[/]")
298309

roast/custom_rules.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import yaml
2+
import re
3+
import os
4+
from dataclasses import dataclass
5+
from typing import List, Optional
6+
7+
@dataclass
8+
class CustomRule:
9+
name: str
10+
pattern: str
11+
severity: str
12+
message: str
13+
category: str = "Code Quality"
14+
15+
def load_custom_rules(config_path: str = ".roast.yaml") -> List[CustomRule]:
16+
try:
17+
if not os.path.exists(config_path):
18+
return []
19+
with open(config_path, "r") as f:
20+
config = yaml.safe_load(f)
21+
if not config or "rules" not in config:
22+
return []
23+
24+
rules = []
25+
for r in config["rules"]:
26+
rules.append(CustomRule(
27+
name=r["name"],
28+
pattern=r["pattern"],
29+
severity=r.get("severity", "medium"),
30+
message=r["message"],
31+
category=r.get("category", "Code Quality")
32+
))
33+
return rules
34+
except Exception as e:
35+
print(f"Error loading custom rules: {e}")
36+
return []

roast/history.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
import os
3+
from datetime import datetime
4+
from pathlib import Path
5+
from typing import Dict, List
6+
7+
HISTORY_DIR = Path(".roast/history")
8+
9+
def save_history(report_data: Dict):
10+
HISTORY_DIR.mkdir(parents=True, exist_ok=True)
11+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
12+
filepath = HISTORY_DIR / f"scan_{timestamp}.json"
13+
with open(filepath, "w") as f:
14+
json.dump(report_data, f)
15+
16+
def get_history() -> List[Dict]:
17+
if not HISTORY_DIR.exists():
18+
return []
19+
20+
history = []
21+
for filepath in sorted(HISTORY_DIR.glob("scan_*.json")):
22+
with open(filepath, "r") as f:
23+
try:
24+
history.append(json.load(f))
25+
except json.JSONDecodeError:
26+
continue
27+
return history

roast/reporter.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from roast.analyzer import AnalysisReport, Issue
1818
from roast.roaster import RoastResult
19+
from roast.history import get_history
1920

2021
HEADER_ART = r"""
2122
██████╗ ██████╗ █████╗ ███████╗████████╗
@@ -77,6 +78,8 @@ def build_report_payload(report: AnalysisReport, roast: RoastResult) -> dict[str
7778
category_counts = _issue_counts_by_category(report.issues)
7879
hotspots = _hotspot_files(report.issues)
7980
badge_color = _badge_markdown_color(overall_score)
81+
history = get_history()
82+
trend = [h["overall_score"] for h in history]
8083

8184
return {
8285
"summary": {
@@ -85,6 +88,7 @@ def build_report_payload(report: AnalysisReport, roast: RoastResult) -> dict[str
8588
"total_issues": len(report.issues),
8689
"scores": report.scores,
8790
},
91+
"trend": trend,
8892
"counts": {
8993
"by_severity": severity_counts,
9094
"by_category": category_counts,
@@ -214,31 +218,30 @@ def export_html_report(
214218
radius = 90
215219
circumference = 2 * pi * radius
216220
dash_offset = circumference * (1 - (overall_score / 100))
217-
issue_rows = payload["issues"]
218-
score_items = [
219-
{
220-
"name": "AI Slop",
221-
"value": report.scores.get("AI Slop", 0),
222-
"color": _badge_color(report.scores.get("AI Slop", 0)),
223-
},
224-
{
225-
"name": "Code Quality",
226-
"value": report.scores.get("Code Quality", 0),
227-
"color": _badge_color(report.scores.get("Code Quality", 0)),
228-
},
229-
{
230-
"name": "Style",
231-
"value": report.scores.get("Style", 0),
232-
"color": _badge_color(report.scores.get("Style", 0)),
233-
},
234-
]
235-
221+
236222
rendered = template.render(
223+
payload=payload,
237224
report=report,
238225
roast=roast,
239226
overall_score=overall_score,
240-
score_items=score_items,
241-
issues=issue_rows,
227+
score_items=[
228+
{
229+
"name": "AI Slop",
230+
"value": report.scores.get("AI Slop", 0),
231+
"color": _badge_color(report.scores.get("AI Slop", 0)),
232+
},
233+
{
234+
"name": "Code Quality",
235+
"value": report.scores.get("Code Quality", 0),
236+
"color": _badge_color(report.scores.get("Code Quality", 0)),
237+
},
238+
{
239+
"name": "Style",
240+
"value": report.scores.get("Style", 0),
241+
"color": _badge_color(report.scores.get("Style", 0)),
242+
},
243+
],
244+
issues=payload["issues"],
242245
severity_counts=payload["counts"]["by_severity"],
243246
category_counts=payload["counts"]["by_category"],
244247
hotspots=payload["hotspots"],

roast/roaster.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
class RoastResult:
2929
headline: str
3030
roast_lines: list[str]
31+
remediations: list[str]
3132
verdict: str
3233
verdict_emoji: str
3334

@@ -85,8 +86,9 @@ def _build_user_prompt(report: AnalysisReport, files: list[FileResult]) -> str:
8586
"Generate:\n"
8687
"1. A one-liner headline roast\n"
8788
"2. 5-8 specific roast bullets\n"
88-
"3. A verdict: SHIP IT (score >= 75), NEEDS WORK (40-74), BURN IT DOWN (<40)\n\n"
89-
"Respond strictly as JSON with keys: headline, roast_lines, verdict, verdict_emoji."
89+
"3. 3-5 specific refactoring/fix suggestions for the issues found\n"
90+
"4. A verdict: SHIP IT (score >= 75), NEEDS WORK (40-74), BURN IT DOWN (<40)\n\n"
91+
"Respond strictly as JSON with keys: headline, roast_lines, remediations, verdict, verdict_emoji."
9092
)
9193

9294

@@ -104,9 +106,16 @@ def _normalize_roast_payload(payload: dict[str, Any], overall_score: int) -> Roa
104106
if len(roast_lines) < 5:
105107
roast_lines.extend(_fallback_roast_lines(overall_score, needed=5 - len(roast_lines)))
106108

109+
remediations_raw = payload.get("remediations", [])
110+
if not isinstance(remediations_raw, list):
111+
remediations_raw = []
112+
remediations = [str(r).strip() for r in remediations_raw if str(r).strip()]
113+
remediations = remediations[:5]
114+
107115
return RoastResult(
108116
headline=headline,
109117
roast_lines=roast_lines,
118+
remediations=remediations,
110119
verdict=verdict,
111120
verdict_emoji=emoji,
112121
)
@@ -142,6 +151,7 @@ def _generate_fallback_roast(report: AnalysisReport) -> RoastResult:
142151
return RoastResult(
143152
headline=headline,
144153
roast_lines=_fallback_roast_lines(overall_score, needed=6),
154+
remediations=["Refactor to remove AI slop", "Standardize naming conventions"],
145155
verdict=verdict,
146156
verdict_emoji=emoji,
147157
)

roast/templates/report.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,16 @@ <h1>🔥 Roast My Code</h1>
344344

345345
<section class="card">
346346
<h2>Scan Summary</h2>
347+
{% if payload.trend|length > 1 %}
348+
<div class="summary-card" style="margin-bottom: 20px;">
349+
<h3>Score Trend</h3>
350+
<div style="height: 100px; display: flex; align-items: flex-end; gap: 4px;">
351+
{% for score in payload.trend %}
352+
<div style="flex: 1; background: #6366f1; height: {{ score }}%;" title="Score: {{ score }}"></div>
353+
{% endfor %}
354+
</div>
355+
</div>
356+
{% endif %}
347357
<div class="summary-grid">
348358
<div class="summary-card">
349359
<h3>Severity Breakdown</h3>

0 commit comments

Comments
 (0)