package iace import "strings" // Capability-Domain-Gating — the cure for cross-domain leakage. // // Many domain-specific hazard patterns were authored gated only by a GENERIC // capability tag (e.g. "rotating_part"), so they fire for every machine that // has rotating parts — a lift, a robot cell — even though the hazard belongs to // a press, a spinning machine or a PV array. This is the precision-killing // inverse of ghost patterns; both stem from inconsistent applicability. // // The fix is capability-driven (NOT a machine-type whitelist hack): a pattern // whose OWN scenario text names a foreign machine gets that domain's capability // tag appended to its RequiredComponentTags. The same tag is emitted by the // domain's narrative keywords (keyword_dictionary.go), so the pattern still // fires for its real domain but no longer leaks into unrelated machines. // // INVARIANT: every tag below MUST be emittable via keyword_dictionary.go, // otherwise the gated pattern becomes a ghost. TestTagVocabulary_GhostPatterns // is the regression guard for this. // domainGateTerms maps a machine-betraying term (umlaut-normalised, lowercase) // to the domain capability tag that gates patterns mentioning it. var domainGateTerms = map[string]string{ // Pressen / Stanzen / Umformen "stanzhub": "dom_press", "pressenhub": "dom_press", "pressenstoessel": "dom_press", "dauerhub": "dom_press", "exzenterpresse": "dom_press", "beinpresse": "dom_press", "stanzpresse": "dom_press", "umformpresse": "dom_press", // Kunststoff / Spritzguss / Extrusion "spritzgie": "dom_plastics", "extruder": "dom_plastics", "extrusion": "dom_plastics", "kunststoffschmelze": "dom_plastics", "schliesseinheit": "dom_plastics", // Walzen / Kalander / Laminieren "walzenspalt": "dom_rolling", "zweiwalzenwerk": "dom_rolling", "kalander": "dom_rolling", "walzwerk": "dom_rolling", "laminieranlage": "dom_rolling", "laminier": "dom_rolling", // Textil "spinnmaschine": "dom_textile", "webmaschine": "dom_textile", "spinnerei": "dom_textile", // Schleifen "schleifscheibe": "dom_grinding", "schleifbock": "dom_grinding", // Schweissen "widerstandsschweiss": "dom_welding", "lichtbogenschweiss": "dom_welding", "schutzgasschweiss": "dom_welding", // Solar / PV "pv-modul": "dom_solar", "photovoltaik": "dom_solar", "pv-anlage": "dom_solar", "dc-steckverbindung": "dom_solar", "solarmodul": "dom_solar", // Windkraft "gondel": "dom_wind", "rotorblatt": "dom_wind", "windenergieanlage": "dom_wind", // CNC / Zerspanung "drehmaschine": "dom_cnc", "fraesmaschine": "dom_cnc", // Landwirtschaft "maehdrescher": "dom_agri", "ballenpresse": "dom_agri", "feldhaecksler": "dom_agri", // Roll-/Fahrtreppe "rolltreppe": "dom_escalator", "fahrtreppe": "dom_escalator", } // applyDomainGates appends a domain capability tag to every pattern whose own // text betrays that domain, so domain-specific hazards stop leaking into // unrelated machines. Idempotent; safe to run once after pattern collection. func applyDomainGates(patterns []HazardPattern) []HazardPattern { for i := range patterns { text := normalizeGateText(patterns[i].NameDE + " " + patterns[i].ScenarioDE + " " + patterns[i].TriggerDE + " " + patterns[i].HarmDE) present := make(map[string]bool, len(patterns[i].RequiredComponentTags)) for _, t := range patterns[i].RequiredComponentTags { present[t] = true } for term, tag := range domainGateTerms { if present[tag] { continue } if strings.Contains(text, term) { patterns[i].RequiredComponentTags = append(patterns[i].RequiredComponentTags, tag) present[tag] = true } } } return patterns } // normalizeGateText lowercases and folds umlauts, matching keyword_dictionary's // normalisation so gate terms and emit keywords use one vocabulary. func normalizeGateText(s string) string { s = strings.ToLower(s) s = strings.ReplaceAll(s, "ä", "ae") s = strings.ReplaceAll(s, "ö", "oe") s = strings.ReplaceAll(s, "ü", "ue") s = strings.ReplaceAll(s, "ß", "ss") return s }