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:
@@ -13,6 +13,10 @@ type ComponentMatch struct {
|
||||
MatchedOn string `json:"matched_on"` // The keyword that triggered the match
|
||||
Tags []string `json:"tags"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
// Negated is the best-effort verdict that the keyword appeared only in a
|
||||
// negated clause ("keine Pneumatik"). Negated components are surfaced for
|
||||
// expert review but do NOT contribute tags/energy to pattern matching.
|
||||
Negated bool `json:"negated"`
|
||||
}
|
||||
|
||||
// EnergyMatch represents an energy source detected from narrative text.
|
||||
@@ -189,7 +193,14 @@ func ParseNarrative(text string, machineType ...string) ParseResult {
|
||||
kwNorm = strings.ReplaceAll(kwNorm, "ß", "ss")
|
||||
|
||||
if strings.Contains(lower, kwNorm) {
|
||||
// Add components
|
||||
// Best-effort negation verdict: the keyword is present, but if
|
||||
// every occurrence sits in a negated clause ("keine Pneumatik")
|
||||
// the component is surfaced as negated and contributes NO tags /
|
||||
// energy to matching (so it generates no phantom hazards). The
|
||||
// expert can flip the verdict in the Components view.
|
||||
negated := !hasUnnegatedOccurrence(lower, kwNorm)
|
||||
|
||||
// Add components (negated ones carry the flag, no tags)
|
||||
for _, cid := range entry.ComponentIDs {
|
||||
if !seenComponents[cid] {
|
||||
seenComponents[cid] = true
|
||||
@@ -200,27 +211,31 @@ func ParseNarrative(text string, machineType ...string) ParseResult {
|
||||
MatchedOn: kw,
|
||||
Tags: comp.Tags,
|
||||
Confidence: 0.8,
|
||||
Negated: negated,
|
||||
})
|
||||
// Add component tags
|
||||
for _, t := range comp.Tags {
|
||||
tagSet[t] = true
|
||||
if !negated {
|
||||
for _, t := range comp.Tags {
|
||||
tagSet[t] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add energy sources
|
||||
for _, eid := range entry.EnergyIDs {
|
||||
if !seenEnergy[eid] {
|
||||
seenEnergy[eid] = true
|
||||
result.EnergySources = append(result.EnergySources, EnergyMatch{
|
||||
SourceID: eid,
|
||||
NameDE: eid, // Will be enriched by caller
|
||||
MatchedOn: kw,
|
||||
})
|
||||
if !negated {
|
||||
// Add energy sources
|
||||
for _, eid := range entry.EnergyIDs {
|
||||
if !seenEnergy[eid] {
|
||||
seenEnergy[eid] = true
|
||||
result.EnergySources = append(result.EnergySources, EnergyMatch{
|
||||
SourceID: eid,
|
||||
NameDE: eid, // Will be enriched by caller
|
||||
MatchedOn: kw,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Add extra tags
|
||||
for _, t := range entry.ExtraTags {
|
||||
tagSet[t] = true
|
||||
}
|
||||
}
|
||||
// Add extra tags
|
||||
for _, t := range entry.ExtraTags {
|
||||
tagSet[t] = true
|
||||
}
|
||||
break // First keyword match is enough per entry
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user