ba9558384f
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
213 lines
6.0 KiB
Go
213 lines
6.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Norms Library & Norm Suggestions
|
|
// ============================================================================
|
|
|
|
// ListNormsLibrary handles GET /norms-library
|
|
// Returns the full norms library, optionally filtered by ?type=A|B1|B2|C
|
|
// and ?hazard_category=xxx.
|
|
func (h *IACEHandler) ListNormsLibrary(c *gin.Context) {
|
|
normType := c.Query("type")
|
|
hazardCat := c.Query("hazard_category")
|
|
|
|
allNorms := iace.GetNormsLibrary()
|
|
allNorms = append(allNorms, iace.GetExtendedB2Norms()...)
|
|
allNorms = append(allNorms, iace.GetCNormsLibrary()...)
|
|
allNorms = append(allNorms, iace.GetExtendedCNormsLibrary()...)
|
|
allNorms = append(allNorms, iace.GetWoodMetalCNorms()...)
|
|
allNorms = append(allNorms, iace.GetFoodPkgCNorms()...)
|
|
allNorms = append(allNorms, iace.GetLiftMiscCNorms()...)
|
|
allNorms = append(allNorms, iace.GetMachiningCNorms()...)
|
|
allNorms = append(allNorms, iace.GetConveyorAutoCNorms()...)
|
|
allNorms = append(allNorms, iace.GetProcessCNorms()...)
|
|
allNorms = append(allNorms, iace.GetConstructionCNorms()...)
|
|
allNorms = append(allNorms, iace.GetNiche1CNorms()...)
|
|
allNorms = append(allNorms, iace.GetNiche2CNorms()...)
|
|
allNorms = append(allNorms, iace.GetNiche3CNorms()...)
|
|
allNorms = append(allNorms, iace.GetExtendedB2Norms2()...)
|
|
allNorms = append(allNorms, iace.GetWave3aCNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3a2CNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3bCNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3cCNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3c2CNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3dCNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3dExtCNorms()...)
|
|
allNorms = append(allNorms, iace.GetWave3dHvacCNorms()...)
|
|
|
|
var filtered []iace.NormReference
|
|
for _, norm := range allNorms {
|
|
if normType != "" && norm.NormType != normType {
|
|
continue
|
|
}
|
|
if hazardCat != "" && !containsString(norm.HazardCats, hazardCat) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, norm)
|
|
}
|
|
|
|
if filtered == nil {
|
|
filtered = []iace.NormReference{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"norms": filtered,
|
|
"total": len(filtered),
|
|
})
|
|
}
|
|
|
|
// SuggestProjectNorms handles GET /projects/:id/suggested-norms
|
|
// Returns norm suggestions based on the project's machine type, identified
|
|
// hazards, and component tags.
|
|
func (h *IACEHandler) SuggestProjectNorms(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
// Fetch project to get machine type
|
|
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
|
|
}
|
|
|
|
// Collect unique hazard categories from project hazards
|
|
hazardCategories := collectHazardCategories(h, c, projectID)
|
|
|
|
// Collect tags from component metadata
|
|
tags := collectComponentTags(h, c, projectID)
|
|
|
|
result := iace.SuggestNorms(project.MachineType, hazardCategories, tags)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"suggestions": result,
|
|
"machine_type": project.MachineType,
|
|
"hazard_categories": hazardCategories,
|
|
"tags": tags,
|
|
})
|
|
}
|
|
|
|
// collectHazardCategories extracts unique hazard categories from a project's hazards.
|
|
func collectHazardCategories(h *IACEHandler, c *gin.Context, projectID uuid.UUID) []string {
|
|
hazards, err := h.store.ListHazards(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
var categories []string
|
|
for _, hz := range hazards {
|
|
if hz.Category != "" && !seen[hz.Category] {
|
|
seen[hz.Category] = true
|
|
categories = append(categories, hz.Category)
|
|
}
|
|
}
|
|
return categories
|
|
}
|
|
|
|
// collectComponentTags extracts tags from the metadata JSON of project components.
|
|
// Components store tags in their metadata field as {"tags": ["tag1", "tag2"]}.
|
|
// Additionally, component types are mapped to tags via the component library.
|
|
func collectComponentTags(h *IACEHandler, c *gin.Context, projectID uuid.UUID) []string {
|
|
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
var tags []string
|
|
|
|
for _, comp := range components {
|
|
// Extract tags from metadata JSON
|
|
extracted := extractTagsFromMetadata(comp.Metadata)
|
|
for _, t := range extracted {
|
|
if !seen[t] {
|
|
seen[t] = true
|
|
tags = append(tags, t)
|
|
}
|
|
}
|
|
|
|
// Extract tags from description field ("Tags: x, y, z" pattern)
|
|
descTags := parseDescriptionTags(comp.Description)
|
|
for _, t := range descTags {
|
|
if !seen[t] {
|
|
seen[t] = true
|
|
tags = append(tags, t)
|
|
}
|
|
}
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
// extractTagsFromMetadata parses the component metadata JSON for a "tags" array.
|
|
func extractTagsFromMetadata(metadata json.RawMessage) []string {
|
|
if len(metadata) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(metadata, &m); err != nil {
|
|
return nil
|
|
}
|
|
|
|
tagsRaw, ok := m["tags"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
arr, ok := tagsRaw.([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var tags []string
|
|
for _, v := range arr {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
tags = append(tags, s)
|
|
}
|
|
}
|
|
return tags
|
|
}
|
|
|
|
// parseDescriptionTags looks for a "Tags: x, y, z" pattern in the description.
|
|
func parseDescriptionTags(description string) []string {
|
|
idx := strings.Index(strings.ToLower(description), "tags:")
|
|
if idx < 0 {
|
|
return nil
|
|
}
|
|
|
|
// Take everything after "Tags:"
|
|
rest := strings.TrimSpace(description[idx+5:])
|
|
if rest == "" {
|
|
return nil
|
|
}
|
|
|
|
// Split by comma and trim each tag
|
|
parts := strings.Split(rest, ",")
|
|
var tags []string
|
|
for _, p := range parts {
|
|
t := strings.TrimSpace(p)
|
|
if t != "" {
|
|
tags = append(tags, t)
|
|
}
|
|
}
|
|
return tags
|
|
}
|