Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/pattern_engine.go
T
Benjamin Admin 05839e36aa
Build + Deploy / build-admin-compliance (push) Successful in 9s
Build + Deploy / build-backend-compliance (push) Successful in 8s
Build + Deploy / build-ai-sdk (push) Successful in 37s
Build + Deploy / build-developer-portal (push) Successful in 7s
Build + Deploy / build-tts (push) Successful in 7s
Build + Deploy / build-document-crawler (push) Successful in 8s
Build + Deploy / build-dsms-gateway (push) Successful in 7s
Build + Deploy / build-dsms-node (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m55s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 49s
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Successful in 32s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 16s
Build + Deploy / trigger-orca (push) Successful in 2m11s
feat: Hazard-Patterns auf 475 erweitert (Ziel: 1000)
8 neue Pattern-Dateien fuer:
- Aufzuege (25), AGV/Landmaschinen (30), Lebensmittel/Verpackung (35)
- Laser/Medizin/Druck (40), Bau/Krane (20), Forst/Foerderer (31)
- Kunststoff/Metall (30), Schweissen/Glas/Textil (30)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 13:31:23 +02:00

278 lines
9.3 KiB
Go

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"`
// 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"`
}
// 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.
func NewPatternEngine() *PatternEngine {
// Combine all pattern sources
patterns := GetBuiltinHazardPatterns() // HP001-HP044
patterns = append(patterns, GetExtendedHazardPatterns()...) // HP045+ from rule library
patterns = append(patterns, GetPressHazardPatterns()...) // HP045-HP058 press-specific
patterns = append(patterns, GetCobotHazardPatterns()...) // HP059-HP065 cobot-specific
patterns = append(patterns, GetOperationalHazardPatterns()...) // HP066-HP093 operational states
patterns = append(patterns, GetDGUVExtendedPatterns()...) // HP094-HP133 DGUV themes
patterns = append(patterns, GetExtendedHazardPatterns2()...) // HP134-HP173 additional hazards
patterns = append(patterns, GetElevatorPatterns()...) // HP174-HP198 elevator/lift
patterns = append(patterns, GetAGVAgriPatterns()...) // HP199-HP228 AGV + agricultural
patterns = append(patterns, GetFoodPkgPatterns()...) // HP300-HP334 food + packaging
patterns = append(patterns, GetLaserMedicalPatterns()...) // HP335-HP374 laser + medical + pressure
patterns = append(patterns, GetConstructionPatterns()...) // HP400-HP419 construction/crane
patterns = append(patterns, GetForestryConveyorPatterns()...) // HP420-HP450 forestry/conveyor
patterns = append(patterns, GetPlasticsMetalPatterns()...) // HP500-HP529 plastics + metalworking
patterns = append(patterns, GetWeldingGlassTextilePatterns()...) // HP530-HP559 welding + glass + textile
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,
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,
})
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
}