Files
breakpilot-compliance/ai-compliance-sdk/internal/ucca/store.go
Benjamin Admin 312c2c9b60
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
feat: Use-Cases/UCCA Module auf 100% — Interface Fix, Search/Offset/Total, Explain/Export, Edit-Mode
Kritische Bug Fixes:
- [id]/page.tsx: FullAssessment Interface repariert (nested result → flat fields)
- resultForCard baut explizit aus flachen Assessment-Feldern (feasibility, risk_score etc.)
- Use-Case-Text-Pfad: assessment.intake?.use_case_text statt assessment.use_case_text
- rule_code/code Mapping beim Übergeben an AssessmentResultCard

Backend (A2+A3):
- store.go: AssessmentFilters um Search + Offset erweitert
- ListAssessments: COUNT-Query (total), ILIKE-Search auf title, OFFSET-Pagination
- ListAssessments Signatur: ([]Assessment, int, error)
- Handler: search/offset aus Query-Params, total in Response
- import "strconv" hinzugefügt

Neue Features:
- KI-Erklärung Button (POST /explain) mit lila Erklärungsbox
- Export-Buttons Markdown + JSON (Download-Links)
- Edit-Mode in new/page.tsx: useSearchParams(?edit=id), Form vorausfüllen
- Bedingte PUT/POST Logik; nach Edit → Detail-Seite Redirect
- Suspense-Wrapper für useSearchParams (Next.js 15 Requirement)

Backend Edit:
- store.go: UpdateAssessment() Methode (UPDATE-Query)
- ucca_handlers.go: UpdateAssessment Handler (re-evaluiert Intake)
- main.go: PUT /ucca/assessments/:id Route registriert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:13:42 +01:00

369 lines
12 KiB
Go

package ucca
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Store handles UCCA data persistence
type Store struct {
pool *pgxpool.Pool
}
// NewStore creates a new UCCA store
func NewStore(pool *pgxpool.Pool) *Store {
return &Store{pool: pool}
}
// ============================================================================
// Assessment CRUD Operations
// ============================================================================
// CreateAssessment creates a new assessment
func (s *Store) CreateAssessment(ctx context.Context, a *Assessment) error {
a.ID = uuid.New()
a.CreatedAt = time.Now().UTC()
a.UpdatedAt = a.CreatedAt
if a.PolicyVersion == "" {
a.PolicyVersion = "1.0.0"
}
if a.Status == "" {
a.Status = "completed"
}
// Marshal JSONB fields
intake, _ := json.Marshal(a.Intake)
triggeredRules, _ := json.Marshal(a.TriggeredRules)
requiredControls, _ := json.Marshal(a.RequiredControls)
recommendedArchitecture, _ := json.Marshal(a.RecommendedArchitecture)
forbiddenPatterns, _ := json.Marshal(a.ForbiddenPatterns)
exampleMatches, _ := json.Marshal(a.ExampleMatches)
_, err := s.pool.Exec(ctx, `
INSERT INTO ucca_assessments (
id, tenant_id, namespace_id, title, policy_version, status,
intake, use_case_text_stored, use_case_text_hash,
feasibility, risk_level, complexity, risk_score,
triggered_rules, required_controls, recommended_architecture,
forbidden_patterns, example_matches,
dsfa_recommended, art22_risk, training_allowed,
corpus_version_id, corpus_version,
explanation_text, explanation_generated_at, explanation_model,
domain, created_at, updated_at, created_by
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9,
$10, $11, $12, $13,
$14, $15, $16,
$17, $18,
$19, $20, $21,
$22, $23,
$24, $25, $26,
$27, $28, $29, $30
)
`,
a.ID, a.TenantID, a.NamespaceID, a.Title, a.PolicyVersion, a.Status,
intake, a.UseCaseTextStored, a.UseCaseTextHash,
string(a.Feasibility), string(a.RiskLevel), string(a.Complexity), a.RiskScore,
triggeredRules, requiredControls, recommendedArchitecture,
forbiddenPatterns, exampleMatches,
a.DSFARecommended, a.Art22Risk, string(a.TrainingAllowed),
a.CorpusVersionID, a.CorpusVersion,
a.ExplanationText, a.ExplanationGeneratedAt, a.ExplanationModel,
string(a.Domain), a.CreatedAt, a.UpdatedAt, a.CreatedBy,
)
return err
}
// GetAssessment retrieves an assessment by ID
func (s *Store) GetAssessment(ctx context.Context, id uuid.UUID) (*Assessment, error) {
var a Assessment
var intake, triggeredRules, requiredControls, recommendedArchitecture, forbiddenPatterns, exampleMatches []byte
var feasibility, riskLevel, complexity, trainingAllowed, domain string
err := s.pool.QueryRow(ctx, `
SELECT
id, tenant_id, namespace_id, title, policy_version, status,
intake, use_case_text_stored, use_case_text_hash,
feasibility, risk_level, complexity, risk_score,
triggered_rules, required_controls, recommended_architecture,
forbidden_patterns, example_matches,
dsfa_recommended, art22_risk, training_allowed,
corpus_version_id, corpus_version,
explanation_text, explanation_generated_at, explanation_model,
domain, created_at, updated_at, created_by
FROM ucca_assessments WHERE id = $1
`, id).Scan(
&a.ID, &a.TenantID, &a.NamespaceID, &a.Title, &a.PolicyVersion, &a.Status,
&intake, &a.UseCaseTextStored, &a.UseCaseTextHash,
&feasibility, &riskLevel, &complexity, &a.RiskScore,
&triggeredRules, &requiredControls, &recommendedArchitecture,
&forbiddenPatterns, &exampleMatches,
&a.DSFARecommended, &a.Art22Risk, &trainingAllowed,
&a.CorpusVersionID, &a.CorpusVersion,
&a.ExplanationText, &a.ExplanationGeneratedAt, &a.ExplanationModel,
&domain, &a.CreatedAt, &a.UpdatedAt, &a.CreatedBy,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
// Unmarshal JSONB fields
json.Unmarshal(intake, &a.Intake)
json.Unmarshal(triggeredRules, &a.TriggeredRules)
json.Unmarshal(requiredControls, &a.RequiredControls)
json.Unmarshal(recommendedArchitecture, &a.RecommendedArchitecture)
json.Unmarshal(forbiddenPatterns, &a.ForbiddenPatterns)
json.Unmarshal(exampleMatches, &a.ExampleMatches)
// Convert string fields to typed constants
a.Feasibility = Feasibility(feasibility)
a.RiskLevel = RiskLevel(riskLevel)
a.Complexity = Complexity(complexity)
a.TrainingAllowed = TrainingAllowed(trainingAllowed)
a.Domain = Domain(domain)
return &a, nil
}
// ListAssessments lists assessments for a tenant with optional filters
func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters *AssessmentFilters) ([]Assessment, int, error) {
baseWhere := " WHERE tenant_id = $1"
args := []interface{}{tenantID}
argIdx := 2
// Build WHERE clause from filters
if filters != nil {
if filters.Feasibility != "" {
baseWhere += " AND feasibility = $" + itoa(argIdx)
args = append(args, filters.Feasibility)
argIdx++
}
if filters.Domain != "" {
baseWhere += " AND domain = $" + itoa(argIdx)
args = append(args, filters.Domain)
argIdx++
}
if filters.RiskLevel != "" {
baseWhere += " AND risk_level = $" + itoa(argIdx)
args = append(args, filters.RiskLevel)
argIdx++
}
if filters.Search != "" {
baseWhere += " AND title ILIKE $" + strconv.Itoa(argIdx)
args = append(args, "%"+filters.Search+"%")
argIdx++
}
}
// COUNT query (same WHERE, no ORDER/LIMIT/OFFSET)
var total int
s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM ucca_assessments"+baseWhere, args...).Scan(&total)
// Data query
query := `
SELECT
id, tenant_id, namespace_id, title, policy_version, status,
intake, use_case_text_stored, use_case_text_hash,
feasibility, risk_level, complexity, risk_score,
triggered_rules, required_controls, recommended_architecture,
forbidden_patterns, example_matches,
dsfa_recommended, art22_risk, training_allowed,
corpus_version_id, corpus_version,
explanation_text, explanation_generated_at, explanation_model,
domain, created_at, updated_at, created_by
FROM ucca_assessments` + baseWhere + " ORDER BY created_at DESC"
// Apply LIMIT
if filters != nil && filters.Limit > 0 {
query += " LIMIT $" + strconv.Itoa(argIdx)
args = append(args, filters.Limit)
argIdx++
}
// Apply OFFSET
if filters != nil && filters.Offset > 0 {
query += " OFFSET $" + strconv.Itoa(argIdx)
args = append(args, filters.Offset)
argIdx++
}
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
var assessments []Assessment
for rows.Next() {
var a Assessment
var intake, triggeredRules, requiredControls, recommendedArchitecture, forbiddenPatterns, exampleMatches []byte
var feasibility, riskLevel, complexity, trainingAllowed, domain string
err := rows.Scan(
&a.ID, &a.TenantID, &a.NamespaceID, &a.Title, &a.PolicyVersion, &a.Status,
&intake, &a.UseCaseTextStored, &a.UseCaseTextHash,
&feasibility, &riskLevel, &complexity, &a.RiskScore,
&triggeredRules, &requiredControls, &recommendedArchitecture,
&forbiddenPatterns, &exampleMatches,
&a.DSFARecommended, &a.Art22Risk, &trainingAllowed,
&a.CorpusVersionID, &a.CorpusVersion,
&a.ExplanationText, &a.ExplanationGeneratedAt, &a.ExplanationModel,
&domain, &a.CreatedAt, &a.UpdatedAt, &a.CreatedBy,
)
if err != nil {
return nil, 0, err
}
// Unmarshal JSONB fields
json.Unmarshal(intake, &a.Intake)
json.Unmarshal(triggeredRules, &a.TriggeredRules)
json.Unmarshal(requiredControls, &a.RequiredControls)
json.Unmarshal(recommendedArchitecture, &a.RecommendedArchitecture)
json.Unmarshal(forbiddenPatterns, &a.ForbiddenPatterns)
json.Unmarshal(exampleMatches, &a.ExampleMatches)
// Convert string fields
a.Feasibility = Feasibility(feasibility)
a.RiskLevel = RiskLevel(riskLevel)
a.Complexity = Complexity(complexity)
a.TrainingAllowed = TrainingAllowed(trainingAllowed)
a.Domain = Domain(domain)
assessments = append(assessments, a)
}
return assessments, total, nil
}
// DeleteAssessment deletes an assessment by ID
func (s *Store) DeleteAssessment(ctx context.Context, id uuid.UUID) error {
_, err := s.pool.Exec(ctx, "DELETE FROM ucca_assessments WHERE id = $1", id)
return err
}
// UpdateAssessment updates an existing assessment's intake and re-evaluated results
func (s *Store) UpdateAssessment(ctx context.Context, id uuid.UUID, a *Assessment) error {
intake, _ := json.Marshal(a.Intake)
triggeredRules, _ := json.Marshal(a.TriggeredRules)
requiredControls, _ := json.Marshal(a.RequiredControls)
recommendedArchitecture, _ := json.Marshal(a.RecommendedArchitecture)
forbiddenPatterns, _ := json.Marshal(a.ForbiddenPatterns)
exampleMatches, _ := json.Marshal(a.ExampleMatches)
now := time.Now().UTC()
_, err := s.pool.Exec(ctx, `
UPDATE ucca_assessments SET
title = $2, intake = $3, use_case_text_stored = $4, use_case_text_hash = $5,
feasibility = $6, risk_level = $7, complexity = $8, risk_score = $9,
triggered_rules = $10, required_controls = $11, recommended_architecture = $12,
forbidden_patterns = $13, example_matches = $14,
dsfa_recommended = $15, art22_risk = $16, training_allowed = $17,
domain = $18, policy_version = $19, updated_at = $20
WHERE id = $1
`, id, a.Title, intake, a.UseCaseTextStored, a.UseCaseTextHash,
string(a.Feasibility), string(a.RiskLevel), string(a.Complexity), a.RiskScore,
triggeredRules, requiredControls, recommendedArchitecture,
forbiddenPatterns, exampleMatches,
a.DSFARecommended, a.Art22Risk, string(a.TrainingAllowed),
string(a.Domain), a.PolicyVersion, now)
return err
}
// UpdateExplanation updates the LLM explanation for an assessment
func (s *Store) UpdateExplanation(ctx context.Context, id uuid.UUID, explanation string, model string) error {
now := time.Now().UTC()
_, err := s.pool.Exec(ctx, `
UPDATE ucca_assessments SET
explanation_text = $2,
explanation_generated_at = $3,
explanation_model = $4,
updated_at = $5
WHERE id = $1
`, id, explanation, now, model, now)
return err
}
// ============================================================================
// Statistics
// ============================================================================
// UCCAStats contains UCCA module statistics
type UCCAStats struct {
TotalAssessments int `json:"total_assessments"`
AssessmentsYES int `json:"assessments_yes"`
AssessmentsCONDITIONAL int `json:"assessments_conditional"`
AssessmentsNO int `json:"assessments_no"`
AverageRiskScore int `json:"average_risk_score"`
DSFARecommendedCount int `json:"dsfa_recommended_count"`
}
// GetStats returns UCCA statistics for a tenant
func (s *Store) GetStats(ctx context.Context, tenantID uuid.UUID) (*UCCAStats, error) {
stats := &UCCAStats{}
// Total count
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM ucca_assessments WHERE tenant_id = $1",
tenantID).Scan(&stats.TotalAssessments)
// Count by feasibility
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM ucca_assessments WHERE tenant_id = $1 AND feasibility = 'YES'",
tenantID).Scan(&stats.AssessmentsYES)
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM ucca_assessments WHERE tenant_id = $1 AND feasibility = 'CONDITIONAL'",
tenantID).Scan(&stats.AssessmentsCONDITIONAL)
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM ucca_assessments WHERE tenant_id = $1 AND feasibility = 'NO'",
tenantID).Scan(&stats.AssessmentsNO)
// Average risk score
s.pool.QueryRow(ctx,
"SELECT COALESCE(AVG(risk_score)::int, 0) FROM ucca_assessments WHERE tenant_id = $1",
tenantID).Scan(&stats.AverageRiskScore)
// DSFA recommended count
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM ucca_assessments WHERE tenant_id = $1 AND dsfa_recommended = true",
tenantID).Scan(&stats.DSFARecommendedCount)
return stats, nil
}
// ============================================================================
// Filter Types
// ============================================================================
// AssessmentFilters defines filters for listing assessments
type AssessmentFilters struct {
Feasibility string
Domain string
RiskLevel string
Search string // ILIKE on title
Limit int
Offset int // OFFSET for pagination
}
// ============================================================================
// Helpers
// ============================================================================
// itoa converts int to string for query building
func itoa(i int) string {
return string(rune('0' + i))
}