Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/store_projects.go
T
Benjamin Admin 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
feat(iace): benchmark system + erklaerteil + dedup-fix
- 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>
2026-05-13 01:02:33 +02:00

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
}