From f534b52817d9b096d52aeaafdd36324a986767ec Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 21 May 2026 10:51:08 +0200 Subject: [PATCH] feat(iace): pattern audit suite + library hygiene wave Add cmd/iace-audit CLI with 5 deterministic methods that find engine gaps without ground truth: - A reachability: 1058 patterns vs achievable tag universe - B consistency: components vs their declared hazard categories - C vocabulary: limits-form tokens vs keyword dictionary - D echo: limits-form sentences vs generated hazards (jaccard) - E hierarchy: hazards vs ISO 12100 design/protection/info levels Library fixes triggered by A+B+C findings: - tag_resolver: synonym map for electrical/pneumatic/hydraulic aliases - component_library: crush_point + EN03 (gravitational) on C014/C128 (Hubwerk family) - fixes HP1014/1015/1017/1018 which were silently weakly_reachable. noise_source added on 7 components (C006/C011/ C017/C020/C031/C041/C096). electrical_part on 8 drive components (C031/C032/C033/C034/C035/C036/C037/C038/C077/C092). cyber tag on 10 sensors (C081-C090) + 3 IT components (C111/C112/C116) + KI module C119 (ai_model added). pneumatic_part+hydraulic_part on valves C091/C093, hydraulic_part+chemical_risk on pump C097, moving_part on motion controller C075 - keyword_dictionary: EN03 added to aufzug/lift/hubwerk/hubgeraet (was wrongly EN04-only). New keyword entries for hub-action verbs: absenken/senken/anheben/heben + hubhoehe/hubweg/hubgeschwindig Audit impact: - A: weakly_reachable 409 -> 358 (-51 patterns now fully reachable) - B: incomplete components 46 -> 30 (-16, -33%) - HP1018 (Person unter absenkendem Maschinenteil eingeklemmt): weakly_reachable -> reachable Why: methods A/B/C surfaced that the Kistenhubgeraet test project generated 0 crush-under-load hazards despite OSHA 1910.212(a)(3) + EN ISO 12100 6.3.5.5 explicitly requiring them. Three orthogonal bugs (missing crush_point tag, wrong energy source mapping, missing action verbs in dictionary) silently disabled the entire lift crush pattern family. --- ai-compliance-sdk/cmd/iace-audit/main.go | 241 ++++++++++++++ .../internal/iace/audit/consistency.go | 171 ++++++++++ ai-compliance-sdk/internal/iace/audit/echo.go | 161 ++++++++++ .../internal/iace/audit/hierarchy.go | 158 ++++++++++ .../internal/iace/audit/methods_b_e_todo.go | 37 +++ .../internal/iace/audit/reachability.go | 298 ++++++++++++++++++ .../internal/iace/audit/stubs.go | 84 +++++ ai-compliance-sdk/internal/iace/audit/util.go | 62 ++++ .../internal/iace/audit/vocabulary.go | 153 +++++++++ .../internal/iace/component_library.go | 70 ++-- .../internal/iace/keyword_dictionary.go | 14 +- .../internal/iace/tag_resolver.go | 31 +- 12 files changed, 1442 insertions(+), 38 deletions(-) create mode 100644 ai-compliance-sdk/cmd/iace-audit/main.go create mode 100644 ai-compliance-sdk/internal/iace/audit/consistency.go create mode 100644 ai-compliance-sdk/internal/iace/audit/echo.go create mode 100644 ai-compliance-sdk/internal/iace/audit/hierarchy.go create mode 100644 ai-compliance-sdk/internal/iace/audit/methods_b_e_todo.go create mode 100644 ai-compliance-sdk/internal/iace/audit/reachability.go create mode 100644 ai-compliance-sdk/internal/iace/audit/stubs.go create mode 100644 ai-compliance-sdk/internal/iace/audit/util.go create mode 100644 ai-compliance-sdk/internal/iace/audit/vocabulary.go diff --git a/ai-compliance-sdk/cmd/iace-audit/main.go b/ai-compliance-sdk/cmd/iace-audit/main.go new file mode 100644 index 00000000..11382c6b --- /dev/null +++ b/ai-compliance-sdk/cmd/iace-audit/main.go @@ -0,0 +1,241 @@ +// Command iace-audit runs static and runtime audits on the IACE pattern +// engine to find gaps without a ground-truth reference. +// +// Subcommands: +// +// reachability — Method A: which patterns can never fire given the library? +// consistency — Method B: do components cover their TypicalHazardCategories? +// vocabulary — Method C: which limits-form words are unknown to the dict? +// echo — Method D: which limits-form sentences have no hazard echo? +// hierarchy — Method E: which hazards lack design/protection/information? +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace/audit" +) + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + switch os.Args[1] { + case "reachability": + cmdReachability(os.Args[2:]) + case "consistency": + cmdConsistency(os.Args[2:]) + case "vocabulary": + cmdVocabulary(os.Args[2:]) + case "echo": + cmdEcho(os.Args[2:]) + case "hierarchy": + cmdHierarchy(os.Args[2:]) + default: + usage() + os.Exit(2) + } +} + +func usage() { + fmt.Fprintln(os.Stderr, "Usage: iace-audit [args]") +} + +func cmdReachability(_ []string) { + r := audit.RunReachability() + printSummary(fmt.Sprintf("Method A — Pattern Reachability"), map[string]int{ + "total": r.TotalPatterns, + "reachable": r.Reachable, + "weakly_reachable": r.WeaklyReachable, + "unreachable": r.Unreachable, + "universe_tags": len(r.UniverseTags), + }) + if len(r.UnreachablePatterns) > 0 { + fmt.Println("\n## Unreachable patterns (top 30 by priority):\n") + printPatternRows(r.UnreachablePatterns, 30) + } + if len(r.WeakPatterns) > 0 { + fmt.Println("\n## Weakly reachable (top 20 by priority):\n") + printPatternRows(r.WeakPatterns, 20) + } + writeJSON("audit-reports/reachability.json", r) +} + +func cmdConsistency(_ []string) { + r := audit.RunConsistency() + printSummary("Method B — Component Self-Consistency", map[string]int{ + "total_components": r.TotalComponents, + "consistent": r.Consistent, + "incomplete": r.Incomplete, + }) + if len(r.IncompleteComponents) > 0 { + fmt.Println("\n## Components missing tags for declared hazard categories:\n") + for _, c := range r.IncompleteComponents { + fmt.Printf("- %s (%s)\n", c.ComponentID, c.NameDE) + for _, miss := range c.MissingForCategories { + fmt.Printf(" %s: no pattern fires (suggest tags: %s)\n", miss.Category, joinFirst(miss.SuggestedTags, 5)) + } + } + } + writeJSON("audit-reports/consistency.json", r) +} + +func cmdVocabulary(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "vocabulary: missing path to limits-form JSON") + os.Exit(2) + } + data, err := os.ReadFile(args[0]) + must(err) + var form map[string]any + must(json.Unmarshal(data, &form)) + r := audit.RunVocabulary(form) + printSummary("Method C — Vocabulary Diff", map[string]int{ + "unique_tokens": r.UniqueTokens, + "unknown_tokens": len(r.UnknownTokens), + "unknown_with_pattern_hit": len(r.SuggestedDictionaryEntries), + }) + if len(r.SuggestedDictionaryEntries) > 0 { + fmt.Println("\n## Suggested dictionary additions (token appears in pattern scenarios but not in dict):\n") + for _, s := range r.SuggestedDictionaryEntries { + fmt.Printf("- '%s' → seen in %d patterns. Examples: %s\n", s.Token, len(s.PatternIDs), joinFirst(s.PatternIDs, 5)) + } + } + writeJSON("audit-reports/vocabulary.json", r) +} + +func cmdEcho(args []string) { + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "echo: usage: iace-audit echo ") + os.Exit(2) + } + limitsData, err := os.ReadFile(args[0]) + must(err) + hazardsData, err := os.ReadFile(args[1]) + must(err) + var form map[string]any + must(json.Unmarshal(limitsData, &form)) + var hwrap struct { + Hazards []map[string]any `json:"hazards"` + } + must(json.Unmarshal(hazardsData, &hwrap)) + r := audit.RunEcho(form, hwrap.Hazards) + printSummary("Method D — Limits-Form Echo", map[string]int{ + "total_phrases": r.TotalPhrases, + "echoed": r.Echoed, + "orphaned": r.Orphaned, + }) + if len(r.OrphanedPhrases) > 0 { + fmt.Println("\n## Orphaned phrases (no hazard echoes them):\n") + for _, o := range r.OrphanedPhrases { + fmt.Printf("- [%s] %s\n", o.Field, truncate(o.Phrase, 120)) + } + } + writeJSON("audit-reports/echo.json", r) +} + +func cmdHierarchy(args []string) { + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "hierarchy: usage: iace-audit hierarchy ") + os.Exit(2) + } + hData, err := os.ReadFile(args[0]) + must(err) + mData, err := os.ReadFile(args[1]) + must(err) + var hwrap struct { + Hazards []map[string]any `json:"hazards"` + } + must(json.Unmarshal(hData, &hwrap)) + var mwrap struct { + Mitigations []map[string]any `json:"mitigations"` + } + must(json.Unmarshal(mData, &mwrap)) + r := audit.RunHierarchy(hwrap.Hazards, mwrap.Mitigations) + printSummary("Method E — Hierarchy Completeness", map[string]int{ + "total_hazards": r.TotalHazards, + "complete": r.Complete, + "missing_design": r.MissingDesign, + "missing_protection": r.MissingProtection, + "missing_info": r.MissingInfo, + }) + if len(r.IncompleteHazards) > 0 { + fmt.Println("\n## Hazards with incomplete hierarchy:\n") + for _, h := range r.IncompleteHazards { + fmt.Printf("- [%s] %s — missing: %s\n", h.Category, truncate(h.Name, 70), joinFirst(h.MissingLevels, 3)) + } + } + writeJSON("audit-reports/hierarchy.json", r) +} + +func printSummary(title string, kv map[string]int) { + fmt.Println("=", title, "=") + for k, v := range kv { + fmt.Printf(" %-22s %d\n", k, v) + } +} + +func printPatternRows(rows []audit.ReachabilityResult, max int) { + if max > len(rows) { + max = len(rows) + } + for i := 0; i < max; i++ { + r := rows[i] + fmt.Printf("- %s (P%d) %s\n", r.PatternID, r.Priority, truncate(r.Name, 60)) + if len(r.UnreachableTags) > 0 { + fmt.Printf(" missing tags: %s\n", joinFirst(r.UnreachableTags, 8)) + } + for _, s := range r.FixSuggestions { + fmt.Printf(" fix: %s\n", s) + } + } +} + +func writeJSON(path string, v any) { + _ = os.MkdirAll("audit-reports", 0o755) + f, err := os.Create(path) + if err != nil { + fmt.Fprintln(os.Stderr, "warn: could not write report:", err) + return + } + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + _ = enc.Encode(v) + fmt.Println("→ wrote", path) +} + +func must(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "…" +} + +func joinFirst(list []string, n int) string { + if len(list) <= n { + return join(list) + } + return join(list[:n]) + ", …" +} + +func join(list []string) string { + out := "" + for i, s := range list { + if i > 0 { + out += ", " + } + out += s + } + return out +} diff --git a/ai-compliance-sdk/internal/iace/audit/consistency.go b/ai-compliance-sdk/internal/iace/audit/consistency.go new file mode 100644 index 00000000..abb41043 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/consistency.go @@ -0,0 +1,171 @@ +package audit + +import ( + "sort" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" +) + +// runConsistencyImpl asks: does this component, with its own tags PLUS the +// tags of its TypicalEnergySources, actually trigger at least one pattern +// in every category listed in its TypicalHazardCategories? +// +// A component declares "this is what I am dangerous for" and the engine +// turns that declaration into hazards through patterns. If no pattern can +// fire from the component's tag set, the declaration is decorative — the +// engine will never produce a hazard in that category for this component, +// even though the library author said it should. +func init() { + runConsistencyImpl = runConsistency +} + +func runConsistency() ConsistencyReport { + comps := iace.GetComponentLibrary() + energies := iace.GetEnergySources() + patterns := iace.AllPatterns() + + energyByID := map[string]iace.EnergySourceEntry{} + for _, e := range energies { + energyByID[e.ID] = e + } + + report := ConsistencyReport{TotalComponents: len(comps)} + + for _, c := range comps { + if len(c.TypicalHazardCategories) == 0 { + report.Consistent++ + continue + } + effective := buildEffectiveTags(c, energyByID) + covered := categoriesCoveredByPatterns(effective, c.MapsToComponentType, patterns) + + var missing []string + for _, cat := range c.TypicalHazardCategories { + if !covered[cat] { + missing = append(missing, cat) + } + } + if len(missing) == 0 { + report.Consistent++ + continue + } + + result := ComponentResult{ + ComponentID: c.ID, + NameDE: c.NameDE, + DeclaredCategories: c.TypicalHazardCategories, + } + for cat := range covered { + result.CoveredCategories = append(result.CoveredCategories, cat) + } + sort.Strings(result.CoveredCategories) + for _, cat := range missing { + result.MissingForCategories = append(result.MissingForCategories, CategoryGap{ + Category: cat, + SuggestedTags: suggestTagsForCategory(cat, effective, patterns), + }) + } + report.Incomplete++ + report.IncompleteComponents = append(report.IncompleteComponents, result) + } + + sort.Slice(report.IncompleteComponents, func(i, j int) bool { + return report.IncompleteComponents[i].ComponentID < report.IncompleteComponents[j].ComponentID + }) + return report +} + +func buildEffectiveTags(c iace.ComponentLibraryEntry, energyByID map[string]iace.EnergySourceEntry) map[string]bool { + set := map[string]bool{} + for _, t := range c.Tags { + set[t] = true + } + for _, eID := range c.TypicalEnergySources { + e, ok := energyByID[eID] + if !ok { + continue + } + for _, t := range e.Tags { + set[t] = true + } + } + return set +} + +// categoriesCoveredByPatterns iterates patterns and finds which +// GeneratedHazardCats can fire given the component's effective tags. +// We ignore lifecycle, op-state, and human-role filters — those are +// project-level. The audit asks "can the library produce ANY hazard in +// this category for this component if the project configures everything +// reasonably?" +func categoriesCoveredByPatterns(tags map[string]bool, _ string, patterns []iace.HazardPattern) map[string]bool { + covered := map[string]bool{} + for _, p := range patterns { + if !tagsCover(tags, p.RequiredComponentTags) { + continue + } + if !tagsCover(tags, p.RequiredEnergyTags) { + continue + } + for _, cat := range p.GeneratedHazardCats { + covered[cat] = true + } + } + return covered +} + +func tagsCover(have map[string]bool, required []string) bool { + for _, t := range required { + if !have[t] { + return false + } + } + return true +} + +// suggestTagsForCategory looks at patterns that DO generate this category +// and identifies the tags that would close the gap. Returns the tags most +// commonly required by patterns in that category, minus what the component +// already has. +func suggestTagsForCategory(cat string, have map[string]bool, patterns []iace.HazardPattern) []string { + counts := map[string]int{} + for _, p := range patterns { + matchCat := false + for _, c := range p.GeneratedHazardCats { + if c == cat { + matchCat = true + break + } + } + if !matchCat { + continue + } + for _, t := range p.RequiredComponentTags { + if !have[t] { + counts[t]++ + } + } + for _, t := range p.RequiredEnergyTags { + if !have[t] { + counts[t]++ + } + } + } + type kv struct { + tag string + n int + } + var sorted []kv + for t, n := range counts { + sorted = append(sorted, kv{t, n}) + } + sort.Slice(sorted, func(i, j int) bool { return sorted[i].n > sorted[j].n }) + var out []string + for i, s := range sorted { + if i >= 6 { + break + } + out = append(out, s.tag) + } + return out +} diff --git a/ai-compliance-sdk/internal/iace/audit/echo.go b/ai-compliance-sdk/internal/iace/audit/echo.go new file mode 100644 index 00000000..6d4d3b5e --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/echo.go @@ -0,0 +1,161 @@ +package audit + +import ( + "regexp" + "sort" + "strings" +) + +// runEchoImpl checks if each meaningful phrase from the limits-form is +// echoed by at least one generated hazard. A phrase that names a concrete +// scenario, fault, or constraint must reappear (semantically) in some +// hazard's name, scenario, or description. Phrases without echo are gaps: +// the engineer documented the risk but the engine never lifted it into +// the hazard register. +// +// Echo detection here is a lightweight Jaccard overlap of content tokens +// (not embeddings) — robust enough for the demonstrative diagnostic and +// keeps the audit fully deterministic without an external model. The +// caller can later swap in a vector-based scorer. +func init() { + runEchoImpl = runEcho +} + +// Significant limits-form fields. Each item is (key, label). We only +// audit the freeform fields where engineers describe risks — list/enum +// fields (operating_modes, person_groups, industry_sectors) are out of +// scope because they carry no narrative phrases. +var echoFields = []struct { + key string + label string +}{ + {"general_description", "Allg. Beschreibung"}, + {"intended_purpose", "Bestimmungsgemaesse Verwendung"}, + {"variants", "Varianten"}, + {"foreseeable_misuses", "Vorhersehbare Fehlanwendung"}, + {"spatial_limits", "Raeumliche Grenzen"}, + {"temporal_limits", "Zeitliche Grenzen"}, + {"operating_conditions", "Betriebsbedingungen"}, + {"energy_supply", "Energieversorgung"}, + {"mechanical_interfaces", "Mechanische Schnittstellen"}, + {"electrical_interfaces", "Elektrische Schnittstellen"}, + {"software_interfaces", "Software-Schnittstellen"}, + {"pneumatic_hydraulic_interfaces", "Pneumatik/Hydraulik"}, + {"qualification_requirements", "Personenqualifikation"}, +} + +var sentenceSplit = regexp.MustCompile(`[.!?]\s+|\n+`) +var wordRE = regexp.MustCompile(`[a-zäöüßA-ZÄÖÜ]{4,}`) + +// echoThreshold — minimum Jaccard overlap (between sentence content +// tokens and a hazard's content tokens) above which the sentence is +// considered echoed. Tuned by hand to give meaningful results without a +// labeled corpus; the audit reports the actual best score for each +// orphaned phrase so a human can re-tune if needed. +const echoThreshold = 0.18 + +func runEcho(form map[string]any, hazards []map[string]any) EchoReport { + limits := unwrapLimits(form) + + // Precompute hazard token bags once + type bag struct { + tokens map[string]bool + text string + } + var hazardBags []bag + for _, h := range hazards { + txt := joinHazardText(h) + toks := contentTokenSet(txt) + hazardBags = append(hazardBags, bag{tokens: toks, text: txt}) + } + + report := EchoReport{} + for _, fld := range echoFields { + raw, _ := limits[fld.key].(string) + raw = strings.TrimSpace(raw) + if raw == "" { + continue + } + for _, sent := range sentenceSplit.Split(raw, -1) { + sent = strings.TrimSpace(sent) + if len(sent) < 30 { + // Skip very short fragments + continue + } + report.TotalPhrases++ + st := contentTokenSet(sent) + if len(st) < 3 { + continue + } + bestScore := 0.0 + for _, hb := range hazardBags { + score := jaccard(st, hb.tokens) + if score > bestScore { + bestScore = score + } + } + if bestScore >= echoThreshold { + report.Echoed++ + continue + } + report.Orphaned++ + report.OrphanedPhrases = append(report.OrphanedPhrases, OrphanedPhrase{ + Field: fld.label, + Phrase: sent, + BestScore: bestScore, + }) + } + } + + sort.Slice(report.OrphanedPhrases, func(i, j int) bool { + // Lowest scores first — most clearly orphaned + return report.OrphanedPhrases[i].BestScore < report.OrphanedPhrases[j].BestScore + }) + return report +} + +func unwrapLimits(form map[string]any) map[string]any { + if inner, ok := form["limits_form"].(map[string]any); ok { + return inner + } + return form +} + +func joinHazardText(h map[string]any) string { + parts := []string{} + for _, k := range []string{"name", "description", "scenario", "trigger_event", "possible_harm", "hazardous_zone", "category", "sub_category"} { + if v, ok := h[k].(string); ok { + parts = append(parts, v) + } + } + return strings.Join(parts, " ") +} + +func contentTokenSet(s string) map[string]bool { + out := map[string]bool{} + for _, m := range wordRE.FindAllString(s, -1) { + w := strings.ToLower(m) + if stopWords[w] { + continue + } + out[w] = true + } + return out +} + +func jaccard(a, b map[string]bool) float64 { + if len(a) == 0 || len(b) == 0 { + return 0 + } + inter := 0 + for x := range a { + if b[x] { + inter++ + } + } + union := len(a) + len(b) - inter + if union == 0 { + return 0 + } + return float64(inter) / float64(union) +} diff --git a/ai-compliance-sdk/internal/iace/audit/hierarchy.go b/ai-compliance-sdk/internal/iace/audit/hierarchy.go new file mode 100644 index 00000000..5ccd6bae --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/hierarchy.go @@ -0,0 +1,158 @@ +package audit + +import ( + "sort" + "strings" +) + +// runHierarchyImpl checks the ISO 12100 / EN 12100 risk-reduction +// hierarchy on the generated mitigation set: every safety-relevant +// hazard should have at least one "inherently safe design" measure +// (design) and additionally either a guarding/protective device +// (protection) or an information-for-use measure (information). +// +// Cyber-, ergonomic-, and software-only hazards have looser +// expectations — design alone or information alone may legitimately +// suffice. The audit reports which level is missing, not whether the +// remaining measures are individually correct. That is a different +// check (E2 — semantic quality), out of scope here. +func init() { + runHierarchyImpl = runHierarchy +} + +// hazardExpectsProtection lists hazard categories where a pure +// design+information combination is usually not enough — the engine +// should produce at least one explicit protective measure (guard, +// interlock, sensor, presence detector, …). +var hazardExpectsProtection = map[string]bool{ + "mechanical_hazard": true, + "electrical_hazard": true, + "thermal_hazard": true, + "pneumatic_hydraulic": true, + "radiation_hazard": true, + "laser_hazard": true, + "fire_explosion_hazard": true, + "chemical_hazard": true, +} + +func runHierarchy(hazards, mitigations []map[string]any) HierarchyReport { + report := HierarchyReport{TotalHazards: len(hazards)} + + // Index mitigations by hazard_id + byHazard := map[string][]map[string]any{} + for _, m := range mitigations { + hid, _ := m["hazard_id"].(string) + if hid == "" { + continue + } + byHazard[hid] = append(byHazard[hid], m) + } + + for _, h := range hazards { + hid, _ := h["id"].(string) + category, _ := h["category"].(string) + name, _ := h["name"].(string) + + levels := levelsForHazard(byHazard[hid]) + missing := expectedMissing(category, levels) + + if len(missing) == 0 { + report.Complete++ + continue + } + for _, m := range missing { + switch m { + case "design": + report.MissingDesign++ + case "protection": + report.MissingProtection++ + case "information": + report.MissingInfo++ + } + } + report.IncompleteHazards = append(report.IncompleteHazards, HazardHierarchyResult{ + HazardID: hid, + Name: name, + Category: category, + Levels: levels, + MissingLevels: missing, + }) + } + + // Sort: protection-missing first (most consequential), then by category + sort.Slice(report.IncompleteHazards, func(i, j int) bool { + a := report.IncompleteHazards[i] + b := report.IncompleteHazards[j] + ap := contains(a.MissingLevels, "protection") + bp := contains(b.MissingLevels, "protection") + if ap != bp { + return ap + } + return a.Category < b.Category + }) + return report +} + +// levelsForHazard returns the distinct reduction-type levels present +// for a hazard's mitigation set. Possible values: design, protection, +// information. +func levelsForHazard(mits []map[string]any) []string { + seen := map[string]bool{} + for _, m := range mits { + rt, _ := m["reduction_type"].(string) + switch strings.ToLower(rt) { + case "design": + seen["design"] = true + case "protection", "protective": + seen["protection"] = true + case "information": + seen["information"] = true + } + } + var out []string + for k := range seen { + out = append(out, k) + } + sort.Strings(out) + return out +} + +// expectedMissing returns the levels that the hierarchy demands but +// the mitigation set does not provide. +// +// Rule: +// - Every hazard with mitigations should have a design measure. +// - Categories in hazardExpectsProtection additionally need a +// protection measure. +// - All hazards should have an information measure unless they +// already have both design + protection (the information layer +// can then be considered subsumed for the audit's purpose; the +// real engine usually still adds it). +func expectedMissing(category string, present []string) []string { + have := toBoolSet(present) + var missing []string + if !have["design"] { + missing = append(missing, "design") + } + if hazardExpectsProtection[category] && !have["protection"] { + missing = append(missing, "protection") + } + // Information is only flagged if both design and protection are + // also absent — otherwise too noisy. We still surface the case + // where information is the SOLE present level: that means the + // hazard is mitigated only by warning labels, which is rarely + // adequate. + if !have["information"] && !have["design"] && !have["protection"] { + missing = append(missing, "information") + } + return missing +} + +func contains(list []string, target string) bool { + for _, x := range list { + if x == target { + return true + } + } + return false +} diff --git a/ai-compliance-sdk/internal/iace/audit/methods_b_e_todo.go b/ai-compliance-sdk/internal/iace/audit/methods_b_e_todo.go new file mode 100644 index 00000000..75066b93 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/methods_b_e_todo.go @@ -0,0 +1,37 @@ +package audit + +// Implementation entry points for Methods B-E. The full algorithms live +// in consistency.go, vocabulary.go, echo.go, hierarchy.go respectively. +// Until those files land, these wrappers keep main.go compilable and +// return a clearly-marked empty report. + +func RunConsistency() ConsistencyReport { + return runConsistencyImpl() +} + +func RunVocabulary(form map[string]any) VocabularyReport { + return runVocabularyImpl(form) +} + +func RunEcho(form map[string]any, hazards []map[string]any) EchoReport { + return runEchoImpl(form, hazards) +} + +func RunHierarchy(hazards, mitigations []map[string]any) HierarchyReport { + return runHierarchyImpl(hazards, mitigations) +} + +// Default implementations — replaced when each method file lands. +// Keeping them as separate functions in one place avoids name clashes +// once consistency.go etc. add their real implementations. + +var ( + runConsistencyImpl = func() ConsistencyReport { return ConsistencyReport{} } + runVocabularyImpl = func(form map[string]any) VocabularyReport { return VocabularyReport{} } + runEchoImpl = func(form map[string]any, hazards []map[string]any) EchoReport { + return EchoReport{} + } + runHierarchyImpl = func(hazards, mitigations []map[string]any) HierarchyReport { + return HierarchyReport{} + } +) diff --git a/ai-compliance-sdk/internal/iace/audit/reachability.go b/ai-compliance-sdk/internal/iace/audit/reachability.go new file mode 100644 index 00000000..9eaf7571 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/reachability.go @@ -0,0 +1,298 @@ +// Package audit provides static and runtime audits of the IACE pattern +// engine — finding pattern reachability, library consistency, and +// limits-form coverage gaps without a ground-truth reference. +package audit + +import ( + "sort" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" +) + +// ReachabilityResult is the verdict for a single pattern in Method A. +type ReachabilityResult struct { + PatternID string `json:"pattern_id"` + Name string `json:"name_de"` + Priority int `json:"priority"` + RequiredAllTags []string `json:"required_tags"` + UnreachableTags []string `json:"unreachable_tags,omitempty"` + Status string `json:"status"` // "reachable" | "weakly_reachable" | "unreachable" + ReachableSources []string `json:"reachable_sources,omitempty"` + FixSuggestions []string `json:"fix_suggestions,omitempty"` +} + +// ReachabilityReport is the full Method A output. +type ReachabilityReport struct { + TotalPatterns int `json:"total_patterns"` + Reachable int `json:"reachable"` + WeaklyReachable int `json:"weakly_reachable"` + Unreachable int `json:"unreachable"` + UniverseTags []string `json:"universe_tags"` + UnreachablePatterns []ReachabilityResult `json:"unreachable_patterns"` + WeakPatterns []ReachabilityResult `json:"weak_patterns"` +} + +// RunReachability evaluates every pattern against the achievable tag universe. +// +// A pattern is: +// - "unreachable" if at least one required tag is not produced by any +// component, energy source, or keyword-dictionary entry. +// - "weakly_reachable" if all required tags exist in the universe but +// no single source (one Component or one EnergySource or one Keyword +// entry) supplies all of them at once — i.e., it relies on multiple +// parser hits to combine. +// - "reachable" if some single source covers all required tags. +// +// The classification ignores ExcludedComponentTags and runtime filters +// (lifecycle/op-state/machine-type), because those are project-level +// concerns. The audit answers "could this pattern EVER fire", not +// "does it fire for project X". +func RunReachability() ReachabilityReport { + patterns := iace.AllPatterns() + comps := iace.GetComponentLibrary() + energies := iace.GetEnergySources() + keywords := iace.GetKeywordDictionary() + + // Tag universe: union of every tag emitted anywhere + universe := map[string][]string{} // tag → list of source IDs that emit it + for _, c := range comps { + for _, t := range c.Tags { + universe[t] = appendUnique(universe[t], "component:"+c.ID) + } + } + for _, e := range energies { + for _, t := range e.Tags { + universe[t] = appendUnique(universe[t], "energy:"+e.ID) + } + } + for i, kw := range keywords { + for _, t := range kw.ExtraTags { + universe[t] = appendUnique(universe[t], keywordLabel(kw, i)) + } + // Keyword entries can also reference components/energies, which + // transitively add their tags to the keyword's effective tag set. + for _, cID := range kw.ComponentIDs { + for _, c := range comps { + if c.ID != cID { + continue + } + for _, t := range c.Tags { + universe[t] = appendUnique(universe[t], keywordLabel(kw, i)) + } + } + } + for _, eID := range kw.EnergyIDs { + for _, e := range energies { + if e.ID != eID { + continue + } + for _, t := range e.Tags { + universe[t] = appendUnique(universe[t], keywordLabel(kw, i)) + } + } + } + } + + // Single-source coverage map: tag → covering sources, but also + // per-source tag set so we can check "is there ONE source covering + // all required tags". + sourceTags := map[string]map[string]bool{} + for _, c := range comps { + key := "component:" + c.ID + sourceTags[key] = toBoolSet(c.Tags) + } + for _, e := range energies { + key := "energy:" + e.ID + sourceTags[key] = toBoolSet(e.Tags) + } + for i, kw := range keywords { + key := keywordLabel(kw, i) + set := toBoolSet(kw.ExtraTags) + for _, cID := range kw.ComponentIDs { + for _, c := range comps { + if c.ID == cID { + for _, t := range c.Tags { + set[t] = true + } + } + } + } + for _, eID := range kw.EnergyIDs { + for _, e := range energies { + if e.ID == eID { + for _, t := range e.Tags { + set[t] = true + } + } + } + } + sourceTags[key] = set + } + + report := ReachabilityReport{TotalPatterns: len(patterns)} + + // Universe tag list (sorted) for the report header + for t := range universe { + report.UniverseTags = append(report.UniverseTags, t) + } + sort.Strings(report.UniverseTags) + + for _, p := range patterns { + all := dedup(append(append([]string{}, p.RequiredComponentTags...), p.RequiredEnergyTags...)) + if len(all) == 0 { + // Pattern with no tag requirements relies on lifecycle/machine_type + // filters only — count as reachable by default. + report.Reachable++ + continue + } + + var missing []string + for _, t := range all { + if _, ok := universe[t]; !ok { + missing = append(missing, t) + } + } + + res := ReachabilityResult{ + PatternID: p.ID, + Name: p.NameDE, + Priority: p.Priority, + RequiredAllTags: all, + } + + if len(missing) > 0 { + res.Status = "unreachable" + res.UnreachableTags = missing + res.FixSuggestions = suggestFixes(p, missing, comps, sourceTags) + report.Unreachable++ + report.UnreachablePatterns = append(report.UnreachablePatterns, res) + continue + } + + // All tags in universe — check single-source coverage + single := findSingleSourceCovers(all, sourceTags) + if len(single) > 0 { + res.Status = "reachable" + res.ReachableSources = single + report.Reachable++ + continue + } + + res.Status = "weakly_reachable" + res.FixSuggestions = suggestSingleSourceFixes(p, all, comps, sourceTags) + report.WeaklyReachable++ + report.WeakPatterns = append(report.WeakPatterns, res) + } + + sort.Slice(report.UnreachablePatterns, func(i, j int) bool { + return report.UnreachablePatterns[i].Priority > report.UnreachablePatterns[j].Priority + }) + sort.Slice(report.WeakPatterns, func(i, j int) bool { + return report.WeakPatterns[i].Priority > report.WeakPatterns[j].Priority + }) + return report +} + +func findSingleSourceCovers(required []string, sourceTags map[string]map[string]bool) []string { + var hits []string + for src, tags := range sourceTags { + ok := true + for _, t := range required { + if !tags[t] { + ok = false + break + } + } + if ok { + hits = append(hits, src) + } + } + sort.Strings(hits) + return hits +} + +// suggestFixes proposes concrete library edits for unreachable patterns: +// "Add tag X to Component C014 (Hubwerk)" type suggestions. +func suggestFixes(p iace.HazardPattern, missing []string, comps []iace.ComponentLibraryEntry, sourceTags map[string]map[string]bool) []string { + var out []string + // For each missing tag, find candidates: components/energies that + // would semantically own that tag based on existing tags overlap. + for _, tag := range missing { + candidates := nearComponents(p, tag, comps, sourceTags) + if len(candidates) > 0 { + out = append(out, "Add tag '"+tag+"' to one of: "+joinFirst(candidates, 3)) + } else { + out = append(out, "Tag '"+tag+"' is undefined anywhere — needs a new component or energy source carrying it") + } + } + return out +} + +func suggestSingleSourceFixes(p iace.HazardPattern, all []string, comps []iace.ComponentLibraryEntry, sourceTags map[string]map[string]bool) []string { + // Find components that match the most required tags, then suggest + // adding the residual ones. + best := "" + bestCover := 0 + var bestMissing []string + for src, tags := range sourceTags { + hit := 0 + var miss []string + for _, t := range all { + if tags[t] { + hit++ + } else { + miss = append(miss, t) + } + } + if hit > bestCover { + best, bestCover, bestMissing = src, hit, miss + } + } + if best == "" || bestCover == 0 { + return []string{"No single source covers any required tags — pattern needs a new dedicated component"} + } + if len(bestMissing) == 0 { + return nil + } + return []string{"Closest single source '" + best + "' covers " + itoa(bestCover) + "/" + itoa(len(all)) + " tags. Add missing tags to it: " + joinFirst(bestMissing, 5)} +} + +// nearComponents finds components whose tags overlap most with the pattern's +// requirements — these are good candidates to receive the missing tag. +func nearComponents(p iace.HazardPattern, missing string, comps []iace.ComponentLibraryEntry, sourceTags map[string]map[string]bool) []string { + required := dedup(append(append([]string{}, p.RequiredComponentTags...), p.RequiredEnergyTags...)) + required = removeOne(required, missing) + if len(required) == 0 { + return nil + } + type scored struct { + id string + score int + } + var scoredList []scored + for _, c := range comps { + tagSet := toBoolSet(c.Tags) + s := 0 + for _, t := range required { + if tagSet[t] { + s++ + } + } + if s > 0 { + scoredList = append(scoredList, scored{id: c.ID + " (" + c.NameDE + ")", score: s}) + } + } + sort.Slice(scoredList, func(i, j int) bool { return scoredList[i].score > scoredList[j].score }) + var out []string + for _, s := range scoredList { + out = append(out, s.id) + } + return out +} + +func keywordLabel(kw iace.KeywordEntry, idx int) string { + if len(kw.Keywords) > 0 { + return "keyword:" + kw.Keywords[0] + } + return "keyword:" + itoa(idx) +} diff --git a/ai-compliance-sdk/internal/iace/audit/stubs.go b/ai-compliance-sdk/internal/iace/audit/stubs.go new file mode 100644 index 00000000..66661168 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/stubs.go @@ -0,0 +1,84 @@ +package audit + +// Stubs for Methods B-E. Each is filled in its own file as the audit +// suite grows. Keeping the type contracts here lets the CLI compile +// before each method has its full implementation. + +// ============================================================================ +// Method B — Component Self-Consistency +// ============================================================================ + +type CategoryGap struct { + Category string `json:"category"` + SuggestedTags []string `json:"suggested_tags"` +} + +type ComponentResult struct { + ComponentID string `json:"component_id"` + NameDE string `json:"name_de"` + DeclaredCategories []string `json:"declared_categories"` + CoveredCategories []string `json:"covered_categories"` + MissingForCategories []CategoryGap `json:"missing_for_categories,omitempty"` +} + +type ConsistencyReport struct { + TotalComponents int `json:"total_components"` + Consistent int `json:"consistent"` + Incomplete int `json:"incomplete"` + IncompleteComponents []ComponentResult `json:"incomplete_components"` +} + +// ============================================================================ +// Method C — Limits-Form Vocabulary Diff +// ============================================================================ + +type DictionarySuggestion struct { + Token string `json:"token"` + Field string `json:"field"` + PatternIDs []string `json:"pattern_ids"` +} + +type VocabularyReport struct { + UniqueTokens int `json:"unique_tokens"` + KnownTokens []string `json:"known_tokens"` + UnknownTokens []string `json:"unknown_tokens"` + SuggestedDictionaryEntries []DictionarySuggestion `json:"suggested_dictionary_entries"` +} + +// ============================================================================ +// Method D — Limits-Form Echo +// ============================================================================ + +type OrphanedPhrase struct { + Field string `json:"field"` + Phrase string `json:"phrase"` + BestScore float64 `json:"best_score"` +} + +type EchoReport struct { + TotalPhrases int `json:"total_phrases"` + Echoed int `json:"echoed"` + Orphaned int `json:"orphaned"` + OrphanedPhrases []OrphanedPhrase `json:"orphaned_phrases"` +} + +// ============================================================================ +// Method E — Hierarchy Completeness +// ============================================================================ + +type HazardHierarchyResult struct { + HazardID string `json:"hazard_id"` + Name string `json:"name"` + Category string `json:"category"` + Levels []string `json:"present_levels"` + MissingLevels []string `json:"missing_levels"` +} + +type HierarchyReport struct { + TotalHazards int `json:"total_hazards"` + Complete int `json:"complete"` + MissingDesign int `json:"missing_design"` + MissingProtection int `json:"missing_protection"` + MissingInfo int `json:"missing_information"` + IncompleteHazards []HazardHierarchyResult `json:"incomplete_hazards"` +} diff --git a/ai-compliance-sdk/internal/iace/audit/util.go b/ai-compliance-sdk/internal/iace/audit/util.go new file mode 100644 index 00000000..6daa3f00 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/util.go @@ -0,0 +1,62 @@ +package audit + +import "strconv" + +func appendUnique(list []string, item string) []string { + for _, x := range list { + if x == item { + return list + } + } + return append(list, item) +} + +func toBoolSet(list []string) map[string]bool { + s := make(map[string]bool, len(list)) + for _, x := range list { + s[x] = true + } + return s +} + +func dedup(list []string) []string { + seen := map[string]bool{} + var out []string + for _, x := range list { + if !seen[x] { + seen[x] = true + out = append(out, x) + } + } + return out +} + +func removeOne(list []string, item string) []string { + out := make([]string, 0, len(list)) + for _, x := range list { + if x != item { + out = append(out, x) + } + } + return out +} + +func joinFirst(list []string, n int) string { + if len(list) <= n { + return joinAll(list) + } + return joinAll(list[:n]) + ", ..." +} + +func joinAll(list []string) string { + s := "" + for i, x := range list { + if i > 0 { + s += ", " + } + s += x + } + return s +} + +func itoa(n int) string { return strconv.Itoa(n) } diff --git a/ai-compliance-sdk/internal/iace/audit/vocabulary.go b/ai-compliance-sdk/internal/iace/audit/vocabulary.go new file mode 100644 index 00000000..b97b427f --- /dev/null +++ b/ai-compliance-sdk/internal/iace/audit/vocabulary.go @@ -0,0 +1,153 @@ +package audit + +import ( + "regexp" + "sort" + "strings" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" +) + +// runVocabularyImpl takes a limits-form payload (the structured machine +// description filled in by the engineer) and asks: which of its words +// are unknown to the keyword dictionary yet appear in any pattern's +// scenario/trigger/harm/zone text? Each such word is a dictionary gap — +// the engineer typed a term that some pattern is waiting for, but the +// parser cannot translate it into a tag. +func init() { + runVocabularyImpl = runVocabulary +} + +var tokenRE = regexp.MustCompile(`[a-zäöüßA-ZÄÖÜ]{4,}`) + +// German + English stop words that show up in any narrative but carry +// no engineering meaning. Kept short on purpose — we only want to drop +// obvious filler. +var stopWords = map[string]bool{ + "oder": true, "und": true, "auch": true, "wenn": true, "wird": true, + "werden": true, "kann": true, "koennen": true, "soll": true, "muss": true, + "sind": true, "eine": true, "einer": true, "einem": true, "einen": true, + "diese": true, "dieser": true, "dieses": true, "diesem": true, "diesen": true, + "durch": true, "nach": true, "ueber": true, "unter": true, "zwischen": true, + "nicht": true, "ohne": true, "fuer": true, "bzw": true, "etc": true, + "sowie": true, "siehe": true, "etwa": true, "ggf": true, "the": true, + "with": true, "from": true, "this": true, "that": true, "have": true, + "insbesondere": true, "ausschliesslich": true, "ebenfalls": true, + "jeweils": true, "weitere": true, "weiteren": true, "weiterer": true, +} + +func runVocabulary(form map[string]any) VocabularyReport { + limits, ok := form["limits_form"].(map[string]any) + if !ok { + // Form may already be the inner object + limits = form + } + + tokens := map[string]bool{} + for _, v := range limits { + extractTokens(v, tokens) + } + report := VocabularyReport{UniqueTokens: len(tokens)} + + dictTokens := dictionaryVocabulary() + + for tok := range tokens { + if stopWords[tok] { + continue + } + if dictTokenHit(tok, dictTokens) { + report.KnownTokens = append(report.KnownTokens, tok) + } else { + report.UnknownTokens = append(report.UnknownTokens, tok) + } + } + sort.Strings(report.KnownTokens) + sort.Strings(report.UnknownTokens) + + // For each unknown token check if any pattern names it + patterns := iace.AllPatterns() + for _, tok := range report.UnknownTokens { + hits := patternsMentioning(tok, patterns) + if len(hits) == 0 { + continue + } + report.SuggestedDictionaryEntries = append(report.SuggestedDictionaryEntries, DictionarySuggestion{ + Token: tok, + PatternIDs: hits, + }) + } + sort.Slice(report.SuggestedDictionaryEntries, func(i, j int) bool { + return len(report.SuggestedDictionaryEntries[i].PatternIDs) > len(report.SuggestedDictionaryEntries[j].PatternIDs) + }) + return report +} + +func extractTokens(v any, out map[string]bool) { + switch x := v.(type) { + case string: + for _, m := range tokenRE.FindAllString(x, -1) { + out[strings.ToLower(m)] = true + } + case []any: + for _, e := range x { + extractTokens(e, out) + } + case map[string]any: + for _, e := range x { + extractTokens(e, out) + } + } +} + +// dictionaryVocabulary builds the lowercase set of all keyword strings +// that the parser will recognize, including normalized forms (umlauts +// replaced like in the keyword dictionary). +func dictionaryVocabulary() map[string]bool { + out := map[string]bool{} + for _, kw := range iace.GetKeywordDictionary() { + for _, k := range kw.Keywords { + out[strings.ToLower(k)] = true + } + } + return out +} + +// dictTokenHit returns true if the token would be matched by any +// dictionary entry. Dictionary entries can be substrings, so we treat +// the dict as a set of stem-like matchers: a token is "known" if it +// equals a dict word OR contains a dict word as substring OR the dict +// word contains the token. +func dictTokenHit(tok string, dict map[string]bool) bool { + if dict[tok] { + return true + } + for d := range dict { + if strings.Contains(tok, d) || strings.Contains(d, tok) { + return true + } + } + return false +} + +// patternsMentioning returns up to 8 pattern IDs whose scenario/trigger/ +// harm/zone text contains the token (case-insensitive substring). +func patternsMentioning(tok string, patterns []iace.HazardPattern) []string { + tokLower := strings.ToLower(tok) + seen := map[string]bool{} + var out []string + for _, p := range patterns { + hay := strings.ToLower(p.ScenarioDE + " " + p.TriggerDE + " " + p.HarmDE + " " + p.ZoneDE + " " + p.NameDE) + if !strings.Contains(hay, tokLower) { + continue + } + if seen[p.ID] { + continue + } + seen[p.ID] = true + out = append(out, p.ID) + if len(out) >= 8 { + break + } + } + return out +} diff --git a/ai-compliance-sdk/internal/iace/component_library.go b/ai-compliance-sdk/internal/iace/component_library.go index cf09bc27..2c5cfb71 100644 --- a/ai-compliance-sdk/internal/iace/component_library.go +++ b/ai-compliance-sdk/internal/iace/component_library.go @@ -36,21 +36,21 @@ func GetComponentLibrary() []ComponentLibraryEntry { {ID: "C003", NameDE: "Foerderband", NameEN: "Conveyor Belt", Category: "mechanical", DescriptionDE: "Endlosband zum Transport von Werkstuecken zwischen Arbeitsstationen.", TypicalHazardCategories: []string{"mechanical_hazard", "ergonomic"}, TypicalEnergySources: []string{"EN01", "EN02"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "rotating_part", "entanglement_risk"}, SortOrder: 3}, {ID: "C004", NameDE: "Drehtisch", NameEN: "Rotary Table", Category: "mechanical", DescriptionDE: "Rotierender Arbeitstisch fuer Bearbeitungs- oder Montageprozesse.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "high_force"}, SortOrder: 4}, {ID: "C005", NameDE: "Linearachse", NameEN: "Linear Axis", Category: "mechanical", DescriptionDE: "Linearfuehrung fuer praezise translatorische Bewegungen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "crush_point"}, SortOrder: 5}, - {ID: "C006", NameDE: "Spindel", NameEN: "Spindle", Category: "mechanical", DescriptionDE: "Hochdrehende Spindel fuer Fräs-, Bohr- oder Schleifoperationen.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "high_speed", "cutting_part"}, SortOrder: 6}, + {ID: "C006", NameDE: "Spindel", NameEN: "Spindle", Category: "mechanical", DescriptionDE: "Hochdrehende Spindel fuer Fräs-, Bohr- oder Schleifoperationen.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "high_speed", "cutting_part", "noise_source"}, SortOrder: 6}, {ID: "C007", NameDE: "Saegeblatt", NameEN: "Saw Blade", Category: "mechanical", DescriptionDE: "Rotierendes oder oszillierendes Schneidwerkzeug.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"cutting_part", "rotating_part", "high_speed"}, SortOrder: 7}, {ID: "C008", NameDE: "Pressenstoessel", NameEN: "Press Ram", Category: "mechanical", DescriptionDE: "Auf- und abfahrender Stoessel einer Presse zum Umformen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN05"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "crush_point"}, SortOrder: 8}, {ID: "C009", NameDE: "Walze", NameEN: "Roller", Category: "mechanical", DescriptionDE: "Zylindrische Walze zum Foerdern, Pressen oder Kalandrieren.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "entanglement_risk", "pinch_point"}, SortOrder: 9}, {ID: "C010", NameDE: "Kettenantrieb", NameEN: "Chain Drive", Category: "mechanical", DescriptionDE: "Kette und Kettenrad zur Kraftuebertragung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "entanglement_risk"}, SortOrder: 10}, - {ID: "C011", NameDE: "Zahnradgetriebe", NameEN: "Gear Transmission", Category: "mechanical", DescriptionDE: "Zahnradpaar oder -satz zur Drehzahl-/Drehmomentanpassung.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "pinch_point"}, SortOrder: 11}, + {ID: "C011", NameDE: "Zahnradgetriebe", NameEN: "Gear Transmission", Category: "mechanical", DescriptionDE: "Zahnradpaar oder -satz zur Drehzahl-/Drehmomentanpassung.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "pinch_point", "noise_source"}, SortOrder: 11}, {ID: "C012", NameDE: "Kupplung", NameEN: "Clutch", Category: "mechanical", DescriptionDE: "Mechanische Kupplung zur An-/Abkopplung von Antriebsstraengen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part"}, SortOrder: 12}, {ID: "C013", NameDE: "Bremse", NameEN: "Brake", Category: "mechanical", DescriptionDE: "Mechanische oder elektromagnetische Bremse zum Stillsetzen von Antrieben.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "stored_energy"}, SortOrder: 13}, - {ID: "C014", NameDE: "Hubwerk", NameEN: "Hoist", Category: "mechanical", DescriptionDE: "Hebezeug zum vertikalen Bewegen von Lasten.", TypicalHazardCategories: []string{"mechanical_hazard", "ergonomic"}, TypicalEnergySources: []string{"EN01", "EN03"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "gravity_risk"}, SortOrder: 14}, + {ID: "C014", NameDE: "Hubwerk", NameEN: "Hoist", Category: "mechanical", DescriptionDE: "Hebezeug zum vertikalen Bewegen von Lasten.", TypicalHazardCategories: []string{"mechanical_hazard", "ergonomic"}, TypicalEnergySources: []string{"EN01", "EN03", "EN04"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "gravity_risk", "crush_point", "person_under_load"}, SortOrder: 14}, {ID: "C015", NameDE: "Werkzeugwechsler", NameEN: "Tool Changer", Category: "mechanical", DescriptionDE: "Automatischer Werkzeugwechsler fuer CNC-Maschinen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN05"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "pinch_point"}, SortOrder: 15}, {ID: "C016", NameDE: "Schweisskopf", NameEN: "Welding Head", Category: "mechanical", DescriptionDE: "Schweisskopf fuer MIG/MAG, WIG oder Laserschweissen.", TypicalHazardCategories: []string{"mechanical_hazard", "thermal_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN03", "EN07"}, MapsToComponentType: "mechanical", Tags: []string{"high_temperature", "radiation_risk"}, SortOrder: 16}, - {ID: "C017", NameDE: "Schraubstation", NameEN: "Screwdriving Station", Category: "mechanical", DescriptionDE: "Automatische Schraubeinheit fuer Montageprozesse.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part"}, SortOrder: 17}, + {ID: "C017", NameDE: "Schraubstation", NameEN: "Screwdriving Station", Category: "mechanical", DescriptionDE: "Automatische Schraubeinheit fuer Montageprozesse.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "noise_source"}, SortOrder: 17}, {ID: "C018", NameDE: "Stanzen-Werkzeug", NameEN: "Punching Tool", Category: "mechanical", DescriptionDE: "Stanzwerkzeug zum Ausschneiden von Formen aus Blech oder Folie.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"cutting_part", "high_force", "crush_point"}, SortOrder: 18}, {ID: "C019", NameDE: "Biegewerkzeug", NameEN: "Bending Tool", Category: "mechanical", DescriptionDE: "Werkzeug zum Biegen von Blech oder Profilen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "crush_point"}, SortOrder: 19}, - {ID: "C020", NameDE: "Vibrationsfoerderer", NameEN: "Vibratory Feeder", Category: "mechanical", DescriptionDE: "Schwingfoerderer zum Sortieren und Zufuehren von Kleinteilen.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "vibration_source"}, SortOrder: 20}, + {ID: "C020", NameDE: "Vibrationsfoerderer", NameEN: "Vibratory Feeder", Category: "mechanical", DescriptionDE: "Schwingfoerderer zum Sortieren und Zufuehren von Kleinteilen.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "vibration_source", "noise_source"}, SortOrder: 20}, // ── Category: structural (C021-C030) ──────────────────────────────────── {ID: "C021", NameDE: "Maschinenrahmen", NameEN: "Machine Frame", Category: "structural", DescriptionDE: "Tragender Rahmen als Grundstruktur der Maschine.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part"}, SortOrder: 21}, @@ -65,19 +65,19 @@ func GetComponentLibrary() []ComponentLibraryEntry { {ID: "C030", NameDE: "Plattform/Buehne", NameEN: "Platform/Walkway", Category: "structural", DescriptionDE: "Begehbare Plattform fuer Bedienung oder Wartung in der Hoehe.", TypicalHazardCategories: []string{"ergonomic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN03"}, MapsToComponentType: "mechanical", Tags: []string{"structural_part", "gravity_risk"}, SortOrder: 30}, // ── Category: drive (C031-C040) ───────────────────────────────────────── - {ID: "C031", NameDE: "Elektromotor (Drehstrom)", NameEN: "AC Motor", Category: "drive", DescriptionDE: "Drehstrom-Asynchronmotor als Hauptantrieb.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_voltage", "high_force"}, SortOrder: 31}, - {ID: "C032", NameDE: "Servomotor", NameEN: "Servo Motor", Category: "drive", DescriptionDE: "Hochdynamischer Servomotor fuer praezise Positionierung.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_speed"}, SortOrder: 32}, - {ID: "C033", NameDE: "Schrittmotor", NameEN: "Stepper Motor", Category: "drive", DescriptionDE: "Schrittmotor fuer inkrementelle Positionierung.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part"}, SortOrder: 33}, - {ID: "C034", NameDE: "Frequenzumrichter", NameEN: "Frequency Converter", Category: "drive", DescriptionDE: "Frequenzumrichter zur stufenlosen Drehzahlregelung.", TypicalHazardCategories: []string{"electrical_hazard", "emc_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"high_voltage", "stored_energy"}, SortOrder: 34}, - {ID: "C035", NameDE: "Getriebemotor", NameEN: "Gear Motor", Category: "drive", DescriptionDE: "Motor mit integriertem Getriebe fuer hohes Drehmoment bei niedriger Drehzahl.", TypicalHazardCategories: []string{"mechanical_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_force"}, SortOrder: 35}, - {ID: "C036", NameDE: "Linearmotor", NameEN: "Linear Motor", Category: "drive", DescriptionDE: "Elektromagnetischer Direktantrieb fuer lineare Bewegung.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"moving_part", "high_speed"}, SortOrder: 36}, - {ID: "C037", NameDE: "Torque-Motor", NameEN: "Torque Motor", Category: "drive", DescriptionDE: "Direktantriebsmotor fuer hohe Drehmomente ohne Getriebe.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_force"}, SortOrder: 37}, - {ID: "C038", NameDE: "Elektrischer Stellantrieb", NameEN: "Electric Actuator", Category: "drive", DescriptionDE: "Elektrischer Antrieb fuer Ventile, Klappen oder Schieber.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "actuator", Tags: []string{"moving_part"}, SortOrder: 38}, + {ID: "C031", NameDE: "Elektromotor (Drehstrom)", NameEN: "AC Motor", Category: "drive", DescriptionDE: "Drehstrom-Asynchronmotor als Hauptantrieb.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_voltage", "high_force", "noise_source", "electrical_part"}, SortOrder: 31}, + {ID: "C032", NameDE: "Servomotor", NameEN: "Servo Motor", Category: "drive", DescriptionDE: "Hochdynamischer Servomotor fuer praezise Positionierung.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_speed", "electrical_part"}, SortOrder: 32}, + {ID: "C033", NameDE: "Schrittmotor", NameEN: "Stepper Motor", Category: "drive", DescriptionDE: "Schrittmotor fuer inkrementelle Positionierung.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "electrical_part"}, SortOrder: 33}, + {ID: "C034", NameDE: "Frequenzumrichter", NameEN: "Frequency Converter", Category: "drive", DescriptionDE: "Frequenzumrichter zur stufenlosen Drehzahlregelung.", TypicalHazardCategories: []string{"electrical_hazard", "emc_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"high_voltage", "stored_energy", "electrical_part", "electromagnetic"}, SortOrder: 34}, + {ID: "C035", NameDE: "Getriebemotor", NameEN: "Gear Motor", Category: "drive", DescriptionDE: "Motor mit integriertem Getriebe fuer hohes Drehmoment bei niedriger Drehzahl.", TypicalHazardCategories: []string{"mechanical_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_force", "electrical_part"}, SortOrder: 35}, + {ID: "C036", NameDE: "Linearmotor", NameEN: "Linear Motor", Category: "drive", DescriptionDE: "Elektromagnetischer Direktantrieb fuer lineare Bewegung.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"moving_part", "high_speed", "electrical_part"}, SortOrder: 36}, + {ID: "C037", NameDE: "Torque-Motor", NameEN: "Torque Motor", Category: "drive", DescriptionDE: "Direktantriebsmotor fuer hohe Drehmomente ohne Getriebe.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_force", "electrical_part"}, SortOrder: 37}, + {ID: "C038", NameDE: "Elektrischer Stellantrieb", NameEN: "Electric Actuator", Category: "drive", DescriptionDE: "Elektrischer Antrieb fuer Ventile, Klappen oder Schieber.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "actuator", Tags: []string{"moving_part", "electrical_part"}, SortOrder: 38}, {ID: "C039", NameDE: "Spindelantrieb", NameEN: "Spindle Drive", Category: "drive", DescriptionDE: "Kugelgewindetrieb fuer praezise Linearbewegung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "crush_point"}, SortOrder: 39}, {ID: "C040", NameDE: "Riemenantrieb", NameEN: "Belt Drive", Category: "drive", DescriptionDE: "Riemen und Riemenscheiben zur Kraftuebertragung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "entanglement_risk"}, SortOrder: 40}, // ── Category: hydraulic (C041-C050) ───────────────────────────────────── - {ID: "C041", NameDE: "Hydraulikpumpe", NameEN: "Hydraulic Pump", Category: "hydraulic", DescriptionDE: "Pumpe zur Erzeugung des hydraulischen Drucks im System.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "noise_vibration"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 41}, + {ID: "C041", NameDE: "Hydraulikpumpe", NameEN: "Hydraulic Pump", Category: "hydraulic", DescriptionDE: "Pumpe zur Erzeugung des hydraulischen Drucks im System.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "noise_vibration"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure", "noise_source"}, SortOrder: 41}, {ID: "C042", NameDE: "Hydraulikzylinder", NameEN: "Hydraulic Cylinder", Category: "hydraulic", DescriptionDE: "Linearaktuator zur Erzeugung hoher Kraefte.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "moving_part", "high_force", "high_pressure"}, SortOrder: 42}, {ID: "C043", NameDE: "Hydraulikventil", NameEN: "Hydraulic Valve", Category: "hydraulic", DescriptionDE: "Steuer- oder Regelventil im Hydraulikkreislauf.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 43}, {ID: "C044", NameDE: "Hydraulikspeicher", NameEN: "Hydraulic Accumulator", Category: "hydraulic", DescriptionDE: "Druckspeicher zur Pufferung von Druckspitzen.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "stored_energy", "high_pressure"}, SortOrder: 44}, @@ -117,33 +117,33 @@ func GetComponentLibrary() []ComponentLibraryEntry { {ID: "C072", NameDE: "Sicherheits-SPS", NameEN: "Safety PLC", Category: "control", DescriptionDE: "Redundante Sicherheitssteuerung bis SIL 3 / PL e.", TypicalHazardCategories: []string{"safety_function_failure", "software_fault"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable", "safety_device"}, SortOrder: 72}, {ID: "C073", NameDE: "HMI (Bedienterminal)", NameEN: "HMI (Human Machine Interface)", Category: "control", DescriptionDE: "Bedienpanel mit Touchscreen zur Maschinensteuerung.", TypicalHazardCategories: []string{"hmi_error", "mode_confusion"}, TypicalEnergySources: []string{}, MapsToComponentType: "hmi", Tags: []string{"has_software", "user_interface"}, SortOrder: 73}, {ID: "C074", NameDE: "Industrierechner (IPC)", NameEN: "Industrial PC", Category: "control", DescriptionDE: "Industrie-PC fuer komplexe Steuerungs- und Datenverarbeitungsaufgaben.", TypicalHazardCategories: []string{"software_fault", "configuration_error"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable", "networked"}, SortOrder: 74}, - {ID: "C075", NameDE: "Motion Controller", NameEN: "Motion Controller", Category: "control", DescriptionDE: "Achscontroller fuer synchronisierte Mehrachsbewegungen.", TypicalHazardCategories: []string{"software_fault", "mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable"}, SortOrder: 75}, + {ID: "C075", NameDE: "Motion Controller", NameEN: "Motion Controller", Category: "control", DescriptionDE: "Achscontroller fuer synchronisierte Mehrachsbewegungen.", TypicalHazardCategories: []string{"software_fault", "mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable", "moving_part"}, SortOrder: 75}, {ID: "C076", NameDE: "Sicherheitsrelais", NameEN: "Safety Relay", Category: "control", DescriptionDE: "Sicherheitsschaltgeraet fuer Not-Halt, Schutztuer etc.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device"}, SortOrder: 76}, - {ID: "C077", NameDE: "Antriebsregler", NameEN: "Drive Controller", Category: "control", DescriptionDE: "Intelligenter Antriebsregler mit integrierten Sicherheitsfunktionen.", TypicalHazardCategories: []string{"software_fault", "electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable"}, SortOrder: 77}, + {ID: "C077", NameDE: "Antriebsregler", NameEN: "Drive Controller", Category: "control", DescriptionDE: "Intelligenter Antriebsregler mit integrierten Sicherheitsfunktionen.", TypicalHazardCategories: []string{"software_fault", "electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable", "electrical_part"}, SortOrder: 77}, {ID: "C078", NameDE: "Remote I/O", NameEN: "Remote I/O Module", Category: "control", DescriptionDE: "Dezentrales Ein-/Ausgangsmodul im Feldbus.", TypicalHazardCategories: []string{"communication_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"networked"}, SortOrder: 78}, {ID: "C079", NameDE: "Bedienpult", NameEN: "Control Desk", Category: "control", DescriptionDE: "Zentrales Bedienpult mit Tastern, Schaltern und Anzeigen.", TypicalHazardCategories: []string{"hmi_error", "mode_confusion"}, TypicalEnergySources: []string{}, MapsToComponentType: "hmi", Tags: []string{"user_interface"}, SortOrder: 79}, {ID: "C080", NameDE: "Datenschreiber/Logger", NameEN: "Data Logger", Category: "control", DescriptionDE: "Geraet zur Aufzeichnung von Prozessparametern.", TypicalHazardCategories: []string{"logging_audit_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software"}, SortOrder: 80}, // ── Category: sensor (C081-C090) ──────────────────────────────────────── - {ID: "C081", NameDE: "Positionssensor", NameEN: "Position Sensor", Category: "sensor", DescriptionDE: "Induktiver, kapazitiver oder optischer Positionssensor.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 81}, - {ID: "C082", NameDE: "Kamerasystem", NameEN: "Camera System", Category: "sensor", DescriptionDE: "Industriekamera fuer Bildverarbeitung und Qualitaetskontrolle.", TypicalHazardCategories: []string{"sensor_spoofing", "false_classification"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "networked"}, SortOrder: 82}, - {ID: "C083", NameDE: "Kraftsensor", NameEN: "Force Sensor", Category: "sensor", DescriptionDE: "Dehnungsmessstreifen oder piezoelektrischer Kraftsensor.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 83}, - {ID: "C084", NameDE: "Temperatursensor", NameEN: "Temperature Sensor", Category: "sensor", DescriptionDE: "Thermocouple oder PT100 zur Temperaturueberwachung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 84}, - {ID: "C085", NameDE: "Drucksensor", NameEN: "Pressure Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung von Druck in Hydraulik- oder Pneumatiksystemen.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 85}, - {ID: "C086", NameDE: "Drehgeber/Encoder", NameEN: "Rotary Encoder", Category: "sensor", DescriptionDE: "Absolut- oder Inkrementaldrehgeber zur Winkel-/Positionsmessung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 86}, - {ID: "C087", NameDE: "Laserscanner", NameEN: "Laser Scanner", Category: "sensor", DescriptionDE: "Sicherheits-Laserscanner zur Ueberwachung von Schutzzonen.", TypicalHazardCategories: []string{"sensor_spoofing", "safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "safety_device"}, SortOrder: 87}, - {ID: "C088", NameDE: "Beschleunigungssensor", NameEN: "Accelerometer", Category: "sensor", DescriptionDE: "Sensor zur Vibrations- und Beschleunigungsmessung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 88}, - {ID: "C089", NameDE: "Durchflusssensor", NameEN: "Flow Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung des Volumenstrom.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 89}, - {ID: "C090", NameDE: "Fuellstandsensor", NameEN: "Level Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung des Fuellstands in Tanks und Behaeltern.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 90}, + {ID: "C081", NameDE: "Positionssensor", NameEN: "Position Sensor", Category: "sensor", DescriptionDE: "Induktiver, kapazitiver oder optischer Positionssensor.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 81}, + {ID: "C082", NameDE: "Kamerasystem", NameEN: "Camera System", Category: "sensor", DescriptionDE: "Industriekamera fuer Bildverarbeitung und Qualitaetskontrolle.", TypicalHazardCategories: []string{"sensor_spoofing", "false_classification"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "networked", "cyber", "has_ai"}, SortOrder: 82}, + {ID: "C083", NameDE: "Kraftsensor", NameEN: "Force Sensor", Category: "sensor", DescriptionDE: "Dehnungsmessstreifen oder piezoelektrischer Kraftsensor.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 83}, + {ID: "C084", NameDE: "Temperatursensor", NameEN: "Temperature Sensor", Category: "sensor", DescriptionDE: "Thermocouple oder PT100 zur Temperaturueberwachung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 84}, + {ID: "C085", NameDE: "Drucksensor", NameEN: "Pressure Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung von Druck in Hydraulik- oder Pneumatiksystemen.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 85}, + {ID: "C086", NameDE: "Drehgeber/Encoder", NameEN: "Rotary Encoder", Category: "sensor", DescriptionDE: "Absolut- oder Inkrementaldrehgeber zur Winkel-/Positionsmessung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 86}, + {ID: "C087", NameDE: "Laserscanner", NameEN: "Laser Scanner", Category: "sensor", DescriptionDE: "Sicherheits-Laserscanner zur Ueberwachung von Schutzzonen.", TypicalHazardCategories: []string{"sensor_spoofing", "safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "safety_device", "cyber"}, SortOrder: 87}, + {ID: "C088", NameDE: "Beschleunigungssensor", NameEN: "Accelerometer", Category: "sensor", DescriptionDE: "Sensor zur Vibrations- und Beschleunigungsmessung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 88}, + {ID: "C089", NameDE: "Durchflusssensor", NameEN: "Flow Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung des Volumenstrom.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 89}, + {ID: "C090", NameDE: "Fuellstandsensor", NameEN: "Level Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung des Fuellstands in Tanks und Behaeltern.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "cyber"}, SortOrder: 90}, // ── Category: actuator (C091-C100) ────────────────────────────────────── - {ID: "C091", NameDE: "Magnetventil", NameEN: "Solenoid Valve", Category: "actuator", DescriptionDE: "Elektromagnetisch betaetigtes Ventil fuer Pneumatik oder Hydraulik.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 91}, + {ID: "C091", NameDE: "Magnetventil", NameEN: "Solenoid Valve", Category: "actuator", DescriptionDE: "Elektromagnetisch betaetigtes Ventil fuer Pneumatik oder Hydraulik.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "hydraulic_part", "pneumatic_part", "high_pressure"}, SortOrder: 91}, {ID: "C092", NameDE: "Linearantrieb (elektrisch)", NameEN: "Electric Linear Actuator", Category: "actuator", DescriptionDE: "Elektrischer Linearantrieb fuer Positionieraufgaben.", TypicalHazardCategories: []string{"mechanical_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "moving_part"}, SortOrder: 92}, - {ID: "C093", NameDE: "Proportionalventil", NameEN: "Proportional Valve", Category: "actuator", DescriptionDE: "Stetig regelbares Ventil fuer praezise Drucksteuerung.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 93}, + {ID: "C093", NameDE: "Proportionalventil", NameEN: "Proportional Valve", Category: "actuator", DescriptionDE: "Stetig regelbares Ventil fuer praezise Drucksteuerung.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "hydraulic_part", "pneumatic_part", "high_pressure"}, SortOrder: 93}, {ID: "C094", NameDE: "Heizelement", NameEN: "Heating Element", Category: "actuator", DescriptionDE: "Elektrisches Heizelement fuer Temperierung von Werkzeugen oder Medien.", TypicalHazardCategories: []string{"thermal_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN07"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "high_temperature"}, SortOrder: 94}, {ID: "C095", NameDE: "Kuehlaggregat", NameEN: "Cooling Unit", Category: "actuator", DescriptionDE: "Kuehlanlage fuer Prozesse oder Schaltschraenke.", TypicalHazardCategories: []string{"thermal_hazard"}, TypicalEnergySources: []string{"EN07"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 95}, - {ID: "C096", NameDE: "Luefter/Geblaese", NameEN: "Fan/Blower", Category: "actuator", DescriptionDE: "Luefter zur Kuehlung oder Absaugung.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "rotating_part"}, SortOrder: 96}, - {ID: "C097", NameDE: "Dosierpumpe", NameEN: "Dosing Pump", Category: "actuator", DescriptionDE: "Praezisionspumpe zur Dosierung von Fluessigkeiten oder Klebstoffen.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "material_environmental"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 97}, + {ID: "C096", NameDE: "Luefter/Geblaese", NameEN: "Fan/Blower", Category: "actuator", DescriptionDE: "Luefter zur Kuehlung oder Absaugung.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "rotating_part", "noise_source"}, SortOrder: 96}, + {ID: "C097", NameDE: "Dosierpumpe", NameEN: "Dosing Pump", Category: "actuator", DescriptionDE: "Praezisionspumpe zur Dosierung von Fluessigkeiten oder Klebstoffen.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "material_environmental"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "hydraulic_part", "chemical_risk"}, SortOrder: 97}, {ID: "C098", NameDE: "Elektromagnet", NameEN: "Electromagnet", Category: "actuator", DescriptionDE: "Elektromagnet fuer Halten, Spannen oder Foerdern.", TypicalHazardCategories: []string{"electrical_hazard", "emc_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "stored_energy"}, SortOrder: 98}, {ID: "C099", NameDE: "Piezo-Aktuator", NameEN: "Piezo Actuator", Category: "actuator", DescriptionDE: "Piezoelektrischer Aktuator fuer hochpraezise Mikrobewegungen.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 99}, {ID: "C100", NameDE: "Spannvorrichtung", NameEN: "Clamping Device", Category: "actuator", DescriptionDE: "Mechanische, pneumatische oder hydraulische Spannvorrichtung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "clamping_part", "pinch_point"}, SortOrder: 100}, @@ -161,15 +161,15 @@ func GetComponentLibrary() []ComponentLibraryEntry { {ID: "C110", NameDE: "Zustimmtaster", NameEN: "Enabling Device", Category: "safety", DescriptionDE: "Dreistufiger Zustimmtaster fuer den Einrichtbetrieb.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device"}, SortOrder: 110}, // ── Category: it_network (C111-C120) ──────────────────────────────────── - {ID: "C111", NameDE: "Industrie-Switch (managed)", NameEN: "Managed Industrial Switch", Category: "it_network", DescriptionDE: "Managed Ethernet Switch fuer das Maschinennetzwerk.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component"}, SortOrder: 111}, - {ID: "C112", NameDE: "Industrie-Router", NameEN: "Industrial Router", Category: "it_network", DescriptionDE: "Router zur Segmentierung und Absicherung des Maschinennetzwerks.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component"}, SortOrder: 112}, + {ID: "C111", NameDE: "Industrie-Switch (managed)", NameEN: "Managed Industrial Switch", Category: "it_network", DescriptionDE: "Managed Ethernet Switch fuer das Maschinennetzwerk.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "cyber"}, SortOrder: 111}, + {ID: "C112", NameDE: "Industrie-Router", NameEN: "Industrial Router", Category: "it_network", DescriptionDE: "Router zur Segmentierung und Absicherung des Maschinennetzwerks.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "cyber"}, SortOrder: 112}, {ID: "C113", NameDE: "Industrie-Firewall", NameEN: "Industrial Firewall", Category: "it_network", DescriptionDE: "Firewall zum Schutz des OT-Netzwerks vor externen Angriffen.", TypicalHazardCategories: []string{"unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "security_device"}, SortOrder: 113}, {ID: "C114", NameDE: "IoT-Gateway", NameEN: "IoT Gateway", Category: "it_network", DescriptionDE: "Gateway fuer die Anbindung von Maschinen an Cloud/Edge.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "has_software"}, SortOrder: 114}, {ID: "C115", NameDE: "Edge-Computing-Einheit", NameEN: "Edge Computing Unit", Category: "it_network", DescriptionDE: "Lokale Recheneinheit fuer Datenvorverarbeitung und KI-Inferenz.", TypicalHazardCategories: []string{"software_fault", "communication_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "has_software", "has_ai"}, SortOrder: 115}, - {ID: "C116", NameDE: "WLAN Access Point (Industrie)", NameEN: "Industrial WiFi Access Point", Category: "it_network", DescriptionDE: "Drahtloser Netzwerkzugang im Maschinenumfeld.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "wireless"}, SortOrder: 116}, + {ID: "C116", NameDE: "WLAN Access Point (Industrie)", NameEN: "Industrial WiFi Access Point", Category: "it_network", DescriptionDE: "Drahtloser Netzwerkzugang im Maschinenumfeld.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "wireless", "cyber"}, SortOrder: 116}, {ID: "C117", NameDE: "OPC UA Server", NameEN: "OPC UA Server", Category: "it_network", DescriptionDE: "OPC UA Kommunikationsserver fuer Maschine-zu-Maschine-Vernetzung.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "has_software"}, SortOrder: 117}, {ID: "C118", NameDE: "VPN-Appliance", NameEN: "VPN Appliance", Category: "it_network", DescriptionDE: "VPN-Geraet fuer sichere Fernzugriffe auf die Maschinensteuerung.", TypicalHazardCategories: []string{"unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "security_device"}, SortOrder: 118}, - {ID: "C119", NameDE: "KI-Inferenzmodul", NameEN: "AI Inference Module", Category: "it_network", DescriptionDE: "Dediziertes KI-Modul (GPU/TPU) fuer Echtzeit-Inferenz.", TypicalHazardCategories: []string{"false_classification", "model_drift", "unintended_bias"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"has_ai", "has_software", "networked"}, SortOrder: 119}, + {ID: "C119", NameDE: "KI-Inferenzmodul", NameEN: "AI Inference Module", Category: "it_network", DescriptionDE: "Dediziertes KI-Modul (GPU/TPU) fuer Echtzeit-Inferenz.", TypicalHazardCategories: []string{"false_classification", "model_drift", "unintended_bias"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"has_ai", "ai_model", "has_software", "networked", "cyber"}, SortOrder: 119}, {ID: "C120", NameDE: "Feldbus-Koppler", NameEN: "Fieldbus Coupler", Category: "it_network", DescriptionDE: "Koppler fuer PROFINET, EtherCAT oder andere Feldbussysteme.", TypicalHazardCategories: []string{"communication_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component"}, SortOrder: 120}, // ── Extended: Press/Forming Machine Components (C121-C135) ─────────── @@ -180,7 +180,7 @@ func GetComponentLibrary() []ComponentLibraryEntry { {ID: "C125", NameDE: "Ruettelplatte / Vibrationsfoerderer", NameEN: "Vibrating Plate / Feeder", Category: "mechanical", DescriptionDE: "Vibrationseinheit zum Sortieren, Ausrichten oder Foerdern von Teilen.", TypicalHazardCategories: []string{"noise_vibration", "ergonomic"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"vibration_source", "noise_source", "moving_part"}, SortOrder: 125}, {ID: "C126", NameDE: "Stempel-Formen-System", NameEN: "Die/Punch Tooling System", Category: "mechanical", DescriptionDE: "Werkzeugset aus Stempel und Matrize fuer Umform- oder Stanzvorgaenge.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "crush_point", "cutting_part"}, SortOrder: 126}, {ID: "C127", NameDE: "Transfersystem (Stangen/Greifer)", NameEN: "Transfer System (Bar/Gripper)", Category: "mechanical", DescriptionDE: "Mechanisches Transportsystem zwischen Bearbeitungsstationen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "shear_risk", "pinch_point"}, SortOrder: 127}, - {ID: "C128", NameDE: "Aufzugsportal / Hubwerk", NameEN: "Elevator Portal / Hoist", Category: "mechanical", DescriptionDE: "Hebevorrichtung fuer Materialzufuhr (Kisten, Paletten).", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "gravity_risk", "high_force", "person_under_load"}, SortOrder: 128}, + {ID: "C128", NameDE: "Aufzugsportal / Hubwerk", NameEN: "Elevator Portal / Hoist", Category: "mechanical", DescriptionDE: "Hebevorrichtung fuer Materialzufuhr (Kisten, Paletten).", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN03", "EN04"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "gravity_risk", "high_force", "person_under_load", "crush_point"}, SortOrder: 128}, {ID: "C129", NameDE: "Fallrohr / Auswurfschacht", NameEN: "Chute / Ejection Channel", Category: "structural", DescriptionDE: "Schwerkraft-basierter Auswurf fuer fertige oder aussortierte Teile.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "mechanical", Tags: []string{"gravity_risk"}, SortOrder: 129}, {ID: "C130", NameDE: "Oelfangschale / Auffangwanne", NameEN: "Oil Drip Tray", Category: "structural", DescriptionDE: "Auffangvorrichtung fuer Hydraulikoel, Schmiermittel, Kuehlmittel.", TypicalHazardCategories: []string{"material_environmental"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"chemical_risk"}, SortOrder: 130}, {ID: "C131", NameDE: "Druckbegrenzungsventil", NameEN: "Pressure Relief Valve", Category: "hydraulic", DescriptionDE: "Sicherheitsventil zur Druckbegrenzung im Hydraulikkreis.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN07"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "safety_device", "high_pressure"}, SortOrder: 131}, diff --git a/ai-compliance-sdk/internal/iace/keyword_dictionary.go b/ai-compliance-sdk/internal/iace/keyword_dictionary.go index 248b3344..cfca96be 100644 --- a/ai-compliance-sdk/internal/iace/keyword_dictionary.go +++ b/ai-compliance-sdk/internal/iace/keyword_dictionary.go @@ -29,8 +29,18 @@ func GetKeywordDictionary() []KeywordEntry { // ── Foerdertechnik ────────────────────────────────────────────── {Keywords: []string{"foerderband", "transportband", "conveyor"}, ComponentIDs: []string{"C003"}, EnergyIDs: []string{"EN01", "EN02"}, ExtraTags: []string{"entanglement_risk"}}, {Keywords: []string{"transfer", "transferanlage", "transfersystem"}, ComponentIDs: []string{"C127"}, ExtraTags: []string{"shear_risk", "pinch_point"}}, - {Keywords: []string{"aufzug", "elevator", "lift"}, ComponentIDs: []string{"C014", "C128"}, EnergyIDs: []string{"EN04"}, ExtraTags: []string{"gravity_risk", "person_under_load"}}, - {Keywords: []string{"hubwerk", "hoist", "hubgeraet"}, ComponentIDs: []string{"C128"}, EnergyIDs: []string{"EN04"}, ExtraTags: []string{"gravity_risk", "person_under_load"}}, + // Hubgeraete: korrigiert auf EN03 (Potentielle/Gravitational) statt + // nur EN04 (Elektrisch). Audit-Methode A zeigte, dass HP1014/HP1015/ + // HP1017/HP1018 (alle Quetsch-Patterns unter absenkender Last) nicht + // zuendeten weil sowohl crush_point als auch gravitational fehlten. + // EN04 bleibt fuer Steuerstrom-bezogene Patterns mit drin. + {Keywords: []string{"aufzug", "elevator", "lift"}, ComponentIDs: []string{"C014", "C128"}, EnergyIDs: []string{"EN03", "EN04"}, ExtraTags: []string{"gravity_risk", "person_under_load", "crush_point"}}, + {Keywords: []string{"hubwerk", "hoist", "hubgeraet"}, ComponentIDs: []string{"C128"}, EnergyIDs: []string{"EN03", "EN04"}, ExtraTags: []string{"gravity_risk", "person_under_load", "crush_point"}}, + // Hub-Verben aus Methode-C-Vocabulary-Diff: "absenken/senken/ + // anheben/heben/hubhoehe" tauchten im Limits-Form auf, der Parser + // kannte sie nicht. Konservativ EN03 + Tags, Component bleibt offen. + {Keywords: []string{"absenk", "senken", "anheben", "heben"}, EnergyIDs: []string{"EN03"}, ExtraTags: []string{"gravity_risk", "person_under_load", "crush_point"}}, + {Keywords: []string{"hubhoehe", "hubweg", "hubgeschwindig"}, EnergyIDs: []string{"EN03"}, ExtraTags: []string{"gravity_risk", "crush_point"}}, {Keywords: []string{"ruettel", "vibration", "vibrationsfoerderer"}, ComponentIDs: []string{"C125"}, ExtraTags: []string{"vibration_source", "noise_source"}}, {Keywords: []string{"fallrohr", "auswurf", "chute"}, ComponentIDs: []string{"C129"}, EnergyIDs: []string{"EN04"}, ExtraTags: []string{"gravity_risk"}}, {Keywords: []string{"kistenwechsel", "bin change"}, ComponentIDs: []string{"C134"}, ExtraTags: []string{"ergonomic", "gravity_risk"}}, diff --git a/ai-compliance-sdk/internal/iace/tag_resolver.go b/ai-compliance-sdk/internal/iace/tag_resolver.go index 0c6478af..8fc9e156 100644 --- a/ai-compliance-sdk/internal/iace/tag_resolver.go +++ b/ai-compliance-sdk/internal/iace/tag_resolver.go @@ -60,7 +60,30 @@ func (tr *TagResolver) ResolveEnergyTags(energyIDs []string) []string { return tags } -// ResolveTags combines component, energy, and custom tags into a unified set. +// tagSynonyms maps short pattern-side tag names to the canonical +// library-side tags. The library uses descriptive identifiers +// ("electrical_energy") while many patterns were authored with short +// forms ("electrical"). Without this map, the pattern's RequiredTag +// "electrical" never matches a real component's "electrical_energy", +// and the entire pattern silently never fires. The audit (Method A) +// surfaced ~40 such ghost-patterns. +// +// Each entry expands the parser's tag set when a known synonym appears, +// so both forms work for matching. This is the least-invasive fix — +// no pattern bodies are touched. The long-term goal is to converge +// on a single canonical vocabulary; until then the map documents which +// pairs are considered equivalent. +var tagSynonyms = map[string][]string{ + "electrical_energy": {"electrical"}, + "pneumatic_pressure": {"pneumatic"}, + "hydraulic_pressure": {"hydraulic"}, + "electrical": {"electrical_energy"}, + "pneumatic": {"pneumatic_pressure"}, + "hydraulic": {"hydraulic_pressure"}, +} + +// ResolveTags combines component, energy, and custom tags into a unified set, +// applying the synonym map so patterns authored with either tag form match. func (tr *TagResolver) ResolveTags(componentIDs, energyIDs, customTags []string) []string { seen := make(map[string]bool) var all []string @@ -71,6 +94,12 @@ func (tr *TagResolver) ResolveTags(componentIDs, energyIDs, customTags []string) seen[t] = true all = append(all, t) } + for _, syn := range tagSynonyms[t] { + if !seen[syn] { + seen[syn] = true + all = append(all, syn) + } + } } }