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,103 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// producibleTagUniverse returns every component/energy tag that some machine
|
||||
// could ever carry: tags from the component library, the energy library, the
|
||||
// keyword dictionary's ExtraTags, and the domain-gate tags. A pattern whose
|
||||
// RequiredComponentTags/RequiredEnergyTags include a tag outside this set can
|
||||
// never match — no machine can produce that tag.
|
||||
func producibleTagUniverse() map[string]bool {
|
||||
u := make(map[string]bool)
|
||||
for _, c := range GetComponentLibrary() {
|
||||
for _, t := range c.Tags {
|
||||
u[t] = true
|
||||
}
|
||||
}
|
||||
for _, e := range GetEnergySources() {
|
||||
for _, t := range e.Tags {
|
||||
u[t] = true
|
||||
}
|
||||
}
|
||||
for _, k := range GetKeywordDictionary() {
|
||||
for _, t := range k.ExtraTags {
|
||||
u[t] = true
|
||||
}
|
||||
}
|
||||
// Domain-gate tags are produced from narrative domain terms.
|
||||
for _, t := range []string{
|
||||
"dom_agri", "dom_cnc", "dom_escalator", "dom_glass", "dom_grinding",
|
||||
"dom_plastics", "dom_press", "dom_rolling", "dom_solar", "dom_textile",
|
||||
"dom_welding", "dom_wind",
|
||||
} {
|
||||
u[t] = true
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// TestPatternReachability reports patterns that can never fire because they
|
||||
// require a component/energy tag that nothing in the libraries produces. Every
|
||||
// pattern should be usable in SOME CE risk assessment. Currently informational
|
||||
// (t.Log) so we can review the list before deciding to prune.
|
||||
func TestPatternReachability(t *testing.T) {
|
||||
universe := producibleTagUniverse()
|
||||
patterns := AllPatterns()
|
||||
|
||||
type dead struct {
|
||||
id, name string
|
||||
missing []string
|
||||
}
|
||||
var deads []dead
|
||||
missingTagCount := make(map[string]int)
|
||||
|
||||
for _, p := range patterns {
|
||||
var missing []string
|
||||
for _, tag := range append(append([]string{}, p.RequiredComponentTags...), p.RequiredEnergyTags...) {
|
||||
if !universe[tag] {
|
||||
missing = append(missing, tag)
|
||||
missingTagCount[tag]++
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
deads = append(deads, dead{p.ID, p.NameDE, missing})
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Patterns gesamt: %d", len(patterns))
|
||||
t.Logf("Unerreichbare (tote) Patterns: %d", len(deads))
|
||||
|
||||
// Most common unsatisfiable tags first — these point at the systemic gaps.
|
||||
type kv struct {
|
||||
tag string
|
||||
n int
|
||||
}
|
||||
var ranked []kv
|
||||
for tag, n := range missingTagCount {
|
||||
ranked = append(ranked, kv{tag, n})
|
||||
}
|
||||
sort.Slice(ranked, func(i, j int) bool { return ranked[i].n > ranked[j].n })
|
||||
t.Log("--- Unerfuellbare Required-Tags (Haeufigkeit) ---")
|
||||
for _, r := range ranked {
|
||||
t.Logf(" %3d %s", r.n, r.tag)
|
||||
}
|
||||
|
||||
t.Log("--- Tote Patterns (erste 60) ---")
|
||||
for i, d := range deads {
|
||||
if i >= 60 {
|
||||
break
|
||||
}
|
||||
t.Logf(" %s %q fehlend: %s", d.id, d.name, strings.Join(d.missing, ","))
|
||||
}
|
||||
|
||||
// Guard: every pattern must be reachable by some CE risk assessment. A
|
||||
// pattern requiring a tag no component/energy/keyword can ever produce is
|
||||
// dead weight (and often a tag-naming typo). Keep this at zero.
|
||||
if len(deads) > 0 {
|
||||
t.Errorf("%d unreachable pattern(s) — required tags that nothing produces: %v",
|
||||
len(deads), missingTagCount)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user