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:
@@ -0,0 +1,61 @@
|
||||
package iace
|
||||
|
||||
import "testing"
|
||||
|
||||
// Helper-level: operates on already-normalised (lowercase, umlaut-folded) text.
|
||||
func TestHasUnnegatedOccurrence(t *testing.T) {
|
||||
neg := "keine pneumatischen oder hydraulischen schnittstellen. hubantrieb ueber kette (kettenspannung zyklisch zu pruefen)."
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
kw string
|
||||
want bool
|
||||
}{
|
||||
{"negated hydraulik", neg, "hydraulisch", false},
|
||||
{"negated pneumatik", neg, "pneumatisch", false},
|
||||
{"positive kette after period", neg, "kette", true},
|
||||
{"ohne negates", "ohne hydraulik vorhanden", "hydraulik", false},
|
||||
{"mit ends scope", "ohne hydraulik, mit pneumatikzylinder", "pneumatik", true},
|
||||
{"plain positive", "die maschine hat eine hydraulikpumpe", "hydraulik", true},
|
||||
{"weder/contrast", "weder pneumatik noch hydraulik verbaut", "pneumatik", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := hasUnnegatedOccurrence(tt.text, tt.kw); got != tt.want {
|
||||
t.Errorf("hasUnnegatedOccurrence(%q,%q)=%v, want %v", tt.text, tt.kw, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Integration: the Kistenhub interface sentence SURFACES the hydraulic (C041)
|
||||
// and pneumatic (C051) components flagged as negated (for expert review), keeps
|
||||
// the chain drive (C010) as present, and crucially leaks NO pneumatic/hydraulic
|
||||
// tags into the matching set (so no phantom hazards).
|
||||
func TestParseNarrative_NegatedInterfacesFlagged(t *testing.T) {
|
||||
text := "Keine pneumatischen oder hydraulischen Schnittstellen. " +
|
||||
"Hubantrieb über Kette (Kettenspannung zyklisch zu prüfen)."
|
||||
res := ParseNarrative(text)
|
||||
status := func(id string) (found, negated bool) {
|
||||
for _, c := range res.Components {
|
||||
if c.LibraryID == id {
|
||||
return true, c.Negated
|
||||
}
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
if f, n := status("C041"); !f || !n {
|
||||
t.Errorf("C041 (Hydraulikpumpe) should be surfaced & negated; found=%v negated=%v", f, n)
|
||||
}
|
||||
if f, n := status("C051"); !f || !n {
|
||||
t.Errorf("C051 (Pneumatikzylinder) should be surfaced & negated; found=%v negated=%v", f, n)
|
||||
}
|
||||
if f, n := status("C010"); !f || n {
|
||||
t.Errorf("C010 (Kettenantrieb) should be present & not negated; found=%v negated=%v", f, n)
|
||||
}
|
||||
for _, tag := range res.CustomTags {
|
||||
if tag == "pneumatic_part" || tag == "hydraulic_part" {
|
||||
t.Errorf("negated component leaked tag %q into the matching set", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user