feat(iace): generic cross-domain leak gates + norm vocab reconciliation
- Domain-gate ~15 foreign machine classes (pool, amusement, paint booth, tank farm, reactor, lathe/chips, saw, film/carton, robot, mobile cab, asbestos, playground swing) in pattern_domain_gates.go so ungated hazard patterns stop leaking into unrelated machines; matching emit keywords added in keyword_dictionary.go (gate+emit share one vocabulary). - Extend the cross-domain precision guard to 6 machine classes (press, cobot, motor, welding + the 2 GTs) with per-case homeDomains, so a machine's own domain terms are never flagged. GT coverage stays 100%. - Reconcile the fine-grained norm machine-type vocabulary (455 keys) with the 68 canonical dropdown keys via canonicalMachineType() family folding in matchNorm — welding 0->17, robotics_cobot 0->6, press 8->13, circular_saw 1->35 machine-specific C-norms. Pattern gating left strict. - Fix initialize?force=true summary index-shift that mislabeled counts (reported matched-patterns as "hazards"); now uses named step vars. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -447,13 +447,19 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
mustMarshalJSON(map[string]interface{}{"steps": steps}),
|
||||
)
|
||||
|
||||
// Summary is built from the NAMED step variables, not positional steps[]
|
||||
// indices: with ?force=true an extra "Alte Daten geloescht" step is prepended,
|
||||
// which previously shifted steps[1..5] by one and mislabeled every count
|
||||
// (e.g. reporting matched-patterns as "hazards"). These vars are stable.
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"project_id": projectID.String(),
|
||||
"steps": steps,
|
||||
"summary": gin.H{
|
||||
"components": steps[1].Count, "patterns": steps[2].Count,
|
||||
"hazards": steps[3].Count, "mitigations": steps[4].Count,
|
||||
"norms": steps[5].Count,
|
||||
"components": compStep.Count,
|
||||
"patterns": len(matchOutput.MatchedPatterns),
|
||||
"hazards": hazardStep.Count,
|
||||
"mitigations": mitStep.Count,
|
||||
"norms": normCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ type gtCase struct {
|
||||
// keyword-stuffed — they represent how an engineer would describe the
|
||||
// machine, so the benchmark stays honest about extraction quality.
|
||||
narrativeOverride string
|
||||
// homeDomains lists the foreignDomainTerms domains that are NATIVE to this
|
||||
// machine, so the cross-domain precision guard does not flag a press's own
|
||||
// "stoessel"/"werkzeugeinbauraum" or a robot cell's "roboterzelle" as a leak.
|
||||
// Empty for machines whose domain has no entry in foreignDomainTerms.
|
||||
homeDomains []string
|
||||
}
|
||||
|
||||
// gtBenchmarkCases is the registry the harness iterates over. Add a new GT
|
||||
@@ -50,6 +55,7 @@ var gtBenchmarkCases = []gtCase{
|
||||
"Pneumatische Greifer und Spannvorrichtungen. Betrieb im Automatikbetrieb, Einrichten " +
|
||||
"und Einlernen (Teachen), Wartung und Stoerungsbeseitigung. Gefaehrdungen durch " +
|
||||
"Quetschen und Einzug bei Roboterbewegung, elektrische Energie und Druckluft.",
|
||||
homeDomains: []string{"robot"},
|
||||
},
|
||||
{
|
||||
name: "Kistenhub (Hebevorrichtung)",
|
||||
@@ -65,6 +71,59 @@ var gtBenchmarkCases = []gtCase{
|
||||
},
|
||||
}
|
||||
|
||||
// precisionOnlyCases are real machines from breakpilot-core/docs-src that have a
|
||||
// Grenzen description but NO expert GT hazard list, so they cannot be coverage-
|
||||
// benchmarked — only checked for cross-domain precision (no foreign-domain
|
||||
// nonsense). They diversify the gating guard beyond the 2 ground truths (lift +
|
||||
// robot cell) across a press, a cobot, a motor and a welding system. Each leak
|
||||
// they would otherwise produce (pool, carousel, paint booth, tank farm, lathe
|
||||
// chuck, band saw, robot-into-press ...) is now a permanent regression guard.
|
||||
var precisionOnlyCases = []gtCase{
|
||||
{
|
||||
name: "Kniehebelpresse (Presse)",
|
||||
machineType: "mechanical_press",
|
||||
homeDomains: []string{"press"},
|
||||
narrativeOverride: "Vollautomatische Kniehebelpresse zur Kaltmassivumformung metallischer " +
|
||||
"Rohlinge. Eine Transferanlage fuehrt Rohlinge ueber ein Foerderband in die Presse, wo sie " +
|
||||
"in mehreren Stufen im Werkzeugeinbauraum zwischen Ober- und Unterwerkzeug umgeformt werden. " +
|
||||
"Stoessel mit Schwungradantrieb, Hydraulikoel und Druckluft im System, integrierte " +
|
||||
"Schmieranlage und Absaugung. Schutzumhausung mit verriegelten Tueren. Elektrische " +
|
||||
"Versorgung 400 V, Steuerung ueber SPS. Betrieb vollautomatisch, Einrichten und Umruesten, " +
|
||||
"Instandhaltung. Impulslaerm und heisse Werkstuecke beim Pressvorgang.",
|
||||
},
|
||||
{
|
||||
name: "Eigenbauzelle (Cobot)",
|
||||
machineType: "robotics_cobot",
|
||||
homeDomains: []string{"robot"},
|
||||
narrativeOverride: "Arbeitstisch mit integriertem kollaborierendem Roboterarm (Cobot) zur " +
|
||||
"Bestueckung von Maschinen. Ein Sicherheitsscanner setzt den Roboterarm bei Annaeherung " +
|
||||
"still. Programmierung ueber Touchscreen. Spannungsversorgung 230 V. Quetsch- und " +
|
||||
"Stossgefahr im Roboterarbeitsraum durch Bewegung des Roboterarms. Betrieb kollaborierend " +
|
||||
"und nicht kollaborierend, Teachen und Programmieren, Reinigung, Instandhaltung.",
|
||||
},
|
||||
{
|
||||
name: "Elektromotoren (Antrieb)",
|
||||
machineType: "general_industry",
|
||||
homeDomains: nil,
|
||||
narrativeOverride: "Gleichstrom- und Asynchronmotoren mit oder ohne integriertes Getriebe als " +
|
||||
"Antrieb in Maschinen. Energieversorgung 24 bis 400 V Gleich- und Wechselstrom. Rotierende " +
|
||||
"Welle und bewegliche Teile des Motors, Gehaeuse mit Stromschlag- und Erhitzungsgefahr, " +
|
||||
"elektrische Anschluesse, Uebertemperaturueberwachung und Schutzleiter. Betrieb, Montage, " +
|
||||
"Reinigung, Instandhaltung, Demontage.",
|
||||
},
|
||||
{
|
||||
name: "Schwingarm (Rundschweissanlage)",
|
||||
machineType: "welding",
|
||||
homeDomains: []string{"welding"},
|
||||
narrativeOverride: "Rundschweissanlage Schwingarm als Auf-Tisch-Version zum Schweissen von " +
|
||||
"Rundnaehten. Pneumatisch bewegter Brennerarm, Anschluss an MIG/MAG- und TIG-Stromquellen, " +
|
||||
"maximaler Schweissstrom 350 A. Werkstuecke werden in zwei Backenfuttern eingespannt und " +
|
||||
"pneumatisch gesichert, rotierende Werkstueckaufnahme mit Reitstock. Formiergas durch die " +
|
||||
"Hohlwelle. Leitfaehige Gehaeuseoberflaechen, Brenner mit Verbrennungsgefahr. Bedienung " +
|
||||
"ueber Fusspedal, integrierte Steuerung.",
|
||||
},
|
||||
}
|
||||
|
||||
// readGTNarrative extracts a machine narrative from the raw GT JSON, trying the
|
||||
// richer machine_description field before the generic description.
|
||||
func readGTNarrative(t *testing.T, path string) (gt GroundTruth, narrative, machineName string) {
|
||||
@@ -233,8 +292,39 @@ var foreignDomainTerms = map[string]string{
|
||||
// playground / fitness
|
||||
"klettergeraet": "playground", "spielplatz": "playground", "kraftstation": "fitness",
|
||||
"bankdrueck": "fitness", "kniebeug": "fitness",
|
||||
"schaukelkette": "playground", "nestschaukel": "playground",
|
||||
// palletizer
|
||||
"palettierer": "palletizer",
|
||||
// aquatic / pool
|
||||
"schwimmbecken": "aquatic", "schwimmbad": "aquatic", "beckenumrandung": "aquatic",
|
||||
"massageduese": "aquatic", "schwimmbadtechnik": "aquatic", "sprungturm": "aquatic",
|
||||
// amusement
|
||||
"karussell": "amusement", "fahrgeschaeft": "amusement", "riesenrad": "amusement",
|
||||
// mobile machine with driver cab
|
||||
"fahrersitz": "mobile_cab", "fahrerkabine": "mobile_cab", "fahrerstand": "mobile_cab",
|
||||
// coating / paint booth
|
||||
"lackier": "coating", "loesemitteldampf": "coating", "pulverbeschicht": "coating",
|
||||
// ex process / tank farm
|
||||
"tanklager": "exproc", "raffinerie": "exproc",
|
||||
// chemical reactor
|
||||
"reaktor": "chem", "mischbereich": "chem", "exotherme reaktion": "chem",
|
||||
// oxygen / gas supply
|
||||
"sauerstoffanreicherung": "o2", "sauerstoff-versorgung": "o2",
|
||||
// lathe / chip machining
|
||||
"drehfutter": "cnc", "spannfutterbacke": "cnc", "spaeneflug": "cnc",
|
||||
"spanflug": "cnc", "spindelumgebung": "cnc",
|
||||
// sawing
|
||||
"bandsaege": "sawing", "saegeband": "sawing",
|
||||
// film / carton converting
|
||||
"folienwickler": "converting", "folientrennbereich": "converting", "kartonschneider": "converting",
|
||||
// blow molding (plastics)
|
||||
"blasformwerkzeug": "plastics", "blasstation": "plastics",
|
||||
// textile cutting
|
||||
"stoffauflage": "textile", "konfektionierung": "textile",
|
||||
// asbestos legacy
|
||||
"asbest": "asbestos",
|
||||
// robot (home for cobot/robot-cell cases via homeDomains)
|
||||
"roboterzelle": "robot", "schwenkbereich roboter": "robot", "roboter-arbeitsraum": "robot",
|
||||
}
|
||||
|
||||
// TestGT_DomainLeakage names the patterns that leak across domains. For each GT
|
||||
@@ -252,6 +342,10 @@ func TestGT_DomainLeakage(t *testing.T) {
|
||||
if c.narrativeOverride != "" {
|
||||
narrative = c.narrativeOverride
|
||||
}
|
||||
home := make(map[string]bool, len(c.homeDomains))
|
||||
for _, d := range c.homeDomains {
|
||||
home[d] = true
|
||||
}
|
||||
pr := ParseNarrative(narrative, c.machineType)
|
||||
out := NewPatternEngine().Match(parseResultToMatchInput(pr, c.machineType))
|
||||
|
||||
@@ -259,6 +353,9 @@ func TestGT_DomainLeakage(t *testing.T) {
|
||||
for _, pm := range out.MatchedPatterns {
|
||||
text := normalizeDE(pm.PatternName + " " + pm.ScenarioDE)
|
||||
for term, domain := range foreignDomainTerms {
|
||||
if home[domain] {
|
||||
continue // native to this machine — not a leak
|
||||
}
|
||||
if strings.Contains(text, term) {
|
||||
leaks = append(leaks, pm.PatternID)
|
||||
leakCount[pm.PatternID]++
|
||||
|
||||
@@ -74,8 +74,20 @@ func GetKeywordDictionary() []KeywordEntry {
|
||||
{Keywords: []string{"kuehlschmierstoff", "kss-anlage", "kss-kreislauf", "kss-aufbereitung", "bearbeitungszentrum", "kuehlturm"}, ExtraTags: []string{"dom_machining"}},
|
||||
{Keywords: []string{"silo", "schuettgut", "gaerbehaelter", "getreidesilo", "mehlsilo", "schuettgutfoerder"}, ExtraTags: []string{"dom_bulk"}},
|
||||
{Keywords: []string{"palettierer", "palettieranlage", "palettierroboter"}, ExtraTags: []string{"dom_palletizer"}},
|
||||
{Keywords: []string{"spielplatz", "klettergeraet", "spielgeraet", "spielturm"}, ExtraTags: []string{"dom_playground"}},
|
||||
{Keywords: []string{"spielplatz", "klettergeraet", "spielgeraet", "spielturm", "schaukel", "wippe", "rutsche", "sandkasten"}, ExtraTags: []string{"dom_playground"}},
|
||||
{Keywords: []string{"kraftstation", "fitnessgeraet", "trainingsgeraet", "kraftgeraet", "langhantel"}, ExtraTags: []string{"dom_fitness"}},
|
||||
{Keywords: []string{"schwimmbad", "schwimmbecken", "schwimmhalle", "planschbecken", "whirlpool", "badebecken"}, ExtraTags: []string{"dom_aquatic"}},
|
||||
{Keywords: []string{"karussell", "fahrgeschaeft", "freizeitpark", "vergnuegungspark", "riesenrad", "achterbahn"}, ExtraTags: []string{"dom_amusement"}},
|
||||
{Keywords: []string{"fahrersitz", "fahrerkabine", "fahrerstand", "fahrerhaus", "gabelstapler", "radlader", "baumaschine"}, ExtraTags: []string{"dom_mobile_cab"}},
|
||||
{Keywords: []string{"lackier", "lackierkabine", "pulverbeschicht", "beschichtungsanlage", "spritzlackier", "tauchlackier"}, ExtraTags: []string{"dom_coating"}},
|
||||
{Keywords: []string{"tanklager", "raffinerie", "tankfarm", "chemiepark", "tankfeld"}, ExtraTags: []string{"dom_exproc"}},
|
||||
{Keywords: []string{"reaktor", "chemieanlage", "ruehrkessel", "mischreaktor", "reaktionsbehaelter"}, ExtraTags: []string{"dom_chem"}},
|
||||
{Keywords: []string{"sauerstoffanlage", "sauerstoff-versorgung", "sauerstoffversorgung", "medizinische gase"}, ExtraTags: []string{"dom_o2"}},
|
||||
{Keywords: []string{"roboter", "roboterarm", "roboterzelle", "cobot", "industrieroboter", "knickarm", "manipulatorarm"}, ExtraTags: []string{"dom_robot"}},
|
||||
{Keywords: []string{"bandsaege", "saegemaschine", "gattersaege", "saegewerk", "blockbandsaege"}, ExtraTags: []string{"dom_sawing"}},
|
||||
{Keywords: []string{"folienwickler", "wickelmaschine", "konfektioniermaschine", "folienverpackung", "wellpappe"}, ExtraTags: []string{"dom_converting"}},
|
||||
{Keywords: []string{"bergbau", "untertage", "tunnelbau", "off-grid"}, ExtraTags: []string{"dom_remote"}},
|
||||
{Keywords: []string{"asbest", "asbestsanierung", "asbestexposition"}, ExtraTags: []string{"dom_asbestos"}},
|
||||
// Ghost-Closure (Emit-Seite): macht die 34 toten Required-Tags
|
||||
// emittierbar, jeweils NUR via domaenenspezifische Keywords -> die 120
|
||||
// Ghost-Patterns feuern wieder, aber nur fuer ihre echte Maschine (kein
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Machine-type family reconciliation for NORM suggestion.
|
||||
//
|
||||
// Problem: the C-norm library (norms_library_c_*.go) tags norms with a free-form,
|
||||
// very fine-grained machine-type vocabulary (~455 keys: "welding_machine",
|
||||
// "band_saw", "mobile_crane", "stamping_press" ...). The project/dropdown
|
||||
// vocabulary (machine_types.go) has only 68 canonical keys ("welding", "press",
|
||||
// "circular_saw", "crane" ...). matchNorm() compares them with exact "==", so a
|
||||
// "welding" project matched 0 of 605 C-norms, "robotics_cobot" 0, "general_industry"
|
||||
// 0 — machine-type-based C-norm suggestion was effectively dead for most dropdown
|
||||
// values.
|
||||
//
|
||||
// Fix: canonicalMachineType() folds both the norm key AND the project key into a
|
||||
// shared canonical family before comparison. A "welding_machine" norm and a
|
||||
// "welding" project both fold to "welding" → they match. This is applied ONLY in
|
||||
// matchNorm (norm suggestion is advisory). Pattern gating in patternMatches() is
|
||||
// left STRICT and untouched — loosening it there could re-introduce the cross-
|
||||
// domain hazard leaks the domain-gating work eliminated.
|
||||
//
|
||||
// Coverage is asymmetric on purpose: only norm keys that are genuine synonyms /
|
||||
// sub-types of one of the 68 canonical dropdown keys are mapped. Niche norm keys
|
||||
// with no dropdown counterpart (boiler, centrifuge, carousel ...) stay unmapped —
|
||||
// no project can select them anyway, so mapping would be dead weight.
|
||||
|
||||
// machineTypeSynonyms maps a fine-grained machine-type key (norm side OR a
|
||||
// canonical sub-type on the project side) to its canonical family key. Curated:
|
||||
// every entry is a true synonym/sub-type. Deliberate EXCLUSIONS guard against
|
||||
// false folds — e.g. "pressure_vessel"/"pressure_washer"/"blood_pressure_monitor"
|
||||
// are NOT presses, "treadmill" is not a mill, "seesaw"/"chainsaw" are not saws,
|
||||
// "heat_pump" is not the industrial pump dropdown.
|
||||
var machineTypeSynonyms = map[string]string{
|
||||
// ── Welding ──
|
||||
"welding_machine": "welding", "arc_welding": "welding", "arc_welder": "welding",
|
||||
"resistance_welder": "welding", "mig_welder": "welding", "tig_welder": "welding",
|
||||
"gas_welding": "welding", "welding_clamp": "welding", "welding_fume_extractor": "welding",
|
||||
"spot_welder": "welding",
|
||||
// ── Press (forming) — collapses canonical sub-types too ──
|
||||
"mechanical_press": "press", "hydraulic_press": "press", "stamping_press": "press",
|
||||
"press_brake": "press", "pneumatic_press": "press", "power_press": "press",
|
||||
"forging_press": "press", "punch_press": "press", "eccentric_press": "press",
|
||||
"toggle_press": "press", "plastics_press": "press", "pressing_machine": "press",
|
||||
// ── Saw (bench/stationary) ──
|
||||
"saw": "circular_saw", "panel_saw": "circular_saw", "metal_saw": "circular_saw",
|
||||
"miter_saw": "circular_saw", "band_saw": "circular_saw", "bandsaw": "circular_saw",
|
||||
"cold_saw": "circular_saw", "crosscut_saw": "circular_saw", "log_saw": "circular_saw",
|
||||
"pendulum_saw": "circular_saw", "table_saw": "circular_saw", "tile_saw": "circular_saw",
|
||||
"food_saw": "circular_saw", "pneumatic_saw": "circular_saw", "chop_saw": "circular_saw",
|
||||
// ── Crane ──
|
||||
"mobile_crane": "crane", "overhead_crane": "crane", "gantry_crane": "crane",
|
||||
"offshore_crane": "crane", "bridge_crane": "crane", "floating_crane": "crane",
|
||||
"hydraulic_crane": "crane", "loader_crane": "crane", "slewing_crane": "crane",
|
||||
"tower_crane": "crane", "harbour_crane": "crane", "jib_crane": "crane",
|
||||
// ── Hoist ──
|
||||
"chain_hoist": "hoist", "construction_hoist": "hoist", "engine_hoist": "hoist",
|
||||
"manual_hoist": "hoist", "electric_hoist": "hoist", "wire_rope_hoist": "hoist",
|
||||
// ── Lift / elevator ──
|
||||
"passenger_lift": "elevator", "goods_lift": "elevator", "lifting_equipment": "lift",
|
||||
"lift_table": "lift", "lifting_platform": "lift",
|
||||
// ── Conveyor ──
|
||||
"conveyor_belt": "conveyor", "conveyor_system": "conveyor", "belt_conveyor": "conveyor",
|
||||
"roller_conveyor": "conveyor", "screw_conveyor": "conveyor", "baggage_conveyor": "conveyor",
|
||||
"telescopic_conveyor": "conveyor", "vibrating_conveyor": "conveyor", "pneumatic_conveyor": "conveyor",
|
||||
"mining_conveyor": "conveyor",
|
||||
// ── Robot / cobot ──
|
||||
"robot": "robotics_cobot", "industrial_robot": "robotics_cobot", "robot_cell": "robotics_cobot",
|
||||
"collaborative_robot": "robotics_cobot", "cobot": "robotics_cobot", "articulated_robot": "robotics_cobot",
|
||||
// ── Lathe / turning ──
|
||||
"large_lathe": "lathe", "small_lathe": "lathe", "turning_machine": "lathe", "cnc_lathe": "lathe",
|
||||
// ── Milling ──
|
||||
"milling_machine": "milling",
|
||||
// ── Grinding ──
|
||||
"grinding_machine": "grinding", "grinder": "grinding", "die_grinder": "grinding",
|
||||
"handheld_grinder": "grinding", "pneumatic_grinder": "grinding",
|
||||
// ── Drilling ──
|
||||
"drilling_machine": "drilling", "horizontal_drill": "drilling", "rotary_percussion_drill": "drilling",
|
||||
"drilling_rig": "drilling",
|
||||
// ── CNC / machining centre ──
|
||||
"machining_center": "machining_centre", "cnc_machine": "cnc", "machining_cell": "machining_centre",
|
||||
// ── Textile ──
|
||||
"textile_machine": "textile", "weaving_machine": "weaving", "spinning_machine": "spinning",
|
||||
"knitting_machine": "knitting", "dyeing_machine": "dyeing",
|
||||
// ── Woodworking ──
|
||||
"woodworking_machine": "woodworking", "planer": "woodworking", "wood_machine": "woodworking",
|
||||
// ── Food processing ──
|
||||
"food_machine": "food_processing", "food_cutter": "food_processing", "food_slicer": "food_processing",
|
||||
"bakery_machine": "food_processing", "bakery": "food_processing", "dough_machine": "food_processing",
|
||||
"bread_slicer": "food_processing", "meat_grinder": "food_processing", "meat_processing": "food_processing",
|
||||
// ── Packaging ──
|
||||
"packaging_machine": "packaging", "filling_machine": "bottling", "bottling_machine": "bottling",
|
||||
// ── Laser ──
|
||||
"laser_machine": "laser_device", "laser_cutter": "laser_device", "medical_laser": "laser_device",
|
||||
// ── Glass processing / washing ──
|
||||
"glass_processing_machine": "glass_processing", "glass_breaking_machine": "glass_processing",
|
||||
"glass_cutting_machine": "glass_processing", "glass_drilling_machine": "glass_processing",
|
||||
"glass_edge_grinder": "glass_processing", "glass_handling": "glass_processing",
|
||||
"glass_laminator": "glass_processing", "glass_tempering_furnace": "glass_processing",
|
||||
"glass_tilting_device": "glass_processing", "glass_machine": "glass",
|
||||
"glass_washing_machine": "glass_washing",
|
||||
// ── Agriculture ──
|
||||
"agricultural_machine": "agricultural", "combine_harvester": "combine",
|
||||
"sugarcane_harvester": "harvester", "agri_tractor": "tractor",
|
||||
// ── Pump (industrial) ──
|
||||
"hydraulic_pump": "pump", "vacuum_pump": "pump", "fire_pump": "pump", "coating_pump": "pump",
|
||||
// ── Surface treatment / coating ──
|
||||
"coating_machine": "surface_treatment", "coating": "surface_treatment",
|
||||
"powder_coating": "surface_treatment", "painting_machine": "surface_treatment",
|
||||
// ── Printing ──
|
||||
"printing_machine": "printing", "printing_press_machine": "printing_press",
|
||||
// ── Rotary transfer ──
|
||||
"rotary_transfer_machine": "rotary_transfer",
|
||||
// ── Medical ──
|
||||
"medical_electrical_equipment": "medical_device", "dental_equipment": "medical_device",
|
||||
"dental_unit": "medical_device", "ct_scanner": "medical_device", "defibrillator": "medical_device",
|
||||
"dialysis_device": "medical_device", "ventilator_device": "ventilator",
|
||||
// ── Compressor ──
|
||||
"air_compressor": "compressor", "screw_compressor": "compressor",
|
||||
}
|
||||
|
||||
// genericMachineSuffixes are folded off when the remainder is itself a canonical
|
||||
// dropdown key — auto-covers "<canonical>_machine"/"<canonical>_equipment" pairs
|
||||
// (welding_machine→welding, playground_equipment→playground) without listing each.
|
||||
var genericMachineSuffixes = []string{"_machine", "_equipment", "_system", "_unit", "_plant", "_line"}
|
||||
|
||||
var (
|
||||
canonicalVocabOnce sync.Once
|
||||
canonicalVocabSet map[string]bool
|
||||
)
|
||||
|
||||
func canonicalVocab() map[string]bool {
|
||||
canonicalVocabOnce.Do(func() {
|
||||
canonicalVocabSet = make(map[string]bool)
|
||||
for _, v := range MachineTypeVocabulary() {
|
||||
canonicalVocabSet[v.Key] = true
|
||||
}
|
||||
})
|
||||
return canonicalVocabSet
|
||||
}
|
||||
|
||||
// canonicalMachineType folds a machine-type key (norm side or project side) into
|
||||
// its canonical family so the norm engine can match across the two vocabularies.
|
||||
// Unknown keys are returned unchanged (exact-match fallback = pre-existing
|
||||
// behaviour, so a niche norm key never starts matching a wrong project).
|
||||
func canonicalMachineType(key string) string {
|
||||
k := strings.ToLower(strings.TrimSpace(key))
|
||||
if k == "" {
|
||||
return ""
|
||||
}
|
||||
if c, ok := machineTypeSynonyms[k]; ok {
|
||||
return c
|
||||
}
|
||||
for _, suf := range genericMachineSuffixes {
|
||||
if strings.HasSuffix(k, suf) {
|
||||
base := strings.TrimSuffix(k, suf)
|
||||
if c, ok := machineTypeSynonyms[base]; ok {
|
||||
return c
|
||||
}
|
||||
if canonicalVocab()[base] {
|
||||
return base
|
||||
}
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCanonicalMachineType_Folds verifies that fine-grained norm machine-type
|
||||
// keys fold to the right canonical family — and that the deliberate exclusions
|
||||
// (pressure_*, treadmill, seesaw, chainsaw, heat_pump, blood_pressure_monitor)
|
||||
// do NOT fold into a wrong family.
|
||||
func TestCanonicalMachineType_Folds(t *testing.T) {
|
||||
folds := map[string]string{
|
||||
"welding_machine": "welding", "arc_welder": "welding",
|
||||
"stamping_press": "press", "mechanical_press": "press", "hydraulic_press": "press",
|
||||
"band_saw": "circular_saw", "panel_saw": "circular_saw", "saw": "circular_saw",
|
||||
"mobile_crane": "crane", "overhead_crane": "crane",
|
||||
"robot": "robotics_cobot", "industrial_robot": "robotics_cobot", "cobot": "robotics_cobot",
|
||||
"conveyor_belt": "conveyor", "belt_conveyor": "conveyor",
|
||||
"food_machine": "food_processing", "bakery_machine": "food_processing",
|
||||
"milling_machine": "milling", "grinding_machine": "grinding", "drilling_machine": "drilling",
|
||||
"textile_machine": "textile", "weaving_machine": "weaving",
|
||||
"glass_processing_machine": "glass_processing", "playground_equipment": "playground",
|
||||
"laser_machine": "laser_device", "passenger_lift": "elevator",
|
||||
// generic suffix-strip fallback (not in synonym map):
|
||||
"packaging_machine": "packaging",
|
||||
}
|
||||
for in, want := range folds {
|
||||
if got := canonicalMachineType(in); got != want {
|
||||
t.Errorf("canonicalMachineType(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Must NOT fold into a wrong family — these keep their own identity.
|
||||
noFold := []string{
|
||||
"pressure_vessel", "pressure_washer", "blood_pressure_monitor", // not press
|
||||
"treadmill", // not milling
|
||||
"seesaw", // not saw (playground)
|
||||
"chainsaw", // not bench saw (forestry/handheld)
|
||||
"heat_pump", // not the industrial pump dropdown
|
||||
"two_roll_mill", // rolling, not milling
|
||||
}
|
||||
for _, in := range noFold {
|
||||
if got := canonicalMachineType(in); got == "press" || got == "milling" || got == "circular_saw" || got == "pump" {
|
||||
t.Errorf("canonicalMachineType(%q) = %q — must NOT fold into a wrong family", in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormMachineTypeReconciliation asserts the norm engine now reaches machine-
|
||||
// specific C-norms via the canonical family (regression guard for the broken
|
||||
// exact-"==" matching where welding/cobot matched 0 of 605 C-norms).
|
||||
func TestNormMachineTypeReconciliation(t *testing.T) {
|
||||
cases := []struct {
|
||||
machineType string
|
||||
minCNorms int
|
||||
}{
|
||||
{"welding", 8},
|
||||
{"press", 8},
|
||||
{"mechanical_press", 8},
|
||||
{"robotics_cobot", 3},
|
||||
{"circular_saw", 10},
|
||||
{"crane", 10},
|
||||
{"lift", 10},
|
||||
{"food_processing", 10},
|
||||
{"conveyor", 6},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
res := SuggestNorms(tc.machineType, nil, nil)
|
||||
if res == nil {
|
||||
t.Fatalf("%s: SuggestNorms returned nil", tc.machineType)
|
||||
}
|
||||
// Count C-norms suggested via a machine_type source.
|
||||
n := 0
|
||||
for _, s := range res.CNorms {
|
||||
for _, src := range s.Sources {
|
||||
if strings.HasPrefix(src, "machine_type") {
|
||||
n++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if n < tc.minCNorms {
|
||||
t.Errorf("%s: %d machine-type C-norms suggested, want >= %d", tc.machineType, n, tc.minCNorms)
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-family guard: a lift must NOT pull welding-specific C-norms.
|
||||
lift := SuggestNorms("lift", nil, nil)
|
||||
for _, s := range lift.CNorms {
|
||||
for _, mt := range s.Norm.MachineTypes {
|
||||
if canonicalMachineType(mt) == "welding" {
|
||||
t.Errorf("lift project suggested a welding C-norm %q (cross-family leak)", s.Norm.Number)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,11 +88,15 @@ func matchNorm(norm NormReference, machineType string, hazardSet, tagSet map[str
|
||||
var reasons []string
|
||||
var sources []string
|
||||
|
||||
// Machine type match
|
||||
// Machine type match — compared on the CANONICAL family so the fine-grained
|
||||
// norm vocabulary (welding_machine, band_saw, mobile_crane ...) reconciles
|
||||
// with the 68 canonical dropdown keys (welding, circular_saw, crane ...).
|
||||
// Without this fold, exact "==" left most C-norms unreachable by machine type.
|
||||
machineTypeMatched := false
|
||||
if machineType != "" && len(norm.MachineTypes) > 0 {
|
||||
projCanon := canonicalMachineType(machineType)
|
||||
for _, mt := range norm.MachineTypes {
|
||||
if mt == machineType {
|
||||
if canonicalMachineType(mt) == projCanon {
|
||||
bestConfidence = 0.9
|
||||
reasons = append(reasons, "Maschinentyp: "+machineType)
|
||||
sources = append(sources, "machine_type:"+machineType)
|
||||
|
||||
@@ -79,6 +79,48 @@ var domainGateTerms = map[string]string{
|
||||
// Fitness / Kraftgeraet
|
||||
"gewichtstapel": "dom_fitness", "langhantel": "dom_fitness", "bankdrueck": "dom_fitness",
|
||||
"kniebeug": "dom_fitness", "kraftstation": "dom_fitness",
|
||||
// Schwimmbad / Aquatik (Entrapment, Nassbereich-Strom, Beckenrand)
|
||||
"schwimmbecken": "dom_aquatic", "schwimmbad": "dom_aquatic", "schwimmhalle": "dom_aquatic",
|
||||
"beckenumrandung": "dom_aquatic", "beckenrand": "dom_aquatic", "massageduese": "dom_aquatic",
|
||||
"badegaeste": "dom_aquatic", "sprungturm": "dom_aquatic", "schwimmbadtechnik": "dom_aquatic",
|
||||
// Fahrgeschaeft / Vergnuegungspark
|
||||
"karussell": "dom_amusement", "fahrgeschaeft": "dom_amusement", "riesenrad": "dom_amusement",
|
||||
"achterbahn": "dom_amusement",
|
||||
// Mobile Maschine mit Fahrerstand (Ganzkoerpervibration etc.)
|
||||
"fahrersitz": "dom_mobile_cab", "fahrerkabine": "dom_mobile_cab", "fahrerstand": "dom_mobile_cab",
|
||||
"fahrerhaus": "dom_mobile_cab",
|
||||
// Lackieren / Beschichten (Loesemittel, ESD-Zuendung Lackierzone)
|
||||
"lackier": "dom_coating", "loesemitteldampf": "dom_coating", "pulverbeschicht": "dom_coating",
|
||||
"spritzlackier": "dom_coating",
|
||||
// Ex-Prozessanlage / Tanklager
|
||||
"tanklager": "dom_exproc", "raffinerie": "dom_exproc", "tankfarm": "dom_exproc",
|
||||
// Chemie-Reaktor / Mischanlage
|
||||
"reaktor": "dom_chem", "mischbereich": "dom_chem", "exotherme reaktion": "dom_chem",
|
||||
"ruehrkessel": "dom_chem",
|
||||
// Sauerstoff-/Gasversorgungsanlage
|
||||
"sauerstoffanreicherung": "dom_o2", "sauerstoff-versorgung": "dom_o2",
|
||||
// Drehmaschine / Zerspanung (Spannfutter, Spaeneflug, Spindelumgebung)
|
||||
"drehfutter": "dom_cnc", "spannfutterbacke": "dom_cnc", "spannbacke": "dom_cnc",
|
||||
"spaeneflug": "dom_cnc", "spanflug": "dom_cnc", "spindelumgebung": "dom_cnc",
|
||||
"werkzeugmaschine": "dom_cnc",
|
||||
// Roboter / Cobot (ungated Roboterzellen-Hazards)
|
||||
"roboterzelle": "dom_robot", "roboterarm": "dom_robot", "roboter-arbeitsraum": "dom_robot",
|
||||
"schwenkbereich roboter": "dom_robot", "knickarmroboter": "dom_robot", "teach-zone": "dom_robot",
|
||||
// Saege (Bandsaege, Gattersaege)
|
||||
"bandsaege": "dom_sawing", "saegeband": "dom_sawing", "gattersaege": "dom_sawing",
|
||||
// Folien-/Karton-Konfektionierung (Wickler, Trennmesser)
|
||||
"folienwickler": "dom_converting", "folieneinlauf": "dom_converting", "wickelachse": "dom_converting",
|
||||
"folientrennbereich": "dom_converting", "kartonschneider": "dom_converting",
|
||||
// Kunststoff Blasformen (ergaenzt dom_plastics)
|
||||
"blasformwerkzeug": "dom_plastics", "blasstation": "dom_plastics", "blasform": "dom_plastics",
|
||||
// Textil-Zuschnitt / Konfektionierung (ergaenzt dom_textile)
|
||||
"stoffauflage": "dom_textile", "konfektionierung": "dom_textile", "schneidkopfbereich": "dom_textile",
|
||||
// Abgelegener / untertage Einzelarbeitsplatz (kein Notruf-Empfang)
|
||||
"kein empfang": "dom_remote", "unterirdisch": "dom_remote", "untertage": "dom_remote",
|
||||
// Asbest-Altanlagen
|
||||
"asbest": "dom_asbestos",
|
||||
// Spielplatz-Schaukel (ergaenzt dom_playground: Kettenglied-Fingerfang)
|
||||
"schaukelkette": "dom_playground", "nestschaukel": "dom_playground", "schaukelsitz": "dom_playground",
|
||||
}
|
||||
|
||||
// applyDomainGates appends a domain capability tag to every pattern whose own
|
||||
|
||||
@@ -37,11 +37,20 @@ func firedHazardsForCase(c gtCase) []PatternMatch {
|
||||
return fired
|
||||
}
|
||||
|
||||
// TestCrossDomainPrecision asserts that no fired pattern is foreign to the GT
|
||||
// machine — neither machine-type-incompatible nor matching a foreign-domain term.
|
||||
// TestCrossDomainPrecision asserts that no fired pattern is foreign to the
|
||||
// machine — neither machine-type-incompatible nor matching a foreign-domain
|
||||
// term. Runs over the 2 ground truths AND the 4 Grenzen-only machines
|
||||
// (press/cobot/motor/welding), so the gating guard is validated across six
|
||||
// machine classes. A term whose domain is in the case's homeDomains is its
|
||||
// OWN domain and never counts as a leak.
|
||||
func TestCrossDomainPrecision(t *testing.T) {
|
||||
for _, c := range gtBenchmarkCases {
|
||||
cases := append(append([]gtCase{}, gtBenchmarkCases...), precisionOnlyCases...)
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
home := make(map[string]bool, len(c.homeDomains))
|
||||
for _, d := range c.homeDomains {
|
||||
home[d] = true
|
||||
}
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
fired := firedHazardsForCase(c)
|
||||
t.Logf("%s (%s): %d patterns fired", c.name, c.machineType, len(fired))
|
||||
@@ -50,6 +59,9 @@ func TestCrossDomainPrecision(t *testing.T) {
|
||||
for _, mp := range fired {
|
||||
text := normalizeDE(mp.PatternName + " " + mp.ZoneDE + " " + mp.ScenarioDE)
|
||||
for term, domain := range foreignDomainTerms {
|
||||
if home[domain] {
|
||||
continue // this domain is native to the machine
|
||||
}
|
||||
if strings.Contains(text, term) {
|
||||
domainLeaks = append(domainLeaks, domain+"/"+term+" → "+mp.PatternName)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user