Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/pattern_domain_gates.go
T
Benjamin Admin afb3f83f30 feat(iace): cross-domain precision overhaul + component review + schema reconcile
Engine precision (stop foreign-machine patterns leaking into a project):
- Wire project.MachineType into the engine machine-type gate (empty input no
  longer fires every machine class — press/cnc/excavator/crane/medical...).
- Capability-domain gating extended by 7 domains (outdoor, ventilation,
  machining, bulk, palletizer, playground, fitness) so domain-specific hazards
  only fire when the narrative names that domain; emitted via keyword_dictionary.
- Relevance backstop moved into iace (single gating contract, testable), and its
  dominant false-anchor class removed (a long pattern word no longer matches a
  short common token; prepositions/leitung added to the generic stoplist).
- New guard tests: TestCrossDomainPrecision (full pipeline, 0 foreign per GT) and
  TestPatternReachability now asserts 0 dead patterns. Both GTs keep coverage 1.0.

Reachability fix: the 51 dead patterns required electrical/pneumatic/hydraulic
tags nothing produced — renamed to the canonical electrical_energy/
pneumatic_pressure/hydraulic_pressure/hydraulic_part.

Component review (negation is best-effort + expert-correctable):
- Parser surfaces negated components (ComponentMatch.Negated) instead of dropping
  them; negated contribute no tags/energy → no phantom hazards.
- presence_status (vorhanden|nicht_vorhanden|geloescht) + ce_marked on components;
  only `vorhanden` feed matching. CE+safety-relevant flags the PL/SIL obligation.
- Force re-seed preserves the expert's component decisions instead of wiping them.
- Tag-based component→hazard assignment (was: all on the first component).
- Negation-aware narrative parsing ("keine Pneumatik" no longer extracts it).

Local-dev DB: ai-sdk sets search_path=compliance,core,public; reconcile migrations
152-156 bring the consolidated local iace tables to the current schema + add the
presence_status/ce_marked columns. Machine-type vocabulary endpoint for the form.

[migration-approved]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 17:15:55 +02:00

119 lines
5.7 KiB
Go

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",
"pressenteil": "dom_press", "pressraum": "dom_press", "blechbearbeitung": "dom_press",
"werkzeugraum der presse": "dom_press",
// Glas-Bearbeitung
"glasschneid": "dom_glass", "glasbearbeitung": "dom_glass", "glasscheibe": "dom_glass",
"glaskante": "dom_glass",
// 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", "punktschweiss": "dom_welding",
"schweisselektrod": "dom_welding", "elektrodenspalt": "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",
// Aussen-/Witterungs-/Bioarbeit (Forst, Bau im Freien)
"zecke": "dom_outdoor", "zeckenstich": "dom_outdoor", "fsme": "dom_outdoor",
"borreliose": "dom_outdoor", "im freien": "dom_outdoor", "freigelaende": "dom_outdoor",
"aussengelaende": "dom_outdoor", "ausseneinsatz": "dom_outdoor", "witterung": "dom_outdoor",
"winterarbeit": "dom_outdoor", "nagerkot": "dom_outdoor", "hantavirus": "dom_outdoor",
// Lueftung/Feuchte (Schimmel)
"schimmel": "dom_ventilation", "schimmelspor": "dom_ventilation",
"lueftungsanlage": "dom_ventilation", "lueftungskanal": "dom_ventilation",
// Zerspanung / Kuehlschmierstoff
"kuehlschmierstoff": "dom_machining", "kss-kreislauf": "dom_machining",
"kss-aufbereitung": "dom_machining", "kuehlturm": "dom_machining",
"bearbeitungszentrum": "dom_machining",
// Schuettgut / Silo / Gaerbehaelter (Confined Space mit Schuettgut)
"silo": "dom_bulk", "schuettgut": "dom_bulk", "gaerbehaelter": "dom_bulk",
"getreidesilo": "dom_bulk", "mehlsilo": "dom_bulk",
// Palettierer
"palettierer": "dom_palletizer", "palettieranlage": "dom_palletizer",
// Spielplatz / Spielgeraet
"klettergeraet": "dom_playground", "spielplatz": "dom_playground", "spielgeraet": "dom_playground",
// Fitness / Kraftgeraet
"gewichtstapel": "dom_fitness", "langhantel": "dom_fitness", "bankdrueck": "dom_fitness",
"kniebeug": "dom_fitness", "kraftstation": "dom_fitness",
}
// 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 + " " + patterns[i].ZoneDE)
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
}