package iace import "sort" // MatchInput defines the inputs for the pattern-matching engine. type MatchInput struct { ComponentLibraryIDs []string `json:"component_library_ids"` 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"` // HumanRoles are the human roles interacting with the machine in this project. // Used to filter patterns that only apply to specific roles (e.g. programmer, maintenance_tech). HumanRoles []string `json:"human_roles,omitempty"` // FailureModes are the active failure mode IDs relevant for this project. // Used to filter patterns that require specific failure modes. FailureModes []string `json:"failure_modes,omitempty"` // MachineTypes are the industry sectors / machine types for this project. // Patterns with MachineTypes filter only fire if at least one matches. // Empty = all patterns fire (backwards compatible). MachineTypes []string `json:"machine_types,omitempty"` } // MatchOutput contains the results of pattern matching. type MatchOutput struct { MatchedPatterns []PatternMatch `json:"matched_patterns"` SuggestedHazards []HazardSuggestion `json:"suggested_hazards"` SuggestedMeasures []MeasureSuggestion `json:"suggested_measures"` SuggestedEvidence []EvidenceSuggestion `json:"suggested_evidence"` ResolvedTags []string `json:"resolved_tags"` } // MatchReason explains why a specific check passed or was relevant for a pattern match. type MatchReason struct { Type string `json:"type"` // "required_component_tag", "required_energy_tag", "lifecycle_match", "no_exclusion" Tag string `json:"tag"` Met bool `json:"met"` } // PatternMatch records which pattern fired and why. type PatternMatch struct { PatternID string `json:"pattern_id"` PatternName string `json:"pattern_name"` Priority int `json:"priority"` MatchedTags []string `json:"matched_tags"` // Explainability: structured reasons why this pattern fired MatchReasons []MatchReason `json:"match_reasons,omitempty"` // Detail fields from the pattern definition ScenarioDE string `json:"scenario_de,omitempty"` TriggerDE string `json:"trigger_de,omitempty"` HarmDE string `json:"harm_de,omitempty"` AffectedDE string `json:"affected_de,omitempty"` 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"` OperationalStates []string `json:"operational_states,omitempty"` StateTransitions []string `json:"state_transitions,omitempty"` HumanRoles []string `json:"human_roles,omitempty"` GeneratedHazardType string `json:"generated_hazard_type,omitempty"` MatchedFailureModes []string `json:"matched_failure_modes,omitempty"` } // HazardSuggestion is a suggested hazard from pattern matching. type HazardSuggestion struct { Category string `json:"category"` SourcePatterns []string `json:"source_patterns"` Confidence float64 `json:"confidence"` } // MeasureSuggestion is a suggested protective measure from pattern matching. type MeasureSuggestion struct { MeasureID string `json:"measure_id"` SourcePatterns []string `json:"source_patterns"` } // EvidenceSuggestion is a suggested evidence type from pattern matching. type EvidenceSuggestion struct { EvidenceID string `json:"evidence_id"` SourcePatterns []string `json:"source_patterns"` } // PatternEngine evaluates hazard patterns against resolved tags. type PatternEngine struct { resolver *TagResolver patterns []HazardPattern } // NewPatternEngine creates a PatternEngine with all pattern sources and resolver. // Pattern registration is in pattern_registry.go (collectAllPatterns). func NewPatternEngine() *PatternEngine { return &PatternEngine{ resolver: NewTagResolver(), patterns: collectAllPatterns(), } } // Match executes the pattern-matching algorithm: // 1. Resolve component + energy IDs → tags // 2. Merge with custom tags // 3. Sort patterns by priority (descending) // 4. For each pattern: check required_tags (AND) and excluded_tags (NOT) // 5. Collect hazards, measures, evidence from fired patterns (deduplicated) // 6. Calculate confidence scores func (e *PatternEngine) Match(input MatchInput) *MatchOutput { // Step 1+2: resolve all tags allTags := e.resolver.ResolveTags(input.ComponentLibraryIDs, input.EnergySourceIDs, input.CustomTags) if len(allTags) == 0 { return &MatchOutput{ResolvedTags: []string{}} } tagSet := toSet(allTags) // Step 3: sort patterns by priority descending patterns := make([]HazardPattern, len(e.patterns)) copy(patterns, e.patterns) sort.Slice(patterns, func(i, j int) bool { return patterns[i].Priority > patterns[j].Priority }) // Step 4+5: evaluate each pattern var matchedPatterns []PatternMatch hazardCatSources := make(map[string][]string) // category → pattern IDs measureSources := make(map[string][]string) // measure ID → pattern IDs evidenceSources := make(map[string][]string) // evidence ID → pattern IDs for _, p := range patterns { if !patternMatches(p, tagSet, input) { continue } // Collect the tags that contributed + build explainability reasons var matchedTags []string var reasons []MatchReason for _, t := range p.RequiredComponentTags { if tagSet[t] { matchedTags = append(matchedTags, t) reasons = append(reasons, MatchReason{Type: "required_component_tag", Tag: t, Met: true}) } } for _, t := range p.RequiredEnergyTags { if tagSet[t] { matchedTags = append(matchedTags, t) reasons = append(reasons, MatchReason{Type: "required_energy_tag", Tag: t, Met: true}) } } for _, t := range p.ExcludedComponentTags { reasons = append(reasons, MatchReason{Type: "no_exclusion", Tag: t, Met: !tagSet[t]}) } if len(p.RequiredLifecycles) > 0 { for _, lc := range p.RequiredLifecycles { found := false for _, ilc := range input.LifecyclePhases { if ilc == lc { found = true break } } 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]}) } } if len(p.HumanRoles) > 0 { roleSet := toSet(input.HumanRoles) for _, r := range p.HumanRoles { reasons = append(reasons, MatchReason{Type: "human_role", Tag: r, Met: roleSet[r]}) } } var matchedFMs []string if len(p.RequiredFailureModes) > 0 { fmSet := toSet(input.FailureModes) for _, fm := range p.RequiredFailureModes { met := fmSet[fm] reasons = append(reasons, MatchReason{Type: "failure_mode", Tag: fm, Met: met}) if met { matchedFMs = append(matchedFMs, fm) } } } matchedPatterns = append(matchedPatterns, PatternMatch{ PatternID: p.ID, PatternName: p.NameDE, Priority: p.Priority, MatchedTags: matchedTags, MatchReasons: reasons, ScenarioDE: p.ScenarioDE, TriggerDE: p.TriggerDE, HarmDE: p.HarmDE, AffectedDE: p.AffectedDE, ZoneDE: p.ZoneDE, DefaultSeverity: p.DefaultSeverity, DefaultExposure: p.DefaultExposure, HazardCats: p.GeneratedHazardCats, ExpertHintDE: p.ExpertHintDE, RequiresExpert: p.RequiresExpertCalculation, OperationalStates: p.OperationalStates, StateTransitions: p.StateTransitions, HumanRoles: p.HumanRoles, GeneratedHazardType: p.GeneratedHazardType, MatchedFailureModes: matchedFMs, }) for _, cat := range p.GeneratedHazardCats { hazardCatSources[cat] = appendUnique(hazardCatSources[cat], p.ID) } for _, mid := range p.SuggestedMeasureIDs { measureSources[mid] = appendUnique(measureSources[mid], p.ID) } for _, eid := range p.SuggestedEvidenceIDs { evidenceSources[eid] = appendUnique(evidenceSources[eid], p.ID) } } // Step 6: build output with confidence scores totalPatterns := len(patterns) firedCount := len(matchedPatterns) var suggestedHazards []HazardSuggestion for cat, sources := range hazardCatSources { confidence := float64(len(sources)) / float64(maxInt(firedCount, 1)) if confidence > 1.0 { confidence = 1.0 } suggestedHazards = append(suggestedHazards, HazardSuggestion{ Category: cat, SourcePatterns: sources, Confidence: confidence, }) } // Sort by confidence descending sort.Slice(suggestedHazards, func(i, j int) bool { return suggestedHazards[i].Confidence > suggestedHazards[j].Confidence }) var suggestedMeasures []MeasureSuggestion for mid, sources := range measureSources { suggestedMeasures = append(suggestedMeasures, MeasureSuggestion{ MeasureID: mid, SourcePatterns: sources, }) } sort.Slice(suggestedMeasures, func(i, j int) bool { return suggestedMeasures[i].MeasureID < suggestedMeasures[j].MeasureID }) var suggestedEvidence []EvidenceSuggestion for eid, sources := range evidenceSources { suggestedEvidence = append(suggestedEvidence, EvidenceSuggestion{ EvidenceID: eid, SourcePatterns: sources, }) } sort.Slice(suggestedEvidence, func(i, j int) bool { return suggestedEvidence[i].EvidenceID < suggestedEvidence[j].EvidenceID }) _ = totalPatterns // used conceptually for confidence normalization return &MatchOutput{ MatchedPatterns: matchedPatterns, SuggestedHazards: suggestedHazards, SuggestedMeasures: suggestedMeasures, SuggestedEvidence: suggestedEvidence, ResolvedTags: allTags, } } // 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 { // If pattern requires specific machine types, project must match at least one. // Patterns without MachineTypes fire for ALL projects (backwards compatible). if len(p.MachineTypes) > 0 && len(input.MachineTypes) > 0 { found := false mtSet := toSet(input.MachineTypes) for _, mt := range p.MachineTypes { if mtSet[mt] { found = true break } } if !found { return false } } // All required component tags must be present (AND) for _, t := range p.RequiredComponentTags { if !tagSet[t] { return false } } // All required energy tags must be present (AND) for _, t := range p.RequiredEnergyTags { if !tagSet[t] { return false } } // None of the excluded tags may be present (NOT) for _, t := range p.ExcludedComponentTags { if tagSet[t] { return false } } // If pattern requires specific lifecycle phases, at least one must match if len(p.RequiredLifecycles) > 0 && len(input.LifecyclePhases) > 0 { found := false phaseSet := toSet(input.LifecyclePhases) for _, lp := range p.RequiredLifecycles { if phaseSet[lp] { found = true break } } if !found { return false } } // 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 } } // If pattern requires specific human roles, at least one must match. if len(p.HumanRoles) > 0 && len(input.HumanRoles) > 0 { found := false roleSet := toSet(input.HumanRoles) for _, r := range p.HumanRoles { if roleSet[r] { found = true break } } if !found { return false } } // If pattern requires specific failure modes, at least one must match. if len(p.RequiredFailureModes) > 0 && len(input.FailureModes) > 0 { found := false fmSet := toSet(input.FailureModes) for _, fm := range p.RequiredFailureModes { if fmSet[fm] { found = true break } } if !found { return false } } return true } // appendUnique appends val to slice if not already present. func appendUnique(slice []string, val string) []string { for _, s := range slice { if s == val { return slice } } return append(slice, val) } // maxInt returns the larger of a and b. func maxInt(a, b int) int { if a > b { return a } 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", } }