e7f2f98da3
Major features: - 215 norms library with section references + Beuth URLs (A/B1/B2/C norms) - 173 hazard patterns with detail fields (scenario, trigger, harm, zone) - Deterministic pattern matching: Component × Lifecycle × Pattern cross-product - SIL/PL auto-calculation from S×E×P risk graph - Risk assessment table with editable S/E/P dropdowns - Production Line Dashboard with animated station flow (Running Dots) - IACE process flow + norms coverage on start page - Non-blocking cookie banner, ProcessFlow SSR fix - 104 Playwright E2E tests passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
package iace
|
|
|
|
import "sort"
|
|
|
|
// NormSuggestion represents a single norm matched to a project context.
|
|
type NormSuggestion struct {
|
|
Norm NormReference `json:"norm"`
|
|
Reason string `json:"reason"` // Why this norm was suggested
|
|
Confidence float64 `json:"confidence"` // 0.0-1.0 relevance score
|
|
Sources []string `json:"sources"` // What triggered the match
|
|
}
|
|
|
|
// NormSuggestionResult groups suggested norms by hierarchy type.
|
|
type NormSuggestionResult struct {
|
|
ANorms []NormSuggestion `json:"a_norms"`
|
|
B1Norms []NormSuggestion `json:"b1_norms"`
|
|
B2Norms []NormSuggestion `json:"b2_norms"`
|
|
CNorms []NormSuggestion `json:"c_norms"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
// SuggestNorms matches relevant norms for a project based on its machine type,
|
|
// identified hazard categories, and component/energy tags.
|
|
// A-norms are always included (they apply universally). B/C norms are matched
|
|
// by machine type (confidence 0.9), hazard category (0.8), or tag (0.7).
|
|
func SuggestNorms(machineType string, hazardCategories []string, tags []string) *NormSuggestionResult {
|
|
allNorms := GetNormsLibrary()
|
|
allNorms = append(allNorms, GetExtendedB2Norms()...)
|
|
allNorms = append(allNorms, GetCNormsLibrary()...)
|
|
allNorms = append(allNorms, GetExtendedCNormsLibrary()...)
|
|
allNorms = append(allNorms, GetWoodMetalCNorms()...)
|
|
allNorms = append(allNorms, GetFoodPkgCNorms()...)
|
|
allNorms = append(allNorms, GetLiftMiscCNorms()...)
|
|
|
|
// Build lookup sets for efficient matching
|
|
hazardSet := toSet(hazardCategories)
|
|
tagSet := toSet(tags)
|
|
|
|
seen := make(map[string]bool)
|
|
var suggestions []NormSuggestion
|
|
|
|
for _, norm := range allNorms {
|
|
if seen[norm.ID] {
|
|
continue
|
|
}
|
|
|
|
suggestion := matchNorm(norm, machineType, hazardSet, tagSet)
|
|
if suggestion != nil {
|
|
seen[norm.ID] = true
|
|
suggestions = append(suggestions, *suggestion)
|
|
}
|
|
}
|
|
|
|
return groupByType(suggestions)
|
|
}
|
|
|
|
// matchNorm checks a single norm against the project context and returns
|
|
// a suggestion if it matches, or nil otherwise.
|
|
func matchNorm(norm NormReference, machineType string, hazardSet, tagSet map[string]bool) *NormSuggestion {
|
|
// A-norms always apply to all machines
|
|
if norm.NormType == "A" {
|
|
return &NormSuggestion{
|
|
Norm: norm,
|
|
Reason: "Grundnorm — gilt fuer alle Maschinen",
|
|
Confidence: 1.0,
|
|
Sources: []string{"norm_type:A"},
|
|
}
|
|
}
|
|
|
|
var bestConfidence float64
|
|
var reasons []string
|
|
var sources []string
|
|
|
|
// Machine type match
|
|
machineTypeMatched := false
|
|
if machineType != "" && len(norm.MachineTypes) > 0 {
|
|
for _, mt := range norm.MachineTypes {
|
|
if mt == machineType {
|
|
bestConfidence = 0.9
|
|
reasons = append(reasons, "Maschinentyp: "+machineType)
|
|
sources = append(sources, "machine_type:"+machineType)
|
|
machineTypeMatched = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// C-norms with machine_types ONLY match via machine type — skip tag/hazard matching
|
|
// to avoid suggesting e.g. lathe norms for a press just because both have "rotating_part"
|
|
if norm.NormType == "C" && len(norm.MachineTypes) > 0 && !machineTypeMatched {
|
|
return nil
|
|
}
|
|
|
|
// Hazard category match (B-norms and C-norms without machine_types)
|
|
for _, hc := range norm.HazardCats {
|
|
if hazardSet[hc] {
|
|
if 0.8 > bestConfidence {
|
|
bestConfidence = 0.8
|
|
}
|
|
reasons = append(reasons, "Gefaehrdung: "+hc)
|
|
sources = append(sources, "hazard_cat:"+hc)
|
|
}
|
|
}
|
|
|
|
// Tag match
|
|
for _, t := range norm.Tags {
|
|
if tagSet[t] {
|
|
if 0.7 > bestConfidence {
|
|
bestConfidence = 0.7
|
|
}
|
|
reasons = append(reasons, "Tag: "+t)
|
|
sources = append(sources, "tag:"+t)
|
|
}
|
|
}
|
|
|
|
if bestConfidence == 0 {
|
|
return nil
|
|
}
|
|
|
|
return &NormSuggestion{
|
|
Norm: norm,
|
|
Reason: joinReasons(reasons),
|
|
Confidence: bestConfidence,
|
|
Sources: sources,
|
|
}
|
|
}
|
|
|
|
// groupByType sorts suggestions by confidence and groups them by norm type.
|
|
func groupByType(suggestions []NormSuggestion) *NormSuggestionResult {
|
|
sort.Slice(suggestions, func(i, j int) bool {
|
|
return suggestions[i].Confidence > suggestions[j].Confidence
|
|
})
|
|
|
|
result := &NormSuggestionResult{
|
|
ANorms: []NormSuggestion{},
|
|
B1Norms: []NormSuggestion{},
|
|
B2Norms: []NormSuggestion{},
|
|
CNorms: []NormSuggestion{},
|
|
}
|
|
|
|
for _, s := range suggestions {
|
|
switch s.Norm.NormType {
|
|
case "A":
|
|
result.ANorms = append(result.ANorms, s)
|
|
case "B1":
|
|
result.B1Norms = append(result.B1Norms, s)
|
|
case "B2":
|
|
result.B2Norms = append(result.B2Norms, s)
|
|
case "C":
|
|
result.CNorms = append(result.CNorms, s)
|
|
}
|
|
}
|
|
|
|
result.Total = len(suggestions)
|
|
return result
|
|
}
|
|
|
|
// joinReasons combines multiple reason strings with "; ".
|
|
func joinReasons(reasons []string) string {
|
|
if len(reasons) == 0 {
|
|
return ""
|
|
}
|
|
result := reasons[0]
|
|
for i := 1; i < len(reasons); i++ {
|
|
result += "; " + reasons[i]
|
|
}
|
|
return result
|
|
}
|