Skip to content

Commit 00da086

Browse files
committed
feat(cli): --format {table,json,md} y --output; Markdown report
1 parent 300f419 commit 00da086

File tree

3 files changed

+108
-31
lines changed

3 files changed

+108
-31
lines changed

src/diff_risk_dashboard/cli.py

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,69 @@
33
import argparse
44
import json
55
import sys
6+
from collections.abc import Mapping
67
from pathlib import Path
8+
from typing import Any, Literal, TypedDict
79

8-
from .core import Summary, summarize
9-
10-
11-
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)
10+
from .core import summarize_apv_json
11+
from .report import to_markdown
12+
13+
14+
class Summary(TypedDict):
15+
total: int
16+
worst: str
17+
risk: Literal["red", "yellow", "green"]
18+
by_severity: dict
19+
20+
21+
def _exit_code(risk: str) -> int:
22+
return {"green": 0, "yellow": 1, "red": 2}.get(risk, 0)
23+
24+
25+
def _print_table(summary: Mapping[str, Any]) -> None:
26+
by = summary.get("by_severity", {})
27+
print("Severity\tCount")
28+
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
29+
print(f"{sev}\t{by.get(sev, by.get(sev.lower(), 0))}")
30+
print(f"TOTAL\t{summary.get('total',0)}")
31+
32+
33+
def main() -> int:
34+
p = argparse.ArgumentParser(
35+
prog="diff_risk_dashboard", description="Diff Risk Dashboard (APV JSON -> summary)"
36+
)
37+
p.add_argument("input", help="Path o texto JSON de ai-patch-verifier")
38+
p.add_argument(
39+
"-f", "--format", choices=["table", "json", "md"], default="table", help="Formato de salida"
40+
)
41+
p.add_argument("-o", "--output", default="-", help="Archivo de salida; '-' = stdout")
42+
p.add_argument(
43+
"--no-exit-by-risk", action="store_true", help="No ajustar el exit code por nivel de riesgo"
44+
)
45+
args = p.parse_args()
46+
47+
summary: Summary = summarize_apv_json(args.input) # acepta path o texto
48+
if args.format == "json":
49+
out = json.dumps(summary, indent=2)
50+
elif args.format == "md":
51+
out = to_markdown(summary)
52+
else:
53+
_print_table(summary)
54+
out = None
55+
56+
if out is not None:
57+
if args.output == "-":
58+
print(out)
59+
else:
60+
Path(args.output).write_text(out, encoding="utf-8")
61+
print(f"Wrote {args.output}", file=sys.stderr)
62+
63+
# compat: summary puede traer 'risk' o 'risk_level'
64+
risk = str(summary.get("risk", summary.get("risk_level", "green")))
65+
if not args.no_exit_by_risk:
66+
return _exit_code(risk)
67+
return 0
3868

3969

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

src/diff_risk_dashboard/report.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
counts[sev] = int(by.get(sev, by.get(sev.lower(), 0)) or 0)
15+
return counts
16+
17+
18+
def to_markdown(summary: Mapping[str, Any]) -> str:
19+
counts = _get_counts(summary)
20+
total = int(summary.get("total", 0) or 0)
21+
worst = str(summary.get("worst", "INFO") or "INFO").upper()
22+
risk = str(summary.get("risk", summary.get("risk_level", "green")) or "green").lower()
23+
risk_emoji = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(risk, "🟢")
24+
lines = []
25+
lines.append(f"# Diff Risk Dashboard {risk_emoji} — Worst: **{worst}**")
26+
lines.append("")
27+
lines.append("| Severity | Count |")
28+
lines.append("|---|---:|")
29+
for sev in _SEVERITIES:
30+
lines.append(f"| {sev} | {counts.get(sev, 0)} |")
31+
lines.append(f"| **TOTAL** | **{total}** |")
32+
lines.append("")
33+
lines.append("> Generated by diff-risk-dashboard CLI")
34+
return "\n".join(lines)

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_total():
5+
md = to_markdown(
6+
{
7+
"total": 3,
8+
"worst": "HIGH",
9+
"risk": "yellow",
10+
"by_severity": {"HIGH": 1, "MEDIUM": 1, "LOW": 1},
11+
}
12+
)
13+
assert "| **TOTAL** | **3** |" in md

0 commit comments

Comments
 (0)