feat(iace): add hazard-matching-engine with component library, tag system, and pattern engine
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s
Implements Phases 1-4 of the IACE Hazard-Matching-Engine: - 120 machine components (C001-C120) in 11 categories - 20 energy sources (EN01-EN20) - ~85 tag taxonomy across 5 domains - 44 hazard patterns with AND/NOT matching logic - Pattern engine with tag resolution and confidence scoring - 8 new API endpoints (component-library, energy-sources, tags, patterns, match/apply) - Completeness gate G09 for pattern matching - 320 tests passing (36 new) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
240
ai-compliance-sdk/internal/iace/pattern_engine.go
Normal file
240
ai-compliance-sdk/internal/iace/pattern_engine.go
Normal file
@@ -0,0 +1,240 @@
|
||||
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 patterns and resolver.
|
||||
func NewPatternEngine() *PatternEngine {
|
||||
return &PatternEngine{
|
||||
resolver: NewTagResolver(),
|
||||
patterns: GetBuiltinHazardPatterns(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user