feat(iace): read ALL limits-form fields + always include universal lifecycles
CI / detect-changes (push) Successful in 5s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 5s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Failing after 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-go (push) Failing after 37s
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / iace-gt-coverage (push) Successful in 23s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 5s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 5s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Failing after 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-go (push) Failing after 37s
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / iace-gt-coverage (push) Successful in 23s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
(1) extractNarrativeFromMetadata now reads every limits-form field generically (no whitelist) — intended use, foreseeable misuse, all machine limits and all four interface groups (electrical/mechanical/pneumatic/software). Field-schema drift no longer silently drops hazard sources. (2) withUniversalLifecycles always adds normal_operation/setup/maintenance/ cleaning to the matched lifecycle phases — these occur on virtually every machine and the professional assesses them, so their hazards must be derived even when the form omits them. Kistenhubgeraet recall jumped 42.9% -> 74.3% (electrical 9% -> 82%) from the field-name fix alone; this broadens it further. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -123,7 +123,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
matchOutput := engine.Match(iace.MatchInput{
|
matchOutput := engine.Match(iace.MatchInput{
|
||||||
ComponentLibraryIDs: componentIDs,
|
ComponentLibraryIDs: componentIDs,
|
||||||
EnergySourceIDs: energyIDs,
|
EnergySourceIDs: energyIDs,
|
||||||
LifecyclePhases: parseResult.LifecyclePhases,
|
LifecyclePhases: withUniversalLifecycles(parseResult.LifecyclePhases),
|
||||||
CustomTags: parseResult.CustomTags,
|
CustomTags: parseResult.CustomTags,
|
||||||
OperationalStates: operationalStates,
|
OperationalStates: operationalStates,
|
||||||
StateTransitions: parseResult.StateTransitions,
|
StateTransitions: parseResult.StateTransitions,
|
||||||
|
|||||||
@@ -2,12 +2,35 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// withUniversalLifecycles ensures the lifecycle phases that occur on virtually
|
||||||
|
// every machine — normal operation, setup, maintenance, cleaning — are always
|
||||||
|
// present, so their hazards are derived even when the limits form does not list
|
||||||
|
// them explicitly. The professional assesses these phases on most devices.
|
||||||
|
func withUniversalLifecycles(parsed []string) []string {
|
||||||
|
seen := make(map[string]bool, len(parsed)+4)
|
||||||
|
out := make([]string, 0, len(parsed)+4)
|
||||||
|
for _, p := range parsed {
|
||||||
|
if p != "" && !seen[p] {
|
||||||
|
seen[p] = true
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, u := range []string{"normal_operation", "setup", "maintenance", "cleaning"} {
|
||||||
|
if !seen[u] {
|
||||||
|
seen[u] = true
|
||||||
|
out = append(out, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
||||||
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
@@ -26,48 +49,34 @@ func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every limits-form field that carries machine-describing text. Field
|
// Read EVERY field of the limits form — intended use, foreseeable misuse,
|
||||||
// names MUST match the real form schema — the per-interface fields
|
// machine limits, and ALL interfaces (electrical/mechanical/pneumatic/
|
||||||
// (electrical/mechanical/pneumatic/software) each contribute hazards, not
|
// software). Each is a hazard source. We don't whitelist field names (the
|
||||||
// just the prose. Legacy names are kept so older projects still parse.
|
// form schema evolves); noise fields like serial number / year are harmless
|
||||||
textFields := []string{
|
// because the parser only extracts from recognised keywords. Keys are
|
||||||
// description / purpose
|
// sorted for deterministic output.
|
||||||
"general_description", "machine_designation", "intended_purpose",
|
keys := make([]string, 0, len(limits))
|
||||||
"foreseeable_misuse", "foreseeable_misuses", "variants", "area_of_use",
|
for k := range limits {
|
||||||
// limits
|
keys = append(keys, k)
|
||||||
"space_limits", "spatial_limits", "time_limits", "temporal_limits",
|
|
||||||
"environmental_conditions", "operating_conditions",
|
|
||||||
// energy + materials
|
|
||||||
"energy_sources", "energy_supply", "materials_processed",
|
|
||||||
// interfaces (the previously-ignored ones)
|
|
||||||
"interfaces_description", "control_system_description", "safety_functions_description",
|
|
||||||
"electrical_interfaces", "mechanical_interfaces",
|
|
||||||
"pneumatic_hydraulic_interfaces", "software_interfaces",
|
|
||||||
// people / maintenance
|
|
||||||
"maintenance_requirements", "personnel_requirements", "qualification_requirements",
|
|
||||||
}
|
}
|
||||||
arrayFields := []string{"operating_modes", "person_groups", "industry_sectors"}
|
sort.Strings(keys)
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, field := range textFields {
|
for _, k := range keys {
|
||||||
if v, ok := limits[field]; ok {
|
switch val := limits[k].(type) {
|
||||||
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
|
case string:
|
||||||
sb.WriteString(s)
|
if strings.TrimSpace(val) != "" {
|
||||||
|
sb.WriteString(val)
|
||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
}
|
case []interface{}:
|
||||||
}
|
for _, e := range val {
|
||||||
for _, field := range arrayFields {
|
if s, ok := e.(string); ok && s != "" {
|
||||||
if v, ok := limits[field]; ok {
|
sb.WriteString(s)
|
||||||
if arr, ok := v.([]interface{}); ok {
|
sb.WriteString(", ")
|
||||||
for _, e := range arr {
|
|
||||||
if s, ok := e.(string); ok && s != "" {
|
|
||||||
sb.WriteString(s)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sb.WriteString("\n\n")
|
|
||||||
}
|
}
|
||||||
|
sb.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
|
|||||||
Reference in New Issue
Block a user