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>
51 lines
2.1 KiB
Python
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"),
|
|
)
|