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,41 @@
|
||||
"""CanonicalProductRegulatoryProfile -> gap.ProductProfile JSON shape.
|
||||
|
||||
Emits exactly the keys the Go gap engine already consumes (gap/models.go json
|
||||
tags), so the gap engine runs UNCHANGED — the canonical is a superset and gap is
|
||||
its lossless projection. Canonical-only fields (role/radio/components/...) are
|
||||
intentionally not emitted here; they reach the reasoning side via to_reasoning.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from .canonical import CanonicalProductRegulatoryProfile
|
||||
|
||||
|
||||
def to_gap_profile(c: CanonicalProductRegulatoryProfile) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": c.name,
|
||||
"description": c.description,
|
||||
"product_type": c.product_type.value if c.product_type else "",
|
||||
"technologies": list(c.technologies),
|
||||
"data_processing": list(c.data_processing),
|
||||
"markets": list(c.markets),
|
||||
"existing_certifications": list(c.existing_certifications),
|
||||
"applied_norms": list(c.applied_norms),
|
||||
"connected_to_internet": bool(c.connected_to_internet),
|
||||
"has_software_updates": bool(c.has_software_updates),
|
||||
"uses_ai": bool(c.uses_ai),
|
||||
"processes_personal_data": bool(c.processes_personal_data),
|
||||
"is_critical_infra_supplier": bool(c.is_critical_infra_supplier),
|
||||
"has_risk_assessment": bool(c.has_risk_assessment),
|
||||
"has_technical_file": bool(c.has_technical_file),
|
||||
"has_operating_manual": bool(c.has_operating_manual),
|
||||
"has_sbom": bool(c.has_sbom),
|
||||
"has_vuln_management": bool(c.has_vuln_management),
|
||||
"has_update_mechanism": bool(c.has_update_mechanism),
|
||||
"has_incident_response": bool(c.has_incident_response),
|
||||
"has_supply_chain_mgmt": bool(c.has_supply_chain_mgmt),
|
||||
"ce_marking_since": c.ce_marking_since if c.ce_marking_since is not None else "",
|
||||
"product_age": c.product_age if c.product_age is not None else "",
|
||||
}
|
||||
Reference in New Issue
Block a user