diff --git a/scripts/obligation_discovery/advisor_proof.py b/scripts/obligation_discovery/advisor_proof.py new file mode 100644 index 00000000..650d74c1 --- /dev/null +++ b/scripts/obligation_discovery/advisor_proof.py @@ -0,0 +1,77 @@ +"""P3 — Compliance-Advisor-Proof: obligation-basierte Antwort aus der Registry (NICHT RAG-Text, +KEIN LLM). Demonstriert den besseren Antworttyp: PFLICHT (LEGAL_MINIMUM + Rechtsgrundlage + +Applicability) / BEST PRACTICE (guidance_basis) / NACHWEISE (evidence_facets + member controls) ++ Beziehungen. Deterministisch + zitierfähig — das ist der Produktnutzen der Registry. + + python3 scripts/obligation_discovery/advisor_proof.py --registry obligations/cra.json \ + --topic sbom --has-digital-elements +""" +from __future__ import annotations + +import argparse +import json + + +def applies(obl: dict, has_digital: bool) -> tuple[bool, str]: + a = obl.get("applicability", "universal") + if a == "universal": + return True, "" + if a.startswith("domain:products_with_digital_elements"): + return (has_digital, "nur für Produkte mit digitalen Elementen (CRA Art. 3)") + if a.startswith("domain:"): + return True, a.split(":", 1)[1] + if a.startswith("conditional:"): + return True, f"bedingt: {a.split(':',1)[1]}" + return True, "" + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--registry", required=True) + ap.add_argument("--topic", default="sbom", help="Family/Keyword der Pflicht (z.B. sbom)") + ap.add_argument("--has-digital-elements", action="store_true") + ap.add_argument("--question", default="Muss ich als Maschinenbauer eine SBOM bereitstellen?") + a = ap.parse_args() + reg = json.load(open(a.registry, encoding="utf-8")) + obls = [o for o in reg["obligations"] + if a.topic in o.get("family", "") or a.topic in o["id"] or a.topic in o.get("name", "").lower()] + ids = {o["id"] for o in obls} + pflicht = [o for o in obls if o["tier"] == "LEGAL_MINIMUM"] + best = [o for o in obls if o["tier"] in ("BEST_PRACTICE", "IMPLEMENTATION_GUIDANCE")] + + print(f"FRAGE: {a.question}") + print(f"KONTEXT: Hersteller; digitale Elemente = {a.has_digital_elements} → CRA-Geltungsbereich") + applicable_pflicht = [(o, applies(o, a.has_digital_elements)) for o in pflicht] + any_pflicht = any(ok for _, (ok, _) in applicable_pflicht) + n_pflicht = sum(1 for _, (ok, _) in applicable_pflicht if ok) + verdict = "JA" if any_pflicht and a.has_digital_elements else "NUR WENN CRA-anwendbar" + print(f"\nANTWORT: {verdict} — unter dem CRA bestehen {n_pflicht} gesetzliche " + f"Mindestpflichten zum Thema {a.topic}.\n") + + print("── PFLICHT (LEGAL_MINIMUM, Wortlaut im CRA) ──") + for o, (ok, note) in applicable_pflicht: + if not ok: + continue + lb = "; ".join(f"{b.get('source','')} {b.get('anchor','')}".strip() for b in o.get("legal_basis", [])) + print(f" • {o['id']} — {o.get('description','')[:84]}") + print(f" Rechtsgrundlage: {lb or '—'}" + (f" | {note}" if note else "")) + + print("\n── BEST PRACTICE (anerkannte Umsetzung, KEINE CRA-Wortlautpflicht) ──") + for o in best: + gb = "; ".join(b.get("source", "") for b in o.get("guidance_basis", [])) + print(f" • {o['id']} — {o.get('description','')[:74]} | Guidance: {gb or '—'}") + + print("\n── NACHWEISE (Evidence) ──") + for o in pflicht + best: + f = o.get("evidence_facets", {}) + facets = "/".join(k for k in ("governance", "capability", "evidence") if f.get(k)) or "—" + print(f" • {o['id']}: {o.get('member_count','?')} Controls · Facetten {facets}") + + print("\n── BEZIEHUNGEN (warum es zählt) ──") + for r in reg.get("relationships", []): + if r.get("from") in ids and r.get("to") not in ids: + print(f" • {r['from']} --{r['type']}--> {r['to']}: {r.get('note','')[:70]}") + + +if __name__ == "__main__": + main()