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

159 lines
5.7 KiB
Python

"""CanonicalProductRegulatoryProfile — the single semantic product profile.
Convergence layer (spec 2026-06-26): instead of letting the Go `gap.ProductProfile`
and the Python reasoning `ProductProfile` drift, ONE canonical type is the source
of truth. The Go gap engine LEADS (it carries real engine logic), so the canonical
mirrors gap's field names and adds the Navigator gaps the audit found missing
(economic-operator role, radio module, generates_usage_data, lifecycle phase,
structured BOM, safety-vs-security split, machine-vs-component) plus a
forward-looking Environmental-Impact domain.
No regulation logic lives here — types only. Mappers live in sibling modules.
Python 3.9 compatible (no `|` unions).
"""
from __future__ import annotations
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field
class CanonicalProductType(str, Enum): # mirrors gap.ProductType
SOFTWARE = "software"
HARDWARE = "hardware"
IOT = "iot"
SAAS = "saas"
EXCHANGE = "exchange"
MEDICAL_DEVICE = "medical_device"
MACHINERY = "machinery"
OTHER = "other"
class EconomicOperatorRole(str, Enum): # CE/CRA role — gap.ProductProfile has none
MANUFACTURER = "manufacturer"
IMPORTER = "importer"
DISTRIBUTOR = "distributor"
INTEGRATOR = "integrator"
OPERATOR = "operator"
SERVICE_PROVIDER = "service_provider"
class CanonicalLifecyclePhase(str, Enum):
DEVELOPMENT = "development"
PLACING_ON_MARKET = "placing_on_market"
OPERATION = "operation"
MAINTENANCE = "maintenance"
UPDATE = "update"
END_OF_LIFE = "end_of_life"
class ComponentKind(str, Enum):
MOTOR = "motor"
PUMP = "pump"
HEATING = "heating"
COOLING = "cooling"
CONTROLLER = "controller"
PLC = "plc"
HMI = "hmi"
SENSOR = "sensor"
ACTUATOR = "actuator"
CAMERA = "camera"
NETWORK_INTERFACE = "network_interface"
RADIO_MODULE = "radio_module"
CHEMICAL_DOSING = "chemical_dosing"
WATER_INLET = "water_inlet"
WASTEWATER_OUTLET = "wastewater_outlet"
BATTERY = "battery"
OTHER = "other"
class ProductComponent(BaseModel):
"""One structured BOM node — these nodes are what later trigger domains."""
name: str
kind: ComponentKind = ComponentKind.OTHER
notes: Optional[str] = None
class EnvironmentalImpact(BaseModel):
"""Forward-looking Umweltmedien-Trigger (own Navigator domain).
No regulation logic consumes these yet — profile fields only, so the model
is not blind to wastewater/air/chemicals/waste questions when that domain
is wired later (AbwV/WRRL/REACH/CLP/IED/BImSchG ...).
"""
discharges_to_wastewater: Optional[bool] = None
uses_cleaning_chemicals: Optional[bool] = None
supplies_chemicals: Optional[bool] = None
emits_to_air: Optional[bool] = None
uses_solvents: Optional[bool] = None
creates_waste: Optional[bool] = None
contains_restricted_substances: Optional[bool] = None
consumes_energy_or_water: Optional[bool] = None
has_cooling_or_spraying_water: Optional[bool] = None
class CanonicalProductRegulatoryProfile(BaseModel):
# --- identity ---
name: str = ""
description: str = ""
product_type: Optional[CanonicalProductType] = None
product_profile_id: Optional[str] = None
tenant_id: Optional[str] = None
iace_project_id: Optional[str] = None
# --- gap-native lists ---
technologies: List[str] = Field(default_factory=list)
data_processing: List[str] = Field(default_factory=list)
markets: List[str] = Field(default_factory=list) # real list — never hardcoded ['EU']
existing_certifications: List[str] = Field(default_factory=list)
applied_norms: List[str] = Field(default_factory=list)
# --- gap-native product / IST-state booleans (tri-state: None = unknown) ---
connected_to_internet: Optional[bool] = None
has_software_updates: Optional[bool] = None
uses_ai: Optional[bool] = None
processes_personal_data: Optional[bool] = None
is_critical_infra_supplier: Optional[bool] = None
has_risk_assessment: Optional[bool] = None
has_technical_file: Optional[bool] = None
has_operating_manual: Optional[bool] = None
has_sbom: Optional[bool] = None
has_vuln_management: Optional[bool] = None
has_update_mechanism: Optional[bool] = None
has_incident_response: Optional[bool] = None
has_supply_chain_mgmt: Optional[bool] = None
ce_marking_since: Optional[str] = None
product_age: Optional[str] = None
# --- NEW Navigator-gap fields (audit 2026-06-26) ---
economic_operator_role: Optional[EconomicOperatorRole] = None
has_radio_module: Optional[bool] = None
generates_usage_data: Optional[bool] = None
lifecycle_phase: Optional[CanonicalLifecyclePhase] = None
components: List[ProductComponent] = Field(default_factory=list)
has_safety_function: Optional[bool] = None
safety_function_description: Optional[str] = None
has_security_function: Optional[bool] = None # safety vs security split
has_remote_access: Optional[bool] = None
has_embedded_software: Optional[bool] = None
is_machine: Optional[bool] = None
is_component: Optional[bool] = None
is_spare_part: Optional[bool] = None
# --- company / market context (NIS2 + scope; from company-profile) ---
b2b_or_b2c: Optional[str] = None
sector_industry: Optional[str] = None
company_size: Optional[str] = None
primary_jurisdiction: Optional[str] = None
# --- AI context (classification stays delegated to ai-act/ucca) ---
ai_integration_type: List[str] = Field(default_factory=list)
human_oversight_level: Optional[str] = None
# --- forward-looking environmental domain ---
environmental: EnvironmentalImpact = Field(default_factory=EnvironmentalImpact)