"""Deklaration-vs-Bibliothek-Diff. Regroupt die `analyze_cookies`-Findings PRO COOKIE zu Feld-Diffs (deklariert → Library) — nur für die Library-getroffene Teilmenge, denn nur dort gibt es eine Ground-Truth. Plus ein ehrlicher Funnel (gesamt → geprüft → abweichend), damit nie der Eindruck entsteht, ALLE Cookies seien geprüft (passt zur BreakPilot-Tonalität: nicht-getroffene Cookies = nicht prüfbar, kein Pass, kein Fail). Single source of truth bleibt `analyze_cookies` (Erkennung); dieses Modul ist reine Präsentations-Aggregation und damit isoliert testbar. """ from __future__ import annotations # Finding-Typ → Feld-Label. Nur Typen mit echtem Library-Soll ('expected'). # vague_duration/missing_retention/missing_opt_out haben KEINEN Library-Vergleich # und third_country/eu_alternative sind Hinweise → bewusst NICHT im Diff. _FIELD = { "tracker_as_necessary": "Kategorie", "excessive_lifetime": "Laufzeit", "missing_purpose": "Zweck", } _SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2} def build_declaration_diff(analysis: dict) -> dict: """Aus dem `analyze_cookies`-Ergebnis die Diff-Sicht + Funnel bauen.""" findings = analysis.get("findings") or [] summary = analysis.get("summary") or {} rows: dict[tuple, dict] = {} for f in findings: field = _FIELD.get(f.get("type")) if not field: continue key = (f.get("vendor") or "", f.get("cookie") or "") row = rows.get(key) if row is None: row = { "cookie": f.get("cookie") or "", "vendor": f.get("vendor") or "", "diffs": [], "measures": [], "severity": "LOW", } rows[key] = row row["diffs"].append({ "field": field, "declared": str(f.get("declared") or "—"), "expected": str(f.get("expected") or f.get("library_purpose") or "—"), "severe": f.get("severity") == "HIGH", }) rem = f.get("remediation") if rem and rem not in row["measures"]: row["measures"].append(rem) if _SEV_ORDER.get(f.get("severity"), 3) < _SEV_ORDER.get(row["severity"], 3): row["severity"] = f.get("severity") or "LOW" out_rows = sorted(rows.values(), key=lambda r: _SEV_ORDER.get(r["severity"], 3)) total = int(summary.get("checked") or 0) # alle Cookies checked = int(summary.get("in_library") or 0) # davon mit Library-Treffer return { "coverage": { "total": total, "checked": checked, "discrepant": len(out_rows), }, "rows": out_rows, }