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

42 lines
1.9 KiB
Python

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