feat(iace): Sprint 3A — Operational State Graph + fix(ucca) flaky keyword sort
State Graph: - 9 Standard-Betriebszustaende (startup, homing, automatic_operation, manual_operation, teach_mode, maintenance, cleaning, emergency_stop, recovery_mode) - 20 State-Transitions als gerichteter Graph - OperationalStates + StateTransitions Felder in HazardPattern, MatchInput, PatternMatch - patternMatches() filtert Patterns nach Betriebszustand (nil = feuert immer) - Narrative-Parser extrahiert States aus Maschinenbeschreibung (22 Keywords + 4 Transition-Keywords) - 27 bestehende Patterns mit State-Einschraenkungen annotiert (10 operational, 15 maintenance, 2 cobot) - MatchReason um operational_state + state_transition Typen erweitert (Explainability) - 6 neue Tests: NilFiresAlways, MaintenanceFilter, StateTransition, MatchReasons, Count, TransitionValid UCCA fix: - Stabiler Tiebreaker (Pattern-ID aufsteigend) bei gleichem Keyword-Score in MatchByKeywords - Behebt flaky TestControlPatternIndex_MatchByKeywords (1/10 Failure-Rate durch Go map iteration order) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
EnergySourceIDs: energyIDs,
|
EnergySourceIDs: energyIDs,
|
||||||
LifecyclePhases: parseResult.LifecyclePhases,
|
LifecyclePhases: parseResult.LifecyclePhases,
|
||||||
CustomTags: parseResult.CustomTags,
|
CustomTags: parseResult.CustomTags,
|
||||||
|
OperationalStates: parseResult.OperationalStates,
|
||||||
|
StateTransitions: parseResult.StateTransitions,
|
||||||
})
|
})
|
||||||
steps = append(steps, InitStep{
|
steps = append(steps, InitStep{
|
||||||
Name: "Patterns abgeglichen",
|
Name: "Patterns abgeglichen",
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ func (h *IACEHandler) ParseNarrative(c *gin.Context) {
|
|||||||
EnergySourceIDs: energyIDs,
|
EnergySourceIDs: energyIDs,
|
||||||
LifecyclePhases: parseResult.LifecyclePhases,
|
LifecyclePhases: parseResult.LifecyclePhases,
|
||||||
CustomTags: parseResult.CustomTags,
|
CustomTags: parseResult.CustomTags,
|
||||||
|
OperationalStates: parseResult.OperationalStates,
|
||||||
|
StateTransitions: parseResult.StateTransitions,
|
||||||
}
|
}
|
||||||
matchOutput := engine.Match(matchInput)
|
matchOutput := engine.Match(matchInput)
|
||||||
|
|
||||||
|
|||||||
@@ -32,4 +32,14 @@ type HazardPattern struct {
|
|||||||
// project's machine_type is in this list. Prevents e.g. press-specific
|
// project's machine_type is in this list. Prevents e.g. press-specific
|
||||||
// patterns from firing for a cobot project.
|
// patterns from firing for a cobot project.
|
||||||
MachineTypes []string `json:"machine_types,omitempty"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func GetCobotHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 4, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"programmable", "force_limited"},
|
||||||
RequiredEnergyTags: []string{},
|
RequiredEnergyTags: []string{},
|
||||||
GeneratedHazardCats: []string{"safety_function_failure"},
|
GeneratedHazardCats: []string{"safety_function_failure"},
|
||||||
@@ -94,7 +94,7 @@ func GetCobotHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 3, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"moving_part", "high_force", "sensor_part"},
|
||||||
RequiredEnergyTags: []string{},
|
RequiredEnergyTags: []string{},
|
||||||
ExcludedComponentTags: []string{"collaborative_operation"},
|
ExcludedComponentTags: []string{"collaborative_operation"},
|
||||||
|
|||||||
@@ -5,70 +5,70 @@ package iace
|
|||||||
func GetMaintenanceExtPatterns() []HazardPattern {
|
func GetMaintenanceExtPatterns() []HazardPattern {
|
||||||
return []HazardPattern{
|
return []HazardPattern{
|
||||||
// — Wartung allgemein (HP700-HP709) —
|
// — 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"},
|
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||||
SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08", "E20"}, Priority: 95,
|
SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08", "E20"}, Priority: 95,
|
||||||
ScenarioDE: "Arbeit ohne Freischaltung der Maschine", TriggerDE: "Fehlende LOTO-Prozedur",
|
ScenarioDE: "Arbeit ohne Freischaltung der Maschine", TriggerDE: "Fehlende LOTO-Prozedur",
|
||||||
HarmDE: "Erfassen durch anlaufende Teile, Tod", AffectedDE: "Instandhalter", ZoneDE: "Gesamte Maschine",
|
HarmDE: "Erfassen durch anlaufende Teile, Tod", AffectedDE: "Instandhalter", ZoneDE: "Gesamte Maschine",
|
||||||
DefaultSeverity: 5, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"},
|
GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"},
|
||||||
SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 90,
|
SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 90,
|
||||||
ScenarioDE: "Gespeicherte Energie entlaedt sich bei Wartung", TriggerDE: "Nicht abgelassener Druckspeicher",
|
ScenarioDE: "Gespeicherte Energie entlaedt sich bei Wartung", TriggerDE: "Nicht abgelassener Druckspeicher",
|
||||||
HarmDE: "Unkontrollierte Bewegung, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Antriebe, Speicher",
|
HarmDE: "Unkontrollierte Bewegung, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Antriebe, Speicher",
|
||||||
DefaultSeverity: 5, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
|
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
|
||||||
ScenarioDE: "Ungeeignetes oder defektes Werkzeug", TriggerDE: "Falscher Schraubenschluessel",
|
ScenarioDE: "Ungeeignetes oder defektes Werkzeug", TriggerDE: "Falscher Schraubenschluessel",
|
||||||
HarmDE: "Abrutschen, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Wartungszugang",
|
HarmDE: "Abrutschen, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Wartungszugang",
|
||||||
DefaultSeverity: 3, DefaultExposure: 4},
|
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"},
|
RequiredComponentTags: []string{"electrical_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"electrical_hazard", "safety_function_failure"},
|
GeneratedHazardCats: []string{"electrical_hazard", "safety_function_failure"},
|
||||||
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70,
|
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70,
|
||||||
ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft",
|
ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft",
|
||||||
HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank",
|
HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank",
|
||||||
DefaultSeverity: 4, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 75,
|
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 75,
|
||||||
ScenarioDE: "Schwere Teile fallen bei Demontage herab", TriggerDE: "Fehlende Abstuetzung",
|
ScenarioDE: "Schwere Teile fallen bei Demontage herab", TriggerDE: "Fehlende Abstuetzung",
|
||||||
HarmDE: "Quetschung, Frakturen, Tod", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich",
|
HarmDE: "Quetschung, Frakturen, Tod", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich",
|
||||||
DefaultSeverity: 5, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
|
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
|
||||||
ScenarioDE: "Zurueckgelassenes Werkzeug wird Geschoss", TriggerDE: "Keine Werkzeugkontrolle",
|
ScenarioDE: "Zurueckgelassenes Werkzeug wird Geschoss", TriggerDE: "Keine Werkzeugkontrolle",
|
||||||
HarmDE: "Herausgeschleudertes Teil, Verletzungen", AffectedDE: "Bedienpersonal", ZoneDE: "Arbeitsraum",
|
HarmDE: "Herausgeschleudertes Teil, Verletzungen", AffectedDE: "Bedienpersonal", ZoneDE: "Arbeitsraum",
|
||||||
DefaultSeverity: 4, DefaultExposure: 2},
|
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"},
|
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45,
|
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45,
|
||||||
ScenarioDE: "Scharfe Kanten und Grate verletzen", TriggerDE: "Fehlende Schutzhandschuhe",
|
ScenarioDE: "Scharfe Kanten und Grate verletzen", TriggerDE: "Fehlende Schutzhandschuhe",
|
||||||
HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen",
|
HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen",
|
||||||
DefaultSeverity: 2, DefaultExposure: 4},
|
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"},
|
RequiredComponentTags: []string{"high_temperature"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60,
|
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60,
|
||||||
ScenarioDE: "Maschine nicht abgekuehlt vor Wartung", TriggerDE: "Zu kurze Abkuehlzeit",
|
ScenarioDE: "Maschine nicht abgekuehlt vor Wartung", TriggerDE: "Zu kurze Abkuehlzeit",
|
||||||
HarmDE: "Kontaktverbrennungen", AffectedDE: "Instandhalter", ZoneDE: "Heizplatten, Motorgehaeuse",
|
HarmDE: "Kontaktverbrennungen", AffectedDE: "Instandhalter", ZoneDE: "Heizplatten, Motorgehaeuse",
|
||||||
DefaultSeverity: 3, DefaultExposure: 4},
|
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"},
|
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"safety_function_failure"},
|
GeneratedHazardCats: []string{"safety_function_failure"},
|
||||||
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70,
|
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70,
|
||||||
ScenarioDE: "Wartung ohne formale Freigabe", TriggerDE: "Fehlender Erlaubnisschein",
|
ScenarioDE: "Wartung ohne formale Freigabe", TriggerDE: "Fehlender Erlaubnisschein",
|
||||||
HarmDE: "Unerwarteter Maschinenbetrieb", AffectedDE: "Instandhalter", ZoneDE: "Gesamte Maschine",
|
HarmDE: "Unerwarteter Maschinenbetrieb", AffectedDE: "Instandhalter", ZoneDE: "Gesamte Maschine",
|
||||||
DefaultSeverity: 5, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"material_environmental"},
|
GeneratedHazardCats: []string{"material_environmental"},
|
||||||
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
|
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
|
||||||
@@ -76,35 +76,35 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
|||||||
HarmDE: "Hautinfektionen, Atemwegsbeschwerden", AffectedDE: "Instandhalter", ZoneDE: "KSS-System",
|
HarmDE: "Hautinfektionen, Atemwegsbeschwerden", AffectedDE: "Instandhalter", ZoneDE: "KSS-System",
|
||||||
DefaultSeverity: 2, DefaultExposure: 3},
|
DefaultSeverity: 2, DefaultExposure: 3},
|
||||||
// — Einrichten / Umruesten (HP710-HP719) —
|
// — 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"},
|
RequiredComponentTags: []string{"programmable"}, RequiredLifecycles: []string{"setup"},
|
||||||
GeneratedHazardCats: []string{"safety_function_failure"},
|
GeneratedHazardCats: []string{"safety_function_failure"},
|
||||||
SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 75,
|
SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 75,
|
||||||
ScenarioDE: "Falsche Maschinenparameter nach Produktwechsel", TriggerDE: "Falsche Rezeptnummer",
|
ScenarioDE: "Falsche Maschinenparameter nach Produktwechsel", TriggerDE: "Falsche Rezeptnummer",
|
||||||
HarmDE: "Uebergeschwindigkeit, Werkzeugbruch", AffectedDE: "Einrichter", ZoneDE: "Bedienterminal",
|
HarmDE: "Uebergeschwindigkeit, Werkzeugbruch", AffectedDE: "Einrichter", ZoneDE: "Bedienterminal",
|
||||||
DefaultSeverity: 4, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"moving_part", "high_force"}, RequiredLifecycles: []string{"setup"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M003", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
|
SuggestedMeasureIDs: []string{"M003", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
|
||||||
ScenarioDE: "Schwere Werkzeuge manuell gewechselt", TriggerDE: "Kein Hebezeug, Finger eingeklemmt",
|
ScenarioDE: "Schwere Werkzeuge manuell gewechselt", TriggerDE: "Kein Hebezeug, Finger eingeklemmt",
|
||||||
HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme",
|
HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme",
|
||||||
DefaultSeverity: 4, DefaultExposure: 4},
|
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"},
|
RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
|
SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
|
||||||
ScenarioDE: "Testlauf mit Person im Gefahrenbereich", TriggerDE: "Volle Geschwindigkeit, kein Schutz",
|
ScenarioDE: "Testlauf mit Person im Gefahrenbereich", TriggerDE: "Volle Geschwindigkeit, kein Schutz",
|
||||||
HarmDE: "Erfassen, Quetschen", AffectedDE: "Einrichter", ZoneDE: "Arbeitsraum",
|
HarmDE: "Erfassen, Quetschen", AffectedDE: "Einrichter", ZoneDE: "Arbeitsraum",
|
||||||
DefaultSeverity: 5, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||||
SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 85,
|
SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 85,
|
||||||
ScenarioDE: "Volle Geschwindigkeit im Einrichtmodus", TriggerDE: "Defekter Betriebsartschalter",
|
ScenarioDE: "Volle Geschwindigkeit im Einrichtmodus", TriggerDE: "Defekter Betriebsartschalter",
|
||||||
HarmDE: "Erfassen bei voller Geschwindigkeit", AffectedDE: "Einrichter", ZoneDE: "Werkzeugbereich",
|
HarmDE: "Erfassen bei voller Geschwindigkeit", AffectedDE: "Einrichter", ZoneDE: "Werkzeugbereich",
|
||||||
DefaultSeverity: 5, DefaultExposure: 3},
|
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"},
|
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 55,
|
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 55,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 4, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"moving_part", "programmable"},
|
||||||
RequiredLifecycles: []string{"fault_clearing"},
|
RequiredLifecycles: []string{"fault_clearing"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||||
@@ -57,7 +57,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 5, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"stored_energy"},
|
||||||
RequiredLifecycles: []string{"maintenance", "fault_clearing"},
|
RequiredLifecycles: []string{"maintenance", "fault_clearing"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard", "electrical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard", "electrical_hazard"},
|
||||||
@@ -72,7 +72,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 4, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"moving_part"},
|
||||||
RequiredLifecycles: []string{"fault_clearing"},
|
RequiredLifecycles: []string{"fault_clearing"},
|
||||||
ExcludedComponentTags: []string{"interlocked"},
|
ExcludedComponentTags: []string{"interlocked"},
|
||||||
@@ -120,7 +120,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
// Wartung / Instandhaltung (HP073-HP079)
|
// 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"},
|
RequiredComponentTags: []string{"moving_part"},
|
||||||
RequiredLifecycles: []string{"maintenance"},
|
RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"maintenance_hazard"},
|
GeneratedHazardCats: []string{"maintenance_hazard"},
|
||||||
@@ -136,7 +136,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 5, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"structural_part"},
|
||||||
RequiredLifecycles: []string{"maintenance", "cleaning"},
|
RequiredLifecycles: []string{"maintenance", "cleaning"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
@@ -150,7 +150,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 4, DefaultExposure: 2,
|
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"},
|
RequiredComponentTags: []string{"high_temperature"},
|
||||||
RequiredLifecycles: []string{"maintenance"},
|
RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||||
@@ -165,7 +165,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 3, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"chemical_risk"},
|
||||||
RequiredLifecycles: []string{"maintenance", "cleaning"},
|
RequiredLifecycles: []string{"maintenance", "cleaning"},
|
||||||
GeneratedHazardCats: []string{"material_environmental"},
|
GeneratedHazardCats: []string{"material_environmental"},
|
||||||
@@ -179,7 +179,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 3, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"high_voltage"},
|
||||||
RequiredLifecycles: []string{"maintenance", "fault_clearing"},
|
RequiredLifecycles: []string{"maintenance", "fault_clearing"},
|
||||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||||
@@ -195,7 +195,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
DefaultSeverity: 5, DefaultExposure: 3,
|
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"},
|
RequiredComponentTags: []string{"structural_part"},
|
||||||
RequiredLifecycles: []string{"maintenance"},
|
RequiredLifecycles: []string{"maintenance"},
|
||||||
GeneratedHazardCats: []string{"ergonomic"},
|
GeneratedHazardCats: []string{"ergonomic"},
|
||||||
@@ -228,7 +228,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
|||||||
// Einrichten / Umruesten / Werkzeugwechsel (HP080-HP085)
|
// 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"},
|
RequiredComponentTags: []string{"crush_point", "high_force"},
|
||||||
RequiredLifecycles: []string{"changeover", "setup"},
|
RequiredLifecycles: []string{"changeover", "setup"},
|
||||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type ParseResult struct {
|
|||||||
CustomTags []string `json:"custom_tags"`
|
CustomTags []string `json:"custom_tags"`
|
||||||
TechSpecs []TechSpec `json:"tech_specs"`
|
TechSpecs []TechSpec `json:"tech_specs"`
|
||||||
Confidence float64 `json:"confidence"`
|
Confidence float64 `json:"confidence"`
|
||||||
|
OperationalStates []string `json:"operational_states,omitempty"`
|
||||||
|
StateTransitions []string `json:"state_transitions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// techSpecPattern matches numeric values with engineering units.
|
// techSpecPattern matches numeric values with engineering units.
|
||||||
@@ -91,6 +93,40 @@ var roleKeywords = map[string]string{
|
|||||||
"leiharbeiter": "temp_worker",
|
"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,
|
// ParseNarrative extracts components, energy sources, lifecycle phases,
|
||||||
// roles, and tags from a machine description text. Fully deterministic,
|
// roles, and tags from a machine description text. Fully deterministic,
|
||||||
// no LLM required.
|
// 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 {
|
for t := range tagSet {
|
||||||
result.CustomTags = append(result.CustomTags, t)
|
result.CustomTags = append(result.CustomTags, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Calculate overall confidence
|
// 9. Calculate overall confidence
|
||||||
if len(result.Components) > 0 {
|
if len(result.Components) > 0 {
|
||||||
result.Confidence = float64(len(result.Components)) / 15.0 // Normalize to ~1.0 for 15 components
|
result.Confidence = float64(len(result.Components)) / 15.0 // Normalize to ~1.0 for 15 components
|
||||||
if result.Confidence > 1.0 {
|
if result.Confidence > 1.0 {
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ type MatchInput struct {
|
|||||||
EnergySourceIDs []string `json:"energy_source_ids"`
|
EnergySourceIDs []string `json:"energy_source_ids"`
|
||||||
LifecyclePhases []string `json:"lifecycle_phases"`
|
LifecyclePhases []string `json:"lifecycle_phases"`
|
||||||
CustomTags []string `json:"custom_tags"`
|
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.
|
// MatchOutput contains the results of pattern matching.
|
||||||
@@ -45,6 +51,8 @@ type PatternMatch struct {
|
|||||||
HazardCats []string `json:"hazard_categories,omitempty"`
|
HazardCats []string `json:"hazard_categories,omitempty"`
|
||||||
ExpertHintDE string `json:"expert_hint_de,omitempty"`
|
ExpertHintDE string `json:"expert_hint_de,omitempty"`
|
||||||
RequiresExpert bool `json:"requires_expert,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.
|
// 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
|
evidenceSources := make(map[string][]string) // evidence ID → pattern IDs
|
||||||
|
|
||||||
for _, p := range patterns {
|
for _, p := range patterns {
|
||||||
if !patternMatches(p, tagSet, input.LifecyclePhases) {
|
if !patternMatches(p, tagSet, input) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +183,18 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
|||||||
reasons = append(reasons, MatchReason{Type: "lifecycle_match", Tag: lc, Met: found})
|
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{
|
matchedPatterns = append(matchedPatterns, PatternMatch{
|
||||||
PatternID: p.ID,
|
PatternID: p.ID,
|
||||||
@@ -192,6 +212,8 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
|||||||
HazardCats: p.GeneratedHazardCats,
|
HazardCats: p.GeneratedHazardCats,
|
||||||
ExpertHintDE: p.ExpertHintDE,
|
ExpertHintDE: p.ExpertHintDE,
|
||||||
RequiresExpert: p.RequiresExpertCalculation,
|
RequiresExpert: p.RequiresExpertCalculation,
|
||||||
|
OperationalStates: p.OperationalStates,
|
||||||
|
StateTransitions: p.StateTransitions,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, cat := range p.GeneratedHazardCats {
|
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.
|
// patternMatches checks if a pattern fires given the resolved tag set, lifecycle phases,
|
||||||
func patternMatches(p HazardPattern, tagSet map[string]bool, lifecyclePhases []string) bool {
|
// operational states, and state transitions.
|
||||||
|
func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) bool {
|
||||||
// All required component tags must be present (AND)
|
// All required component tags must be present (AND)
|
||||||
for _, t := range p.RequiredComponentTags {
|
for _, t := range p.RequiredComponentTags {
|
||||||
if !tagSet[t] {
|
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 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
|
found := false
|
||||||
phaseSet := toSet(lifecyclePhases)
|
phaseSet := toSet(input.LifecyclePhases)
|
||||||
for _, lp := range p.RequiredLifecycles {
|
for _, lp := range p.RequiredLifecycles {
|
||||||
if phaseSet[lp] {
|
if phaseSet[lp] {
|
||||||
found = true
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,3 +371,54 @@ func maxInt(a, b int) int {
|
|||||||
}
|
}
|
||||||
return b
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -215,3 +215,219 @@ func TestPatternEngine_SafetyDevice_HighPriority(t *testing.T) {
|
|||||||
t.Error("expected safety_function_failure for Not-Halt-Taster")
|
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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 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]
|
matches[j], matches[j-1] = matches[j-1], matches[j]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user