"""Regulatory Map renderer (step 4) — pure composition, no new logic. It explains the engine's state, it does not extend it: every statement comes from `resolve_product_scope` (scope verdict) or `derive_obligations` (registry- linked obligations + overlaps). No legal decisions here; obligations are shown ONLY where a registry id is linkable (registry_anchor); the executive summary carries counts but NO percentage. """ from __future__ import annotations from typing import Dict, List from compliance.navigator.engine import navigate from compliance.product_scope.orchestrator import resolve_product_scope from compliance.product_scope.schemas import RegulatoryScopeResult, ScopeStatus from compliance.profile.canonical import CanonicalProductRegulatoryProfile from compliance.profile.to_reasoning import to_reasoning_profile from compliance.reasoning.obligation_engine import derive_obligations from .schemas import ( ApplicableRegulationView, ExcludedRegulationView, ObligationRef, OverlapView, RegulatoryMap, UncertainRegulationView, ) _DOMAIN_BY_REG = { "CRA": "cyber", "MaschinenVO": "machine_safety", "RED": "radio", "DataAct": "data", "EMV": "emv", "NIS2": None, } def _product_summary(c: CanonicalProductRegulatoryProfile) -> str: bits: List[str] = [c.name or "Produkt"] if c.product_type: bits.append("(%s)" % c.product_type.value) sig: List[str] = [] if c.is_machine: sig.append("Maschine") if c.has_remote_access or c.connected_to_internet or "cloud" in c.technologies: sig.append("vernetzt") if c.has_embedded_software: sig.append("Firmware") if c.economic_operator_role: sig.append("Rolle: %s" % c.economic_operator_role.value) if c.markets: sig.append("Märkte: %s" % ", ".join(c.markets)) if sig: bits.append("— " + "; ".join(sig)) return " ".join(bits) def render_regulatory_map(profile: CanonicalProductRegulatoryProfile) -> RegulatoryMap: scope_resp = resolve_product_scope(profile) summary = _product_summary(profile) if scope_resp.status == ScopeStatus.NEEDS_FACTS: return RegulatoryMap( scope_resolved=False, product_summary=summary, executive_summary=( "Regulatorischer Scope noch nicht bestimmbar — zuerst Mindestfakten klären: " + "; ".join(scope_resp.missing_facts[:6]) + "." ), ) scope = scope_resp.regulatory_scope assert scope is not None obligations = derive_obligations(to_reasoning_profile(profile)) nav_questions = navigate(profile).suggested_questions linked_ids = {o.obligation_id for o in obligations.applicable_obligations if o.registry_anchor} by_reg: Dict[str, List[ObligationRef]] = {} shared_ev: Dict[str, List[str]] = {} for o in obligations.applicable_obligations: if not o.registry_anchor: continue by_reg.setdefault(o.source_regulation, []).append( ObligationRef( obligation_id=o.obligation_id, title=o.title, legal_basis_refs=o.legal_basis_refs, authority_level=o.authority_level, ) ) for ev in o.required_evidence: shared_ev.setdefault(ev, []).append(o.obligation_id) applicable_views = [] for r in scope.applicable_regulations: obs = by_reg.get(r.regulation_id, []) applicable_views.append( ApplicableRegulationView( regulation_id=r.regulation_id, name=r.name, why_applicable=r.explanation, triggered_by=r.trigger_facts, obligations=obs, obligations_note="" if obs else "Pflichten für dieses Regelwerk sind noch nicht registry-verlinkt.", confidence=r.confidence, ) ) uncertain_views = [] for u in scope.uncertain_regulations: domain = _DOMAIN_BY_REG.get(u.regulation_id) qrefs = [q.question_id for q in nav_questions if domain and domain in q.regulatory_domains_unblocked] uncertain_views.append( UncertainRegulationView( regulation_id=u.regulation_id, name=u.name, missing_facts=u.missing_facts, question_refs=qrefs ) ) overlap_views = [] for ov in obligations.overlaps: members = [m for m in ov.obligations if m in linked_ids] if len(members) >= 2: overlap_views.append( OverlapView(overlap_group_id=ov.overlap_group_id, shared_obligations=members, explanation=ov.explanation) ) trigger_facts: List[str] = [] for v in applicable_views: for t in v.triggered_by: if t not in trigger_facts: trigger_facts.append(t) return RegulatoryMap( scope_resolved=True, product_summary=summary, trigger_facts=trigger_facts, applicable_regulations=applicable_views, uncertain_regulations=uncertain_views, excluded_regulations=[ ExcludedRegulationView(regulation_id=e.regulation_id, name=e.name, exclusion_reason=e.reason) for e in scope.excluded_regulations ], unsupported_domains=scope.unsupported_domains, overlaps=overlap_views, shared_evidence={ev: ids for ev, ids in shared_ev.items() if len(ids) > 1}, executive_summary=_executive_summary(summary, applicable_views, uncertain_views, scope, len(linked_ids)), ) def _executive_summary( summary: str, applicable: List[ApplicableRegulationView], uncertain: List[UncertainRegulationView], scope: RegulatoryScopeResult, n_obligations: int, ) -> str: appl = ", ".join(v.regulation_id for v in applicable) or "—" unc = ", ".join(v.regulation_id for v in uncertain) or "keine" exc = ", ".join(e.regulation_id for e in scope.excluded_regulations) or "keine" uns = ", ".join(d.domain for d in scope.unsupported_domains) or "keine" return ( "Für %s gelten nach derzeitigem Stand wahrscheinlich: %s. Unsicher (fehlende Fakten): %s. " "Ausgeschlossen: %s. Nicht abgedeckt (Regelkorpus fehlt): %s. Ermittelt: %d registry-verlinkte " "Pflichten. Es wurden keine weiteren Regelwerke im aktuellen Korpus identifiziert." % (summary, appl, unc, exc, uns, n_obligations) )