From 733d2bcc7b7aa51635cb0815e76747f469e7a66b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 13 May 2026 10:00:45 +0200 Subject: [PATCH] feat(iace): per-category hazard caps for precision improvement 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) --- .../api/handlers/iace_handler_init.go | 8 +++ .../api/handlers/iace_handler_init_helpers.go | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go index e255034..daf0368 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -159,6 +159,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { created := 0 seenCatZone := make(map[string]bool) + catCount := make(map[string]int) for _, mp := range matchOutput.MatchedPatterns { // Narrative relevance filter: skip patterns whose zone/scenario // 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 { + // 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 zoneKey := normalizeZoneKey(mp.ZoneDE) if zoneKey == "" { @@ -212,6 +219,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { }) if cerr == nil { created++ + catCount[cat]++ hazardIDsByCategory[cat] = hz.ID } } diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go index 0f35569..f32fa7b 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go @@ -260,6 +260,67 @@ func isPatternRelevant(mp iace.PatternMatch, narrative string, compNames []strin 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. // E.g. "Schaltschrank, Sammelschiene" and "Schaltschrank-Innenraum, Sammelschienen" // should dedup to the same key.