diff --git a/src/diff_risk_dashboard/cli.py b/src/diff_risk_dashboard/cli.py index 0085bf1..dffdb55 100644 --- a/src/diff_risk_dashboard/cli.py +++ b/src/diff_risk_dashboard/cli.py @@ -3,39 +3,69 @@ import argparse import json import sys +from collections.abc import Mapping from pathlib import Path +from typing import Any, Literal, TypedDict -from .core import Summary, summarize - - -def _print_table(summary: Summary) -> None: - bs = summary["by_severity"] - rows = [ - ("CRITICAL", bs["CRITICAL"]), - ("HIGH", bs["HIGH"]), - ("MEDIUM", bs["MEDIUM"]), - ("LOW", bs["LOW"]), - ("INFO", bs["INFO"]), - ] - print("\n=== Diff Risk Summary ===") - print(f"Total findings: {summary['total']}") - print("Severity counts:") - w = max(len(r[0]) for r in rows) - for name, cnt in rows: - print(f" {name:<{w}} : {cnt}") - print(f"Worst severity : {summary['worst']}") - print(f"Risk level : {summary['risk_level']}\n") - - -def main(argv: list[str] | None = None) -> int: - p = argparse.ArgumentParser(description="Diff Risk Dashboard (APV JSON -> summary)") - p.add_argument("apv_json", help="Path to ai-patch-verifier JSON") - args = p.parse_args(argv) - data = json.loads(Path(args.apv_json).read_text(encoding="utf-8")) - sm = summarize(data) - _print_table(sm) - return 2 if sm["risk_level"] == "red" else (1 if sm["risk_level"] == "yellow" else 0) +from .core import summarize_apv_json +from .report import to_markdown + + +class Summary(TypedDict): + total: int + worst: str + risk: Literal["red", "yellow", "green"] + by_severity: dict + + +def _exit_code(risk: str) -> int: + return {"green": 0, "yellow": 1, "red": 2}.get(risk, 0) + + +def _print_table(summary: Mapping[str, Any]) -> None: + by = summary.get("by_severity", {}) + print("Severity\tCount") + for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]: + print(f"{sev}\t{by.get(sev, by.get(sev.lower(), 0))}") + print(f"TOTAL\t{summary.get('total',0)}") + + +def main() -> int: + p = argparse.ArgumentParser( + prog="diff_risk_dashboard", description="Diff Risk Dashboard (APV JSON -> summary)" + ) + p.add_argument("input", help="Path o texto JSON de ai-patch-verifier") + p.add_argument( + "-f", "--format", choices=["table", "json", "md"], default="table", help="Formato de salida" + ) + p.add_argument("-o", "--output", default="-", help="Archivo de salida; '-' = stdout") + p.add_argument( + "--no-exit-by-risk", action="store_true", help="No ajustar el exit code por nivel de riesgo" + ) + args = p.parse_args() + + summary: Summary = summarize_apv_json(args.input) # acepta path o texto + if args.format == "json": + out = json.dumps(summary, indent=2) + elif args.format == "md": + out = to_markdown(summary) + else: + _print_table(summary) + out = None + + if out is not None: + if args.output == "-": + print(out) + else: + Path(args.output).write_text(out, encoding="utf-8") + print(f"Wrote {args.output}", file=sys.stderr) + + # compat: summary puede traer 'risk' o 'risk_level' + risk = str(summary.get("risk", summary.get("risk_level", "green"))) + if not args.no_exit_by_risk: + return _exit_code(risk) + return 0 if __name__ == "__main__": - sys.exit(main()) + raise SystemExit(main()) diff --git a/src/diff_risk_dashboard/report.py b/src/diff_risk_dashboard/report.py new file mode 100644 index 0000000..9aad4c7 --- /dev/null +++ b/src/diff_risk_dashboard/report.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] + + +def _get_counts(summary: Mapping[str, Any]) -> dict[str, int]: + counts: dict[str, int] = {} + by = summary.get("by_severity", {}) or {} + if isinstance(by, Mapping): + for sev in _SEVERITIES: + counts[sev] = int(by.get(sev, by.get(sev.lower(), 0)) or 0) + return counts + + +def to_markdown(summary: Mapping[str, Any]) -> str: + counts = _get_counts(summary) + total = int(summary.get("total", 0) or 0) + worst = str(summary.get("worst", "INFO") or "INFO").upper() + risk = str(summary.get("risk", summary.get("risk_level", "green")) or "green").lower() + risk_emoji = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(risk, "🟢") + lines = [] + lines.append(f"# Diff Risk Dashboard {risk_emoji} — Worst: **{worst}**") + lines.append("") + lines.append("| Severity | Count |") + lines.append("|---|---:|") + for sev in _SEVERITIES: + lines.append(f"| {sev} | {counts.get(sev, 0)} |") + lines.append(f"| **TOTAL** | **{total}** |") + lines.append("") + lines.append("> Generated by diff-risk-dashboard CLI") + return "\n".join(lines) diff --git a/tests/test_report_md.py b/tests/test_report_md.py new file mode 100644 index 0000000..1b3d748 --- /dev/null +++ b/tests/test_report_md.py @@ -0,0 +1,13 @@ +from diff_risk_dashboard.report import to_markdown + + +def test_md_total(): + md = to_markdown( + { + "total": 3, + "worst": "HIGH", + "risk": "yellow", + "by_severity": {"HIGH": 1, "MEDIUM": 1, "LOW": 1}, + } + ) + assert "| **TOTAL** | **3** |" in md