Files
breakpilot-compliance/backend-compliance/compliance/profile/from_product_wizard.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

51 lines
2.1 KiB
Python

"""ProductWizard payload -> CanonicalProductRegulatoryProfile (lossless).
The gap-analysis ProductWizard POSTs exactly the gap.ProductProfile JSON shape
(see admin-compliance/.../ProductWizard.tsx handleSubmit). This mapper copies
every gap field verbatim so that `to_gap_profile(from_product_wizard(p))`
reproduces the gap subset of `p` byte-for-byte (acceptance #1). New Navigator
fields the wizard does not ask stay None.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from .canonical import CanonicalProductRegulatoryProfile, CanonicalProductType
def _as_product_type(value: Any) -> Optional[CanonicalProductType]:
try:
return CanonicalProductType(value)
except ValueError:
return None
def from_product_wizard(payload: Dict[str, Any]) -> CanonicalProductRegulatoryProfile:
g = payload.get
return CanonicalProductRegulatoryProfile(
name=g("name", ""),
description=g("description", ""),
product_type=_as_product_type(g("product_type")),
technologies=list(g("technologies") or []),
data_processing=list(g("data_processing") or []),
markets=list(g("markets") or []),
existing_certifications=list(g("existing_certifications") or []),
applied_norms=list(g("applied_norms") or []),
connected_to_internet=g("connected_to_internet"),
has_software_updates=g("has_software_updates"),
uses_ai=g("uses_ai"),
processes_personal_data=g("processes_personal_data"),
is_critical_infra_supplier=g("is_critical_infra_supplier"),
has_risk_assessment=g("has_risk_assessment"),
has_technical_file=g("has_technical_file"),
has_operating_manual=g("has_operating_manual"),
has_sbom=g("has_sbom"),
has_vuln_management=g("has_vuln_management"),
has_update_mechanism=g("has_update_mechanism"),
has_incident_response=g("has_incident_response"),
has_supply_chain_mgmt=g("has_supply_chain_mgmt"),
ce_marking_since=g("ce_marking_since"),
product_age=g("product_age"),
)