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,86 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// componentReviewDecision captures an expert's manual review of an auto-detected
|
||||
// component (presence move, CE marking, safety relevance) so a force re-seed can
|
||||
// restore it instead of wiping it. Keyed by normalised component name — the
|
||||
// deterministic narrative parser re-derives the same names.
|
||||
type componentReviewDecision struct {
|
||||
presence string
|
||||
ceMarked bool
|
||||
safetyRelevant bool
|
||||
}
|
||||
|
||||
// resolvePresence returns the presence_status for a freshly parsed component:
|
||||
// the expert's prior decision wins; otherwise the engine's negation verdict
|
||||
// (negated → nicht_vorhanden, else vorhanden).
|
||||
func resolvePresence(dec componentReviewDecision, hasDecision, negated bool) string {
|
||||
if hasDecision && dec.presence != "" {
|
||||
return dec.presence
|
||||
}
|
||||
if negated {
|
||||
return iace.PresenceAbsent
|
||||
}
|
||||
return iace.PresencePresent
|
||||
}
|
||||
|
||||
// pickComponentForPattern links a matched hazard pattern to the project
|
||||
// component that most plausibly causes it.
|
||||
//
|
||||
// matchedTags are the component/energy tags that actually made the pattern fire
|
||||
// (RequiredComponentTags + RequiredEnergyTags satisfied by the machine). The
|
||||
// project component whose library tags overlap them best is treated as the
|
||||
// cause. Falls back to zone-name overlap, then to the default (first) component.
|
||||
//
|
||||
// Without this, every hazard defaulted to the first component, so the knowledge
|
||||
// graph drew all "erzeugt" edges from a single node.
|
||||
func pickComponentForPattern(
|
||||
matchedTags []string,
|
||||
zoneDE string,
|
||||
parseComps []iace.ComponentMatch,
|
||||
compByName map[string]uuid.UUID,
|
||||
defaultCompID uuid.UUID,
|
||||
) uuid.UUID {
|
||||
if len(matchedTags) > 0 {
|
||||
want := make(map[string]bool, len(matchedTags))
|
||||
for _, t := range matchedTags {
|
||||
want[t] = true
|
||||
}
|
||||
bestScore := 0
|
||||
bestID := uuid.Nil
|
||||
for _, c := range parseComps {
|
||||
id, ok := compByName[iace.NormalizeDEPublic(c.NameDE)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
score := 0
|
||||
for _, t := range c.Tags {
|
||||
if want[t] {
|
||||
score++
|
||||
}
|
||||
}
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestID = id
|
||||
}
|
||||
}
|
||||
if bestScore > 0 {
|
||||
return bestID
|
||||
}
|
||||
}
|
||||
|
||||
if zoneDE != "" {
|
||||
zoneNorm := iace.NormalizeDEPublic(zoneDE)
|
||||
for cName, cID := range compByName {
|
||||
if containsSubstring(zoneNorm, cName) || containsSubstring(cName, zoneNorm) {
|
||||
return cID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultCompID
|
||||
}
|
||||
Reference in New Issue
Block a user