fix(iace): pattern-specific measures take priority over category fallback

Each hazard now gets measures from its SOURCE PATTERN first
(SuggestedMeasureIDs), then category fallback for remaining slots.

Previously all mechanical hazards got the same generic top-5 measures
(Gefahrstelle eliminieren, Sicherheitsabstaende, Scharfe Kanten...).
Now a KSS-Schlauch hazard gets M420 (Druckfeste Auslegung) first.

SuggestedMeasureIDs added to PatternMatch struct and passed through
from pattern definition to hazard creation to measure assignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-15 14:17:32 +02:00
parent 958c03ab40
commit 2b5376ed54
2 changed files with 36 additions and 27 deletions
@@ -140,6 +140,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
existingHazards, _ := h.store.ListHazards(ctx, projectID)
hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"}
hazardIDsByCategory := make(map[string][]uuid.UUID)
hazardPatternMeasures := make(map[uuid.UUID][]string)
if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
comps, _ := h.store.ListComponents(ctx, projectID)
@@ -226,6 +227,10 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
created++
catCount[cat]++
hazardIDsByCategory[cat] = append(hazardIDsByCategory[cat], hz.ID)
// Remember this pattern's suggested measures for this hazard
if len(mp.SuggestedMeasureIDs) > 0 {
hazardPatternMeasures[hz.ID] = mp.SuggestedMeasureIDs
}
}
}
}
@@ -266,42 +271,44 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}
// For each hazard: assign up to maxMitigationsPerHazard measures
// Priority: pattern-suggested first, then category fallback
suggestedByMeasCat := make(map[string][]iace.ProtectiveMeasureEntry)
for _, sm := range matchOutput.SuggestedMeasures {
if entry, ok := measureByID[sm.MeasureID]; ok {
suggestedByMeasCat[entry.HazardCategory] = append(suggestedByMeasCat[entry.HazardCategory], entry)
}
}
// Priority 1: Pattern-specific SuggestedMeasureIDs (from the pattern that created this hazard)
// Priority 2: Category fallback (generic measures for the hazard category)
for _, hazID := range allHazardIDs {
hazCat := hazardCatByID[hazID]
measCat := patternCatToMeasureCat(hazCat)
added := 0
usedIDs := make(map[string]bool)
// First: pattern-suggested measures for this category
for _, entry := range suggestedByMeasCat[measCat] {
if added >= maxMitigationsPerHazard {
break
}
rt := iace.ReductionType(entry.ReductionType)
if rt == "" {
rt = iace.ReductionTypeInformation
}
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
HazardID: hazID, ReductionType: rt,
Name: entry.Name, Description: entry.Description,
})
if cerr == nil {
created++
added++
// Priority 1: Pattern-specific measures
if patternMIDs, ok := hazardPatternMeasures[hazID]; ok {
for _, mid := range patternMIDs {
if added >= maxMitigationsPerHazard {
break
}
entry, ok := measureByID[mid]
if !ok {
continue
}
rt := iace.ReductionType(entry.ReductionType)
if rt == "" {
rt = iace.ReductionTypeInformation
}
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
HazardID: hazID, ReductionType: rt,
Name: entry.Name, Description: entry.Description,
})
if cerr == nil {
created++
added++
usedIDs[mid] = true
}
}
}
// Then: category fallback if still under limit
// Priority 2: Category fallback (skip already-used IDs)
for _, m := range measuresByCat[measCat] {
if added >= maxMitigationsPerHazard {
break
if added >= maxMitigationsPerHazard || usedIDs[m.ID] {
continue
}
rt := iace.ReductionType(m.ReductionType)
if rt == "" {