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.
This commit is contained in:
@@ -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 <reachability|consistency|vocabulary|echo|hierarchy> [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 <limits-form.json> <hazards.json>")
|
||||
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 <hazards.json> <mitigations.json>")
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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"}},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user