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
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user