From afb3f83f305e0332920f502fca38514fa4ec3f91 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 10 Jun 2026 17:15:55 +0200 Subject: [PATCH] feat(iace): cross-domain precision overhaul + component review + schema reconcile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../api/handlers/iace_component_assign.go | 86 +++++++++ .../iace_component_assign_phase4_test.go | 32 ++++ .../handlers/iace_component_assign_test.go | 43 +++++ .../api/handlers/iace_handler_init.go | 65 +++++-- .../api/handlers/iace_handler_init_helpers.go | 75 -------- .../handlers/iace_handler_machine_types.go | 21 ++ ai-compliance-sdk/internal/app/app.go | 21 +- ai-compliance-sdk/internal/app/routes_iace.go | 1 + .../internal/iace/document_export_sources.go | 2 +- .../iace/gt_benchmark_harness_test.go | 21 ++ .../internal/iace/hazard_patterns_agv_agri.go | 6 +- .../internal/iace/hazard_patterns_cnc.go | 2 +- .../internal/iace/hazard_patterns_cnc_ext.go | 2 +- .../iace/hazard_patterns_cyber_extended.go | 2 +- .../internal/iace/hazard_patterns_elevator.go | 2 +- .../internal/iace/hazard_patterns_final_b.go | 2 +- .../internal/iace/hazard_patterns_final_c.go | 2 +- .../internal/iace/hazard_patterns_final_d.go | 2 +- .../iace/hazard_patterns_gt_bremse.go | 6 +- .../iace/hazard_patterns_iso12100_gaps.go | 2 +- .../iace/hazard_patterns_maintenance_ext.go | 6 +- .../iace/hazard_patterns_plastics_metal.go | 10 +- .../iace/hazard_patterns_robot_cell.go | 4 +- .../iace/hazard_patterns_specific_machines.go | 32 ++-- .../hazard_patterns_specific_machines2.go | 4 +- .../iace/hazard_patterns_textile_agri.go | 4 +- .../hazard_patterns_welding_glass_textile.go | 6 +- .../internal/iace/hazard_patterns_workshop.go | 8 +- .../internal/iace/keyword_dictionary.go | 13 +- .../internal/iace/machine_types.go | 126 ++++++++++++ ai-compliance-sdk/internal/iace/models_api.go | 3 + .../internal/iace/models_entities.go | 18 ++ .../internal/iace/narrative_negation.go | 78 ++++++++ .../internal/iace/narrative_negation_test.go | 61 ++++++ .../internal/iace/narrative_parser.go | 49 +++-- .../iace/narrative_parser_roller_test.go | 37 ++++ .../internal/iace/pattern_domain_gates.go | 22 +++ .../internal/iace/pattern_precision_test.go | 68 +++++++ .../iace/pattern_reachability_test.go | 103 ++++++++++ .../internal/iace/pattern_relevance.go | 181 ++++++++++++++++++ .../internal/iace/pattern_relevance_test.go | 45 +++++ .../internal/iace/store_components.go | 28 ++- .../152_iace_projects_reconcile.sql | 47 +++++ .../153_iace_children_reconcile.sql | 70 +++++++ .../154_iace_mitigation_constraints.sql | 12 ++ .../155_iace_component_presence.sql | 5 + .../migrations/156_iace_component_ce.sql | 9 + 47 files changed, 1275 insertions(+), 169 deletions(-) create mode 100644 ai-compliance-sdk/internal/api/handlers/iace_component_assign.go create mode 100644 ai-compliance-sdk/internal/api/handlers/iace_component_assign_phase4_test.go create mode 100644 ai-compliance-sdk/internal/api/handlers/iace_component_assign_test.go create mode 100644 ai-compliance-sdk/internal/api/handlers/iace_handler_machine_types.go create mode 100644 ai-compliance-sdk/internal/iace/machine_types.go create mode 100644 ai-compliance-sdk/internal/iace/narrative_negation.go create mode 100644 ai-compliance-sdk/internal/iace/narrative_negation_test.go create mode 100644 ai-compliance-sdk/internal/iace/narrative_parser_roller_test.go create mode 100644 ai-compliance-sdk/internal/iace/pattern_precision_test.go create mode 100644 ai-compliance-sdk/internal/iace/pattern_reachability_test.go create mode 100644 ai-compliance-sdk/internal/iace/pattern_relevance.go create mode 100644 ai-compliance-sdk/internal/iace/pattern_relevance_test.go create mode 100644 ai-compliance-sdk/migrations/152_iace_projects_reconcile.sql create mode 100644 ai-compliance-sdk/migrations/153_iace_children_reconcile.sql create mode 100644 ai-compliance-sdk/migrations/154_iace_mitigation_constraints.sql create mode 100644 ai-compliance-sdk/migrations/155_iace_component_presence.sql create mode 100644 ai-compliance-sdk/migrations/156_iace_component_ce.sql diff --git a/ai-compliance-sdk/internal/api/handlers/iace_component_assign.go b/ai-compliance-sdk/internal/api/handlers/iace_component_assign.go new file mode 100644 index 00000000..1b5ce915 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_component_assign.go @@ -0,0 +1,86 @@ +package handlers + +import ( + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/google/uuid" +) + +// componentReviewDecision captures an expert's manual review of an auto-detected +// component (presence move, CE marking, safety relevance) so a force re-seed can +// restore it instead of wiping it. Keyed by normalised component name — the +// deterministic narrative parser re-derives the same names. +type componentReviewDecision struct { + presence string + ceMarked bool + safetyRelevant bool +} + +// resolvePresence returns the presence_status for a freshly parsed component: +// the expert's prior decision wins; otherwise the engine's negation verdict +// (negated → nicht_vorhanden, else vorhanden). +func resolvePresence(dec componentReviewDecision, hasDecision, negated bool) string { + if hasDecision && dec.presence != "" { + return dec.presence + } + if negated { + return iace.PresenceAbsent + } + return iace.PresencePresent +} + +// pickComponentForPattern links a matched hazard pattern to the project +// component that most plausibly causes it. +// +// matchedTags are the component/energy tags that actually made the pattern fire +// (RequiredComponentTags + RequiredEnergyTags satisfied by the machine). The +// project component whose library tags overlap them best is treated as the +// cause. Falls back to zone-name overlap, then to the default (first) component. +// +// Without this, every hazard defaulted to the first component, so the knowledge +// graph drew all "erzeugt" edges from a single node. +func pickComponentForPattern( + matchedTags []string, + zoneDE string, + parseComps []iace.ComponentMatch, + compByName map[string]uuid.UUID, + defaultCompID uuid.UUID, +) uuid.UUID { + if len(matchedTags) > 0 { + want := make(map[string]bool, len(matchedTags)) + for _, t := range matchedTags { + want[t] = true + } + bestScore := 0 + bestID := uuid.Nil + for _, c := range parseComps { + id, ok := compByName[iace.NormalizeDEPublic(c.NameDE)] + if !ok { + continue + } + score := 0 + for _, t := range c.Tags { + if want[t] { + score++ + } + } + if score > bestScore { + bestScore = score + bestID = id + } + } + if bestScore > 0 { + return bestID + } + } + + if zoneDE != "" { + zoneNorm := iace.NormalizeDEPublic(zoneDE) + for cName, cID := range compByName { + if containsSubstring(zoneNorm, cName) || containsSubstring(cName, zoneNorm) { + return cID + } + } + } + + return defaultCompID +} diff --git a/ai-compliance-sdk/internal/api/handlers/iace_component_assign_phase4_test.go b/ai-compliance-sdk/internal/api/handlers/iace_component_assign_phase4_test.go new file mode 100644 index 00000000..a2b858a4 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_component_assign_phase4_test.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "testing" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" +) + +func TestResolvePresence(t *testing.T) { + none := componentReviewDecision{} + tests := []struct { + name string + dec componentReviewDecision + has bool + negated bool + want string + }{ + {"no decision, present", none, false, false, iace.PresencePresent}, + {"no decision, negated", none, false, true, iace.PresenceAbsent}, + {"expert un-negated wins", componentReviewDecision{presence: iace.PresencePresent}, true, true, iace.PresencePresent}, + {"expert negated wins", componentReviewDecision{presence: iace.PresenceAbsent}, true, false, iace.PresenceAbsent}, + {"expert deleted persists", componentReviewDecision{presence: iace.PresenceDeleted}, true, false, iace.PresenceDeleted}, + {"empty decision falls back to engine", componentReviewDecision{presence: ""}, true, true, iace.PresenceAbsent}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := resolvePresence(tt.dec, tt.has, tt.negated); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/ai-compliance-sdk/internal/api/handlers/iace_component_assign_test.go b/ai-compliance-sdk/internal/api/handlers/iace_component_assign_test.go new file mode 100644 index 00000000..3fe697b1 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_component_assign_test.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "testing" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/google/uuid" +) + +func TestPickComponentForPattern(t *testing.T) { + motor := uuid.New() + hubwerk := uuid.New() + def := uuid.New() + parseComps := []iace.ComponentMatch{ + {NameDE: "Elektromotor (Drehstrom)", Tags: []string{"electrical", "rotating_part"}}, + {NameDE: "Hubwerk", Tags: []string{"moving_part", "crush_point"}}, + } + compByName := map[string]uuid.UUID{ + iace.NormalizeDEPublic("Elektromotor (Drehstrom)"): motor, + iace.NormalizeDEPublic("Hubwerk"): hubwerk, + } + + tests := []struct { + name string + matchedTags []string + zone string + want uuid.UUID + }{ + {"electrical tag → motor", []string{"electrical"}, "", motor}, + {"crush tags → hubwerk", []string{"crush_point", "moving_part"}, "", hubwerk}, + {"no overlap → default", []string{"unknown_tag"}, "", def}, + {"zone fallback → hubwerk", nil, "Gefahr am Hubwerk-Bereich", hubwerk}, + {"nothing → default", nil, "", def}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pickComponentForPattern(tt.matchedTags, tt.zone, parseComps, compByName, def) + if got != tt.want { + t.Errorf("got %v, want %v", got, tt.want) + } + }) + } +} 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 5a17e568..8890128c 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -47,6 +47,11 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { steps := make([]InitStep, 0, 6) + // Phase 4: preserve the expert's component review across a force re-seed. + // Captured (by normalised name) before deletion, re-applied after the fresh + // parse so manual presence moves, CE marks and safety flags are not wiped. + componentDecisions := make(map[string]componentReviewDecision) + // ── Step 0 (optional): Clear existing data for force re-init ── if forceReinit { cleared := 0 @@ -62,6 +67,19 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { cleared++ } } + // Also clear components so the fresh narrative parse re-creates them — + // but first remember the expert's review so it survives the re-seed. + if comps, _ := h.store.ListComponents(ctx, projectID); len(comps) > 0 { + for _, cp := range comps { + componentDecisions[iace.NormalizeDEPublic(cp.Name)] = componentReviewDecision{ + presence: cp.PresenceStatus, + ceMarked: cp.CEMarked, + safetyRelevant: cp.IsSafetyRelevant, + } + _ = h.store.DeleteComponent(ctx, cp.ID) + cleared++ + } + } steps = append(steps, InitStep{Name: "Alte Daten geloescht", Status: "done", Count: cleared}) } @@ -90,11 +108,15 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { created := 0 for _, comp := range parseResult.Components { compType := deriveComponentType(comp.Tags) + dec, hasDecision := componentDecisions[iace.NormalizeDEPublic(comp.NameDE)] _, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ - ProjectID: projectID, - Name: comp.NameDE, - ComponentType: compType, - Description: "Auto-erkannt aus Maschinenbeschreibung (" + comp.MatchedOn + ")", + ProjectID: projectID, + Name: comp.NameDE, + ComponentType: compType, + Description: "Auto-erkannt aus Maschinenbeschreibung (" + comp.MatchedOn + ")", + PresenceStatus: resolvePresence(dec, hasDecision, comp.Negated), + CEMarked: dec.ceMarked, + IsSafetyRelevant: dec.safetyRelevant, }) if cerr == nil { created++ @@ -110,6 +132,10 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { // ── Step 4: Fire pattern engine ── var componentIDs, energyIDs []string for _, comp := range parseResult.Components { + dec, hasDecision := componentDecisions[iace.NormalizeDEPublic(comp.NameDE)] + if resolvePresence(dec, hasDecision, comp.Negated) != iace.PresencePresent { + continue // negated/deleted (engine verdict OR expert decision) → no matching + } componentIDs = append(componentIDs, comp.LibraryID) } for _, e := range parseResult.EnergySources { @@ -118,6 +144,14 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { operationalStates := mergeStringSlices(parseResult.OperationalStates, extractOperationalStatesFromMetadata(project.Metadata)) machineTypes := extractIndustrySectorsFromMetadata(project.Metadata) + // The project's own machine type MUST reach the engine's machine-type gate, + // otherwise (empty input) every pattern scoped to a foreign machine class + // fires — press, CNC, excavator, crane, road-roller, medical ... For a lift + // this floods the result with cross-domain nonsense. With it set, those + // patterns are gated out; only this machine class + ungated patterns remain. + if project.MachineType != "" { + machineTypes = append(machineTypes, project.MachineType) + } engine := iace.NewPatternEngine() matchOutput := engine.Match(iace.MatchInput{ @@ -143,7 +177,15 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { hazardPatternMeasures := make(map[uuid.UUID][]string) if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 { - comps, _ := h.store.ListComponents(ctx, projectID) + allComps, _ := h.store.ListComponents(ctx, projectID) + // Hazards only attach to PRESENT components — negated/deleted ones are + // surfaced for review but never own a generated hazard. + comps := make([]iace.Component, 0, len(allComps)) + for _, c := range allComps { + if c.PresenceStatus == iace.PresencePresent || c.PresenceStatus == "" { + comps = append(comps, c) + } + } var defaultCompID uuid.UUID compByName := make(map[string]uuid.UUID) if len(comps) > 0 { @@ -164,7 +206,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { catCount := make(map[string]int) for _, mp := range matchOutput.MatchedPatterns { // Narrative relevance filter - if !isPatternRelevant(mp, narrativeText, compNames) { + if !iace.IsPatternRelevant(mp, narrativeText, compNames) { continue } @@ -198,16 +240,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { name = name + " (" + mp.ZoneDE + ")" } - compID := defaultCompID - if mp.ZoneDE != "" { - zoneNorm := iace.NormalizeDEPublic(mp.ZoneDE) - for cName, cID := range compByName { - if containsSubstring(zoneNorm, cName) || containsSubstring(cName, zoneNorm) { - compID = cID - break - } - } - } + compID := pickComponentForPattern(mp.MatchedTags, mp.ZoneDE, parseResult.Components, compByName, defaultCompID) // Join all applicable lifecycles as comma-separated string lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",") diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go index b389f04a..53031921 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go @@ -339,81 +339,6 @@ func containsSubstring(haystack, needle string) bool { ) } -// genericSafetyTerms are words that appear in almost all risk assessments -// and should NOT be used to determine machine-specificity. -var genericSafetyTerms = map[string]bool{ - "maschine": true, "anlage": true, "bereich": true, "gesamte": true, - "arbeitsplatz": true, "gefahrbereich": true, "gefahrstelle": true, - "gefahrenstelle": true, "person": true, "werker": true, "bediener": true, - "steuerung": true, "schutzeinrichtung": true, "sicherheit": true, - "betrieb": true, "wartung": true, "instandhaltung": true, "reinigung": true, - "bewegung": true, "beweglich": true, "feststehend": true, "teil": true, - "teile": true, "oeffnung": true, "zugang": true, "gefahr": true, - "verletzung": true, "quetsch": true, "scher": true, "schneid": true, - "stoss": true, "schlag": true, "einzug": true, "brand": true, - "motor": true, "antrieb": true, "achse": true, "achsen": true, - "kabel": true, "leitung": true, "schaltschrank": true, "spannung": true, - "schutz": true, "gehaeuse": true, "oberflaeche": true, "boden": true, - "leitfaehig": true, "elektrisch": true, "mechanisch": true, - "bedienfeld": true, "display": true, "anzeige": true, - "energie": true, "druck": true, "temperatur": true, - // Abbreviations and synonyms that should not trigger relevance filter - "kss": true, "emv": true, "esd": true, "dcs": true, "plr": true, "sil": true, - "hmi": true, "sps": true, "rcd": true, "loto": true, "psa": true, - // Common action words - "bersten": true, "platzen": true, "abspringen": true, "spritzen": true, - "einatmen": true, "ausrutschen": true, "herabfallen": true, - "durchschlaegen": true, "wegschleudern": true, - // Common structural terms that don't indicate a specific machine - "gesamter": true, "gesamtes": true, "bereichs": true, "stelle": true, - "innen": true, "aussen": true, "transport": true, "seite": true, - "front": true, "rueck": true, "ober": true, "unter": true, - "fuehrung": true, "lager": true, "verschleiss": true, "welle": true, - "getriebe": true, "kette": true, "riemen": true, "feder": true, - "spindel": true, "werkzeug": true, "werkstueck": true, "flucht": true, -} - -// isPatternRelevant checks whether a pattern match is relevant to the actual -// machine described in the narrative. Uses narrative vocabulary overlap: -// if the pattern's zone/scenario contains machine-specific words (not generic -// safety terms) and NONE of them appear in the narrative → irrelevant. -func isPatternRelevant(mp iace.PatternMatch, narrative string, compNames []string) bool { - patternText := iace.NormalizeDEPublic(mp.ZoneDE + " " + mp.ScenarioDE + " " + mp.PatternName) - narrativeNorm := iace.NormalizeDEPublic(narrative) - - // Extract machine-specific words from pattern (not generic safety terms) - patternWords := strings.Fields(patternText) - var specificWords []string - for _, w := range patternWords { - // Clean punctuation - w = strings.Trim(w, ".,;:!?()/-") - if len(w) < 5 || genericSafetyTerms[w] { - continue - } - specificWords = append(specificWords, w) - } - - // If pattern has no specific words, it's generic → always relevant - if len(specificWords) == 0 { - return true - } - - // Check if at least one specific word appears in the narrative or components - for _, sw := range specificWords { - if strings.Contains(narrativeNorm, sw) { - return true - } - for _, cn := range compNames { - if strings.Contains(cn, sw) { - return true - } - } - } - - // No specific word found in narrative → pattern is for a different machine - return false -} - // categoryHazardCap returns the maximum number of hazards to generate per category. // Caps are based on typical ISO 12100 risk assessment proportions: // - Core physical categories (mechanical, electrical): scale with component count diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_machine_types.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_machine_types.go new file mode 100644 index 00000000..3c6e4561 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_machine_types.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "net/http" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/gin-gonic/gin" +) + +// ListMachineTypes handles GET /machine-types +// Returns the controlled machine-type vocabulary the pattern engine gates on, +// each with a German label + UI group. The project-create form uses this so the +// machine type always matches the engine vocabulary (no free-text flood or +// under-coverage from a value that matches no pattern). +func (h *IACEHandler) ListMachineTypes(c *gin.Context) { + types := iace.MachineTypeVocabulary() + c.JSON(http.StatusOK, gin.H{ + "machine_types": types, + "total": len(types), + }) +} diff --git a/ai-compliance-sdk/internal/app/app.go b/ai-compliance-sdk/internal/app/app.go index f584db5c..b01b3769 100644 --- a/ai-compliance-sdk/internal/app/app.go +++ b/ai-compliance-sdk/internal/app/app.go @@ -39,7 +39,26 @@ func Run() { } ctx := context.Background() - pool, err := pgxpool.New(ctx, cfg.DatabaseURL) + poolCfg, err := pgxpool.ParseConfig(cfg.DatabaseURL) + if err != nil { + log.Fatalf("Failed to parse database URL: %v", err) + } + // The iace/compliance tables live in the `compliance` schema (see CLAUDE.md: + // "DB search_path: compliance,core,public"). Set it explicitly so the + // connection does not silently resolve to `public` (an empty/legacy schema) + // when the URL carries no search_path — as happened on the local dev DB. + // Only set when not already specified in the URL, so prod stays untouched. + if poolCfg.ConnConfig.RuntimeParams == nil { + poolCfg.ConnConfig.RuntimeParams = map[string]string{} + } + if poolCfg.ConnConfig.RuntimeParams["search_path"] == "" { + searchPath := os.Getenv("DB_SEARCH_PATH") + if searchPath == "" { + searchPath = "compliance,core,public" + } + poolCfg.ConnConfig.RuntimeParams["search_path"] = searchPath + } + pool, err := pgxpool.NewWithConfig(ctx, poolCfg) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } diff --git a/ai-compliance-sdk/internal/app/routes_iace.go b/ai-compliance-sdk/internal/app/routes_iace.go index 0c1da632..611e93f1 100644 --- a/ai-compliance-sdk/internal/app/routes_iace.go +++ b/ai-compliance-sdk/internal/app/routes_iace.go @@ -22,6 +22,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.GET("/norms-library/crossref", h.ListNormCrossRefs) iaceRoutes.GET("/norms-library/:id/crossref", h.GetNormCrossRef) iaceRoutes.GET("/lifecycle-phases", h.ListLifecyclePhases) + iaceRoutes.GET("/machine-types", h.ListMachineTypes) iaceRoutes.GET("/roles", h.ListRoles) iaceRoutes.GET("/evidence-types", h.ListEvidenceTypes) iaceRoutes.GET("/protective-measures-library", h.ListProtectiveMeasures) diff --git a/ai-compliance-sdk/internal/iace/document_export_sources.go b/ai-compliance-sdk/internal/iace/document_export_sources.go index 1134dc91..e7df2b5f 100644 --- a/ai-compliance-sdk/internal/iace/document_export_sources.go +++ b/ai-compliance-sdk/internal/iace/document_export_sources.go @@ -94,7 +94,7 @@ func extractCitedNorms(hz []Hazard, mt []Mitigation) []string { seen := make(map[string]bool) consider := func(s string) { fields := strings.FieldsFunc(s, func(r rune) bool { - return r == ' ' || r == ',' || r == ';' || r == '\n' || r == ';' || r == '(' + return r == ' ' || r == ',' || r == ';' || r == '\n' || r == '(' }) for i := 0; i < len(fields)-1; i++ { head := strings.ToUpper(strings.TrimSpace(fields[i])) 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 142908eb..d92f95fb 100644 --- a/ai-compliance-sdk/internal/iace/gt_benchmark_harness_test.go +++ b/ai-compliance-sdk/internal/iace/gt_benchmark_harness_test.go @@ -214,6 +214,27 @@ var foreignDomainTerms = map[string]string{ "schweissen": "welding", "lichtbogenschweiss": "welding", "rolltreppe": "escalator", "fahrtreppe": "escalator", "spinnerei ": "textile", "extrusion": "plastics", + // construction / mobile machinery + "radlader": "construction", "bagger": "construction", "mobilkran": "crane", + "betonpump": "construction", "strassenwalze": "construction", "strassenbau": "construction", + // press / forming tool space + "werkzeugeinbauraum": "press", "stoessel": "press", "oberwerkzeug": "press", + "unterwerkzeug": "press", "abfuellstempel": "filling", + // machining coolant + "kss-": "machining", "kuehlschmierstoff": "machining", + // confined space / bulk material + "silo": "bulk", "gaerbehaelter": "bulk", "getreidesilo": "bulk", "mehlsilo": "bulk", + "schuettgut": "bulk", "sauerstoffmangel": "confined_space", "erstickung": "confined_space", + // medical + "patient": "medical", "sterilis": "medical", "defibrill": "medical", + // outdoor / biological / cold + "zecke": "outdoor", "hantavirus": "outdoor", "schimmel": "environmental", + "nagerkot": "outdoor", "winterarbeit": "outdoor", "tiefkuehl": "cold", "unterkuehl": "cold", + // playground / fitness + "klettergeraet": "playground", "spielplatz": "playground", "kraftstation": "fitness", + "bankdrueck": "fitness", "kniebeug": "fitness", + // palletizer + "palettierer": "palletizer", } // TestGT_DomainLeakage names the patterns that leak across domains. For each GT diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_agv_agri.go b/ai-compliance-sdk/internal/iace/hazard_patterns_agv_agri.go index 14543154..bec3cb7a 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_agv_agri.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_agv_agri.go @@ -117,7 +117,7 @@ func GetAGVAgriPatterns() []HazardPattern { { ID: "HP206", NameDE: "Batteriebrand im AGV", NameEN: "Battery fire in AGV", RequiredComponentTags: []string{"agv", "battery"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"thermal_hazard", "material_environmental"}, SuggestedMeasureIDs: []string{"M124", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E20"}, @@ -132,7 +132,7 @@ func GetAGVAgriPatterns() []HazardPattern { { ID: "HP207", NameDE: "Quetschen beim automatischen Laden", NameEN: "Crushing during automatic charging", RequiredComponentTags: []string{"agv", "pinch_point", "battery"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"mechanical_hazard", "electrical_hazard"}, SuggestedMeasureIDs: []string{"M001", "M054"}, SuggestedEvidenceIDs: []string{"E01", "E08"}, @@ -193,7 +193,7 @@ func GetAGVAgriPatterns() []HazardPattern { { ID: "HP211", NameDE: "EMV-Stoerung deaktiviert AGV-Sicherheit", NameEN: "EMI disables AGV safety systems", RequiredComponentTags: []string{"agv", "sensor_part", "electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard", "safety_function_failure"}, SuggestedMeasureIDs: []string{"M478", "M479", "M141"}, SuggestedEvidenceIDs: []string{"E01"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_cnc.go b/ai-compliance-sdk/internal/iace/hazard_patterns_cnc.go index 282197a5..12f8e4c9 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_cnc.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_cnc.go @@ -276,7 +276,7 @@ func GetCNCHazardPatterns() []HazardPattern { { ID: "HP1418", NameDE: "Elektrischer Schlag durch Schweissgeraet", NameEN: "Electric shock from welding equipment", RequiredComponentTags: []string{"welding_equipment"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M087", "M090", "MN025"}, SuggestedEvidenceIDs: []string{"E01", "E10"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_cnc_ext.go b/ai-compliance-sdk/internal/iace/hazard_patterns_cnc_ext.go index c7456f5f..ea6f6a68 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_cnc_ext.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_cnc_ext.go @@ -211,7 +211,7 @@ func GetCNCHazardPatternsExt() []HazardPattern { { ID: "HP1434", NameDE: "Restkuehlmittel tropft auf elektrische Komponenten", NameEN: "Residual coolant dripping on electrical components", RequiredComponentTags: []string{"cutting_tool"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M087", "M274"}, SuggestedEvidenceIDs: []string{"E01", "E10"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_cyber_extended.go b/ai-compliance-sdk/internal/iace/hazard_patterns_cyber_extended.go index caba0af6..466b66df 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_cyber_extended.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_cyber_extended.go @@ -101,7 +101,7 @@ func GetCyberExtendedPatterns() []HazardPattern { { ID: "HP806", NameDE: "Datenverlust nach Spannungsausfall", NameEN: "Data loss after power failure", RequiredComponentTags: []string{"has_software"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"software_fault"}, SuggestedMeasureIDs: []string{"M103", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E14"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_elevator.go b/ai-compliance-sdk/internal/iace/hazard_patterns_elevator.go index 0e79932c..5c1a3482 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_elevator.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_elevator.go @@ -164,7 +164,7 @@ func GetElevatorPatterns() []HazardPattern { ID: "HP183", NameDE: "Elektrischer Schlag im Triebwerksraum", NameEN: "Electric shock in machine room", MachineTypes: []string{"elevator", "lift", "escalator"}, RequiredComponentTags: []string{"elevator_traction", "electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M051", "M054", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E08"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_final_b.go b/ai-compliance-sdk/internal/iace/hazard_patterns_final_b.go index 74d392fe..c33d3681 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_final_b.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_final_b.go @@ -56,7 +56,7 @@ func GetFinalPatternsB() []HazardPattern { { ID: "HP1089", NameDE: "Elektrostatische Entladung", NameEN: "Electrostatic discharge", RequiredComponentTags: []string{"electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M088", "M329", "M141"}, SuggestedEvidenceIDs: []string{"E01"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_final_c.go b/ai-compliance-sdk/internal/iace/hazard_patterns_final_c.go index 5642bb7a..d8ae04bb 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_final_c.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_final_c.go @@ -190,7 +190,7 @@ func GetFinalPatternsC() []HazardPattern { { ID: "HP1185", NameDE: "Sensor-Kurzschluss durch Feuchtigkeit", NameEN: "Sensor short by moisture", RequiredComponentTags: []string{"sensor_part", "electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"sensor_fault", "electrical_hazard"}, SuggestedMeasureIDs: []string{"M119", "M214", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E06"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_final_d.go b/ai-compliance-sdk/internal/iace/hazard_patterns_final_d.go index 4c7e64c8..5bf94c2f 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_final_d.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_final_d.go @@ -971,7 +971,7 @@ func GetFinalPatternsD() []HazardPattern { { ID: "HP1334", NameDE: "Statische Aufladung Schuettgut", NameEN: "Static charge bulk material", RequiredComponentTags: []string{"chemical_risk", "structural_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"fire_explosion", "electrical_hazard"}, SuggestedMeasureIDs: []string{"M088", "M329", "M385", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E06"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_gt_bremse.go b/ai-compliance-sdk/internal/iace/hazard_patterns_gt_bremse.go index 983d3750..3d70e684 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_gt_bremse.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_gt_bremse.go @@ -56,7 +56,7 @@ func GetGTBremseHazardPatterns() []HazardPattern { { ID: "HP1712", NameDE: "Augen-/Hautverletzung durch Druckluft-Reinigungsduese in Bearbeitungszelle", NameEN: "Eye/skin injury from compressed-air cleaning nozzle in machining cell", RequiredComponentTags: []string{}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M504", "M505", "M501"}, Priority: 97, @@ -137,7 +137,7 @@ func GetGTBremseHazardPatterns() []HazardPattern { { ID: "HP1716", NameDE: "Kurzschluss/Brand durch Reinigung am elektrisch aktiven Schaltschrank", NameEN: "Short circuit/fire from cleaning at live cabinet", RequiredComponentTags: []string{"electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M521", "M522", "M539"}, Priority: 96, @@ -158,7 +158,7 @@ func GetGTBremseHazardPatterns() []HazardPattern { { ID: "HP1717", NameDE: "Verletzung durch unvermittelt austretende pneumatische Restenergie", NameEN: "Injury from unexpectedly released pneumatic stored energy", RequiredComponentTags: []string{"stored_energy"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M485", "M534", "M527"}, Priority: 96, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_iso12100_gaps.go b/ai-compliance-sdk/internal/iace/hazard_patterns_iso12100_gaps.go index 82bb0853..e416e4f2 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_iso12100_gaps.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_iso12100_gaps.go @@ -13,7 +13,7 @@ func GetISO12100GapPatterns() []HazardPattern { { ID: "HP1900", NameDE: "Verletzung durch Vakuum (Vakuumgreifer / Saug-Anlage)", NameEN: "Vacuum injury (suction/vacuum equipment)", RequiredComponentTags: []string{"clamping_part"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M061", "M002", "M141"}, Priority: 90, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go b/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go index 621dc4ad..72976a7e 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_maintenance_ext.go @@ -112,7 +112,7 @@ func GetMaintenanceExtPatterns() []HazardPattern { HarmDE: "Quetschung der Fuesse", AffectedDE: "Einrichter", ZoneDE: "Spannbereich", DefaultSeverity: 3, DefaultExposure: 4}, {ID: "HP715", NameDE: "Stromschlag bei Steckerwechsel", NameEN: "Shock during connector change", - RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical_energy"}, RequiredLifecycles: []string{"setup"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M522", "M539", "M518", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 65, ScenarioDE: "Steckverbinder unter Spannung gewechselt", TriggerDE: "Nicht spannungsfrei", @@ -312,7 +312,7 @@ func GetMaintenanceExtPatterns() []HazardPattern { DefaultSeverity: 3, DefaultExposure: 2}, // — Reinigung (HP913-HP917) — {ID: "HP913", NameDE: "Nassreinigung nahe Elektrik", NameEN: "Wet cleaning near electrics", - RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical_energy"}, RequiredLifecycles: []string{"cleaning"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 75, ScenarioDE: "Wasser gelangt in Schaltschrank", TriggerDE: "Hochdruckreiniger nahe Elektrik", @@ -407,7 +407,7 @@ func GetMaintenanceExtPatterns() []HazardPattern { HarmDE: "Erfassen, Quetschen", AffectedDE: "Pruefpersonal", ZoneDE: "Maschinenarbeitsraum", DefaultSeverity: 5, DefaultExposure: 2}, {ID: "HP926", NameDE: "Messung an spannungsfuehrender Anlage", NameEN: "Measurement on energized system", - RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical_energy"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 75, ScenarioDE: "Messung unter Spannung", TriggerDE: "Messgeraet rutscht ab", diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_plastics_metal.go b/ai-compliance-sdk/internal/iace/hazard_patterns_plastics_metal.go index ea8fb3a9..d2362d94 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_plastics_metal.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_plastics_metal.go @@ -26,7 +26,7 @@ func GetPlasticsMetalPatterns() []HazardPattern { { ID: "HP501", NameDE: "Quetschen durch Schliesseinheit Spritzgiessmaschine", NameEN: "Crushing by injection moulding clamping unit", RequiredComponentTags: []string{"crush_point", "moving_part"}, - RequiredEnergyTags: []string{"hydraulic"}, + RequiredEnergyTags: []string{"hydraulic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedEvidenceIDs: []string{"E08", "E09"}, @@ -41,7 +41,7 @@ func GetPlasticsMetalPatterns() []HazardPattern { { ID: "HP502", NameDE: "Hochdruck-Injektion von Kunststoff in die Hand", NameEN: "High-pressure injection of plastic into hand", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"hydraulic"}, + RequiredEnergyTags: []string{"hydraulic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M003", "M082", "M141"}, SuggestedEvidenceIDs: []string{"E09", "E20"}, @@ -88,7 +88,7 @@ func GetPlasticsMetalPatterns() []HazardPattern { { ID: "HP505", NameDE: "Platzen des Blasformwerkzeugs", NameEN: "Blow mould burst", RequiredComponentTags: []string{"high_pressure", "structural_part"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M001", "M003", "M141"}, SuggestedEvidenceIDs: []string{"E09", "E20"}, @@ -209,7 +209,7 @@ func GetPlasticsMetalPatterns() []HazardPattern { { ID: "HP513", NameDE: "Hydraulikversagen Schliessdruck", NameEN: "Hydraulic failure of clamping pressure", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"hydraulic"}, + RequiredEnergyTags: []string{"hydraulic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M003", "M082", "M141"}, SuggestedEvidenceIDs: []string{"E09", "E20"}, @@ -442,7 +442,7 @@ func GetPlasticsMetalPatterns() []HazardPattern { { ID: "HP528", NameDE: "Hydraulikoelnebel in Atemluft", NameEN: "Hydraulic oil mist in breathing air", RequiredComponentTags: []string{"chemical_risk", "high_pressure"}, - RequiredEnergyTags: []string{"hydraulic"}, + RequiredEnergyTags: []string{"hydraulic_pressure"}, GeneratedHazardCats: []string{"material_environmental"}, SuggestedMeasureIDs: []string{"M141"}, SuggestedEvidenceIDs: []string{"E20"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_robot_cell.go b/ai-compliance-sdk/internal/iace/hazard_patterns_robot_cell.go index 83f05d2e..65e9b8e3 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_robot_cell.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_robot_cell.go @@ -377,7 +377,7 @@ func GetRobotCellPatterns() []HazardPattern { { ID: "HP1641", NameDE: "Gefaehrliche Beruehrungsspannung durch Schutzleiterfehler", NameEN: "Dangerous touch voltage due to PE failure", RequiredComponentTags: []string{}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M511", "M512", "M514", "M515", "M475"}, Priority: 98, @@ -394,7 +394,7 @@ func GetRobotCellPatterns() []HazardPattern { { ID: "HP1642", NameDE: "Kabelbrand durch Ueberlast oder Kurzschluss", NameEN: "Cable fire from overload or short circuit", RequiredComponentTags: []string{}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M009"}, Priority: 98, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines.go b/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines.go index af3cd72e..f89d2af9 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines.go @@ -11,7 +11,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP730", NameDE: "Bersten eines Druckbehaelters", NameEN: "Pressure vessel burst", RequiredComponentTags: []string{"high_pressure", "structural_part"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E11", "E20"}, @@ -26,7 +26,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP731", NameDE: "Dampfaustritt an Flanschverbindung", NameEN: "Steam leak at flange connection", RequiredComponentTags: []string{"high_pressure", "high_temperature"}, - RequiredEnergyTags: []string{"thermal", "pneumatic"}, + RequiredEnergyTags: []string{"thermal", "pneumatic_pressure"}, GeneratedHazardCats: []string{"thermal_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M005", "M141"}, SuggestedEvidenceIDs: []string{"E10", "E11", "E20"}, @@ -41,7 +41,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP732", NameDE: "Sicherheitsventil klemmt unter Druck", NameEN: "Safety valve stuck under pressure", RequiredComponentTags: []string{"high_pressure", "safety_device"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"safety_function_failure", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M104", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E08", "E11"}, @@ -58,7 +58,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP733", NameDE: "Wasserschlag in Rohrleitung", NameEN: "Water hammer in pipeline", RequiredComponentTags: []string{"high_pressure", "structural_part"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E11"}, @@ -103,7 +103,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP736", NameDE: "Druckstoss bei schnellem Ventilschluss", NameEN: "Pressure surge from rapid valve closure", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E11"}, @@ -133,7 +133,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP738", NameDE: "Bersten des Schauglases am Druckbehaelter", NameEN: "Sight glass burst on pressure vessel", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M003", "M005", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E11", "E20"}, @@ -148,7 +148,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP739", NameDE: "Dampfkessel Trockenlauf", NameEN: "Boiler dry firing", RequiredComponentTags: []string{"high_pressure", "high_temperature"}, - RequiredEnergyTags: []string{"thermal", "pneumatic"}, + RequiredEnergyTags: []string{"thermal", "pneumatic_pressure"}, GeneratedHazardCats: []string{"thermal_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M104", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E08", "E11"}, @@ -199,7 +199,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP742", NameDE: "Oeluebertritt in Druckluftsystem", NameEN: "Oil carry-over into compressed air system", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"material_environmental"}, SuggestedMeasureIDs: []string{"M141"}, SuggestedEvidenceIDs: []string{"E01", "E20"}, @@ -214,7 +214,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP743", NameDE: "Druckluftschlauch reisst und peitscht", NameEN: "Compressed air hose rupture and whiplash", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M003", "M005", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E11", "E20"}, @@ -229,7 +229,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP744", NameDE: "Pulsation in Druckleitung", NameEN: "Pulsation in pressure line", RequiredComponentTags: []string{"high_pressure", "structural_part"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M141"}, SuggestedEvidenceIDs: []string{"E01", "E11"}, @@ -278,7 +278,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP747", NameDE: "Brand in Gondel einer Windenergieanlage", NameEN: "Fire in wind turbine nacelle", RequiredComponentTags: []string{"high_temperature", "electrical_part"}, - RequiredEnergyTags: []string{"electrical", "thermal"}, + RequiredEnergyTags: []string{"electrical_energy", "thermal"}, GeneratedHazardCats: []string{"thermal_hazard", "electrical_hazard"}, SuggestedMeasureIDs: []string{"M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, @@ -308,7 +308,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP749", NameDE: "Blitzschlag an Windturbine", NameEN: "Lightning strike on wind turbine", RequiredComponentTags: []string{"structural_part", "electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, @@ -327,7 +327,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP750", NameDE: "Lichtbogen an DC-Steckverbindung", NameEN: "Arc fault at DC connector", RequiredComponentTags: []string{"electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard", "thermal_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, @@ -357,7 +357,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP752", NameDE: "Elektrischer Schlag DC-Seite (Spannung bei Abschaltung)", NameEN: "DC shock (voltage present even when isolated)", RequiredComponentTags: []string{"electrical_part", "stored_energy"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, @@ -376,7 +376,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP753", NameDE: "Thermal Runaway bei Lithium-Batterie", NameEN: "Thermal runaway of lithium battery", RequiredComponentTags: []string{"stored_energy", "high_temperature"}, - RequiredEnergyTags: []string{"electrical", "thermal"}, + RequiredEnergyTags: []string{"electrical_energy", "thermal"}, GeneratedHazardCats: []string{"thermal_hazard", "electrical_hazard"}, SuggestedMeasureIDs: []string{"M005", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, @@ -406,7 +406,7 @@ func GetSpecificMachinePatterns() []HazardPattern { { ID: "HP755", NameDE: "Elektrischer Schlag an Hochvolt-Batteriespeicher", NameEN: "Electric shock from high-voltage battery storage", RequiredComponentTags: []string{"stored_energy", "electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines2.go b/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines2.go index 742fae30..c91ac80b 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines2.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_specific_machines2.go @@ -90,7 +90,7 @@ func GetSpecificMachinePatterns2() []HazardPattern { { ID: "HP761", NameDE: "Ansaugen durch Wassereinlass (Entrapment)", NameEN: "Suction entrapment by pool drain", RequiredComponentTags: []string{"high_pressure"}, - RequiredEnergyTags: []string{"pneumatic"}, + RequiredEnergyTags: []string{"pneumatic_pressure"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E08", "E20"}, @@ -120,7 +120,7 @@ func GetSpecificMachinePatterns2() []HazardPattern { { ID: "HP763", NameDE: "Elektrischer Schlag im Nassbereich", NameEN: "Electric shock in wet area", RequiredComponentTags: []string{"electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E01", "E10", "E20"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_textile_agri.go b/ai-compliance-sdk/internal/iace/hazard_patterns_textile_agri.go index 0ed02203..cd1d0326 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_textile_agri.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_textile_agri.go @@ -128,7 +128,7 @@ func GetTextileAgriPatterns() []HazardPattern { HarmDE: "Amputation, schwere Schnittverletzungen", AffectedDE: "Bediener, Wartungspersonal", ZoneDE: "Schneidwerksbereich", DefaultSeverity: 5, DefaultExposure: 3}, {ID: "HP1568", NameDE: "Hydraulik-Leitungsriss unter Hochdruck", NameEN: "Hydraulic hose burst", - RequiredComponentTags: []string{"hydraulic"}, GeneratedHazardCats: []string{"pneumatic_hydraulic"}, + RequiredComponentTags: []string{"hydraulic_part"}, GeneratedHazardCats: []string{"pneumatic_hydraulic"}, SuggestedMeasureIDs: []string{"M466", "M234"}, SuggestedEvidenceIDs: []string{"E01"}, Priority: 88, MachineTypes: []string{"agricultural", "tractor", "harvester"}, OperationalStates: []string{"automatic_operation"}, @@ -191,7 +191,7 @@ func GetTextileAgriPatterns() []HazardPattern { AffectedDE: "Fahrer", ZoneDE: "Fahrersitz", DefaultSeverity: 3, DefaultExposure: 5}, {ID: "HP1575", NameDE: "Quetschung durch absenkenden Dreipunktanbau", NameEN: "Crushing by lowering three-point hitch", - RequiredComponentTags: []string{"hydraulic"}, GeneratedHazardCats: []string{"mechanical_hazard"}, + RequiredComponentTags: []string{"hydraulic_part"}, GeneratedHazardCats: []string{"mechanical_hazard"}, SuggestedMeasureIDs: []string{"M461", "M474"}, SuggestedEvidenceIDs: []string{"E01", "E08"}, Priority: 90, MachineTypes: []string{"agricultural", "tractor"}, OperationalStates: []string{"manual_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_welding_glass_textile.go b/ai-compliance-sdk/internal/iace/hazard_patterns_welding_glass_textile.go index 651e8bf5..aff6faf2 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_welding_glass_textile.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_welding_glass_textile.go @@ -10,7 +10,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern { { ID: "HP530", NameDE: "Lichtbogen-Verbrennung an Haut/Augen", NameEN: "Arc burn on skin/eyes", RequiredComponentTags: []string{"high_temperature", "electrical_part"}, - RequiredEnergyTags: []string{"thermal", "electrical"}, + RequiredEnergyTags: []string{"thermal", "electrical_energy"}, GeneratedHazardCats: []string{"thermal_hazard", "electrical_hazard"}, SuggestedMeasureIDs: []string{"M005", "M141"}, SuggestedEvidenceIDs: []string{"E10", "E20"}, @@ -25,7 +25,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern { { ID: "HP531", NameDE: "Elektrischer Schlag durch Schweissgeraet", NameEN: "Electric shock from welding equipment", RequiredComponentTags: []string{"electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09", "E20"}, @@ -132,7 +132,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern { { ID: "HP538", NameDE: "Elektrischer Schlag bei Widerstandsschweissen", NameEN: "Electric shock from resistance welding", RequiredComponentTags: []string{"electrical_part"}, - RequiredEnergyTags: []string{"electrical"}, + RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09", "E20"}, diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_workshop.go b/ai-compliance-sdk/internal/iace/hazard_patterns_workshop.go index 49151981..4e8c1fb3 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_workshop.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_workshop.go @@ -118,14 +118,14 @@ func GetWorkshopPatterns() []HazardPattern { DefaultSeverity: 3, DefaultExposure: 4}, // — Elektrische Gefaehrdungen erweitert (HP618-HP622) — {ID: "HP618", NameDE: "Lichtbogenbildung bei Kurzschluss", NameEN: "Arc flash from short circuit", - RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard", "thermal_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09", "E20"}, Priority: 85, ScenarioDE: "Kurzschluss erzeugt Lichtbogen mit Hitze und Druckwelle", TriggerDE: "Beschaedigte Isolation", HarmDE: "Schwere Verbrennungen, Augenschaeden", AffectedDE: "Elektrofachkraefte", ZoneDE: "Schaltschrank", DefaultSeverity: 5, DefaultExposure: 2}, {ID: "HP619", NameDE: "Kriechstrom durch Feuchtigkeit", NameEN: "Leakage current from humidity", - RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 60, ScenarioDE: "Feuchtigkeit erzeugt Kriechstroeme", TriggerDE: "Kondenswasser, fehlender IP-Schutz", @@ -138,14 +138,14 @@ func GetWorkshopPatterns() []HazardPattern { HarmDE: "Funkenbildung, Zuendquelle im Ex-Bereich", AffectedDE: "Bedienpersonal", ZoneDE: "Foerderbaender", DefaultSeverity: 2, DefaultExposure: 4}, {ID: "HP621", NameDE: "Fehlender Potentialausgleich", NameEN: "Missing equipotential bonding", - RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 65, ScenarioDE: "Unterbrochener Schutzleiter am Gehaeuse", TriggerDE: "Korrodierte Erdung, defekter Schutzleiter", HarmDE: "Beruehrungsspannung, Stromschlag", AffectedDE: "Bedienpersonal", ZoneDE: "Maschinengehaeuse", DefaultSeverity: 4, DefaultExposure: 3}, {ID: "HP622", NameDE: "Blitzeinschlag bei Aussenaufstellung", NameEN: "Lightning strike outdoor installation", - RequiredComponentTags: []string{"electrical_part", "structural_part"}, RequiredEnergyTags: []string{"electrical"}, + RequiredComponentTags: []string{"electrical_part", "structural_part"}, RequiredEnergyTags: []string{"electrical_energy"}, GeneratedHazardCats: []string{"electrical_hazard"}, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 55, ScenarioDE: "Maschine im Freien ohne Blitzschutz", TriggerDE: "Fehlender Ueberspannungsableiter", diff --git a/ai-compliance-sdk/internal/iace/keyword_dictionary.go b/ai-compliance-sdk/internal/iace/keyword_dictionary.go index e9aaae1f..28ed79f2 100644 --- a/ai-compliance-sdk/internal/iace/keyword_dictionary.go +++ b/ai-compliance-sdk/internal/iace/keyword_dictionary.go @@ -69,6 +69,13 @@ func GetKeywordDictionary() []KeywordEntry { {Keywords: []string{"maehdrescher", "ballenpresse", "feldhaecksler", "traktor"}, ExtraTags: []string{"dom_agri"}}, {Keywords: []string{"rolltreppe", "fahrtreppe", "fahrsteig"}, ExtraTags: []string{"dom_escalator"}}, {Keywords: []string{"glasschneid", "glasbearbeitung", "flachglas", "glasscheibe", "glaskante", "glasmaschine"}, ExtraTags: []string{"dom_glass"}}, + {Keywords: []string{"im freien", "freigelaende", "ausseneinsatz", "aussenarbeit", "witterung", "forstarbeit", "freiland", "aussengelaende", "winterdienst"}, ExtraTags: []string{"dom_outdoor"}}, + {Keywords: []string{"lueftungsanlage", "lueftungskanal", "klimaanlage", "feuchtraum"}, ExtraTags: []string{"dom_ventilation"}}, + {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{"kraftstation", "fitnessgeraet", "trainingsgeraet", "kraftgeraet", "langhantel"}, ExtraTags: []string{"dom_fitness"}}, // 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 @@ -193,7 +200,11 @@ func GetKeywordDictionary() []KeywordEntry { {Keywords: []string{"greifer", "gripper"}, ComponentIDs: []string{"C002"}, ExtraTags: []string{"clamping_part", "pinch_point"}}, {Keywords: []string{"spindel", "spindle"}, ComponentIDs: []string{"C006"}, EnergyIDs: []string{"EN02"}, ExtraTags: []string{"rotating_part", "high_speed"}}, {Keywords: []string{"saege", "saw"}, ComponentIDs: []string{"C007"}, ExtraTags: []string{"cutting_part"}}, - {Keywords: []string{"walze", "roller"}, ComponentIDs: []string{"C009"}, EnergyIDs: []string{"EN02"}, ExtraTags: []string{"rotating_part", "entanglement_risk"}}, + // "roller" (English) removed: as a bare substring it false-matches + // German compounds like "Bodenroller" (a floor dolly) and "Controller", + // wrongly creating a rotating mill-roller component. "walze" and its + // compounds are the precise German terms for C009. + {Keywords: []string{"walze", "kalanderwalze", "walzwerk"}, ComponentIDs: []string{"C009"}, EnergyIDs: []string{"EN02"}, ExtraTags: []string{"rotating_part", "entanglement_risk"}}, {Keywords: []string{"kette", "chain"}, ComponentIDs: []string{"C010"}, ExtraTags: []string{"entanglement_risk"}}, {Keywords: []string{"bremse", "brake"}, ComponentIDs: []string{"C013"}, ExtraTags: []string{"safety_device"}}, {Keywords: []string{"schweiss", "weld"}, ComponentIDs: []string{"C016"}, EnergyIDs: []string{"EN05"}, ExtraTags: []string{"high_temperature"}}, diff --git a/ai-compliance-sdk/internal/iace/machine_types.go b/ai-compliance-sdk/internal/iace/machine_types.go new file mode 100644 index 00000000..8d9002c6 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/machine_types.go @@ -0,0 +1,126 @@ +package iace + +import ( + "sort" + "strings" +) + +// MachineTypeInfo is one entry of the controlled machine-type vocabulary the +// pattern engine gates on. The project's machine type MUST come from this list +// (a form dropdown), otherwise the engine's machine-type gate either floods +// (empty) or under-covers (a free-text value that matches no pattern). +type MachineTypeInfo struct { + Key string `json:"key"` + LabelDE string `json:"label_de"` + Group string `json:"group"` +} + +// machineTypeLabels gives German labels + UI groups for the machine-type keys +// used in pattern MachineTypes. Keys not listed here still appear (derived from +// the pattern set) with a humanised fallback label, so the vocabulary never +// silently drops a gated machine class. +var machineTypeLabels = map[string]MachineTypeInfo{ + // Heben & Fördern + "lift": {LabelDE: "Hebevorrichtung / Hubgerät", Group: "Heben & Fördern"}, + "elevator": {LabelDE: "Aufzug", Group: "Heben & Fördern"}, + "escalator": {LabelDE: "Fahrtreppe / Rolltreppe", Group: "Heben & Fördern"}, + "crane": {LabelDE: "Kran", Group: "Heben & Fördern"}, + "conveyor": {LabelDE: "Förderanlage / Stetigförderer", Group: "Heben & Fördern"}, + "packaging": {LabelDE: "Verpackungsmaschine", Group: "Heben & Fördern"}, + "bottling": {LabelDE: "Abfüllanlage", Group: "Heben & Fördern"}, + // Pressen & Umformen + "press": {LabelDE: "Presse (allgemein)", Group: "Pressen & Umformen"}, + "hydraulic_press": {LabelDE: "Hydraulikpresse", Group: "Pressen & Umformen"}, + "mechanical_press": {LabelDE: "Mechanische Presse", Group: "Pressen & Umformen"}, + "printing_press": {LabelDE: "Druckmaschine", Group: "Pressen & Umformen"}, + // Zerspanung & Bearbeitung + "cnc": {LabelDE: "CNC-Bearbeitungszentrum", Group: "Zerspanung & Bearbeitung"}, + "lathe": {LabelDE: "Drehmaschine", Group: "Zerspanung & Bearbeitung"}, + "milling": {LabelDE: "Fräsmaschine", Group: "Zerspanung & Bearbeitung"}, + "grinding": {LabelDE: "Schleifmaschine", Group: "Zerspanung & Bearbeitung"}, + "circular_saw": {LabelDE: "Kreissäge", Group: "Zerspanung & Bearbeitung"}, + "metalworking": {LabelDE: "Metallbearbeitung", Group: "Zerspanung & Bearbeitung"}, + "woodworking": {LabelDE: "Holzbearbeitung", Group: "Zerspanung & Bearbeitung"}, + "welding": {LabelDE: "Schweißanlage", Group: "Zerspanung & Bearbeitung"}, + // Roboter & Automation + "robotics_cobot": {LabelDE: "Roboterzelle / Cobot", Group: "Roboter & Automation"}, + "rotary_transfer": {LabelDE: "Rundtaktmaschine", Group: "Roboter & Automation"}, + "autonomous_vehicle": {LabelDE: "Fahrerloses Transportsystem (FTS/AGV)", Group: "Roboter & Automation"}, + // Kunststoff, Glas & Textil + "textile": {LabelDE: "Textilmaschine", Group: "Kunststoff, Glas & Textil"}, + "spinning": {LabelDE: "Spinnmaschine", Group: "Kunststoff, Glas & Textil"}, + "weaving": {LabelDE: "Webmaschine", Group: "Kunststoff, Glas & Textil"}, + "knitting": {LabelDE: "Strickmaschine", Group: "Kunststoff, Glas & Textil"}, + "dyeing": {LabelDE: "Färbemaschine", Group: "Kunststoff, Glas & Textil"}, + "carding": {LabelDE: "Kardiermaschine", Group: "Kunststoff, Glas & Textil"}, + "twisting": {LabelDE: "Zwirnmaschine", Group: "Kunststoff, Glas & Textil"}, + "stenter": {LabelDE: "Spannrahmen", Group: "Kunststoff, Glas & Textil"}, + "finishing": {LabelDE: "Veredelungsmaschine", Group: "Kunststoff, Glas & Textil"}, + // Bau & mobile Maschinen + "construction": {LabelDE: "Baumaschine", Group: "Bau & mobile Maschinen"}, + "excavator": {LabelDE: "Bagger", Group: "Bau & mobile Maschinen"}, + "tractor": {LabelDE: "Traktor", Group: "Bau & mobile Maschinen"}, + "harvester": {LabelDE: "Erntemaschine", Group: "Bau & mobile Maschinen"}, + "combine": {LabelDE: "Mähdrescher", Group: "Bau & mobile Maschinen"}, + "agricultural": {LabelDE: "Landmaschine", Group: "Bau & mobile Maschinen"}, + "forestry": {LabelDE: "Forstmaschine", Group: "Bau & mobile Maschinen"}, + "sprayer": {LabelDE: "Spritzgerät / Sprühmaschine", Group: "Bau & mobile Maschinen"}, + // Medizin & Labor + "medical_device": {LabelDE: "Medizingerät (allgemein)", Group: "Medizin & Labor"}, + "patient_monitor": {LabelDE: "Patientenmonitor", Group: "Medizin & Labor"}, + "infusion_pump": {LabelDE: "Infusionspumpe", Group: "Medizin & Labor"}, + "ventilator": {LabelDE: "Beatmungsgerät", Group: "Medizin & Labor"}, + "laser_device": {LabelDE: "Lasergerät", Group: "Medizin & Labor"}, + "pharmaceutical": {LabelDE: "Pharmamaschine", Group: "Medizin & Labor"}, + // Prozess & Sonstige + "food_processing": {LabelDE: "Lebensmittelmaschine", Group: "Prozess & Sonstige"}, + "pump": {LabelDE: "Pumpe", Group: "Prozess & Sonstige"}, + "compressor": {LabelDE: "Kompressor", Group: "Prozess & Sonstige"}, + "surface_treatment": {LabelDE: "Oberflächenbehandlung", Group: "Prozess & Sonstige"}, + "spray_booth": {LabelDE: "Lackierkabine", Group: "Prozess & Sonstige"}, + "electroplating": {LabelDE: "Galvanik", Group: "Prozess & Sonstige"}, + "grain_handling": {LabelDE: "Getreide- / Schüttgutanlage", Group: "Prozess & Sonstige"}, + "glass_washing": {LabelDE: "Glaswaschanlage", Group: "Prozess & Sonstige"}, + "laundry": {LabelDE: "Wäschereimaschine", Group: "Prozess & Sonstige"}, + "playground": {LabelDE: "Spielplatzgerät", Group: "Prozess & Sonstige"}, + "wind_turbine": {LabelDE: "Windenergieanlage", Group: "Prozess & Sonstige"}, + "general_industry": {LabelDE: "Allgemeine Industriemaschine", Group: "Prozess & Sonstige"}, +} + +// MachineTypeVocabulary returns the controlled machine-type vocabulary derived +// from the live pattern set (so it never drifts from what the engine gates on), +// each with a German label + UI group. Sorted by group, then label. +func MachineTypeVocabulary() []MachineTypeInfo { + seen := make(map[string]bool) + for _, p := range AllPatterns() { + for _, mt := range p.MachineTypes { + seen[mt] = true + } + } + out := make([]MachineTypeInfo, 0, len(seen)) + for key := range seen { + info, ok := machineTypeLabels[key] + if !ok { + info = MachineTypeInfo{LabelDE: humanizeMachineType(key), Group: "Sonstige"} + } + info.Key = key + out = append(out, info) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Group != out[j].Group { + return out[i].Group < out[j].Group + } + return out[i].LabelDE < out[j].LabelDE + }) + return out +} + +func humanizeMachineType(key string) string { + parts := strings.Split(key, "_") + for i, p := range parts { + if p != "" { + parts[i] = strings.ToUpper(p[:1]) + p[1:] + } + } + return strings.Join(parts, " ") +} diff --git a/ai-compliance-sdk/internal/iace/models_api.go b/ai-compliance-sdk/internal/iace/models_api.go index 04808795..caff540c 100644 --- a/ai-compliance-sdk/internal/iace/models_api.go +++ b/ai-compliance-sdk/internal/iace/models_api.go @@ -45,6 +45,9 @@ type CreateComponentRequest struct { Description string `json:"description,omitempty"` IsSafetyRelevant bool `json:"is_safety_relevant"` IsNetworked bool `json:"is_networked"` + CEMarked bool `json:"ce_marked"` + // PresenceStatus defaults to "vorhanden" when empty (see Store.CreateComponent). + PresenceStatus string `json:"presence_status,omitempty"` } // CreateHazardRequest is the API request for creating a new hazard diff --git a/ai-compliance-sdk/internal/iace/models_entities.go b/ai-compliance-sdk/internal/iace/models_entities.go index bc1cdc96..80a0acb0 100644 --- a/ai-compliance-sdk/internal/iace/models_entities.go +++ b/ai-compliance-sdk/internal/iace/models_entities.go @@ -37,6 +37,13 @@ type Project struct { ArchivedAt *time.Time `json:"archived_at,omitempty"` } +// Component presence states for expert review of auto-detected components. +const ( + PresencePresent = "vorhanden" + PresenceAbsent = "nicht_vorhanden" + PresenceDeleted = "geloescht" +) + // Component represents a system component within a project type Component struct { ID uuid.UUID `json:"id"` @@ -48,6 +55,17 @@ type Component struct { Description string `json:"description,omitempty"` IsSafetyRelevant bool `json:"is_safety_relevant"` IsNetworked bool `json:"is_networked"` + // CEMarked: bought component that carries its own CE / Declaration of + // Conformity (finished robot, actuator, drive, safety PLC). SAFE semantics: + // does NOT suppress hazards — only drives provenance/evidence hints and the + // "validate the integrated safety function (PL/SIL)" obligation when also + // safety-relevant. + CEMarked bool `json:"ce_marked"` + // PresenceStatus: vorhanden | nicht_vorhanden | geloescht. Only `vorhanden` + // components feed pattern matching. `nicht_vorhanden` = engine's best-effort + // negation verdict awaiting expert review; `geloescht` = expert removed it + // (kept as a soft-deleted audit row). + PresenceStatus string `json:"presence_status"` Metadata json.RawMessage `json:"metadata,omitempty"` SortOrder int `json:"sort_order"` CreatedAt time.Time `json:"created_at"` diff --git a/ai-compliance-sdk/internal/iace/narrative_negation.go b/ai-compliance-sdk/internal/iace/narrative_negation.go new file mode 100644 index 00000000..c9547116 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/narrative_negation.go @@ -0,0 +1,78 @@ +package iace + +import "strings" + +// Negation-aware keyword matching for the narrative parser. +// +// The limits form often states what a machine does NOT have, e.g. +// "Keine pneumatischen oder hydraulischen Schnittstellen. Hubantrieb ueber +// Kette." Naive substring matching wrongly extracted hydraulic/pneumatic +// components (and their energy sources), which then generated phantom hazards +// the assessor never raised. We only suppress DETERMINER negations +// ("keine/kein/ohne/weder ") — these directly negate the following noun +// phrase and are safe. Plain "nicht" is intentionally excluded: it modifies +// verbs/adjectives and over-negates ("Schutz nicht erforderlich, Zylinder +// vorhanden"). + +var negationDeterminers = map[string]bool{ + "keine": true, "kein": true, "keinen": true, "keiner": true, + "keinem": true, "keines": true, "keinerlei": true, "weder": true, + "ohne": true, "no": true, "without": true, +} + +// Tokens that end a negation's scope: a contrast ("aber") or a positive-presence +// cue ("mit ... vorhanden"). After one of these, a following keyword is positive. +var negationScopeEnders = map[string]bool{ + "aber": true, "jedoch": true, "sondern": true, "doch": true, + "mit": true, "vorhanden": true, "verbaut": true, "vorgesehen": true, + "installiert": true, "ausgestattet": true, "but": true, "with": true, +} + +// A negation determiner only reaches a keyword a few tokens away (a short list +// like "keine A, B oder C"). Beyond this span we assume the keyword is unrelated. +const negationMaxTokenSpan = 8 + +// keywordIsNegated reports whether the keyword occurrence starting at byte index +// idx in the (already normalised) text sits inside the scope of a determiner +// negation. It walks back to the start of the current sentence, then scans the +// preceding tokens right-to-left for a determiner negation, stopping at any +// scope-ender or after negationMaxTokenSpan tokens. +func keywordIsNegated(text string, idx int) bool { + start := 0 + for i := idx - 1; i >= 0; i-- { + c := text[i] + if c == '.' || c == '\n' || c == ';' || c == '!' || c == '?' || c == ':' { + start = i + 1 + break + } + } + tokens := strings.Fields(text[start:idx]) + for d := 0; d < len(tokens) && d < negationMaxTokenSpan; d++ { + w := strings.Trim(tokens[len(tokens)-1-d], ",.;:()-") + if negationScopeEnders[w] { + return false + } + if negationDeterminers[w] { + return true + } + } + return false +} + +// hasUnnegatedOccurrence reports whether kw appears in text at least once outside +// a negation scope. A term that is ONLY ever negated must not create components, +// energy sources or tags. +func hasUnnegatedOccurrence(text, kw string) bool { + from := 0 + for { + rel := strings.Index(text[from:], kw) + if rel < 0 { + return false + } + abs := from + rel + if !keywordIsNegated(text, abs) { + return true + } + from = abs + len(kw) + } +} diff --git a/ai-compliance-sdk/internal/iace/narrative_negation_test.go b/ai-compliance-sdk/internal/iace/narrative_negation_test.go new file mode 100644 index 00000000..d3cb35ec --- /dev/null +++ b/ai-compliance-sdk/internal/iace/narrative_negation_test.go @@ -0,0 +1,61 @@ +package iace + +import "testing" + +// Helper-level: operates on already-normalised (lowercase, umlaut-folded) text. +func TestHasUnnegatedOccurrence(t *testing.T) { + neg := "keine pneumatischen oder hydraulischen schnittstellen. hubantrieb ueber kette (kettenspannung zyklisch zu pruefen)." + tests := []struct { + name string + text string + kw string + want bool + }{ + {"negated hydraulik", neg, "hydraulisch", false}, + {"negated pneumatik", neg, "pneumatisch", false}, + {"positive kette after period", neg, "kette", true}, + {"ohne negates", "ohne hydraulik vorhanden", "hydraulik", false}, + {"mit ends scope", "ohne hydraulik, mit pneumatikzylinder", "pneumatik", true}, + {"plain positive", "die maschine hat eine hydraulikpumpe", "hydraulik", true}, + {"weder/contrast", "weder pneumatik noch hydraulik verbaut", "pneumatik", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasUnnegatedOccurrence(tt.text, tt.kw); got != tt.want { + t.Errorf("hasUnnegatedOccurrence(%q,%q)=%v, want %v", tt.text, tt.kw, got, tt.want) + } + }) + } +} + +// Integration: the Kistenhub interface sentence SURFACES the hydraulic (C041) +// and pneumatic (C051) components flagged as negated (for expert review), keeps +// the chain drive (C010) as present, and crucially leaks NO pneumatic/hydraulic +// tags into the matching set (so no phantom hazards). +func TestParseNarrative_NegatedInterfacesFlagged(t *testing.T) { + text := "Keine pneumatischen oder hydraulischen Schnittstellen. " + + "Hubantrieb über Kette (Kettenspannung zyklisch zu prüfen)." + res := ParseNarrative(text) + status := func(id string) (found, negated bool) { + for _, c := range res.Components { + if c.LibraryID == id { + return true, c.Negated + } + } + return false, false + } + if f, n := status("C041"); !f || !n { + t.Errorf("C041 (Hydraulikpumpe) should be surfaced & negated; found=%v negated=%v", f, n) + } + if f, n := status("C051"); !f || !n { + t.Errorf("C051 (Pneumatikzylinder) should be surfaced & negated; found=%v negated=%v", f, n) + } + if f, n := status("C010"); !f || n { + t.Errorf("C010 (Kettenantrieb) should be present & not negated; found=%v negated=%v", f, n) + } + for _, tag := range res.CustomTags { + if tag == "pneumatic_part" || tag == "hydraulic_part" { + t.Errorf("negated component leaked tag %q into the matching set", tag) + } + } +} diff --git a/ai-compliance-sdk/internal/iace/narrative_parser.go b/ai-compliance-sdk/internal/iace/narrative_parser.go index 73a3a071..bd3751bf 100644 --- a/ai-compliance-sdk/internal/iace/narrative_parser.go +++ b/ai-compliance-sdk/internal/iace/narrative_parser.go @@ -13,6 +13,10 @@ type ComponentMatch struct { MatchedOn string `json:"matched_on"` // The keyword that triggered the match Tags []string `json:"tags"` Confidence float64 `json:"confidence"` + // Negated is the best-effort verdict that the keyword appeared only in a + // negated clause ("keine Pneumatik"). Negated components are surfaced for + // expert review but do NOT contribute tags/energy to pattern matching. + Negated bool `json:"negated"` } // EnergyMatch represents an energy source detected from narrative text. @@ -189,7 +193,14 @@ func ParseNarrative(text string, machineType ...string) ParseResult { kwNorm = strings.ReplaceAll(kwNorm, "ß", "ss") if strings.Contains(lower, kwNorm) { - // Add components + // Best-effort negation verdict: the keyword is present, but if + // every occurrence sits in a negated clause ("keine Pneumatik") + // the component is surfaced as negated and contributes NO tags / + // energy to matching (so it generates no phantom hazards). The + // expert can flip the verdict in the Components view. + negated := !hasUnnegatedOccurrence(lower, kwNorm) + + // Add components (negated ones carry the flag, no tags) for _, cid := range entry.ComponentIDs { if !seenComponents[cid] { seenComponents[cid] = true @@ -200,27 +211,31 @@ func ParseNarrative(text string, machineType ...string) ParseResult { MatchedOn: kw, Tags: comp.Tags, Confidence: 0.8, + Negated: negated, }) - // Add component tags - for _, t := range comp.Tags { - tagSet[t] = true + if !negated { + for _, t := range comp.Tags { + tagSet[t] = true + } } } } - // Add energy sources - for _, eid := range entry.EnergyIDs { - if !seenEnergy[eid] { - seenEnergy[eid] = true - result.EnergySources = append(result.EnergySources, EnergyMatch{ - SourceID: eid, - NameDE: eid, // Will be enriched by caller - MatchedOn: kw, - }) + if !negated { + // Add energy sources + for _, eid := range entry.EnergyIDs { + if !seenEnergy[eid] { + seenEnergy[eid] = true + result.EnergySources = append(result.EnergySources, EnergyMatch{ + SourceID: eid, + NameDE: eid, // Will be enriched by caller + MatchedOn: kw, + }) + } + } + // Add extra tags + for _, t := range entry.ExtraTags { + tagSet[t] = true } - } - // Add extra tags - for _, t := range entry.ExtraTags { - tagSet[t] = true } break // First keyword match is enough per entry } diff --git a/ai-compliance-sdk/internal/iace/narrative_parser_roller_test.go b/ai-compliance-sdk/internal/iace/narrative_parser_roller_test.go new file mode 100644 index 00000000..2a60a40d --- /dev/null +++ b/ai-compliance-sdk/internal/iace/narrative_parser_roller_test.go @@ -0,0 +1,37 @@ +package iace + +import "testing" + +// Regression: "Bodenroller" (a floor dolly the load sits on) and "Controller" +// must NOT be detected as a rotating mill roller (C009 "Walze"). Before the fix, +// the bare English keyword "roller" matched as a substring inside these German +// compounds and created a bogus Walze component for a crate-lift. +func TestParseNarrative_BodenrollerControllerNotWalze(t *testing.T) { + cases := []string{ + "Das Kistenhubgeraet hebt Behaelter auf Bodenrollern und Transportwagen.", + "Beladung wahlweise von Bodenroller 400x600 mm.", + "Der SPS-Controller steuert den Hubantrieb.", + } + for _, text := range cases { + res := ParseNarrative(text) + for _, c := range res.Components { + if c.LibraryID == "C009" { + t.Errorf("text %q wrongly mapped to C009 (Walze) via %q", text, c.MatchedOn) + } + } + } +} + +// The precise German term must still create the roller component. +func TestParseNarrative_WalzeDetectsC009(t *testing.T) { + res := ParseNarrative("Die Maschine besitzt eine angetriebene Walze zum Kalandrieren.") + found := false + for _, c := range res.Components { + if c.LibraryID == "C009" { + found = true + } + } + if !found { + t.Fatal("expected 'Walze' to detect component C009") + } +} diff --git a/ai-compliance-sdk/internal/iace/pattern_domain_gates.go b/ai-compliance-sdk/internal/iace/pattern_domain_gates.go index fca4a123..0af0e0f7 100644 --- a/ai-compliance-sdk/internal/iace/pattern_domain_gates.go +++ b/ai-compliance-sdk/internal/iace/pattern_domain_gates.go @@ -57,6 +57,28 @@ var domainGateTerms = map[string]string{ "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 diff --git a/ai-compliance-sdk/internal/iace/pattern_precision_test.go b/ai-compliance-sdk/internal/iace/pattern_precision_test.go new file mode 100644 index 00000000..322d59ec --- /dev/null +++ b/ai-compliance-sdk/internal/iace/pattern_precision_test.go @@ -0,0 +1,68 @@ +package iace + +import ( + "sort" + "strings" + "testing" +) + +// The cross-domain term list lives in gt_benchmark_harness_test.go +// (foreignDomainTerms, term → home domain). This precision guard reuses it and, +// unlike the diagnostic TestGT_DomainLeakage, runs the FULL production gating +// path including the relevance backstop, then ASSERTS zero leaks. It catches +// machine-type wiring regressions and weak-tag (structural_part) leaks in CI. + +// firedHazardsForCase runs the exact production gating path (parse → engine match +// → relevance backstop) for one GT case and returns the surviving patterns. +func firedHazardsForCase(c gtCase) []PatternMatch { + narrative := c.narrativeOverride + pr := ParseNarrative(narrative, c.machineType) + input := parseResultToMatchInput(pr, c.machineType) + + compNames := make([]string, 0, len(pr.Components)) + for _, comp := range pr.Components { + if comp.Negated { + continue + } + compNames = append(compNames, NormalizeDEPublic(comp.NameDE)) + } + + out := NewPatternEngine().Match(input) + fired := make([]PatternMatch, 0, len(out.MatchedPatterns)) + for _, mp := range out.MatchedPatterns { + if IsPatternRelevant(mp, narrative, compNames) { + fired = append(fired, mp) + } + } + return fired +} + +// TestCrossDomainPrecision asserts that no fired pattern is foreign to the GT +// machine — neither machine-type-incompatible nor matching a foreign-domain term. +func TestCrossDomainPrecision(t *testing.T) { + for _, c := range gtBenchmarkCases { + c := c + t.Run(c.name, func(t *testing.T) { + fired := firedHazardsForCase(c) + t.Logf("%s (%s): %d patterns fired", c.name, c.machineType, len(fired)) + + var domainLeaks []string + for _, mp := range fired { + text := normalizeDE(mp.PatternName + " " + mp.ZoneDE + " " + mp.ScenarioDE) + for term, domain := range foreignDomainTerms { + if strings.Contains(text, term) { + domainLeaks = append(domainLeaks, domain+"/"+term+" → "+mp.PatternName) + break + } + } + } + sort.Strings(domainLeaks) + for _, l := range domainLeaks { + t.Logf(" FOREIGN-DOMAIN: %s", l) + } + if len(domainLeaks) > 0 { + t.Errorf("%s: %d cross-domain leak(s) — patterns from foreign machine classes fired", c.name, len(domainLeaks)) + } + }) + } +} diff --git a/ai-compliance-sdk/internal/iace/pattern_reachability_test.go b/ai-compliance-sdk/internal/iace/pattern_reachability_test.go new file mode 100644 index 00000000..4c66f52e --- /dev/null +++ b/ai-compliance-sdk/internal/iace/pattern_reachability_test.go @@ -0,0 +1,103 @@ +package iace + +import ( + "sort" + "strings" + "testing" +) + +// producibleTagUniverse returns every component/energy tag that some machine +// could ever carry: tags from the component library, the energy library, the +// keyword dictionary's ExtraTags, and the domain-gate tags. A pattern whose +// RequiredComponentTags/RequiredEnergyTags include a tag outside this set can +// never match — no machine can produce that tag. +func producibleTagUniverse() map[string]bool { + u := make(map[string]bool) + for _, c := range GetComponentLibrary() { + for _, t := range c.Tags { + u[t] = true + } + } + for _, e := range GetEnergySources() { + for _, t := range e.Tags { + u[t] = true + } + } + for _, k := range GetKeywordDictionary() { + for _, t := range k.ExtraTags { + u[t] = true + } + } + // Domain-gate tags are produced from narrative domain terms. + for _, t := range []string{ + "dom_agri", "dom_cnc", "dom_escalator", "dom_glass", "dom_grinding", + "dom_plastics", "dom_press", "dom_rolling", "dom_solar", "dom_textile", + "dom_welding", "dom_wind", + } { + u[t] = true + } + return u +} + +// TestPatternReachability reports patterns that can never fire because they +// require a component/energy tag that nothing in the libraries produces. Every +// pattern should be usable in SOME CE risk assessment. Currently informational +// (t.Log) so we can review the list before deciding to prune. +func TestPatternReachability(t *testing.T) { + universe := producibleTagUniverse() + patterns := AllPatterns() + + type dead struct { + id, name string + missing []string + } + var deads []dead + missingTagCount := make(map[string]int) + + for _, p := range patterns { + var missing []string + for _, tag := range append(append([]string{}, p.RequiredComponentTags...), p.RequiredEnergyTags...) { + if !universe[tag] { + missing = append(missing, tag) + missingTagCount[tag]++ + } + } + if len(missing) > 0 { + deads = append(deads, dead{p.ID, p.NameDE, missing}) + } + } + + t.Logf("Patterns gesamt: %d", len(patterns)) + t.Logf("Unerreichbare (tote) Patterns: %d", len(deads)) + + // Most common unsatisfiable tags first — these point at the systemic gaps. + type kv struct { + tag string + n int + } + var ranked []kv + for tag, n := range missingTagCount { + ranked = append(ranked, kv{tag, n}) + } + sort.Slice(ranked, func(i, j int) bool { return ranked[i].n > ranked[j].n }) + t.Log("--- Unerfuellbare Required-Tags (Haeufigkeit) ---") + for _, r := range ranked { + t.Logf(" %3d %s", r.n, r.tag) + } + + t.Log("--- Tote Patterns (erste 60) ---") + for i, d := range deads { + if i >= 60 { + break + } + t.Logf(" %s %q fehlend: %s", d.id, d.name, strings.Join(d.missing, ",")) + } + + // Guard: every pattern must be reachable by some CE risk assessment. A + // pattern requiring a tag no component/energy/keyword can ever produce is + // dead weight (and often a tag-naming typo). Keep this at zero. + if len(deads) > 0 { + t.Errorf("%d unreachable pattern(s) — required tags that nothing produces: %v", + len(deads), missingTagCount) + } +} diff --git a/ai-compliance-sdk/internal/iace/pattern_relevance.go b/ai-compliance-sdk/internal/iace/pattern_relevance.go new file mode 100644 index 00000000..4518cdec --- /dev/null +++ b/ai-compliance-sdk/internal/iace/pattern_relevance.go @@ -0,0 +1,181 @@ +package iace + +import "strings" + +// Pattern relevance gating (engine-side backstop). +// +// The pattern engine fires any pattern whose required tags are present. Some +// patterns are gated only on near-universal tags (e.g. "structural_part" — every +// machine has a frame) or on strong-but-broad tags a machine genuinely has +// (gravity_risk, moving_part, high_force). That lets context-specific patterns +// from other environments leak in (tick bites, confined-space oxygen, palletizer +// reach-in). IsPatternRelevant is the text backstop: a pattern made only of +// GENERIC hazard vocabulary (quetschen, stromschlag, absturz, person ...) is a +// universal machine hazard and stays; a pattern carrying a machine-, environment- +// or organism-specific word (palettierer, klettergeraet, zeckenbiss) only applies +// if that word actually appears in this machine's limits. +// +// This is a BACKSTOP, not the primary gate — the authoritative gate is the +// engine's machine-type + required-tag matching (see patternMatches). Keeping the +// relevance logic in this package lets the precision test exercise the exact +// production path. + +// genericSafetyTerms are exact words that appear in almost all risk assessments +// and must NOT be treated as machine-specific. +var genericSafetyTerms = map[string]bool{ + "maschine": true, "anlage": true, "bereich": true, "gesamte": true, + "arbeitsplatz": true, "gefahrbereich": true, "gefahrstelle": true, + "gefahrenstelle": true, "person": true, "werker": true, "bediener": true, + "steuerung": true, "schutzeinrichtung": true, "sicherheit": true, + "betrieb": true, "wartung": true, "instandhaltung": true, "reinigung": true, + "bewegung": true, "beweglich": true, "feststehend": true, "teil": true, + "teile": true, "oeffnung": true, "zugang": true, "gefahr": true, + "verletzung": true, "quetsch": true, "scher": true, "schneid": true, + "stoss": true, "schlag": true, "einzug": true, "brand": true, + "motor": true, "antrieb": true, "achse": true, "achsen": true, + "kabel": true, "leitung": true, "schaltschrank": true, "spannung": true, + "schutz": true, "gehaeuse": true, "oberflaeche": true, "boden": true, + "leitfaehig": true, "elektrisch": true, "mechanisch": true, + "bedienfeld": true, "display": true, "anzeige": true, + "energie": true, "druck": true, "temperatur": true, + // Abbreviations and synonyms that should not trigger relevance filter + "kss": true, "emv": true, "esd": true, "dcs": true, "plr": true, "sil": true, + "hmi": true, "sps": true, "rcd": true, "loto": true, "psa": true, + // Common action words + "bersten": true, "platzen": true, "abspringen": true, "spritzen": true, + "einatmen": true, "ausrutschen": true, "herabfallen": true, + "durchschlaegen": true, "wegschleudern": true, + // Common structural terms that don't indicate a specific machine + "gesamter": true, "gesamtes": true, "bereichs": true, "stelle": true, + "innen": true, "aussen": true, "transport": true, "seite": true, + "front": true, "rueck": true, "ober": true, "unter": true, + "fuehrung": true, "lager": true, "verschleiss": true, "welle": true, + "getriebe": true, "kette": true, "riemen": true, "feder": true, + "spindel": true, "werkzeug": true, "werkstueck": true, "flucht": true, + // Generic anatomy (too short for safe prefix stems) + "arm": true, "arme": true, "bein": true, "beine": true, "fuss": true, + "fuesse": true, "kopf": true, "koepfe": true, "hand": true, "haende": true, + // Common German function words (prepositions/conjunctions/determiners) that + // are not machine-specific but survive the >=5-char specific-word cut. + "zwischen": true, "durch": true, "gegen": true, "neben": true, + "hinter": true, "waehrend": true, "sowie": true, "dabei": true, + "dadurch": true, "wodurch": true, "beim": true, "etwa": true, + "jeder": true, "jede": true, "jedes": true, "dieser": true, "diese": true, + "dieses": true, "welche": true, "welcher": true, "deren": true, + "dessen": true, "sodass": true, "damit": true, + // Location prepositions — never machine-distinctive. + "ueber": true, "oberhalb": true, "unterhalb": true, "innerhalb": true, + "ausserhalb": true, "entlang": true, "angrenzend": true, "darunter": true, + "umliegend": true, "benachbart": true, +} + +// genericStems cover inflected generic words by prefix (German adds suffixes: +// person→personen/personal, arbeit→arbeiten/arbeitsraum, quetsch→quetschen). +// Only stems long/distinct enough that a prefix match cannot catch an unrelated +// specific compound are listed. This is the lemma half of the filter. +var genericStems = []string{ + // actors / organisation + "person", "arbeit", "taetig", "mitarbeit", "bedien", "werker", "nutzer", + "betrieb", "wartung", "instandhalt", "reinig", "einricht", "transport", + "qualifik", "unterweis", "schulung", + // hazard phenomena / kinematics + "quetsch", "scher", "schneid", "schnitt", "stich", "stoss", "schlag", + "einzug", "einzieh", "erfass", "wickel", "absturz", "abstuerz", "sturz", + "stuerz", "kollision", "anprall", "anstoss", "verbrenn", "verbrueh", + "verletz", "gefaehrd", "klemm", + // energy / electrical / thermal descriptors + "stromschl", "spannung", "elektr", "thermi", "energie", "leitung", + // anatomy (long enough for prefix) + "finger", "koerper", "gliedmass", "extremit", + // structure / location / generic qualifiers + "bereich", "struktur", "gehaeuse", "oberflaech", "beweg", "feststehend", + "schutz", "sicher", "maschine", "anlage", "betriebs", + // common generic verbs / adjectives (contact, motion, causation, state) + "beruehr", "greif", "treff", "fall", "faell", "loes", "oeffn", "schliess", + "gelang", "erreich", "direkt", "schwer", "offen", "scharf", "teil", + "moeglich", "fehlend", "unerwart", "ploetzl", "unkontroll", "versehentl", +} + +func isGenericTerm(w string) bool { + if genericSafetyTerms[w] { + return true + } + for _, s := range genericStems { + if strings.HasPrefix(w, s) { + return true + } + } + return false +} + +// narrativeTokenSet builds the set of words the machine actually describes +// (limits text + component names), normalised and de-duplicated. +func narrativeTokenSet(narrative string, compNames []string) map[string]bool { + set := make(map[string]bool) + add := func(text string) { + for _, t := range strings.Fields(NormalizeDEPublic(text)) { + t = strings.Trim(t, ".,;:!?()/-\"") + if len(t) >= 4 { + set[t] = true + } + } + } + add(narrative) + for _, cn := range compNames { + add(cn) + } + return set +} + +// specificWordInNarrative reports whether a machine-specific pattern word is +// present in the machine's vocabulary. Matches on token boundaries (full token, +// or either word a prefix of the other for ≥5 chars) so German inflection is +// tolerated ("behaelter" ~ "behaeltern") without substring false positives +// ("arbeiten" inside "bearbeiten"). +func specificWordInNarrative(sw string, tokens map[string]bool) bool { + if tokens[sw] { + return true + } + if len(sw) < 5 { + return false + } + for t := range tokens { + // Only the inflection direction: a narrative token is the specific word + // plus a German suffix ("behaelter" → "behaeltern"). The REVERSE + // direction is dropped — it let a long pattern word anchor on a short + // common narrative token (pattern "uebertragen" matching "ueber", + // "zugangsbereich" matching "zugang"), the dominant false-positive class. + if len(t) >= 5 && strings.HasPrefix(t, sw) { + return true + } + } + return false +} + +// IsPatternRelevant checks whether a pattern applies to the machine in the +// narrative. A pattern with no machine-specific word is generic → relevant. A +// pattern with specific words is relevant only if at least one appears in the +// machine's own vocabulary. +func IsPatternRelevant(mp PatternMatch, narrative string, compNames []string) bool { + patternText := NormalizeDEPublic(mp.ZoneDE + " " + mp.ScenarioDE + " " + mp.PatternName) + + var specificWords []string + for _, w := range strings.Fields(patternText) { + w = strings.Trim(w, ".,;:!?()/-\"") + if len(w) < 5 || isGenericTerm(w) { + continue + } + specificWords = append(specificWords, w) + } + if len(specificWords) == 0 { + return true + } + + tokens := narrativeTokenSet(narrative, compNames) + for _, sw := range specificWords { + if specificWordInNarrative(sw, tokens) { + return true + } + } + return false +} diff --git a/ai-compliance-sdk/internal/iace/pattern_relevance_test.go b/ai-compliance-sdk/internal/iace/pattern_relevance_test.go new file mode 100644 index 00000000..0ed1fc7b --- /dev/null +++ b/ai-compliance-sdk/internal/iace/pattern_relevance_test.go @@ -0,0 +1,45 @@ +package iace + +import "testing" + +func TestIsPatternRelevant(t *testing.T) { + // A chain-driven crate lift: no press, no outdoor work, no palletizer. + narrative := "Kistenhubgeraet hebt Behaelter ueber Kette. Elektromotor und SPS-Steuerung. " + + "Hubwerk mit Plattform und Not-Halt-Taster." + comps := []string{"Hubwerk", "Kettenantrieb", "Elektromotor (Drehstrom)", "SPS", "Plattform/Buehne"} + + tests := []struct { + name string + mp PatternMatch + want bool + }{ + {"foreign: tick bite", PatternMatch{ + PatternName: "Zeckenbiss bei Ausseneinsatz", ScenarioDE: "Zeckenstich bei Arbeiten im Gruenen", + ZoneDE: "Freigelände, Wald, Wiese"}, false}, + {"foreign: climbing structure", PatternMatch{ + PatternName: "Absturz von Klettergeraet", ScenarioDE: "Kind stuerzt von Klettergeraet auf den Boden", + ZoneDE: "Klettergeraet, Fallzone darunter"}, false}, + {"foreign: palletizer", PatternMatch{ + PatternName: "Palettierer — mechanisch", ScenarioDE: "Palettierer bewegt schwere Gebinde und quetscht Personen", + ZoneDE: "Palettierer-Arbeitsraum, Palettenwechselzone"}, false}, + {"foreign: ventilation mold", PatternMatch{ + PatternName: "Schimmelpilz in Lueftungsanlage", ScenarioDE: "Staub mit Nagerkot in selten gereinigten Raeumen", + ZoneDE: "Lueftungskanal, Filterbereich"}, false}, + {"generic: crush by moving parts", PatternMatch{ + PatternName: "Quetschgefahr durch bewegte Teile", ScenarioDE: "Quetschen zwischen beweglichen und feststehenden Teilen", + ZoneDE: "Bewegungsbereich"}, true}, + {"generic: electric shock", PatternMatch{ + PatternName: "Direktes Beruehren", ScenarioDE: "Stromschlag bei Beruehrung spannungsfuehrender Teile", + ZoneDE: "Schaltschrank"}, true}, + {"machine-word match: chain drive", PatternMatch{ + PatternName: "Erfassen durch Kettenantrieb", ScenarioDE: "Einzug an offener Kette", + ZoneDE: "Kettenbereich"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsPatternRelevant(tt.mp, narrative, comps); got != tt.want { + t.Errorf("IsPatternRelevant(%q)=%v, want %v", tt.mp.PatternName, got, tt.want) + } + }) + } +} diff --git a/ai-compliance-sdk/internal/iace/store_components.go b/ai-compliance-sdk/internal/iace/store_components.go index afcfcb98..6a879b30 100644 --- a/ai-compliance-sdk/internal/iace/store_components.go +++ b/ai-compliance-sdk/internal/iace/store_components.go @@ -17,6 +17,10 @@ import ( // CreateComponent creates a new component within a project func (s *Store) CreateComponent(ctx context.Context, req CreateComponentRequest) (*Component, error) { + status := req.PresenceStatus + if status == "" { + status = PresencePresent + } comp := &Component{ ID: uuid.New(), ProjectID: req.ProjectID, @@ -27,6 +31,8 @@ func (s *Store) CreateComponent(ctx context.Context, req CreateComponentRequest) Description: req.Description, IsSafetyRelevant: req.IsSafetyRelevant, IsNetworked: req.IsNetworked, + CEMarked: req.CEMarked, + PresenceStatus: status, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } @@ -35,16 +41,16 @@ func (s *Store) CreateComponent(ctx context.Context, req CreateComponentRequest) INSERT INTO iace_components ( id, project_id, parent_id, name, component_type, version, description, is_safety_relevant, is_networked, - metadata, sort_order, created_at, updated_at + ce_marked, presence_status, metadata, sort_order, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, - $10, $11, $12, $13 + $10, $11, $12, $13, $14, $15 ) `, comp.ID, comp.ProjectID, comp.ParentID, comp.Name, string(comp.ComponentType), comp.Version, comp.Description, comp.IsSafetyRelevant, comp.IsNetworked, - comp.Metadata, comp.SortOrder, comp.CreatedAt, comp.UpdatedAt, + comp.CEMarked, comp.PresenceStatus, comp.Metadata, comp.SortOrder, comp.CreatedAt, comp.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create component: %w", err) @@ -63,12 +69,12 @@ func (s *Store) GetComponent(ctx context.Context, id uuid.UUID) (*Component, err SELECT id, project_id, parent_id, name, component_type, version, description, is_safety_relevant, is_networked, - metadata, sort_order, created_at, updated_at + ce_marked, presence_status, metadata, sort_order, created_at, updated_at FROM iace_components WHERE id = $1 `, id).Scan( &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType, &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked, - &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt, + &c.CEMarked, &c.PresenceStatus, &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil @@ -89,7 +95,7 @@ func (s *Store) ListComponents(ctx context.Context, projectID uuid.UUID) ([]Comp SELECT id, project_id, parent_id, name, component_type, version, description, is_safety_relevant, is_networked, - metadata, sort_order, created_at, updated_at + ce_marked, presence_status, metadata, sort_order, created_at, updated_at FROM iace_components WHERE project_id = $1 ORDER BY sort_order ASC, created_at ASC `, projectID) @@ -107,7 +113,7 @@ func (s *Store) ListComponents(ctx context.Context, projectID uuid.UUID) ([]Comp err := rows.Scan( &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType, &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked, - &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt, + &c.CEMarked, &c.PresenceStatus, &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list components scan: %w", err) @@ -150,6 +156,14 @@ func (s *Store) UpdateComponent(ctx context.Context, id uuid.UUID, updates map[s query += fmt.Sprintf(", is_networked = $%d", argIdx) args = append(args, val) argIdx++ + case "presence_status": + query += fmt.Sprintf(", presence_status = $%d", argIdx) + args = append(args, val) + argIdx++ + case "ce_marked": + query += fmt.Sprintf(", ce_marked = $%d", argIdx) + args = append(args, val) + argIdx++ case "sort_order": query += fmt.Sprintf(", sort_order = $%d", argIdx) args = append(args, val) diff --git a/ai-compliance-sdk/migrations/152_iace_projects_reconcile.sql b/ai-compliance-sdk/migrations/152_iace_projects_reconcile.sql new file mode 100644 index 00000000..eaaf914f --- /dev/null +++ b/ai-compliance-sdk/migrations/152_iace_projects_reconcile.sql @@ -0,0 +1,47 @@ +-- Migration 152: reconcile iace_projects with the current Go schema. +-- ========================================================================== +-- The iace tables were created ad-hoc (no CREATE migration) and drifted across +-- environments. The consolidation copied an OLDER iace_projects into the local +-- `compliance` schema: it carried legacy columns (intended_use, +-- limits_description, reasonably_foreseeable_misuse, completeness_pct, +-- can_export) and lacked the columns the current store layer reads/writes +-- (store_projects.go CreateProject/GetProject): customer_name, description, +-- narrative_text, ce_marking_target, completeness_score, risk_summary, +-- triggered_regulations, archived_at. +-- +-- This migration brings the table to the code's expectation. Idempotent and +-- guarded so it is a no-op where the table already matches. Legacy columns are +-- only made nullable (not dropped) so any historical data survives while the +-- current INSERT (which omits them) no longer fails on NOT NULL. +-- ========================================================================== + +ALTER TABLE iace_projects + ADD COLUMN IF NOT EXISTS parent_project_id uuid, + ADD COLUMN IF NOT EXISTS customer_name text, + ADD COLUMN IF NOT EXISTS description text, + ADD COLUMN IF NOT EXISTS narrative_text text, + ADD COLUMN IF NOT EXISTS ce_marking_target text, + ADD COLUMN IF NOT EXISTS completeness_score double precision, + ADD COLUMN IF NOT EXISTS risk_summary jsonb, + ADD COLUMN IF NOT EXISTS triggered_regulations jsonb, + ADD COLUMN IF NOT EXISTS archived_at timestamptz; + +-- Relax legacy NOT NULL columns the current code never writes, so INSERTs that +-- omit them succeed. DROP NOT NULL is a no-op when already nullable; the guard +-- skips columns that do not exist in this schema variant. +DO $$ +DECLARE + col text; +BEGIN + FOREACH col IN ARRAY ARRAY[ + 'intended_use', 'limits_description', 'reasonably_foreseeable_misuse', + 'completeness_pct', 'can_export' + ] LOOP + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'iace_projects' AND column_name = col + ) THEN + EXECUTE format('ALTER TABLE iace_projects ALTER COLUMN %I DROP NOT NULL', col); + END IF; + END LOOP; +END $$; diff --git a/ai-compliance-sdk/migrations/153_iace_children_reconcile.sql b/ai-compliance-sdk/migrations/153_iace_children_reconcile.sql new file mode 100644 index 00000000..d854371f --- /dev/null +++ b/ai-compliance-sdk/migrations/153_iace_children_reconcile.sql @@ -0,0 +1,70 @@ +-- Migration 153: reconcile iace_components / iace_hazards / iace_mitigations +-- with the current Go schema. +-- ========================================================================== +-- Same drift as 152 (iace_projects): the consolidation copied an OLDER table +-- generation with different column names (title vs name, component_ids vs +-- component_id, safety_relevant vs is_safety_relevant). This adds every column +-- the current store layer reads/writes and relaxes legacy NOT NULL columns the +-- code no longer fills, so INSERTs succeed. Idempotent + guarded. +-- ========================================================================== + +-- iace_components +ALTER TABLE iace_components + ADD COLUMN IF NOT EXISTS is_safety_relevant boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS is_networked boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS parent_id uuid, + ADD COLUMN IF NOT EXISTS description text, + ADD COLUMN IF NOT EXISTS version text, + ADD COLUMN IF NOT EXISTS metadata jsonb; + +-- iace_hazards +ALTER TABLE iace_hazards + ADD COLUMN IF NOT EXISTS component_id uuid, + ADD COLUMN IF NOT EXISTS library_hazard_id uuid, + ADD COLUMN IF NOT EXISTS name text, + ADD COLUMN IF NOT EXISTS scenario text, + ADD COLUMN IF NOT EXISTS sub_category text, + ADD COLUMN IF NOT EXISTS machine_module text, + ADD COLUMN IF NOT EXISTS function text, + ADD COLUMN IF NOT EXISTS lifecycle_phase text, + ADD COLUMN IF NOT EXISTS hazardous_zone text, + ADD COLUMN IF NOT EXISTS trigger_event text, + ADD COLUMN IF NOT EXISTS affected_person text, + ADD COLUMN IF NOT EXISTS possible_harm text, + ADD COLUMN IF NOT EXISTS review_status text; + +-- iace_mitigations +ALTER TABLE iace_mitigations + ADD COLUMN IF NOT EXISTS name text, + ADD COLUMN IF NOT EXISTS verification_method text, + ADD COLUMN IF NOT EXISTS verification_result text, + ADD COLUMN IF NOT EXISTS verified_at timestamptz, + ADD COLUMN IF NOT EXISTS verified_by uuid, + ADD COLUMN IF NOT EXISTS is_relevant boolean NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS is_customer_standard boolean NOT NULL DEFAULT false; + +-- Relax legacy NOT NULL columns the current code never writes. +DO $$ +DECLARE + rec record; +BEGIN + FOR rec IN SELECT * FROM (VALUES + ('iace_components','tenant_id'),('iace_components','safety_relevant'),('iace_components','tags'), + ('iace_hazards','tenant_id'),('iace_hazards','title'),('iace_hazards','component_ids'), + ('iace_hazards','library_id'),('iace_hazards','severity'),('iace_hazards','exposure'), + ('iace_hazards','probability'),('iace_hazards','avoidance'),('iace_hazards','risk_score'), + ('iace_hazards','risk_level'), + ('iace_mitigations','tenant_id'),('iace_mitigations','project_id'),('iace_mitigations','title'), + ('iace_mitigations','sub_type'),('iace_mitigations','control_ids'), + ('iace_mitigations','verification_evidence') + ) AS t(tbl, col) + LOOP + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = rec.tbl AND column_name = rec.col + ) THEN + EXECUTE format('ALTER TABLE %I ALTER COLUMN %I DROP NOT NULL', rec.tbl, rec.col); + END IF; + END LOOP; +END $$; diff --git a/ai-compliance-sdk/migrations/154_iace_mitigation_constraints.sql b/ai-compliance-sdk/migrations/154_iace_mitigation_constraints.sql new file mode 100644 index 00000000..b4beb4a6 --- /dev/null +++ b/ai-compliance-sdk/migrations/154_iace_mitigation_constraints.sql @@ -0,0 +1,12 @@ +-- Migration 154: add the mitigation UNIQUE/CHECK that the current code relies on. +-- The reconciled (consolidated) iace_mitigations lacked the constraints from +-- migrations 029/030, so CreateMitigation's `ON CONFLICT (hazard_id, name)` +-- failed (SQLSTATE 42P10) and no mitigations were created. Idempotent. + +ALTER TABLE iace_mitigations + DROP CONSTRAINT IF EXISTS iace_mitigations_hazard_name_uniq; +ALTER TABLE iace_mitigations + ADD CONSTRAINT iace_mitigations_hazard_name_uniq UNIQUE (hazard_id, name); + +CREATE INDEX IF NOT EXISTS idx_iace_mitigations_relevant + ON iace_mitigations(hazard_id) WHERE is_relevant = TRUE; diff --git a/ai-compliance-sdk/migrations/155_iace_component_presence.sql b/ai-compliance-sdk/migrations/155_iace_component_presence.sql new file mode 100644 index 00000000..34a77549 --- /dev/null +++ b/ai-compliance-sdk/migrations/155_iace_component_presence.sql @@ -0,0 +1,5 @@ +-- Migration 155: component presence status for expert review. +-- vorhanden | nicht_vorhanden (engine negation verdict) | geloescht (soft-delete). +-- Only `vorhanden` components feed pattern matching. +ALTER TABLE iace_components + ADD COLUMN IF NOT EXISTS presence_status text NOT NULL DEFAULT 'vorhanden'; diff --git a/ai-compliance-sdk/migrations/156_iace_component_ce.sql b/ai-compliance-sdk/migrations/156_iace_component_ce.sql new file mode 100644 index 00000000..030008a9 --- /dev/null +++ b/ai-compliance-sdk/migrations/156_iace_component_ce.sql @@ -0,0 +1,9 @@ +-- Migration 156: ce_marked on iace_components. +-- Marks a bought component that carries its own CE / Declaration of Conformity +-- (finished robot, actuator, drive, safety PLC ...). SAFE semantics: hazards are +-- NOT suppressed (integration/application hazards remain the integrator's job); +-- ce_marked only drives provenance + evidence hints in the UI, and flags that a +-- CE + safety-relevant component still needs its integrated safety function +-- (PL/SIL) validated. +ALTER TABLE iace_components + ADD COLUMN IF NOT EXISTS ce_marked boolean NOT NULL DEFAULT false;