Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/store_mitigations.go
Sharang Parnerkar 9f96061631 refactor(go): split training/store, ucca/rules, ucca_handlers, document_export under 500 LOC
Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC,
ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group
into same-package files, all under the 500-line hard cap. Zero behavior changes,
no renamed exported symbols. Also fixed pre-existing hazard_library split (missing
functions and duplicate UUID keys from a prior session).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 09:29:54 +02:00

507 lines
14 KiB
Go

package iace
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// ============================================================================
// Mitigation CRUD Operations
// ============================================================================
// CreateMitigation creates a new mitigation measure for a hazard
func (s *Store) CreateMitigation(ctx context.Context, req CreateMitigationRequest) (*Mitigation, error) {
m := &Mitigation{
ID: uuid.New(),
HazardID: req.HazardID,
ReductionType: req.ReductionType,
Name: req.Name,
Description: req.Description,
Status: MitigationStatusPlanned,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
_, err := s.pool.Exec(ctx, `
INSERT INTO iace_mitigations (
id, hazard_id, reduction_type, name, description,
status, verification_method, verification_result,
verified_at, verified_by,
created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
$9, $10,
$11, $12
)
`,
m.ID, m.HazardID, string(m.ReductionType), m.Name, m.Description,
string(m.Status), "", "",
nil, uuid.Nil,
m.CreatedAt, m.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create mitigation: %w", err)
}
return m, nil
}
// UpdateMitigation updates a mitigation with a dynamic set of fields
func (s *Store) UpdateMitigation(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Mitigation, error) {
if len(updates) == 0 {
return s.getMitigation(ctx, id)
}
query := "UPDATE iace_mitigations SET updated_at = NOW()"
args := []interface{}{id}
argIdx := 2
for key, val := range updates {
switch key {
case "name", "description", "verification_result":
query += fmt.Sprintf(", %s = $%d", key, argIdx)
args = append(args, val)
argIdx++
case "status":
query += fmt.Sprintf(", status = $%d", argIdx)
args = append(args, val)
argIdx++
case "reduction_type":
query += fmt.Sprintf(", reduction_type = $%d", argIdx)
args = append(args, val)
argIdx++
case "verification_method":
query += fmt.Sprintf(", verification_method = $%d", argIdx)
args = append(args, val)
argIdx++
}
}
query += " WHERE id = $1"
_, err := s.pool.Exec(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("update mitigation: %w", err)
}
return s.getMitigation(ctx, id)
}
// VerifyMitigation marks a mitigation as verified
func (s *Store) VerifyMitigation(ctx context.Context, id uuid.UUID, verificationResult string, verifiedBy string) error {
now := time.Now().UTC()
verifiedByUUID, err := uuid.Parse(verifiedBy)
if err != nil {
return fmt.Errorf("invalid verified_by UUID: %w", err)
}
_, err = s.pool.Exec(ctx, `
UPDATE iace_mitigations SET
status = $2,
verification_result = $3,
verified_at = $4,
verified_by = $5,
updated_at = $4
WHERE id = $1
`, id, string(MitigationStatusVerified), verificationResult, now, verifiedByUUID)
if err != nil {
return fmt.Errorf("verify mitigation: %w", err)
}
return nil
}
// ListMitigations lists all mitigations for a hazard
func (s *Store) ListMitigations(ctx context.Context, hazardID uuid.UUID) ([]Mitigation, error) {
rows, err := s.pool.Query(ctx, `
SELECT
id, hazard_id, reduction_type, name, description,
status, verification_method, verification_result,
verified_at, verified_by,
created_at, updated_at
FROM iace_mitigations WHERE hazard_id = $1
ORDER BY created_at ASC
`, hazardID)
if err != nil {
return nil, fmt.Errorf("list mitigations: %w", err)
}
defer rows.Close()
var mitigations []Mitigation
for rows.Next() {
var m Mitigation
var reductionType, status, verificationMethod string
err := rows.Scan(
&m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
&status, &verificationMethod, &m.VerificationResult,
&m.VerifiedAt, &m.VerifiedBy,
&m.CreatedAt, &m.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("list mitigations scan: %w", err)
}
m.ReductionType = ReductionType(reductionType)
m.Status = MitigationStatus(status)
m.VerificationMethod = VerificationMethod(verificationMethod)
mitigations = append(mitigations, m)
}
return mitigations, nil
}
// GetMitigation fetches a single mitigation by ID.
func (s *Store) GetMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, error) {
return s.getMitigation(ctx, id)
}
// getMitigation is a helper to fetch a single mitigation by ID
func (s *Store) getMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, error) {
var m Mitigation
var reductionType, status, verificationMethod string
err := s.pool.QueryRow(ctx, `
SELECT
id, hazard_id, reduction_type, name, description,
status, verification_method, verification_result,
verified_at, verified_by,
created_at, updated_at
FROM iace_mitigations WHERE id = $1
`, id).Scan(
&m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
&status, &verificationMethod, &m.VerificationResult,
&m.VerifiedAt, &m.VerifiedBy,
&m.CreatedAt, &m.UpdatedAt,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get mitigation: %w", err)
}
m.ReductionType = ReductionType(reductionType)
m.Status = MitigationStatus(status)
m.VerificationMethod = VerificationMethod(verificationMethod)
return &m, nil
}
// ============================================================================
// Evidence Operations
// ============================================================================
// CreateEvidence creates a new evidence record
func (s *Store) CreateEvidence(ctx context.Context, evidence *Evidence) error {
if evidence.ID == uuid.Nil {
evidence.ID = uuid.New()
}
if evidence.CreatedAt.IsZero() {
evidence.CreatedAt = time.Now().UTC()
}
_, err := s.pool.Exec(ctx, `
INSERT INTO iace_evidence (
id, project_id, mitigation_id, verification_plan_id,
file_name, file_path, file_hash, file_size, mime_type,
description, uploaded_by, created_at
) VALUES (
$1, $2, $3, $4,
$5, $6, $7, $8, $9,
$10, $11, $12
)
`,
evidence.ID, evidence.ProjectID, evidence.MitigationID, evidence.VerificationPlanID,
evidence.FileName, evidence.FilePath, evidence.FileHash, evidence.FileSize, evidence.MimeType,
evidence.Description, evidence.UploadedBy, evidence.CreatedAt,
)
if err != nil {
return fmt.Errorf("create evidence: %w", err)
}
return nil
}
// ListEvidence lists all evidence for a project
func (s *Store) ListEvidence(ctx context.Context, projectID uuid.UUID) ([]Evidence, error) {
rows, err := s.pool.Query(ctx, `
SELECT
id, project_id, mitigation_id, verification_plan_id,
file_name, file_path, file_hash, file_size, mime_type,
description, uploaded_by, created_at
FROM iace_evidence WHERE project_id = $1
ORDER BY created_at DESC
`, projectID)
if err != nil {
return nil, fmt.Errorf("list evidence: %w", err)
}
defer rows.Close()
var evidence []Evidence
for rows.Next() {
var e Evidence
err := rows.Scan(
&e.ID, &e.ProjectID, &e.MitigationID, &e.VerificationPlanID,
&e.FileName, &e.FilePath, &e.FileHash, &e.FileSize, &e.MimeType,
&e.Description, &e.UploadedBy, &e.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("list evidence scan: %w", err)
}
evidence = append(evidence, e)
}
return evidence, nil
}
// ============================================================================
// Verification Plan Operations
// ============================================================================
// CreateVerificationPlan creates a new verification plan
func (s *Store) CreateVerificationPlan(ctx context.Context, req CreateVerificationPlanRequest) (*VerificationPlan, error) {
vp := &VerificationPlan{
ID: uuid.New(),
ProjectID: req.ProjectID,
HazardID: req.HazardID,
MitigationID: req.MitigationID,
Title: req.Title,
Description: req.Description,
AcceptanceCriteria: req.AcceptanceCriteria,
Method: req.Method,
Status: "planned",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
_, err := s.pool.Exec(ctx, `
INSERT INTO iace_verification_plans (
id, project_id, hazard_id, mitigation_id,
title, description, acceptance_criteria, method,
status, result, completed_at, completed_by,
created_at, updated_at
) VALUES (
$1, $2, $3, $4,
$5, $6, $7, $8,
$9, $10, $11, $12,
$13, $14
)
`,
vp.ID, vp.ProjectID, vp.HazardID, vp.MitigationID,
vp.Title, vp.Description, vp.AcceptanceCriteria, string(vp.Method),
vp.Status, "", nil, uuid.Nil,
vp.CreatedAt, vp.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("create verification plan: %w", err)
}
return vp, nil
}
// UpdateVerificationPlan updates a verification plan with a dynamic set of fields
func (s *Store) UpdateVerificationPlan(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*VerificationPlan, error) {
if len(updates) == 0 {
return s.getVerificationPlan(ctx, id)
}
query := "UPDATE iace_verification_plans SET updated_at = NOW()"
args := []interface{}{id}
argIdx := 2
for key, val := range updates {
switch key {
case "title", "description", "acceptance_criteria", "result", "status":
query += fmt.Sprintf(", %s = $%d", key, argIdx)
args = append(args, val)
argIdx++
case "method":
query += fmt.Sprintf(", method = $%d", argIdx)
args = append(args, val)
argIdx++
}
}
query += " WHERE id = $1"
_, err := s.pool.Exec(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("update verification plan: %w", err)
}
return s.getVerificationPlan(ctx, id)
}
// CompleteVerification marks a verification plan as completed
func (s *Store) CompleteVerification(ctx context.Context, id uuid.UUID, result string, completedBy string) error {
now := time.Now().UTC()
completedByUUID, err := uuid.Parse(completedBy)
if err != nil {
return fmt.Errorf("invalid completed_by UUID: %w", err)
}
_, err = s.pool.Exec(ctx, `
UPDATE iace_verification_plans SET
status = 'completed',
result = $2,
completed_at = $3,
completed_by = $4,
updated_at = $3
WHERE id = $1
`, id, result, now, completedByUUID)
if err != nil {
return fmt.Errorf("complete verification: %w", err)
}
return nil
}
// ListVerificationPlans lists all verification plans for a project
func (s *Store) ListVerificationPlans(ctx context.Context, projectID uuid.UUID) ([]VerificationPlan, error) {
rows, err := s.pool.Query(ctx, `
SELECT
id, project_id, hazard_id, mitigation_id,
title, description, acceptance_criteria, method,
status, result, completed_at, completed_by,
created_at, updated_at
FROM iace_verification_plans WHERE project_id = $1
ORDER BY created_at ASC
`, projectID)
if err != nil {
return nil, fmt.Errorf("list verification plans: %w", err)
}
defer rows.Close()
var plans []VerificationPlan
for rows.Next() {
var vp VerificationPlan
var method string
err := rows.Scan(
&vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID,
&vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method,
&vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy,
&vp.CreatedAt, &vp.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("list verification plans scan: %w", err)
}
vp.Method = VerificationMethod(method)
plans = append(plans, vp)
}
return plans, nil
}
// getVerificationPlan is a helper to fetch a single verification plan by ID
func (s *Store) getVerificationPlan(ctx context.Context, id uuid.UUID) (*VerificationPlan, error) {
var vp VerificationPlan
var method string
err := s.pool.QueryRow(ctx, `
SELECT
id, project_id, hazard_id, mitigation_id,
title, description, acceptance_criteria, method,
status, result, completed_at, completed_by,
created_at, updated_at
FROM iace_verification_plans WHERE id = $1
`, id).Scan(
&vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID,
&vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method,
&vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy,
&vp.CreatedAt, &vp.UpdatedAt,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get verification plan: %w", err)
}
vp.Method = VerificationMethod(method)
return &vp, nil
}
// ============================================================================
// Reference Data Operations
// ============================================================================
// ListLifecyclePhases returns all 12 lifecycle phases with DE/EN labels
func (s *Store) ListLifecyclePhases(ctx context.Context) ([]LifecyclePhaseInfo, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, label_de, label_en, sort_order
FROM iace_lifecycle_phases
ORDER BY sort_order ASC
`)
if err != nil {
return nil, fmt.Errorf("list lifecycle phases: %w", err)
}
defer rows.Close()
var phases []LifecyclePhaseInfo
for rows.Next() {
var p LifecyclePhaseInfo
if err := rows.Scan(&p.ID, &p.LabelDE, &p.LabelEN, &p.Sort); err != nil {
return nil, fmt.Errorf("list lifecycle phases scan: %w", err)
}
phases = append(phases, p)
}
return phases, nil
}
// ListRoles returns all affected person roles from the reference table
func (s *Store) ListRoles(ctx context.Context) ([]RoleInfo, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, label_de, label_en, sort_order
FROM iace_roles
ORDER BY sort_order ASC
`)
if err != nil {
return nil, fmt.Errorf("list roles: %w", err)
}
defer rows.Close()
var roles []RoleInfo
for rows.Next() {
var r RoleInfo
if err := rows.Scan(&r.ID, &r.LabelDE, &r.LabelEN, &r.Sort); err != nil {
return nil, fmt.Errorf("list roles scan: %w", err)
}
roles = append(roles, r)
}
return roles, nil
}
// ListEvidenceTypes returns all evidence types from the reference table
func (s *Store) ListEvidenceTypes(ctx context.Context) ([]EvidenceTypeInfo, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, category, label_de, label_en, sort_order
FROM iace_evidence_types
ORDER BY sort_order ASC
`)
if err != nil {
return nil, fmt.Errorf("list evidence types: %w", err)
}
defer rows.Close()
var types []EvidenceTypeInfo
for rows.Next() {
var e EvidenceTypeInfo
if err := rows.Scan(&e.ID, &e.Category, &e.LabelDE, &e.LabelEN, &e.Sort); err != nil {
return nil, fmt.Errorf("list evidence types scan: %w", err)
}
types = append(types, e)
}
return types, nil
}