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"` } // 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"` } // 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"` } // 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 built-in + extended patterns and resolver. func NewPatternEngine() *PatternEngine { // Combine built-in (HP001-HP044) and extended (HP045+) patterns patterns := GetBuiltinHazardPatterns() patterns = append(patterns, GetExtendedHazardPatterns()...) return &PatternEngine{ resolver: NewTagResolver(), patterns: patterns, } } // 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.LifecyclePhases) { continue } // Collect the tags that contributed to this match var matchedTags []string for _, t := range p.RequiredComponentTags { if tagSet[t] { matchedTags = append(matchedTags, t) } } for _, t := range p.RequiredEnergyTags { if tagSet[t] { matchedTags = append(matchedTags, t) } } matchedPatterns = append(matchedPatterns, PatternMatch{ PatternID: p.ID, PatternName: p.NameDE, Priority: p.Priority, MatchedTags: matchedTags, }) 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 and lifecycle phases. func patternMatches(p HazardPattern, tagSet map[string]bool, lifecyclePhases []string) bool { // 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(lifecyclePhases) > 0 { found := false phaseSet := toSet(lifecyclePhases) for _, lp := range p.RequiredLifecycles { if phaseSet[lp] { 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 }