Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC, ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group into same-package files, all under the 500-line hard cap. Zero behavior changes, no renamed exported symbols. Also fixed pre-existing hazard_library split (missing functions and duplicate UUID keys from a prior session). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
384 lines
11 KiB
Go
384 lines
11 KiB
Go
package iace
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// ============================================================================
|
|
// 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
|
|
}
|