feat(iace): Sprint 3A — Operational State Graph + fix(ucca) flaky keyword sort

State Graph:
- 9 Standard-Betriebszustaende (startup, homing, automatic_operation, manual_operation, teach_mode, maintenance, cleaning, emergency_stop, recovery_mode)
- 20 State-Transitions als gerichteter Graph
- OperationalStates + StateTransitions Felder in HazardPattern, MatchInput, PatternMatch
- patternMatches() filtert Patterns nach Betriebszustand (nil = feuert immer)
- Narrative-Parser extrahiert States aus Maschinenbeschreibung (22 Keywords + 4 Transition-Keywords)
- 27 bestehende Patterns mit State-Einschraenkungen annotiert (10 operational, 15 maintenance, 2 cobot)
- MatchReason um operational_state + state_transition Typen erweitert (Explainability)
- 6 neue Tests: NilFiresAlways, MaintenanceFilter, StateTransition, MatchReasons, Count, TransitionValid

UCCA fix:
- Stabiler Tiebreaker (Pattern-ID aufsteigend) bei gleichem Keyword-Score in MatchByKeywords
- Behebt flaky TestControlPatternIndex_MatchByKeywords (1/10 Failure-Rate durch Go map iteration order)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-10 08:05:02 +02:00
parent 33f0a64ff6
commit 77a497d930
10 changed files with 449 additions and 48 deletions
@@ -33,13 +33,15 @@ type TechSpec struct {
// ParseResult contains all entities extracted from a machine narrative.
type ParseResult struct {
Components []ComponentMatch `json:"components"`
EnergySources []EnergyMatch `json:"energy_sources"`
LifecyclePhases []string `json:"lifecycle_phases"`
Roles []string `json:"roles"`
CustomTags []string `json:"custom_tags"`
TechSpecs []TechSpec `json:"tech_specs"`
Confidence float64 `json:"confidence"`
Components []ComponentMatch `json:"components"`
EnergySources []EnergyMatch `json:"energy_sources"`
LifecyclePhases []string `json:"lifecycle_phases"`
Roles []string `json:"roles"`
CustomTags []string `json:"custom_tags"`
TechSpecs []TechSpec `json:"tech_specs"`
Confidence float64 `json:"confidence"`
OperationalStates []string `json:"operational_states,omitempty"`
StateTransitions []string `json:"state_transitions,omitempty"`
}
// techSpecPattern matches numeric values with engineering units.
@@ -91,6 +93,40 @@ var roleKeywords = map[string]string{
"leiharbeiter": "temp_worker",
}
// operationalStateKeywords maps German text patterns to operational state IDs.
var operationalStateKeywords = map[string]string{
"hochfahren": "startup",
"anlauf": "startup",
"anfahren": "startup",
"referenzfahrt": "homing",
"referenzpunkt": "homing",
"automatikbetrieb": "automatic_operation",
"automatisch": "automatic_operation",
"handbetrieb": "manual_operation",
"manuell": "manual_operation",
"tippbetrieb": "manual_operation",
"teach": "teach_mode",
"einrichtbetrieb": "teach_mode",
"programmier": "teach_mode",
"wartung": "maintenance",
"instandhaltung": "maintenance",
"reinigung": "cleaning",
"not-halt": "emergency_stop",
"nothalt": "emergency_stop",
"notabschaltung": "emergency_stop",
"wiederanlauf": "recovery_mode",
"wiederinbetriebnahme":"recovery_mode",
"quittier": "recovery_mode",
}
// stateTransitionKeywords maps keyword combinations to state transitions.
var stateTransitionKeywords = map[string]string{
"unerwarteter wiederanlauf": "maintenance→automatic_operation",
"wiederanlauf nach not": "emergency_stop→recovery_mode",
"automatischer anlauf": "startup→automatic_operation",
"betriebsartwechsel": "manual_operation→automatic_operation",
}
// ParseNarrative extracts components, energy sources, lifecycle phases,
// roles, and tags from a machine description text. Fully deterministic,
// no LLM required.
@@ -221,12 +257,37 @@ func ParseNarrative(text string, machineType ...string) ParseResult {
}
}
// 6. Collect all tags
// 6. Extract operational states
stateSet := make(map[string]bool)
for kw, state := range operationalStateKeywords {
kwNorm := strings.ReplaceAll(kw, "ä", "ae")
kwNorm = strings.ReplaceAll(kwNorm, "ö", "oe")
kwNorm = strings.ReplaceAll(kwNorm, "ü", "ue")
if strings.Contains(lower, kwNorm) {
if !stateSet[state] {
stateSet[state] = true
result.OperationalStates = append(result.OperationalStates, state)
}
}
}
// 7. Extract state transitions
transSet := make(map[string]bool)
for kw, trans := range stateTransitionKeywords {
if strings.Contains(lower, kw) {
if !transSet[trans] {
transSet[trans] = true
result.StateTransitions = append(result.StateTransitions, trans)
}
}
}
// 8. Collect all tags
for t := range tagSet {
result.CustomTags = append(result.CustomTags, t)
}
// 7. Calculate overall confidence
// 9. Calculate overall confidence
if len(result.Components) > 0 {
result.Confidence = float64(len(result.Components)) / 15.0 // Normalize to ~1.0 for 15 components
if result.Confidence > 1.0 {