Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/norms_engine.go
T
Benjamin Admin 97a52533a8
Build + Deploy / build-admin-compliance (push) Successful in 2m29s
Build + Deploy / build-backend-compliance (push) Successful in 3m23s
Build + Deploy / build-ai-sdk (push) Failing after 47s
Build + Deploy / build-developer-portal (push) Successful in 1m19s
Build + Deploy / build-tts (push) Failing after 1m29s
Build + Deploy / build-document-crawler (push) Successful in 43s
Build + Deploy / build-dsms-gateway (push) Successful in 25s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m17s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 48s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 31s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 18s
Merge remote gitea/main — resolve conflicts keeping local (origin) state
Local origin is 20+ commits ahead of remote gitea. All conflicts
resolved by keeping HEAD (our version) which includes the full
56→138 check expansion and doc_checks package split.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 12:40:23 +02:00

173 lines
4.9 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()...)
allNorms = append(allNorms, GetMachiningCNorms()...)
allNorms = append(allNorms, GetConveyorAutoCNorms()...)
allNorms = append(allNorms, GetProcessCNorms()...)
allNorms = append(allNorms, GetConstructionCNorms()...)
// 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
}