feat(iace): Phase 5+6 — frontend integration, RAG library search, comprehensive tests
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 34s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 23s
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 2s

Phase 5 — Frontend Integration:
- components/page.tsx: ComponentLibraryModal with 120 components + 20 energy sources
- hazards/page.tsx: AutoSuggestPanel with 3-column pattern matching review
- mitigations/page.tsx: SuggestMeasuresModal per hazard with 3-level grouping
- verification/page.tsx: SuggestEvidenceModal per mitigation with evidence types

Phase 6 — RAG Library Search:
- Added bp_iace_libraries to AllowedCollections whitelist in rag_handlers.go
- SearchLibrary endpoint: POST /iace/library-search (semantic search across libraries)
- EnrichTechFileSection endpoint: POST /projects/:id/tech-file/:section/enrich
- Created ingest-iace-libraries.sh ingestion script for Qdrant collection

Tests (123 passing):
- tag_taxonomy_test.go: 8 tests for taxonomy entries, domains, essential tags
- controls_library_test.go: 7 tests for measures, reduction types, subtypes
- integration_test.go: 7 integration tests for full match flow and library consistency
- Extended tag_resolver_test.go: 9 new tests for FindByTags and cross-category resolution

Documentation:
- Updated iace.md with Hazard-Matching-Engine, RAG enrichment, and new DB tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-16 10:22:49 +01:00
parent 3b2006ebce
commit 9c1355c05f
13 changed files with 2422 additions and 43 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
@@ -25,6 +26,7 @@ type IACEHandler struct {
engine *iace.RiskEngine
classifier *iace.Classifier
checker *iace.CompletenessChecker
ragClient *ucca.LegalRAGClient
}
// NewIACEHandler creates a new IACEHandler with all required dependencies.
@@ -34,6 +36,7 @@ func NewIACEHandler(store *iace.Store) *IACEHandler {
engine: iace.NewRiskEngine(),
classifier: iace.NewClassifier(),
checker: iace.NewCompletenessChecker(),
ragClient: ucca.NewLegalRAGClient(),
}
}
@@ -2325,6 +2328,138 @@ func (h *IACEHandler) SuggestEvidenceForMitigation(c *gin.Context) {
})
}
// ============================================================================
// RAG Library Search (Phase 6)
// ============================================================================
// IACELibrarySearchRequest represents a semantic search against the IACE library corpus.
type IACELibrarySearchRequest struct {
Query string `json:"query" binding:"required"`
Category string `json:"category,omitempty"`
TopK int `json:"top_k,omitempty"`
Filters []string `json:"filters,omitempty"`
}
// SearchLibrary handles POST /iace/library-search
// Performs semantic search across the IACE hazard/component/measure library in Qdrant.
func (h *IACEHandler) SearchLibrary(c *gin.Context) {
var req IACELibrarySearchRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
topK := req.TopK
if topK <= 0 || topK > 50 {
topK = 10
}
// Use regulation filter for category-based search within the IACE collection
var filters []string
if req.Category != "" {
filters = append(filters, req.Category)
}
filters = append(filters, req.Filters...)
results, err := h.ragClient.SearchCollection(
c.Request.Context(),
"bp_iace_libraries",
req.Query,
filters,
topK,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "RAG search failed",
"details": err.Error(),
})
return
}
if results == nil {
results = []ucca.LegalSearchResult{}
}
c.JSON(http.StatusOK, gin.H{
"query": req.Query,
"results": results,
"total": len(results),
})
}
// EnrichTechFileSection handles POST /projects/:id/tech-file/:section/enrich
// Uses RAG to find relevant library content for a specific tech file section.
func (h *IACEHandler) EnrichTechFileSection(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
sectionType := c.Param("section")
if sectionType == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"})
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
}
// Build a contextual query based on section type and project data
queryParts := []string{project.MachineName, project.MachineType}
switch sectionType {
case "risk_assessment_report", "hazard_log_combined":
queryParts = append(queryParts, "Gefaehrdungen", "Risikobewertung", "ISO 12100")
case "essential_requirements":
queryParts = append(queryParts, "Sicherheitsanforderungen", "Maschinenrichtlinie")
case "design_specifications":
queryParts = append(queryParts, "Konstruktionsspezifikation", "Sicherheitskonzept")
case "test_reports":
queryParts = append(queryParts, "Pruefbericht", "Verifikation", "Nachweis")
case "standards_applied":
queryParts = append(queryParts, "harmonisierte Normen", "EN ISO")
case "ai_risk_management":
queryParts = append(queryParts, "KI-Risikomanagement", "AI Act", "Algorithmen")
case "ai_human_oversight":
queryParts = append(queryParts, "menschliche Aufsicht", "Human Oversight", "KI-Transparenz")
default:
queryParts = append(queryParts, sectionType)
}
query := strings.Join(queryParts, " ")
results, err := h.ragClient.SearchCollection(
c.Request.Context(),
"bp_iace_libraries",
query,
nil,
5,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "RAG enrichment failed",
"details": err.Error(),
})
return
}
if results == nil {
results = []ucca.LegalSearchResult{}
}
c.JSON(http.StatusOK, gin.H{
"project_id": projectID.String(),
"section_type": sectionType,
"query": query,
"context": results,
"total": len(results),
})
}
// mustMarshalJSON marshals the given value to json.RawMessage.
func mustMarshalJSON(v interface{}) json.RawMessage {
data, err := json.Marshal(v)

View File

@@ -33,6 +33,7 @@ var AllowedCollections = map[string]bool{
"bp_dsfa_templates": true,
"bp_dsfa_risks": true,
"bp_legal_templates": true,
"bp_iace_libraries": true,
}
// SearchRequest represents a RAG search request.