From 005a2ed7113d325a686553c931384aa3ff27ec5d Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 10 Jun 2026 22:29:10 +0200 Subject: [PATCH] feat(iace): generic cross-domain leak gates + norm vocab reconciliation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../api/handlers/iace_handler_init.go | 12 +- .../iace/gt_benchmark_harness_test.go | 97 ++++++++++ .../internal/iace/keyword_dictionary.go | 14 +- .../internal/iace/machine_type_families.go | 169 ++++++++++++++++++ .../iace/machine_type_families_test.go | 97 ++++++++++ .../internal/iace/norms_engine.go | 8 +- .../internal/iace/pattern_domain_gates.go | 42 +++++ .../internal/iace/pattern_precision_test.go | 18 +- .../scripts/reinit_and_verify.sh | 48 +++++ .../scripts/seed_new_machines.py | 160 +++++++++++++++++ 10 files changed, 656 insertions(+), 9 deletions(-) create mode 100644 ai-compliance-sdk/internal/iace/machine_type_families.go create mode 100644 ai-compliance-sdk/internal/iace/machine_type_families_test.go create mode 100644 ai-compliance-sdk/scripts/reinit_and_verify.sh create mode 100644 ai-compliance-sdk/scripts/seed_new_machines.py diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go index 8890128c..0ecf2188 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -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, }, }) } diff --git a/ai-compliance-sdk/internal/iace/gt_benchmark_harness_test.go b/ai-compliance-sdk/internal/iace/gt_benchmark_harness_test.go index d92f95fb..5d25374a 100644 --- a/ai-compliance-sdk/internal/iace/gt_benchmark_harness_test.go +++ b/ai-compliance-sdk/internal/iace/gt_benchmark_harness_test.go @@ -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]++ diff --git a/ai-compliance-sdk/internal/iace/keyword_dictionary.go b/ai-compliance-sdk/internal/iace/keyword_dictionary.go index 28ed79f2..4d0f9817 100644 --- a/ai-compliance-sdk/internal/iace/keyword_dictionary.go +++ b/ai-compliance-sdk/internal/iace/keyword_dictionary.go @@ -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 diff --git a/ai-compliance-sdk/internal/iace/machine_type_families.go b/ai-compliance-sdk/internal/iace/machine_type_families.go new file mode 100644 index 00000000..f2fc7304 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/machine_type_families.go @@ -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 "_machine"/"_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 +} diff --git a/ai-compliance-sdk/internal/iace/machine_type_families_test.go b/ai-compliance-sdk/internal/iace/machine_type_families_test.go new file mode 100644 index 00000000..b21cbde5 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/machine_type_families_test.go @@ -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) + } + } + } +} diff --git a/ai-compliance-sdk/internal/iace/norms_engine.go b/ai-compliance-sdk/internal/iace/norms_engine.go index 4c3a4eff..86dba085 100644 --- a/ai-compliance-sdk/internal/iace/norms_engine.go +++ b/ai-compliance-sdk/internal/iace/norms_engine.go @@ -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) diff --git a/ai-compliance-sdk/internal/iace/pattern_domain_gates.go b/ai-compliance-sdk/internal/iace/pattern_domain_gates.go index 0af0e0f7..cb41af55 100644 --- a/ai-compliance-sdk/internal/iace/pattern_domain_gates.go +++ b/ai-compliance-sdk/internal/iace/pattern_domain_gates.go @@ -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 diff --git a/ai-compliance-sdk/internal/iace/pattern_precision_test.go b/ai-compliance-sdk/internal/iace/pattern_precision_test.go index 322d59ec..1b26ec81 100644 --- a/ai-compliance-sdk/internal/iace/pattern_precision_test.go +++ b/ai-compliance-sdk/internal/iace/pattern_precision_test.go @@ -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 diff --git a/ai-compliance-sdk/scripts/reinit_and_verify.sh b/ai-compliance-sdk/scripts/reinit_and_verify.sh new file mode 100644 index 00000000..45bc119b --- /dev/null +++ b/ai-compliance-sdk/scripts/reinit_and_verify.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Re-initialise all 6 IACE projects (4 Grenzen-only machines + 2 GTs) with the +# new domain-gated engine, then check for the specific foreign-domain leak +# signatures we set out to eliminate. Run AFTER the ai-sdk container is rebuilt. +# (bash 3.2 compatible — no associative arrays.) +set -uo pipefail + +API="https://macmini:8093/sdk/v1/iace" +TEN="9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" +PSQL='/usr/local/bin/docker exec bp-core-postgres psql -U breakpilot -d breakpilot_db -At -F"|"' + +PROJECTS=" +Kniehebelpresse:1e00ca16-ed0f-456d-80dd-b3035e413370 +Eigenbauzelle:1f17051e-5ed1-4f56-a734-1bbcdc5c0c48 +Elektromotoren:79566165-1561-43c7-97bf-4b6421a0641d +Schwingarm:00a573ec-caa0-4b7e-8af6-0acf3ed62989 +Kistenhub_GT:1646d728-a6cb-4275-b147-44ca282bb6f0 +Bremse_GT:f9347149-3d03-4c12-8a8e-0f8e32a71a91 +" + +echo "########## RE-INIT (force=true) — Summary vs DB ##########" +for row in $PROJECTS; do + name="${row%%:*}"; pid="${row##*:}" + resp=$(curl -sk -X POST -H "X-Tenant-ID: $TEN" "$API/projects/$pid/initialize?force=true") + sum=$(echo "$resp" | python3 -c "import sys,json +try: + s=json.load(sys.stdin).get('summary',{}) + print(f\"comp={s.get('components')} pat={s.get('patterns')} haz={s.get('hazards')} mit={s.get('mitigations')} norm={s.get('norms')}\") +except Exception as e: print('(keine summary)')" 2>/dev/null) + hz=$(ssh macmini "$PSQL -c \"SELECT count(*) FROM compliance.iace_hazards WHERE project_id='$pid';\"" 2>/dev/null) + # Konsistenz: summary.haz muss == DB-Hazards sein (Summary-Bug-Check) + shz=$(echo "$sum" | sed -nE 's/.*haz=([0-9]+).*/\1/p') + ok="OK"; [ "$shz" = "$hz" ] || ok="!! MISMATCH" + printf " %-16s summary[%s] DB-Hazards=%s -> %s\n" "$name" "$sum" "$hz" "$ok" +done + +echo +echo "########## LEAK-SIGNATUR-CHECK (Grenzen-Maschinen, sollte 0 sein) ##########" +LEAKS="Schwimmbecken|Massageduesen|Nassbereich|Karussell|Fahrersitz|Lackier|Loesemitteldampf|Tanklager|Reaktor|Sauerstoffanreicherung|Spannfutterbacke|Bandsaege|Folienwickler|Blasformwerkzeug|Asbest|Roboterzelle|Schwenkbereich Roboter|Spaeneflug|Spanflug" +for row in $PROJECTS; do + name="${row%%:*}"; pid="${row##*:}" + case "$name" in *_GT) continue;; esac + echo "=== $name ===" + ssh macmini "$PSQL -c \"SELECT category||' | '||name FROM compliance.iace_hazards WHERE project_id='$pid' AND (name ~* '($LEAKS)' OR hazardous_zone ~* '($LEAKS)') ORDER BY name;\"" 2>/dev/null \ + | sed 's/^/ LEAK: /' + cnt=$(ssh macmini "$PSQL -c \"SELECT count(*) FROM compliance.iace_hazards WHERE project_id='$pid' AND (name ~* '($LEAKS)' OR hazardous_zone ~* '($LEAKS)');\"" 2>/dev/null) + echo " -> Leak-Treffer: $cnt" +done diff --git a/ai-compliance-sdk/scripts/seed_new_machines.py b/ai-compliance-sdk/scripts/seed_new_machines.py new file mode 100644 index 00000000..6dd766c3 --- /dev/null +++ b/ai-compliance-sdk/scripts/seed_new_machines.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Seed 4 new generic-validation IACE projects from their Grenzen documents. + +Faithful limits_form reconstructed from the BP risk-assessment .docx files in +breakpilot-core/docs-src. Used to test pattern-engine genericity (no foreign- +domain nonsense) beyond the 2 existing ground truths (Kistenhub + Bremse). + +Run from MacBook against macmini:8093 (TLS, self-signed -> verify off). +""" +import json +import ssl +import urllib.request + +BASE = "https://macmini:8093/sdk/v1/iace" +TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" +CTX = ssl.create_default_context() +CTX.check_hostname = False +CTX.verify_mode = ssl.CERT_NONE + + +def call(method, path, body=None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(BASE + path, data=data, method=method) + req.add_header("Content-Type", "application/json") + req.add_header("X-Tenant-ID", TENANT) + try: + with urllib.request.urlopen(req, context=CTX, timeout=120) as r: + return r.status, json.loads(r.read().decode()) + except urllib.error.HTTPError as e: + return e.code, e.read().decode() + + +MACHINES = [ + { + "machine_name": "Kniehebelpresse (Kaltmassivumformung)", + "machine_type": "mechanical_press", + "manufacturer": "Eigenbau GmbH", + "limits_form": { + "general_description": "Die Anlage ist eine vollautomatische Kniehebelpresse (Kaltmassivumformung). Sie besteht aus Rohlings-Zufuehrung, Transferanlage zur Zufuehrung der Rohlinge in die Presse, der Kniehebelpresse selbst, einer vollstaendig in die Presse integrierten Schmieranlage, einer Absaugung, dem Abtransport der fertigen Teile (Eigenbau) und einer Schutzumhausung. Die Rohlinge werden automatisch zugefuehrt, vor Einbringung in die Presse gewendet und positioniert. In der Presse werden die Teile in 5 Schritten umgeformt; fuer jeden Schritt gibt es einen eigenen Werkzeugsatz. Die fertigen heissen Teile fallen auf ein Foerderband und werden in Kisten transportiert.", + "intended_purpose": "Die Anlage dient der automatischen Kaltmassivumformung von metallischen Rohlingen.", + "foreseeable_misuses": "Ueberschreiten zulaessiger Belastungsgrenzen. Verwendung ungeeigneter Rohlinge. Betrieb in explosionsgefaehrdeter Atmosphaere. Betrieb bei Austreten von Hydraulikoel, Schmierfluessigkeit oder Druckluft. Betrieb bei Rauchentwicklung oder ungewoehnlichen Geraeuschen. Bedienung ohne erforderliche persoenliche Schutzausruestung. Zugang zu abgesicherten Bereichen unter Umgehung der Sicherheitseinrichtungen.", + "spatial_limits": "Aufzugsportal. Zugaenglicher Bereich der Zufuehrung. Presseninnenraum (Werkzeugeinbauraum), erreichbar ueber vordere Tuer. Presseninnenraum, erreichbar ueber hintere Tuer. Bereich um die Abfuehrbaender. Quetsch- und Scherstellen am Stoessel und an den Werkzeugsaetzen.", + "temporal_limits": "Wartungsarbeiten gemaess Anleitungen der Lieferanten der jeweiligen Bauteile. Umruestung auf verschiedene Produkte.", + "technical_data": "Nennkraft 20000 kN. Nennkraftweg vor U.T. 30 mm. Arbeitsvermoegen 400 kJ (vergroessert 600 kJ ab Hubzahl 16/min). Hubzahl 16-36/min. Stoesselhub 630 mm. Stoesselflaeche 1800x1000 mm. Tischflaeche 1800x1000 mm. 5 Stufen, Abstand 350 mm. Kippmoment max. 4500 kNm. Bauhoehe ueber Flur ca. +8600 mm, Fundamenttiefe ca. -5000 mm.", + "electrical_interfaces": "Hauptstromkreis 400 V, 50 Hz. Steuerstromkreis 230 V Wechselspannung und 24 V Gleichspannung. Absauganlage-Motor 3,0 kW, 400 V. Elektrische Komponenten und Steuerung.", + "mechanical_interfaces": "Rohlings-Zufuehrung ueber Aufzugsportal (Kisten ca. 3 m nach oben). Ruettelplatte. Foerderband zur Presse. Stoessel und Werkzeugsaetze. Abfuehrbaender. Bewegliche Teile mit Quetsch- und Scherstellen im Presseninnenraum.", + "pneumatic_hydraulic_interfaces": "Hydraulikoel im System (Oelfangschalen unter dem Staenderdurchgang). Druckluft im System. Schmieranlage vollstaendig in die Presse integriert.", + "software_interfaces": "Vollautomatische Steuerung der Zufuehrung, Bearbeitung und Abfuehrung.", + "operating_conditions": "Zulaessige Umgebungstemperatur +5 bis +40 Grad C. Relative Luftfeuchte nicht betauend 30% bei 40 Grad C bis 95% bei 20 Grad C. Hoehenlage bis 1000 m ueber NN.", + "operating_modes": "Betrieb vollautomatisch. Einrichten und Umruesten. Instandhaltung und Reinigung. Instandsetzung (Reparaturen).", + "person_groups": "Bedienpersonal: eingewiesenes Personal ohne besondere Fachkenntnisse.", + "qualification_requirements": "Bedienpersonal ist eingewiesenes Personal ohne besondere Fachkenntnisse, mit der Funktion der Anlage vertraut. Werkzeugwechsel sowie Instandhaltung und Instandsetzung werden in dieser Beurteilung nicht betrachtet.", + "emergency_stop": "Not-Halt an der Bedieneinheit der Presse.", + "malfunction_interventions": "", + "other_limits": "Organisatorische Schutzmassnahme: Tragen von Sicherheitsschuhen, Gehoerschutz und Handschuhen in der Fertigungshalle (Handschuhe nur fuer Werker, nicht fuer Instandhaltung/Werkzeugwechsel).", + }, + }, + { + "machine_name": "EIGENBAU-Zelle (Arbeitstisch mit Roboterarm)", + "machine_type": "robotics_cobot", + "manufacturer": "Eigenbau GmbH", + "limits_form": { + "general_description": "Die EIGENBAU-Zelle ist ein Arbeitstisch mit integriertem Roboterarm zum Einsatz im industriellen Umfeld. Es werden zwei Ausfuehrungsvarianten betrachtet: nicht kollaborierender Betrieb (ein Sicherheitsscanner setzt den Roboterarm bei Annaeherung von Personen still) und kollaborierender Betrieb fuer nicht gefaehrliche Anwendungen. Der Roboterarm dient in erster Linie dem Bewegen von Teilen, insbesondere der Bestueckung von Maschinen, und kann abhaengig vom gewaehlten Werkzeug einfache Bearbeitungsvorgaenge selbst vornehmen.", + "intended_purpose": "Die EIGENBAU-Zelle ist fuer die Bestueckung und Bedienung von Maschinen und Anlagen vorgesehen. Ergaenzend fuer die Variante kollaborierender Betrieb besteht die Moeglichkeit eines Betriebs im kollaborierenden Modus fuer nicht gefaehrliche Anwendungen.", + "foreseeable_misuses": "Nutzung mit nicht ausreichend programmierten Grenzen fuer Kraft und Geschwindigkeit. Nutzung bei Anwendungen mit unzureichenden Reaktionszeiten der Sicherheitsfunktionen. Verwendung von Werkzeugen mit Nachlauf. Nutzung als Steighilfe. Betrieb ausserhalb der zulaessigen Betriebsparameter. Nutzung in potentiell explosionsgefaehrdeten Umgebungen. Nutzung mit fuer einen kollaborierenden Betrieb nicht geeigneten Werkzeugen oder Werkstuecken.", + "spatial_limits": "Raum in Reichweite des Roboterarms. Oberflaeche des Tischs. Oberflaeche des Roboterarms. Bereich um das verwendete Werkzeug und Werkstueck. Quetsch- und Stossgefahr durch bewegliche Teile des Roboterarms.", + "temporal_limits": "Die korrekte Arbeitsweise aller Sicherheitsfunktionen sollte mindestens einmal jaehrlich ueberprueft werden. Berechnete Betriebsdauer des Roboterarms: 35.000 h.", + "technical_data": "Abmessungen max. 2000x800x1500 mm. Gewicht max. 500 kg. Traglast des Roboterarms 5 kg. Reichweite des Roboterarms 850 mm. Geschwindigkeit max. 180 Grad/s. Geschwindigkeit des Werkzeugs max. 1 m/s.", + "electrical_interfaces": "Spannungsversorgung 230 V. Leistungsaufnahme max. 325 W. Schutzart IP.", + "mechanical_interfaces": "Integrierter Roboterarm zum Bewegen von Teilen. Werkzeug und Werkstueck gemaess Betriebsanleitung. Bewegliche Teile des Roboterarms mit Quetsch- und Stossgefahr in Reichweite.", + "pneumatic_hydraulic_interfaces": "Keine pneumatischen oder hydraulischen Schnittstellen.", + "software_interfaces": "Programmierung des Roboterarms ueber einen in den Tisch integrierten Touchscreen. Sicherheitsscanner zur Personenerkennung in der nicht kollaborierenden Variante. Sicherheitsfunktionen zur Begrenzung von Kraft und Geschwindigkeit.", + "operating_conditions": "Umgebungsbedingungen 0 bis 50 Grad C.", + "operating_modes": "Betrieb im kollaborierenden oder nicht kollaborierenden Modus. Einrichten, Einlernen (Teachen) und Programmieren. Umruesten. Reinigung. Instandhaltung.", + "person_groups": "Laien, eingewiesene Personen, Fachpersonal, Fachpersonal mit gesonderter Schulung durch den Hersteller.", + "qualification_requirements": "Transport: Fachpersonal. Montage, Installation und Inbetriebnahme: Eingewiesene Personen. Einrichten, Teachen, Programmieren und Umruesten: Fachpersonal mit gesonderter Schulung. Betrieb und Reinigung: Eingewiesene Personen. Instandhaltung, Sichtpruefungen, Fehlersuche und Demontage: Fachpersonal.", + "emergency_stop": "", + "malfunction_interventions": "", + "other_limits": "Es bestehen keine weiteren relevanten Grenzen.", + }, + }, + { + "machine_name": "Elektromotoren (Gleichstrom- und Asynchronmotoren)", + "machine_type": "general_industry", + "manufacturer": "Eigenbau GmbH", + "limits_form": { + "general_description": "Die Eigenbau GmbH stellt Gleichstrom- und Asynchronmotoren mit oder ohne integriertes Getriebe her. Die Motoren werden als Antriebe in Maschinen und Anlagen eingebaut.", + "intended_purpose": "Einbau als Antrieb in Maschinen und Anlagen entsprechend den technischen Daten.", + "foreseeable_misuses": "Betrieb unter Wasser. Betrieb in explosionsfaehiger Atmosphaere. Betrieb in aetzender Atmosphaere. Betrieb unter Schock- und Vibrationseinwirkung. Nutzung des Motors als Steighilfe. Nutzung ohne ausreichende Kuehlung oder in zu hoher Umgebungstemperatur. Nutzung ohne angeschlossene Uebertemperaturueberwachung. Automatischer Wiederanlauf nach Ausloesen des Thermoschalters. Fehlerhafte elektrische Installation, zum Beispiel nicht angeschlossener Schutzleiter.", + "spatial_limits": "Gehaeuse (Stromschlag, Erhitzung). Elektrische Anschluesse. Bewegliche Teile des Motors.", + "temporal_limits": "Lebensdauer 2.000 bis 20.000 Stunden. Wartungsintervalle gemaess Herstellerangaben.", + "technical_data": "Spannungsversorgung 24 bis 400 V, Gleichstrom bzw. ein- oder dreiphasiger Wechselstrom. Nennleistung 4,3 W bis 562 W. Nenndrehzahl 1250 rpm bis 3800 rpm. Nennmoment 3,3 cNm bis 27 Nm. Schutzart IP 00 (offene Bauart) bis IP 65. Isolierstoffklasse B oder F.", + "electrical_interfaces": "Energieversorgung 24 bis 400 V, Gleichstrom bzw. ein- oder dreiphasiger Wechselstrom. Anschluesse der Energieversorgung. Uebertemperaturueberwachung ueber Thermowiderstand oder Bimetallschalter. Schutzleiter.", + "mechanical_interfaces": "Bewegliche und rotierende Teile des Motors (Welle). Optional integriertes Getriebe.", + "pneumatic_hydraulic_interfaces": "Keine pneumatischen oder hydraulischen Schnittstellen.", + "software_interfaces": "", + "operating_conditions": "Umgebungstemperatur -20 Grad C bis 40 Grad C.", + "operating_modes": "Betrieb. Montage, Installation und Inbetriebnahme. Reinigung. Instandhaltung. Fehlersuche und -beseitigung. Demontage und Ausserbetriebnahme.", + "person_groups": "Laie, unterwiesene Person, befaehigte Person, Fachpersonal.", + "qualification_requirements": "Transport: Laie. Montage, Installation und Inbetriebnahme: Unterwiesene Person. Betrieb und Reinigung: Laie. Instandhaltung sowie Fehlersuche und -beseitigung: Fachpersonal. Demontage und Ausserbetriebnahme: Unterwiesene Person.", + "emergency_stop": "", + "malfunction_interventions": "Ausloesen des Thermoschalters bei Uebertemperatur.", + "other_limits": "Es bestehen keine weiteren relevanten Grenzen.", + }, + }, + { + "machine_name": "Rundschweissanlage Schwingarm", + "machine_type": "welding", + "manufacturer": "Eigenbau GmbH", + "limits_form": { + "general_description": "Die Rundschweissanlage Schwingarm ist eine Rundnahtschweissanlage als Auf-Tisch-Version. Sie kann durch ein START/STOP-Fusspedal oder einen Fussregler bedient werden. Die Anlage ist mit einem pneumatisch bewegten Brennerarm ausgestattet. Der Reitstock kann zur Vermeidung von Torsionskraeften mit einem Synchronantrieb ausgestattet werden. Die Anlage ermoeglicht es, Formiergas durch die Hohlwelle an das Werkstueck zu fuehren. Die Maschine ist zum Anschluss an MIG/MAG- und TIG-Stromquellen sowie fuer Impulslichtbogen vorgesehen.", + "intended_purpose": "Die Schwingarm Rundschweissanlage dient zum manuellen oder automatisierten Schweissen von Rundnaehten.", + "foreseeable_misuses": "Bearbeitung von Werkstuecken mit mehr als 25 kg Gewicht. Schweissen im Viertaktmodus. Schweissen ohne Masseanschluss.", + "spatial_limits": "Leitfaehige Oberflaechen des Gehaeuses (elektrischer Schlag). Brenner (Verbrennung). Brennerhalterung (Quetschgefahr). Bereich unterhalb der Schweissanlage (Verbrennung, Quetschgefahr).", + "temporal_limits": "Wartungsintervalle gemaess Herstellerangaben (halbjaehrlich, jaehrlich, taeglich, woechentlich).", + "technical_data": "Maximale mechanische Belastung 25 kg. Drehmoment Getriebe A/B/C 33/98/196 Nm. Geschwindigkeitsbereiche 0,01 bis 25 U/min. Kippbereich 0 bis 90 Grad, stufenlos manuell. Maximaler Abstand zwischen den Flanschen 500 bzw. 800 mm. Maximaler Werkstueckdurchmesser 340 mm. Maximaler Schweissstrom DC 350 / AC 270 A. Durchmesser Hohlwelle 28 mm.", + "electrical_interfaces": "Anschluss an MIG/MAG- und TIG-Stromquellen sowie Impulslichtbogen. Maximaler Schweissstrom DC 350 / AC 270 A. Leitfaehige Oberflaechen des Gehaeuses. Masseanschluss erforderlich.", + "mechanical_interfaces": "Werkstuecke werden von Hand mithilfe zweier Backenfutter eingespannt und pneumatisch gesichert. Reitstock, optional mit Synchronantrieb. Rotierende Werkstueckaufnahme. Brennerhalterung mit Quetschgefahr.", + "pneumatic_hydraulic_interfaces": "Pneumatisch bewegter Brennerarm. Pneumatische Sicherung der Werkstuecke in den Backenfuttern.", + "software_interfaces": "Integrierte Steuerung mit Bedienpanel. Bedienung ueber START/STOP-Fusspedal oder Fussregler.", + "operating_conditions": "Verwendung in Innenraeumen; Hoehen bis 2000 m; Umgebungstemperatur 5 Grad C bis 40 Grad C.", + "operating_modes": "Manuelles Schweissen. Automatisiertes Schweissen. Reinigung. Instandhaltung.", + "person_groups": "Laien, unterwiesene Personen, Fachpersonal.", + "qualification_requirements": "Transport: Laien. Aufstellung, Montage, Installation und Inbetriebnahme: Fachpersonal. Betrieb: Unterwiesene Person. Reinigung: Laien nach Einweisung. Instandhaltung und Fehlersuche: Unterwiesene Person (einfache Arbeiten) oder Fachpersonal (komplexere Arbeiten). Demontage und Ausserbetriebnahme: Fachpersonal.", + "emergency_stop": "Bedienung ueber START/STOP-Fusspedal.", + "malfunction_interventions": "", + "other_limits": "Beim Schweissen entstehen Schweissrauch und es wird Formiergas (Schutzgas) eingesetzt. Es bestehen keine weiteren relevanten Grenzen.", + }, + }, +] + + +def main(): + for m in MACHINES: + print("=" * 70) + print(f"MASCHINE: {m['machine_name']} [machine_type={m['machine_type']}]") + status, resp = call("POST", "/projects", { + "machine_name": m["machine_name"], + "machine_type": m["machine_type"], + "manufacturer": m["manufacturer"], + "metadata": {"limits_form": m["limits_form"]}, + }) + if status not in (200, 201): + print(f" CREATE FEHLER {status}: {resp}") + continue + pid = resp.get("project", resp).get("id") if isinstance(resp, dict) else None + print(f" Projekt angelegt: {pid}") + + status, resp = call("POST", f"/projects/{pid}/initialize?force=true") + if status != 200: + print(f" INIT FEHLER {status}: {resp}") + continue + summ = resp.get("summary", {}) + print(f" Seed OK: Komponenten={summ.get('components')} Patterns={summ.get('patterns')} " + f"Hazards={summ.get('hazards')} Massnahmen={summ.get('mitigations')} Normen={summ.get('norms')}") + print(f" PROJECT_ID={pid}") + + +if __name__ == "__main__": + main()