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:
217
ai-compliance-sdk/internal/iace/pattern_engine_test.go
Normal file
217
ai-compliance-sdk/internal/iace/pattern_engine_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package iace
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPatternEngine_RobotArm_MechanicalHazards(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C001"}, // Roboterarm: moving_part, rotating_part, high_force
|
||||
EnergySourceIDs: []string{"EN01", "EN02"}, // kinetic translational + rotational
|
||||
})
|
||||
|
||||
if len(result.MatchedPatterns) == 0 {
|
||||
t.Fatal("expected matched patterns for robot arm + kinetic energy")
|
||||
}
|
||||
|
||||
// Should match mechanical hazard patterns (HP001, HP002, HP006)
|
||||
hasMechHazard := false
|
||||
for _, h := range result.SuggestedHazards {
|
||||
if h.Category == "mechanical_hazard" {
|
||||
hasMechHazard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasMechHazard {
|
||||
t.Error("expected mechanical_hazard in suggested hazards for robot arm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_Schaltschrank_ElectricalHazards(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C061"}, // Schaltschrank: high_voltage, electrical_part
|
||||
EnergySourceIDs: []string{"EN04"}, // Elektrische Energie
|
||||
})
|
||||
|
||||
if len(result.MatchedPatterns) == 0 {
|
||||
t.Fatal("expected matched patterns for control cabinet + electrical energy")
|
||||
}
|
||||
|
||||
hasElecHazard := false
|
||||
for _, h := range result.SuggestedHazards {
|
||||
if h.Category == "electrical_hazard" {
|
||||
hasElecHazard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasElecHazard {
|
||||
t.Error("expected electrical_hazard in suggested hazards for Schaltschrank")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_SPS_Switch_SoftwareCyberHazards(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C071", "C111"}, // SPS + Switch: has_software, programmable, networked, it_component
|
||||
EnergySourceIDs: []string{"EN18"}, // Cyber/Data energy
|
||||
})
|
||||
|
||||
if len(result.MatchedPatterns) == 0 {
|
||||
t.Fatal("expected matched patterns for SPS + Switch + cyber energy")
|
||||
}
|
||||
|
||||
categories := make(map[string]bool)
|
||||
for _, h := range result.SuggestedHazards {
|
||||
categories[h.Category] = true
|
||||
}
|
||||
|
||||
if !categories["software_fault"] {
|
||||
t.Error("expected software_fault hazard for SPS")
|
||||
}
|
||||
if !categories["unauthorized_access"] {
|
||||
t.Error("expected unauthorized_access hazard for networked components + cyber energy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_AIModule_AISpecificHazards(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C119"}, // KI-Inferenzmodul: has_ai, has_software, networked
|
||||
EnergySourceIDs: []string{"EN19", "EN18"}, // AI model + cyber energy
|
||||
})
|
||||
|
||||
if len(result.MatchedPatterns) == 0 {
|
||||
t.Fatal("expected matched patterns for AI inference module")
|
||||
}
|
||||
|
||||
categories := make(map[string]bool)
|
||||
for _, h := range result.SuggestedHazards {
|
||||
categories[h.Category] = true
|
||||
}
|
||||
|
||||
if !categories["false_classification"] {
|
||||
t.Error("expected false_classification hazard for AI module")
|
||||
}
|
||||
if !categories["model_drift"] {
|
||||
t.Error("expected model_drift hazard for AI module")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_EmptyInput_EmptyResults(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{})
|
||||
|
||||
if len(result.MatchedPatterns) != 0 {
|
||||
t.Errorf("expected no matched patterns for empty input, got %d", len(result.MatchedPatterns))
|
||||
}
|
||||
if len(result.SuggestedHazards) != 0 {
|
||||
t.Errorf("expected no suggested hazards for empty input, got %d", len(result.SuggestedHazards))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_ExclusionTags(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
|
||||
// HP029 requires has_software+programmable, excludes has_ai
|
||||
// C071 (SPS) has has_software+programmable but NOT has_ai → should match HP029
|
||||
result1 := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C071"}, // SPS
|
||||
})
|
||||
hasHP029 := false
|
||||
for _, p := range result1.MatchedPatterns {
|
||||
if p.PatternID == "HP029" {
|
||||
hasHP029 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasHP029 {
|
||||
t.Error("HP029 should match for SPS (has_software+programmable, no has_ai)")
|
||||
}
|
||||
|
||||
// C119 (KI-Inferenzmodul) has has_ai → HP029 should be excluded
|
||||
result2 := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C119"}, // KI-Inferenzmodul: has_ai
|
||||
})
|
||||
hasHP029_2 := false
|
||||
for _, p := range result2.MatchedPatterns {
|
||||
if p.PatternID == "HP029" {
|
||||
hasHP029_2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasHP029_2 {
|
||||
t.Error("HP029 should NOT match for AI module (excluded by has_ai tag)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_HydraulicPatterns(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C042"}, // Hydraulikzylinder
|
||||
EnergySourceIDs: []string{"EN05"}, // Hydraulische Energie
|
||||
})
|
||||
|
||||
hasHydraulicHazard := false
|
||||
for _, h := range result.SuggestedHazards {
|
||||
if h.Category == "pneumatic_hydraulic" {
|
||||
hasHydraulicHazard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasHydraulicHazard {
|
||||
t.Error("expected pneumatic_hydraulic hazard for hydraulic cylinder + hydraulic energy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_MeasuresAndEvidence(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C001"}, // Roboterarm
|
||||
EnergySourceIDs: []string{"EN01"}, // Kinetische Energie
|
||||
})
|
||||
|
||||
if len(result.SuggestedMeasures) == 0 {
|
||||
t.Error("expected suggested measures for robot arm")
|
||||
}
|
||||
if len(result.SuggestedEvidence) == 0 {
|
||||
t.Error("expected suggested evidence for robot arm")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_ResolvedTags(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C001"},
|
||||
EnergySourceIDs: []string{"EN01"},
|
||||
CustomTags: []string{"my_custom_tag"},
|
||||
})
|
||||
|
||||
tagSet := toSet(result.ResolvedTags)
|
||||
if !tagSet["moving_part"] {
|
||||
t.Error("expected 'moving_part' in resolved tags")
|
||||
}
|
||||
if !tagSet["kinetic"] {
|
||||
t.Error("expected 'kinetic' in resolved tags")
|
||||
}
|
||||
if !tagSet["my_custom_tag"] {
|
||||
t.Error("expected 'my_custom_tag' in resolved tags")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternEngine_SafetyDevice_HighPriority(t *testing.T) {
|
||||
engine := NewPatternEngine()
|
||||
result := engine.Match(MatchInput{
|
||||
ComponentLibraryIDs: []string{"C101"}, // Not-Halt-Taster: safety_device, emergency_stop
|
||||
})
|
||||
|
||||
hasSafetyFail := false
|
||||
for _, h := range result.SuggestedHazards {
|
||||
if h.Category == "safety_function_failure" {
|
||||
hasSafetyFail = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSafetyFail {
|
||||
t.Error("expected safety_function_failure for Not-Halt-Taster")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user