Skip to content

Commit a143d01

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

File tree

3 files changed

+104
-45
lines changed

3 files changed

+104
-45
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ name: CI / build
22
on:
33
pull_request:
44
push:
5-
branches: [main]
5+
branches: [ main ]
66
jobs:
77
python:
8+
name: python (${{ matrix.python-version }})
89
runs-on: ubuntu-latest
910
strategy:
10-
matrix:
11-
python-version: ["3.11","3.12"]
11+
matrix: { python-version: ["3.11","3.12"] }
1212
steps:
1313
- uses: actions/checkout@v5
1414
- uses: actions/setup-python@v5
15-
with:
16-
python-version: ${{ matrix.python-version }}
15+
with: { python-version: ${{ matrix.python-version }} }
1716
- run: python -m pip install -U pip
18-
- run: pip install poetry
19-
- run: poetry install --no-interaction
20-
- run: poetry run ruff check .
21-
- run: poetry run black --check .
22-
- run: PYTHONPATH=src poetry run pytest -q
23-
- run: poetry run mypy src
17+
- run: pip install pytest mypy ruff black
18+
- run: ruff check .
19+
- run: black --check .
20+
- run: pytest -q
21+
- name: mypy (solo 3.12)
22+
if: matrix.python-version == '3.12'
23+
run: mypy src

src/diff_risk_dashboard/cli.py

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,63 @@
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)
38-
39-
40-
if __name__ == "__main__":
41-
sys.exit(main())
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+
summary: Summary = summarize_apv_json(args.input)
47+
out: str | None = None
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+
if out is not None:
55+
if args.output == "-":
56+
print(out)
57+
else:
58+
Path(args.output).write_text(out, encoding="utf-8")
59+
print(f"Wrote {args.output}", file=sys.stderr)
60+
risk = str(summary.get("risk", summary.get("risk_level", "green")))
61+
return 0 if args.no_exit_by_risk else _exit_code(risk)
62+
63+
64+
if __name__ == "__main__": # pragma: no cover
65+
raise SystemExit(main())

src/diff_risk_dashboard/report.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
emoji = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(risk, "🟢")
24+
lines = [
25+
f"# Diff Risk Dashboard {emoji} — Worst: **{worst}**",
26+
"",
27+
"| Severity | Count |",
28+
"|---|---:|",
29+
]
30+
for sev in _SEVERITIES:
31+
lines.append(f"| {sev} | {counts.get(sev,0)} |")
32+
lines.append(f"| **TOTAL** | **{total}** |")
33+
lines.append("")
34+
lines.append("> Generated by diff-risk-dashboard CLI")
35+
return "\n".join(lines)

0 commit comments

Comments
 (0)