"""Scope-Audit: Adressaten-Prüfung der Obligation-Registry (Review-Stage-Werkzeug). Prinzip (User 2026-07-01): **Adressat der Norm ⊥ Handlungspflicht des Herstellers.** Die Registry modelliert Hersteller-Pflichten. `scope`-Achse (Attribut-Enum, KEINE neue Objektklasse — freeze-safe): - in_scope : Norm adressiert direkt den Hersteller (default). - out_of_scope : reines Staats-/Durchsetzungs-/Institutions-Recht (Adressat != Hersteller, KEINE mittelbare Herstellerpflicht) — z.B. Sanktionen, Marktüberwachung. Präzedenz CSIRT/ENISA (CRA-Vuln-Cut). Aus join_keys gefiltert. - derived_obligation : Norm adressiert primär eine andere Rolle, erzeugt aber MITTELBAR eine Hersteller-Handlungspflicht — bleibt im Set (Wissen nicht verwerfen), ggf. `scope_split_candidate` (Aufspaltung Normadressat ↔ abgeleitete Pflicht). False-Positive-Guard: Melde-AN-Behörde-Pflichten (applicability=domain:products…) sind IN-SCOPE (Adressat = Hersteller, Behörde nur Empfänger) — der Audit key't auf `applicability`, nicht auf Behörden-Nennung im Namen. Deterministisch, kein LLM. Audit FLAGGT; Reklassifizierung = User/Owner. """ from __future__ import annotations import glob import json NON_MANUFACTURER_DOMAINS = { "domain:authority", "domain:notified_body", "domain:market_surveillance", "domain:member_state", "domain:commission", } def main() -> None: classified = [] # bereits per scope entschieden unclassified = [] # nicht-Hersteller-Adressat OHNE scope -> Review-Kandidat total = 0 for f in sorted(glob.glob("obligations/cra*.json")): d = json.load(open(f, encoding="utf-8")) for o in d.get("obligations", []): total += 1 appl = (o.get("applicability") or "").strip() scope = o.get("scope") rec = { "file": f.split("/")[-1], "id": o.get("id") or o.get("obligation_id"), "name": o.get("name"), "tier": o.get("tier"), "applicability": appl, "scope": scope, "scope_reason": o.get("scope_reason"), } if scope in ("out_of_scope", "derived_obligation"): if o.get("scope_split_candidate"): rec["scope_split_note"] = o.get("scope_split_note") classified.append(rec) elif appl in NON_MANUFACTURER_DOMAINS: rec["reason"] = "Adressat Behörde/notifizierte Stelle/Mitgliedstaat, nicht klassifiziert" rec["recommendation"] = "out_of_scope (reine Durchsetzung) ODER derived_obligation (mittelbare Herstellerpflicht)" unclassified.append(rec) out = { "audit": "obligation scope audit (Adressat: Hersteller vs Behörde/notified_body)", "principle": "Adressat der Norm != Handlungspflicht des Herstellers; scope-Achse in_scope/out_of_scope/derived_obligation", "false_positive_guard": "Melde-AN-Behörde-Pflichten (applicability=domain:products…) bleiben IN-SCOPE", "obligations_scanned": total, "classified": classified, "unclassified_candidates": unclassified, "decision_owner": "User/Registry-Owner — Audit FLAGGT nur; für jeden künftigen Cut mitlaufen lassen", } json.dump(out, open("obligations/scope_audit_findings.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1) print(f"gescannt {total} | klassifiziert {len(classified)} | unklassifizierte Kandidaten {len(unclassified)}") for r in classified: extra = " [SPLIT-KANDIDAT]" if r.get("scope_split_note") else "" print(f" [{r['scope']}] {r['id']} appl={r['applicability']}{extra}") for r in unclassified: print(f" [UNKLASSIFIZIERT] {r['id']} appl={r['applicability']}") if __name__ == "__main__": main()