feat(iace): per-category hazard caps for precision improvement
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
Build + Deploy / build-admin-compliance (push) Successful in 12s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 40s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 10s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
CI / loc-budget (push) Failing after 13s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m33s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 46s
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m15s

Add categoryHazardCap() with ISO 12100-proportional limits:
- mechanical: 3x components (min 15, max 60)
- electrical: 1x components (min 8, max 20)
- secondary (thermal, noise, material): 4-8
- software/IT/organizational: 2-5 (minimal for machinery assessment)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-13 10:00:45 +02:00
parent 977e63f372
commit 733d2bcc7b
2 changed files with 69 additions and 0 deletions
@@ -159,6 +159,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
created := 0 created := 0
seenCatZone := make(map[string]bool) seenCatZone := make(map[string]bool)
catCount := make(map[string]int)
for _, mp := range matchOutput.MatchedPatterns { for _, mp := range matchOutput.MatchedPatterns {
// Narrative relevance filter: skip patterns whose zone/scenario // Narrative relevance filter: skip patterns whose zone/scenario
// mentions machine-specific terms that don't appear in our components // mentions machine-specific terms that don't appear in our components
@@ -167,6 +168,12 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
for _, cat := range mp.HazardCats { for _, cat := range mp.HazardCats {
// Per-category cap: limit hazards per category based on relevance
maxForCat := categoryHazardCap(cat, len(comps))
if catCount[cat] >= maxForCat {
continue
}
// Dedup by category + normalized zone // Dedup by category + normalized zone
zoneKey := normalizeZoneKey(mp.ZoneDE) zoneKey := normalizeZoneKey(mp.ZoneDE)
if zoneKey == "" { if zoneKey == "" {
@@ -212,6 +219,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}) })
if cerr == nil { if cerr == nil {
created++ created++
catCount[cat]++
hazardIDsByCategory[cat] = hz.ID hazardIDsByCategory[cat] = hz.ID
} }
} }
@@ -260,6 +260,67 @@ func isPatternRelevant(mp iace.PatternMatch, narrative string, compNames []strin
return false return false
} }
// categoryHazardCap returns the maximum number of hazards to generate per category.
// Caps are based on typical ISO 12100 risk assessment proportions:
// - Core physical categories (mechanical, electrical): scale with component count
// - Secondary categories (thermal, noise, material): smaller fixed caps
// - Software/IT/organizational categories: minimal (these are usually covered by
// other standards like IEC 62443, not ISO 12100 machinery risk assessment)
func categoryHazardCap(cat string, componentCount int) int {
// Core machinery hazard categories — scale with complexity
switch cat {
case "mechanical_hazard":
// Typically 1-3 hazards per component (quetschen, scheren, stoss...)
cap := componentCount * 3
if cap < 15 {
cap = 15
}
if cap > 60 {
cap = 60
}
return cap
case "electrical_hazard":
// Typically 8-15 for a standard machine
cap := componentCount
if cap < 8 {
cap = 8
}
if cap > 20 {
cap = 20
}
return cap
case "pneumatic_hydraulic":
return 8
case "thermal_hazard":
return 6
case "noise_vibration":
return 4
case "material_environmental":
return 6
case "ergonomic", "ergonomic_hazard":
return 4
case "fire_explosion":
return 4
case "radiation_hazard", "emc_hazard":
return 3
// Software/IT/organizational — minimal for machinery assessment
case "safety_function_failure":
return 5
case "software_fault":
return 3
case "configuration_error":
return 3
case "hmi_error":
return 3
case "maintenance_hazard":
return 4
case "mode_confusion":
return 2
default:
return 3
}
}
// normalizeZoneKey reduces a zone string to its core components for better dedup. // normalizeZoneKey reduces a zone string to its core components for better dedup.
// E.g. "Schaltschrank, Sammelschiene" and "Schaltschrank-Innenraum, Sammelschienen" // E.g. "Schaltschrank, Sammelschiene" and "Schaltschrank-Innenraum, Sammelschienen"
// should dedup to the same key. // should dedup to the same key.