6a3e96d54c
Two-part nachhaltiger fix replacing the previous "fill to 5 mitigations no matter what" behavior that the GT-Bremse benchmark proved unfaithful (e.g. HP1625 "scharfe Kanten" returning M005 "Rotations- bewegung vermeiden" via category fallback; HP1651 "Wiederanlauf Roboter" returning M054 "Sichere thermische Auslegung" via mismatched pattern reference). PART A — Set-based category filter (handlers package): - acceptableMeasureCategories: replaces 1:1 patternCatToMeasureCat with a curated set per pattern category, so e.g. safety_function_failure now accepts software_control measures (watchdogs, plausibility checks) and emc_hazard accepts both electrical and software_control measures - isCategoryCompatible: gate every measure id against the accepted set before creating a mitigation; mismatches log MEASURE-SKIP - The old category fallback is REMOVED. A hazard whose pattern has no category-compatible measure is now created with zero mitigations and logged as COVERAGE-GAP — the operator must consult an expert. No more silent invention of generic defaults. PART B — 235 pattern author-error fixes across 26 files: - HP040-HP044 (AI): M101/M102/M103 (Auffangwanne/Absauganlage) -> M133 Anomalieerkennung + M214 Plausibilitaet + M213 Sensor-Redundanz + M044 Zweikanalige Steuerung + others - HP011-HP015, HP104-HP109, HP1085-HP1095, HP1281-HP1334 (electrical): M001-M005/M054/M061 placeholders -> M481/M482 Isolation + M511-M522 PE/Schutzleiter/RCD/Hauptschalter - HP110-HP1331 (material_environmental): M101-M103 -> M384-M395 Brandschutz/Laserschutz + M533/M408 SDB/PSA - HP800-HP858, HP1178-HP1264 (software/sensor/hmi): M101/M104 -> M105/M106/M107/M214 SPS/Watchdog/Plausibilitaet - HP026, HP611-HP1690 (ergonomic): M001/M082 -> M353-M360 + M530-M532 Hebehilfe/ergonomische Hoehe - HP201-HP1697 (mechanical): M054/M051 -> M002/M008/M061/M141 + M487/M488 Tueroeffnung-Stillsetzung/Wiederanlauf - Plus EMF/Strahlung/Brand/Lärm/Vibration/Kommunikation/Cyber Coverage shift (Pattern-Author-Fehler bei aktiviertem Set-Filter): start: 237 patterns with zero category-compatible measures after Stufe 1A: 5 (AI) after Stufe 1B: 20 (mechanical Bestand) after Stufe 1C: 35 (electrical Bestand) after Stufe 1D: 29 (material_environmental) after Stufe 1E: 29 (software/sensor/hmi) after Stufe 1F: 20 (ergonomic) after Stufe 1G: 80 (thermal/comm/radiation/fire/safety) final: 0 (28 extended.go/extended2.go duplicates fixed) New regression tests: - TestEveryPattern_HasCategoryCompatibleMeasure: every pattern in collectAllPatterns() must reference at least one category-compatible measure; gaps must be explicitly listed in AllowlistKnownGaps (currently empty). Fails CI for any new pattern that drifts. - TestAcceptableMeasureCategories: pins the set-mapping for the 7 most-bug-prone pattern categories. - TestIsCategoryCompatible_EmptyMeasureCat: protects legacy entries. A separate task #11 tracks 58 HP-ID duplicates between extended.go/extended2.go and cobot.go/press.go/operational.go — patterns are semantically different and TestGetBuiltinHazardPatterns_- UniqueIDs misses them because it only checks HP001-HP044. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
6.7 KiB
Go
162 lines
6.7 KiB
Go
package iace
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestGTBremse_PinnedHazardToMeasureMappings is a regression net for the IACE
|
|
// benchmark fix. Each pinned (GT-Nr, hazard pattern, measure) triple was
|
|
// validated by an expert review on 2026-05 against testdata/ground_truth_bremse.json.
|
|
// If any pattern stops referencing the listed measures, this test fails — so the
|
|
// underlying GT scenario is no longer answered with the Fachmann-grade mitigation.
|
|
//
|
|
// Adding new entries here pins the Engine's answer for a specific GT scenario.
|
|
// Removing entries means the GT scenario is no longer covered with the same
|
|
// concrete measure (e.g. because the library was reorganized) — that needs an
|
|
// active decision, not a silent drift.
|
|
func TestGTBremse_PinnedHazardToMeasureMappings(t *testing.T) {
|
|
cases := []struct {
|
|
gtNr string
|
|
patternID string
|
|
requiredMeasures []string
|
|
}{
|
|
// GT 2.1/2.2: Elektrischer Schlag durch direktes Beruehren
|
|
// Expert demand: konkrete Isolation MOhm + IP2X Einhausung
|
|
{"2.1/2.2", "HP1640", []string{"M481", "M482"}},
|
|
// GT 2.4: Schutzleiterfehler (>10 mA Ableitstroeme)
|
|
// Expert demand: mech. Schutz + 10mm²-Cu + Ueberwachung + durchgehende Verbindung
|
|
{"2.4", "HP1641", []string{"M511", "M512", "M514", "M515"}},
|
|
// GT 2.5: Indirektes Beruehren — Schutzleiter durchgaengig + SK II / Kleinspannung
|
|
{"2.5", "HP1685", []string{"M511", "M512", "M515", "M516"}},
|
|
// GT 2.7: RCD an Steckdosenkreisen
|
|
{"2.7", "HP1689", []string{"M518"}},
|
|
// GT 2.12: Potentialausgleich zwischen Anlagenteilen
|
|
{"2.12", "HP1688", []string{"M475", "M477"}},
|
|
// GT 1.3: Pneumatik-Komponenten + Schlauchsicherung
|
|
{"1.3", "HP1630", []string{"M483", "M484", "M485"}},
|
|
// GT 1.5: Pneumatik-Restenergie nach Abschaltung
|
|
{"1.5", "HP1717", []string{"M485", "M534"}},
|
|
// GT 1.7: Teach-Modus mit Schluesselschalter + 250 mm/s + Zustimmtaster
|
|
{"1.7", "HP1605", []string{"M491", "M492", "M493"}},
|
|
// GT 1.8: Sicher begrenzter Bewegungsbereich + Zaun-Lastbemessung
|
|
{"1.8", "HP1604", []string{"M494", "M501"}},
|
|
// GT 1.10/1.18: Reach-over Sicherheitsabstand
|
|
{"1.10/1.18", "HP1602", []string{"M495", "M486"}},
|
|
// GT 1.11: Foerderband-Geometrie (Abstand + Oeffnungsgroesse)
|
|
{"1.11", "HP1621", []string{"M496", "M497", "M498"}},
|
|
// GT 1.22: Greifer-Versagen + Werkstueck weggeschleudert
|
|
{"1.22", "HP1711", []string{"M501", "M502", "M536"}},
|
|
// GT 1.24: Eingeschlossen in Zelle — Innenoeffnung + bewusster Wiederanlauf
|
|
{"1.24", "HP1603", []string{"M489", "M488"}},
|
|
// GT 1.12/1.24 (HP1651 Wiederanlauf-Variante): Wiederanlauf-Schutz-Measures —
|
|
// NOT thermal (M054 was wrongly placed here and surfaced as
|
|
// "Sichere thermische Auslegung" for a restart hazard)
|
|
{"1.12/1.24", "HP1651", []string{"M488", "M487", "M489", "M490"}},
|
|
// GT 1.1 (HP1625 sharp edges): edge-specific only, no rotational/distance fillers
|
|
{"1.1", "HP1625", []string{"M003", "M004", "M027"}},
|
|
// GT 1.26: Foerderband-Geschwindigkeit < 100 mm/s
|
|
{"1.26", "HP1620", []string{"M498", "M499"}},
|
|
// GT 1.27: Mechanischer Anschlag am Bandende
|
|
{"1.27", "HP1622", []string{"M500"}},
|
|
// GT 1.30: Druckluft-Reinigungsduese
|
|
{"1.30", "HP1712", []string{"M504", "M505"}},
|
|
// GT 1.32: WZM-Beladetuer + zweikanaliger Tuerschalter
|
|
{"1.32", "HP1634", []string{}}, // skipped: HP1634 already had M061; verify exists
|
|
// GT 1.34/2.10: KSS-Druckschlauch
|
|
{"1.34/2.10", "HP1675", []string{"M484", "M483"}},
|
|
// GT 1.38/1.39: KSS-Auslauf unten + Druck begrenzt
|
|
{"1.38/1.39", "HP1703", []string{"M505", "M506", "M526"}},
|
|
// GT 2.9: Wasser/Reinigung Schaltschrank
|
|
{"2.9", "HP1716", []string{"M521", "M522", "M539"}},
|
|
// GT 7.1: KSS-Hautkontakt
|
|
{"7.1", "HP1715", []string{"M408", "M533"}},
|
|
// GT 8.1: Manuelle Werkstueck-Handhabung + Hebehilfe >25kg
|
|
{"8.1", "HP1713", []string{"M530", "M532"}},
|
|
// GT 8.2: Bedienelement-Position ergonomisch
|
|
{"8.2", "HP1714", []string{"M531"}},
|
|
}
|
|
|
|
patterns := collectAllPatterns()
|
|
measureByID := make(map[string]ProtectiveMeasureEntry)
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measureByID[m.ID] = m
|
|
}
|
|
patternByID := make(map[string]HazardPattern)
|
|
for _, p := range patterns {
|
|
patternByID[p.ID] = p
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.gtNr+"_"+c.patternID, func(t *testing.T) {
|
|
p, ok := patternByID[c.patternID]
|
|
if !ok {
|
|
t.Fatalf("pattern %s missing — GT %s no longer covered", c.patternID, c.gtNr)
|
|
}
|
|
suggested := make(map[string]bool)
|
|
for _, m := range p.SuggestedMeasureIDs {
|
|
suggested[m] = true
|
|
}
|
|
for _, req := range c.requiredMeasures {
|
|
if _, exists := measureByID[req]; !exists {
|
|
t.Errorf("required measure %s referenced by GT %s does not exist in library", req, c.gtNr)
|
|
continue
|
|
}
|
|
if !suggested[req] {
|
|
t.Errorf("pattern %s no longer suggests %s — GT %s expert mitigation lost (current: %v)",
|
|
c.patternID, req, c.gtNr, p.SuggestedMeasureIDs)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGTBremse_ExpertMeasuresAllResolvable pins the static-text expectation
|
|
// that every Fachmann measure newly added during the 2026-05 GT coverage work
|
|
// (M481-M482, M483-M539) carries the concrete EN/IEC/ISO/DGUV norm reference
|
|
// that the expert cited in the GT file. A measure without a concrete norm
|
|
// reference is a regression — generic "Sichere X" entries were exactly the
|
|
// problem this work was meant to fix.
|
|
func TestGTBremse_ExpertMeasuresAllResolvable(t *testing.T) {
|
|
expertIDs := []string{
|
|
"M481", "M482", "M483", "M484", "M485", "M486", "M487", "M488", "M489", "M490",
|
|
"M491", "M492", "M493", "M494", "M495", "M496", "M497", "M498", "M499", "M500",
|
|
"M501", "M502", "M503", "M504", "M505", "M506", "M507", "M508", "M509", "M510",
|
|
"M511", "M512", "M513", "M514", "M515", "M516", "M517", "M518", "M519", "M520",
|
|
"M521", "M522", "M523", "M524", "M525", "M526", "M527", "M528", "M529", "M530",
|
|
"M531", "M532", "M533", "M534", "M535", "M536", "M537", "M538", "M539",
|
|
}
|
|
measureByID := make(map[string]ProtectiveMeasureEntry)
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measureByID[m.ID] = m
|
|
}
|
|
knownPrefixes := []string{"EN ", "IEC ", "ISO ", "DIN ", "TRBS", "TRGS", "ASR ", "DGUV", "OSHA", "VDE", "EN ISO", "DIN EN"}
|
|
for _, id := range expertIDs {
|
|
m, ok := measureByID[id]
|
|
if !ok {
|
|
t.Errorf("expert measure %s missing from library", id)
|
|
continue
|
|
}
|
|
if len(m.NormReferences) == 0 {
|
|
t.Errorf("measure %s (%q) has no NormReferences — concrete norm anchor missing", id, m.Name)
|
|
continue
|
|
}
|
|
found := false
|
|
for _, nr := range m.NormReferences {
|
|
for _, p := range knownPrefixes {
|
|
if strings.HasPrefix(nr, p) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("measure %s (%q) NormReferences %v contain no recognized norm prefix",
|
|
id, m.Name, m.NormReferences)
|
|
}
|
|
}
|
|
}
|