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:
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user