fix: Struktureller Fix — Maschinentyp-Filter fuer Keywords + Patterns

PROBLEM: Cobot-Projekt hatte 52 Pressen-Hazards weil Keywords wie
"stempel" und "stoessel" ohne Maschinentyp-Kontext matchten.

FIX an 3 Stellen:
1. KeywordEntry.MachineTypes — Pressen-Keywords nur fuer press/*_press
2. ParseNarrative(text, machineType) — Parser laedt Maschinentyp aus Projekt
3. HazardPattern.MachineTypes — Pressen-Patterns (HP045-HP058) nur fuer Pressen

Verhindert zukuenftig falsche Zuordnungen bei neuen Kundenprojekten.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-09 08:30:45 +02:00
parent 751f4a5ee7
commit 0371eecc03
5 changed files with 64 additions and 24 deletions
@@ -6,6 +6,7 @@ import (
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ParseNarrativeRequest is the request body for POST /projects/:id/parse-narrative.
@@ -43,8 +44,16 @@ func (h *IACEHandler) ParseNarrative(c *gin.Context) {
return
}
// 1. Parse narrative text deterministically
parseResult := iace.ParseNarrative(req.NarrativeText)
// Load project to get machine type for context-aware parsing
var machineType string
if projectID, err := uuid.Parse(c.Param("id")); err == nil {
if project, err := h.store.GetProject(c.Request.Context(), projectID); err == nil && project != nil {
machineType = project.MachineType
}
}
// 1. Parse narrative text deterministically (machine-type-aware)
parseResult := iace.ParseNarrative(req.NarrativeText, machineType)
// 2. Feed parsed tags into pattern engine
// Collect all component IDs for tag resolution
@@ -27,4 +27,9 @@ type HazardPattern struct {
ZoneDE string `json:"zone_de,omitempty"` // Gefahrstelle/Zone
DefaultSeverity int `json:"default_severity,omitempty"` // 1-5
DefaultExposure int `json:"default_exposure,omitempty"` // 1-5
// MachineTypes restricts this pattern to specific machine types.
// Empty = fires for all machine types. If set, only fires when the
// project's machine_type is in this list. Prevents e.g. press-specific
// patterns from firing for a cobot project.
MachineTypes []string `json:"machine_types,omitempty"`
}
@@ -23,7 +23,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Toedliche Quetschverletzung, Amputation von Gliedmassen.",
AffectedDE: "Einrichter, Bedienpersonal im Werkzeugeinbauraum.",
ZoneDE: "Werkzeugeinbauraum unterhalb des Stoessels.",
DefaultSeverity: 5, DefaultExposure: 2,
DefaultSeverity: 5, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP046", NameDE: "Quetschen im Werkzeugeinbauraum", NameEN: "Crushing in die space",
@@ -38,7 +38,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Toedliche Quetschverletzung, Amputation der oberen Extremitaeten.",
AffectedDE: "Einrichter, Werkzeugbauer, Instandhaltungspersonal.",
ZoneDE: "Werkzeugeinbauraum zwischen Ober- und Unterwerkzeug.",
DefaultSeverity: 5, DefaultExposure: 3,
DefaultSeverity: 5, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP047", NameDE: "Oelnebelexposition Atemwege", NameEN: "Oil mist inhalation exposure",
@@ -53,7 +53,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Atemwegsreizung, chronische Lungenerkrankung bei Langzeitexposition.",
AffectedDE: "Bedienpersonal, Personen im Nahbereich der Presse.",
ZoneDE: "Arbeitsbereich rund um die Presse, insbesondere Bedienerseite.",
DefaultSeverity: 3, DefaultExposure: 4,
DefaultSeverity: 3, DefaultExposure: 4, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP048", NameDE: "Verbrennung durch heisse Werkstuecke", NameEN: "Burns from hot workpieces",
@@ -68,7 +68,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Verbrennungen zweiten oder dritten Grades an Haenden und Unterarmen.",
AffectedDE: "Bedienpersonal, Einrichter bei Werkzeugwechsel.",
ZoneDE: "Entnahmebereich, Werkzeugeinbauraum, Ablagetisch.",
DefaultSeverity: 4, DefaultExposure: 3,
DefaultSeverity: 4, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP049", NameDE: "Schwebende Last (Hubwerk/Aufzug)", NameEN: "Suspended load (hoist/elevator)",
@@ -83,7 +83,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Toedliche Verletzung durch herabfallende Last, Knochenbrueche.",
AffectedDE: "Personen im Gefahrenbereich unter der schwebenden Last.",
ZoneDE: "Bereich unterhalb des Hubwerks, Werkzeugwechselzone.",
DefaultSeverity: 5, DefaultExposure: 2,
DefaultSeverity: 5, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP050", NameDE: "Einziehen/Scheren Transfersystem", NameEN: "Draw-in/shearing at transfer system",
@@ -98,7 +98,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Schnittverletzungen, Amputation von Fingern, Quetschungen.",
AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.",
ZoneDE: "Transferbereich zwischen den Pressenstationen.",
DefaultSeverity: 4, DefaultExposure: 3,
DefaultSeverity: 4, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP051", NameDE: "Sturzgefahr Auswurfbereich", NameEN: "Fall hazard at ejection area",
@@ -114,7 +114,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Knochenbrueche, Prellungen, Kopfverletzungen bei Sturz.",
AffectedDE: "Bedienpersonal, Logistikmitarbeiter im Auswurfbereich.",
ZoneDE: "Auswurfschacht und angrenzender Bodenbereich.",
DefaultSeverity: 3, DefaultExposure: 4,
DefaultSeverity: 3, DefaultExposure: 4, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP052", NameDE: "Druckfreisetzung Hydraulikspeicher", NameEN: "Pressure release from hydraulic accumulator",
@@ -129,7 +129,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.",
AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.",
ZoneDE: "Hydraulikaggregat, Speicherbereich, Leitungsfuehrung.",
DefaultSeverity: 5, DefaultExposure: 2,
DefaultSeverity: 5, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP053", NameDE: "Impulslaerm Pressvorgang", NameEN: "Impact noise during press operation",
@@ -144,7 +144,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Laermschwerhoerigkeit, Tinnitus bei Langzeitexposition.",
AffectedDE: "Bedienpersonal, Personen in angrenzenden Arbeitsbereichen.",
ZoneDE: "Gesamter Pressenbereich, Radius ca. 5-10 m um die Maschine.",
DefaultSeverity: 3, DefaultExposure: 5,
DefaultSeverity: 3, DefaultExposure: 5, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP054", NameDE: "Schwungrad-Restenergie nach Abschaltung", NameEN: "Flywheel residual energy after shutdown",
@@ -159,7 +159,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Erfassen durch rotierende Teile, schwere Schnittverletzungen, Skalpierung.",
AffectedDE: "Instandhaltungspersonal, Einrichter nach Maschinenstopp.",
ZoneDE: "Schwungradbereich, Kupplungsraum, Antriebsseite der Presse.",
DefaultSeverity: 4, DefaultExposure: 2,
DefaultSeverity: 4, DefaultExposure: 2, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP055", NameDE: "Umgehung Schutzeinrichtung (Pressentuer)", NameEN: "Bypass of safety guard (press door)",
@@ -174,7 +174,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Toedliche Quetsch- oder Scherverletzungen bei Eingriff in den Gefahrenbereich.",
AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.",
ZoneDE: "Gesamter Werkzeugeinbauraum hinter der Schutztuer.",
DefaultSeverity: 5, DefaultExposure: 3,
DefaultSeverity: 5, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP056", NameDE: "Fehlbedienung Zweihandschaltung", NameEN: "Two-hand control misoperation",
@@ -189,7 +189,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Quetschverletzungen der freien Hand im Werkzeugbereich.",
AffectedDE: "Bedienpersonal an der Pressenbedienung.",
ZoneDE: "Gefahrenbereich zwischen Ober- und Unterwerkzeug.",
DefaultSeverity: 5, DefaultExposure: 3,
DefaultSeverity: 5, DefaultExposure: 3, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP057", NameDE: "Hydraulikoelleckage + Rutschgefahr", NameEN: "Hydraulic oil leakage + slip hazard",
@@ -204,7 +204,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Sturzverletzungen durch Ausrutschen, Hautreizungen bei Hautkontakt.",
AffectedDE: "Bedienpersonal, Logistikmitarbeiter, alle Personen im Pressenbereich.",
ZoneDE: "Bodenbereich rund um das Hydraulikaggregat und unter der Presse.",
DefaultSeverity: 2, DefaultExposure: 4,
DefaultSeverity: 2, DefaultExposure: 4, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
{
ID: "HP058", NameDE: "Ergonomische Belastung Kistenwechsel", NameEN: "Ergonomic strain during bin changeover",
@@ -219,7 +219,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Rueckenverletzungen, Bandscheibenvorfall, Muskel-Skelett-Erkrankungen.",
AffectedDE: "Bedienpersonal, Logistikmitarbeiter an der Presse.",
ZoneDE: "Auswurfbereich, Palettenstellplatz neben der Presse.",
DefaultSeverity: 2, DefaultExposure: 5,
DefaultSeverity: 2, DefaultExposure: 5, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"},
},
}
}
@@ -7,6 +7,10 @@ type KeywordEntry struct {
ComponentIDs []string // Matched component library IDs (C001-C135)
EnergyIDs []string // Matched energy source IDs (EN01-EN20)
ExtraTags []string // Additional tags derived from keyword context
// MachineTypes restricts this keyword to specific machine types.
// Empty = matches all machine types. If set, only matches when the
// project's machine_type is in this list.
MachineTypes []string // e.g. ["press", "hydraulic_press"]
}
// GetKeywordDictionary returns the complete keyword dictionary for
@@ -14,13 +18,13 @@ type KeywordEntry struct {
// machinery terminology in German and English.
func GetKeywordDictionary() []KeywordEntry {
return []KeywordEntry{
// ── Pressen / Umformmaschinen ───────────────────────────────────
{Keywords: []string{"presse", "press", "umform", "umformung"}, ComponentIDs: []string{"C008", "C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"high_force", "crush_point"}},
{Keywords: []string{"kniehebel", "toggle"}, ComponentIDs: []string{"C121"}, ExtraTags: []string{"mechanical_transmission"}},
{Keywords: []string{"stossel", "stoessel", "ram", "slide"}, ComponentIDs: []string{"C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"moving_part", "crush_point", "gravity_risk"}},
{Keywords: []string{"stempel", "punch", "matrize", "die"}, ComponentIDs: []string{"C126"}, ExtraTags: []string{"crush_point", "cutting_part"}},
{Keywords: []string{"schwungrad", "flywheel"}, ComponentIDs: []string{"C133"}, EnergyIDs: []string{"EN02", "EN03"}, ExtraTags: []string{"stored_energy", "rotating_part"}},
{Keywords: []string{"werkzeugeinbauraum", "die space"}, ComponentIDs: []string{"C132"}, ExtraTags: []string{"crush_point", "pinch_point"}},
// ── Pressen / Umformmaschinen (NUR fuer press/hydraulic_press) ──
{Keywords: []string{"presse", "press", "umform", "umformung"}, ComponentIDs: []string{"C008", "C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"high_force", "crush_point"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press", "stamping_press"}},
{Keywords: []string{"kniehebel", "toggle"}, ComponentIDs: []string{"C121"}, ExtraTags: []string{"mechanical_transmission"}, MachineTypes: []string{"press"}},
{Keywords: []string{"stossel", "stoessel", "ram", "slide"}, ComponentIDs: []string{"C122"}, EnergyIDs: []string{"EN01"}, ExtraTags: []string{"moving_part", "crush_point", "gravity_risk"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}},
{Keywords: []string{"stempel", "punch", "matrize", "die"}, ComponentIDs: []string{"C126"}, ExtraTags: []string{"crush_point", "cutting_part"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press", "stamping_press"}},
{Keywords: []string{"schwungrad", "flywheel"}, ComponentIDs: []string{"C133"}, EnergyIDs: []string{"EN02", "EN03"}, ExtraTags: []string{"stored_energy", "rotating_part"}, MachineTypes: []string{"press", "mechanical_press"}},
{Keywords: []string{"werkzeugeinbauraum", "die space"}, ComponentIDs: []string{"C132"}, ExtraTags: []string{"crush_point", "pinch_point"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}},
// ── Foerdertechnik ──────────────────────────────────────────────
{Keywords: []string{"foerderband", "transportband", "conveyor"}, ComponentIDs: []string{"C003"}, EnergyIDs: []string{"EN01", "EN02"}, ExtraTags: []string{"entanglement_risk"}},
@@ -94,7 +94,9 @@ var roleKeywords = map[string]string{
// ParseNarrative extracts components, energy sources, lifecycle phases,
// roles, and tags from a machine description text. Fully deterministic,
// no LLM required.
func ParseNarrative(text string) ParseResult {
// machineType is optional — if provided, keywords with MachineTypes
// restrictions are only matched when the machine type is in the list.
func ParseNarrative(text string, machineType ...string) ParseResult {
result := ParseResult{}
if text == "" {
return result
@@ -122,7 +124,27 @@ func ParseNarrative(text string) ParseResult {
seenEnergy := make(map[string]bool)
tagSet := make(map[string]bool)
// Resolve machine type for filtering
var mType string
if len(machineType) > 0 {
mType = machineType[0]
}
for _, entry := range dictionary {
// Skip keywords restricted to other machine types
if len(entry.MachineTypes) > 0 && mType != "" {
matched := false
for _, mt := range entry.MachineTypes {
if mt == mType {
matched = true
break
}
}
if !matched {
continue // This keyword is for a different machine type
}
}
for _, kw := range entry.Keywords {
kwNorm := strings.ToLower(kw)
kwNorm = strings.ReplaceAll(kwNorm, "ä", "ae")