package iace import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Store handles IACE data persistence using PostgreSQL type Store struct { pool *pgxpool.Pool } // NewStore creates a new IACE store func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // ============================================================================ // Project CRUD Operations // ============================================================================ // CreateProject creates a new IACE project func (s *Store) CreateProject(ctx context.Context, tenantID uuid.UUID, req CreateProjectRequest) (*Project, error) { project := &Project{ ID: uuid.New(), TenantID: tenantID, MachineName: req.MachineName, MachineType: req.MachineType, Manufacturer: req.Manufacturer, Description: req.Description, NarrativeText: req.NarrativeText, Status: ProjectStatusDraft, CEMarkingTarget: req.CEMarkingTarget, Metadata: req.Metadata, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_projects ( id, tenant_id, machine_name, machine_type, manufacturer, description, narrative_text, status, ce_marking_target, completeness_score, risk_summary, triggered_regulations, metadata, created_at, updated_at, archived_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 ) `, project.ID, project.TenantID, project.MachineName, project.MachineType, project.Manufacturer, project.Description, project.NarrativeText, string(project.Status), project.CEMarkingTarget, project.CompletenessScore, nil, project.TriggeredRegulations, project.Metadata, project.CreatedAt, project.UpdatedAt, project.ArchivedAt, ) if err != nil { return nil, fmt.Errorf("create project: %w", err) } return project, nil } // GetProject retrieves a project by ID func (s *Store) GetProject(ctx context.Context, id uuid.UUID) (*Project, error) { var p Project var status string var riskSummary, triggeredRegulations, metadata []byte err := s.pool.QueryRow(ctx, ` SELECT id, tenant_id, machine_name, machine_type, manufacturer, description, narrative_text, status, ce_marking_target, completeness_score, risk_summary, triggered_regulations, metadata, created_at, updated_at, archived_at FROM iace_projects WHERE id = $1 `, id).Scan( &p.ID, &p.TenantID, &p.MachineName, &p.MachineType, &p.Manufacturer, &p.Description, &p.NarrativeText, &status, &p.CEMarkingTarget, &p.CompletenessScore, &riskSummary, &triggeredRegulations, &metadata, &p.CreatedAt, &p.UpdatedAt, &p.ArchivedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get project: %w", err) } p.Status = ProjectStatus(status) json.Unmarshal(riskSummary, &p.RiskSummary) json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations) json.Unmarshal(metadata, &p.Metadata) return &p, nil } // ListProjects lists all projects for a tenant func (s *Store) ListProjects(ctx context.Context, tenantID uuid.UUID) ([]Project, error) { rows, err := s.pool.Query(ctx, ` SELECT id, tenant_id, machine_name, machine_type, manufacturer, description, narrative_text, status, ce_marking_target, completeness_score, risk_summary, triggered_regulations, metadata, created_at, updated_at, archived_at FROM iace_projects WHERE tenant_id = $1 ORDER BY created_at DESC `, tenantID) if err != nil { return nil, fmt.Errorf("list projects: %w", err) } defer rows.Close() var projects []Project for rows.Next() { var p Project var status string var riskSummary, triggeredRegulations, metadata []byte err := rows.Scan( &p.ID, &p.TenantID, &p.MachineName, &p.MachineType, &p.Manufacturer, &p.Description, &p.NarrativeText, &status, &p.CEMarkingTarget, &p.CompletenessScore, &riskSummary, &triggeredRegulations, &metadata, &p.CreatedAt, &p.UpdatedAt, &p.ArchivedAt, ) if err != nil { return nil, fmt.Errorf("list projects scan: %w", err) } p.Status = ProjectStatus(status) json.Unmarshal(riskSummary, &p.RiskSummary) json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations) json.Unmarshal(metadata, &p.Metadata) projects = append(projects, p) } return projects, nil } // UpdateProject updates an existing project's mutable fields func (s *Store) UpdateProject(ctx context.Context, id uuid.UUID, req UpdateProjectRequest) (*Project, error) { // Fetch current project first project, err := s.GetProject(ctx, id) if err != nil { return nil, err } if project == nil { return nil, nil } // Apply partial updates if req.MachineName != nil { project.MachineName = *req.MachineName } if req.MachineType != nil { project.MachineType = *req.MachineType } if req.Manufacturer != nil { project.Manufacturer = *req.Manufacturer } if req.Description != nil { project.Description = *req.Description } if req.NarrativeText != nil { project.NarrativeText = *req.NarrativeText } if req.CEMarkingTarget != nil { project.CEMarkingTarget = *req.CEMarkingTarget } if req.Metadata != nil { project.Metadata = *req.Metadata } project.UpdatedAt = time.Now().UTC() _, err = s.pool.Exec(ctx, ` UPDATE iace_projects SET machine_name = $2, machine_type = $3, manufacturer = $4, description = $5, narrative_text = $6, ce_marking_target = $7, metadata = $8, updated_at = $9 WHERE id = $1 `, id, project.MachineName, project.MachineType, project.Manufacturer, project.Description, project.NarrativeText, project.CEMarkingTarget, project.Metadata, project.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("update project: %w", err) } return project, nil } // ArchiveProject sets the archived_at timestamp and status for a project func (s *Store) ArchiveProject(ctx context.Context, id uuid.UUID) error { now := time.Now().UTC() _, err := s.pool.Exec(ctx, ` UPDATE iace_projects SET status = $2, archived_at = $3, updated_at = $3 WHERE id = $1 `, id, string(ProjectStatusArchived), now) if err != nil { return fmt.Errorf("archive project: %w", err) } return nil } // UpdateProjectStatus updates the lifecycle status of a project func (s *Store) UpdateProjectStatus(ctx context.Context, id uuid.UUID, status ProjectStatus) error { _, err := s.pool.Exec(ctx, ` UPDATE iace_projects SET status = $2, updated_at = NOW() WHERE id = $1 `, id, string(status)) if err != nil { return fmt.Errorf("update project status: %w", err) } return nil } // UpdateProjectCompleteness updates the completeness score and risk summary func (s *Store) UpdateProjectCompleteness(ctx context.Context, id uuid.UUID, score float64, riskSummary map[string]int) error { riskSummaryJSON, err := json.Marshal(riskSummary) if err != nil { return fmt.Errorf("marshal risk summary: %w", err) } _, err = s.pool.Exec(ctx, ` UPDATE iace_projects SET completeness_score = $2, risk_summary = $3, updated_at = NOW() WHERE id = $1 `, id, score, riskSummaryJSON) if err != nil { return fmt.Errorf("update project completeness: %w", err) } return nil } // ============================================================================ // Component CRUD Operations // ============================================================================ // CreateComponent creates a new component within a project func (s *Store) CreateComponent(ctx context.Context, req CreateComponentRequest) (*Component, error) { comp := &Component{ ID: uuid.New(), ProjectID: req.ProjectID, ParentID: req.ParentID, Name: req.Name, ComponentType: req.ComponentType, Version: req.Version, Description: req.Description, IsSafetyRelevant: req.IsSafetyRelevant, IsNetworked: req.IsNetworked, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_components ( id, project_id, parent_id, name, component_type, version, description, is_safety_relevant, is_networked, metadata, sort_order, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) `, comp.ID, comp.ProjectID, comp.ParentID, comp.Name, string(comp.ComponentType), comp.Version, comp.Description, comp.IsSafetyRelevant, comp.IsNetworked, comp.Metadata, comp.SortOrder, comp.CreatedAt, comp.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create component: %w", err) } return comp, nil } // GetComponent retrieves a component by ID func (s *Store) GetComponent(ctx context.Context, id uuid.UUID) (*Component, error) { var c Component var compType string var metadata []byte err := s.pool.QueryRow(ctx, ` SELECT id, project_id, parent_id, name, component_type, version, description, is_safety_relevant, is_networked, metadata, sort_order, created_at, updated_at FROM iace_components WHERE id = $1 `, id).Scan( &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType, &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked, &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get component: %w", err) } c.ComponentType = ComponentType(compType) json.Unmarshal(metadata, &c.Metadata) return &c, nil } // ListComponents lists all components for a project func (s *Store) ListComponents(ctx context.Context, projectID uuid.UUID) ([]Component, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, parent_id, name, component_type, version, description, is_safety_relevant, is_networked, metadata, sort_order, created_at, updated_at FROM iace_components WHERE project_id = $1 ORDER BY sort_order ASC, created_at ASC `, projectID) if err != nil { return nil, fmt.Errorf("list components: %w", err) } defer rows.Close() var components []Component for rows.Next() { var c Component var compType string var metadata []byte err := rows.Scan( &c.ID, &c.ProjectID, &c.ParentID, &c.Name, &compType, &c.Version, &c.Description, &c.IsSafetyRelevant, &c.IsNetworked, &metadata, &c.SortOrder, &c.CreatedAt, &c.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list components scan: %w", err) } c.ComponentType = ComponentType(compType) json.Unmarshal(metadata, &c.Metadata) components = append(components, c) } return components, nil } // UpdateComponent updates a component with a dynamic set of fields func (s *Store) UpdateComponent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Component, error) { if len(updates) == 0 { return s.GetComponent(ctx, id) } query := "UPDATE iace_components SET updated_at = NOW()" args := []interface{}{id} argIdx := 2 for key, val := range updates { switch key { case "name", "version", "description": query += fmt.Sprintf(", %s = $%d", key, argIdx) args = append(args, val) argIdx++ case "component_type": query += fmt.Sprintf(", component_type = $%d", argIdx) args = append(args, val) argIdx++ case "is_safety_relevant": query += fmt.Sprintf(", is_safety_relevant = $%d", argIdx) args = append(args, val) argIdx++ case "is_networked": query += fmt.Sprintf(", is_networked = $%d", argIdx) args = append(args, val) argIdx++ case "sort_order": query += fmt.Sprintf(", sort_order = $%d", argIdx) args = append(args, val) argIdx++ case "metadata": metaJSON, _ := json.Marshal(val) query += fmt.Sprintf(", metadata = $%d", argIdx) args = append(args, metaJSON) argIdx++ case "parent_id": query += fmt.Sprintf(", parent_id = $%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 component: %w", err) } return s.GetComponent(ctx, id) } // DeleteComponent deletes a component by ID func (s *Store) DeleteComponent(ctx context.Context, id uuid.UUID) error { _, err := s.pool.Exec(ctx, "DELETE FROM iace_components WHERE id = $1", id) if err != nil { return fmt.Errorf("delete component: %w", err) } return nil } // ============================================================================ // Classification Operations // ============================================================================ // UpsertClassification inserts or updates a regulatory classification for a project func (s *Store) UpsertClassification(ctx context.Context, projectID uuid.UUID, regulation RegulationType, result string, riskLevel string, confidence float64, reasoning string, ragSources, requirements json.RawMessage) (*RegulatoryClassification, error) { id := uuid.New() now := time.Now().UTC() _, err := s.pool.Exec(ctx, ` INSERT INTO iace_classifications ( id, project_id, regulation, classification_result, risk_level, confidence, reasoning, rag_sources, requirements, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 ) ON CONFLICT (project_id, regulation) DO UPDATE SET classification_result = EXCLUDED.classification_result, risk_level = EXCLUDED.risk_level, confidence = EXCLUDED.confidence, reasoning = EXCLUDED.reasoning, rag_sources = EXCLUDED.rag_sources, requirements = EXCLUDED.requirements, updated_at = EXCLUDED.updated_at `, id, projectID, string(regulation), result, riskLevel, confidence, reasoning, ragSources, requirements, now, now, ) if err != nil { return nil, fmt.Errorf("upsert classification: %w", err) } // Retrieve the upserted row (may have kept the original ID on conflict) return s.getClassificationByProjectAndRegulation(ctx, projectID, regulation) } // getClassificationByProjectAndRegulation is a helper to fetch a single classification func (s *Store) getClassificationByProjectAndRegulation(ctx context.Context, projectID uuid.UUID, regulation RegulationType) (*RegulatoryClassification, error) { var c RegulatoryClassification var reg, rl string var ragSources, requirements []byte err := s.pool.QueryRow(ctx, ` SELECT id, project_id, regulation, classification_result, risk_level, confidence, reasoning, rag_sources, requirements, created_at, updated_at FROM iace_classifications WHERE project_id = $1 AND regulation = $2 `, projectID, string(regulation)).Scan( &c.ID, &c.ProjectID, ®, &c.ClassificationResult, &rl, &c.Confidence, &c.Reasoning, &ragSources, &requirements, &c.CreatedAt, &c.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get classification: %w", err) } c.Regulation = RegulationType(reg) c.RiskLevel = RiskLevel(rl) json.Unmarshal(ragSources, &c.RAGSources) json.Unmarshal(requirements, &c.Requirements) return &c, nil } // GetClassifications retrieves all classifications for a project func (s *Store) GetClassifications(ctx context.Context, projectID uuid.UUID) ([]RegulatoryClassification, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, regulation, classification_result, risk_level, confidence, reasoning, rag_sources, requirements, created_at, updated_at FROM iace_classifications WHERE project_id = $1 ORDER BY regulation ASC `, projectID) if err != nil { return nil, fmt.Errorf("get classifications: %w", err) } defer rows.Close() var classifications []RegulatoryClassification for rows.Next() { var c RegulatoryClassification var reg, rl string var ragSources, requirements []byte err := rows.Scan( &c.ID, &c.ProjectID, ®, &c.ClassificationResult, &rl, &c.Confidence, &c.Reasoning, &ragSources, &requirements, &c.CreatedAt, &c.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("get classifications scan: %w", err) } c.Regulation = RegulationType(reg) c.RiskLevel = RiskLevel(rl) json.Unmarshal(ragSources, &c.RAGSources) json.Unmarshal(requirements, &c.Requirements) classifications = append(classifications, c) } return classifications, nil } // ============================================================================ // Hazard CRUD Operations // ============================================================================ // CreateHazard creates a new hazard within a project func (s *Store) CreateHazard(ctx context.Context, req CreateHazardRequest) (*Hazard, error) { h := &Hazard{ ID: uuid.New(), ProjectID: req.ProjectID, ComponentID: req.ComponentID, LibraryHazardID: req.LibraryHazardID, Name: req.Name, Description: req.Description, Scenario: req.Scenario, Category: req.Category, Status: HazardStatusIdentified, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_hazards ( id, project_id, component_id, library_hazard_id, name, description, scenario, category, status, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 ) `, h.ID, h.ProjectID, h.ComponentID, h.LibraryHazardID, h.Name, h.Description, h.Scenario, h.Category, string(h.Status), h.CreatedAt, h.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create hazard: %w", err) } return h, nil } // GetHazard retrieves a hazard by ID func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) { var h Hazard var status string err := s.pool.QueryRow(ctx, ` SELECT id, project_id, component_id, library_hazard_id, name, description, scenario, category, status, created_at, updated_at FROM iace_hazards WHERE id = $1 `, id).Scan( &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID, &h.Name, &h.Description, &h.Scenario, &h.Category, &status, &h.CreatedAt, &h.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get hazard: %w", err) } h.Status = HazardStatus(status) return &h, nil } // ListHazards lists all hazards for a project func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, component_id, library_hazard_id, name, description, scenario, category, status, created_at, updated_at FROM iace_hazards WHERE project_id = $1 ORDER BY created_at ASC `, projectID) if err != nil { return nil, fmt.Errorf("list hazards: %w", err) } defer rows.Close() var hazards []Hazard for rows.Next() { var h Hazard var status string err := rows.Scan( &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID, &h.Name, &h.Description, &h.Scenario, &h.Category, &status, &h.CreatedAt, &h.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list hazards scan: %w", err) } h.Status = HazardStatus(status) hazards = append(hazards, h) } return hazards, nil } // UpdateHazard updates a hazard with a dynamic set of fields func (s *Store) UpdateHazard(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*Hazard, error) { if len(updates) == 0 { return s.GetHazard(ctx, id) } query := "UPDATE iace_hazards SET updated_at = NOW()" args := []interface{}{id} argIdx := 2 for key, val := range updates { switch key { case "name", "description", "scenario", "category": 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 "component_id": query += fmt.Sprintf(", component_id = $%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 hazard: %w", err) } return s.GetHazard(ctx, id) } // ============================================================================ // Risk Assessment Operations // ============================================================================ // CreateRiskAssessment creates a new risk assessment for a hazard func (s *Store) CreateRiskAssessment(ctx context.Context, assessment *RiskAssessment) error { if assessment.ID == uuid.Nil { assessment.ID = uuid.New() } if assessment.CreatedAt.IsZero() { assessment.CreatedAt = time.Now().UTC() } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_risk_assessments ( id, hazard_id, version, assessment_type, severity, exposure, probability, inherent_risk, control_maturity, control_coverage, test_evidence_strength, c_eff, residual_risk, risk_level, is_acceptable, acceptance_justification, assessed_by, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) `, assessment.ID, assessment.HazardID, assessment.Version, string(assessment.AssessmentType), assessment.Severity, assessment.Exposure, assessment.Probability, assessment.InherentRisk, assessment.ControlMaturity, assessment.ControlCoverage, assessment.TestEvidenceStrength, assessment.CEff, assessment.ResidualRisk, string(assessment.RiskLevel), assessment.IsAcceptable, assessment.AcceptanceJustification, assessment.AssessedBy, assessment.CreatedAt, ) if err != nil { return fmt.Errorf("create risk assessment: %w", err) } return nil } // GetLatestAssessment retrieves the most recent risk assessment for a hazard func (s *Store) GetLatestAssessment(ctx context.Context, hazardID uuid.UUID) (*RiskAssessment, error) { var a RiskAssessment var assessmentType, riskLevel string err := s.pool.QueryRow(ctx, ` SELECT id, hazard_id, version, assessment_type, severity, exposure, probability, inherent_risk, control_maturity, control_coverage, test_evidence_strength, c_eff, residual_risk, risk_level, is_acceptable, acceptance_justification, assessed_by, created_at FROM iace_risk_assessments WHERE hazard_id = $1 ORDER BY version DESC, created_at DESC LIMIT 1 `, hazardID).Scan( &a.ID, &a.HazardID, &a.Version, &assessmentType, &a.Severity, &a.Exposure, &a.Probability, &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage, &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk, &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification, &a.AssessedBy, &a.CreatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get latest assessment: %w", err) } a.AssessmentType = AssessmentType(assessmentType) a.RiskLevel = RiskLevel(riskLevel) return &a, nil } // ListAssessments lists all risk assessments for a hazard, newest first func (s *Store) ListAssessments(ctx context.Context, hazardID uuid.UUID) ([]RiskAssessment, error) { rows, err := s.pool.Query(ctx, ` SELECT id, hazard_id, version, assessment_type, severity, exposure, probability, inherent_risk, control_maturity, control_coverage, test_evidence_strength, c_eff, residual_risk, risk_level, is_acceptable, acceptance_justification, assessed_by, created_at FROM iace_risk_assessments WHERE hazard_id = $1 ORDER BY version DESC, created_at DESC `, hazardID) if err != nil { return nil, fmt.Errorf("list assessments: %w", err) } defer rows.Close() var assessments []RiskAssessment for rows.Next() { var a RiskAssessment var assessmentType, riskLevel string err := rows.Scan( &a.ID, &a.HazardID, &a.Version, &assessmentType, &a.Severity, &a.Exposure, &a.Probability, &a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage, &a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk, &riskLevel, &a.IsAcceptable, &a.AcceptanceJustification, &a.AssessedBy, &a.CreatedAt, ) if err != nil { return nil, fmt.Errorf("list assessments scan: %w", err) } a.AssessmentType = AssessmentType(assessmentType) a.RiskLevel = RiskLevel(riskLevel) assessments = append(assessments, a) } return assessments, nil } // ============================================================================ // 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 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 } // ============================================================================ // Tech File Section Operations // ============================================================================ // CreateTechFileSection creates a new section in the technical file func (s *Store) CreateTechFileSection(ctx context.Context, projectID uuid.UUID, sectionType, title, content string) (*TechFileSection, error) { tf := &TechFileSection{ ID: uuid.New(), ProjectID: projectID, SectionType: sectionType, Title: title, Content: content, Version: 1, Status: TechFileSectionStatusDraft, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_tech_file_sections ( id, project_id, section_type, title, content, version, status, approved_by, approved_at, metadata, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) `, tf.ID, tf.ProjectID, tf.SectionType, tf.Title, tf.Content, tf.Version, string(tf.Status), uuid.Nil, nil, nil, tf.CreatedAt, tf.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create tech file section: %w", err) } return tf, nil } // UpdateTechFileSection updates the content of a tech file section and bumps version func (s *Store) UpdateTechFileSection(ctx context.Context, id uuid.UUID, content string) error { _, err := s.pool.Exec(ctx, ` UPDATE iace_tech_file_sections SET content = $2, version = version + 1, status = $3, updated_at = NOW() WHERE id = $1 `, id, content, string(TechFileSectionStatusDraft)) if err != nil { return fmt.Errorf("update tech file section: %w", err) } return nil } // ApproveTechFileSection marks a tech file section as approved func (s *Store) ApproveTechFileSection(ctx context.Context, id uuid.UUID, approvedBy string) error { now := time.Now().UTC() approvedByUUID, err := uuid.Parse(approvedBy) if err != nil { return fmt.Errorf("invalid approved_by UUID: %w", err) } _, err = s.pool.Exec(ctx, ` UPDATE iace_tech_file_sections SET status = $2, approved_by = $3, approved_at = $4, updated_at = $4 WHERE id = $1 `, id, string(TechFileSectionStatusApproved), approvedByUUID, now) if err != nil { return fmt.Errorf("approve tech file section: %w", err) } return nil } // ListTechFileSections lists all tech file sections for a project func (s *Store) ListTechFileSections(ctx context.Context, projectID uuid.UUID) ([]TechFileSection, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, section_type, title, content, version, status, approved_by, approved_at, metadata, created_at, updated_at FROM iace_tech_file_sections WHERE project_id = $1 ORDER BY section_type ASC, created_at ASC `, projectID) if err != nil { return nil, fmt.Errorf("list tech file sections: %w", err) } defer rows.Close() var sections []TechFileSection for rows.Next() { var tf TechFileSection var status string var metadata []byte err := rows.Scan( &tf.ID, &tf.ProjectID, &tf.SectionType, &tf.Title, &tf.Content, &tf.Version, &status, &tf.ApprovedBy, &tf.ApprovedAt, &metadata, &tf.CreatedAt, &tf.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list tech file sections scan: %w", err) } tf.Status = TechFileSectionStatus(status) json.Unmarshal(metadata, &tf.Metadata) sections = append(sections, tf) } return sections, nil } // ============================================================================ // Monitoring Event Operations // ============================================================================ // CreateMonitoringEvent creates a new post-market monitoring event func (s *Store) CreateMonitoringEvent(ctx context.Context, req CreateMonitoringEventRequest) (*MonitoringEvent, error) { me := &MonitoringEvent{ ID: uuid.New(), ProjectID: req.ProjectID, EventType: req.EventType, Title: req.Title, Description: req.Description, Severity: req.Severity, Status: "open", CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_monitoring_events ( id, project_id, event_type, title, description, severity, impact_assessment, status, resolved_at, resolved_by, metadata, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) `, me.ID, me.ProjectID, string(me.EventType), me.Title, me.Description, me.Severity, "", me.Status, nil, uuid.Nil, nil, me.CreatedAt, me.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create monitoring event: %w", err) } return me, nil } // ListMonitoringEvents lists all monitoring events for a project func (s *Store) ListMonitoringEvents(ctx context.Context, projectID uuid.UUID) ([]MonitoringEvent, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, event_type, title, description, severity, impact_assessment, status, resolved_at, resolved_by, metadata, created_at, updated_at FROM iace_monitoring_events WHERE project_id = $1 ORDER BY created_at DESC `, projectID) if err != nil { return nil, fmt.Errorf("list monitoring events: %w", err) } defer rows.Close() var events []MonitoringEvent for rows.Next() { var me MonitoringEvent var eventType string var metadata []byte err := rows.Scan( &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description, &me.Severity, &me.ImpactAssessment, &me.Status, &me.ResolvedAt, &me.ResolvedBy, &metadata, &me.CreatedAt, &me.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list monitoring events scan: %w", err) } me.EventType = MonitoringEventType(eventType) json.Unmarshal(metadata, &me.Metadata) events = append(events, me) } return events, nil } // UpdateMonitoringEvent updates a monitoring event with a dynamic set of fields func (s *Store) UpdateMonitoringEvent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*MonitoringEvent, error) { if len(updates) == 0 { return s.getMonitoringEvent(ctx, id) } query := "UPDATE iace_monitoring_events SET updated_at = NOW()" args := []interface{}{id} argIdx := 2 for key, val := range updates { switch key { case "title", "description", "severity", "impact_assessment", "status": query += fmt.Sprintf(", %s = $%d", key, argIdx) args = append(args, val) argIdx++ case "event_type": query += fmt.Sprintf(", event_type = $%d", argIdx) args = append(args, val) argIdx++ case "resolved_at": query += fmt.Sprintf(", resolved_at = $%d", argIdx) args = append(args, val) argIdx++ case "resolved_by": query += fmt.Sprintf(", resolved_by = $%d", argIdx) args = append(args, val) argIdx++ case "metadata": metaJSON, _ := json.Marshal(val) query += fmt.Sprintf(", metadata = $%d", argIdx) args = append(args, metaJSON) argIdx++ } } query += " WHERE id = $1" _, err := s.pool.Exec(ctx, query, args...) if err != nil { return nil, fmt.Errorf("update monitoring event: %w", err) } return s.getMonitoringEvent(ctx, id) } // getMonitoringEvent is a helper to fetch a single monitoring event by ID func (s *Store) getMonitoringEvent(ctx context.Context, id uuid.UUID) (*MonitoringEvent, error) { var me MonitoringEvent var eventType string var metadata []byte err := s.pool.QueryRow(ctx, ` SELECT id, project_id, event_type, title, description, severity, impact_assessment, status, resolved_at, resolved_by, metadata, created_at, updated_at FROM iace_monitoring_events WHERE id = $1 `, id).Scan( &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description, &me.Severity, &me.ImpactAssessment, &me.Status, &me.ResolvedAt, &me.ResolvedBy, &metadata, &me.CreatedAt, &me.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get monitoring event: %w", err) } me.EventType = MonitoringEventType(eventType) json.Unmarshal(metadata, &me.Metadata) return &me, nil } // ============================================================================ // Audit Trail Operations // ============================================================================ // AddAuditEntry adds an immutable audit trail entry func (s *Store) AddAuditEntry(ctx context.Context, projectID uuid.UUID, entityType string, entityID uuid.UUID, action AuditAction, userID string, oldValues, newValues json.RawMessage) error { id := uuid.New() now := time.Now().UTC() userUUID, err := uuid.Parse(userID) if err != nil { return fmt.Errorf("invalid user_id UUID: %w", err) } // Compute a simple hash for integrity: sha256(entityType + entityID + action + timestamp) hashInput := fmt.Sprintf("%s:%s:%s:%s:%s", projectID, entityType, entityID, string(action), now.Format(time.RFC3339Nano)) // Use a simple deterministic hash representation hash := fmt.Sprintf("%x", hashInput) _, err = s.pool.Exec(ctx, ` INSERT INTO iace_audit_trail ( id, project_id, entity_type, entity_id, action, user_id, old_values, new_values, hash, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 ) `, id, projectID, entityType, entityID, string(action), userUUID, oldValues, newValues, hash, now, ) if err != nil { return fmt.Errorf("add audit entry: %w", err) } return nil } // ListAuditTrail lists all audit trail entries for a project, newest first func (s *Store) ListAuditTrail(ctx context.Context, projectID uuid.UUID) ([]AuditTrailEntry, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, entity_type, entity_id, action, user_id, old_values, new_values, hash, created_at FROM iace_audit_trail WHERE project_id = $1 ORDER BY created_at DESC `, projectID) if err != nil { return nil, fmt.Errorf("list audit trail: %w", err) } defer rows.Close() var entries []AuditTrailEntry for rows.Next() { var e AuditTrailEntry var action string err := rows.Scan( &e.ID, &e.ProjectID, &e.EntityType, &e.EntityID, &action, &e.UserID, &e.OldValues, &e.NewValues, &e.Hash, &e.CreatedAt, ) if err != nil { return nil, fmt.Errorf("list audit trail scan: %w", err) } e.Action = AuditAction(action) entries = append(entries, e) } return entries, nil } // ============================================================================ // Hazard Library Operations // ============================================================================ // ListHazardLibrary lists hazard library entries, optionally filtered by category and component type func (s *Store) ListHazardLibrary(ctx context.Context, category string, componentType string) ([]HazardLibraryEntry, error) { query := ` SELECT id, category, name, description, default_severity, default_probability, applicable_component_types, regulation_references, suggested_mitigations, is_builtin, tenant_id, created_at FROM iace_hazard_library WHERE 1=1` args := []interface{}{} argIdx := 1 if category != "" { query += fmt.Sprintf(" AND category = $%d", argIdx) args = append(args, category) argIdx++ } if componentType != "" { query += fmt.Sprintf(" AND applicable_component_types @> $%d::jsonb", argIdx) componentTypeJSON, _ := json.Marshal([]string{componentType}) args = append(args, string(componentTypeJSON)) argIdx++ } query += " ORDER BY category ASC, name ASC" rows, err := s.pool.Query(ctx, query, args...) if err != nil { return nil, fmt.Errorf("list hazard library: %w", err) } defer rows.Close() var entries []HazardLibraryEntry for rows.Next() { var e HazardLibraryEntry var applicableComponentTypes, regulationReferences, suggestedMitigations []byte err := rows.Scan( &e.ID, &e.Category, &e.Name, &e.Description, &e.DefaultSeverity, &e.DefaultProbability, &applicableComponentTypes, ®ulationReferences, &suggestedMitigations, &e.IsBuiltin, &e.TenantID, &e.CreatedAt, ) if err != nil { return nil, fmt.Errorf("list hazard library scan: %w", err) } json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes) json.Unmarshal(regulationReferences, &e.RegulationReferences) json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations) if e.ApplicableComponentTypes == nil { e.ApplicableComponentTypes = []string{} } if e.RegulationReferences == nil { e.RegulationReferences = []string{} } entries = append(entries, e) } return entries, nil } // GetHazardLibraryEntry retrieves a single hazard library entry by ID func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*HazardLibraryEntry, error) { var e HazardLibraryEntry var applicableComponentTypes, regulationReferences, suggestedMitigations []byte err := s.pool.QueryRow(ctx, ` SELECT id, category, name, description, default_severity, default_probability, applicable_component_types, regulation_references, suggested_mitigations, is_builtin, tenant_id, created_at FROM iace_hazard_library WHERE id = $1 `, id).Scan( &e.ID, &e.Category, &e.Name, &e.Description, &e.DefaultSeverity, &e.DefaultProbability, &applicableComponentTypes, ®ulationReferences, &suggestedMitigations, &e.IsBuiltin, &e.TenantID, &e.CreatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get hazard library entry: %w", err) } json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes) json.Unmarshal(regulationReferences, &e.RegulationReferences) json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations) if e.ApplicableComponentTypes == nil { e.ApplicableComponentTypes = []string{} } if e.RegulationReferences == nil { e.RegulationReferences = []string{} } return &e, nil } // ============================================================================ // Risk Summary (Aggregated View) // ============================================================================ // GetRiskSummary computes an aggregated risk overview for a project func (s *Store) GetRiskSummary(ctx context.Context, projectID uuid.UUID) (*RiskSummaryResponse, error) { // Get all hazards for the project hazards, err := s.ListHazards(ctx, projectID) if err != nil { return nil, fmt.Errorf("get risk summary - list hazards: %w", err) } summary := &RiskSummaryResponse{ TotalHazards: len(hazards), AllAcceptable: true, } if len(hazards) == 0 { summary.OverallRiskLevel = RiskLevelNegligible return summary, nil } highestRisk := RiskLevelNegligible for _, h := range hazards { latest, err := s.GetLatestAssessment(ctx, h.ID) if err != nil { return nil, fmt.Errorf("get risk summary - get assessment for hazard %s: %w", h.ID, err) } if latest == nil { // Hazard without assessment counts as unassessed; consider it not acceptable summary.AllAcceptable = false continue } switch latest.RiskLevel { case RiskLevelCritical: summary.Critical++ case RiskLevelHigh: summary.High++ case RiskLevelMedium: summary.Medium++ case RiskLevelLow: summary.Low++ case RiskLevelNegligible: summary.Negligible++ } if !latest.IsAcceptable { summary.AllAcceptable = false } // Track highest risk level if riskLevelSeverity(latest.RiskLevel) > riskLevelSeverity(highestRisk) { highestRisk = latest.RiskLevel } } summary.OverallRiskLevel = highestRisk return summary, nil } // riskLevelSeverity returns a numeric severity for risk level comparison func riskLevelSeverity(rl RiskLevel) int { switch rl { case RiskLevelCritical: return 5 case RiskLevelHigh: return 4 case RiskLevelMedium: return 3 case RiskLevelLow: return 2 case RiskLevelNegligible: return 1 default: return 0 } }