8bb90d73e5
Build + Deploy / build-backend-compliance (push) Successful in 3m34s
Build + Deploy / build-ai-sdk (push) Successful in 1m6s
Build + Deploy / build-developer-portal (push) Successful in 1m7s
Build + Deploy / build-tts (push) Successful in 1m58s
Build + Deploy / build-document-crawler (push) Successful in 57s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-dsms-node (push) Successful in 29s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m28s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 3m10s
- Erklaerteil-Template fuer Risikobeurteilungen (risk_assessment_template.go) in PDF-Export, Markdown-Export und Frontend ReportPrintView eingebaut - Ground Truth Benchmark-System: Datenmodell, Fuzzy-Matching-Engine, 3 API Endpoints (import-gt, benchmark, benchmark/summary) - Frontend Benchmark-Tab mit Score-Cards, Kategorie-Breakdown, Hazard-Vergleichstabelle (Zugeordnet/Fehlend/Extra), Business Impact - Erster Benchmark: 13.3% Coverage (Baseline) gegen 60 GT-Eintraege - Dedup-Fix: seenCat[cat] -> seenCatZone[cat+zone] erlaubt mehrere Gefaehrdungen pro Kategorie an verschiedenen Gefahrenstellen - Komponenten-spezifische Hazard-Namen und Zone-basierte Zuordnung Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
395 lines
12 KiB
Go
395 lines
12 KiB
Go
package iace
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// ============================================================================
|
|
// 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,
|
|
ParentProjectID: req.ParentProjectID,
|
|
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, parent_project_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, $17
|
|
)
|
|
`,
|
|
project.ID, project.TenantID, project.ParentProjectID,
|
|
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, parent_project_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.ParentProjectID, &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, parent_project_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.ParentProjectID, &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
|
|
}
|
|
|
|
// UpdateProjectMetadata replaces the metadata JSON for a project.
|
|
func (s *Store) UpdateProjectMetadata(ctx context.Context, id uuid.UUID, metadata json.RawMessage) error {
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE iace_projects SET metadata = $2, updated_at = NOW()
|
|
WHERE id = $1
|
|
`, id, metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("update project metadata: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListVariants returns all variant sub-projects for a given parent project
|
|
func (s *Store) ListVariants(ctx context.Context, parentID uuid.UUID) ([]Project, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT
|
|
id, tenant_id, parent_project_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 parent_project_id = $1
|
|
ORDER BY created_at DESC
|
|
`, parentID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list variants: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var variants []Project
|
|
for rows.Next() {
|
|
var p Project
|
|
var status string
|
|
var riskSummary, triggeredRegulations, metadata []byte
|
|
|
|
err := rows.Scan(
|
|
&p.ID, &p.TenantID, &p.ParentProjectID, &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 variants scan: %w", err)
|
|
}
|
|
|
|
p.Status = ProjectStatus(status)
|
|
json.Unmarshal(riskSummary, &p.RiskSummary)
|
|
json.Unmarshal(triggeredRegulations, &p.TriggeredRegulations)
|
|
json.Unmarshal(metadata, &p.Metadata)
|
|
|
|
variants = append(variants, p)
|
|
}
|
|
|
|
return variants, nil
|
|
}
|
|
|
|
// GetVariantGap computes the gap analysis between a variant and its base project.
|
|
// It counts hazards and measures in each, and returns the categories affected.
|
|
func (s *Store) GetVariantGap(ctx context.Context, variantID uuid.UUID) (*VariantGap, error) {
|
|
variant, err := s.GetProject(ctx, variantID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get variant: %w", err)
|
|
}
|
|
if variant == nil {
|
|
return nil, fmt.Errorf("variant project not found")
|
|
}
|
|
if variant.ParentProjectID == nil {
|
|
return nil, fmt.Errorf("project is not a variant (no parent_project_id)")
|
|
}
|
|
|
|
parent, err := s.GetProject(ctx, *variant.ParentProjectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get parent: %w", err)
|
|
}
|
|
if parent == nil {
|
|
return nil, fmt.Errorf("parent project not found")
|
|
}
|
|
|
|
// Count hazards and measures for both projects
|
|
parentHazards, parentMeasures, err := s.countHazardsAndMeasures(ctx, parent.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("count parent stats: %w", err)
|
|
}
|
|
variantHazards, variantMeasures, err := s.countHazardsAndMeasures(ctx, variant.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("count variant stats: %w", err)
|
|
}
|
|
|
|
// Get unique hazard categories in the variant
|
|
categories, err := s.getVariantHazardCategories(ctx, variant.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get variant categories: %w", err)
|
|
}
|
|
|
|
return &VariantGap{
|
|
BaseProject: ProjectSummary{
|
|
ID: parent.ID,
|
|
Name: parent.MachineName,
|
|
HazardCount: parentHazards,
|
|
MeasureCount: parentMeasures,
|
|
},
|
|
Variant: ProjectSummary{
|
|
ID: variant.ID,
|
|
Name: variant.MachineName,
|
|
HazardCount: variantHazards,
|
|
MeasureCount: variantMeasures,
|
|
},
|
|
Gap: GapDetail{
|
|
AdditionalHazards: variantHazards,
|
|
AdditionalMeasures: variantMeasures,
|
|
CategoriesAffected: categories,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// countHazardsAndMeasures returns (hazardCount, measureCount) for a project
|
|
func (s *Store) countHazardsAndMeasures(ctx context.Context, projectID uuid.UUID) (int, int, error) {
|
|
var hazardCount int
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT COUNT(*) FROM iace_hazards WHERE project_id = $1`, projectID,
|
|
).Scan(&hazardCount)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
var measureCount int
|
|
err = s.pool.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM iace_mitigations m
|
|
JOIN iace_hazards h ON m.hazard_id = h.id
|
|
WHERE h.project_id = $1
|
|
`, projectID).Scan(&measureCount)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
return hazardCount, measureCount, nil
|
|
}
|
|
|
|
// getVariantHazardCategories returns distinct hazard categories for a project
|
|
func (s *Store) getVariantHazardCategories(ctx context.Context, projectID uuid.UUID) ([]string, error) {
|
|
rows, err := s.pool.Query(ctx,
|
|
`SELECT DISTINCT category FROM iace_hazards WHERE project_id = $1 ORDER BY category`,
|
|
projectID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var categories []string
|
|
for rows.Next() {
|
|
var cat string
|
|
if err := rows.Scan(&cat); err != nil {
|
|
return nil, err
|
|
}
|
|
categories = append(categories, cat)
|
|
}
|
|
return categories, nil
|
|
}
|
|
|