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 }