739a477d3f
ONE canonical product profile so the Go gap engine and the Python reasoning
engine stop diverging ("SPS mit Remote Access" means the same everywhere).
gap.ProductProfile LEADS; the reasoning ProductProfile becomes an adapter/DTO.
Types + mappers only — no regulation logic, no Go changes, no UI, no new questions.
- CanonicalProductRegulatoryProfile mirrors gap.ProductProfile + the Navigator
gaps the audit found: economic-operator role, radio_module, generates_usage_data,
lifecycle_phase, structured BOM (ProductComponent), safety-vs-security split,
machine-vs-component + a forward-looking EnvironmentalImpact domain (wastewater/
air/chemicals triggers — fields only, no rules yet).
- Mappers: from_product_wizard (lossless), from_company_profile (prefill incl.
the machineBuilder block), to_gap_profile (emits the unchanged gap JSON shape),
to_reasoning_profile (projects into the reasoning ProductProfile; AI stays
delegated to ai-act/ucca). Only profile->reasoning is coupled; reasoning stays
hermetic.
- 10 tests = the 10 acceptance criteria incl. ProductWizard round-trip lossless,
markets no longer forced ['EU'], and canonical->reasoning->discover_scope
proving one semantic profile drives the engine. 33 tests green, mypy clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
60 lines
2.4 KiB
Python
60 lines
2.4 KiB
Python
"""company-profile -> CanonicalProductRegulatoryProfile (prefill, acceptance #2).
|
|
|
|
Pulls master data (industry, business model, size, markets) and the conditional
|
|
`machine_builder` block (camelCase JSONB keys, defined frontend-side) so the user
|
|
re-answers nothing. The machineBuilder block is the richest product/safety/
|
|
connectivity source — note it is industry-gated in the UI, so a prefill may find
|
|
it empty; that is fine (fields stay None = unknown).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
from .canonical import CanonicalProductRegulatoryProfile
|
|
|
|
_EU_MEMBER_HINTS = {"DE", "AT", "FR", "IT", "NL", "LU", "LI", "EU", "EWR", "EEA", "DACH"}
|
|
|
|
|
|
def _markets(p: Dict[str, Any], mb: Dict[str, Any]) -> List[str]:
|
|
out: List[str] = []
|
|
for source in (p.get("target_markets"), mb.get("exportMarkets"), [p.get("primary_jurisdiction")], [p.get("headquarters_country")]):
|
|
for m in source or []:
|
|
if m and m not in out:
|
|
out.append(m)
|
|
return out
|
|
|
|
|
|
def _is_machine(mb: Dict[str, Any]) -> Any:
|
|
types = mb.get("productTypes")
|
|
if types:
|
|
return True
|
|
return None
|
|
|
|
|
|
def from_company_profile(profile: Dict[str, Any]) -> CanonicalProductRegulatoryProfile:
|
|
p = profile
|
|
mb = p.get("machine_builder") or {}
|
|
contains_ai = mb.get("containsAI")
|
|
uses_ai = contains_ai if contains_ai is not None else p.get("uses_ai")
|
|
return CanonicalProductRegulatoryProfile(
|
|
description=mb.get("productDescription") or "",
|
|
sector_industry=p.get("industry") or None,
|
|
b2b_or_b2c=p.get("business_model") or None,
|
|
company_size=p.get("company_size") or None,
|
|
primary_jurisdiction=p.get("primary_jurisdiction") or None,
|
|
markets=_markets(p, mb),
|
|
uses_ai=uses_ai,
|
|
ai_integration_type=list(mb.get("aiIntegrationType") or []),
|
|
human_oversight_level=mb.get("humanOversightLevel") or None,
|
|
has_embedded_software=mb.get("containsFirmware"),
|
|
has_safety_function=mb.get("hasSafetyFunction"),
|
|
safety_function_description=mb.get("safetyFunctionDescription") or None,
|
|
has_remote_access=mb.get("hasRemoteAccess"),
|
|
connected_to_internet=mb.get("isNetworked"),
|
|
has_software_updates=mb.get("hasOTAUpdates"),
|
|
has_risk_assessment=mb.get("hasRiskAssessment"),
|
|
is_machine=_is_machine(mb),
|
|
is_critical_infra_supplier=mb.get("criticalSectorClients"),
|
|
)
|