package iace import "github.com/google/uuid" // Non-test plumbing for the offline proposer (P2 slice 3): run the engine for a // narrative and produce the fired patterns + the engine-built hazards/mitigations // the dedup proposer and GT screen consume. This is the same pipeline the GT // benchmark tests use, lifted out of test scope so the dev-time CLI can call it. // universalLifecyclePhases are appended so patterns gated to a specific lifecycle // (maintenance/cleaning/setup/fault clearing) still fire — the proposer wants the // full hazard picture, not only normal-operation hazards. var universalLifecyclePhases = []string{"normal_operation", "maintenance", "cleaning", "setup", "fault_clearing"} // BuildProposerInput parses a narrative, runs the pattern engine, keeps the // narrative-relevant patterns, and returns the hazards, mitigations and fired // patterns. NOTE: it does not apply the CE cyber-category skip, so the proposer // view may include cyber/AI hazards that the CE log excludes — harmless for the // GT recall screen (they match no CE ground-truth entry). func BuildProposerInput(narrative, machineType string, extraMachineTypes []string) ([]Hazard, []Mitigation, []PatternMatch) { res := ParseNarrative(narrative, machineType) var compIDs, compNames, energyIDs []string for _, c := range res.Components { if c.Negated { continue } compIDs = append(compIDs, c.LibraryID) compNames = append(compNames, c.NameDE) } for _, e := range res.EnergySources { energyIDs = append(energyIDs, e.SourceID) } machineTypes := append([]string{}, extraMachineTypes...) if machineType != "" { machineTypes = append(machineTypes, machineType) } lifecycles := append(append([]string{}, res.LifecyclePhases...), universalLifecyclePhases...) out := NewPatternEngine().Match(MatchInput{ ComponentLibraryIDs: compIDs, EnergySourceIDs: energyIDs, LifecyclePhases: lifecycles, CustomTags: res.CustomTags, OperationalStates: res.OperationalStates, StateTransitions: res.StateTransitions, HumanRoles: res.Roles, MachineTypes: machineTypes, }) kept := make([]PatternMatch, 0, len(out.MatchedPatterns)) for _, pm := range out.MatchedPatterns { if IsPatternRelevant(pm, narrative, compNames) { kept = append(kept, pm) } } filtered := *out filtered.MatchedPatterns = kept hazards, mits := patternsToHazardsAndMitigations(&filtered) return hazards, mits, kept } // patternsToHazardsAndMitigations converts engine output into the hazard/mitigation // entities the benchmark + proposer compare on. Simplified vs InitializeProject // (no risk estimation, no norm refs) — it only needs category/zone/scenario/measures. func patternsToHazardsAndMitigations(out *MatchOutput) ([]Hazard, []Mitigation) { hazards := make([]Hazard, 0, len(out.MatchedPatterns)) patternToHazard := make(map[string]uuid.UUID, len(out.MatchedPatterns)) for _, pm := range out.MatchedPatterns { cat := "" if len(pm.HazardCats) > 0 { cat = pm.HazardCats[0] } lifecycle := "" if len(pm.ApplicableLifecycles) > 0 { lifecycle = pm.ApplicableLifecycles[0] } h := Hazard{ ID: uuid.New(), Name: pm.ScenarioDE, Category: cat, Description: pm.ScenarioDE, Scenario: pm.ScenarioDE, TriggerEvent: pm.TriggerDE, PossibleHarm: pm.HarmDE, AffectedPerson: pm.AffectedDE, HazardousZone: pm.ZoneDE, LifecyclePhase: lifecycle, } if h.Name == "" { h.Name = pm.PatternName } hazards = append(hazards, h) patternToHazard[pm.PatternID] = h.ID } measureNames := make(map[string]string) for _, m := range GetProtectiveMeasureLibrary() { measureNames[m.ID] = m.Name } var mitigations []Mitigation for _, sm := range out.SuggestedMeasures { name := measureNames[sm.MeasureID] if name == "" { name = sm.MeasureID } for _, srcPattern := range sm.SourcePatterns { hid, ok := patternToHazard[srcPattern] if !ok { continue } mitigations = append(mitigations, Mitigation{ ID: uuid.New(), HazardID: hid, Name: name, }) } } return hazards, mitigations }