package iace import ( "context" "fmt" "strings" "github.com/google/uuid" ) // CustomerStandardSuggestion aggregates one reusable mitigation across prior // projects of the same customer. The same mitigation name may appear in // multiple prior projects; we collapse them into a single suggestion and // count the prior occurrences so the expert sees a confidence signal. type CustomerStandardSuggestion struct { Name string `json:"name"` ReductionType string `json:"reduction_type"` Description string `json:"description"` // SourceProjectCount tells the expert in how many of the customer's // earlier projects this mitigation was already flagged. Higher count // = stronger reuse signal. SourceProjectCount int `json:"source_project_count"` SourceProjectNames []string `json:"source_project_names"` IsCustomerStandard bool `json:"is_customer_standard"` HasVerifiedInstances bool `json:"has_verified_instances"` } // ListCustomerStandardSuggestions returns reusable mitigations from prior // projects of the same customer as projectID. The customer key is the // case-insensitive trimmed customer_name; an empty customer_name short- // circuits to an empty result. // // includeVerified=false → only mitigations with is_customer_standard=true // includeVerified=true → also include status='verified' mitigations // (broader pool, useful when the customer-standard // habit isn't yet established in the data) func (s *Store) ListCustomerStandardSuggestions( ctx context.Context, projectID uuid.UUID, includeVerified bool, ) ([]CustomerStandardSuggestion, error) { // Resolve the customer + tenant for the current project. var tenantID uuid.UUID var customerName string err := s.pool.QueryRow(ctx, `SELECT tenant_id, COALESCE(customer_name, '') FROM iace_projects WHERE id = $1`, projectID, ).Scan(&tenantID, &customerName) if err != nil { return nil, fmt.Errorf("resolve project for customer-standards: %w", err) } customerName = strings.TrimSpace(customerName) if customerName == "" { return []CustomerStandardSuggestion{}, nil } filterClause := "m.is_customer_standard = TRUE" if includeVerified { filterClause = "(m.is_customer_standard = TRUE OR m.status = 'verified')" } query := fmt.Sprintf(` SELECT m.name, m.reduction_type, MAX(m.description) AS description, COUNT(DISTINCT p.id) AS source_count, array_agg(DISTINCT p.machine_name ORDER BY p.machine_name) AS source_names, BOOL_OR(m.is_customer_standard) AS has_customer_std, BOOL_OR(m.status = 'verified') AS has_verified FROM iace_mitigations m JOIN iace_hazards h ON h.id = m.hazard_id JOIN iace_projects p ON p.id = h.project_id WHERE p.tenant_id = $1 AND p.id <> $2 AND p.archived_at IS NULL AND LOWER(TRIM(COALESCE(p.customer_name, ''))) = LOWER($3) AND %s GROUP BY m.name, m.reduction_type ORDER BY source_count DESC, m.name `, filterClause) rows, err := s.pool.Query(ctx, query, tenantID, projectID, customerName) if err != nil { return nil, fmt.Errorf("query customer-standards: %w", err) } defer rows.Close() var out []CustomerStandardSuggestion for rows.Next() { var sg CustomerStandardSuggestion if scanErr := rows.Scan( &sg.Name, &sg.ReductionType, &sg.Description, &sg.SourceProjectCount, &sg.SourceProjectNames, &sg.IsCustomerStandard, &sg.HasVerifiedInstances, ); scanErr != nil { return nil, fmt.Errorf("scan customer-standards: %w", scanErr) } out = append(out, sg) } return out, nil } // ImportCustomerStandardSuggestion applies a suggestion to the current // project: for every hazard in the project whose name matches one of the // suggestion's source hazards (by mitigation.name → hazard.name pairing in // prior projects), it ensures a relevant + customer-standard mitigation // exists. New mitigations are inserted via CreateMitigation (idempotent // via UNIQUE(hazard_id, name)), existing ones are flipped to // is_relevant=true + is_customer_standard=true + status='verified'. // // Returns the number of mitigations affected (created + updated). func (s *Store) ImportCustomerStandardSuggestion( ctx context.Context, projectID uuid.UUID, mitigationName string, ) (int, error) { // Find tenant + customer of the target project. var tenantID uuid.UUID var customerName string if err := s.pool.QueryRow(ctx, `SELECT tenant_id, COALESCE(customer_name, '') FROM iace_projects WHERE id = $1`, projectID, ).Scan(&tenantID, &customerName); err != nil { return 0, fmt.Errorf("resolve project: %w", err) } customerName = strings.TrimSpace(customerName) if customerName == "" { return 0, fmt.Errorf("project has no customer_name — nothing to reuse") } // Collect the hazard names this mitigation was attached to in the // customer's prior projects + a representative reduction_type/description. priorRows, err := s.pool.Query(ctx, ` SELECT DISTINCT h.name, m.reduction_type, COALESCE(m.description, '') FROM iace_mitigations m JOIN iace_hazards h ON h.id = m.hazard_id JOIN iace_projects p ON p.id = h.project_id WHERE p.tenant_id = $1 AND p.id <> $2 AND p.archived_at IS NULL AND LOWER(TRIM(COALESCE(p.customer_name, ''))) = LOWER($3) AND m.name = $4 `, tenantID, projectID, customerName, mitigationName) if err != nil { return 0, fmt.Errorf("collect prior hazards: %w", err) } defer priorRows.Close() type proto struct{ hazardName, reductionType, description string } var prototypes []proto for priorRows.Next() { var p proto if err := priorRows.Scan(&p.hazardName, &p.reductionType, &p.description); err != nil { return 0, err } prototypes = append(prototypes, p) } if len(prototypes) == 0 { return 0, nil } // For every prototype hazard name, find the matching hazard in the // current project (same name) and ensure a relevant + customer-standard // mitigation with mitigationName exists for it. affected := 0 for _, p := range prototypes { var hazardIDs []uuid.UUID hazRows, err := s.pool.Query(ctx, `SELECT id FROM iace_hazards WHERE project_id = $1 AND name = $2`, projectID, p.hazardName, ) if err != nil { return affected, fmt.Errorf("find target hazards: %w", err) } for hazRows.Next() { var hid uuid.UUID if scanErr := hazRows.Scan(&hid); scanErr != nil { hazRows.Close() return affected, scanErr } hazardIDs = append(hazardIDs, hid) } hazRows.Close() for _, hid := range hazardIDs { // Idempotent insert; UPDATE sets relevance + verified state. _, err := s.CreateMitigation(ctx, CreateMitigationRequest{ HazardID: hid, Name: mitigationName, Description: p.description, ReductionType: ReductionType(p.reductionType), }) if err != nil { return affected, fmt.Errorf("create mitigation: %w", err) } if _, err := s.pool.Exec(ctx, ` UPDATE iace_mitigations SET is_relevant = TRUE, is_customer_standard = TRUE, status = 'verified', updated_at = NOW() WHERE hazard_id = $1 AND name = $2 `, hid, mitigationName); err != nil { return affected, fmt.Errorf("upgrade mitigation: %w", err) } affected++ } } return affected, nil }