8440ddfecb
Makes the offline proposer runnable end-to-end.
- BuildProposerInput (proposer_input.go): non-test engine->hazards path. The
PatternMatch->Hazard converter is lifted out of the GT test files into
production scope so both the tests and the CLI share one pipeline.
- iace-audit propose <narrative.json> [<ground-truth.json>]: detect candidates ->
GT-screen survivors (when a ground truth is given) -> judge (HeuristicJudge by
default, LLMJudge over ollama when IACE_PROPOSE_LLM=1) -> write the human-review
queue to audit-reports/proposals.{md,json}. Propose-only.
Smoke run on a dishwasher narrative: 32 fired -> 3 candidates -> queue with a
confident duplicate, a confident distinct, and one punted to the LLM judge; GT
wall recall-safe. Live qwen is opt-in via env; the heuristic default keeps the
tool runnable (and CI deterministic) without a model. Proposal types 2-4
(foreign-framing gates, vocab->tag, coverage blind spots) remain for slice 4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
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
|
|
}
|