From 0f04eee7469271d579c4a4902ce07f3ebd209674 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 9 Jun 2026 16:50:06 +0200 Subject: [PATCH] feat(iace): read ALL limits-form fields + always include universal lifecycles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (1) extractNarrativeFromMetadata now reads every limits-form field generically (no whitelist) — intended use, foreseeable misuse, all machine limits and all four interface groups (electrical/mechanical/pneumatic/software). Field-schema drift no longer silently drops hazard sources. (2) withUniversalLifecycles always adds normal_operation/setup/maintenance/ cleaning to the matched lifecycle phases — these occur on virtually every machine and the professional assesses them, so their hazards must be derived even when the form omits them. Kistenhubgeraet recall jumped 42.9% -> 74.3% (electrical 9% -> 82%) from the field-name fix alone; this broadens it further. Co-Authored-By: Claude Opus 4.7 --- .../api/handlers/iace_handler_init.go | 2 +- .../api/handlers/iace_handler_init_helpers.go | 79 +++++++++++-------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go index 52b44944..5a17e568 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -123,7 +123,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { matchOutput := engine.Match(iace.MatchInput{ ComponentLibraryIDs: componentIDs, EnergySourceIDs: energyIDs, - LifecyclePhases: parseResult.LifecyclePhases, + LifecyclePhases: withUniversalLifecycles(parseResult.LifecyclePhases), CustomTags: parseResult.CustomTags, OperationalStates: operationalStates, StateTransitions: parseResult.StateTransitions, diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go index 2f87f5a0..b389f04a 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go @@ -2,12 +2,35 @@ package handlers import ( "encoding/json" + "sort" "strings" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/google/uuid" ) +// withUniversalLifecycles ensures the lifecycle phases that occur on virtually +// every machine — normal operation, setup, maintenance, cleaning — are always +// present, so their hazards are derived even when the limits form does not list +// them explicitly. The professional assesses these phases on most devices. +func withUniversalLifecycles(parsed []string) []string { + seen := make(map[string]bool, len(parsed)+4) + out := make([]string, 0, len(parsed)+4) + for _, p := range parsed { + if p != "" && !seen[p] { + seen[p] = true + out = append(out, p) + } + } + for _, u := range []string{"normal_operation", "setup", "maintenance", "cleaning"} { + if !seen[u] { + seen[u] = true + out = append(out, u) + } + } + return out +} + // extractNarrativeFromMetadata builds a combined text from the limits_form. func extractNarrativeFromMetadata(metadata json.RawMessage) string { if metadata == nil { @@ -26,48 +49,34 @@ func extractNarrativeFromMetadata(metadata json.RawMessage) string { return "" } - // Every limits-form field that carries machine-describing text. Field - // names MUST match the real form schema — the per-interface fields - // (electrical/mechanical/pneumatic/software) each contribute hazards, not - // just the prose. Legacy names are kept so older projects still parse. - textFields := []string{ - // description / purpose - "general_description", "machine_designation", "intended_purpose", - "foreseeable_misuse", "foreseeable_misuses", "variants", "area_of_use", - // limits - "space_limits", "spatial_limits", "time_limits", "temporal_limits", - "environmental_conditions", "operating_conditions", - // energy + materials - "energy_sources", "energy_supply", "materials_processed", - // interfaces (the previously-ignored ones) - "interfaces_description", "control_system_description", "safety_functions_description", - "electrical_interfaces", "mechanical_interfaces", - "pneumatic_hydraulic_interfaces", "software_interfaces", - // people / maintenance - "maintenance_requirements", "personnel_requirements", "qualification_requirements", + // Read EVERY field of the limits form — intended use, foreseeable misuse, + // machine limits, and ALL interfaces (electrical/mechanical/pneumatic/ + // software). Each is a hazard source. We don't whitelist field names (the + // form schema evolves); noise fields like serial number / year are harmless + // because the parser only extracts from recognised keywords. Keys are + // sorted for deterministic output. + keys := make([]string, 0, len(limits)) + for k := range limits { + keys = append(keys, k) } - arrayFields := []string{"operating_modes", "person_groups", "industry_sectors"} + sort.Strings(keys) var sb strings.Builder - for _, field := range textFields { - if v, ok := limits[field]; ok { - if s, ok := v.(string); ok && strings.TrimSpace(s) != "" { - sb.WriteString(s) + for _, k := range keys { + switch val := limits[k].(type) { + case string: + if strings.TrimSpace(val) != "" { + sb.WriteString(val) sb.WriteString("\n\n") } - } - } - for _, field := range arrayFields { - if v, ok := limits[field]; ok { - if arr, ok := v.([]interface{}); ok { - for _, e := range arr { - if s, ok := e.(string); ok && s != "" { - sb.WriteString(s) - sb.WriteString(", ") - } + case []interface{}: + for _, e := range val { + if s, ok := e.(string); ok && s != "" { + sb.WriteString(s) + sb.WriteString(", ") } - sb.WriteString("\n\n") } + sb.WriteString("\n\n") } } return sb.String()