"""P3 — Compliance-Advisor-Proof: obligation-basierte Antwort als vollstaendige BEGRUENDUNGSKETTE aus der Registry (NICHT RAG-Text, KEIN LLM): Rechtsgrundlage -> Obligation -> Procedure -> Controls -> Evidence -> Antwort. Deterministisch + zitierfaehig. Der Unterschied zu RAG: RAG beantwortet — BreakPilot begruendet UND operationalisiert. python3 scripts/obligation_discovery/advisor_proof.py --registry obligations/cra.json \ --procedures obligations/cra_procedures.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 fuer 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("--procedures", required=True) ap.add_argument("--topic", default="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")) procs = json.load(open(a.procedures, encoding="utf-8"))["procedures"] obls = [o for o in reg["obligations"] if a.topic in o.get("family", "") or a.topic in o["id"]] ids = {o["id"] for o in obls} by_obl: dict[str, list] = {} for p in procs: for oid in p.get("fulfills_obligations", []): by_obl.setdefault(oid, []).append(p) pflicht = [o for o in obls if o["tier"] == "LEGAL_MINIMUM" and applies(o, a.has_digital_elements)[0]] best = [o for o in obls if o["tier"] != "LEGAL_MINIMUM"] print(f"FRAGE: {a.question}") print(f"\nANTWORT: {'JA' if pflicht and a.has_digital_elements else 'NUR WENN CRA-anwendbar'} — " f"sofern das Produkt unter den CRA faellt (product with digital elements, Art. 3).") print("\n══ BEGRUENDUNGSKETTE (Recht → Obligation → Procedure → Controls → Evidence) ══") req_evidence: list[str] = [] for o in pflicht: lb = "; ".join(f"{b.get('source','')} {b.get('anchor','')}".strip() for b in o.get("legal_basis", [])) print(f"\n● PFLICHT: {o['id']} — {o.get('description','')[:80]}") print(f" Rechtsgrundlage: {lb or '—'}") ps = by_obl.get(o["id"], []) for p in ps: print(f" Procedure (wie umgesetzt): {p['procedure_id']} — Schritte: {len(p.get('steps',[]))}") print(f" Controls (Pruefung): {' · '.join(p.get('controls', []))[:96]}") print(f" Nachweis: {' · '.join(p.get('evidence', []))}") req_evidence += p.get("evidence", []) if not ps: print(" Procedure: (noch keine modelliert)") print("\n── REQUIRED EVIDENCE (aggregiert, womit wird es nachgewiesen) ──") print(" " + " · ".join(dict.fromkeys(req_evidence)) if req_evidence 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','')[:64]} | Guidance: {gb or '—'}") print("\n── BEZIEHUNG (warum es zaehlt) ──") 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','')[:64]}") pend = sum(1 for o in pflicht if o.get("citation_status") == "pending_span_anchor") print(f"\n── CITATION ──\n {pend}/{len(pflicht)} Pflichten: pending_span_anchor " f"(Textstellen-Anker folgen mit dem zitierfaehigen Re-Ingest)") print("\n(RAG beantwortet — BreakPilot begruendet UND operationalisiert.)") if __name__ == "__main__": main()