Skip to content

Commit 7719aaf

Browse files
committed
feat: markdown reports + optional FastAPI viewer; CLI formats (json|md|table)
1 parent 300f419 commit 7719aaf

File tree

4 files changed

+163
-28
lines changed

4 files changed

+163
-28
lines changed

src/diff_risk_dashboard/cli.py

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,70 @@
44
import json
55
import sys
66
from pathlib import Path
7+
from typing import Literal, TypedDict
78

8-
from .core import Summary, summarize
9+
from .core import summarize_apv_json
10+
from .report import to_markdown
11+
12+
13+
class Summary(TypedDict):
14+
total: int
15+
worst: str
16+
risk: Literal["red", "yellow", "green"]
17+
by_severity: dict
918

1019

1120
def _print_table(summary: Summary) -> None:
12-
bs = summary["by_severity"]
13-
rows = [
14-
("CRITICAL", bs["CRITICAL"]),
15-
("HIGH", bs["HIGH"]),
16-
("MEDIUM", bs["MEDIUM"]),
17-
("LOW", bs["LOW"]),
18-
("INFO", bs["INFO"]),
19-
]
20-
print("\n=== Diff Risk Summary ===")
21-
print(f"Total findings: {summary['total']}")
22-
print("Severity counts:")
23-
w = max(len(r[0]) for r in rows)
24-
for name, cnt in rows:
25-
print(f" {name:<{w}} : {cnt}")
26-
print(f"Worst severity : {summary['worst']}")
27-
print(f"Risk level : {summary['risk_level']}\n")
28-
29-
30-
def main(argv: list[str] | None = None) -> int:
31-
p = argparse.ArgumentParser(description="Diff Risk Dashboard (APV JSON -> summary)")
32-
p.add_argument("apv_json", help="Path to ai-patch-verifier JSON")
33-
args = p.parse_args(argv)
34-
data = json.loads(Path(args.apv_json).read_text(encoding="utf-8"))
35-
sm = summarize(data)
36-
_print_table(sm)
37-
return 2 if sm["risk_level"] == "red" else (1 if sm["risk_level"] == "yellow" else 0)
21+
by = summary["by_severity"]
22+
print("Severity\tCount")
23+
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
24+
print(f"{sev}\t{by.get(sev, by.get(sev.lower(), 0))}")
25+
print(f"TOTAL\t{summary['total']}")
26+
27+
28+
def _exit_code(risk: str) -> int:
29+
# 0=green, 1=yellow, 2=red
30+
return {"green": 0, "yellow": 1, "red": 2}.get(risk, 0)
31+
32+
33+
def main() -> int:
34+
p = argparse.ArgumentParser(prog="diff-risk")
35+
p.add_argument("input", help="Path o texto JSON de APV (ai-patch-verifier)")
36+
p.add_argument(
37+
"-f",
38+
"--format",
39+
choices=["table", "json", "md"],
40+
default="table",
41+
help="Formato de salida (por defecto: table)",
42+
)
43+
p.add_argument(
44+
"-o", "--output", default="-", help="Archivo de salida; '-' = stdout (por defecto)"
45+
)
46+
p.add_argument(
47+
"--no-exit-by-risk", action="store_true", help="No ajustar el exit code por nivel de riesgo"
48+
)
49+
args = p.parse_args()
50+
51+
summary: Summary = summarize_apv_json(args.input) # path o texto
52+
out = ""
53+
if args.format == "json":
54+
out = json.dumps(summary, indent=2)
55+
elif args.format == "md":
56+
out = to_markdown(summary)
57+
else:
58+
_print_table(summary)
59+
60+
if args.format in {"json", "md"}:
61+
if args.output == "-":
62+
print(out)
63+
else:
64+
Path(args.output).write_text(out, encoding="utf-8")
65+
print(f"Wrote {args.output}", file=sys.stderr)
66+
67+
if not args.no - exit - by - risk:
68+
return _exit_code(summary["risk"])
69+
return 0
3870

3971

4072
if __name__ == "__main__":
41-
sys.exit(main())
73+
raise SystemExit(main())

src/diff_risk_dashboard/report.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping
4+
from typing import Any
5+
6+
_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
7+
8+
9+
def _get_counts(summary: Mapping[str, Any]) -> dict[str, int]:
10+
counts: dict[str, int] = {}
11+
by = summary.get("by_severity", {}) or {}
12+
if isinstance(by, Mapping):
13+
for sev in _SEVERITIES:
14+
# Acepta claves en minúsculas o MAYÚSCULAS
15+
counts[sev] = int(by.get(sev, by.get(sev.lower(), 0)) or 0)
16+
return counts
17+
18+
19+
def to_markdown(summary: Mapping[str, Any]) -> str:
20+
counts = _get_counts(summary)
21+
total = int(summary.get("total", 0) or 0)
22+
worst = str(summary.get("worst", "INFO") or "INFO").upper()
23+
risk = str(summary.get("risk", "green") or "green").lower()
24+
25+
risk_emoji = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(risk, "🟢")
26+
lines = []
27+
lines.append(f"# Diff Risk Dashboard {risk_emoji} — Worst: **{worst}**")
28+
lines.append("")
29+
lines.append("| Severity | Count |")
30+
lines.append("|---|---:|")
31+
for sev in _SEVERITIES:
32+
lines.append(f"| {sev} | {counts.get(sev, 0)} |")
33+
lines.append(f"| **TOTAL** | **{total}** |")
34+
lines.append("")
35+
lines.append("> Generated by diff-risk-dashboard CLI")
36+
return "\n".join(lines)

src/diff_risk_dashboard/web.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# FastAPI opcional: no se importa en tiempo de análisis para evitar fallos de mypy/CI.
2+
# Uso local:
3+
# python -m pip install "fastapi>=0.110" "uvicorn[standard]>=0.27"
4+
# python -m diff_risk_dashboard.web
5+
from __future__ import annotations
6+
7+
from typing import Any
8+
9+
from .core import summarize_apv_json
10+
from .report import to_markdown
11+
12+
13+
def create_app() -> Any:
14+
fastapi = __import__("fastapi")
15+
responses = __import__("fastapi.responses", fromlist=["HTMLResponse"])
16+
FastAPI = getattr(fastapi, "FastAPI")
17+
HTMLResponse = getattr(responses, "HTMLResponse")
18+
19+
app = FastAPI(title="Diff Risk Dashboard")
20+
21+
@app.get("/", response_class=HTMLResponse)
22+
def index() -> Any:
23+
return HTMLResponse(
24+
content="""
25+
<!doctype html><html><body>
26+
<h1>Diff Risk Dashboard</h1>
27+
<form action="/summarize" method="post" enctype="multipart/form-data">
28+
<input type="file" name="file" accept=".json"/>
29+
<button type="submit">Summarize</button>
30+
</form>
31+
</body></html>
32+
""",
33+
status_code=200,
34+
)
35+
36+
@app.post("/summarize", response_class=HTMLResponse)
37+
async def summarize(file: Any) -> Any:
38+
data = await file.read()
39+
text = data.decode("utf-8")
40+
summary = summarize_apv_json(text)
41+
md = to_markdown(summary).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
42+
return HTMLResponse(f"<pre>{md}</pre>", status_code=200)
43+
44+
return app
45+
46+
47+
def main() -> None:
48+
uvicorn = __import__("uvicorn")
49+
app = create_app()
50+
uvicorn.run(app, host="127.0.0.1", port=8000)
51+
52+
53+
if __name__ == "__main__":
54+
main()

tests/test_report_md.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from diff_risk_dashboard.report import to_markdown
2+
3+
4+
def test_md_contains_table_and_total():
5+
s = {
6+
"total": 3,
7+
"worst": "HIGH",
8+
"risk": "yellow",
9+
"by_severity": {"HIGH": 1, "MEDIUM": 1, "LOW": 1},
10+
}
11+
md = to_markdown(s)
12+
assert "# Diff Risk Dashboard" in md
13+
assert "| **TOTAL** | **3** |" in md

0 commit comments

Comments
 (0)