Files
breakpilot-compliance/backend-compliance/compliance/profile/from_company_profile.py
T
Benjamin Admin 739a477d3f feat(profile): CanonicalProductRegulatoryProfile convergence layer (types + mappers + tests)
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>
2026-06-26 09:52:46 +02:00

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"),
)