Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/store.go
Benjamin Admin 6d2de9b897
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
feat(iace): complete CE risk assessment — LLM tech-file generation, multi-format export, TipTap editor
Phase 1: Fix completeness gates G23 (require verified/rejected mitigations) and G09 (audit trail check)
Phase 2: LLM-based tech-file section generation with 19 German prompts and RAG enrichment
Phase 3: Multi-format document export (PDF/Excel/DOCX/Markdown/JSON)
Phase 4: Company profile → IACE data flow with auto component/classification creation
Phase 5: TipTap WYSIWYG editor replacing textarea for tech-file sections
Phase 6: User journey tests, developer portal API reference, updated documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:50:53 +01:00

1949 lines
58 KiB
Go

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, &reg, &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, &reg, &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, &regulationReferences,
&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, &regulationReferences,
&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
}