feat(iace): cross-domain precision overhaul + component review + schema reconcile

Engine precision (stop foreign-machine patterns leaking into a project):
- Wire project.MachineType into the engine machine-type gate (empty input no
  longer fires every machine class — press/cnc/excavator/crane/medical...).
- Capability-domain gating extended by 7 domains (outdoor, ventilation,
  machining, bulk, palletizer, playground, fitness) so domain-specific hazards
  only fire when the narrative names that domain; emitted via keyword_dictionary.
- Relevance backstop moved into iace (single gating contract, testable), and its
  dominant false-anchor class removed (a long pattern word no longer matches a
  short common token; prepositions/leitung added to the generic stoplist).
- New guard tests: TestCrossDomainPrecision (full pipeline, 0 foreign per GT) and
  TestPatternReachability now asserts 0 dead patterns. Both GTs keep coverage 1.0.

Reachability fix: the 51 dead patterns required electrical/pneumatic/hydraulic
tags nothing produced — renamed to the canonical electrical_energy/
pneumatic_pressure/hydraulic_pressure/hydraulic_part.

Component review (negation is best-effort + expert-correctable):
- Parser surfaces negated components (ComponentMatch.Negated) instead of dropping
  them; negated contribute no tags/energy → no phantom hazards.
- presence_status (vorhanden|nicht_vorhanden|geloescht) + ce_marked on components;
  only `vorhanden` feed matching. CE+safety-relevant flags the PL/SIL obligation.
- Force re-seed preserves the expert's component decisions instead of wiping them.
- Tag-based component→hazard assignment (was: all on the first component).
- Negation-aware narrative parsing ("keine Pneumatik" no longer extracts it).

Local-dev DB: ai-sdk sets search_path=compliance,core,public; reconcile migrations
152-156 bring the consolidated local iace tables to the current schema + add the
presence_status/ce_marked columns. Machine-type vocabulary endpoint for the form.

[migration-approved]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-10 17:15:55 +02:00
parent 3bd4e0aaaf
commit afb3f83f30
47 changed files with 1275 additions and 169 deletions
@@ -0,0 +1,45 @@
package iace
import "testing"
func TestIsPatternRelevant(t *testing.T) {
// A chain-driven crate lift: no press, no outdoor work, no palletizer.
narrative := "Kistenhubgeraet hebt Behaelter ueber Kette. Elektromotor und SPS-Steuerung. " +
"Hubwerk mit Plattform und Not-Halt-Taster."
comps := []string{"Hubwerk", "Kettenantrieb", "Elektromotor (Drehstrom)", "SPS", "Plattform/Buehne"}
tests := []struct {
name string
mp PatternMatch
want bool
}{
{"foreign: tick bite", PatternMatch{
PatternName: "Zeckenbiss bei Ausseneinsatz", ScenarioDE: "Zeckenstich bei Arbeiten im Gruenen",
ZoneDE: "Freigelände, Wald, Wiese"}, false},
{"foreign: climbing structure", PatternMatch{
PatternName: "Absturz von Klettergeraet", ScenarioDE: "Kind stuerzt von Klettergeraet auf den Boden",
ZoneDE: "Klettergeraet, Fallzone darunter"}, false},
{"foreign: palletizer", PatternMatch{
PatternName: "Palettierer — mechanisch", ScenarioDE: "Palettierer bewegt schwere Gebinde und quetscht Personen",
ZoneDE: "Palettierer-Arbeitsraum, Palettenwechselzone"}, false},
{"foreign: ventilation mold", PatternMatch{
PatternName: "Schimmelpilz in Lueftungsanlage", ScenarioDE: "Staub mit Nagerkot in selten gereinigten Raeumen",
ZoneDE: "Lueftungskanal, Filterbereich"}, false},
{"generic: crush by moving parts", PatternMatch{
PatternName: "Quetschgefahr durch bewegte Teile", ScenarioDE: "Quetschen zwischen beweglichen und feststehenden Teilen",
ZoneDE: "Bewegungsbereich"}, true},
{"generic: electric shock", PatternMatch{
PatternName: "Direktes Beruehren", ScenarioDE: "Stromschlag bei Beruehrung spannungsfuehrender Teile",
ZoneDE: "Schaltschrank"}, true},
{"machine-word match: chain drive", PatternMatch{
PatternName: "Erfassen durch Kettenantrieb", ScenarioDE: "Einzug an offener Kette",
ZoneDE: "Kettenbereich"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsPatternRelevant(tt.mp, narrative, comps); got != tt.want {
t.Errorf("IsPatternRelevant(%q)=%v, want %v", tt.mp.PatternName, got, tt.want)
}
})
}
}