Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 63 additions & 28 deletions src/diff_risk_dashboard/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,72 @@
import sys
from pathlib import Path

from .core import Summary, summarize
from .core import Summary as CoreSummary
from .core import summarize_apv_json
from .report import to_markdown

Summary = CoreSummary


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)
by = summary["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['total']}")


def _exit_code(risk: str) -> int:
return {"green": 0, "yellow": 1, "red": 2}.get(risk, 0)


def main() -> int:
p = argparse.ArgumentParser(prog="diff-risk")
p.add_argument("input", help="Path o texto JSON de APV (ai-patch-verifier)")
p.add_argument(
"-f",
"--format",
choices=["table", "json", "md"],
default="table",
help="Formato de salida (por defecto: table)",
)
p.add_argument(
"-o", "--output", default="-", help="Archivo de salida; '-' = stdout (por defecto)"
)
p.add_argument(
"--no-exit-by-risk",
dest="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) # path o texto

# compat: aceptar 'risk' o 'risk_level'
risk = summary.get("risk", summary.get("risk_level", "green")) # type: ignore[assignment]
if "risk" not in summary:
summary["risk"] = risk # type: ignore[index]

if args.format == "json":
out = json.dumps(summary, indent=2)
elif args.format == "md":
out = to_markdown(summary)
else:
_print_table(summary)
out = ""

if args.format in {"json", "md"}:
if args.output == "-":
print(out)
else:
Path(args.output).write_text(out, encoding="utf-8")
print(f"Wrote {args.output}", file=sys.stderr)

if not args.no_exit_by_risk:
return _exit_code(risk)
return 0


if __name__ == "__main__":
sys.exit(main())
raise SystemExit(main())
36 changes: 36 additions & 0 deletions src/diff_risk_dashboard/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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:
# Acepta claves en minúsculas o MAYÚSCULAS
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", "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)
49 changes: 49 additions & 0 deletions src/diff_risk_dashboard/web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

from typing import Any

# Importes perezosos para no requerir fastapi/uvicorn en CI
from .core import summarize_apv_json
from .report import to_markdown


def create_app() -> Any:
fastapi = __import__("fastapi")
responses = __import__("fastapi.responses", fromlist=["HTMLResponse"])
FastAPI = getattr(fastapi, "FastAPI")
HTMLResponse = getattr(responses, "HTMLResponse")
app: Any = FastAPI(title="Diff Risk Dashboard")

@app.get("/", response_class=HTMLResponse) # type: ignore[misc]
def index() -> Any:
return HTMLResponse(
content="""
<!doctype html><html><body>
<h1>Diff Risk Dashboard</h1>
<form action="/summarize" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".json"/>
<button type="submit">Summarize</button>
</form>
</body></html>
""",
status_code=200,
)

@app.post("/summarize", response_class=HTMLResponse) # type: ignore[misc]
async def summarize(file: Any) -> Any:
data = await file.read()
summary = summarize_apv_json(data.decode("utf-8"))
md = to_markdown(summary).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
return HTMLResponse(f"<pre>{md}</pre>", status_code=200)

return app


def main() -> None:
uvicorn = __import__("uvicorn")
app = create_app()
uvicorn.run(app, host="127.0.0.1", port=8000)


if __name__ == "__main__":
main()
13 changes: 13 additions & 0 deletions tests/test_report_md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from diff_risk_dashboard.report import to_markdown


def test_md_contains_table_and_total():
s = {
"total": 3,
"worst": "HIGH",
"risk": "yellow",
"by_severity": {"HIGH": 1, "MEDIUM": 1, "LOW": 1},
}
md = to_markdown(s)
assert "# Diff Risk Dashboard" in md
assert "| **TOTAL** | **3** |" in md
Loading