1502ac6d8f
- HP059 Trigger: "DSFA erforderlich" → "zu pruefen" mit Entscheidungslogik (Edge-Processing ohne Speicherung/Personenerkennung = keine DSFA) - 6 FAQ-Eintraege: Kamera-PII, zugekaufte Baugruppen, Herstellererklaerung, KI-Hochrisiko, CRA OTA-Updates, verkettete Produktionslinien - GET /compliance-faq Endpoint mit Kategorie-Filter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
3.4 KiB
Go
107 lines
3.4 KiB
Go
package handlers
|
||
|
||
import (
|
||
"net/http"
|
||
|
||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// ============================================================================
|
||
// CE x Compliance Crossover Engine
|
||
// ============================================================================
|
||
|
||
// GetComplianceTriggers handles GET /projects/:id/compliance-triggers.
|
||
// It analyses the project's hazards and component patterns to determine
|
||
// which DSGVO, AI Act, CRA, NIS2, and EU Data Act obligations are triggered.
|
||
// The response includes deduplicated triggers sorted by severity, plus boolean
|
||
// summary flags (dsfa_required, ai_act_relevant, cra_relevant, etc.).
|
||
func (h *IACEHandler) GetComplianceTriggers(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 {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
if project == nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||
return
|
||
}
|
||
|
||
// Fetch all hazards for this project
|
||
hazards, err := h.store.ListHazards(c.Request.Context(), projectID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// Also run pattern matching with component tags to catch tag-based triggers.
|
||
// Collect tags from the project's components (reuse the norms handler logic).
|
||
componentTags := collectComponentTags(h, c, projectID)
|
||
|
||
// Get all patterns from the pattern library
|
||
allPatterns := iace.AllPatterns()
|
||
|
||
// Additionally derive extra fired patterns by re-matching component tags
|
||
// against the pattern engine. This ensures patterns that are not yet
|
||
// applied as hazards still contribute their compliance triggers.
|
||
engine := iace.NewPatternEngine()
|
||
matchInput := iace.MatchInput{
|
||
CustomTags: componentTags,
|
||
}
|
||
matchResult := engine.Match(matchInput)
|
||
|
||
// Merge matched pattern IDs into a pseudo-hazard list so the crossover
|
||
// engine picks them up. We create lightweight Hazard structs with the
|
||
// pattern ID embedded in the Description field.
|
||
mergedHazards := make([]iace.Hazard, len(hazards))
|
||
copy(mergedHazards, hazards)
|
||
for _, pm := range matchResult.MatchedPatterns {
|
||
mergedHazards = append(mergedHazards, iace.Hazard{
|
||
Name: pm.PatternName,
|
||
Description: "Pattern " + pm.PatternID,
|
||
Category: firstOrEmpty(pm.HazardCats),
|
||
})
|
||
}
|
||
|
||
// Run the crossover engine
|
||
summary := iace.GetProjectComplianceTriggers(mergedHazards, allPatterns)
|
||
|
||
c.JSON(http.StatusOK, summary)
|
||
}
|
||
|
||
// GetComplianceFAQ handles GET /compliance-faq
|
||
// Returns CE × Compliance FAQ entries, optionally filtered by ?category=
|
||
func (h *IACEHandler) GetComplianceFAQ(c *gin.Context) {
|
||
category := c.Query("category")
|
||
all := iace.GetComplianceFAQ()
|
||
|
||
if category != "" {
|
||
var filtered []iace.ComplianceFAQEntry
|
||
for _, f := range all {
|
||
if f.Category == category {
|
||
filtered = append(filtered, f)
|
||
}
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"faq": filtered, "total": len(filtered)})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"faq": all, "total": len(all)})
|
||
}
|
||
|
||
// firstOrEmpty returns the first element of a string slice or "".
|
||
func firstOrEmpty(ss []string) string {
|
||
if len(ss) > 0 {
|
||
return ss[0]
|
||
}
|
||
return ""
|
||
}
|