package handlers import ( "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/google/uuid" ) // componentReviewDecision captures an expert's manual review of an auto-detected // component (presence move, CE marking, safety relevance) so a force re-seed can // restore it instead of wiping it. Keyed by normalised component name — the // deterministic narrative parser re-derives the same names. type componentReviewDecision struct { presence string ceMarked bool safetyRelevant bool } // resolvePresence returns the presence_status for a freshly parsed component: // the expert's prior decision wins; otherwise the engine's negation verdict // (negated → nicht_vorhanden, else vorhanden). func resolvePresence(dec componentReviewDecision, hasDecision, negated bool) string { if hasDecision && dec.presence != "" { return dec.presence } if negated { return iace.PresenceAbsent } return iace.PresencePresent } // pickComponentForPattern links a matched hazard pattern to the project // component that most plausibly causes it. // // matchedTags are the component/energy tags that actually made the pattern fire // (RequiredComponentTags + RequiredEnergyTags satisfied by the machine). The // project component whose library tags overlap them best is treated as the // cause. Falls back to zone-name overlap, then to the default (first) component. // // Without this, every hazard defaulted to the first component, so the knowledge // graph drew all "erzeugt" edges from a single node. func pickComponentForPattern( matchedTags []string, zoneDE string, parseComps []iace.ComponentMatch, compByName map[string]uuid.UUID, defaultCompID uuid.UUID, ) uuid.UUID { if len(matchedTags) > 0 { want := make(map[string]bool, len(matchedTags)) for _, t := range matchedTags { want[t] = true } bestScore := 0 bestID := uuid.Nil for _, c := range parseComps { id, ok := compByName[iace.NormalizeDEPublic(c.NameDE)] if !ok { continue } score := 0 for _, t := range c.Tags { if want[t] { score++ } } if score > bestScore { bestScore = score bestID = id } } if bestScore > 0 { return bestID } } if zoneDE != "" { zoneNorm := iace.NormalizeDEPublic(zoneDE) for cName, cID := range compByName { if containsSubstring(zoneNorm, cName) || containsSubstring(cName, zoneNorm) { return cID } } } return defaultCompID }