feat: AI Act Decision Tree — Zwei-Achsen-Klassifikation (GPAI + High-Risk)

Interaktiver 12-Fragen-Entscheidungsbaum für die AI Act Klassifikation
auf zwei Achsen: High-Risk (Anhang III, Q1-Q7) und GPAI (Art. 51-56, Q8-Q12).
Deterministische Auswertung ohne LLM.

Backend (Go):
- Neue Structs: GPAIClassification, DecisionTreeAnswer, DecisionTreeResult
- Decision Tree Engine mit BuildDecisionTreeDefinition() und EvaluateDecisionTree()
- Store-Methoden für CRUD der Ergebnisse
- API-Endpoints: GET/POST /decision-tree, GET/DELETE /decision-tree/results
- 12 Unit Tests (alle bestanden)

Frontend (Next.js):
- DecisionTreeWizard: Wizard-UI mit Ja/Nein-Fragen, Dual-Progress-Bar, Ergebnis-Ansicht
- AI Act Page refactored: Tabs (Übersicht | Entscheidungsbaum | Ergebnisse)
- Proxy-Route für decision-tree Endpoints

Migration 083: ai_act_decision_tree_results Tabelle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-29 10:14:09 +02:00
parent 712fa8cb74
commit bc75b4455d
11 changed files with 2016 additions and 94 deletions

View File

@@ -358,6 +358,128 @@ type AssessmentFilters struct {
Offset int // OFFSET for pagination
}
// ============================================================================
// Decision Tree Result CRUD
// ============================================================================
// CreateDecisionTreeResult stores a new decision tree result
func (s *Store) CreateDecisionTreeResult(ctx context.Context, r *DecisionTreeResult) error {
r.ID = uuid.New()
r.CreatedAt = time.Now().UTC()
r.UpdatedAt = r.CreatedAt
answers, _ := json.Marshal(r.Answers)
gpaiResult, _ := json.Marshal(r.GPAIResult)
obligations, _ := json.Marshal(r.CombinedObligations)
articles, _ := json.Marshal(r.ApplicableArticles)
_, err := s.pool.Exec(ctx, `
INSERT INTO ai_act_decision_tree_results (
id, tenant_id, project_id, system_name, system_description,
answers, high_risk_level, gpai_result,
combined_obligations, applicable_articles,
created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
$9, $10,
$11, $12
)
`,
r.ID, r.TenantID, r.ProjectID, r.SystemName, r.SystemDescription,
answers, string(r.HighRiskResult), gpaiResult,
obligations, articles,
r.CreatedAt, r.UpdatedAt,
)
return err
}
// GetDecisionTreeResult retrieves a decision tree result by ID
func (s *Store) GetDecisionTreeResult(ctx context.Context, id uuid.UUID) (*DecisionTreeResult, error) {
var r DecisionTreeResult
var answersBytes, gpaiBytes, oblBytes, artBytes []byte
var highRiskLevel string
err := s.pool.QueryRow(ctx, `
SELECT id, tenant_id, project_id, system_name, system_description,
answers, high_risk_level, gpai_result,
combined_obligations, applicable_articles,
created_at, updated_at
FROM ai_act_decision_tree_results WHERE id = $1
`, id).Scan(
&r.ID, &r.TenantID, &r.ProjectID, &r.SystemName, &r.SystemDescription,
&answersBytes, &highRiskLevel, &gpaiBytes,
&oblBytes, &artBytes,
&r.CreatedAt, &r.UpdatedAt,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
json.Unmarshal(answersBytes, &r.Answers)
json.Unmarshal(gpaiBytes, &r.GPAIResult)
json.Unmarshal(oblBytes, &r.CombinedObligations)
json.Unmarshal(artBytes, &r.ApplicableArticles)
r.HighRiskResult = AIActRiskLevel(highRiskLevel)
return &r, nil
}
// ListDecisionTreeResults lists all decision tree results for a tenant
func (s *Store) ListDecisionTreeResults(ctx context.Context, tenantID uuid.UUID) ([]DecisionTreeResult, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, tenant_id, project_id, system_name, system_description,
answers, high_risk_level, gpai_result,
combined_obligations, applicable_articles,
created_at, updated_at
FROM ai_act_decision_tree_results
WHERE tenant_id = $1
ORDER BY created_at DESC
LIMIT 100
`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var results []DecisionTreeResult
for rows.Next() {
var r DecisionTreeResult
var answersBytes, gpaiBytes, oblBytes, artBytes []byte
var highRiskLevel string
err := rows.Scan(
&r.ID, &r.TenantID, &r.ProjectID, &r.SystemName, &r.SystemDescription,
&answersBytes, &highRiskLevel, &gpaiBytes,
&oblBytes, &artBytes,
&r.CreatedAt, &r.UpdatedAt,
)
if err != nil {
return nil, err
}
json.Unmarshal(answersBytes, &r.Answers)
json.Unmarshal(gpaiBytes, &r.GPAIResult)
json.Unmarshal(oblBytes, &r.CombinedObligations)
json.Unmarshal(artBytes, &r.ApplicableArticles)
r.HighRiskResult = AIActRiskLevel(highRiskLevel)
results = append(results, r)
}
return results, nil
}
// DeleteDecisionTreeResult deletes a decision tree result by ID
func (s *Store) DeleteDecisionTreeResult(ctx context.Context, id uuid.UUID) error {
_, err := s.pool.Exec(ctx, "DELETE FROM ai_act_decision_tree_results WHERE id = $1", id)
return err
}
// ============================================================================
// Helpers
// ============================================================================