All oversized iace files now comply with the 500-line hard cap: - hazard_library_ai_sw.go split into ai_sw (false_classification..communication) and ai_fw (unauthorized_access..update_failure) - hazard_library_software_hmi.go split into software_hmi (software_fault+hmi) and config_integration (configuration_error+logging+integration) - hazard_library_machine_safety.go split to keep mechanical/electrical/thermal/emc, safety_functions extracted into hazard_library_safety_functions.go - store_hazards.go split: hazard library queries moved to store_hazard_library.go - store_projects.go split: component and classification ops to store_components.go - store_mitigations.go split: evidence/verification/ref-data to store_evidence.go - hazard_library.go GetBuiltinHazardLibrary() updated to call all sub-functions - All iace tests pass (go test ./internal/iace/...) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
322 lines
9.0 KiB
Go
322 lines
9.0 KiB
Go
package iace
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// ============================================================================
|
|
// 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
|
|
}
|