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, SubCategory: req.SubCategory, Status: HazardStatusIdentified, MachineModule: req.MachineModule, Function: req.Function, LifecyclePhase: req.LifecyclePhase, HazardousZone: req.HazardousZone, TriggerEvent: req.TriggerEvent, AffectedPerson: req.AffectedPerson, PossibleHarm: req.PossibleHarm, ReviewStatus: ReviewStatusDraft, 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, sub_category, status, machine_module, function, lifecycle_phase, hazardous_zone, trigger_event, affected_person, possible_harm, review_status, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20 ) `, h.ID, h.ProjectID, h.ComponentID, h.LibraryHazardID, h.Name, h.Description, h.Scenario, h.Category, h.SubCategory, string(h.Status), h.MachineModule, h.Function, h.LifecyclePhase, h.HazardousZone, h.TriggerEvent, h.AffectedPerson, h.PossibleHarm, string(h.ReviewStatus), 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, reviewStatus string err := s.pool.QueryRow(ctx, ` SELECT id, project_id, component_id, library_hazard_id, name, description, scenario, category, sub_category, status, machine_module, function, lifecycle_phase, hazardous_zone, trigger_event, affected_person, possible_harm, review_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, &h.SubCategory, &status, &h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone, &h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus, &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) h.ReviewStatus = ReviewStatus(reviewStatus) 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, sub_category, status, machine_module, function, lifecycle_phase, hazardous_zone, trigger_event, affected_person, possible_harm, review_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, reviewStatus string err := rows.Scan( &h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID, &h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status, &h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone, &h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus, &h.CreatedAt, &h.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list hazards scan: %w", err) } h.Status = HazardStatus(status) h.ReviewStatus = ReviewStatus(reviewStatus) 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 allowedFields := map[string]bool{ "name": true, "description": true, "scenario": true, "category": true, "sub_category": true, "status": true, "component_id": true, "machine_module": true, "function": true, "lifecycle_phase": true, "hazardous_zone": true, "trigger_event": true, "affected_person": true, "possible_harm": true, "review_status": true, } for key, val := range updates { if allowedFields[key] { query += fmt.Sprintf(", %s = $%d", key, 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 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 } // ============================================================================ // 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 } // HasAuditEntryForType checks if an audit trail entry exists for the given entity type within a project. func (s *Store) HasAuditEntryForType(ctx context.Context, projectID uuid.UUID, entityType string) (bool, error) { var exists bool err := s.pool.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM iace_audit_trail WHERE project_id = $1 AND entity_type = $2 ) `, projectID, entityType).Scan(&exists) if err != nil { return false, fmt.Errorf("has audit entry: %w", err) } return exists, 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, COALESCE(sub_category, ''), name, description, default_severity, default_probability, COALESCE(default_exposure, 3), COALESCE(default_avoidance, 3), applicable_component_types, regulation_references, suggested_mitigations, COALESCE(typical_causes, '[]'::jsonb), COALESCE(typical_harm, ''), COALESCE(relevant_lifecycle_phases, '[]'::jsonb), COALESCE(recommended_measures_design, '[]'::jsonb), COALESCE(recommended_measures_technical, '[]'::jsonb), COALESCE(recommended_measures_information, '[]'::jsonb), COALESCE(suggested_evidence, '[]'::jsonb), COALESCE(related_keywords, '[]'::jsonb), 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 var typicalCauses, relevantPhases, measuresDesign, measuresTechnical, measuresInfo, evidence, keywords []byte err := rows.Scan( &e.ID, &e.Category, &e.SubCategory, &e.Name, &e.Description, &e.DefaultSeverity, &e.DefaultProbability, &e.DefaultExposure, &e.DefaultAvoidance, &applicableComponentTypes, ®ulationReferences, &suggestedMitigations, &typicalCauses, &e.TypicalHarm, &relevantPhases, &measuresDesign, &measuresTechnical, &measuresInfo, &evidence, &keywords, &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) json.Unmarshal(typicalCauses, &e.TypicalCauses) json.Unmarshal(relevantPhases, &e.RelevantLifecyclePhases) json.Unmarshal(measuresDesign, &e.RecommendedMeasuresDesign) json.Unmarshal(measuresTechnical, &e.RecommendedMeasuresTechnical) json.Unmarshal(measuresInfo, &e.RecommendedMeasuresInformation) json.Unmarshal(evidence, &e.SuggestedEvidence) json.Unmarshal(keywords, &e.RelatedKeywords) 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 var typicalCauses, relevantLifecyclePhases []byte var recommendedMeasuresDesign, recommendedMeasuresTechnical, recommendedMeasuresInformation []byte var suggestedEvidence, relatedKeywords []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, COALESCE(sub_category, ''), COALESCE(default_exposure, 3), COALESCE(default_avoidance, 3), COALESCE(typical_causes, '[]'), COALESCE(typical_harm, ''), COALESCE(relevant_lifecycle_phases, '[]'), COALESCE(recommended_measures_design, '[]'), COALESCE(recommended_measures_technical, '[]'), COALESCE(recommended_measures_information, '[]'), COALESCE(suggested_evidence, '[]'), COALESCE(related_keywords, '[]') 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, &e.SubCategory, &e.DefaultExposure, &e.DefaultAvoidance, &typicalCauses, &e.TypicalHarm, &relevantLifecyclePhases, &recommendedMeasuresDesign, &recommendedMeasuresTechnical, &recommendedMeasuresInformation, &suggestedEvidence, &relatedKeywords, ) 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) json.Unmarshal(typicalCauses, &e.TypicalCauses) json.Unmarshal(relevantLifecyclePhases, &e.RelevantLifecyclePhases) json.Unmarshal(recommendedMeasuresDesign, &e.RecommendedMeasuresDesign) json.Unmarshal(recommendedMeasuresTechnical, &e.RecommendedMeasuresTechnical) json.Unmarshal(recommendedMeasuresInformation, &e.RecommendedMeasuresInformation) json.Unmarshal(suggestedEvidence, &e.SuggestedEvidence) json.Unmarshal(relatedKeywords, &e.RelatedKeywords) 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 RiskLevelNotAcceptable: summary.NotAcceptable++ case RiskLevelVeryHigh: summary.VeryHigh++ 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 RiskLevelNotAcceptable: return 7 case RiskLevelVeryHigh: return 6 case RiskLevelCritical: return 5 case RiskLevelHigh: return 4 case RiskLevelMedium: return 3 case RiskLevelLow: return 2 case RiskLevelNegligible: return 1 default: return 0 } } // 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 }