f5664612ad
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-backend-compliance (push) Successful in 13s
Build + Deploy / build-ai-sdk (push) Successful in 55s
Build + Deploy / build-developer-portal (push) Successful in 12s
Build + Deploy / build-tts (push) Successful in 34s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 13s
Build + Deploy / build-dsms-node (push) Successful in 14s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m5s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 46s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m19s
Neues Feld "Einsatzbereich" auf Interview-Seite (Sektion 7) mit 15 Branchen. Pattern Engine bekommt MachineTypes aus MatchInput → branchenfremde Patterns (Medizin, Aufzug, Bau etc.) feuern nur wenn die Branche ausgewählt ist. Refactoring: iace_handler_init.go aufgeteilt in init + init_helpers (LOC-Limit). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
208 lines
6.7 KiB
Go
208 lines
6.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
|
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
|
if metadata == nil {
|
|
return ""
|
|
}
|
|
var meta map[string]json.RawMessage
|
|
if err := json.Unmarshal(metadata, &meta); err != nil {
|
|
return ""
|
|
}
|
|
limitsRaw, ok := meta["limits_form"]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
var limits map[string]interface{}
|
|
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
|
return ""
|
|
}
|
|
|
|
textFields := []string{
|
|
"general_description", "intended_purpose", "foreseeable_misuse",
|
|
"space_limits", "time_limits", "environmental_conditions",
|
|
"energy_sources", "materials_processed", "operating_modes",
|
|
"maintenance_requirements", "personnel_requirements",
|
|
"interfaces_description", "control_system_description",
|
|
"safety_functions_description",
|
|
}
|
|
var result string
|
|
for _, field := range textFields {
|
|
if v, ok := limits[field]; ok {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
result += s + "\n\n"
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// patternCatToMeasureCat maps pattern hazard categories to measure categories.
|
|
func patternCatToMeasureCat(patternCat string) string {
|
|
m := map[string]string{
|
|
"mechanical_hazard": "mechanical", "electrical_hazard": "electrical",
|
|
"thermal_hazard": "thermal", "noise_vibration": "noise_vibration",
|
|
"pneumatic_hydraulic": "pneumatic_hydraulic", "material_environmental": "material_environmental",
|
|
"ergonomic": "ergonomic", "ergonomic_hazard": "ergonomic",
|
|
"software_fault": "software_control", "safety_function_failure": "safety_function",
|
|
"fire_explosion": "thermal", "radiation_hazard": "material_environmental",
|
|
"unauthorized_access": "cyber_network", "communication_failure": "cyber_network",
|
|
"firmware_corruption": "cyber_network", "logging_audit_failure": "cyber_network",
|
|
"ai_misclassification": "ai_specific", "false_classification": "ai_specific",
|
|
"model_drift": "ai_specific", "data_poisoning": "ai_specific",
|
|
"sensor_spoofing": "ai_specific", "unintended_bias": "ai_specific",
|
|
"sensor_fault": "software_control", "configuration_error": "software_control",
|
|
"update_failure": "software_control", "hmi_error": "software_control",
|
|
"emc_hazard": "electrical", "maintenance_hazard": "mechanical",
|
|
"mode_confusion": "software_control", "chemical_risk": "material_environmental",
|
|
}
|
|
if cat, ok := m[patternCat]; ok {
|
|
return cat
|
|
}
|
|
return "general"
|
|
}
|
|
|
|
// deriveComponentType guesses the component type from its tags.
|
|
func deriveComponentType(tags []string) iace.ComponentType {
|
|
for _, t := range tags {
|
|
switch {
|
|
case t == "software" || t == "has_software":
|
|
return iace.ComponentTypeSoftware
|
|
case t == "firmware" || t == "has_firmware":
|
|
return iace.ComponentTypeFirmware
|
|
case t == "has_ai" || t == "ai_model":
|
|
return iace.ComponentTypeAIModel
|
|
case t == "hmi" || t == "display" || t == "touchscreen":
|
|
return iace.ComponentTypeHMI
|
|
case t == "sensor" || t == "camera":
|
|
return iace.ComponentTypeSensor
|
|
case t == "electric_motor" || t == "electric_drive":
|
|
return iace.ComponentTypeElectrical
|
|
case t == "networked" || t == "ethernet" || t == "wifi":
|
|
return iace.ComponentTypeNetwork
|
|
case t == "hydraulic" || t == "pneumatic":
|
|
return iace.ComponentTypeActuator
|
|
}
|
|
}
|
|
return iace.ComponentTypeMechanical
|
|
}
|
|
|
|
// extractOperationalStatesFromMetadata reads the explicit operational_states
|
|
// selection that the user set via the Betriebszustand-UI.
|
|
func extractOperationalStatesFromMetadata(metadata json.RawMessage) []string {
|
|
if metadata == nil {
|
|
return nil
|
|
}
|
|
var meta map[string]json.RawMessage
|
|
if err := json.Unmarshal(metadata, &meta); err != nil {
|
|
return nil
|
|
}
|
|
raw, ok := meta["operational_states"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var states []string
|
|
if err := json.Unmarshal(raw, &states); err != nil {
|
|
return nil
|
|
}
|
|
return states
|
|
}
|
|
|
|
// mergeStringSlices merges two string slices, deduplicating entries.
|
|
func mergeStringSlices(a, b []string) []string {
|
|
seen := make(map[string]bool, len(a)+len(b))
|
|
var result []string
|
|
for _, s := range a {
|
|
if !seen[s] {
|
|
seen[s] = true
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
for _, s := range b {
|
|
if !seen[s] {
|
|
seen[s] = true
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// extractIndustrySectorsFromMetadata reads the industry_sectors selection
|
|
// from project metadata and maps them to MachineTypes for pattern filtering.
|
|
func extractIndustrySectorsFromMetadata(metadata json.RawMessage) []string {
|
|
if metadata == nil {
|
|
return nil
|
|
}
|
|
var meta map[string]json.RawMessage
|
|
if err := json.Unmarshal(metadata, &meta); err != nil {
|
|
return nil
|
|
}
|
|
limitsRaw, ok := meta["limits_form"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var limits map[string]json.RawMessage
|
|
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
|
return nil
|
|
}
|
|
sectorsRaw, ok := limits["industry_sectors"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var sectors []string
|
|
if err := json.Unmarshal(sectorsRaw, §ors); err != nil {
|
|
return nil
|
|
}
|
|
labelMap := map[string][]string{
|
|
"Allgemeiner Maschinenbau": {"general_industry"},
|
|
"Automobil / Zulieferer": {"automotive"},
|
|
"Robotik / Cobot": {"robotics_cobot", "cobot"},
|
|
"Medizintechnik": {"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
|
"Lebensmittel / Getraenke": {"food_processing"},
|
|
"Verpackung": {"packaging"},
|
|
"Pharma / Chemie": {"chemical", "pharmaceutical"},
|
|
"Bau / Baumaschinen": {"construction", "crane", "excavator"},
|
|
"Forst / Holzbearbeitung": {"forestry", "woodworking", "circular_saw"},
|
|
"Aufzuege / Foerdertechnik": {"elevator", "lift", "escalator", "conveyor"},
|
|
"Textil": {"textile", "spinning", "weaving", "finishing"},
|
|
"Landmaschinen": {"agricultural", "tractor", "harvester"},
|
|
"Druck / Papier": {"printing"},
|
|
"Metall / CNC": {"cnc", "metalworking", "lathe", "milling"},
|
|
"Schweissen / Oberflaechentechnik": {"welding", "surface_treatment"},
|
|
}
|
|
var result []string
|
|
seen := make(map[string]bool)
|
|
for _, sector := range sectors {
|
|
for _, mt := range labelMap[sector] {
|
|
if !seen[mt] {
|
|
seen[mt] = true
|
|
result = append(result, mt)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// findHazardForMeasureByCategory finds a matching hazard for a measure.
|
|
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
|
|
if id, ok := hazardsByCategory[measureCat]; ok {
|
|
return id
|
|
}
|
|
for cat, id := range hazardsByCategory {
|
|
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
|
|
return id
|
|
}
|
|
}
|
|
for _, id := range hazardsByCategory {
|
|
return id
|
|
}
|
|
return uuid.Nil
|
|
}
|