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>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""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"),
|
||||
)
|
||||
Reference in New Issue
Block a user