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,68 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The cross-domain term list lives in gt_benchmark_harness_test.go
|
||||
// (foreignDomainTerms, term → home domain). This precision guard reuses it and,
|
||||
// unlike the diagnostic TestGT_DomainLeakage, runs the FULL production gating
|
||||
// path including the relevance backstop, then ASSERTS zero leaks. It catches
|
||||
// machine-type wiring regressions and weak-tag (structural_part) leaks in CI.
|
||||
|
||||
// firedHazardsForCase runs the exact production gating path (parse → engine match
|
||||
// → relevance backstop) for one GT case and returns the surviving patterns.
|
||||
func firedHazardsForCase(c gtCase) []PatternMatch {
|
||||
narrative := c.narrativeOverride
|
||||
pr := ParseNarrative(narrative, c.machineType)
|
||||
input := parseResultToMatchInput(pr, c.machineType)
|
||||
|
||||
compNames := make([]string, 0, len(pr.Components))
|
||||
for _, comp := range pr.Components {
|
||||
if comp.Negated {
|
||||
continue
|
||||
}
|
||||
compNames = append(compNames, NormalizeDEPublic(comp.NameDE))
|
||||
}
|
||||
|
||||
out := NewPatternEngine().Match(input)
|
||||
fired := make([]PatternMatch, 0, len(out.MatchedPatterns))
|
||||
for _, mp := range out.MatchedPatterns {
|
||||
if IsPatternRelevant(mp, narrative, compNames) {
|
||||
fired = append(fired, mp)
|
||||
}
|
||||
}
|
||||
return fired
|
||||
}
|
||||
|
||||
// TestCrossDomainPrecision asserts that no fired pattern is foreign to the GT
|
||||
// machine — neither machine-type-incompatible nor matching a foreign-domain term.
|
||||
func TestCrossDomainPrecision(t *testing.T) {
|
||||
for _, c := range gtBenchmarkCases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
fired := firedHazardsForCase(c)
|
||||
t.Logf("%s (%s): %d patterns fired", c.name, c.machineType, len(fired))
|
||||
|
||||
var domainLeaks []string
|
||||
for _, mp := range fired {
|
||||
text := normalizeDE(mp.PatternName + " " + mp.ZoneDE + " " + mp.ScenarioDE)
|
||||
for term, domain := range foreignDomainTerms {
|
||||
if strings.Contains(text, term) {
|
||||
domainLeaks = append(domainLeaks, domain+"/"+term+" → "+mp.PatternName)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(domainLeaks)
|
||||
for _, l := range domainLeaks {
|
||||
t.Logf(" FOREIGN-DOMAIN: %s", l)
|
||||
}
|
||||
if len(domainLeaks) > 0 {
|
||||
t.Errorf("%s: %d cross-domain leak(s) — patterns from foreign machine classes fired", c.name, len(domainLeaks))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user