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/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// ParseNarrativeRequest is the request body for POST /projects/:id/parse-narrative. // ParseNarrativeRequest is the request body for POST /projects/:id/parse-narrative.
@@ -43,8 +44,16 @@ func (h *IACEHandler) ParseNarrative(c *gin.Context) {
return return
} }
// 1. Parse narrative text deterministically // Load project to get machine type for context-aware parsing
parseResult := iace.ParseNarrative(req.NarrativeText) 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 // 2. Feed parsed tags into pattern engine
// Collect all component IDs for tag resolution // Collect all component IDs for tag resolution
@@ -27,4 +27,9 @@ type HazardPattern struct {
ZoneDE string `json:"zone_de,omitempty"` // Gefahrstelle/Zone ZoneDE string `json:"zone_de,omitempty"` // Gefahrstelle/Zone
DefaultSeverity int `json:"default_severity,omitempty"` // 1-5 DefaultSeverity int `json:"default_severity,omitempty"` // 1-5
DefaultExposure int `json:"default_exposure,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.", HarmDE: "Toedliche Quetschverletzung, Amputation von Gliedmassen.",
AffectedDE: "Einrichter, Bedienpersonal im Werkzeugeinbauraum.", AffectedDE: "Einrichter, Bedienpersonal im Werkzeugeinbauraum.",
ZoneDE: "Werkzeugeinbauraum unterhalb des Stoessels.", 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", 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.", HarmDE: "Toedliche Quetschverletzung, Amputation der oberen Extremitaeten.",
AffectedDE: "Einrichter, Werkzeugbauer, Instandhaltungspersonal.", AffectedDE: "Einrichter, Werkzeugbauer, Instandhaltungspersonal.",
ZoneDE: "Werkzeugeinbauraum zwischen Ober- und Unterwerkzeug.", 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", ID: "HP047", NameDE: "Oelnebelexposition Atemwege", NameEN: "Oil mist inhalation exposure",
@@ -53,7 +53,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Atemwegsreizung, chronische Lungenerkrankung bei Langzeitexposition.", HarmDE: "Atemwegsreizung, chronische Lungenerkrankung bei Langzeitexposition.",
AffectedDE: "Bedienpersonal, Personen im Nahbereich der Presse.", AffectedDE: "Bedienpersonal, Personen im Nahbereich der Presse.",
ZoneDE: "Arbeitsbereich rund um die Presse, insbesondere Bedienerseite.", 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", 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.", HarmDE: "Verbrennungen zweiten oder dritten Grades an Haenden und Unterarmen.",
AffectedDE: "Bedienpersonal, Einrichter bei Werkzeugwechsel.", AffectedDE: "Bedienpersonal, Einrichter bei Werkzeugwechsel.",
ZoneDE: "Entnahmebereich, Werkzeugeinbauraum, Ablagetisch.", 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)", 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.", HarmDE: "Toedliche Verletzung durch herabfallende Last, Knochenbrueche.",
AffectedDE: "Personen im Gefahrenbereich unter der schwebenden Last.", AffectedDE: "Personen im Gefahrenbereich unter der schwebenden Last.",
ZoneDE: "Bereich unterhalb des Hubwerks, Werkzeugwechselzone.", 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", 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.", HarmDE: "Schnittverletzungen, Amputation von Fingern, Quetschungen.",
AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.", AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.",
ZoneDE: "Transferbereich zwischen den Pressenstationen.", 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", ID: "HP051", NameDE: "Sturzgefahr Auswurfbereich", NameEN: "Fall hazard at ejection area",
@@ -114,7 +114,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Knochenbrueche, Prellungen, Kopfverletzungen bei Sturz.", HarmDE: "Knochenbrueche, Prellungen, Kopfverletzungen bei Sturz.",
AffectedDE: "Bedienpersonal, Logistikmitarbeiter im Auswurfbereich.", AffectedDE: "Bedienpersonal, Logistikmitarbeiter im Auswurfbereich.",
ZoneDE: "Auswurfschacht und angrenzender Bodenbereich.", 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", 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.", HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.",
AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.", AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.",
ZoneDE: "Hydraulikaggregat, Speicherbereich, Leitungsfuehrung.", 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", ID: "HP053", NameDE: "Impulslaerm Pressvorgang", NameEN: "Impact noise during press operation",
@@ -144,7 +144,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Laermschwerhoerigkeit, Tinnitus bei Langzeitexposition.", HarmDE: "Laermschwerhoerigkeit, Tinnitus bei Langzeitexposition.",
AffectedDE: "Bedienpersonal, Personen in angrenzenden Arbeitsbereichen.", AffectedDE: "Bedienpersonal, Personen in angrenzenden Arbeitsbereichen.",
ZoneDE: "Gesamter Pressenbereich, Radius ca. 5-10 m um die Maschine.", 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", 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.", HarmDE: "Erfassen durch rotierende Teile, schwere Schnittverletzungen, Skalpierung.",
AffectedDE: "Instandhaltungspersonal, Einrichter nach Maschinenstopp.", AffectedDE: "Instandhaltungspersonal, Einrichter nach Maschinenstopp.",
ZoneDE: "Schwungradbereich, Kupplungsraum, Antriebsseite der Presse.", 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)", 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.", HarmDE: "Toedliche Quetsch- oder Scherverletzungen bei Eingriff in den Gefahrenbereich.",
AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.", AffectedDE: "Bedienpersonal, Einrichter bei Stoerungsbeseitigung.",
ZoneDE: "Gesamter Werkzeugeinbauraum hinter der Schutztuer.", 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", ID: "HP056", NameDE: "Fehlbedienung Zweihandschaltung", NameEN: "Two-hand control misoperation",
@@ -189,7 +189,7 @@ func GetPressHazardPatterns() []HazardPattern {
HarmDE: "Quetschverletzungen der freien Hand im Werkzeugbereich.", HarmDE: "Quetschverletzungen der freien Hand im Werkzeugbereich.",
AffectedDE: "Bedienpersonal an der Pressenbedienung.", AffectedDE: "Bedienpersonal an der Pressenbedienung.",
ZoneDE: "Gefahrenbereich zwischen Ober- und Unterwerkzeug.", 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", 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.", HarmDE: "Sturzverletzungen durch Ausrutschen, Hautreizungen bei Hautkontakt.",
AffectedDE: "Bedienpersonal, Logistikmitarbeiter, alle Personen im Pressenbereich.", AffectedDE: "Bedienpersonal, Logistikmitarbeiter, alle Personen im Pressenbereich.",
ZoneDE: "Bodenbereich rund um das Hydraulikaggregat und unter der Presse.", 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", 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.", HarmDE: "Rueckenverletzungen, Bandscheibenvorfall, Muskel-Skelett-Erkrankungen.",
AffectedDE: "Bedienpersonal, Logistikmitarbeiter an der Presse.", AffectedDE: "Bedienpersonal, Logistikmitarbeiter an der Presse.",
ZoneDE: "Auswurfbereich, Palettenstellplatz neben 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) ComponentIDs []string // Matched component library IDs (C001-C135)
EnergyIDs []string // Matched energy source IDs (EN01-EN20) EnergyIDs []string // Matched energy source IDs (EN01-EN20)
ExtraTags []string // Additional tags derived from keyword context 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 // GetKeywordDictionary returns the complete keyword dictionary for
@@ -14,13 +18,13 @@ type KeywordEntry struct {
// machinery terminology in German and English. // machinery terminology in German and English.
func GetKeywordDictionary() []KeywordEntry { func GetKeywordDictionary() []KeywordEntry {
return []KeywordEntry{ return []KeywordEntry{
// ── Pressen / Umformmaschinen ─────────────────────────────────── // ── 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"}}, {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"}}, {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"}}, {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"}}, {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"}}, {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"}}, {Keywords: []string{"werkzeugeinbauraum", "die space"}, ComponentIDs: []string{"C132"}, ExtraTags: []string{"crush_point", "pinch_point"}, MachineTypes: []string{"press", "hydraulic_press", "mechanical_press"}},
// ── Foerdertechnik ────────────────────────────────────────────── // ── Foerdertechnik ──────────────────────────────────────────────
{Keywords: []string{"foerderband", "transportband", "conveyor"}, ComponentIDs: []string{"C003"}, EnergyIDs: []string{"EN01", "EN02"}, ExtraTags: []string{"entanglement_risk"}}, {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, // ParseNarrative extracts components, energy sources, lifecycle phases,
// roles, and tags from a machine description text. Fully deterministic, // roles, and tags from a machine description text. Fully deterministic,
// no LLM required. // 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{} result := ParseResult{}
if text == "" { if text == "" {
return result return result
@@ -122,7 +124,27 @@ func ParseNarrative(text string) ParseResult {
seenEnergy := make(map[string]bool) seenEnergy := make(map[string]bool)
tagSet := 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 { 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 { for _, kw := range entry.Keywords {
kwNorm := strings.ToLower(kw) kwNorm := strings.ToLower(kw)
kwNorm = strings.ReplaceAll(kwNorm, "ä", "ae") kwNorm = strings.ReplaceAll(kwNorm, "ä", "ae")