feat(iace): add hazard-matching-engine with component library, tag system, and pattern engine
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s

Implements Phases 1-4 of the IACE Hazard-Matching-Engine:
- 120 machine components (C001-C120) in 11 categories
- 20 energy sources (EN01-EN20)
- ~85 tag taxonomy across 5 domains
- 44 hazard patterns with AND/NOT matching logic
- Pattern engine with tag resolution and confidence scoring
- 8 new API endpoints (component-library, energy-sources, tags, patterns, match/apply)
- Completeness gate G09 for pattern matching
- 320 tests passing (36 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-16 08:50:11 +01:00
parent c7651796c9
commit 3b2006ebce
21 changed files with 4993 additions and 41 deletions

View File

@@ -2052,3 +2052,284 @@ func (h *IACEHandler) ListEvidenceTypes(c *gin.Context) {
"total": len(types),
})
}
// ============================================================================
// Component Library & Energy Sources (Phase 1)
// ============================================================================
// ListComponentLibrary handles GET /component-library
// Returns the built-in component library with optional category filter.
func (h *IACEHandler) ListComponentLibrary(c *gin.Context) {
category := c.Query("category")
all := iace.GetComponentLibrary()
var filtered []iace.ComponentLibraryEntry
for _, entry := range all {
if category != "" && entry.Category != category {
continue
}
filtered = append(filtered, entry)
}
if filtered == nil {
filtered = []iace.ComponentLibraryEntry{}
}
c.JSON(http.StatusOK, gin.H{
"components": filtered,
"total": len(filtered),
})
}
// ListEnergySources handles GET /energy-sources
// Returns the built-in energy source library.
func (h *IACEHandler) ListEnergySources(c *gin.Context) {
sources := iace.GetEnergySources()
c.JSON(http.StatusOK, gin.H{
"energy_sources": sources,
"total": len(sources),
})
}
// ============================================================================
// Tag Taxonomy (Phase 2)
// ============================================================================
// ListTags handles GET /tags
// Returns the tag taxonomy with optional domain filter.
func (h *IACEHandler) ListTags(c *gin.Context) {
domain := c.Query("domain")
all := iace.GetTagTaxonomy()
var filtered []iace.TagEntry
for _, entry := range all {
if domain != "" && entry.Domain != domain {
continue
}
filtered = append(filtered, entry)
}
if filtered == nil {
filtered = []iace.TagEntry{}
}
c.JSON(http.StatusOK, gin.H{
"tags": filtered,
"total": len(filtered),
})
}
// ============================================================================
// Hazard Patterns & Pattern Engine (Phase 3+4)
// ============================================================================
// ListHazardPatterns handles GET /hazard-patterns
// Returns all built-in hazard patterns.
func (h *IACEHandler) ListHazardPatterns(c *gin.Context) {
patterns := iace.GetBuiltinHazardPatterns()
c.JSON(http.StatusOK, gin.H{
"patterns": patterns,
"total": len(patterns),
})
}
// MatchPatterns handles POST /projects/:id/match-patterns
// Runs the pattern engine against the project's components and energy sources.
func (h *IACEHandler) MatchPatterns(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
// Verify project exists
project, err := h.store.GetProject(c.Request.Context(), projectID)
if err != nil || project == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
return
}
var input iace.MatchInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
engine := iace.NewPatternEngine()
result := engine.Match(input)
c.JSON(http.StatusOK, result)
}
// ApplyPatternResults handles POST /projects/:id/apply-patterns
// Accepts matched patterns and creates concrete hazards, mitigations, and
// verification plans in the project.
func (h *IACEHandler) ApplyPatternResults(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
tenantID, err := getTenantID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
project, err := h.store.GetProject(c.Request.Context(), projectID)
if err != nil || project == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
return
}
var req struct {
AcceptedHazards []iace.CreateHazardRequest `json:"accepted_hazards"`
AcceptedMeasures []iace.CreateMitigationRequest `json:"accepted_measures"`
AcceptedEvidence []iace.CreateVerificationPlanRequest `json:"accepted_evidence"`
SourcePatternIDs []string `json:"source_pattern_ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
var createdHazards int
var createdMeasures int
var createdEvidence int
// Create hazards
for _, hazardReq := range req.AcceptedHazards {
hazardReq.ProjectID = projectID
_, err := h.store.CreateHazard(ctx, hazardReq)
if err != nil {
continue
}
createdHazards++
}
// Create mitigations
for _, mitigReq := range req.AcceptedMeasures {
_, err := h.store.CreateMitigation(ctx, mitigReq)
if err != nil {
continue
}
createdMeasures++
}
// Create verification plans
for _, evidReq := range req.AcceptedEvidence {
evidReq.ProjectID = projectID
_, err := h.store.CreateVerificationPlan(ctx, evidReq)
if err != nil {
continue
}
createdEvidence++
}
// Audit trail
h.store.AddAuditEntry(ctx, projectID, "pattern_matching", projectID,
iace.AuditActionCreate, tenantID.String(),
nil,
mustMarshalJSON(map[string]interface{}{
"source_patterns": req.SourcePatternIDs,
"created_hazards": createdHazards,
"created_measures": createdMeasures,
"created_evidence": createdEvidence,
}),
)
c.JSON(http.StatusOK, gin.H{
"created_hazards": createdHazards,
"created_measures": createdMeasures,
"created_evidence": createdEvidence,
"message": "Pattern results applied successfully",
})
}
// SuggestMeasuresForHazard handles POST /projects/:id/hazards/:hid/suggest-measures
// Suggests measures for a specific hazard based on its tags and category.
func (h *IACEHandler) SuggestMeasuresForHazard(c *gin.Context) {
hazardID, err := uuid.Parse(c.Param("hid"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"})
return
}
hazard, err := h.store.GetHazard(c.Request.Context(), hazardID)
if err != nil || hazard == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"})
return
}
// Find measures matching the hazard category
all := iace.GetProtectiveMeasureLibrary()
var suggested []iace.ProtectiveMeasureEntry
for _, m := range all {
if m.HazardCategory == hazard.Category || m.HazardCategory == "general" {
suggested = append(suggested, m)
}
}
if suggested == nil {
suggested = []iace.ProtectiveMeasureEntry{}
}
c.JSON(http.StatusOK, gin.H{
"hazard_id": hazardID.String(),
"hazard_category": hazard.Category,
"suggested_measures": suggested,
"total": len(suggested),
})
}
// SuggestEvidenceForMitigation handles POST /projects/:id/mitigations/:mid/suggest-evidence
// Suggests evidence types for a specific mitigation.
func (h *IACEHandler) SuggestEvidenceForMitigation(c *gin.Context) {
mitigationID, err := uuid.Parse(c.Param("mid"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"})
return
}
mitigation, err := h.store.GetMitigation(c.Request.Context(), mitigationID)
if err != nil || mitigation == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"})
return
}
// Map reduction type to relevant evidence tags
var relevantTags []string
switch mitigation.ReductionType {
case iace.ReductionTypeDesign:
relevantTags = []string{"design_evidence", "analysis_evidence"}
case iace.ReductionTypeProtective:
relevantTags = []string{"test_evidence", "inspection_evidence"}
case iace.ReductionTypeInformation:
relevantTags = []string{"training_evidence", "operational_evidence"}
}
resolver := iace.NewTagResolver()
suggested := resolver.FindEvidenceByTags(relevantTags)
if suggested == nil {
suggested = []iace.EvidenceTypeInfo{}
}
c.JSON(http.StatusOK, gin.H{
"mitigation_id": mitigationID.String(),
"reduction_type": string(mitigation.ReductionType),
"suggested_evidence": suggested,
"total": len(suggested),
})
}
// mustMarshalJSON marshals the given value to json.RawMessage.
func mustMarshalJSON(v interface{}) json.RawMessage {
data, err := json.Marshal(v)
if err != nil {
return nil
}
return data
}