diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5604e4..b7b74a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,22 +2,22 @@ name: CI / build on: pull_request: push: - branches: [main] + branches: [ main ] jobs: python: + name: python (${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: - matrix: - python-version: ["3.11","3.12"] + matrix: { python-version: ["3.11","3.12"] } steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} + with: { python-version: ${{ matrix.python-version }} } - run: python -m pip install -U pip - - run: pip install poetry - - run: poetry install --no-interaction - - run: poetry run ruff check . - - run: poetry run black --check . - - run: PYTHONPATH=src poetry run pytest -q - - run: poetry run mypy src + - run: pip install pytest mypy ruff black + - run: ruff check . + - run: black --check . + - run: pytest -q + - name: mypy (solo 3.12) + if: matrix.python-version == '3.12' + run: mypy src diff --git a/src/diff_risk_dashboard/cli.py b/src/diff_risk_dashboard/cli.py index 0085bf1..323264e 100644 --- a/src/diff_risk_dashboard/cli.py +++ b/src/diff_risk_dashboard/cli.py @@ -3,39 +3,59 @@ import argparse import json import sys +from collections.abc import Mapping from pathlib import Path - -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) - - -if __name__ == "__main__": - sys.exit(main()) +from typing import Any + +from .core import summarize_apv_json +from .report import to_markdown + + +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", {}) or {} + 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: Mapping[str, Any] = summarize_apv_json(args.input) + out: str | None = None + if args.format == "json": + out = json.dumps(summary, indent=2) + elif args.format == "md": + out = to_markdown(summary) + else: + _print_table(summary) + + 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) + + risk = str(summary.get("risk", summary.get("risk_level", "green"))) + return 0 if args.no_exit_by_risk else _exit_code(risk) + + +if __name__ == "__main__": # pragma: no cover + 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..c536ff5 --- /dev/null +++ b/src/diff_risk_dashboard/report.py @@ -0,0 +1,33 @@ +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() + emoji = {"red": "🔴", "yellow": "🟡", "green": "🟢"}.get(risk, "🟢") + lines = [ + f"# Diff Risk Dashboard {emoji} — Worst: **{worst}**", + "", + "| Severity | Count |", + "|---|---:|", + ] + for sev in _SEVERITIES: + lines.append(f"| {sev} | {counts.get(sev, 0)} |") + lines += [f"| **TOTAL** | **{total}** |", "", "> Generated by diff-risk-dashboard CLI"] + return "\n".join(lines)