"""Scope discovery engine (spec Modus 1). Answers "which regulations apply to my product?" — and, crucially, never says "X applies" without the triggers, and never hides a missing fact behind a false verdict. Pure rule evaluation, deterministic. """ from __future__ import annotations from typing import List, Optional from .enums import ApplicabilityStatus, Confidence from .predicates import Condition, evaluate, true_leaves, unknown_fields from .rules_regulations import REGULATION_RULES, FIELD_LABELS, RegulationRule from .schemas import ( ApplicableRegulation, ExcludedRegulation, ProductProfile, RegulatoryScope, UncertainRegulation, ) _DOWNGRADE = {Confidence.HIGH: Confidence.MEDIUM, Confidence.MEDIUM: Confidence.LOW, Confidence.LOW: Confidence.LOW} def _fields_in(condition: Optional[Condition]) -> List[str]: if condition is None: return [] if isinstance(condition, tuple): return [condition[0]] out: List[str] = [] for c in condition.get("all") or condition.get("any") or []: out.extend(_fields_in(c)) return out def _trigger_facts(rule: RegulationRule, profile: ProductProfile) -> List[str]: labels: List[str] = [] for leaf in true_leaves(rule.trigger, profile): label = FIELD_LABELS.get(leaf[0]) if label and label not in labels: labels.append(label) return labels def _missing_prompts(rule: RegulationRule, profile: ProductProfile) -> List[str]: fields = list(dict.fromkeys(rule.required_facts + _fields_in(rule.trigger))) unknown = unknown_fields(fields, profile) prompts: List[str] = [] for f in unknown: prompt = rule.fact_prompts.get(f) if prompt and prompt not in prompts: prompts.append(prompt) return prompts def discover_scope(profile: ProductProfile) -> RegulatoryScope: scope = RegulatoryScope(product_profile_id=profile.product_profile_id) for rule in REGULATION_RULES: role_value = profile.manufacturer_role.value if profile.manufacturer_role is not None else None role_excluded = role_value is not None and role_value in rule.excludable_roles trig = evaluate(rule.trigger, profile) missing = _missing_prompts(rule, profile) if role_excluded: scope.excluded_regulations.append( ExcludedRegulation( regulation_id=rule.regulation_id, name=rule.name, reason="Rolle '%s' ist von dieser Regulierung nicht unmittelbar adressiert." % role_value, ) ) continue if trig is True: conf = Confidence.MEDIUM if rule.inferred else rule.confidence_when_applicable status = ( ApplicabilityStatus.PARTIALLY_APPLICABLE if rule.inferred else ApplicabilityStatus.APPLICABLE ) unresolved = unknown_fields(rule.required_facts, profile) if unresolved: conf = _DOWNGRADE[conf] for f in unresolved: prompt = rule.fact_prompts.get(f) if prompt and prompt not in scope.missing_facts: scope.missing_facts.append(prompt) scope.applicable_regulations.append( ApplicableRegulation( regulation_id=rule.regulation_id, name=rule.name, applicability_status=status, trigger_facts=_trigger_facts(rule, profile), legal_basis_refs=rule.legal_basis_refs, confidence=conf, explanation=rule.summary, ) ) elif trig is None: scope.uncertain_regulations.append( UncertainRegulation( regulation_id=rule.regulation_id, name=rule.name, missing_facts=missing, explanation=rule.summary, ) ) for m in missing: if m not in scope.missing_facts: scope.missing_facts.append(m) else: # trig is False -> definitively excluded by a known fact scope.excluded_regulations.append( ExcludedRegulation( regulation_id=rule.regulation_id, name=rule.name, reason="Auslösende Voraussetzungen sind anhand der bekannten Fakten nicht erfüllt.", ) ) scope.confidence = _overall_confidence(scope) scope.reasoning_summary = _summary(scope) return scope def _overall_confidence(scope: RegulatoryScope) -> Confidence: if scope.applicable_regulations and not scope.uncertain_regulations and not scope.missing_facts: return Confidence.HIGH if scope.applicable_regulations: return Confidence.MEDIUM return Confidence.LOW def _summary(scope: RegulatoryScope) -> str: applicable = ", ".join(r.regulation_id for r in scope.applicable_regulations) or "—" uncertain = ", ".join(r.regulation_id for r in scope.uncertain_regulations) or "—" return "Wahrscheinlich anwendbar: %s. Unsicher (fehlende Fakten): %s." % (applicable, uncertain)