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 80344ea..99c6a2f 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -102,6 +102,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { EnergySourceIDs: energyIDs, LifecyclePhases: parseResult.LifecyclePhases, CustomTags: parseResult.CustomTags, + OperationalStates: parseResult.OperationalStates, + StateTransitions: parseResult.StateTransitions, }) steps = append(steps, InitStep{ Name: "Patterns abgeglichen", diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go index 5600b3e..aea1bda 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_parser.go @@ -73,6 +73,8 @@ func (h *IACEHandler) ParseNarrative(c *gin.Context) { EnergySourceIDs: energyIDs, LifecyclePhases: parseResult.LifecyclePhases, CustomTags: parseResult.CustomTags, + OperationalStates: parseResult.OperationalStates, + StateTransitions: parseResult.StateTransitions, } matchOutput := engine.Match(matchInput) diff --git a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go index 2bb689f..5c25c85 100644 --- a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go +++ b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go @@ -32,4 +32,14 @@ type HazardPattern struct { // project's machine_type is in this list. Prevents e.g. press-specific // patterns from firing for a cobot project. MachineTypes []string `json:"machine_types,omitempty"` + // OperationalStates restricts this pattern to specific operational states. + // Empty/nil = fires in ALL states (backwards compatible). If set, the pattern + // only fires when the project declares at least one matching state. + // Standard states: startup, homing, automatic_operation, manual_operation, + // teach_mode, maintenance, cleaning, emergency_stop, recovery_mode. + OperationalStates []string `json:"operational_states,omitempty"` + // StateTransitions restricts to specific state transitions (format: "from→to"). + // Hazards at transitions are critical — e.g. "maintenance→automatic_operation" + // is where "unexpected restart" fatalities occur. Empty = no transition filter. + StateTransitions []string `json:"state_transitions,omitempty"` } diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_cobot.go b/ai-compliance-sdk/internal/iace/hazard_patterns_cobot.go index 766543d..318f966 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_cobot.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_cobot.go @@ -63,7 +63,7 @@ func GetCobotHazardPatterns() []HazardPattern { DefaultSeverity: 4, DefaultExposure: 3, }, { - ID: "HP062", NameDE: "Fehlprogrammierung Kraft-/Geschwindigkeitsgrenzwerte", NameEN: "Misprogramming of force/speed limits", + ID: "HP062", OperationalStates: []string{"teach_mode"}, NameDE: "Fehlprogrammierung Kraft-/Geschwindigkeitsgrenzwerte", NameEN: "Misprogramming of force/speed limits", RequiredComponentTags: []string{"programmable", "force_limited"}, RequiredEnergyTags: []string{}, GeneratedHazardCats: []string{"safety_function_failure"}, @@ -94,7 +94,7 @@ func GetCobotHazardPatterns() []HazardPattern { DefaultSeverity: 3, DefaultExposure: 3, }, { - ID: "HP064", NameDE: "Quetschen im Roboter-Arbeitsraum (nicht-kollaborierend)", NameEN: "Crushing in robot workspace (non-collaborative)", + ID: "HP064", OperationalStates: []string{"automatic_operation"}, NameDE: "Quetschen im Roboter-Arbeitsraum (nicht-kollaborierend)", NameEN: "Crushing in robot workspace (non-collaborative)", RequiredComponentTags: []string{"moving_part", "high_force", "sensor_part"}, RequiredEnergyTags: []string{}, ExcludedComponentTags: []string{"collaborative_operation"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go b/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go index c7c9480..6304665 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go @@ -5,70 +5,70 @@ package iace func GetMaintenanceExtPatterns() []HazardPattern { return []HazardPattern{ // — Wartung allgemein (HP700-HP709) — - {ID: "HP700", NameDE: "LOTO-Fehler: Maschine nicht freigeschaltet", NameEN: "LOTO failure: not locked out", + {ID: "HP700", OperationalStates: []string{"maintenance"}, NameDE: "LOTO-Fehler: Maschine nicht freigeschaltet", NameEN: "LOTO failure: not locked out", RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"}, SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08", "E20"}, Priority: 95, ScenarioDE: "Arbeit ohne Freischaltung der Maschine", TriggerDE: "Fehlende LOTO-Prozedur", HarmDE: "Erfassen durch anlaufende Teile, Tod", AffectedDE: "Instandhalter", ZoneDE: "Gesamte Maschine", DefaultSeverity: 5, DefaultExposure: 3}, - {ID: "HP701", NameDE: "Restenergie trotz Abschaltung", NameEN: "Residual energy after shutdown", + {ID: "HP701", OperationalStates: []string{"maintenance"}, NameDE: "Restenergie trotz Abschaltung", NameEN: "Residual energy after shutdown", RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 90, ScenarioDE: "Gespeicherte Energie entlaedt sich bei Wartung", TriggerDE: "Nicht abgelassener Druckspeicher", HarmDE: "Unkontrollierte Bewegung, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Antriebe, Speicher", DefaultSeverity: 5, DefaultExposure: 3}, - {ID: "HP702", NameDE: "Falsches Werkzeug bei Wartung", NameEN: "Wrong tool during maintenance", + {ID: "HP702", OperationalStates: []string{"maintenance"}, NameDE: "Falsches Werkzeug bei Wartung", NameEN: "Wrong tool during maintenance", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50, ScenarioDE: "Ungeeignetes oder defektes Werkzeug", TriggerDE: "Falscher Schraubenschluessel", HarmDE: "Abrutschen, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Wartungszugang", DefaultSeverity: 3, DefaultExposure: 4}, - {ID: "HP703", NameDE: "Fehlende Qualifikation des Instandhalters", NameEN: "Insufficient maintainer qualification", + {ID: "HP703", OperationalStates: []string{"maintenance"}, NameDE: "Fehlende Qualifikation des Instandhalters", NameEN: "Insufficient maintainer qualification", RequiredComponentTags: []string{"electrical_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"electrical_hazard", "safety_function_failure"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70, ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft", HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank", DefaultSeverity: 4, DefaultExposure: 3}, - {ID: "HP704", NameDE: "Herabfallen schwerer Teile bei Demontage", NameEN: "Heavy parts falling during disassembly", + {ID: "HP704", OperationalStates: []string{"maintenance"}, NameDE: "Herabfallen schwerer Teile bei Demontage", NameEN: "Heavy parts falling during disassembly", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 75, ScenarioDE: "Schwere Teile fallen bei Demontage herab", TriggerDE: "Fehlende Abstuetzung", HarmDE: "Quetschung, Frakturen, Tod", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich", DefaultSeverity: 5, DefaultExposure: 3}, - {ID: "HP705", NameDE: "Vergessenes Werkzeug in Maschine", NameEN: "Forgotten tool in machine", + {ID: "HP705", OperationalStates: []string{"maintenance"}, NameDE: "Vergessenes Werkzeug in Maschine", NameEN: "Forgotten tool in machine", RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65, ScenarioDE: "Zurueckgelassenes Werkzeug wird Geschoss", TriggerDE: "Keine Werkzeugkontrolle", HarmDE: "Herausgeschleudertes Teil, Verletzungen", AffectedDE: "Bedienpersonal", ZoneDE: "Arbeitsraum", DefaultSeverity: 4, DefaultExposure: 2}, - {ID: "HP706", NameDE: "Schnittwunden an scharfkantigen Teilen", NameEN: "Cuts on sharp-edged parts", + {ID: "HP706", OperationalStates: []string{"maintenance"}, NameDE: "Schnittwunden an scharfkantigen Teilen", NameEN: "Cuts on sharp-edged parts", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45, ScenarioDE: "Scharfe Kanten und Grate verletzen", TriggerDE: "Fehlende Schutzhandschuhe", HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen", DefaultSeverity: 2, DefaultExposure: 4}, - {ID: "HP707", NameDE: "Verbrennung an heissen Teilen bei Wartung", NameEN: "Burn on hot parts during maintenance", + {ID: "HP707", OperationalStates: []string{"maintenance"}, NameDE: "Verbrennung an heissen Teilen bei Wartung", NameEN: "Burn on hot parts during maintenance", RequiredComponentTags: []string{"high_temperature"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"thermal_hazard"}, SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60, ScenarioDE: "Maschine nicht abgekuehlt vor Wartung", TriggerDE: "Zu kurze Abkuehlzeit", HarmDE: "Kontaktverbrennungen", AffectedDE: "Instandhalter", ZoneDE: "Heizplatten, Motorgehaeuse", DefaultSeverity: 3, DefaultExposure: 4}, - {ID: "HP708", NameDE: "Fehlende Wartungsfreigabe", NameEN: "Missing maintenance permit", + {ID: "HP708", OperationalStates: []string{"maintenance"}, NameDE: "Fehlende Wartungsfreigabe", NameEN: "Missing maintenance permit", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"safety_function_failure"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70, ScenarioDE: "Wartung ohne formale Freigabe", TriggerDE: "Fehlender Erlaubnisschein", HarmDE: "Unerwarteter Maschinenbetrieb", AffectedDE: "Instandhalter", ZoneDE: "Gesamte Maschine", DefaultSeverity: 5, DefaultExposure: 3}, - {ID: "HP709", NameDE: "Biologische Gefaehrdung bei KSS-Wartung", NameEN: "Biological hazard MWF maintenance", + {ID: "HP709", OperationalStates: []string{"maintenance"}, NameDE: "Biologische Gefaehrdung bei KSS-Wartung", NameEN: "Biological hazard MWF maintenance", RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"material_environmental"}, SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50, @@ -76,35 +76,35 @@ func GetMaintenanceExtPatterns() []HazardPattern { HarmDE: "Hautinfektionen, Atemwegsbeschwerden", AffectedDE: "Instandhalter", ZoneDE: "KSS-System", DefaultSeverity: 2, DefaultExposure: 3}, // — Einrichten / Umruesten (HP710-HP719) — - {ID: "HP710", NameDE: "Falsche Parameter nach Umruestung", NameEN: "Wrong parameters after changeover", + {ID: "HP710", OperationalStates: []string{"teach_mode"}, NameDE: "Falsche Parameter nach Umruestung", NameEN: "Wrong parameters after changeover", RequiredComponentTags: []string{"programmable"}, RequiredLifecycles: []string{"setup"}, GeneratedHazardCats: []string{"safety_function_failure"}, SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 75, ScenarioDE: "Falsche Maschinenparameter nach Produktwechsel", TriggerDE: "Falsche Rezeptnummer", HarmDE: "Uebergeschwindigkeit, Werkzeugbruch", AffectedDE: "Einrichter", ZoneDE: "Bedienterminal", DefaultSeverity: 4, DefaultExposure: 3}, - {ID: "HP711", NameDE: "Quetschung bei Werkzeugwechsel", NameEN: "Crushing during tool change", + {ID: "HP711", OperationalStates: []string{"maintenance"}, NameDE: "Quetschung bei Werkzeugwechsel", NameEN: "Crushing during tool change", RequiredComponentTags: []string{"moving_part", "high_force"}, RequiredLifecycles: []string{"setup"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M003", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80, ScenarioDE: "Schwere Werkzeuge manuell gewechselt", TriggerDE: "Kein Hebezeug, Finger eingeklemmt", HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme", DefaultSeverity: 4, DefaultExposure: 4}, - {ID: "HP712", NameDE: "Unkontrollierte Bewegung bei Testlauf", NameEN: "Uncontrolled movement test run", + {ID: "HP712", OperationalStates: []string{"teach_mode", "manual_operation"}, NameDE: "Unkontrollierte Bewegung bei Testlauf", NameEN: "Uncontrolled movement test run", RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80, ScenarioDE: "Testlauf mit Person im Gefahrenbereich", TriggerDE: "Volle Geschwindigkeit, kein Schutz", HarmDE: "Erfassen, Quetschen", AffectedDE: "Einrichter", ZoneDE: "Arbeitsraum", DefaultSeverity: 5, DefaultExposure: 3}, - {ID: "HP713", NameDE: "Einrichtbetrieb ohne reduzierte Geschwindigkeit", NameEN: "Setup without reduced speed", + {ID: "HP713", OperationalStates: []string{"teach_mode"}, NameDE: "Einrichtbetrieb ohne reduzierte Geschwindigkeit", NameEN: "Setup without reduced speed", RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"}, GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"}, SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 85, ScenarioDE: "Volle Geschwindigkeit im Einrichtmodus", TriggerDE: "Defekter Betriebsartschalter", HarmDE: "Erfassen bei voller Geschwindigkeit", AffectedDE: "Einrichter", ZoneDE: "Werkzeugbereich", DefaultSeverity: 5, DefaultExposure: 3}, - {ID: "HP714", NameDE: "Werkstueck faellt bei Aufspannung", NameEN: "Workpiece falls during clamping", + {ID: "HP714", OperationalStates: []string{"teach_mode", "maintenance"}, NameDE: "Werkstueck faellt bei Aufspannung", NameEN: "Workpiece falls during clamping", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 55, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_operational.go b/ai-compliance-sdk/internal/iace/hazard_patterns_operational.go index 163c975..863e066 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_operational.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_operational.go @@ -41,7 +41,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 4, DefaultExposure: 3, }, { - ID: "HP068", NameDE: "Unerwarteter Wiederanlauf nach Stoerung", NameEN: "Unexpected restart after fault", + ID: "HP068", NameDE: "Unerwarteter Wiederanlauf nach Stoerung", OperationalStates: []string{"recovery_mode", "emergency_stop"}, StateTransitions: []string{"maintenance→automatic_operation", "emergency_stop→recovery_mode"}, NameEN: "Unexpected restart after fault", RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"fault_clearing"}, GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"}, @@ -57,7 +57,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 5, DefaultExposure: 3, }, { - ID: "HP069", NameDE: "Restenergie nach Abschaltung (gespeichert)", NameEN: "Residual stored energy after shutdown", + ID: "HP069", NameDE: "Restenergie nach Abschaltung (gespeichert)", OperationalStates: []string{"maintenance", "emergency_stop"}, NameEN: "Residual stored energy after shutdown", RequiredComponentTags: []string{"stored_energy"}, RequiredLifecycles: []string{"maintenance", "fault_clearing"}, GeneratedHazardCats: []string{"mechanical_hazard", "electrical_hazard"}, @@ -72,7 +72,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 4, DefaultExposure: 3, }, { - ID: "HP070", NameDE: "Eingriff in laufende Maschine bei Stoerung", NameEN: "Intervention in running machine during fault", + ID: "HP070", NameDE: "Eingriff in laufende Maschine bei Stoerung", OperationalStates: []string{"recovery_mode"}, NameEN: "Intervention in running machine during fault", RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"fault_clearing"}, ExcludedComponentTags: []string{"interlocked"}, @@ -120,7 +120,7 @@ func GetOperationalHazardPatterns() []HazardPattern { // Wartung / Instandhaltung (HP073-HP079) // ================================================================ { - ID: "HP073", NameDE: "Wartung ohne LOTO (Lockout/Tagout)", NameEN: "Maintenance without LOTO", + ID: "HP073", NameDE: "Wartung ohne LOTO (Lockout/Tagout)", OperationalStates: []string{"maintenance"}, NameEN: "Maintenance without LOTO", RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"maintenance_hazard"}, @@ -136,7 +136,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 5, DefaultExposure: 3, }, { - ID: "HP074", NameDE: "Sturz von Wartungsbuehne / Leiter", NameEN: "Fall from maintenance platform / ladder", + ID: "HP074", OperationalStates: []string{"maintenance"}, NameDE: "Sturz von Wartungsbuehne / Leiter", NameEN: "Fall from maintenance platform / ladder", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance", "cleaning"}, GeneratedHazardCats: []string{"mechanical_hazard"}, @@ -150,7 +150,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 4, DefaultExposure: 2, }, { - ID: "HP075", NameDE: "Kontakt mit heissen Teilen bei Wartung", NameEN: "Contact with hot parts during maintenance", + ID: "HP075", OperationalStates: []string{"maintenance"}, NameDE: "Kontakt mit heissen Teilen bei Wartung", NameEN: "Contact with hot parts during maintenance", RequiredComponentTags: []string{"high_temperature"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"thermal_hazard"}, @@ -165,7 +165,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 3, DefaultExposure: 3, }, { - ID: "HP076", NameDE: "Kontakt mit Gefahrstoffen bei Wartung", NameEN: "Contact with hazardous substances during maintenance", + ID: "HP076", OperationalStates: []string{"maintenance"}, NameDE: "Kontakt mit Gefahrstoffen bei Wartung", NameEN: "Contact with hazardous substances during maintenance", RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance", "cleaning"}, GeneratedHazardCats: []string{"material_environmental"}, @@ -179,7 +179,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 3, DefaultExposure: 3, }, { - ID: "HP077", NameDE: "Elektrischer Schlag bei Wartungsarbeiten", NameEN: "Electric shock during maintenance", + ID: "HP077", OperationalStates: []string{"maintenance"}, NameDE: "Elektrischer Schlag bei Wartungsarbeiten", NameEN: "Electric shock during maintenance", RequiredComponentTags: []string{"high_voltage"}, RequiredLifecycles: []string{"maintenance", "fault_clearing"}, GeneratedHazardCats: []string{"electrical_hazard"}, @@ -195,7 +195,7 @@ func GetOperationalHazardPatterns() []HazardPattern { DefaultSeverity: 5, DefaultExposure: 3, }, { - ID: "HP078", NameDE: "Ergonomische Belastung bei Wartungszugang", NameEN: "Ergonomic strain at maintenance access", + ID: "HP078", OperationalStates: []string{"maintenance"}, NameDE: "Ergonomische Belastung bei Wartungszugang", NameEN: "Ergonomic strain at maintenance access", RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"ergonomic"}, @@ -228,7 +228,7 @@ func GetOperationalHazardPatterns() []HazardPattern { // Einrichten / Umruesten / Werkzeugwechsel (HP080-HP085) // ================================================================ { - ID: "HP080", NameDE: "Quetschen bei Werkzeugwechsel", NameEN: "Crushing during tool change", + ID: "HP080", NameDE: "Quetschen bei Werkzeugwechsel", OperationalStates: []string{"maintenance", "teach_mode"}, NameEN: "Crushing during tool change", RequiredComponentTags: []string{"crush_point", "high_force"}, RequiredLifecycles: []string{"changeover", "setup"}, GeneratedHazardCats: []string{"mechanical_hazard"}, diff --git a/ai-compliance-sdk/internal/iace/narrative_parser.go b/ai-compliance-sdk/internal/iace/narrative_parser.go index 18bc7b6..73a3a07 100644 --- a/ai-compliance-sdk/internal/iace/narrative_parser.go +++ b/ai-compliance-sdk/internal/iace/narrative_parser.go @@ -33,13 +33,15 @@ type TechSpec struct { // ParseResult contains all entities extracted from a machine narrative. type ParseResult struct { - Components []ComponentMatch `json:"components"` - EnergySources []EnergyMatch `json:"energy_sources"` - LifecyclePhases []string `json:"lifecycle_phases"` - Roles []string `json:"roles"` - CustomTags []string `json:"custom_tags"` - TechSpecs []TechSpec `json:"tech_specs"` - Confidence float64 `json:"confidence"` + Components []ComponentMatch `json:"components"` + EnergySources []EnergyMatch `json:"energy_sources"` + LifecyclePhases []string `json:"lifecycle_phases"` + Roles []string `json:"roles"` + CustomTags []string `json:"custom_tags"` + TechSpecs []TechSpec `json:"tech_specs"` + Confidence float64 `json:"confidence"` + OperationalStates []string `json:"operational_states,omitempty"` + StateTransitions []string `json:"state_transitions,omitempty"` } // techSpecPattern matches numeric values with engineering units. @@ -91,6 +93,40 @@ var roleKeywords = map[string]string{ "leiharbeiter": "temp_worker", } +// operationalStateKeywords maps German text patterns to operational state IDs. +var operationalStateKeywords = map[string]string{ + "hochfahren": "startup", + "anlauf": "startup", + "anfahren": "startup", + "referenzfahrt": "homing", + "referenzpunkt": "homing", + "automatikbetrieb": "automatic_operation", + "automatisch": "automatic_operation", + "handbetrieb": "manual_operation", + "manuell": "manual_operation", + "tippbetrieb": "manual_operation", + "teach": "teach_mode", + "einrichtbetrieb": "teach_mode", + "programmier": "teach_mode", + "wartung": "maintenance", + "instandhaltung": "maintenance", + "reinigung": "cleaning", + "not-halt": "emergency_stop", + "nothalt": "emergency_stop", + "notabschaltung": "emergency_stop", + "wiederanlauf": "recovery_mode", + "wiederinbetriebnahme":"recovery_mode", + "quittier": "recovery_mode", +} + +// stateTransitionKeywords maps keyword combinations to state transitions. +var stateTransitionKeywords = map[string]string{ + "unerwarteter wiederanlauf": "maintenance→automatic_operation", + "wiederanlauf nach not": "emergency_stop→recovery_mode", + "automatischer anlauf": "startup→automatic_operation", + "betriebsartwechsel": "manual_operation→automatic_operation", +} + // ParseNarrative extracts components, energy sources, lifecycle phases, // roles, and tags from a machine description text. Fully deterministic, // no LLM required. @@ -221,12 +257,37 @@ func ParseNarrative(text string, machineType ...string) ParseResult { } } - // 6. Collect all tags + // 6. Extract operational states + stateSet := make(map[string]bool) + for kw, state := range operationalStateKeywords { + kwNorm := strings.ReplaceAll(kw, "ä", "ae") + kwNorm = strings.ReplaceAll(kwNorm, "ö", "oe") + kwNorm = strings.ReplaceAll(kwNorm, "ü", "ue") + if strings.Contains(lower, kwNorm) { + if !stateSet[state] { + stateSet[state] = true + result.OperationalStates = append(result.OperationalStates, state) + } + } + } + + // 7. Extract state transitions + transSet := make(map[string]bool) + for kw, trans := range stateTransitionKeywords { + if strings.Contains(lower, kw) { + if !transSet[trans] { + transSet[trans] = true + result.StateTransitions = append(result.StateTransitions, trans) + } + } + } + + // 8. Collect all tags for t := range tagSet { result.CustomTags = append(result.CustomTags, t) } - // 7. Calculate overall confidence + // 9. Calculate overall confidence if len(result.Components) > 0 { result.Confidence = float64(len(result.Components)) / 15.0 // Normalize to ~1.0 for 15 components if result.Confidence > 1.0 { diff --git a/ai-compliance-sdk/internal/iace/pattern_engine.go b/ai-compliance-sdk/internal/iace/pattern_engine.go index df121a3..f3c4722 100644 --- a/ai-compliance-sdk/internal/iace/pattern_engine.go +++ b/ai-compliance-sdk/internal/iace/pattern_engine.go @@ -8,6 +8,12 @@ type MatchInput struct { EnergySourceIDs []string `json:"energy_source_ids"` LifecyclePhases []string `json:"lifecycle_phases"` CustomTags []string `json:"custom_tags"` + // OperationalStates are the active operational states of the machine. + // Used to filter patterns that only apply in specific states (e.g. teach_mode, maintenance). + OperationalStates []string `json:"operational_states,omitempty"` + // StateTransitions are active state transitions (format: "from→to"). + // Used to detect transition-specific hazards like unexpected restart. + StateTransitions []string `json:"state_transitions,omitempty"` } // MatchOutput contains the results of pattern matching. @@ -42,9 +48,11 @@ type PatternMatch struct { ZoneDE string `json:"zone_de,omitempty"` DefaultSeverity int `json:"default_severity,omitempty"` DefaultExposure int `json:"default_exposure,omitempty"` - HazardCats []string `json:"hazard_categories,omitempty"` - ExpertHintDE string `json:"expert_hint_de,omitempty"` - RequiresExpert bool `json:"requires_expert,omitempty"` + HazardCats []string `json:"hazard_categories,omitempty"` + ExpertHintDE string `json:"expert_hint_de,omitempty"` + RequiresExpert bool `json:"requires_expert,omitempty"` + OperationalStates []string `json:"operational_states,omitempty"` + StateTransitions []string `json:"state_transitions,omitempty"` } // HazardSuggestion is a suggested hazard from pattern matching. @@ -141,7 +149,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { evidenceSources := make(map[string][]string) // evidence ID → pattern IDs for _, p := range patterns { - if !patternMatches(p, tagSet, input.LifecyclePhases) { + if !patternMatches(p, tagSet, input) { continue } @@ -175,6 +183,18 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { reasons = append(reasons, MatchReason{Type: "lifecycle_match", Tag: lc, Met: found}) } } + if len(p.OperationalStates) > 0 { + stateSet := toSet(input.OperationalStates) + for _, s := range p.OperationalStates { + reasons = append(reasons, MatchReason{Type: "operational_state", Tag: s, Met: stateSet[s]}) + } + } + if len(p.StateTransitions) > 0 { + transSet := toSet(input.StateTransitions) + for _, t := range p.StateTransitions { + reasons = append(reasons, MatchReason{Type: "state_transition", Tag: t, Met: transSet[t]}) + } + } matchedPatterns = append(matchedPatterns, PatternMatch{ PatternID: p.ID, @@ -190,8 +210,10 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { DefaultSeverity: p.DefaultSeverity, DefaultExposure: p.DefaultExposure, HazardCats: p.GeneratedHazardCats, - ExpertHintDE: p.ExpertHintDE, - RequiresExpert: p.RequiresExpertCalculation, + ExpertHintDE: p.ExpertHintDE, + RequiresExpert: p.RequiresExpertCalculation, + OperationalStates: p.OperationalStates, + StateTransitions: p.StateTransitions, }) for _, cat := range p.GeneratedHazardCats { @@ -259,8 +281,9 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { } } -// patternMatches checks if a pattern fires given the resolved tag set and lifecycle phases. -func patternMatches(p HazardPattern, tagSet map[string]bool, lifecyclePhases []string) bool { +// patternMatches checks if a pattern fires given the resolved tag set, lifecycle phases, +// operational states, and state transitions. +func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) bool { // All required component tags must be present (AND) for _, t := range p.RequiredComponentTags { if !tagSet[t] { @@ -283,9 +306,9 @@ func patternMatches(p HazardPattern, tagSet map[string]bool, lifecyclePhases []s } // If pattern requires specific lifecycle phases, at least one must match - if len(p.RequiredLifecycles) > 0 && len(lifecyclePhases) > 0 { + if len(p.RequiredLifecycles) > 0 && len(input.LifecyclePhases) > 0 { found := false - phaseSet := toSet(lifecyclePhases) + phaseSet := toSet(input.LifecyclePhases) for _, lp := range p.RequiredLifecycles { if phaseSet[lp] { found = true @@ -297,6 +320,37 @@ func patternMatches(p HazardPattern, tagSet map[string]bool, lifecyclePhases []s } } + // If pattern requires specific operational states, at least one must match. + // nil/empty OperationalStates on pattern = fires in ALL states (backwards compatible). + if len(p.OperationalStates) > 0 && len(input.OperationalStates) > 0 { + found := false + stateSet := toSet(input.OperationalStates) + for _, s := range p.OperationalStates { + if stateSet[s] { + found = true + break + } + } + if !found { + return false + } + } + + // If pattern requires specific state transitions, at least one must match. + if len(p.StateTransitions) > 0 && len(input.StateTransitions) > 0 { + found := false + transSet := toSet(input.StateTransitions) + for _, t := range p.StateTransitions { + if transSet[t] { + found = true + break + } + } + if !found { + return false + } + } + return true } @@ -317,3 +371,54 @@ func maxInt(a, b int) int { } return b } + +// ── Operational State Graph constants ────────────────────────────── + +// Standard operational states for machinery (ISO 12100 + BetrSichV). +const ( + StateStartup = "startup" + StateHoming = "homing" + StateAutomaticOperation = "automatic_operation" + StateManualOperation = "manual_operation" + StateTeachMode = "teach_mode" + StateMaintenance = "maintenance" + StateCleaning = "cleaning" + StateEmergencyStop = "emergency_stop" + StateRecoveryMode = "recovery_mode" +) + +// AllOperationalStates returns the 9 standard operational states. +func AllOperationalStates() []string { + return []string{ + StateStartup, StateHoming, StateAutomaticOperation, + StateManualOperation, StateTeachMode, StateMaintenance, + StateCleaning, StateEmergencyStop, StateRecoveryMode, + } +} + +// StandardStateTransitions returns the valid transitions between states. +// Format: "from→to". These represent the directed edges of the state graph. +func StandardStateTransitions() []string { + return []string{ + "startup→homing", + "homing→automatic_operation", + "automatic_operation→manual_operation", + "manual_operation→automatic_operation", + "automatic_operation→teach_mode", + "teach_mode→automatic_operation", + "automatic_operation→maintenance", + "manual_operation→maintenance", + "maintenance→automatic_operation", + "maintenance→manual_operation", + "automatic_operation→cleaning", + "cleaning→automatic_operation", + "automatic_operation→emergency_stop", + "manual_operation→emergency_stop", + "teach_mode→emergency_stop", + "maintenance→emergency_stop", + "cleaning→emergency_stop", + "emergency_stop→recovery_mode", + "recovery_mode→homing", + "recovery_mode→manual_operation", + } +} diff --git a/ai-compliance-sdk/internal/iace/pattern_engine_test.go b/ai-compliance-sdk/internal/iace/pattern_engine_test.go index d18a514..fe1a2c1 100644 --- a/ai-compliance-sdk/internal/iace/pattern_engine_test.go +++ b/ai-compliance-sdk/internal/iace/pattern_engine_test.go @@ -215,3 +215,219 @@ func TestPatternEngine_SafetyDevice_HighPriority(t *testing.T) { t.Error("expected safety_function_failure for Not-Halt-Taster") } } + +// ── Operational State Graph tests ────────────────────────────────── + +func TestPatternEngine_OperationalState_NilFiresAlways(t *testing.T) { + // Patterns with nil OperationalStates should fire regardless of input states + engine := NewPatternEngine() + result1 := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + }) + result2 := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + OperationalStates: []string{"automatic_operation"}, + }) + + if len(result1.MatchedPatterns) == 0 { + t.Fatal("expected patterns to fire without operational states") + } + if len(result2.MatchedPatterns) == 0 { + t.Fatal("expected patterns to fire with automatic_operation state") + } + // nil-state patterns should fire in both cases + if len(result1.MatchedPatterns) != len(result2.MatchedPatterns) { + t.Logf("patterns without states: %d, with automatic_operation: %d", len(result1.MatchedPatterns), len(result2.MatchedPatterns)) + // This is OK — some patterns may have OperationalStates set and filter differently + } +} + +func TestPatternEngine_OperationalState_MaintenanceFilter(t *testing.T) { + // HP073 (LOTO) has OperationalStates: ["maintenance"] + // It should only fire when maintenance is in the input states + engine := NewPatternEngine() + + // Without maintenance state — HP073 should still fire if no states in input + // (because empty input states = no filtering) + resultNoState := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + LifecyclePhases: []string{"maintenance"}, + }) + + // With maintenance state — HP073 should fire + resultMaint := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + LifecyclePhases: []string{"maintenance"}, + OperationalStates: []string{"maintenance"}, + }) + + // With only automatic_operation — HP073 should NOT fire + resultAuto := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + LifecyclePhases: []string{"maintenance"}, + OperationalStates: []string{"automatic_operation"}, + }) + + // HP073 should be in resultMaint but not in resultAuto + hasHP073Maint := false + for _, p := range resultMaint.MatchedPatterns { + if p.PatternID == "HP073" { + hasHP073Maint = true + break + } + } + + hasHP073Auto := false + for _, p := range resultAuto.MatchedPatterns { + if p.PatternID == "HP073" { + hasHP073Auto = true + break + } + } + + if !hasHP073Maint { + t.Error("HP073 should fire with maintenance operational state") + } + if hasHP073Auto { + t.Error("HP073 should NOT fire with automatic_operation operational state") + } + + _ = resultNoState // HP073 fires here too because empty input states = no filter +} + +func TestPatternEngine_StateTransition_UnexpectedRestart(t *testing.T) { + // HP068 has StateTransitions: ["maintenance→automatic_operation", "emergency_stop→recovery_mode"] + engine := NewPatternEngine() + + // HP068 requires moving_part + programmable — C001 (Roboterarm) + C071 (SPS) + // With matching transition + resultMatch := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001", "C071"}, + EnergySourceIDs: []string{"EN01"}, + LifecyclePhases: []string{"fault_clearing"}, + OperationalStates: []string{"recovery_mode", "emergency_stop"}, + StateTransitions: []string{"maintenance→automatic_operation"}, + }) + + // With non-matching transition + resultNoMatch := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001", "C071"}, + EnergySourceIDs: []string{"EN01"}, + LifecyclePhases: []string{"fault_clearing"}, + OperationalStates: []string{"recovery_mode", "emergency_stop"}, + StateTransitions: []string{"startup→homing"}, + }) + + hasHP068Match := false + for _, p := range resultMatch.MatchedPatterns { + if p.PatternID == "HP068" { + hasHP068Match = true + // Verify explainability reasons include state transition + hasTransReason := false + for _, r := range p.MatchReasons { + if r.Type == "state_transition" { + hasTransReason = true + break + } + } + if !hasTransReason { + t.Error("HP068 match should include state_transition reason") + } + break + } + } + + hasHP068NoMatch := false + for _, p := range resultNoMatch.MatchedPatterns { + if p.PatternID == "HP068" { + hasHP068NoMatch = true + break + } + } + + if !hasHP068Match { + t.Error("HP068 should fire with matching state transition") + } + if hasHP068NoMatch { + t.Error("HP068 should NOT fire with non-matching state transition") + } +} + +func TestPatternEngine_MatchReasons_IncludeOperationalState(t *testing.T) { + // Verify that MatchReasons include operational_state entries + engine := NewPatternEngine() + result := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + LifecyclePhases: []string{"maintenance"}, + OperationalStates: []string{"maintenance"}, + }) + + for _, p := range result.MatchedPatterns { + if p.PatternID == "HP073" { + hasStateReason := false + for _, r := range p.MatchReasons { + if r.Type == "operational_state" && r.Tag == "maintenance" && r.Met { + hasStateReason = true + break + } + } + if !hasStateReason { + t.Error("HP073 MatchReasons should include operational_state:maintenance with Met=true") + } + return + } + } + t.Error("HP073 not found in matched patterns") +} + +func TestAllOperationalStates_Count(t *testing.T) { + states := AllOperationalStates() + if len(states) != 9 { + t.Errorf("expected 9 operational states, got %d", len(states)) + } +} + +func TestStandardStateTransitions_Valid(t *testing.T) { + transitions := StandardStateTransitions() + if len(transitions) < 15 { + t.Errorf("expected at least 15 state transitions, got %d", len(transitions)) + } + // Verify all transitions reference valid states + stateSet := make(map[string]bool) + for _, s := range AllOperationalStates() { + stateSet[s] = true + } + for _, tr := range transitions { + // Format: "from→to" (→ is multi-byte UTF-8) + parts := splitTransition(tr) + if len(parts) != 2 { + t.Errorf("invalid transition format: %q", tr) + continue + } + if !stateSet[parts[0]] { + t.Errorf("transition %q references unknown state: %q", tr, parts[0]) + } + if !stateSet[parts[1]] { + t.Errorf("transition %q references unknown state: %q", tr, parts[1]) + } + } +} + +func splitTransition(tr string) []string { + // Split on → (UTF-8: 0xE2 0x86 0x92) + idx := 0 + for i := 0; i < len(tr); i++ { + if tr[i] == 0xE2 && i+2 < len(tr) && tr[i+1] == 0x86 && tr[i+2] == 0x92 { + return []string{tr[:i], tr[i+3:]} + } + idx = i + } + _ = idx + return []string{tr} +} diff --git a/ai-compliance-sdk/internal/ucca/pattern_loader.go b/ai-compliance-sdk/internal/ucca/pattern_loader.go index b3f9d9d..7fbce53 100644 --- a/ai-compliance-sdk/internal/ucca/pattern_loader.go +++ b/ai-compliance-sdk/internal/ucca/pattern_loader.go @@ -219,9 +219,14 @@ func (idx *ControlPatternIndex) MatchByKeywords(text string) []PatternMatch { }) } - // Simple insertion sort (small N) + // Sort by keyword hits descending, then by pattern ID ascending (stable tiebreaker) for i := 1; i < len(matches); i++ { - for j := i; j > 0 && matches[j].KeywordHits > matches[j-1].KeywordHits; j-- { + for j := i; j > 0; j-- { + higher := matches[j].KeywordHits > matches[j-1].KeywordHits + sameTie := matches[j].KeywordHits == matches[j-1].KeywordHits && matches[j].Pattern.ID < matches[j-1].Pattern.ID + if !higher && !sameTie { + break + } matches[j], matches[j-1] = matches[j-1], matches[j] } }