Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/store_hazards.go
Sharang Parnerkar a83056b5e7 refactor(go/iace): split hazard_library and store into focused files under 500 LOC
All oversized iace files now comply with the 500-line hard cap:
- hazard_library_ai_sw.go split into ai_sw (false_classification..communication)
  and ai_fw (unauthorized_access..update_failure)
- hazard_library_software_hmi.go split into software_hmi (software_fault+hmi)
  and config_integration (configuration_error+logging+integration)
- hazard_library_machine_safety.go split to keep mechanical/electrical/thermal/emc,
  safety_functions extracted into hazard_library_safety_functions.go
- store_hazards.go split: hazard library queries moved to store_hazard_library.go
- store_projects.go split: component and classification ops to store_components.go
- store_mitigations.go split: evidence/verification/ref-data to store_evidence.go
- hazard_library.go GetBuiltinHazardLibrary() updated to call all sub-functions
- All iace tests pass (go test ./internal/iace/...)

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

394 lines
11 KiB
Go

package iace
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// ============================================================================
// 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
}
// ============================================================================
// 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
}
}