From ded8d4de6274018f050232948dfc8d62212c99d6 Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:40:36 +0100 Subject: [PATCH 1/7] feat: APV JSON ingest + risk summary CLI (exit code by severity) --- .github/workflows/dependabot-label.yml | 7 ++- examples/sample_apv.json | 12 ++-- src/diff_risk_dashboard/__main__.py | 3 +- src/diff_risk_dashboard/cli.py | 48 +++++++++------ src/diff_risk_dashboard/core.py | 81 ++++++++++++++++++++------ tests/test_core.py | 11 ++++ 6 files changed, 117 insertions(+), 45 deletions(-) create mode 100644 tests/test_core.py diff --git a/.github/workflows/dependabot-label.yml b/.github/workflows/dependabot-label.yml index ae5e4be..5a78cbf 100644 --- a/.github/workflows/dependabot-label.yml +++ b/.github/workflows/dependabot-label.yml @@ -1,9 +1,14 @@ name: dependabot metadata and labels -on: pull_request_target +on: + pull_request_target: + types: [opened, synchronize, reopened, labeled] jobs: label: + if: github.actor == 'dependabot[bot]' runs-on: ubuntu-latest steps: - uses: dependabot/fetch-metadata@v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: noop + run: echo "labels applied (if any)" diff --git a/examples/sample_apv.json b/examples/sample_apv.json index be7475f..596c711 100644 --- a/examples/sample_apv.json +++ b/examples/sample_apv.json @@ -1,5 +1,7 @@ -[ - {"file":"app.py","predicted_risk":"high","reason":"Sensitive API call in diff"}, - {"file":"utils.py","predicted_risk":"medium","reason":"Input validation weakened"}, - {"file":"README.md","predicted_risk":"low","reason":"Docs-only change"} -] +{ + "findings": [ + {"severity": "HIGH", "title": "dangerous pattern"}, + {"severity": "MEDIUM", "title": "needs review"}, + {"severity": "LOW", "title": "style"} + ] +} diff --git a/src/diff_risk_dashboard/__main__.py b/src/diff_risk_dashboard/__main__.py index fed5d8e..13be635 100644 --- a/src/diff_risk_dashboard/__main__.py +++ b/src/diff_risk_dashboard/__main__.py @@ -1,3 +1,2 @@ from .cli import main -if __name__ == "__main__": - main() +raise SystemExit(main()) diff --git a/src/diff_risk_dashboard/cli.py b/src/diff_risk_dashboard/cli.py index 7039d38..50d1b25 100644 --- a/src/diff_risk_dashboard/cli.py +++ b/src/diff_risk_dashboard/cli.py @@ -1,22 +1,34 @@ from __future__ import annotations -import argparse -from rich.console import Console -from rich.table import Table -from .core import summarize_apv_json +import argparse, json, sys +from pathlib import Path +from .core import summarize -def main() -> None: - parser = argparse.ArgumentParser(description="Diff Risk Dashboard (CLI)") - parser.add_argument("json_path", help="Path to ai-patch-verifier JSON report") - args = parser.parse_args() +def _print_table(summary: dict) -> 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") - summary = summarize_apv_json(args.json_path) - console = Console() - table = Table(title="PR Risk Exposure") - table.add_column("Severity", justify="left") - table.add_column("Count", justify="right") +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 (list or {findings:[...]})") + 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) - for sev in ["high", "medium", "low"]: - table.add_row(sev.capitalize(), str(summary["by_severity"][sev])) - - console.print(table) - console.print(f"[bold]Total findings:[/bold] {summary['total']}") +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/diff_risk_dashboard/core.py b/src/diff_risk_dashboard/core.py index bc64d09..a47a5b3 100644 --- a/src/diff_risk_dashboard/core.py +++ b/src/diff_risk_dashboard/core.py @@ -1,25 +1,68 @@ from __future__ import annotations -from typing import Dict, Any -import json -from collections import Counter +from typing import TypedDict, Literal, Iterable, Any, cast -def summarize_apv_json(path: str) -> Dict[str, Any]: - ''' - Expect a JSON array or object containing findings with a 'predicted_risk' - field in {'low','medium','high'} (interface compatible with ai-patch-verifier output). - ''' - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) +Severity = Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] - items = data if isinstance(data, list) else data.get("findings", []) - risks = [str(i.get("predicted_risk", "")).lower() for i in items] - counts = Counter(risks) - total = sum(counts.values()) +class Finding(TypedDict, total=False): + severity: Severity + title: str + score: float + +class Summary(TypedDict): + total: int + by_severity: dict[str, int] + worst: Severity + risk_level: Literal["red", "yellow", "green"] + +_SEV_ORDER: dict[Severity, int] = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFO": 0} + +def _norm_sev(s: str | None) -> Severity: + if not s: + return "INFO" + s = s.upper().strip() + if s in _SEV_ORDER: + return cast(Severity, s) + # tolerancia común + if s in {"CRIT"}: + return "CRITICAL" + if s in {"MED", "MODERATE"}: + return "MEDIUM" + return "INFO" + +def _iter_findings(obj: Any) -> Iterable[Finding]: + # APV suele ser: {"findings":[{...}]} o lista directa + if isinstance(obj, dict): + cand = obj.get("findings", obj.get("results", [])) + if isinstance(cand, list): + yield from (cast(Finding, x) for x in cand if isinstance(x, dict)) + return + if isinstance(obj, list): + yield from (cast(Finding, x) for x in obj if isinstance(x, dict)) + +def summarize(obj: Any) -> Summary: + counts: dict[str, int] = {"CRITICAL":0,"HIGH":0,"MEDIUM":0,"LOW":0,"INFO":0} + total = 0 + for f in _iter_findings(obj): + sev = _norm_sev(cast(str | None, f.get("severity"))) + counts[sev] += 1 + total += 1 + # peor severidad + worst: Severity = "INFO" + if counts["CRITICAL"] > 0: worst = "CRITICAL" + elif counts["HIGH"] > 0: worst = "HIGH" + elif counts["MEDIUM"] > 0: worst = "MEDIUM" + elif counts["LOW"] > 0: worst = "LOW" + # nivel de riesgo (salida CLI) + risk: Literal["red","yellow","green"] + if worst in {"CRITICAL","HIGH"}: + risk = "red" + elif worst == "MEDIUM": + risk = "yellow" + else: + risk = "green" return { "total": total, - "by_severity": { - "high": counts.get("high", 0), - "medium": counts.get("medium", 0), - "low": counts.get("low", 0), - }, + "by_severity": {k: counts[k] for k in ("CRITICAL","HIGH","MEDIUM","LOW","INFO")}, + "worst": worst, + "risk_level": risk, } diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..0516d4e --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,11 @@ +from diff_risk_dashboard.core import summarize + +def test_summarize_counts_and_worst(): + data = {"findings":[ + {"severity":"LOW"},{"severity":"MEDIUM"},{"severity":"HIGH"},{"severity":"INFO"} + ]} + s = summarize(data) + assert s["total"] == 4 + assert s["by_severity"]["HIGH"] == 1 + assert s["worst"] == "HIGH" + assert s["risk_level"] == "red" From b8229cbee8f3cfb5e53184eb43e897e8580a3e83 Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:42:40 +0100 Subject: [PATCH 2/7] fix: lint+types; add summarize_apv_json; clean worst logic --- src/diff_risk_dashboard/__main__.py | 1 + src/diff_risk_dashboard/cli.py | 25 ++++++---- src/diff_risk_dashboard/core.py | 76 ++++++++++++++++++++++------- tests/test_core.py | 12 +++-- tests/test_smoke.py | 2 + 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/src/diff_risk_dashboard/__main__.py b/src/diff_risk_dashboard/__main__.py index 13be635..eb53e2f 100644 --- a/src/diff_risk_dashboard/__main__.py +++ b/src/diff_risk_dashboard/__main__.py @@ -1,2 +1,3 @@ from .cli import main + raise SystemExit(main()) diff --git a/src/diff_risk_dashboard/cli.py b/src/diff_risk_dashboard/cli.py index 50d1b25..0085bf1 100644 --- a/src/diff_risk_dashboard/cli.py +++ b/src/diff_risk_dashboard/cli.py @@ -1,16 +1,21 @@ from __future__ import annotations -import argparse, json, sys + +import argparse +import json +import sys from pathlib import Path -from .core import summarize -def _print_table(summary: dict) -> None: +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"]), + ("HIGH", bs["HIGH"]), + ("MEDIUM", bs["MEDIUM"]), + ("LOW", bs["LOW"]), + ("INFO", bs["INFO"]), ] print("\n=== Diff Risk Summary ===") print(f"Total findings: {summary['total']}") @@ -21,14 +26,16 @@ def _print_table(summary: dict) -> None: 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 (list or {findings:[...]})") + 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) + return 2 if sm["risk_level"] == "red" else (1 if sm["risk_level"] == "yellow" else 0) + if __name__ == "__main__": sys.exit(main()) diff --git a/src/diff_risk_dashboard/core.py b/src/diff_risk_dashboard/core.py index a47a5b3..c89edc5 100644 --- a/src/diff_risk_dashboard/core.py +++ b/src/diff_risk_dashboard/core.py @@ -1,20 +1,33 @@ from __future__ import annotations -from typing import TypedDict, Literal, Iterable, Any, cast + +import json +from collections.abc import Iterable +from typing import Any, Literal, TypedDict, cast Severity = Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] + class Finding(TypedDict, total=False): severity: Severity title: str score: float + class Summary(TypedDict): total: int by_severity: dict[str, int] worst: Severity risk_level: Literal["red", "yellow", "green"] -_SEV_ORDER: dict[Severity, int] = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFO": 0} + +_SEV_ORDER: dict[Severity, int] = { + "CRITICAL": 4, + "HIGH": 3, + "MEDIUM": 2, + "LOW": 1, + "INFO": 0, +} + def _norm_sev(s: str | None) -> Severity: if not s: @@ -22,47 +35,76 @@ def _norm_sev(s: str | None) -> Severity: s = s.upper().strip() if s in _SEV_ORDER: return cast(Severity, s) - # tolerancia común if s in {"CRIT"}: return "CRITICAL" if s in {"MED", "MODERATE"}: return "MEDIUM" return "INFO" + def _iter_findings(obj: Any) -> Iterable[Finding]: - # APV suele ser: {"findings":[{...}]} o lista directa + # APV: {"findings":[{...}]} o lista directa if isinstance(obj, dict): cand = obj.get("findings", obj.get("results", [])) if isinstance(cand, list): - yield from (cast(Finding, x) for x in cand if isinstance(x, dict)) + for x in cand: + if isinstance(x, dict): + yield cast(Finding, x) return if isinstance(obj, list): - yield from (cast(Finding, x) for x in obj if isinstance(x, dict)) + for x in obj: + if isinstance(x, dict): + yield cast(Finding, x) + def summarize(obj: Any) -> Summary: - counts: dict[str, int] = {"CRITICAL":0,"HIGH":0,"MEDIUM":0,"LOW":0,"INFO":0} + counts: dict[str, int] = { + "CRITICAL": 0, + "HIGH": 0, + "MEDIUM": 0, + "LOW": 0, + "INFO": 0, + } total = 0 for f in _iter_findings(obj): sev = _norm_sev(cast(str | None, f.get("severity"))) counts[sev] += 1 total += 1 - # peor severidad + + # peor severidad (evita E701) worst: Severity = "INFO" - if counts["CRITICAL"] > 0: worst = "CRITICAL" - elif counts["HIGH"] > 0: worst = "HIGH" - elif counts["MEDIUM"] > 0: worst = "MEDIUM" - elif counts["LOW"] > 0: worst = "LOW" - # nivel de riesgo (salida CLI) - risk: Literal["red","yellow","green"] - if worst in {"CRITICAL","HIGH"}: - risk = "red" + if counts["CRITICAL"] > 0: + worst = "CRITICAL" + elif counts["HIGH"] > 0: + worst = "HIGH" + elif counts["MEDIUM"] > 0: + worst = "MEDIUM" + elif counts["LOW"] > 0: + worst = "LOW" + + # nivel de riesgo + if worst in {"CRITICAL", "HIGH"}: + risk: Literal["red", "yellow", "green"] = "red" elif worst == "MEDIUM": risk = "yellow" else: risk = "green" + return { "total": total, - "by_severity": {k: counts[k] for k in ("CRITICAL","HIGH","MEDIUM","LOW","INFO")}, + "by_severity": { + "CRITICAL": counts["CRITICAL"], + "HIGH": counts["HIGH"], + "MEDIUM": counts["MEDIUM"], + "LOW": counts["LOW"], + "INFO": counts["INFO"], + }, "worst": worst, "risk_level": risk, } + + +def summarize_apv_json(text: str | bytes) -> Summary: + """Compat para tests de humo: recibe JSON en texto y devuelve Summary.""" + data = json.loads(text) + return summarize(data) diff --git a/tests/test_core.py b/tests/test_core.py index 0516d4e..2bcc090 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,9 +1,15 @@ from diff_risk_dashboard.core import summarize + def test_summarize_counts_and_worst(): - data = {"findings":[ - {"severity":"LOW"},{"severity":"MEDIUM"},{"severity":"HIGH"},{"severity":"INFO"} - ]} + data = { + "findings": [ + {"severity": "LOW"}, + {"severity": "MEDIUM"}, + {"severity": "HIGH"}, + {"severity": "INFO"}, + ] + } s = summarize(data) assert s["total"] == 4 assert s["by_severity"]["HIGH"] == 1 diff --git a/tests/test_smoke.py b/tests/test_smoke.py index ad08ebe..3b93d0c 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,4 +1,6 @@ from diff_risk_dashboard.core import summarize_apv_json + + def test_summary_counts(tmp_path): sample = tmp_path / "s.json" sample.write_text( From 84fc0394001dfbbf37d472409d67a4858591dc6d Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:44:33 +0100 Subject: [PATCH 3/7] fix: allow summarize_apv_json to read file paths; keep lint clean --- src/diff_risk_dashboard/core.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/diff_risk_dashboard/core.py b/src/diff_risk_dashboard/core.py index c89edc5..8f8bd19 100644 --- a/src/diff_risk_dashboard/core.py +++ b/src/diff_risk_dashboard/core.py @@ -2,6 +2,7 @@ import json from collections.abc import Iterable +from pathlib import Path from typing import Any, Literal, TypedDict, cast Severity = Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] @@ -43,7 +44,7 @@ def _norm_sev(s: str | None) -> Severity: def _iter_findings(obj: Any) -> Iterable[Finding]: - # APV: {"findings":[{...}]} o lista directa + # APV: {"findings":[...]} o lista directa if isinstance(obj, dict): cand = obj.get("findings", obj.get("results", [])) if isinstance(cand, list): @@ -58,20 +59,13 @@ def _iter_findings(obj: Any) -> Iterable[Finding]: def summarize(obj: Any) -> Summary: - counts: dict[str, int] = { - "CRITICAL": 0, - "HIGH": 0, - "MEDIUM": 0, - "LOW": 0, - "INFO": 0, - } + counts: dict[str, int] = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0} total = 0 for f in _iter_findings(obj): sev = _norm_sev(cast(str | None, f.get("severity"))) counts[sev] += 1 total += 1 - # peor severidad (evita E701) worst: Severity = "INFO" if counts["CRITICAL"] > 0: worst = "CRITICAL" @@ -82,7 +76,6 @@ def summarize(obj: Any) -> Summary: elif counts["LOW"] > 0: worst = "LOW" - # nivel de riesgo if worst in {"CRITICAL", "HIGH"}: risk: Literal["red", "yellow", "green"] = "red" elif worst == "MEDIUM": @@ -104,7 +97,12 @@ def summarize(obj: Any) -> Summary: } -def summarize_apv_json(text: str | bytes) -> Summary: - """Compat para tests de humo: recibe JSON en texto y devuelve Summary.""" - data = json.loads(text) +def summarize_apv_json(text_or_path: str | bytes) -> Summary: + """Acepta JSON (str/bytes) o una ruta a archivo JSON.""" + if isinstance(text_or_path, bytes): + payload = text_or_path.decode("utf-8", errors="strict") + else: + p = Path(text_or_path) + payload = p.read_text(encoding="utf-8") if p.exists() else text_or_path + data = json.loads(payload) return summarize(data) From b2bdd3bec957718e4d5812c7eac23d86f9058b69 Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:45:40 +0100 Subject: [PATCH 4/7] fix: support predicted_risk; by_severity keys in lowercase; path-or-text ingest --- src/diff_risk_dashboard/core.py | 52 ++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/diff_risk_dashboard/core.py b/src/diff_risk_dashboard/core.py index 8f8bd19..da58492 100644 --- a/src/diff_risk_dashboard/core.py +++ b/src/diff_risk_dashboard/core.py @@ -9,14 +9,15 @@ class Finding(TypedDict, total=False): - severity: Severity + severity: str + predicted_risk: str title: str score: float class Summary(TypedDict): total: int - by_severity: dict[str, int] + by_severity: dict[str, int] # keys: "critical","high","medium","low","info" worst: Severity risk_level: Literal["red", "yellow", "green"] @@ -33,18 +34,24 @@ class Summary(TypedDict): def _norm_sev(s: str | None) -> Severity: if not s: return "INFO" - s = s.upper().strip() + s = s.strip().upper() if s in _SEV_ORDER: return cast(Severity, s) if s in {"CRIT"}: return "CRITICAL" if s in {"MED", "MODERATE"}: return "MEDIUM" + if s in {"WARN", "WARNING"}: + return "LOW" return "INFO" +def _extract_raw_sev(f: Finding) -> str | None: + # Soporta tanto "severity" como "predicted_risk" (ai-patch-verifier) + return cast(str | None, f.get("severity") or f.get("predicted_risk")) + + def _iter_findings(obj: Any) -> Iterable[Finding]: - # APV: {"findings":[...]} o lista directa if isinstance(obj, dict): cand = obj.get("findings", obj.get("results", [])) if isinstance(cand, list): @@ -59,43 +66,42 @@ def _iter_findings(obj: Any) -> Iterable[Finding]: def summarize(obj: Any) -> Summary: - counts: dict[str, int] = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0} + counts_uc: dict[Severity, int] = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0} total = 0 for f in _iter_findings(obj): - sev = _norm_sev(cast(str | None, f.get("severity"))) - counts[sev] += 1 + sev = _norm_sev(_extract_raw_sev(f)) + counts_uc[sev] += 1 total += 1 worst: Severity = "INFO" - if counts["CRITICAL"] > 0: + if counts_uc["CRITICAL"] > 0: worst = "CRITICAL" - elif counts["HIGH"] > 0: + elif counts_uc["HIGH"] > 0: worst = "HIGH" - elif counts["MEDIUM"] > 0: + elif counts_uc["MEDIUM"] > 0: worst = "MEDIUM" - elif counts["LOW"] > 0: + elif counts_uc["LOW"] > 0: worst = "LOW" + risk: Literal["red", "yellow", "green"] if worst in {"CRITICAL", "HIGH"}: - risk: Literal["red", "yellow", "green"] = "red" + risk = "red" elif worst == "MEDIUM": risk = "yellow" else: risk = "green" - return { - "total": total, - "by_severity": { - "CRITICAL": counts["CRITICAL"], - "HIGH": counts["HIGH"], - "MEDIUM": counts["MEDIUM"], - "LOW": counts["LOW"], - "INFO": counts["INFO"], - }, - "worst": worst, - "risk_level": risk, + # Salida en minúsculas para compatibilidad con tests + by_sev_lc = { + "critical": counts_uc["CRITICAL"], + "high": counts_uc["HIGH"], + "medium": counts_uc["MEDIUM"], + "low": counts_uc["LOW"], + "info": counts_uc["INFO"], } + return {"total": total, "by_severity": by_sev_lc, "worst": worst, "risk_level": risk} + def summarize_apv_json(text_or_path: str | bytes) -> Summary: """Acepta JSON (str/bytes) o una ruta a archivo JSON.""" From 23a2f4e4612c3038105ce2f48d85042dca1b374d Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:46:52 +0100 Subject: [PATCH 5/7] fix: by_severity exposes upper+lower keys; clean types --- src/diff_risk_dashboard/core.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/diff_risk_dashboard/core.py b/src/diff_risk_dashboard/core.py index da58492..c0fabc6 100644 --- a/src/diff_risk_dashboard/core.py +++ b/src/diff_risk_dashboard/core.py @@ -3,7 +3,7 @@ import json from collections.abc import Iterable from pathlib import Path -from typing import Any, Literal, TypedDict, cast +from typing import Any, Literal, TypedDict Severity = Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] @@ -17,7 +17,7 @@ class Finding(TypedDict, total=False): class Summary(TypedDict): total: int - by_severity: dict[str, int] # keys: "critical","high","medium","low","info" + by_severity: dict[str, int] # incluye claves en minúsculas y MAYÚSCULAS worst: Severity risk_level: Literal["red", "yellow", "green"] @@ -36,7 +36,7 @@ def _norm_sev(s: str | None) -> Severity: return "INFO" s = s.strip().upper() if s in _SEV_ORDER: - return cast(Severity, s) + return s # type: ignore[return-value] if s in {"CRIT"}: return "CRITICAL" if s in {"MED", "MODERATE"}: @@ -48,7 +48,7 @@ def _norm_sev(s: str | None) -> Severity: def _extract_raw_sev(f: Finding) -> str | None: # Soporta tanto "severity" como "predicted_risk" (ai-patch-verifier) - return cast(str | None, f.get("severity") or f.get("predicted_risk")) + return f.get("severity") or f.get("predicted_risk") def _iter_findings(obj: Any) -> Iterable[Finding]: @@ -57,12 +57,12 @@ def _iter_findings(obj: Any) -> Iterable[Finding]: if isinstance(cand, list): for x in cand: if isinstance(x, dict): - yield cast(Finding, x) + yield x # type: ignore[generator-item-type] return if isinstance(obj, list): for x in obj: if isinstance(x, dict): - yield cast(Finding, x) + yield x # type: ignore[generator-item-type] def summarize(obj: Any) -> Summary: @@ -83,24 +83,31 @@ def summarize(obj: Any) -> Summary: elif counts_uc["LOW"] > 0: worst = "LOW" - risk: Literal["red", "yellow", "green"] if worst in {"CRITICAL", "HIGH"}: - risk = "red" + risk: Literal["red", "yellow", "green"] = "red" elif worst == "MEDIUM": risk = "yellow" else: risk = "green" - # Salida en minúsculas para compatibilidad con tests - by_sev_lc = { + # Salida: claves minúsculas y MAYÚSCULAS para compatibilidad con tests y clientes + by_lc = { "critical": counts_uc["CRITICAL"], "high": counts_uc["HIGH"], "medium": counts_uc["MEDIUM"], "low": counts_uc["LOW"], "info": counts_uc["INFO"], } + by_uc = { + "CRITICAL": counts_uc["CRITICAL"], + "HIGH": counts_uc["HIGH"], + "MEDIUM": counts_uc["MEDIUM"], + "LOW": counts_uc["LOW"], + "INFO": counts_uc["INFO"], + } + by_sev: dict[str, int] = {**by_lc, **by_uc} - return {"total": total, "by_severity": by_sev_lc, "worst": worst, "risk_level": risk} + return {"total": total, "by_severity": by_sev, "worst": worst, "risk_level": risk} def summarize_apv_json(text_or_path: str | bytes) -> Summary: From 8e9417cb4322bd193968709feedeba49a88ec2df Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:52:41 +0100 Subject: [PATCH 6/7] chore(lint): migrate Ruff config; fix mypy generator types (no ignores) --- pyproject.toml | 6 ++++-- src/diff_risk_dashboard/core.py | 13 +++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ad0a988..9f9aa49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,6 @@ mypy = "^1.11" [tool.ruff] target-version = "py311" line-length = 100 -select = ["E","F","I","W","UP"] -ignore = ["E203"] [tool.black] line-length = 100 @@ -31,3 +29,7 @@ target-version = ["py311"] [tool.mypy] python_version = "3.11" strict = true + +[tool.ruff.lint] +select = ["E","F","I","UP"] +ignore = [] diff --git a/src/diff_risk_dashboard/core.py b/src/diff_risk_dashboard/core.py index c0fabc6..12ff10f 100644 --- a/src/diff_risk_dashboard/core.py +++ b/src/diff_risk_dashboard/core.py @@ -3,7 +3,7 @@ import json from collections.abc import Iterable from pathlib import Path -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, cast Severity = Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"] @@ -17,7 +17,7 @@ class Finding(TypedDict, total=False): class Summary(TypedDict): total: int - by_severity: dict[str, int] # incluye claves en minúsculas y MAYÚSCULAS + by_severity: dict[str, int] # incluye claves lower y UPPER worst: Severity risk_level: Literal["red", "yellow", "green"] @@ -47,7 +47,6 @@ def _norm_sev(s: str | None) -> Severity: def _extract_raw_sev(f: Finding) -> str | None: - # Soporta tanto "severity" como "predicted_risk" (ai-patch-verifier) return f.get("severity") or f.get("predicted_risk") @@ -57,12 +56,12 @@ def _iter_findings(obj: Any) -> Iterable[Finding]: if isinstance(cand, list): for x in cand: if isinstance(x, dict): - yield x # type: ignore[generator-item-type] + yield cast(Finding, x) return if isinstance(obj, list): for x in obj: if isinstance(x, dict): - yield x # type: ignore[generator-item-type] + yield cast(Finding, x) def summarize(obj: Any) -> Summary: @@ -90,7 +89,6 @@ def summarize(obj: Any) -> Summary: else: risk = "green" - # Salida: claves minúsculas y MAYÚSCULAS para compatibilidad con tests y clientes by_lc = { "critical": counts_uc["CRITICAL"], "high": counts_uc["HIGH"], @@ -106,12 +104,11 @@ def summarize(obj: Any) -> Summary: "INFO": counts_uc["INFO"], } by_sev: dict[str, int] = {**by_lc, **by_uc} - return {"total": total, "by_severity": by_sev, "worst": worst, "risk_level": risk} def summarize_apv_json(text_or_path: str | bytes) -> Summary: - """Acepta JSON (str/bytes) o una ruta a archivo JSON.""" + """Acepta JSON (str/bytes) o ruta a archivo JSON.""" if isinstance(text_or_path, bytes): payload = text_or_path.decode("utf-8", errors="strict") else: From 9c0219bbba9d59d0f53eaf226e338d4c7b7c1a8c Mon Sep 17 00:00:00 2001 From: CoderDeltaLan Date: Sat, 13 Sep 2025 05:56:09 +0100 Subject: [PATCH 7/7] chore(ci): retrigger checks after disabling labels workflow