"""SVG generators, card builders, and shared helpers for STEM BIO-AI HTML report.""" from __future__ import annotations import math from typing import Any _C = { "navy": "#0D1F3C", # deeper, truer navy (AIRI-aligned) "teal": "#1A6FA8", # richer mid-blue "green": "#1A7A47", # botanical green "amber": "#C07B10", # warm amber "red": "#9B2335", # AIRI crimson "purple": "#5B3D8A", # deeper purple "slate": "#2D4A6E", # slate-blue "lgray": "#F8FAFC", # cooler background "mgray": "#E4EBF5", # cooler divider "dgray": "#5A6B80", # bluer muted text "white": "#FFFFFF", } _TIER_COLOR = {"T0": _C["red"], "T1": _C["red"], "T2": _C["amber"], "T3": _C["teal"], "T4": _C["green"]} _STATUS_COLOR = {"PASS": _C["green"], "WARN": _C["amber"], "FAIL": _C["red"]} # AIRI 7-domain palette (matches MIT AI Risk Navigator) _AIRI_DOMAIN_COLORS = { 1: "#B5272A", # Discrimination & Toxicity 2: "#1E3A6E", # Privacy & Security 3: "#C07020", # Misinformation 4: "#6B3FA0", # Malicious Actors & Misuse 5: "#1B7A4E", # Human-Computer Interaction 6: "#7A6520", # Socioeconomic & Environmental 7: "#1A5568", # AI System Safety, Failures & Limitations } _AIRI_DOMAIN_NAMES = { 1: "Discrimination & Toxicity", 2: "Privacy & Security", 3: "Misinformation", 4: "Malicious Actors & Misuse", 5: "Human-Computer Interaction", 6: "Socioeconomic & Environmental", 7: "AI System Safety, Failures & Limitations", } _STAGE_TIPS = [ "Stage 1: Scans README/docs for hype language, overconfidence claims, and missing clinical disclaimers.", "Stage 2R: Cross-checks API names advertised in README against actual code exports and __all__ lists.", "Stage 3: AST scan for clinical safety patterns: PII validators, exception handling, dependency pinning.", "Stage 4: Evaluates reproducibility artifacts: seed fixation, dataset pinning, environment lock files.", ] # Human-readable labels for regulatory requirement IDs (shared by MD and HTML renderers). # Stored as PLAIN text — each renderer escapes via its own xt()/_xt() helper. REQ_LABELS: dict[str, str] = { "EU_AI_ACT_ARTICLE_10": "EU AI Act Art. 10 — Data Governance", "EU_AI_ACT_ARTICLE_12": "EU AI Act Art. 12 — Record Keeping", "EU_AI_ACT_ARTICLE_13": "EU AI Act Art. 13 — Transparency", "EU_AI_ACT_ARTICLE_15": "EU AI Act Art. 15 — Robustness", "ICH_M15_SECTION_2_1_2_CONTEXT_OF_USE": "ICH M15 §2.1.2 — Context of Use", "ICH_M15_SECTION_3_VERIFICATION": "ICH M15 §3 — Verification", "ICH_M15_SECTION_3_VALIDATION": "ICH M15 §3 — Validation & Applicability", "ICH_M15_SECTION_4_1_MAP": "ICH M15 §4.1 — Model Analysis Plan", "ICH_M15_SECTION_4_3_CODE_SUBMISSION": "ICH M15 §4.3 — Code & Data Submission", "IMDRF_CLINICAL_CONTEXT_BOUNDARY_SIGNAL": "IMDRF SaMD — Clinical Context Boundary", "IMDRF_ANALYTICAL_VALIDATION_SIGNAL": "IMDRF SaMD — Analytical Validation", "COMPLIANCE_CLAIM_GROUNDING_SIGNAL": "Compliance Claim Grounding", } # HTML badge for each traceability status. REQ_STATUS_BADGE: dict[str, str] = { "signal_only": f'⚠ Signal only', "partially_aligned": f'~ Partially aligned', "aligned": f'✓ Aligned', "not_detected": f'✗ Not detected', } def tier_color(tier: str) -> str: for k, v in _TIER_COLOR.items(): if k in tier: return v return _C["dgray"] def status_color(s: str) -> str: return _STATUS_COLOR.get(str(s).upper(), _C["dgray"]) def xt(t: str) -> str: return str(t).replace("&", "&").replace("<", "<").replace(">", ">") def tip_icon(tip: str) -> str: safe = tip.replace('"', """) return f'' # ── SVG generators ───────────────────────────────────────────────────────────── def svg_gauge(score: int, color: str, size: int = 160) -> str: cx, cy, r = size // 2, size // 2, size // 2 - 16 a = math.radians(180 - min(max(score, 0), 100) * 1.8) ex, ey = round(cx + r * math.cos(a), 2), round(cy - r * math.sin(a), 2) lx, ly, rx, ry, sw = cx - r, cy, cx + r, cy, 14 h = size // 2 + 28 return ( f'' ) def svg_donut(pct: float, color: str, size: int = 90) -> str: cx = cy = size // 2 r = size // 2 - 10 circ = 2 * math.pi * r dash, gap = round(pct * circ, 2), round((1 - pct) * circ, 2) return ( f'' ) def svg_hbar(value: int, max_val: int = 100, color: str = _C["teal"], height: int = 20, width: int = 270) -> str: fw = round(value / max_val * width) if max_val else 0 return ( f'' ) # ── Card builders ────────────────────────────────────────────────────────────── def integrity_card(key: str, info: dict[str, Any]) -> str: status = str(info.get("status", "PASS")) color = status_color(status) evl = info.get("evidence", []) preview = xt(str(evl[0])[:80]) if evl else "No evidence captured" label_map = { "C1_hardcoded_credentials": "C1 Hardcoded Credentials", "C2_dependency_pinning": "C2 Dependency Pinning", "C3_dead_or_deprecated_patient_adjacent_paths": "C3 Deprecated Patient Paths", "C4_exception_handling_clinical_adjacent_paths": "C4 Fail-Open Exceptions", "C5_compliance_boundary_integrity": "C5 Compliance Boundary Integrity", "C6_mock_auth_or_fail_open_boundary": "C6 Mock Auth / Fail-Open Boundary", "CC1_clinical_zero_default": "CC1 Clinical Zero Default", "CC2_api_contract": "CC2 API Contract", "CC3_shallow_validator": "CC3 Shallow Validator", } label = label_map.get(key, key.replace("_", " ").title()) items = "".join(f'