feat(iace): Sprint 4C — Delta Impact Analysis
Neuer Endpoint POST /projects/:id/delta-analysis: - Input: aktuelle + vorgeschlagene Aenderung (Components, Energy, States, Roles) - Output: Diff der Pattern-Matches (added/removed Patterns, Hazards, Measures) - DeltaMatch() auf PatternEngine: Match(current) vs Match(proposed) - DeltaResult mit AddedPatterns, RemovedPatterns, Counts, SummaryDE Beispiel-Output: SPS hinzufuegen → +55 Patterns, +5 Hazard-Kategorien, +17 Massnahmen Maintenance-State hinzufuegen → +10 Patterns, +2 Hazards, +2 Massnahmen 7 Tests: NoChange, AddComponent, RemoveComponent, AddState, AddRole, Summary, Symmetric Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package iace
|
||||
|
||||
import "strconv"
|
||||
|
||||
// DeltaResult contains the impact of a proposed change to a project.
|
||||
// It compares Match(current) vs Match(current+proposed) and returns
|
||||
// the difference in matched patterns, hazards, measures, and evidence.
|
||||
type DeltaResult struct {
|
||||
// Patterns that would fire with the proposed change but not currently
|
||||
AddedPatterns []PatternMatch `json:"added_patterns"`
|
||||
// Patterns that currently fire but would stop with the proposed change
|
||||
RemovedPatterns []PatternMatch `json:"removed_patterns"`
|
||||
// Net change counts
|
||||
AddedHazardCount int `json:"added_hazard_count"`
|
||||
RemovedHazardCount int `json:"removed_hazard_count"`
|
||||
AddedMeasureCount int `json:"added_measure_count"`
|
||||
RemovedMeasureCount int `json:"removed_measure_count"`
|
||||
AddedEvidenceCount int `json:"added_evidence_count"`
|
||||
RemovedEvidenceCount int `json:"removed_evidence_count"`
|
||||
// Added/removed hazard categories
|
||||
AddedHazardCats []string `json:"added_hazard_categories,omitempty"`
|
||||
RemovedHazardCats []string `json:"removed_hazard_categories,omitempty"`
|
||||
// Added/removed measure IDs
|
||||
AddedMeasureIDs []string `json:"added_measure_ids,omitempty"`
|
||||
RemovedMeasureIDs []string `json:"removed_measure_ids,omitempty"`
|
||||
// Summary text (German)
|
||||
SummaryDE string `json:"summary_de"`
|
||||
}
|
||||
|
||||
// DeltaMatch computes the impact of a proposed change by comparing
|
||||
// the current match output with the proposed match output.
|
||||
func (e *PatternEngine) DeltaMatch(current, proposed MatchInput) *DeltaResult {
|
||||
currentOutput := e.Match(current)
|
||||
proposedOutput := e.Match(proposed)
|
||||
|
||||
// Build pattern ID sets
|
||||
currentPatterns := make(map[string]PatternMatch)
|
||||
for _, p := range currentOutput.MatchedPatterns {
|
||||
currentPatterns[p.PatternID] = p
|
||||
}
|
||||
proposedPatterns := make(map[string]PatternMatch)
|
||||
for _, p := range proposedOutput.MatchedPatterns {
|
||||
proposedPatterns[p.PatternID] = p
|
||||
}
|
||||
|
||||
// Find added/removed patterns
|
||||
var added, removed []PatternMatch
|
||||
for id, p := range proposedPatterns {
|
||||
if _, exists := currentPatterns[id]; !exists {
|
||||
added = append(added, p)
|
||||
}
|
||||
}
|
||||
for id, p := range currentPatterns {
|
||||
if _, exists := proposedPatterns[id]; !exists {
|
||||
removed = append(removed, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute hazard category diff
|
||||
currentCats := collectCats(currentOutput.SuggestedHazards)
|
||||
proposedCats := collectCats(proposedOutput.SuggestedHazards)
|
||||
addedCats := diffStrings(proposedCats, currentCats)
|
||||
removedCats := diffStrings(currentCats, proposedCats)
|
||||
|
||||
// Compute measure ID diff
|
||||
currentMIDs := collectMIDs(currentOutput.SuggestedMeasures)
|
||||
proposedMIDs := collectMIDs(proposedOutput.SuggestedMeasures)
|
||||
addedMIDs := diffStrings(proposedMIDs, currentMIDs)
|
||||
removedMIDs := diffStrings(currentMIDs, proposedMIDs)
|
||||
|
||||
// Compute evidence ID diff
|
||||
currentEIDs := collectEIDs(currentOutput.SuggestedEvidence)
|
||||
proposedEIDs := collectEIDs(proposedOutput.SuggestedEvidence)
|
||||
|
||||
result := &DeltaResult{
|
||||
AddedPatterns: added,
|
||||
RemovedPatterns: removed,
|
||||
AddedHazardCount: len(addedCats),
|
||||
RemovedHazardCount: len(removedCats),
|
||||
AddedMeasureCount: len(addedMIDs),
|
||||
RemovedMeasureCount: len(removedMIDs),
|
||||
AddedEvidenceCount: len(diffStrings(proposedEIDs, currentEIDs)),
|
||||
RemovedEvidenceCount: len(diffStrings(currentEIDs, proposedEIDs)),
|
||||
AddedHazardCats: addedCats,
|
||||
RemovedHazardCats: removedCats,
|
||||
AddedMeasureIDs: addedMIDs,
|
||||
RemovedMeasureIDs: removedMIDs,
|
||||
}
|
||||
|
||||
result.SummaryDE = buildDeltaSummary(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func collectCats(hazards []HazardSuggestion) []string {
|
||||
var cats []string
|
||||
for _, h := range hazards {
|
||||
cats = append(cats, h.Category)
|
||||
}
|
||||
return cats
|
||||
}
|
||||
|
||||
func collectMIDs(measures []MeasureSuggestion) []string {
|
||||
var ids []string
|
||||
for _, m := range measures {
|
||||
ids = append(ids, m.MeasureID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func collectEIDs(evidence []EvidenceSuggestion) []string {
|
||||
var ids []string
|
||||
for _, e := range evidence {
|
||||
ids = append(ids, e.EvidenceID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func diffStrings(a, b []string) []string {
|
||||
bSet := make(map[string]bool)
|
||||
for _, s := range b {
|
||||
bSet[s] = true
|
||||
}
|
||||
var diff []string
|
||||
seen := make(map[string]bool)
|
||||
for _, s := range a {
|
||||
if !bSet[s] && !seen[s] {
|
||||
diff = append(diff, s)
|
||||
seen[s] = true
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
func buildDeltaSummary(r *DeltaResult) string {
|
||||
if len(r.AddedPatterns) == 0 && len(r.RemovedPatterns) == 0 {
|
||||
return "Keine Auswirkung — die vorgeschlagene Aenderung veraendert die Risikobeurteilung nicht."
|
||||
}
|
||||
s := ""
|
||||
if len(r.AddedPatterns) > 0 {
|
||||
s += "+" + deltaItoa(len(r.AddedPatterns)) + " neue Gefaehrdungsmuster erkannt"
|
||||
}
|
||||
if len(r.RemovedPatterns) > 0 {
|
||||
if s != "" {
|
||||
s += ", "
|
||||
}
|
||||
s += "-" + deltaItoa(len(r.RemovedPatterns)) + " Gefaehrdungsmuster entfallen"
|
||||
}
|
||||
if r.AddedHazardCount > 0 {
|
||||
s += ". +" + deltaItoa(r.AddedHazardCount) + " neue Gefaehrdungskategorien"
|
||||
}
|
||||
if r.AddedMeasureCount > 0 {
|
||||
s += ". +" + deltaItoa(r.AddedMeasureCount) + " zusaetzliche Massnahmen empfohlen"
|
||||
}
|
||||
s += "."
|
||||
return s
|
||||
}
|
||||
|
||||
func deltaItoa(i int) string {
|
||||
return strconv.Itoa(i)
|
||||
}
|
||||
Reference in New Issue
Block a user