"""Applicable-obligation engine (spec Modus 2). Maps a product profile (optionally a precomputed scope) to the concrete legal obligations, the overlaps between them, and which evidence types satisfy more than one obligation at once (the core USP, spec §16). """ from __future__ import annotations from typing import Dict, List, Optional from .predicates import evaluate, true_leaves from .rules_obligations import ALL_OBLIGATIONS from .rules_overlaps import OVERLAP_GROUPS from .rules_regulations import FIELD_LABELS from .rules_types import ObligationRule from .schemas import ( ApplicableObligation, ObligationOverlap, ObligationsResponse, ProductProfile, RegulatoryScope, ) from .scope_engine import discover_scope def _applicable_regulation_ids(profile: ProductProfile, scope: Optional[RegulatoryScope]) -> List[str]: if scope is None: scope = discover_scope(profile) return [r.regulation_id for r in scope.applicable_regulations] def _applies_because(rule: ObligationRule, profile: ProductProfile) -> List[str]: labels: List[str] = [] for leaf in true_leaves(rule.applies_if, profile): label = FIELD_LABELS.get(leaf[0]) if label and label not in labels: labels.append(label) if not labels: labels.append("%s ist für dieses Produkt anwendbar." % rule.source_regulation) return labels def _role_ok(rule: ObligationRule, profile: ProductProfile) -> bool: role = profile.manufacturer_role if role is None: return True # unknown role -> do not exclude return role.value in rule.applies_to_role def derive_obligations( profile: ProductProfile, scope: Optional[RegulatoryScope] = None ) -> ObligationsResponse: active_regs = set(_applicable_regulation_ids(profile, scope)) response = ObligationsResponse() applied_ids: List[str] = [] for rule in ALL_OBLIGATIONS: if rule.source_regulation not in active_regs: continue if rule.applies_unless is not None and evaluate(rule.applies_unless, profile) is True: continue verdict = evaluate(rule.applies_if, profile) if verdict is not True or not _role_ok(rule, profile): if verdict is False: response.excluded_obligations.append(rule.obligation_id) continue applied_ids.append(rule.obligation_id) response.applicable_obligations.append( ApplicableObligation( obligation_id=rule.obligation_id, title=rule.title, source_regulation=rule.source_regulation, legal_basis_refs=rule.legal_basis_refs, obligation_text=rule.obligation_text, authority_level=rule.authority_level, applies_because=_applies_because(rule, profile), applies_to_role=rule.applies_to_role, lifecycle_phase=rule.lifecycle_phase, overlap_group_id=rule.overlap_group_id, required_evidence=rule.required_evidence, confidence=rule.base_confidence, registry_anchor=rule.registry_anchor, proposed=rule.proposed, ) ) response.overlaps = _overlaps(applied_ids) response.evidence_for_multiple = _evidence_for_multiple(response.applicable_obligations) return response def _overlaps(applied_ids: List[str]) -> List[ObligationOverlap]: applied = set(applied_ids) out: List[ObligationOverlap] = [] for group in OVERLAP_GROUPS: present = [m for m in group.members if m in applied] if len(present) >= 2: out.append( ObligationOverlap( overlap_group_id=group.overlap_group_id, obligations=present, overlap_type=group.overlap_type, canonical_obligation_id=group.canonical_obligation_id, explanation=group.explanation, ) ) return out def _evidence_for_multiple(obligations: List[ApplicableObligation]) -> Dict[str, List[str]]: by_evidence: Dict[str, List[str]] = {} for ob in obligations: for ev in ob.required_evidence: by_evidence.setdefault(ev, []).append(ob.obligation_id) return {ev: ids for ev, ids in by_evidence.items() if len(ids) > 1}