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>
159 lines
5.7 KiB
Python
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)
|