package iace import ( "context" "encoding/json" "fmt" "strings" ) // Coverage blind-spot proposer (P2 slice 6, type 4). DEV-TIME, propose-only. // // Deterministic skeleton: which EN ISO 12100 hazard groups (A-G, the classic CE // groups; H-J are control/CRA and routinely routed elsewhere) did the engine // leave with ZERO hazards for this machine? An empty group is a structural // blind-spot signal — the machine may genuinely lack that hazard, or a pattern // may be missing. The LLM then expands each gap into specific expected-but-missing // hazards a safety assessor would name, for a human to confirm into a new pattern // or GT case. The gaps alone are useful without any model. type isoGroup struct { Key string Label string Cats []string } var iso12100Groups = []isoGroup{ {"mechanical", "A. Mechanisch", []string{"mechanical_hazard", "mechanical", "maintenance_hazard"}}, {"electrical", "B. Elektrisch", []string{"electrical_hazard", "electrical", "emc_hazard"}}, {"thermal", "C. Thermisch", []string{"thermal_hazard", "thermal", "high_temperature", "fire_explosion"}}, {"pneumatic_hydraulic", "D. Pneumatik/Hydraulik", []string{"pneumatic_hydraulic"}}, {"noise_vibration", "E. Laerm/Vibration", []string{"noise_hazard", "noise_vibration", "vibration_hazard"}}, {"ergonomic", "F. Ergonomie", []string{"ergonomic_hazard", "ergonomic"}}, {"material", "G. Stoffe/Umwelt", []string{"material_environmental", "chemical_risk", "radiation_hazard"}}, } // CoverageGap is an ISO 12100 hazard group with no engine hazard. type CoverageGap struct { Group string `json:"group"` Key string `json:"key"` Note string `json:"note"` } // FindCoverageGaps returns the A-G hazard groups that produced zero hazards. func FindCoverageGaps(hazards []Hazard) []CoverageGap { present := make(map[string]bool, len(hazards)) for _, h := range hazards { present[h.Category] = true } var gaps []CoverageGap for _, g := range iso12100Groups { covered := false for _, c := range g.Cats { if present[c] { covered = true break } } if !covered { gaps = append(gaps, CoverageGap{ Group: g.Label, Key: g.Key, Note: "no engine hazard in this ISO 12100 group — verify the machine truly lacks it, or a pattern is missing", }) } } return gaps } // MissingHazard is an LLM-proposed hazard a safety assessor would expect. type MissingHazard struct { Group string `json:"group"` Hazard string `json:"hazard"` Why string `json:"why"` } // ProposeMissingHazards asks the LLM to expand the empty groups into specific // expected hazards. Returns nil without a completer or on any error — propose-only, // never breaks the run. func ProposeMissingHazards(ctx context.Context, completer LLMCompleter, machineClass, narrative string, produced []Hazard, gaps []CoverageGap) []MissingHazard { if completer == nil || len(gaps) == 0 { return nil } system, user := BuildCoveragePrompt(machineClass, narrative, produced, gaps) raw, err := completer.Complete(ctx, system, user) if err != nil { return nil } return parseMissingHazards(raw) } // BuildCoveragePrompt frames the "what is missing?" question for the LLM. func BuildCoveragePrompt(machineClass, narrative string, produced []Hazard, gaps []CoverageGap) (system, user string) { system = "Du bist Sachverstaendiger fuer Maschinensicherheit nach EN ISO 12100. " + "Dir werden eine Maschine, die bereits erkannten Gefaehrdungen und Gefaehrdungsgruppen OHNE Eintrag genannt. " + "Nenne nur Gefaehrdungen, die ein Sachverstaendiger fuer DIESE Maschine ERWARTET, die aber FEHLEN. " + "Erfinde nichts Maschinenfremdes. Antworte AUSSCHLIESSLICH als JSON-Array: " + `[{"group":"...","hazard":"...","why":"..."}].` var have []string seen := map[string]bool{} for _, h := range produced { if h.Category != "" && !seen[h.Category] { seen[h.Category] = true have = append(have, h.Category) } } var empty []string for _, g := range gaps { empty = append(empty, g.Group) } user = fmt.Sprintf("Maschinenklasse: %s\n\nBeschreibung:\n%s\n\nBereits erkannte Kategorien: %s\n\nGruppen OHNE Eintrag (Fokus): %s\n\nWelche erwarteten Gefaehrdungen fehlen?", machineClass, narrative, strings.Join(have, ", "), strings.Join(empty, ", ")) return system, user } func parseMissingHazards(raw string) []MissingHazard { start, end := strings.Index(raw, "["), strings.LastIndex(raw, "]") if start < 0 || end <= start { return nil } var out []MissingHazard if err := json.Unmarshal([]byte(raw[start:end+1]), &out); err != nil { return nil } return out } // RenderCoverageQueue renders the deterministic gaps plus any LLM-proposed missing // hazards as a markdown review queue. func RenderCoverageQueue(machine string, gaps []CoverageGap, missing []MissingHazard) string { var b strings.Builder fmt.Fprintf(&b, "# Coverage blind-spot queue — %s\n\n", machine) fmt.Fprintf(&b, "%d ISO 12100 group(s) (A-G) have no engine hazard. Propose-only — a human confirms whether the machine truly lacks it or a pattern/GT case is missing.\n\n", len(gaps)) for _, g := range gaps { fmt.Fprintf(&b, "- **%s** — %s\n", g.Group, g.Note) } if len(missing) > 0 { fmt.Fprintf(&b, "\n## LLM-proposed expected-but-missing hazards (%d)\n\n", len(missing)) for i, m := range missing { fmt.Fprintf(&b, "%d. [%s] %s\n - why: %s\n", i+1, m.Group, m.Hazard, m.Why) } } return b.String() }