"""CanonicalProductRegulatoryProfile -> reasoning ProductProfile (adapter/DTO). The reasoning engine stays the consumer, never the source of truth (spec): the canonical leads, this projects it into the Python reasoning ProductProfile so the Reasoning engine and the Go gap engine run off ONE semantic profile (acceptance #10). AI classification is NOT done here — only `uses_ai` is forwarded; risk classification stays delegated to ai-act/ucca (acceptance #3). This is the ONLY one-way coupling profile -> reasoning; reasoning never imports profile, so the reasoning layer stays hermetic. """ from __future__ import annotations from typing import List, Optional from compliance.reasoning.enums import ManufacturerRole, MarketModel, ProductLifecyclePhase from compliance.reasoning.schemas import ProductProfile from .canonical import CanonicalProductRegulatoryProfile, CanonicalProductType _SOFTWARE_TYPES = {CanonicalProductType.SOFTWARE, CanonicalProductType.SAAS, CanonicalProductType.IOT} _SOFTWARE_TECH = {"ai", "api", "database", "encryption", "ota_updates", "cloud", "blockchain"} _EU_HINTS = {"DE", "AT", "FR", "IT", "NL", "LU", "LI", "EU", "EWR", "EEA", "DACH"} _B2X = {"B2B": MarketModel.B2B, "B2C": MarketModel.B2C, "B2B_B2C": MarketModel.BOTH, "B2B2C": MarketModel.BOTH} def _or_none(*values: Optional[bool]) -> Optional[bool]: """True if any value is truthy; None if all are None/absent; else False.""" if any(v is True for v in values): return True if all(v is None for v in values): return None return False def _has_software(c: CanonicalProductRegulatoryProfile) -> Optional[bool]: type_sig = True if c.product_type in _SOFTWARE_TYPES else None tech_sig = True if (set(c.technologies) & _SOFTWARE_TECH) else None return _or_none(c.has_embedded_software, c.has_software_updates, c.uses_ai, type_sig, tech_sig) def _eu_market(markets: List[str]) -> Optional[bool]: if not markets: return None return True if (set(markets) & _EU_HINTS) else False def _has_radio(c: CanonicalProductRegulatoryProfile) -> Optional[bool]: if c.has_radio_module is not None: return c.has_radio_module if any(comp.kind.value == "radio_module" for comp in c.components): return True return None def to_reasoning_profile(c: CanonicalProductRegulatoryProfile) -> ProductProfile: role = ManufacturerRole(c.economic_operator_role.value) if c.economic_operator_role else None phase = ProductLifecyclePhase(c.lifecycle_phase.value) if c.lifecycle_phase else None b2x = _B2X.get(c.b2b_or_b2c) if c.b2b_or_b2c else None is_machine = c.is_machine if c.is_machine is not None else ( True if c.product_type == CanonicalProductType.MACHINERY else None ) generates_data = c.generates_usage_data if c.generates_usage_data is not None else ( True if "telemetry" in c.data_processing else None ) return ProductProfile( product_name=c.name or "Produkt", product_profile_id=c.product_profile_id, manufacturer_role=role, product_type=[c.product_type.value] if c.product_type else [], has_software=_has_software(c), has_embedded_software=c.has_embedded_software, has_remote_access=c.has_remote_access, has_cloud_connection=True if "cloud" in c.technologies else None, has_ai_functionality=c.uses_ai, has_radio_module=_has_radio(c), has_safety_function=c.has_safety_function, generates_usage_data=generates_data, is_machine=is_machine, is_component=c.is_component, is_spare_part=c.is_spare_part, eu_market=_eu_market(c.markets), b2b_or_b2c=b2x, lifecycle_phase=phase, company_size=c.company_size, sector=c.sector_industry, )