From 1ac716261cb6d62576688200eeeb2ccd7fa45428 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 23 Apr 2026 09:10:20 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Compliance=20Maximizer=20=E2=80=94=20Re?= =?UTF-8?q?gulatory=20Optimization=20Engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues Modul das den regulatorischen Spielraum fuer KI-Use-Cases deterministisch berechnet und optimale Konfigurationen vorschlaegt. Kernfeatures: - 13-Dimensionen Constraint-Space (DSGVO + AI Act) - 3-Zonen-Analyse: Verboten / Eingeschraenkt / Erlaubt - Deterministische Optimizer-Engine (kein LLM im Kern) - 28 Constraint-Regeln aus DSGVO, AI Act, EDPB Guidelines - 28 Tests (Golden Suite + Meta-Tests) - REST API: /sdk/v1/maximizer/* (9 Endpoints) - Frontend: 3-Zonen-Visualisierung, Dimension-Form, Score-Gauges [migration-approved] Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/sdk/v1/maximizer/[[...path]]/route.ts | 52 ++ .../sdk/compliance-optimizer/[id]/page.tsx | 151 ++++++ .../app/sdk/compliance-optimizer/new/page.tsx | 163 +++++++ .../app/sdk/compliance-optimizer/page.tsx | 124 +++++ .../sdk/Sidebar/SidebarModuleList.tsx | 1 + .../compliance-optimizer/ConfigComparison.tsx | 52 ++ .../DimensionZoneTable.tsx | 74 +++ .../OptimizationScoreCard.tsx | 50 ++ .../sdk/compliance-optimizer/ZoneBadge.tsx | 16 + .../api/handlers/maximizer_handlers.go | 155 ++++++ ai-compliance-sdk/internal/app/app.go | 15 +- ai-compliance-sdk/internal/app/routes.go | 16 + .../internal/maximizer/constraint_loader.go | 52 ++ .../internal/maximizer/constraints.go | 76 +++ .../internal/maximizer/dimensions.go | 306 ++++++++++++ .../internal/maximizer/dimensions_test.go | 201 ++++++++ .../internal/maximizer/evaluator.go | 218 +++++++++ .../internal/maximizer/evaluator_test.go | 229 +++++++++ .../internal/maximizer/intake_mapper.go | 189 +++++++ .../internal/maximizer/optimizer.go | 291 +++++++++++ .../internal/maximizer/optimizer_test.go | 300 ++++++++++++ .../internal/maximizer/scoring.go | 84 ++++ .../internal/maximizer/scoring_test.go | 88 ++++ .../internal/maximizer/service.go | 144 ++++++ ai-compliance-sdk/internal/maximizer/store.go | 209 ++++++++ ai-compliance-sdk/internal/ucca/models.go | 12 + .../internal/ucca/models_assessment.go | 7 + .../internal/ucca/models_intake.go | 3 + .../migrations/026_maximizer_schema.sql | 41 ++ .../policies/maximizer_constraints_v1.json | 461 ++++++++++++++++++ 30 files changed, 3779 insertions(+), 1 deletion(-) create mode 100644 admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts create mode 100644 admin-compliance/app/sdk/compliance-optimizer/[id]/page.tsx create mode 100644 admin-compliance/app/sdk/compliance-optimizer/new/page.tsx create mode 100644 admin-compliance/app/sdk/compliance-optimizer/page.tsx create mode 100644 admin-compliance/components/sdk/compliance-optimizer/ConfigComparison.tsx create mode 100644 admin-compliance/components/sdk/compliance-optimizer/DimensionZoneTable.tsx create mode 100644 admin-compliance/components/sdk/compliance-optimizer/OptimizationScoreCard.tsx create mode 100644 admin-compliance/components/sdk/compliance-optimizer/ZoneBadge.tsx create mode 100644 ai-compliance-sdk/internal/api/handlers/maximizer_handlers.go create mode 100644 ai-compliance-sdk/internal/maximizer/constraint_loader.go create mode 100644 ai-compliance-sdk/internal/maximizer/constraints.go create mode 100644 ai-compliance-sdk/internal/maximizer/dimensions.go create mode 100644 ai-compliance-sdk/internal/maximizer/dimensions_test.go create mode 100644 ai-compliance-sdk/internal/maximizer/evaluator.go create mode 100644 ai-compliance-sdk/internal/maximizer/evaluator_test.go create mode 100644 ai-compliance-sdk/internal/maximizer/intake_mapper.go create mode 100644 ai-compliance-sdk/internal/maximizer/optimizer.go create mode 100644 ai-compliance-sdk/internal/maximizer/optimizer_test.go create mode 100644 ai-compliance-sdk/internal/maximizer/scoring.go create mode 100644 ai-compliance-sdk/internal/maximizer/scoring_test.go create mode 100644 ai-compliance-sdk/internal/maximizer/service.go create mode 100644 ai-compliance-sdk/internal/maximizer/store.go create mode 100644 ai-compliance-sdk/migrations/026_maximizer_schema.sql create mode 100644 ai-compliance-sdk/policies/maximizer_constraints_v1.json diff --git a/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts new file mode 100644 index 0000000..eae4604 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server' + +const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090' + +function buildUrl(request: NextRequest, params: { path?: string[] }) { + const subPath = params.path?.join('/') || '' + const { searchParams } = new URL(request.url) + const qs = searchParams.toString() + return `${SDK_URL}/sdk/v1/maximizer/${subPath}${qs ? `?${qs}` : ''}` +} + +function forwardHeaders(request: NextRequest): Record { + const headers: Record = { 'Content-Type': 'application/json' } + const tenantId = request.headers.get('X-Tenant-ID') + if (tenantId) headers['X-Tenant-ID'] = tenantId + const userId = request.headers.get('X-User-ID') + if (userId) headers['X-User-ID'] = userId + return headers +} + +async function proxy(request: NextRequest, params: { path?: string[] }, method: string) { + try { + const url = buildUrl(request, params) + const init: RequestInit = { method, headers: forwardHeaders(request) } + if (method !== 'GET' && method !== 'DELETE') { + init.body = await request.text() + } + const response = await fetch(url, init) + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json({ error: 'Maximizer backend error', details: errorText }, { status: response.status }) + } + if (response.status === 204) return new NextResponse(null, { status: 204 }) + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Maximizer proxy error:', error) + return NextResponse.json({ error: 'Failed to connect to Maximizer backend' }, { status: 503 }) + } +} + +export async function GET(request: NextRequest, { params }: { params: { path?: string[] } }) { + return proxy(request, params, 'GET') +} + +export async function POST(request: NextRequest, { params }: { params: { path?: string[] } }) { + return proxy(request, params, 'POST') +} + +export async function DELETE(request: NextRequest, { params }: { params: { path?: string[] } }) { + return proxy(request, params, 'DELETE') +} diff --git a/admin-compliance/app/sdk/compliance-optimizer/[id]/page.tsx b/admin-compliance/app/sdk/compliance-optimizer/[id]/page.tsx new file mode 100644 index 0000000..b1c47b5 --- /dev/null +++ b/admin-compliance/app/sdk/compliance-optimizer/[id]/page.tsx @@ -0,0 +1,151 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useParams } from 'next/navigation' +import Link from 'next/link' +import { ZoneBadge } from '@/components/sdk/compliance-optimizer/ZoneBadge' +import { DimensionZoneTable } from '@/components/sdk/compliance-optimizer/DimensionZoneTable' +import { ConfigComparison } from '@/components/sdk/compliance-optimizer/ConfigComparison' +import { OptimizationScoreCard } from '@/components/sdk/compliance-optimizer/OptimizationScoreCard' + +export default function OptimizationDetailPage() { + const params = useParams() + const id = params?.id as string + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [activeVariant, setActiveVariant] = useState(0) + + useEffect(() => { + if (!id) return + fetch(`/api/sdk/v1/maximizer/optimizations/${id}`) + .then((r) => r.ok ? r.json() : null) + .then(setData) + .finally(() => setLoading(false)) + }, [id]) + + if (loading) return
Laden...
+ if (!data) return
Optimierung nicht gefunden.
+ + const maxSafe = data.max_safe_config + const variants = data.variants || [] + const zones = data.zone_map || {} + const controls = data.original_evaluation?.required_controls || [] + const patterns = data.original_evaluation?.required_patterns || [] + const triggered = data.original_evaluation?.triggered_rules || [] + + return ( +
+ {/* Header */} +
+
+ ← Zurueck +

{data.title || 'Optimierung'}

+

{new Date(data.created_at).toLocaleString('de-DE')} — v{data.constraint_version}

+
+ +
+ + {/* 3-Zone Summary */} +
+

3-Zonen-Analyse

+ +
+ + {/* Optimization Result */} + {maxSafe && ( +
+

Optimierte Konfiguration

+ +
+ +
+ {maxSafe.rationale && ( +

{maxSafe.rationale}

+ )} +
+ )} + + {/* Alternative Variants */} + {variants.length > 1 && ( +
+

Alternative Varianten ({variants.length})

+
+ {variants.map((v: any, i: number) => ( + + ))} +
+ {variants[activeVariant] && ( +
+
+ Sicherheit: {variants[activeVariant].safety_score} + Nutzen: {variants[activeVariant].utility_score} + Gesamt: {Math.round(variants[activeVariant].composite_score)} +
+ + {variants[activeVariant].rationale && ( +

{variants[activeVariant].rationale}

+ )} +
+ )} +
+ )} + + {/* Required Controls & Patterns */} + {(controls.length > 0 || patterns.length > 0) && ( +
+

Erforderliche Massnahmen

+
+ {controls.length > 0 && ( +
+

Controls

+
    + {controls.map((c: string, i: number) => ( +
  • + {c} +
  • + ))} +
+
+ )} + {patterns.length > 0 && ( +
+

Architektur-Patterns

+
    + {patterns.map((p: string, i: number) => ( +
  • + {p} +
  • + ))} +
+
+ )} +
+
+ )} + + {/* Triggered Rules (Audit Trail) */} + {triggered.length > 0 && ( +
+

Ausgeloeste Regeln ({triggered.length})

+
+ {triggered.map((r: any, i: number) => ( +
+ {r.rule_id} + {r.title} + {r.article_ref} +
+ ))} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/compliance-optimizer/new/page.tsx b/admin-compliance/app/sdk/compliance-optimizer/new/page.tsx new file mode 100644 index 0000000..1b7c3ab --- /dev/null +++ b/admin-compliance/app/sdk/compliance-optimizer/new/page.tsx @@ -0,0 +1,163 @@ +'use client' + +import React, { useState } from 'react' +import { useRouter } from 'next/navigation' +import { ZoneBadge } from '@/components/sdk/compliance-optimizer/ZoneBadge' + +interface DimensionField { + key: string + label: string + options: { value: string; label: string }[] + type?: 'select' | 'toggle' +} + +const DIMENSIONS: DimensionField[] = [ + { key: 'automation_level', label: 'Automatisierungsgrad', options: [ + { value: 'none', label: 'Keine' }, { value: 'assistive', label: 'Assistierend' }, + { value: 'partial', label: 'Teilautomatisiert' }, { value: 'full', label: 'Vollautomatisiert' }, + ]}, + { key: 'decision_binding', label: 'Entscheidungsbindung', options: [ + { value: 'non_binding', label: 'Unverbindlich' }, { value: 'human_review_required', label: 'Mensch entscheidet' }, + { value: 'fully_binding', label: 'Vollstaendig bindend' }, + ]}, + { key: 'decision_impact', label: 'Entscheidungswirkung', options: [ + { value: 'low', label: 'Niedrig' }, { value: 'medium', label: 'Mittel' }, { value: 'high', label: 'Hoch' }, + ]}, + { key: 'domain', label: 'Branche', options: [ + { value: 'hr', label: 'HR / Personal' }, { value: 'finance', label: 'Finanzen' }, + { value: 'education', label: 'Bildung' }, { value: 'health', label: 'Gesundheit' }, + { value: 'marketing', label: 'Marketing' }, { value: 'general', label: 'Allgemein' }, + ]}, + { key: 'data_type', label: 'Datensensitivitaet', options: [ + { value: 'non_personal', label: 'Keine personenbezogenen' }, { value: 'personal', label: 'Personenbezogen' }, + { value: 'sensitive', label: 'Besondere Kategorien (Art. 9)' }, { value: 'biometric', label: 'Biometrisch' }, + ]}, + { key: 'human_in_loop', label: 'Menschliche Kontrolle', options: [ + { value: 'required', label: 'Erforderlich' }, { value: 'optional', label: 'Optional' }, { value: 'none', label: 'Keine' }, + ]}, + { key: 'explainability', label: 'Erklaerbarkeit', options: [ + { value: 'high', label: 'Hoch' }, { value: 'basic', label: 'Basis' }, { value: 'none', label: 'Keine' }, + ]}, + { key: 'risk_classification', label: 'Risikoklasse (AI Act)', options: [ + { value: 'minimal', label: 'Minimal' }, { value: 'limited', label: 'Begrenzt' }, + { value: 'high', label: 'Hoch' }, { value: 'prohibited', label: 'Verboten' }, + ]}, + { key: 'legal_basis', label: 'Rechtsgrundlage (DSGVO)', options: [ + { value: 'consent', label: 'Einwilligung' }, { value: 'contract', label: 'Vertrag' }, + { value: 'legal_obligation', label: 'Rechtl. Verpflichtung' }, + { value: 'legitimate_interest', label: 'Berechtigtes Interesse' }, + { value: 'public_interest', label: 'Oeffentl. Interesse' }, + ]}, + { key: 'model_type', label: 'Modelltyp', options: [ + { value: 'rule_based', label: 'Regelbasiert' }, { value: 'statistical', label: 'Statistisch / ML' }, + { value: 'blackbox_llm', label: 'Blackbox / LLM' }, + ]}, + { key: 'deployment_scope', label: 'Einsatzbereich', options: [ + { value: 'internal', label: 'Intern' }, { value: 'external', label: 'Extern (Kunden)' }, + { value: 'public', label: 'Oeffentlich' }, + ]}, +] + +const TOGGLE_DIMENSIONS = [ + { key: 'transparency_required', label: 'Transparenzpflicht' }, + { key: 'logging_required', label: 'Protokollierungspflicht' }, +] + +export default function NewOptimizationPage() { + const router = useRouter() + const [title, setTitle] = useState('') + const [config, setConfig] = useState>({ + automation_level: 'assistive', decision_binding: 'non_binding', decision_impact: 'low', + domain: 'general', data_type: 'non_personal', human_in_loop: 'required', + explainability: 'basic', risk_classification: 'minimal', legal_basis: 'contract', + transparency_required: 'false', logging_required: 'false', + model_type: 'rule_based', deployment_scope: 'internal', + }) + const [preview, setPreview] = useState | null>(null) + const [submitting, setSubmitting] = useState(false) + + async function handlePreview() { + try { + const body = { ...config, transparency_required: config.transparency_required === 'true', logging_required: config.logging_required === 'true' } + const res = await fetch('/api/sdk/v1/maximizer/evaluate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) + if (res.ok) { + const data = await res.json() + setPreview(data.zone_map || {}) + } + } catch { /* silent */ } + } + + async function handleSubmit() { + setSubmitting(true) + try { + const body = { + config: { ...config, transparency_required: config.transparency_required === 'true', logging_required: config.logging_required === 'true' }, + title: title || 'Optimierung ' + new Date().toLocaleDateString('de-DE'), + } + const res = await fetch('/api/sdk/v1/maximizer/optimize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) + if (res.ok) { + const data = await res.json() + router.push(`/sdk/compliance-optimizer/${data.id}`) + } + } finally { + setSubmitting(false) + } + } + + return ( +
+

Neue Optimierung

+

Konfigurieren Sie Ihren KI-Use-Case und finden Sie den maximalen regulatorischen Spielraum.

+ +
+ + setTitle(e.target.value)} placeholder="z.B. HR Bewerber-Ranking" + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm" /> +
+ +
+ {DIMENSIONS.map((dim) => ( +
+ + +
+ ))} + + {TOGGLE_DIMENSIONS.map((dim) => ( +
+ { setConfig({ ...config, [dim.key]: String(e.target.checked) }); setPreview(null) }} + className="h-4 w-4 rounded border-gray-300 text-blue-600" /> + +
+ ))} +
+ +
+ + +
+
+ ) +} diff --git a/admin-compliance/app/sdk/compliance-optimizer/page.tsx b/admin-compliance/app/sdk/compliance-optimizer/page.tsx new file mode 100644 index 0000000..8573b8d --- /dev/null +++ b/admin-compliance/app/sdk/compliance-optimizer/page.tsx @@ -0,0 +1,124 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import Link from 'next/link' +import { ZoneBadge } from '@/components/sdk/compliance-optimizer/ZoneBadge' + +interface OptimizationSummary { + id: string + title: string + is_compliant: boolean + constraint_version: string + created_at: string + zone_map: Record + max_safe_config?: { safety_score: number; utility_score: number } +} + +function countZones(zoneMap: Record) { + let forbidden = 0, restricted = 0, safe = 0 + for (const v of Object.values(zoneMap || {})) { + if (v.zone === 'FORBIDDEN') forbidden++ + else if (v.zone === 'RESTRICTED') restricted++ + else safe++ + } + return { forbidden, restricted, safe } +} + +export default function ComplianceOptimizerPage() { + const [optimizations, setOptimizations] = useState([]) + const [loading, setLoading] = useState(true) + const [total, setTotal] = useState(0) + + useEffect(() => { + fetchOptimizations() + }, []) + + async function fetchOptimizations() { + try { + setLoading(true) + const res = await fetch('/api/sdk/v1/maximizer/optimizations?limit=20') + if (res.ok) { + const data = await res.json() + setOptimizations(data.optimizations || []) + setTotal(data.total || 0) + } + } catch { + // silent + } finally { + setLoading(false) + } + } + + return ( +
+
+
+

Compliance Optimizer

+

+ Regulatorischen Spielraum maximieren — KI-Use-Cases optimal konfigurieren +

+
+ + Neue Optimierung + +
+ + {loading ? ( +
Laden...
+ ) : optimizations.length === 0 ? ( +
+

Noch keine Optimierungen durchgefuehrt.

+ + Erste Optimierung starten + +
+ ) : ( +
+ + + + + + + + + + + {optimizations.map((o) => { + const zones = countZones(o.zone_map) + return ( + + + + + + + ) + })} + +
TitelStatusZonenDatum
+ + {o.title || 'Ohne Titel'} + + + + + {zones.forbidden > 0 && {zones.forbidden} verboten} + {zones.restricted > 0 && {zones.restricted} eingeschraenkt} + {zones.safe} erlaubt + + {new Date(o.created_at).toLocaleDateString('de-DE')} +
+ {total > 20 && ( +
+ {total} Optimierungen insgesamt +
+ )} +
+ )} +
+ ) +} diff --git a/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx b/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx index e0fd69a..a384b30 100644 --- a/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx +++ b/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx @@ -53,6 +53,7 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side } label="Use Cases" isActive={pathname?.startsWith('/sdk/use-cases') ?? false} collapsed={collapsed} projectId={projectId} /> } label="AI Act" isActive={pathname?.startsWith('/sdk/ai-act') ?? false} collapsed={collapsed} projectId={projectId} /> } label="EU Registrierung" isActive={pathname?.startsWith('/sdk/ai-registration') ?? false} collapsed={collapsed} projectId={projectId} /> + } label="Compliance Optimizer" isActive={pathname?.startsWith('/sdk/compliance-optimizer') ?? false} collapsed={collapsed} projectId={projectId} /> {/* Payment / Terminal */} diff --git a/admin-compliance/components/sdk/compliance-optimizer/ConfigComparison.tsx b/admin-compliance/components/sdk/compliance-optimizer/ConfigComparison.tsx new file mode 100644 index 0000000..a198878 --- /dev/null +++ b/admin-compliance/components/sdk/compliance-optimizer/ConfigComparison.tsx @@ -0,0 +1,52 @@ +'use client' + +interface DimensionDelta { + dimension: string + from: string + to: string + impact: string +} + +const DIMENSION_LABELS: Record = { + automation_level: 'Automatisierungsgrad', + decision_binding: 'Entscheidungsbindung', + decision_impact: 'Entscheidungswirkung', + domain: 'Branche', + data_type: 'Datensensitivitaet', + human_in_loop: 'Menschliche Kontrolle', + explainability: 'Erklaerbarkeit', + risk_classification: 'Risikoklasse', + legal_basis: 'Rechtsgrundlage', + transparency_required: 'Transparenzpflicht', + logging_required: 'Protokollierung', + model_type: 'Modelltyp', + deployment_scope: 'Einsatzbereich', +} + +export function ConfigComparison({ deltas }: { deltas: DimensionDelta[] }) { + if (deltas.length === 0) { + return ( +
+ Keine Aenderungen noetig — Ihre Konfiguration ist bereits konform. +
+ ) + } + + return ( +
+

Empfohlene Aenderungen ({deltas.length})

+
+ {deltas.map((d, i) => ( +
+ + {DIMENSION_LABELS[d.dimension] || d.dimension} + + {d.from} + + {d.to} +
+ ))} +
+
+ ) +} diff --git a/admin-compliance/components/sdk/compliance-optimizer/DimensionZoneTable.tsx b/admin-compliance/components/sdk/compliance-optimizer/DimensionZoneTable.tsx new file mode 100644 index 0000000..b95c619 --- /dev/null +++ b/admin-compliance/components/sdk/compliance-optimizer/DimensionZoneTable.tsx @@ -0,0 +1,74 @@ +'use client' + +import { ZoneBadge } from './ZoneBadge' + +interface ZoneInfo { + dimension: string + current_value: string + zone: 'FORBIDDEN' | 'RESTRICTED' | 'SAFE' + allowed_values?: string[] + forbidden_values?: string[] + safeguards?: string[] + reason: string + obligation_refs: string[] +} + +const DIMENSION_LABELS: Record = { + automation_level: 'Automatisierungsgrad', + decision_binding: 'Entscheidungsbindung', + decision_impact: 'Entscheidungswirkung', + domain: 'Branche', + data_type: 'Datensensitivitaet', + human_in_loop: 'Menschliche Kontrolle', + explainability: 'Erklaerbarkeit', + risk_classification: 'Risikoklasse', + legal_basis: 'Rechtsgrundlage', + transparency_required: 'Transparenzpflicht', + logging_required: 'Protokollierung', + model_type: 'Modelltyp', + deployment_scope: 'Einsatzbereich', +} + +export function DimensionZoneTable({ zoneMap }: { zoneMap: Record }) { + const dimensions = Object.entries(zoneMap).sort(([, a], [, b]) => { + const order = { FORBIDDEN: 0, RESTRICTED: 1, SAFE: 2 } + return (order[a.zone] ?? 2) - (order[b.zone] ?? 2) + }) + + return ( +
+ + + + + + + + + + + + {dimensions.map(([dim, info]) => ( + + + + + + + + ))} + +
DimensionAktueller WertZoneRegelgrundRechtsgrundlage
+ {DIMENSION_LABELS[dim] || dim} + + {info.current_value} + + + + {info.reason || '-'} + + {info.obligation_refs?.join(', ') || '-'} +
+
+ ) +} diff --git a/admin-compliance/components/sdk/compliance-optimizer/OptimizationScoreCard.tsx b/admin-compliance/components/sdk/compliance-optimizer/OptimizationScoreCard.tsx new file mode 100644 index 0000000..1cba729 --- /dev/null +++ b/admin-compliance/components/sdk/compliance-optimizer/OptimizationScoreCard.tsx @@ -0,0 +1,50 @@ +'use client' + +interface ScoreCardProps { + safetyScore: number + utilityScore: number + compositeScore: number + deltaCount: number +} + +function ScoreGauge({ value, label, color }: { value: number; label: string; color: string }) { + return ( +
+
+ + + + + + {value} + +
+ {label} +
+ ) +} + +export function OptimizationScoreCard({ safetyScore, utilityScore, compositeScore, deltaCount }: ScoreCardProps) { + return ( +
+

Bewertung der optimierten Konfiguration

+
+ + + +
+ {deltaCount} + Aenderungen +
+
+
+ ) +} diff --git a/admin-compliance/components/sdk/compliance-optimizer/ZoneBadge.tsx b/admin-compliance/components/sdk/compliance-optimizer/ZoneBadge.tsx new file mode 100644 index 0000000..9bff557 --- /dev/null +++ b/admin-compliance/components/sdk/compliance-optimizer/ZoneBadge.tsx @@ -0,0 +1,16 @@ +'use client' + +const ZONE_STYLES = { + FORBIDDEN: { bg: 'bg-red-100', text: 'text-red-700', border: 'border-red-300', label: 'Verboten' }, + RESTRICTED: { bg: 'bg-yellow-100', text: 'text-yellow-700', border: 'border-yellow-300', label: 'Eingeschraenkt' }, + SAFE: { bg: 'bg-green-100', text: 'text-green-700', border: 'border-green-300', label: 'Erlaubt' }, +} + +export function ZoneBadge({ zone }: { zone: 'FORBIDDEN' | 'RESTRICTED' | 'SAFE' }) { + const style = ZONE_STYLES[zone] || ZONE_STYLES.SAFE + return ( + + {style.label} + + ) +} diff --git a/ai-compliance-sdk/internal/api/handlers/maximizer_handlers.go b/ai-compliance-sdk/internal/api/handlers/maximizer_handlers.go new file mode 100644 index 0000000..530099e --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/maximizer_handlers.go @@ -0,0 +1,155 @@ +package handlers + +import ( + "net/http" + + "github.com/breakpilot/ai-compliance-sdk/internal/maximizer" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// MaximizerHandlers exposes the Compliance Maximizer API. +type MaximizerHandlers struct { + svc *maximizer.Service +} + +// NewMaximizerHandlers creates handlers backed by a maximizer service. +func NewMaximizerHandlers(svc *maximizer.Service) *MaximizerHandlers { + return &MaximizerHandlers{svc: svc} +} + +// Optimize evaluates a DimensionConfig and returns optimized variants. +func (h *MaximizerHandlers) Optimize(c *gin.Context) { + var req maximizer.OptimizeInput + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + req.TenantID, _ = getTenantID(c) + req.UserID = maximizerGetUserID(c) + result, err := h.svc.Optimize(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, result) +} + +// OptimizeFromIntake maps a UseCaseIntake to dimensions and optimizes. +func (h *MaximizerHandlers) OptimizeFromIntake(c *gin.Context) { + var req maximizer.OptimizeFromIntakeInput + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + req.TenantID, _ = getTenantID(c) + req.UserID = maximizerGetUserID(c) + result, err := h.svc.OptimizeFromIntake(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, result) +} + +// OptimizeFromAssessment optimizes an existing UCCA assessment. +func (h *MaximizerHandlers) OptimizeFromAssessment(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assessment id"}) + return + } + tid, _ := getTenantID(c) + uid := maximizerGetUserID(c) + result, err := h.svc.OptimizeFromAssessment(c.Request.Context(), id, tid, uid) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, result) +} + +// Evaluate performs a 3-zone evaluation without persisting. +func (h *MaximizerHandlers) Evaluate(c *gin.Context) { + var config maximizer.DimensionConfig + if err := c.ShouldBindJSON(&config); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + result := h.svc.Evaluate(&config) + c.JSON(http.StatusOK, result) +} + +// ListOptimizations returns stored optimizations for the tenant. +func (h *MaximizerHandlers) ListOptimizations(c *gin.Context) { + f := &maximizer.OptimizationFilters{ + Search: c.Query("search"), + Limit: maximizerParseInt(c.Query("limit"), 20), + Offset: maximizerParseInt(c.Query("offset"), 0), + } + tid, _ := getTenantID(c) + results, total, err := h.svc.ListOptimizations(c.Request.Context(), tid, f) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"optimizations": results, "total": total}) +} + +// GetOptimization returns a single optimization. +func (h *MaximizerHandlers) GetOptimization(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + result, err := h.svc.GetOptimization(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } + c.JSON(http.StatusOK, result) +} + +// DeleteOptimization removes an optimization. +func (h *MaximizerHandlers) DeleteOptimization(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + if err := h.svc.DeleteOptimization(c.Request.Context(), id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusNoContent, nil) +} + +// GetDimensionSchema returns the dimension enum values for the frontend. +func (h *MaximizerHandlers) GetDimensionSchema(c *gin.Context) { + c.JSON(http.StatusOK, h.svc.GetDimensionSchema()) +} + +func maximizerGetUserID(c *gin.Context) uuid.UUID { + if id, exists := c.Get("user_id"); exists { + if uid, ok := id.(uuid.UUID); ok { + return uid + } + } + return uuid.Nil +} + +// maximizerParseInt is a local helper for query param parsing. +func maximizerParseInt(s string, def int) int { + if s == "" { + return def + } + n := 0 + for _, c := range s { + if c < '0' || c > '9' { + return def + } + n = n*10 + int(c-'0') + } + return n +} diff --git a/ai-compliance-sdk/internal/app/app.go b/ai-compliance-sdk/internal/app/app.go index bf27feb..1d29112 100644 --- a/ai-compliance-sdk/internal/app/app.go +++ b/ai-compliance-sdk/internal/app/app.go @@ -15,6 +15,7 @@ import ( "github.com/breakpilot/ai-compliance-sdk/internal/config" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/breakpilot/ai-compliance-sdk/internal/llm" + "github.com/breakpilot/ai-compliance-sdk/internal/maximizer" "github.com/breakpilot/ai-compliance-sdk/internal/portfolio" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" @@ -132,6 +133,17 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { trainingHandlers := handlers.NewTrainingHandlers(trainingStore, contentGenerator, blockGenerator, ttsClient) ragHandlers := handlers.NewRAGHandlers(corpusVersionStore) obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore) + + // Maximizer + maximizerStore := maximizer.NewStore(pool) + maximizerRules, err := maximizer.LoadConstraintRulesFromDefault() + if err != nil { + log.Printf("WARNING: Maximizer constraints not loaded: %v", err) + maximizerRules = &maximizer.ConstraintRuleSet{Version: "0.0.0"} + } + maximizerSvc := maximizer.NewService(maximizerStore, uccaStore, maximizerRules) + maximizerHandlers := handlers.NewMaximizerHandlers(maximizerSvc) + rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine) // Router @@ -155,7 +167,8 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { rbacHandlers, llmHandlers, auditHandlers, uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers, roadmapHandlers, workshopHandlers, portfolioHandlers, - academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler) + academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler, + maximizerHandlers) return router } diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go index 553a049..23f593a 100644 --- a/ai-compliance-sdk/internal/app/routes.go +++ b/ai-compliance-sdk/internal/app/routes.go @@ -26,6 +26,7 @@ func registerRoutes( trainingHandlers *handlers.TrainingHandlers, whistleblowerHandlers *handlers.WhistleblowerHandlers, iaceHandler *handlers.IACEHandler, + maximizerHandlers *handlers.MaximizerHandlers, ) { v1 := router.Group("/sdk/v1") { @@ -46,6 +47,7 @@ func registerRoutes( registerTrainingRoutes(v1, trainingHandlers) registerWhistleblowerRoutes(v1, whistleblowerHandlers) registerIACERoutes(v1, iaceHandler) + registerMaximizerRoutes(v1, maximizerHandlers) } } @@ -407,3 +409,17 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.POST("/projects/:id/tech-file/:section/enrich", h.EnrichTechFileSection) } } + +func registerMaximizerRoutes(v1 *gin.RouterGroup, h *handlers.MaximizerHandlers) { + m := v1.Group("/maximizer") + { + m.POST("/optimize", h.Optimize) + m.POST("/optimize-from-intake", h.OptimizeFromIntake) + m.POST("/optimize-from-assessment/:id", h.OptimizeFromAssessment) + m.POST("/evaluate", h.Evaluate) + m.GET("/optimizations", h.ListOptimizations) + m.GET("/optimizations/:id", h.GetOptimization) + m.DELETE("/optimizations/:id", h.DeleteOptimization) + m.GET("/dimensions", h.GetDimensionSchema) + } +} diff --git a/ai-compliance-sdk/internal/maximizer/constraint_loader.go b/ai-compliance-sdk/internal/maximizer/constraint_loader.go new file mode 100644 index 0000000..fdf7f86 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/constraint_loader.go @@ -0,0 +1,52 @@ +package maximizer + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" +) + +const defaultConstraintFile = "policies/maximizer_constraints_v1.json" + +// LoadConstraintRules reads a constraint ruleset from a JSON file. +func LoadConstraintRules(path string) (*ConstraintRuleSet, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read constraint file %s: %w", path, err) + } + var rs ConstraintRuleSet + if err := json.Unmarshal(data, &rs); err != nil { + return nil, fmt.Errorf("parse constraint file %s: %w", path, err) + } + if rs.Version == "" { + return nil, fmt.Errorf("constraint file %s: missing version", path) + } + return &rs, nil +} + +// LoadConstraintRulesFromDefault loads from the default policy file +// relative to the project root. +func LoadConstraintRulesFromDefault() (*ConstraintRuleSet, error) { + root := findProjectRoot() + path := filepath.Join(root, defaultConstraintFile) + return LoadConstraintRules(path) +} + +// findProjectRoot walks up from the current source file to find the +// ai-compliance-sdk root (contains go.mod or policies/). +func findProjectRoot() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "." + } + dir := filepath.Dir(filename) + for i := 0; i < 10; i++ { + if _, err := os.Stat(filepath.Join(dir, "policies")); err == nil { + return dir + } + dir = filepath.Dir(dir) + } + return "." +} diff --git a/ai-compliance-sdk/internal/maximizer/constraints.go b/ai-compliance-sdk/internal/maximizer/constraints.go new file mode 100644 index 0000000..a472740 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/constraints.go @@ -0,0 +1,76 @@ +package maximizer + +// ConstraintRuleSet is the top-level container loaded from maximizer_constraints_v1.json. +type ConstraintRuleSet struct { + Version string `json:"version"` + Regulations []string `json:"regulations"` + Rules []ConstraintRule `json:"rules"` +} + +// ConstraintRule maps a regulatory obligation to dimension restrictions. +type ConstraintRule struct { + ID string `json:"id"` + ObligationID string `json:"obligation_id"` + Regulation string `json:"regulation"` + ArticleRef string `json:"article_ref"` + Title string `json:"title"` + Description string `json:"description"` + RuleType string `json:"rule_type"` // hard_prohibition, requirement, classification_rule, optimizer_rule, escalation_gate + Constraints []Constraint `json:"constraints"` +} + +// Constraint is a single if-then rule on the dimension space. +type Constraint struct { + If ConditionSet `json:"if"` + Then EffectSet `json:"then"` +} + +// ConditionSet maps dimension names to their required values. +// Values can be a string (exact match) or []string (any of). +type ConditionSet map[string]interface{} + +// EffectSet defines what must be true when the condition matches. +type EffectSet struct { + // Allowed=false means hard block — no optimization possible for this rule + Allowed *bool `json:"allowed,omitempty"` + + // RequiredValues: dimension must have exactly this value + RequiredValues map[string]string `json:"required_values,omitempty"` + + // RequiredControls: organizational/technical controls needed + RequiredControls []string `json:"required_controls,omitempty"` + + // RequiredPatterns: architectural patterns needed + RequiredPatterns []string `json:"required_patterns,omitempty"` + + // Classification overrides + SetRiskClassification string `json:"set_risk_classification,omitempty"` +} + +// Matches checks if a DimensionConfig satisfies all conditions in this set. +func (cs ConditionSet) Matches(config *DimensionConfig) bool { + for dim, expected := range cs { + actual := config.GetValue(dim) + if actual == "" { + return false + } + switch v := expected.(type) { + case string: + if actual != v { + return false + } + case []interface{}: + found := false + for _, item := range v { + if s, ok := item.(string); ok && actual == s { + found = true + break + } + } + if !found { + return false + } + } + } + return true +} diff --git a/ai-compliance-sdk/internal/maximizer/dimensions.go b/ai-compliance-sdk/internal/maximizer/dimensions.go new file mode 100644 index 0000000..5efac03 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/dimensions.go @@ -0,0 +1,306 @@ +package maximizer + +// DimensionConfig is the normalized representation of an AI use case +// as a point in a 13-dimensional regulatory constraint space. +// Each dimension maps to regulatory obligations from DSGVO, AI Act, etc. +type DimensionConfig struct { + AutomationLevel AutomationLevel `json:"automation_level"` + DecisionBinding DecisionBinding `json:"decision_binding"` + DecisionImpact DecisionImpact `json:"decision_impact"` + Domain DomainCategory `json:"domain"` + DataType DataTypeSensitivity `json:"data_type"` + HumanInLoop HumanInLoopLevel `json:"human_in_loop"` + Explainability ExplainabilityLevel `json:"explainability"` + RiskClassification RiskClass `json:"risk_classification"` + LegalBasis LegalBasisType `json:"legal_basis"` + TransparencyRequired bool `json:"transparency_required"` + LoggingRequired bool `json:"logging_required"` + ModelType ModelType `json:"model_type"` + DeploymentScope DeploymentScope `json:"deployment_scope"` +} + +// --- Dimension Enums --- + +type AutomationLevel string + +const ( + AutoNone AutomationLevel = "none" + AutoAssistive AutomationLevel = "assistive" + AutoPartial AutomationLevel = "partial" + AutoFull AutomationLevel = "full" +) + +type DecisionBinding string + +const ( + BindingNonBinding DecisionBinding = "non_binding" + BindingHumanReview DecisionBinding = "human_review_required" + BindingFullyBinding DecisionBinding = "fully_binding" +) + +type DecisionImpact string + +const ( + ImpactLow DecisionImpact = "low" + ImpactMedium DecisionImpact = "medium" + ImpactHigh DecisionImpact = "high" +) + +type DomainCategory string + +const ( + DomainHR DomainCategory = "hr" + DomainFinance DomainCategory = "finance" + DomainEducation DomainCategory = "education" + DomainHealth DomainCategory = "health" + DomainMarketing DomainCategory = "marketing" + DomainGeneral DomainCategory = "general" +) + +type DataTypeSensitivity string + +const ( + DataNonPersonal DataTypeSensitivity = "non_personal" + DataPersonal DataTypeSensitivity = "personal" + DataSensitive DataTypeSensitivity = "sensitive" + DataBiometric DataTypeSensitivity = "biometric" +) + +type HumanInLoopLevel string + +const ( + HILNone HumanInLoopLevel = "none" + HILOptional HumanInLoopLevel = "optional" + HILRequired HumanInLoopLevel = "required" +) + +type ExplainabilityLevel string + +const ( + ExplainNone ExplainabilityLevel = "none" + ExplainBasic ExplainabilityLevel = "basic" + ExplainHigh ExplainabilityLevel = "high" +) + +type RiskClass string + +const ( + RiskMinimal RiskClass = "minimal" + RiskLimited RiskClass = "limited" + RiskHigh RiskClass = "high" + RiskProhibited RiskClass = "prohibited" +) + +type LegalBasisType string + +const ( + LegalConsent LegalBasisType = "consent" + LegalContract LegalBasisType = "contract" + LegalLegalObligation LegalBasisType = "legal_obligation" + LegalLegitimateInterest LegalBasisType = "legitimate_interest" + LegalPublicInterest LegalBasisType = "public_interest" +) + +type ModelType string + +const ( + ModelRuleBased ModelType = "rule_based" + ModelStatistical ModelType = "statistical" + ModelBlackboxLLM ModelType = "blackbox_llm" +) + +type DeploymentScope string + +const ( + ScopeInternal DeploymentScope = "internal" + ScopeExternal DeploymentScope = "external" + ScopePublic DeploymentScope = "public" +) + +// --- Ordinal Orderings (higher = more regulatory risk) --- + +var automationOrder = map[AutomationLevel]int{ + AutoNone: 0, AutoAssistive: 1, AutoPartial: 2, AutoFull: 3, +} + +var bindingOrder = map[DecisionBinding]int{ + BindingNonBinding: 0, BindingHumanReview: 1, BindingFullyBinding: 2, +} + +var impactOrder = map[DecisionImpact]int{ + ImpactLow: 0, ImpactMedium: 1, ImpactHigh: 2, +} + +var dataTypeOrder = map[DataTypeSensitivity]int{ + DataNonPersonal: 0, DataPersonal: 1, DataSensitive: 2, DataBiometric: 3, +} + +var hilOrder = map[HumanInLoopLevel]int{ + HILRequired: 0, HILOptional: 1, HILNone: 2, +} + +var explainOrder = map[ExplainabilityLevel]int{ + ExplainHigh: 0, ExplainBasic: 1, ExplainNone: 2, +} + +var riskOrder = map[RiskClass]int{ + RiskMinimal: 0, RiskLimited: 1, RiskHigh: 2, RiskProhibited: 3, +} + +var modelTypeOrder = map[ModelType]int{ + ModelRuleBased: 0, ModelStatistical: 1, ModelBlackboxLLM: 2, +} + +var scopeOrder = map[DeploymentScope]int{ + ScopeInternal: 0, ScopeExternal: 1, ScopePublic: 2, +} + +// AllValues returns the ordered list of allowed values for each dimension. +var AllValues = map[string][]string{ + "automation_level": {"none", "assistive", "partial", "full"}, + "decision_binding": {"non_binding", "human_review_required", "fully_binding"}, + "decision_impact": {"low", "medium", "high"}, + "domain": {"hr", "finance", "education", "health", "marketing", "general"}, + "data_type": {"non_personal", "personal", "sensitive", "biometric"}, + "human_in_loop": {"required", "optional", "none"}, + "explainability": {"high", "basic", "none"}, + "risk_classification": {"minimal", "limited", "high", "prohibited"}, + "legal_basis": {"consent", "contract", "legal_obligation", "legitimate_interest", "public_interest"}, + "transparency_required": {"true", "false"}, + "logging_required": {"true", "false"}, + "model_type": {"rule_based", "statistical", "blackbox_llm"}, + "deployment_scope": {"internal", "external", "public"}, +} + +// DimensionDelta represents a single change between two configs. +type DimensionDelta struct { + Dimension string `json:"dimension"` + From string `json:"from"` + To string `json:"to"` + Impact string `json:"impact"` // human-readable impact description +} + +// GetValue returns the string value of a dimension by name. +func (d *DimensionConfig) GetValue(dimension string) string { + switch dimension { + case "automation_level": + return string(d.AutomationLevel) + case "decision_binding": + return string(d.DecisionBinding) + case "decision_impact": + return string(d.DecisionImpact) + case "domain": + return string(d.Domain) + case "data_type": + return string(d.DataType) + case "human_in_loop": + return string(d.HumanInLoop) + case "explainability": + return string(d.Explainability) + case "risk_classification": + return string(d.RiskClassification) + case "legal_basis": + return string(d.LegalBasis) + case "transparency_required": + if d.TransparencyRequired { + return "true" + } + return "false" + case "logging_required": + if d.LoggingRequired { + return "true" + } + return "false" + case "model_type": + return string(d.ModelType) + case "deployment_scope": + return string(d.DeploymentScope) + default: + return "" + } +} + +// SetValue sets a dimension value by name. Returns false if the dimension is unknown. +func (d *DimensionConfig) SetValue(dimension, value string) bool { + switch dimension { + case "automation_level": + d.AutomationLevel = AutomationLevel(value) + case "decision_binding": + d.DecisionBinding = DecisionBinding(value) + case "decision_impact": + d.DecisionImpact = DecisionImpact(value) + case "domain": + d.Domain = DomainCategory(value) + case "data_type": + d.DataType = DataTypeSensitivity(value) + case "human_in_loop": + d.HumanInLoop = HumanInLoopLevel(value) + case "explainability": + d.Explainability = ExplainabilityLevel(value) + case "risk_classification": + d.RiskClassification = RiskClass(value) + case "legal_basis": + d.LegalBasis = LegalBasisType(value) + case "transparency_required": + d.TransparencyRequired = value == "true" + case "logging_required": + d.LoggingRequired = value == "true" + case "model_type": + d.ModelType = ModelType(value) + case "deployment_scope": + d.DeploymentScope = DeploymentScope(value) + default: + return false + } + return true +} + +// Diff computes the changes between two configs. +func (d *DimensionConfig) Diff(other *DimensionConfig) []DimensionDelta { + dimensions := []string{ + "automation_level", "decision_binding", "decision_impact", "domain", + "data_type", "human_in_loop", "explainability", "risk_classification", + "legal_basis", "transparency_required", "logging_required", + "model_type", "deployment_scope", + } + var deltas []DimensionDelta + for _, dim := range dimensions { + from := d.GetValue(dim) + to := other.GetValue(dim) + if from != to { + deltas = append(deltas, DimensionDelta{ + Dimension: dim, + From: from, + To: to, + Impact: describeDeltaImpact(dim, from, to), + }) + } + } + return deltas +} + +// Clone returns a deep copy of the config. +func (d *DimensionConfig) Clone() DimensionConfig { + return *d +} + +func describeDeltaImpact(dimension, from, to string) string { + switch dimension { + case "automation_level": + return "Automatisierungsgrad: " + from + " → " + to + case "decision_binding": + return "Entscheidungsbindung: " + from + " → " + to + case "human_in_loop": + return "Menschliche Kontrolle: " + from + " → " + to + case "explainability": + return "Erklaerbarkeit: " + from + " → " + to + case "data_type": + return "Datensensitivitaet: " + from + " → " + to + case "transparency_required": + return "Transparenzpflicht: " + from + " → " + to + case "logging_required": + return "Protokollierungspflicht: " + from + " → " + to + default: + return dimension + ": " + from + " → " + to + } +} diff --git a/ai-compliance-sdk/internal/maximizer/dimensions_test.go b/ai-compliance-sdk/internal/maximizer/dimensions_test.go new file mode 100644 index 0000000..f4f111b --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/dimensions_test.go @@ -0,0 +1,201 @@ +package maximizer + +import ( + "testing" + + "github.com/breakpilot/ai-compliance-sdk/internal/ucca" +) + +func TestGetValueSetValueRoundtrip(t *testing.T) { + config := DimensionConfig{ + AutomationLevel: AutoFull, + DecisionBinding: BindingFullyBinding, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILNone, + Explainability: ExplainNone, + RiskClassification: RiskHigh, + LegalBasis: LegalContract, + TransparencyRequired: true, + LoggingRequired: false, + ModelType: ModelBlackboxLLM, + DeploymentScope: ScopeExternal, + } + + for _, dim := range allDimensions { + val := config.GetValue(dim) + if val == "" { + t.Errorf("GetValue(%q) returned empty", dim) + } + clone := DimensionConfig{} + ok := clone.SetValue(dim, val) + if !ok { + t.Errorf("SetValue(%q, %q) returned false", dim, val) + } + if clone.GetValue(dim) != val { + t.Errorf("SetValue roundtrip failed for %q: got %q, want %q", dim, clone.GetValue(dim), val) + } + } +} + +func TestGetValueUnknownDimension(t *testing.T) { + config := DimensionConfig{} + if v := config.GetValue("nonexistent"); v != "" { + t.Errorf("expected empty for unknown dimension, got %q", v) + } + if ok := config.SetValue("nonexistent", "x"); ok { + t.Error("expected false for unknown dimension") + } +} + +func TestDiffIdentical(t *testing.T) { + config := DimensionConfig{ + AutomationLevel: AutoAssistive, + DecisionImpact: ImpactLow, + Domain: DomainGeneral, + } + deltas := config.Diff(&config) + if len(deltas) != 0 { + t.Errorf("expected 0 deltas for identical configs, got %d", len(deltas)) + } +} + +func TestDiffDetectsChanges(t *testing.T) { + a := DimensionConfig{ + AutomationLevel: AutoFull, + HumanInLoop: HILNone, + DecisionBinding: BindingFullyBinding, + } + b := DimensionConfig{ + AutomationLevel: AutoAssistive, + HumanInLoop: HILRequired, + DecisionBinding: BindingHumanReview, + } + deltas := a.Diff(&b) + + changed := make(map[string]bool) + for _, d := range deltas { + changed[d.Dimension] = true + } + for _, dim := range []string{"automation_level", "human_in_loop", "decision_binding"} { + if !changed[dim] { + t.Errorf("expected %q in deltas", dim) + } + } +} + +func TestClone(t *testing.T) { + orig := DimensionConfig{ + AutomationLevel: AutoFull, + Domain: DomainHR, + } + clone := orig.Clone() + clone.AutomationLevel = AutoAssistive + if orig.AutomationLevel != AutoFull { + t.Error("clone modified original") + } +} + +func TestMapIntakeToDimensions(t *testing.T) { + intake := &ucca.UseCaseIntake{ + Domain: "hr", + Automation: ucca.AutomationFullyAutomated, + DataTypes: ucca.DataTypes{ + PersonalData: true, + Article9Data: true, + }, + Purpose: ucca.Purpose{ + DecisionMaking: true, + }, + Outputs: ucca.Outputs{ + LegalEffects: true, + }, + ModelUsage: ucca.ModelUsage{ + Training: true, + }, + } + + config := MapIntakeToDimensions(intake) + + tests := []struct { + dimension string + expected string + }{ + {"automation_level", "full"}, + {"domain", "hr"}, + {"data_type", "sensitive"}, + {"decision_impact", "high"}, + {"model_type", "blackbox_llm"}, + {"human_in_loop", "none"}, + {"decision_binding", "fully_binding"}, + } + for _, tc := range tests { + got := config.GetValue(tc.dimension) + if got != tc.expected { + t.Errorf("MapIntakeToDimensions: %s = %q, want %q", tc.dimension, got, tc.expected) + } + } +} + +func TestMapIntakeToDimensionsBiometricWins(t *testing.T) { + intake := &ucca.UseCaseIntake{ + DataTypes: ucca.DataTypes{ + PersonalData: true, + Article9Data: true, + BiometricData: true, + }, + } + config := MapIntakeToDimensions(intake) + if config.DataType != DataBiometric { + t.Errorf("expected biometric (highest sensitivity), got %s", config.DataType) + } +} + +func TestMapDimensionsToIntakePreservesOriginal(t *testing.T) { + original := &ucca.UseCaseIntake{ + UseCaseText: "Test use case", + Domain: "hr", + Title: "My Assessment", + Automation: ucca.AutomationFullyAutomated, + DataTypes: ucca.DataTypes{ + PersonalData: true, + }, + Hosting: ucca.Hosting{ + Region: "eu", + }, + } + + config := &DimensionConfig{ + AutomationLevel: AutoAssistive, + DataType: DataPersonal, + Domain: DomainHR, + } + + result := MapDimensionsToIntake(config, original) + + if result.UseCaseText != "Test use case" { + t.Error("MapDimensionsToIntake did not preserve UseCaseText") + } + if result.Title != "My Assessment" { + t.Error("MapDimensionsToIntake did not preserve Title") + } + if result.Hosting.Region != "eu" { + t.Error("MapDimensionsToIntake did not preserve Hosting") + } + if result.Automation != ucca.AutomationAssistive { + t.Errorf("expected assistive automation, got %s", result.Automation) + } +} + +func TestAllValuesComplete(t *testing.T) { + for _, dim := range allDimensions { + vals, ok := AllValues[dim] + if !ok { + t.Errorf("AllValues missing dimension %q", dim) + } + if len(vals) == 0 { + t.Errorf("AllValues[%q] is empty", dim) + } + } +} diff --git a/ai-compliance-sdk/internal/maximizer/evaluator.go b/ai-compliance-sdk/internal/maximizer/evaluator.go new file mode 100644 index 0000000..e64a97b --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/evaluator.go @@ -0,0 +1,218 @@ +package maximizer + +// Zone classifies a dimension value's regulatory status. +type Zone string + +const ( + ZoneForbidden Zone = "FORBIDDEN" + ZoneRestricted Zone = "RESTRICTED" + ZoneSafe Zone = "SAFE" +) + +// ZoneInfo classifies a single dimension value within the constraint space. +type ZoneInfo struct { + Dimension string `json:"dimension"` + CurrentValue string `json:"current_value"` + Zone Zone `json:"zone"` + AllowedValues []string `json:"allowed_values,omitempty"` + ForbiddenValues []string `json:"forbidden_values,omitempty"` + Safeguards []string `json:"safeguards,omitempty"` + Reason string `json:"reason"` + ObligationRefs []string `json:"obligation_refs"` +} + +// Violation is a hard block triggered by a constraint rule. +type Violation struct { + RuleID string `json:"rule_id"` + ObligationID string `json:"obligation_id"` + ArticleRef string `json:"article_ref"` + Title string `json:"title"` + Description string `json:"description"` + Dimension string `json:"dimension,omitempty"` +} + +// Restriction is a safeguard requirement (yellow zone). +type Restriction struct { + RuleID string `json:"rule_id"` + ObligationID string `json:"obligation_id"` + ArticleRef string `json:"article_ref"` + Title string `json:"title"` + Required map[string]string `json:"required"` +} + +// TriggeredConstraint records which constraint rule was triggered and why. +type TriggeredConstraint struct { + RuleID string `json:"rule_id"` + ObligationID string `json:"obligation_id"` + Regulation string `json:"regulation"` + ArticleRef string `json:"article_ref"` + Title string `json:"title"` + RuleType string `json:"rule_type"` +} + +// EvaluationResult is the complete 3-zone analysis of a DimensionConfig. +type EvaluationResult struct { + IsCompliant bool `json:"is_compliant"` + Violations []Violation `json:"violations"` + Restrictions []Restriction `json:"restrictions"` + ZoneMap map[string]ZoneInfo `json:"zone_map"` + RequiredControls []string `json:"required_controls"` + RequiredPatterns []string `json:"required_patterns"` + TriggeredRules []TriggeredConstraint `json:"triggered_rules"` + RiskClassification string `json:"risk_classification,omitempty"` +} + +// Evaluator evaluates dimension configs against constraint rules. +type Evaluator struct { + rules *ConstraintRuleSet +} + +// NewEvaluator creates an evaluator from a loaded constraint ruleset. +func NewEvaluator(rules *ConstraintRuleSet) *Evaluator { + return &Evaluator{rules: rules} +} + +// Evaluate checks a config against all constraints and produces a 3-zone result. +func (e *Evaluator) Evaluate(config *DimensionConfig) *EvaluationResult { + result := &EvaluationResult{ + IsCompliant: true, + ZoneMap: make(map[string]ZoneInfo), + RequiredControls: []string{}, + RequiredPatterns: []string{}, + } + + // Initialize all dimensions as SAFE + for _, dim := range allDimensions { + result.ZoneMap[dim] = ZoneInfo{ + Dimension: dim, + CurrentValue: config.GetValue(dim), + Zone: ZoneSafe, + } + } + + // Evaluate each rule + for _, rule := range e.rules.Rules { + e.evaluateRule(config, &rule, result) + } + + // Apply risk classification if set + if result.RiskClassification == "" { + result.RiskClassification = string(config.RiskClassification) + } + + return result +} + +func (e *Evaluator) evaluateRule(config *DimensionConfig, rule *ConstraintRule, result *EvaluationResult) { + for _, constraint := range rule.Constraints { + if !constraint.If.Matches(config) { + continue + } + + // Rule triggered + result.TriggeredRules = append(result.TriggeredRules, TriggeredConstraint{ + RuleID: rule.ID, + ObligationID: rule.ObligationID, + Regulation: rule.Regulation, + ArticleRef: rule.ArticleRef, + Title: rule.Title, + RuleType: rule.RuleType, + }) + + // Hard block? + if constraint.Then.Allowed != nil && !*constraint.Then.Allowed { + result.IsCompliant = false + result.Violations = append(result.Violations, Violation{ + RuleID: rule.ID, + ObligationID: rule.ObligationID, + ArticleRef: rule.ArticleRef, + Title: rule.Title, + Description: rule.Description, + }) + e.markForbiddenDimensions(config, constraint.If, rule, result) + continue + } + + // Required values (yellow zone)? + if len(constraint.Then.RequiredValues) > 0 { + e.applyRequiredValues(config, constraint.Then.RequiredValues, rule, result) + } + + // Risk classification override + if constraint.Then.SetRiskClassification != "" { + result.RiskClassification = constraint.Then.SetRiskClassification + } + + // Collect controls and patterns + result.RequiredControls = appendUnique(result.RequiredControls, constraint.Then.RequiredControls...) + result.RequiredPatterns = appendUnique(result.RequiredPatterns, constraint.Then.RequiredPatterns...) + } +} + +// markForbiddenDimensions marks the dimensions from the condition as FORBIDDEN. +func (e *Evaluator) markForbiddenDimensions( + config *DimensionConfig, cond ConditionSet, rule *ConstraintRule, result *EvaluationResult, +) { + for dim := range cond { + zi := result.ZoneMap[dim] + zi.Zone = ZoneForbidden + zi.Reason = rule.Title + zi.ObligationRefs = appendUnique(zi.ObligationRefs, rule.ArticleRef) + zi.ForbiddenValues = appendUnique(zi.ForbiddenValues, config.GetValue(dim)) + result.ZoneMap[dim] = zi + } +} + +// applyRequiredValues checks if required dimension values are met. +func (e *Evaluator) applyRequiredValues( + config *DimensionConfig, required map[string]string, rule *ConstraintRule, result *EvaluationResult, +) { + unmet := make(map[string]string) + for dim, requiredVal := range required { + actual := config.GetValue(dim) + if actual != requiredVal { + unmet[dim] = requiredVal + // Mark as RESTRICTED (upgrade from SAFE, but don't downgrade from FORBIDDEN) + zi := result.ZoneMap[dim] + if zi.Zone != ZoneForbidden { + zi.Zone = ZoneRestricted + zi.Reason = rule.Title + zi.AllowedValues = appendUnique(zi.AllowedValues, requiredVal) + zi.Safeguards = appendUnique(zi.Safeguards, rule.ArticleRef) + zi.ObligationRefs = appendUnique(zi.ObligationRefs, rule.ArticleRef) + result.ZoneMap[dim] = zi + } + } + } + if len(unmet) > 0 { + result.IsCompliant = false + result.Restrictions = append(result.Restrictions, Restriction{ + RuleID: rule.ID, + ObligationID: rule.ObligationID, + ArticleRef: rule.ArticleRef, + Title: rule.Title, + Required: unmet, + }) + } +} + +var allDimensions = []string{ + "automation_level", "decision_binding", "decision_impact", "domain", + "data_type", "human_in_loop", "explainability", "risk_classification", + "legal_basis", "transparency_required", "logging_required", + "model_type", "deployment_scope", +} + +func appendUnique(slice []string, items ...string) []string { + seen := make(map[string]bool, len(slice)) + for _, s := range slice { + seen[s] = true + } + for _, item := range items { + if item != "" && !seen[item] { + slice = append(slice, item) + seen[item] = true + } + } + return slice +} diff --git a/ai-compliance-sdk/internal/maximizer/evaluator_test.go b/ai-compliance-sdk/internal/maximizer/evaluator_test.go new file mode 100644 index 0000000..9082a74 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/evaluator_test.go @@ -0,0 +1,229 @@ +package maximizer + +import ( + "path/filepath" + "runtime" + "testing" +) + +func loadTestRules(t *testing.T) *ConstraintRuleSet { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("cannot determine test file location") + } + // Walk up from internal/maximizer/ to ai-compliance-sdk/ + dir := filepath.Dir(filename) // internal/maximizer + dir = filepath.Dir(dir) // internal + dir = filepath.Dir(dir) // ai-compliance-sdk + path := filepath.Join(dir, "policies", "maximizer_constraints_v1.json") + rules, err := LoadConstraintRules(path) + if err != nil { + t.Fatalf("LoadConstraintRules: %v", err) + } + return rules +} + +func TestLoadConstraintRules(t *testing.T) { + rules := loadTestRules(t) + if rules.Version != "1.0.0" { + t.Errorf("expected version 1.0.0, got %s", rules.Version) + } + if len(rules.Rules) < 20 { + t.Errorf("expected at least 20 rules, got %d", len(rules.Rules)) + } +} + +func TestEvalCompliantConfig(t *testing.T) { + rules := loadTestRules(t) + eval := NewEvaluator(rules) + + config := &DimensionConfig{ + AutomationLevel: AutoAssistive, + DecisionBinding: BindingHumanReview, + DecisionImpact: ImpactLow, + Domain: DomainGeneral, + DataType: DataNonPersonal, + HumanInLoop: HILRequired, + Explainability: ExplainBasic, + RiskClassification: RiskMinimal, + LegalBasis: LegalContract, + TransparencyRequired: false, + LoggingRequired: false, + ModelType: ModelRuleBased, + DeploymentScope: ScopeInternal, + } + + result := eval.Evaluate(config) + if !result.IsCompliant { + t.Errorf("expected compliant, got violations: %+v", result.Violations) + } + // All dimensions should be SAFE + for dim, zi := range result.ZoneMap { + if zi.Zone != ZoneSafe { + t.Errorf("dimension %s: expected SAFE, got %s", dim, zi.Zone) + } + } +} + +func TestEvalHRFullAutomationBlocked(t *testing.T) { + rules := loadTestRules(t) + eval := NewEvaluator(rules) + + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionBinding: BindingFullyBinding, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILNone, + Explainability: ExplainNone, + RiskClassification: RiskMinimal, + LegalBasis: LegalContract, + TransparencyRequired: false, + LoggingRequired: false, + ModelType: ModelBlackboxLLM, + DeploymentScope: ScopeExternal, + } + + result := eval.Evaluate(config) + if result.IsCompliant { + t.Error("expected non-compliant for HR full automation") + } + if len(result.Violations) == 0 { + t.Error("expected at least one violation") + } + + // automation_level should be FORBIDDEN + zi := result.ZoneMap["automation_level"] + if zi.Zone != ZoneForbidden { + t.Errorf("automation_level: expected FORBIDDEN, got %s", zi.Zone) + } +} + +func TestEvalProhibitedClassification(t *testing.T) { + rules := loadTestRules(t) + eval := NewEvaluator(rules) + + config := &DimensionConfig{ + RiskClassification: RiskProhibited, + DeploymentScope: ScopePublic, + } + + result := eval.Evaluate(config) + if result.IsCompliant { + t.Error("expected non-compliant for prohibited classification") + } + + found := false + for _, v := range result.Violations { + if v.RuleID == "MC-AIA-PROHIBITED-001" { + found = true + } + } + if !found { + t.Error("expected MC-AIA-PROHIBITED-001 violation") + } +} + +func TestEvalSensitiveDataRequiresConsent(t *testing.T) { + rules := loadTestRules(t) + eval := NewEvaluator(rules) + + config := &DimensionConfig{ + DataType: DataSensitive, + LegalBasis: LegalLegitimateInterest, // wrong basis for sensitive + } + + result := eval.Evaluate(config) + if result.IsCompliant { + t.Error("expected non-compliant: sensitive data without consent") + } + + // Should require consent + found := false + for _, r := range result.Restrictions { + if val, ok := r.Required["legal_basis"]; ok && val == "consent" { + found = true + } + } + if !found { + t.Error("expected restriction requiring legal_basis=consent") + } +} + +func TestEvalHighRiskRequiresLogging(t *testing.T) { + rules := loadTestRules(t) + eval := NewEvaluator(rules) + + config := &DimensionConfig{ + RiskClassification: RiskHigh, + LoggingRequired: false, + TransparencyRequired: false, + HumanInLoop: HILNone, + Explainability: ExplainNone, + } + + result := eval.Evaluate(config) + if result.IsCompliant { + t.Error("expected non-compliant: high risk without logging/transparency/hil") + } + + // Check logging_required is RESTRICTED + zi := result.ZoneMap["logging_required"] + if zi.Zone != ZoneRestricted { + t.Errorf("logging_required: expected RESTRICTED, got %s", zi.Zone) + } +} + +func TestEvalTriggeredRulesHaveObligationRefs(t *testing.T) { + rules := loadTestRules(t) + eval := NewEvaluator(rules) + + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + } + + result := eval.Evaluate(config) + for _, tr := range result.TriggeredRules { + if tr.RuleID == "" { + t.Error("triggered rule missing RuleID") + } + if tr.ObligationID == "" { + t.Error("triggered rule missing ObligationID") + } + if tr.ArticleRef == "" { + t.Error("triggered rule missing ArticleRef") + } + } +} + +func TestConditionSetMatchesExact(t *testing.T) { + config := &DimensionConfig{ + Domain: DomainHR, + DecisionImpact: ImpactHigh, + } + + tests := []struct { + name string + cond ConditionSet + matches bool + }{ + {"exact match", ConditionSet{"domain": "hr", "decision_impact": "high"}, true}, + {"partial match fails", ConditionSet{"domain": "hr", "decision_impact": "low"}, false}, + {"unknown value", ConditionSet{"domain": "finance"}, false}, + {"empty condition", ConditionSet{}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := tc.cond.Matches(config) + if got != tc.matches { + t.Errorf("expected %v, got %v", tc.matches, got) + } + }) + } +} diff --git a/ai-compliance-sdk/internal/maximizer/intake_mapper.go b/ai-compliance-sdk/internal/maximizer/intake_mapper.go new file mode 100644 index 0000000..c9b1304 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/intake_mapper.go @@ -0,0 +1,189 @@ +package maximizer + +import "github.com/breakpilot/ai-compliance-sdk/internal/ucca" + +// MapIntakeToDimensions converts a UseCaseIntake to a normalized DimensionConfig. +// Highest sensitivity wins for multi-value fields. +func MapIntakeToDimensions(intake *ucca.UseCaseIntake) *DimensionConfig { + config := &DimensionConfig{ + AutomationLevel: mapAutomation(intake.Automation), + DecisionBinding: deriveBinding(intake), + DecisionImpact: deriveImpact(intake), + Domain: mapDomain(intake.Domain), + DataType: deriveDataType(intake.DataTypes), + HumanInLoop: deriveHIL(intake.Automation), + Explainability: ExplainBasic, // default + RiskClassification: RiskMinimal, // will be set by evaluator + LegalBasis: LegalContract, // default + TransparencyRequired: false, + LoggingRequired: false, + ModelType: deriveModelType(intake.ModelUsage), + DeploymentScope: deriveScope(intake), + } + return config +} + +// MapDimensionsToIntake converts a DimensionConfig back to a UseCaseIntake, +// preserving unchanged fields from the original intake. +func MapDimensionsToIntake(config *DimensionConfig, original *ucca.UseCaseIntake) *ucca.UseCaseIntake { + result := *original // shallow copy + + // Map automation level + switch config.AutomationLevel { + case AutoNone: + result.Automation = ucca.AutomationAssistive + case AutoAssistive: + result.Automation = ucca.AutomationAssistive + case AutoPartial: + result.Automation = ucca.AutomationSemiAutomated + case AutoFull: + result.Automation = ucca.AutomationFullyAutomated + } + + // Map data type back + result.DataTypes = mapDataTypeBack(config.DataType, original.DataTypes) + + // Map domain back + result.Domain = mapDomainBack(config.Domain, original.Domain) + + return &result +} + +func mapAutomation(a ucca.AutomationLevel) AutomationLevel { + switch a { + case ucca.AutomationAssistive: + return AutoAssistive + case ucca.AutomationSemiAutomated: + return AutoPartial + case ucca.AutomationFullyAutomated: + return AutoFull + default: + return AutoNone + } +} + +func deriveBinding(intake *ucca.UseCaseIntake) DecisionBinding { + if intake.Outputs.LegalEffects || intake.Outputs.AccessDecisions { + if intake.Automation == ucca.AutomationFullyAutomated { + return BindingFullyBinding + } + return BindingHumanReview + } + return BindingNonBinding +} + +func deriveImpact(intake *ucca.UseCaseIntake) DecisionImpact { + if intake.Outputs.LegalEffects || intake.Outputs.AccessDecisions { + return ImpactHigh + } + if intake.Outputs.RankingsOrScores || intake.Purpose.EvaluationScoring || intake.Purpose.DecisionMaking { + return ImpactMedium + } + return ImpactLow +} + +func mapDomain(d ucca.Domain) DomainCategory { + switch d { + case "hr", "human_resources": + return DomainHR + case "finance", "banking", "insurance", "investment": + return DomainFinance + case "education", "school", "university": + return DomainEducation + case "health", "healthcare", "medical": + return DomainHealth + case "marketing", "advertising": + return DomainMarketing + default: + return DomainGeneral + } +} + +func deriveDataType(dt ucca.DataTypes) DataTypeSensitivity { + // Highest sensitivity wins + if dt.BiometricData { + return DataBiometric + } + if dt.Article9Data { + return DataSensitive + } + if dt.PersonalData || dt.EmployeeData || dt.CustomerData || + dt.FinancialData || dt.MinorData || dt.LocationData || + dt.Images || dt.Audio { + return DataPersonal + } + return DataNonPersonal +} + +func deriveHIL(a ucca.AutomationLevel) HumanInLoopLevel { + switch a { + case ucca.AutomationAssistive: + return HILRequired + case ucca.AutomationSemiAutomated: + return HILOptional + case ucca.AutomationFullyAutomated: + return HILNone + default: + return HILRequired + } +} + +func deriveModelType(mu ucca.ModelUsage) ModelType { + if mu.RAG && !mu.Training && !mu.Finetune { + return ModelRuleBased + } + if mu.Training || mu.Finetune { + return ModelBlackboxLLM + } + return ModelStatistical +} + +func deriveScope(intake *ucca.UseCaseIntake) DeploymentScope { + if intake.Purpose.PublicService || intake.Outputs.DataExport { + return ScopePublic + } + if intake.Purpose.CustomerSupport || intake.Purpose.Marketing { + return ScopeExternal + } + return ScopeInternal +} + +func mapDataTypeBack(dt DataTypeSensitivity, original ucca.DataTypes) ucca.DataTypes { + result := original + switch dt { + case DataNonPersonal: + result.PersonalData = false + result.Article9Data = false + result.BiometricData = false + case DataPersonal: + result.PersonalData = true + result.Article9Data = false + result.BiometricData = false + case DataSensitive: + result.PersonalData = true + result.Article9Data = true + result.BiometricData = false + case DataBiometric: + result.PersonalData = true + result.Article9Data = true + result.BiometricData = true + } + return result +} + +func mapDomainBack(dc DomainCategory, original ucca.Domain) ucca.Domain { + switch dc { + case DomainHR: + return "hr" + case DomainFinance: + return "finance" + case DomainEducation: + return "education" + case DomainHealth: + return "health" + case DomainMarketing: + return "marketing" + default: + return original + } +} diff --git a/ai-compliance-sdk/internal/maximizer/optimizer.go b/ai-compliance-sdk/internal/maximizer/optimizer.go new file mode 100644 index 0000000..015b05d --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/optimizer.go @@ -0,0 +1,291 @@ +package maximizer + +import "sort" + +const maxVariants = 5 + +// OptimizedVariant is a single compliant configuration with scoring. +type OptimizedVariant struct { + Config DimensionConfig `json:"config"` + Evaluation *EvaluationResult `json:"evaluation"` + Deltas []DimensionDelta `json:"deltas"` + DeltaCount int `json:"delta_count"` + SafetyScore int `json:"safety_score"` + UtilityScore int `json:"utility_score"` + CompositeScore float64 `json:"composite_score"` + Rationale string `json:"rationale"` +} + +// OptimizationResult contains the original evaluation and ranked compliant variants. +type OptimizationResult struct { + OriginalConfig DimensionConfig `json:"original_config"` + OriginalCompliant bool `json:"original_compliant"` + OriginalEval *EvaluationResult `json:"original_evaluation"` + Variants []OptimizedVariant `json:"variants"` + MaxSafeConfig *OptimizedVariant `json:"max_safe_config"` +} + +// Optimizer finds the maximum compliant configuration variant. +type Optimizer struct { + evaluator *Evaluator + weights ScoreWeights +} + +// NewOptimizer creates an optimizer backed by the given evaluator. +func NewOptimizer(evaluator *Evaluator) *Optimizer { + return &Optimizer{evaluator: evaluator, weights: DefaultWeights} +} + +// Optimize takes a desired (possibly non-compliant) config and returns +// ranked compliant alternatives. +func (o *Optimizer) Optimize(desired *DimensionConfig) *OptimizationResult { + eval := o.evaluator.Evaluate(desired) + result := &OptimizationResult{ + OriginalConfig: *desired, + OriginalCompliant: eval.IsCompliant, + OriginalEval: eval, + } + + if eval.IsCompliant { + variant := o.scoreVariant(desired, desired, eval) + variant.Rationale = "Konfiguration ist bereits konform" + result.Variants = []OptimizedVariant{variant} + result.MaxSafeConfig = &result.Variants[0] + return result + } + + // Check for hard prohibitions that cannot be optimized + if o.hasProhibitedClassification(desired) { + result.Variants = []OptimizedVariant{} + return result + } + + candidates := o.generateCandidates(desired, eval) + result.Variants = candidates + if len(candidates) > 0 { + result.MaxSafeConfig = &result.Variants[0] + } + return result +} + +func (o *Optimizer) hasProhibitedClassification(config *DimensionConfig) bool { + return config.RiskClassification == RiskProhibited +} + +// generateCandidates builds compliant variants by fixing violations. +func (o *Optimizer) generateCandidates(desired *DimensionConfig, eval *EvaluationResult) []OptimizedVariant { + // Strategy 1: Fix all violations in one pass (greedy nearest fix) + greedy := o.greedyFix(desired, eval) + var candidates []OptimizedVariant + + if greedy != nil { + greedyEval := o.evaluator.Evaluate(&greedy.Config) + if greedyEval.IsCompliant { + v := o.scoreVariant(desired, &greedy.Config, greedyEval) + v.Rationale = "Minimale Anpassung — naechster konformer Zustand" + candidates = append(candidates, v) + } + } + + // Strategy 2: Conservative variant (maximum safety) + conservative := o.conservativeFix(desired, eval) + if conservative != nil { + consEval := o.evaluator.Evaluate(&conservative.Config) + if consEval.IsCompliant { + v := o.scoreVariant(desired, &conservative.Config, consEval) + v.Rationale = "Konservative Variante — maximale regulatorische Sicherheit" + candidates = append(candidates, v) + } + } + + // Strategy 3: Fix restricted dimensions too (belt-and-suspenders) + enhanced := o.enhancedFix(desired, eval) + if enhanced != nil { + enhEval := o.evaluator.Evaluate(&enhanced.Config) + if enhEval.IsCompliant { + v := o.scoreVariant(desired, &enhanced.Config, enhEval) + v.Rationale = "Erweiterte Variante — alle Einschraenkungen vorab behoben" + candidates = append(candidates, v) + } + } + + // Deduplicate and sort by composite score + candidates = deduplicateVariants(candidates) + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].CompositeScore > candidates[j].CompositeScore + }) + if len(candidates) > maxVariants { + candidates = candidates[:maxVariants] + } + return candidates +} + +// greedyFix applies the minimum change per violated dimension. +func (o *Optimizer) greedyFix(desired *DimensionConfig, eval *EvaluationResult) *OptimizedVariant { + fixed := desired.Clone() + + // Fix FORBIDDEN zones + for dim, zi := range eval.ZoneMap { + if zi.Zone != ZoneForbidden { + continue + } + o.fixDimension(&fixed, dim, eval) + } + + // Fix RESTRICTED zones (required values not met) + for _, restriction := range eval.Restrictions { + for dim, requiredVal := range restriction.Required { + fixed.SetValue(dim, requiredVal) + } + } + + // Re-evaluate and iterate (max 3 passes to converge) + for i := 0; i < 3; i++ { + reEval := o.evaluator.Evaluate(&fixed) + if reEval.IsCompliant { + break + } + for dim, zi := range reEval.ZoneMap { + if zi.Zone == ZoneForbidden { + o.fixDimension(&fixed, dim, reEval) + } + } + for _, restriction := range reEval.Restrictions { + for dim, requiredVal := range restriction.Required { + fixed.SetValue(dim, requiredVal) + } + } + } + + return &OptimizedVariant{Config: fixed} +} + +// conservativeFix chooses the safest allowed value for each violated dimension. +func (o *Optimizer) conservativeFix(desired *DimensionConfig, eval *EvaluationResult) *OptimizedVariant { + fixed := desired.Clone() + + for dim, zi := range eval.ZoneMap { + if zi.Zone == ZoneSafe { + continue + } + // Use the safest (lowest ordinal risk) value + vals := AllValues[dim] + if len(vals) > 0 { + fixed.SetValue(dim, vals[0]) // index 0 = safest + } + } + + // Apply all required values + for _, restriction := range eval.Restrictions { + for dim, val := range restriction.Required { + fixed.SetValue(dim, val) + } + } + + return &OptimizedVariant{Config: fixed} +} + +// enhancedFix fixes violations AND proactively resolves restrictions. +func (o *Optimizer) enhancedFix(desired *DimensionConfig, eval *EvaluationResult) *OptimizedVariant { + fixed := desired.Clone() + + // Fix all non-SAFE dimensions + for dim, zi := range eval.ZoneMap { + if zi.Zone == ZoneSafe { + continue + } + if len(zi.AllowedValues) > 0 { + fixed.SetValue(dim, zi.AllowedValues[0]) + } else { + o.fixDimension(&fixed, dim, eval) + } + } + + // Apply required values + for _, restriction := range eval.Restrictions { + for dim, val := range restriction.Required { + fixed.SetValue(dim, val) + } + } + + // Re-evaluate to converge + for i := 0; i < 3; i++ { + reEval := o.evaluator.Evaluate(&fixed) + if reEval.IsCompliant { + break + } + for _, restriction := range reEval.Restrictions { + for dim, val := range restriction.Required { + fixed.SetValue(dim, val) + } + } + } + + return &OptimizedVariant{Config: fixed} +} + +// fixDimension steps the dimension to the nearest safer value. +func (o *Optimizer) fixDimension(config *DimensionConfig, dim string, eval *EvaluationResult) { + vals := AllValues[dim] + if len(vals) == 0 { + return + } + current := config.GetValue(dim) + currentIdx := indexOf(vals, current) + if currentIdx < 0 { + config.SetValue(dim, vals[0]) + return + } + + // For risk-ordered dimensions, step toward the safer end (lower index). + // For inverse dimensions (human_in_loop, explainability), lower index = more safe. + if currentIdx > 0 { + config.SetValue(dim, vals[currentIdx-1]) + } +} + +func (o *Optimizer) scoreVariant(original, variant *DimensionConfig, eval *EvaluationResult) OptimizedVariant { + deltas := original.Diff(variant) + safety := ComputeSafetyScore(eval) + utility := ComputeUtilityScore(original, variant) + composite := ComputeCompositeScore(safety, utility, o.weights) + return OptimizedVariant{ + Config: *variant, + Evaluation: eval, + Deltas: deltas, + DeltaCount: len(deltas), + SafetyScore: safety, + UtilityScore: utility, + CompositeScore: composite, + } +} + +func indexOf(slice []string, val string) int { + for i, v := range slice { + if v == val { + return i + } + } + return -1 +} + +func deduplicateVariants(variants []OptimizedVariant) []OptimizedVariant { + seen := make(map[string]bool) + var unique []OptimizedVariant + for _, v := range variants { + key := configKey(&v.Config) + if !seen[key] { + seen[key] = true + unique = append(unique, v) + } + } + return unique +} + +func configKey(c *DimensionConfig) string { + var key string + for _, dim := range allDimensions { + key += dim + "=" + c.GetValue(dim) + ";" + } + return key +} diff --git a/ai-compliance-sdk/internal/maximizer/optimizer_test.go b/ai-compliance-sdk/internal/maximizer/optimizer_test.go new file mode 100644 index 0000000..50b0954 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/optimizer_test.go @@ -0,0 +1,300 @@ +package maximizer + +import "testing" + +func newTestOptimizer(t *testing.T) *Optimizer { + t.Helper() + rules := loadTestRules(t) + eval := NewEvaluator(rules) + return NewOptimizer(eval) +} + +// --- Golden Test Cases --- + +func TestGC01_HRFullAutomationBlocked(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionBinding: BindingFullyBinding, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILNone, + Explainability: ExplainNone, + RiskClassification: RiskMinimal, + LegalBasis: LegalContract, + ModelType: ModelBlackboxLLM, + DeploymentScope: ScopeExternal, + } + + result := opt.Optimize(config) + if result.OriginalCompliant { + t.Fatal("expected original to be non-compliant") + } + if result.MaxSafeConfig == nil { + t.Fatal("expected an optimized variant") + } + + max := result.MaxSafeConfig + if max.Config.AutomationLevel == AutoFull { + t.Error("optimizer must change automation_level from full") + } + if max.Config.HumanInLoop != HILRequired { + t.Errorf("expected human_in_loop=required, got %s", max.Config.HumanInLoop) + } + if max.Config.DecisionBinding == BindingFullyBinding { + t.Error("expected decision_binding to change from fully_binding") + } + // Verify the optimized config is actually compliant + if !max.Evaluation.IsCompliant { + t.Errorf("MaxSafeConfig is not compliant: violations=%+v", max.Evaluation.Violations) + } +} + +func TestGC02_HRRankingWithHumanReviewAllowed(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoAssistive, + DecisionBinding: BindingHumanReview, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILRequired, + Explainability: ExplainBasic, + RiskClassification: RiskMinimal, + LegalBasis: LegalContract, + TransparencyRequired: true, + LoggingRequired: true, + ModelType: ModelBlackboxLLM, + DeploymentScope: ScopeExternal, + } + + result := opt.Optimize(config) + // Should be allowed with conditions (requirements from high-risk classification) + if result.MaxSafeConfig == nil { + t.Fatal("expected a variant") + } +} + +func TestGC05_SensitiveDataWithoutLegalBasis(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + DataType: DataSensitive, + LegalBasis: LegalLegitimateInterest, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + AutomationLevel: AutoAssistive, + HumanInLoop: HILRequired, + DecisionBinding: BindingHumanReview, + } + + result := opt.Optimize(config) + if result.OriginalCompliant { + t.Error("expected non-compliant: sensitive data with legitimate_interest") + } + if result.MaxSafeConfig == nil { + t.Fatal("expected optimized variant") + } + if result.MaxSafeConfig.Config.LegalBasis != LegalConsent { + t.Errorf("expected legal_basis=consent, got %s", result.MaxSafeConfig.Config.LegalBasis) + } +} + +func TestGC16_ProhibitedPracticeBlocked(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + RiskClassification: RiskProhibited, + DeploymentScope: ScopePublic, + } + + result := opt.Optimize(config) + if result.OriginalCompliant { + t.Error("expected non-compliant for prohibited") + } + // Prohibited = no optimization possible + if len(result.Variants) > 0 { + t.Error("expected no variants for prohibited classification") + } +} + +func TestGC18_OptimizerMinimalChange(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionBinding: BindingFullyBinding, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILNone, + Explainability: ExplainBasic, + RiskClassification: RiskMinimal, + LegalBasis: LegalContract, + ModelType: ModelStatistical, + DeploymentScope: ScopeInternal, + } + + result := opt.Optimize(config) + if result.MaxSafeConfig == nil { + t.Fatal("expected optimized variant") + } + + max := result.MaxSafeConfig + // Domain must NOT change + if max.Config.Domain != DomainHR { + t.Errorf("optimizer must not change domain: got %s", max.Config.Domain) + } + // Explainability was already basic, should stay + if max.Config.Explainability != ExplainBasic { + t.Errorf("optimizer should keep explainability=basic, got %s", max.Config.Explainability) + } + // Model type should not change unnecessarily + if max.Config.ModelType != ModelStatistical { + t.Errorf("optimizer should not change model_type unnecessarily, got %s", max.Config.ModelType) + } +} + +func TestGC20_AlreadyCompliantNoChanges(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoAssistive, + DecisionBinding: BindingNonBinding, + DecisionImpact: ImpactLow, + Domain: DomainGeneral, + DataType: DataNonPersonal, + HumanInLoop: HILRequired, + Explainability: ExplainBasic, + RiskClassification: RiskMinimal, + LegalBasis: LegalContract, + TransparencyRequired: false, + LoggingRequired: false, + ModelType: ModelRuleBased, + DeploymentScope: ScopeInternal, + } + + result := opt.Optimize(config) + if !result.OriginalCompliant { + t.Error("expected compliant") + } + if result.MaxSafeConfig == nil { + t.Fatal("expected variant") + } + if result.MaxSafeConfig.DeltaCount != 0 { + t.Errorf("expected 0 deltas for compliant config, got %d", result.MaxSafeConfig.DeltaCount) + } + if result.MaxSafeConfig.UtilityScore != 100 { + t.Errorf("expected utility 100, got %d", result.MaxSafeConfig.UtilityScore) + } +} + +// --- Meta Tests --- + +func TestMT01_Determinism(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILNone, + } + + r1 := opt.Optimize(config) + r2 := opt.Optimize(config) + + if r1.OriginalCompliant != r2.OriginalCompliant { + t.Error("determinism failed: different compliance result") + } + if len(r1.Variants) != len(r2.Variants) { + t.Errorf("determinism failed: %d vs %d variants", len(r1.Variants), len(r2.Variants)) + } + if r1.MaxSafeConfig != nil && r2.MaxSafeConfig != nil { + if r1.MaxSafeConfig.CompositeScore != r2.MaxSafeConfig.CompositeScore { + t.Error("determinism failed: different composite scores") + } + } +} + +func TestMT03_ViolationsReferenceObligations(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionImpact: ImpactHigh, + DataType: DataSensitive, + } + + result := opt.Optimize(config) + for _, v := range result.OriginalEval.Violations { + if v.ObligationID == "" { + t.Errorf("violation %s missing obligation reference", v.RuleID) + } + } + for _, tr := range result.OriginalEval.TriggeredRules { + if tr.ObligationID == "" { + t.Errorf("triggered rule %s missing obligation reference", tr.RuleID) + } + } +} + +func TestMT05_OptimizerMinimality(t *testing.T) { + opt := newTestOptimizer(t) + // Config that only violates one dimension + config := &DimensionConfig{ + AutomationLevel: AutoAssistive, + DecisionBinding: BindingHumanReview, + DecisionImpact: ImpactLow, + Domain: DomainGeneral, + DataType: DataSensitive, // only violation: needs consent + HumanInLoop: HILRequired, + Explainability: ExplainBasic, + RiskClassification: RiskMinimal, + LegalBasis: LegalLegitimateInterest, // must change to consent + TransparencyRequired: false, + LoggingRequired: false, + ModelType: ModelRuleBased, + DeploymentScope: ScopeInternal, + } + + result := opt.Optimize(config) + if result.MaxSafeConfig == nil { + t.Fatal("expected optimized variant") + } + + // Check that only compliance-related dimensions changed + for _, d := range result.MaxSafeConfig.Deltas { + switch d.Dimension { + case "legal_basis", "transparency_required", "logging_required", "data_type": + // Expected: legal_basis→consent, transparency, logging for sensitive data + // data_type→personal is from optimizer meta-rule (reduce unnecessary sensitivity) + default: + t.Errorf("unexpected dimension change: %s (%s → %s)", d.Dimension, d.From, d.To) + } + } +} + +func TestOptimizeProducesRankedVariants(t *testing.T) { + opt := newTestOptimizer(t) + config := &DimensionConfig{ + AutomationLevel: AutoFull, + DecisionImpact: ImpactHigh, + Domain: DomainHR, + DataType: DataPersonal, + HumanInLoop: HILNone, + Explainability: ExplainNone, + ModelType: ModelBlackboxLLM, + DeploymentScope: ScopeExternal, + } + + result := opt.Optimize(config) + if len(result.Variants) < 2 { + t.Skipf("only %d variants generated", len(result.Variants)) + } + + // Verify descending composite score order + for i := 1; i < len(result.Variants); i++ { + if result.Variants[i].CompositeScore > result.Variants[i-1].CompositeScore { + t.Errorf("variants not sorted: [%d]=%.1f > [%d]=%.1f", + i, result.Variants[i].CompositeScore, + i-1, result.Variants[i-1].CompositeScore) + } + } +} diff --git a/ai-compliance-sdk/internal/maximizer/scoring.go b/ai-compliance-sdk/internal/maximizer/scoring.go new file mode 100644 index 0000000..b487264 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/scoring.go @@ -0,0 +1,84 @@ +package maximizer + +// ScoreWeights controls the balance between safety and business utility. +type ScoreWeights struct { + Safety float64 `json:"safety"` + Utility float64 `json:"utility"` +} + +// DefaultWeights prioritizes business utility slightly over safety margin +// since the optimizer already ensures compliance. +var DefaultWeights = ScoreWeights{Safety: 0.4, Utility: 0.6} + +// dimensionBusinessWeight indicates how much business value each dimension +// contributes. Higher = more costly to change for the business. +var dimensionBusinessWeight = map[string]int{ + "automation_level": 15, + "decision_binding": 12, + "deployment_scope": 10, + "model_type": 8, + "decision_impact": 7, + "explainability": 5, + "data_type": 5, + "human_in_loop": 5, + "legal_basis": 4, + "domain": 3, + "risk_classification": 3, + "transparency_required": 2, + "logging_required": 2, +} + +// ComputeSafetyScore returns 0-100 where 100 = completely safe (no restrictions). +// Decreases with each RESTRICTED or FORBIDDEN zone. +func ComputeSafetyScore(eval *EvaluationResult) int { + if eval == nil { + return 0 + } + total := len(allDimensions) + safe := 0 + for _, zi := range eval.ZoneMap { + if zi.Zone == ZoneSafe { + safe++ + } + } + if total == 0 { + return 100 + } + return (safe * 100) / total +} + +// ComputeUtilityScore returns 0-100 where 100 = no changes from original. +// Decreases based on the business weight of each changed dimension. +func ComputeUtilityScore(original, variant *DimensionConfig) int { + if original == nil || variant == nil { + return 0 + } + deltas := original.Diff(variant) + if len(deltas) == 0 { + return 100 + } + + maxCost := 0 + for _, w := range dimensionBusinessWeight { + maxCost += w + } + + cost := 0 + for _, d := range deltas { + w := dimensionBusinessWeight[d.Dimension] + if w == 0 { + w = 3 // default + } + cost += w + } + + if cost >= maxCost { + return 0 + } + return 100 - (cost*100)/maxCost +} + +// ComputeCompositeScore combines safety and utility into a single ranking score. +func ComputeCompositeScore(safety, utility int, weights ScoreWeights) float64 { + return weights.Safety*float64(safety) + weights.Utility*float64(utility) +} diff --git a/ai-compliance-sdk/internal/maximizer/scoring_test.go b/ai-compliance-sdk/internal/maximizer/scoring_test.go new file mode 100644 index 0000000..431110a --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/scoring_test.go @@ -0,0 +1,88 @@ +package maximizer + +import "testing" + +func TestSafetyScoreAllSafe(t *testing.T) { + zm := make(map[string]ZoneInfo) + for _, dim := range allDimensions { + zm[dim] = ZoneInfo{Zone: ZoneSafe} + } + eval := &EvaluationResult{ZoneMap: zm} + score := ComputeSafetyScore(eval) + if score != 100 { + t.Errorf("expected 100, got %d", score) + } +} + +func TestSafetyScoreWithRestrictions(t *testing.T) { + zm := make(map[string]ZoneInfo) + for _, dim := range allDimensions { + zm[dim] = ZoneInfo{Zone: ZoneSafe} + } + // Mark 3 as restricted + zm["automation_level"] = ZoneInfo{Zone: ZoneRestricted} + zm["human_in_loop"] = ZoneInfo{Zone: ZoneRestricted} + zm["logging_required"] = ZoneInfo{Zone: ZoneForbidden} + + eval := &EvaluationResult{ZoneMap: zm} + score := ComputeSafetyScore(eval) + + safe := len(allDimensions) - 3 + expected := (safe * 100) / len(allDimensions) + if score != expected { + t.Errorf("expected %d, got %d", expected, score) + } +} + +func TestSafetyScoreNil(t *testing.T) { + if s := ComputeSafetyScore(nil); s != 0 { + t.Errorf("expected 0 for nil, got %d", s) + } +} + +func TestUtilityScoreNoChanges(t *testing.T) { + config := &DimensionConfig{AutomationLevel: AutoFull} + score := ComputeUtilityScore(config, config) + if score != 100 { + t.Errorf("expected 100 for identical configs, got %d", score) + } +} + +func TestUtilityScoreWithChanges(t *testing.T) { + original := &DimensionConfig{ + AutomationLevel: AutoFull, + HumanInLoop: HILNone, + } + variant := &DimensionConfig{ + AutomationLevel: AutoAssistive, + HumanInLoop: HILRequired, + } + score := ComputeUtilityScore(original, variant) + if score >= 100 { + t.Errorf("expected < 100 with changes, got %d", score) + } + if score <= 0 { + t.Errorf("expected > 0 for moderate changes, got %d", score) + } +} + +func TestUtilityScoreNil(t *testing.T) { + if s := ComputeUtilityScore(nil, nil); s != 0 { + t.Errorf("expected 0 for nil, got %d", s) + } +} + +func TestCompositeScore(t *testing.T) { + score := ComputeCompositeScore(80, 60, DefaultWeights) + expected := 0.4*80.0 + 0.6*60.0 // 32 + 36 = 68 + if score != expected { + t.Errorf("expected %.1f, got %.1f", expected, score) + } +} + +func TestCompositeScoreCustomWeights(t *testing.T) { + score := ComputeCompositeScore(100, 0, ScoreWeights{Safety: 1.0, Utility: 0.0}) + if score != 100.0 { + t.Errorf("expected 100, got %.1f", score) + } +} diff --git a/ai-compliance-sdk/internal/maximizer/service.go b/ai-compliance-sdk/internal/maximizer/service.go new file mode 100644 index 0000000..8eeaa9e --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/service.go @@ -0,0 +1,144 @@ +package maximizer + +import ( + "context" + "fmt" + + "github.com/breakpilot/ai-compliance-sdk/internal/ucca" + "github.com/google/uuid" +) + +// Service contains the business logic for the Compliance Maximizer. +type Service struct { + store *Store + evaluator *Evaluator + optimizer *Optimizer + uccaStore *ucca.Store + rules *ConstraintRuleSet +} + +// NewService creates a maximizer service. +func NewService(store *Store, uccaStore *ucca.Store, rules *ConstraintRuleSet) *Service { + eval := NewEvaluator(rules) + opt := NewOptimizer(eval) + return &Service{ + store: store, + evaluator: eval, + optimizer: opt, + uccaStore: uccaStore, + rules: rules, + } +} + +// OptimizeInput is the request to optimize a dimension config. +type OptimizeInput struct { + Config DimensionConfig `json:"config"` + Title string `json:"title"` + TenantID uuid.UUID `json:"-"` + UserID uuid.UUID `json:"-"` +} + +// OptimizeFromIntakeInput wraps a UCCA intake for optimization. +type OptimizeFromIntakeInput struct { + Intake ucca.UseCaseIntake `json:"intake"` + Title string `json:"title"` + TenantID uuid.UUID `json:"-"` + UserID uuid.UUID `json:"-"` +} + +// Optimize evaluates and optimizes a dimension config. +func (s *Service) Optimize(ctx context.Context, in *OptimizeInput) (*Optimization, error) { + result := s.optimizer.Optimize(&in.Config) + + o := &Optimization{ + TenantID: in.TenantID, + Title: in.Title, + InputConfig: in.Config, + IsCompliant: result.OriginalCompliant, + OriginalEvaluation: *result.OriginalEval, + Variants: result.Variants, + ZoneMap: result.OriginalEval.ZoneMap, + ConstraintVersion: s.rules.Version, + CreatedBy: in.UserID, + } + if result.MaxSafeConfig != nil { + o.MaxSafeConfig = result.MaxSafeConfig + } + + if err := s.store.CreateOptimization(ctx, o); err != nil { + return nil, fmt.Errorf("optimize: %w", err) + } + return o, nil +} + +// OptimizeFromIntake maps a UCCA intake to dimensions and optimizes. +func (s *Service) OptimizeFromIntake(ctx context.Context, in *OptimizeFromIntakeInput) (*Optimization, error) { + config := MapIntakeToDimensions(&in.Intake) + return s.Optimize(ctx, &OptimizeInput{ + Config: *config, + Title: in.Title, + TenantID: in.TenantID, + UserID: in.UserID, + }) +} + +// OptimizeFromAssessment loads an existing UCCA assessment and optimizes it. +func (s *Service) OptimizeFromAssessment(ctx context.Context, assessmentID, tenantID, userID uuid.UUID) (*Optimization, error) { + assessment, err := s.uccaStore.GetAssessment(ctx, assessmentID) + if err != nil { + return nil, fmt.Errorf("load assessment %s: %w", assessmentID, err) + } + config := MapIntakeToDimensions(&assessment.Intake) + result := s.optimizer.Optimize(config) + + o := &Optimization{ + TenantID: tenantID, + AssessmentID: &assessmentID, + Title: assessment.Title, + InputConfig: *config, + IsCompliant: result.OriginalCompliant, + OriginalEvaluation: *result.OriginalEval, + Variants: result.Variants, + ZoneMap: result.OriginalEval.ZoneMap, + ConstraintVersion: s.rules.Version, + CreatedBy: userID, + } + if result.MaxSafeConfig != nil { + o.MaxSafeConfig = result.MaxSafeConfig + } + + if err := s.store.CreateOptimization(ctx, o); err != nil { + return nil, fmt.Errorf("optimize from assessment: %w", err) + } + return o, nil +} + +// Evaluate only evaluates without persisting (3-zone analysis). +func (s *Service) Evaluate(config *DimensionConfig) *EvaluationResult { + return s.evaluator.Evaluate(config) +} + +// GetOptimization retrieves a stored optimization. +func (s *Service) GetOptimization(ctx context.Context, id uuid.UUID) (*Optimization, error) { + return s.store.GetOptimization(ctx, id) +} + +// ListOptimizations returns optimizations for a tenant. +func (s *Service) ListOptimizations(ctx context.Context, tenantID uuid.UUID, f *OptimizationFilters) ([]Optimization, int, error) { + return s.store.ListOptimizations(ctx, tenantID, f) +} + +// DeleteOptimization removes an optimization. +func (s *Service) DeleteOptimization(ctx context.Context, id uuid.UUID) error { + return s.store.DeleteOptimization(ctx, id) +} + +// GetDimensionSchema returns the dimension schema for the frontend. +func (s *Service) GetDimensionSchema() map[string][]string { + return AllValues +} + +// GetConstraintRules returns the loaded rules for transparency. +func (s *Service) GetConstraintRules() *ConstraintRuleSet { + return s.rules +} diff --git a/ai-compliance-sdk/internal/maximizer/store.go b/ai-compliance-sdk/internal/maximizer/store.go new file mode 100644 index 0000000..469cf00 --- /dev/null +++ b/ai-compliance-sdk/internal/maximizer/store.go @@ -0,0 +1,209 @@ +package maximizer + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +// Optimization is the DB entity for a maximizer optimization result. +type Optimization struct { + ID uuid.UUID `json:"id"` + TenantID uuid.UUID `json:"tenant_id"` + AssessmentID *uuid.UUID `json:"assessment_id,omitempty"` + Title string `json:"title"` + Status string `json:"status"` + InputConfig DimensionConfig `json:"input_config"` + IsCompliant bool `json:"is_compliant"` + OriginalEvaluation EvaluationResult `json:"original_evaluation"` + MaxSafeConfig *OptimizedVariant `json:"max_safe_config,omitempty"` + Variants []OptimizedVariant `json:"variants"` + ZoneMap map[string]ZoneInfo `json:"zone_map"` + ConstraintVersion string `json:"constraint_version"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + CreatedBy uuid.UUID `json:"created_by"` +} + +// Store handles maximizer data persistence. +type Store struct { + pool *pgxpool.Pool +} + +// NewStore creates a new maximizer store. +func NewStore(pool *pgxpool.Pool) *Store { + return &Store{pool: pool} +} + +// CreateOptimization persists a new optimization result. +func (s *Store) CreateOptimization(ctx context.Context, o *Optimization) error { + o.ID = uuid.New() + o.CreatedAt = time.Now().UTC() + o.UpdatedAt = o.CreatedAt + if o.Status == "" { + o.Status = "completed" + } + if o.ConstraintVersion == "" { + o.ConstraintVersion = "1.0.0" + } + + inputConfig, _ := json.Marshal(o.InputConfig) + originalEval, _ := json.Marshal(o.OriginalEvaluation) + maxSafe, _ := json.Marshal(o.MaxSafeConfig) + variants, _ := json.Marshal(o.Variants) + zoneMap, _ := json.Marshal(o.ZoneMap) + + _, err := s.pool.Exec(ctx, ` + INSERT INTO maximizer_optimizations ( + id, tenant_id, assessment_id, title, status, + input_config, is_compliant, original_evaluation, + max_safe_config, variants, zone_map, + constraint_version, created_at, updated_at, created_by + ) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, + $9, $10, $11, + $12, $13, $14, $15 + )`, + o.ID, o.TenantID, o.AssessmentID, o.Title, o.Status, + inputConfig, o.IsCompliant, originalEval, + maxSafe, variants, zoneMap, + o.ConstraintVersion, o.CreatedAt, o.UpdatedAt, o.CreatedBy, + ) + if err != nil { + return fmt.Errorf("create optimization: %w", err) + } + return nil +} + +// GetOptimization retrieves a single optimization by ID. +func (s *Store) GetOptimization(ctx context.Context, id uuid.UUID) (*Optimization, error) { + row := s.pool.QueryRow(ctx, ` + SELECT id, tenant_id, assessment_id, title, status, + input_config, is_compliant, original_evaluation, + max_safe_config, variants, zone_map, + constraint_version, created_at, updated_at, created_by + FROM maximizer_optimizations WHERE id = $1`, id) + return s.scanOptimization(row) +} + +// OptimizationFilters for list queries. +type OptimizationFilters struct { + IsCompliant *bool + Search string + Limit int + Offset int +} + +// ListOptimizations returns optimizations for a tenant. +func (s *Store) ListOptimizations(ctx context.Context, tenantID uuid.UUID, f *OptimizationFilters) ([]Optimization, int, error) { + if f == nil { + f = &OptimizationFilters{} + } + if f.Limit <= 0 { + f.Limit = 20 + } + + where := "WHERE tenant_id = $1" + args := []interface{}{tenantID} + idx := 2 + + if f.IsCompliant != nil { + where += fmt.Sprintf(" AND is_compliant = $%d", idx) + args = append(args, *f.IsCompliant) + idx++ + } + if f.Search != "" { + where += fmt.Sprintf(" AND title ILIKE $%d", idx) + args = append(args, "%"+f.Search+"%") + idx++ + } + + // Count + var total int + countQuery := "SELECT COUNT(*) FROM maximizer_optimizations " + where + if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("count optimizations: %w", err) + } + + // Fetch + query := fmt.Sprintf(` + SELECT id, tenant_id, assessment_id, title, status, + input_config, is_compliant, original_evaluation, + max_safe_config, variants, zone_map, + constraint_version, created_at, updated_at, created_by + FROM maximizer_optimizations %s + ORDER BY created_at DESC + LIMIT $%d OFFSET $%d`, where, idx, idx+1) + args = append(args, f.Limit, f.Offset) + + rows, err := s.pool.Query(ctx, query, args...) + if err != nil { + return nil, 0, fmt.Errorf("list optimizations: %w", err) + } + defer rows.Close() + + var results []Optimization + for rows.Next() { + o, err := s.scanOptimizationRows(rows) + if err != nil { + return nil, 0, err + } + results = append(results, *o) + } + return results, total, nil +} + +// DeleteOptimization removes an optimization. +func (s *Store) DeleteOptimization(ctx context.Context, id uuid.UUID) error { + _, err := s.pool.Exec(ctx, `DELETE FROM maximizer_optimizations WHERE id = $1`, id) + if err != nil { + return fmt.Errorf("delete optimization: %w", err) + } + return nil +} + +func (s *Store) scanOptimization(row pgx.Row) (*Optimization, error) { + var o Optimization + var inputConfig, originalEval, maxSafe, variants, zoneMap []byte + err := row.Scan( + &o.ID, &o.TenantID, &o.AssessmentID, &o.Title, &o.Status, + &inputConfig, &o.IsCompliant, &originalEval, + &maxSafe, &variants, &zoneMap, + &o.ConstraintVersion, &o.CreatedAt, &o.UpdatedAt, &o.CreatedBy, + ) + if err != nil { + return nil, fmt.Errorf("scan optimization: %w", err) + } + json.Unmarshal(inputConfig, &o.InputConfig) + json.Unmarshal(originalEval, &o.OriginalEvaluation) + json.Unmarshal(maxSafe, &o.MaxSafeConfig) + json.Unmarshal(variants, &o.Variants) + json.Unmarshal(zoneMap, &o.ZoneMap) + return &o, nil +} + +func (s *Store) scanOptimizationRows(rows pgx.Rows) (*Optimization, error) { + var o Optimization + var inputConfig, originalEval, maxSafe, variants, zoneMap []byte + err := rows.Scan( + &o.ID, &o.TenantID, &o.AssessmentID, &o.Title, &o.Status, + &inputConfig, &o.IsCompliant, &originalEval, + &maxSafe, &variants, &zoneMap, + &o.ConstraintVersion, &o.CreatedAt, &o.UpdatedAt, &o.CreatedBy, + ) + if err != nil { + return nil, fmt.Errorf("scan optimization row: %w", err) + } + json.Unmarshal(inputConfig, &o.InputConfig) + json.Unmarshal(originalEval, &o.OriginalEvaluation) + json.Unmarshal(maxSafe, &o.MaxSafeConfig) + json.Unmarshal(variants, &o.Variants) + json.Unmarshal(zoneMap, &o.ZoneMap) + return &o, nil +} diff --git a/ai-compliance-sdk/internal/ucca/models.go b/ai-compliance-sdk/internal/ucca/models.go index 77ec727..a0540d1 100644 --- a/ai-compliance-sdk/internal/ucca/models.go +++ b/ai-compliance-sdk/internal/ucca/models.go @@ -1,5 +1,17 @@ package ucca +import ( + "time" + + "github.com/google/uuid" +) + +// Keep imports used by DecisionTreeResult. +var ( + _ uuid.UUID + _ time.Time +) + // ============================================================================ // Constants / Enums // ============================================================================ diff --git a/ai-compliance-sdk/internal/ucca/models_assessment.go b/ai-compliance-sdk/internal/ucca/models_assessment.go index 8716f7a..baf54bd 100644 --- a/ai-compliance-sdk/internal/ucca/models_assessment.go +++ b/ai-compliance-sdk/internal/ucca/models_assessment.go @@ -38,6 +38,13 @@ type AssessmentResult struct { Art22Risk bool `json:"art22_risk"` // Art. 22 GDPR automated decision risk TrainingAllowed TrainingAllowed `json:"training_allowed"` + // BetrVG (Works Council) assessment + BetrvgConflictScore int `json:"betrvg_conflict_score,omitempty"` + BetrvgConsultationRequired bool `json:"betrvg_consultation_required,omitempty"` + + // Intake reference for escalation logic + Intake *UseCaseIntake `json:"intake,omitempty"` + // Summary for humans Summary string `json:"summary"` Recommendation string `json:"recommendation"` diff --git a/ai-compliance-sdk/internal/ucca/models_intake.go b/ai-compliance-sdk/internal/ucca/models_intake.go index f36e176..4293a1f 100644 --- a/ai-compliance-sdk/internal/ucca/models_intake.go +++ b/ai-compliance-sdk/internal/ucca/models_intake.go @@ -40,6 +40,9 @@ type UseCaseIntake struct { // Only applicable for financial domains (banking, finance, insurance, investment) FinancialContext *FinancialContext `json:"financial_context,omitempty"` + // BetrVG: Works council consultation status + WorksCouncilConsulted bool `json:"works_council_consulted,omitempty"` + // Opt-in to store raw text (otherwise only hash) StoreRawText bool `json:"store_raw_text,omitempty"` } diff --git a/ai-compliance-sdk/migrations/026_maximizer_schema.sql b/ai-compliance-sdk/migrations/026_maximizer_schema.sql new file mode 100644 index 0000000..de2a797 --- /dev/null +++ b/ai-compliance-sdk/migrations/026_maximizer_schema.sql @@ -0,0 +1,41 @@ +-- Compliance Maximizer: Regulatory Optimization Engine +-- Stores optimization results with 3-zone analysis and compliant variants. + +CREATE TABLE IF NOT EXISTS maximizer_optimizations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + -- Optional link to existing UCCA assessment + assessment_id UUID, + + title VARCHAR(500) DEFAULT '', + status VARCHAR(50) DEFAULT 'completed', + + -- Input + input_config JSONB NOT NULL, + input_intake JSONB, + + -- Results + is_compliant BOOLEAN NOT NULL DEFAULT false, + original_evaluation JSONB NOT NULL DEFAULT '{}', + max_safe_config JSONB, + variants JSONB DEFAULT '[]', + zone_map JSONB DEFAULT '{}', + + -- Metadata + constraint_version VARCHAR(50) DEFAULT '1.0.0', + + -- Audit + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_maximizer_tenant + ON maximizer_optimizations(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_maximizer_tenant_created + ON maximizer_optimizations(tenant_id, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_maximizer_assessment + ON maximizer_optimizations(assessment_id); diff --git a/ai-compliance-sdk/policies/maximizer_constraints_v1.json b/ai-compliance-sdk/policies/maximizer_constraints_v1.json new file mode 100644 index 0000000..f821d44 --- /dev/null +++ b/ai-compliance-sdk/policies/maximizer_constraints_v1.json @@ -0,0 +1,461 @@ +{ + "version": "1.0.0", + "regulations": ["DSGVO", "AI_Act", "BDSG", "EDPB_Guidelines"], + "rules": [ + { + "id": "MC-AIA-PROHIBITED-001", + "obligation_id": "AIACT-OBL-001", + "regulation": "AI_Act", + "article_ref": "Art. 5 AI Act", + "title": "Verbotene KI-Praktiken", + "description": "Systeme mit verbotener Risikoeinstufung duerfen nicht eingesetzt werden", + "rule_type": "hard_prohibition", + "constraints": [ + { + "if": {"risk_classification": "prohibited"}, + "then": {"allowed": false} + } + ] + }, + { + "id": "MC-GDPR-ART22-001", + "obligation_id": "DSGVO-OBL-022", + "regulation": "DSGVO", + "article_ref": "Art. 22 DSGVO", + "title": "Verbot vollautomatisierter Entscheidungen mit erheblicher Wirkung", + "description": "Keine ausschliesslich automatisierten Entscheidungen mit rechtlicher oder aehnlich erheblicher Wirkung", + "rule_type": "hard_prohibition", + "constraints": [ + { + "if": {"decision_impact": "high", "automation_level": "full"}, + "then": {"allowed": false} + } + ] + }, + { + "id": "MC-GDPR-ART22-002", + "obligation_id": "DSGVO-OBL-022", + "regulation": "DSGVO", + "article_ref": "Art. 22 DSGVO", + "title": "Menschliche Ueberpruefung bei hoher Auswirkung", + "description": "Bei hoher Entscheidungswirkung muss menschliche Kontrolle gewaehrleistet sein", + "rule_type": "requirement", + "constraints": [ + { + "if": {"decision_impact": "high"}, + "then": {"required_values": {"human_in_loop": "required", "decision_binding": "human_review_required"}} + } + ] + }, + { + "id": "MC-GDPR-ART22-003", + "obligation_id": "DSGVO-OBL-022", + "regulation": "EDPB_Guidelines", + "article_ref": "Art. 22 DSGVO / EDPB Guidelines", + "title": "Echte menschliche Entscheidungsmacht erforderlich", + "description": "Pro-forma Human-in-Loop ohne echte Entscheidungsbefugnis genuegt nicht", + "rule_type": "requirement", + "constraints": [ + { + "if": {"decision_impact": "high", "decision_binding": "fully_binding"}, + "then": {"required_values": {"decision_binding": "human_review_required"}} + } + ] + }, + { + "id": "MC-GDPR-ART9-001", + "obligation_id": "DSGVO-OBL-009", + "regulation": "DSGVO", + "article_ref": "Art. 9 DSGVO", + "title": "Besondere Datenkategorien erfordern spezielle Rechtsgrundlage", + "description": "Verarbeitung sensibler Daten nur mit Einwilligung oder oeffentlichem Interesse", + "rule_type": "requirement", + "constraints": [ + { + "if": {"data_type": "sensitive"}, + "then": {"required_values": {"legal_basis": "consent"}, "required_controls": ["C_EXPLICIT_CONSENT"]} + } + ] + }, + { + "id": "MC-GDPR-ART9-002", + "obligation_id": "DSGVO-OBL-009", + "regulation": "DSGVO", + "article_ref": "Art. 9 DSGVO", + "title": "Biometrische Daten erfordern erhoehte Pruefung", + "description": "Biometrische Daten loesen verstaerkte Rechtsgrundlagen-Pruefung und Transparenzpflicht aus", + "rule_type": "escalation_gate", + "constraints": [ + { + "if": {"data_type": "biometric"}, + "then": {"required_values": {"legal_basis": "consent", "transparency_required": "true"}, "required_controls": ["C_EXPLICIT_CONSENT", "C_DSFA"]} + } + ] + }, + { + "id": "MC-AIA-HR-001", + "obligation_id": "AIACT-OBL-HR-001", + "regulation": "AI_Act", + "article_ref": "Annex III Nr. 4 AI Act", + "title": "KI im HR-Bereich ist Hochrisiko", + "description": "KI-Systeme im Bereich Beschaeftigung mit hoher Auswirkung erfordern Hochrisiko-Einstufung", + "rule_type": "classification_rule", + "constraints": [ + { + "if": {"domain": "hr", "decision_impact": "high"}, + "then": {"set_risk_classification": "high", "required_values": {"logging_required": "true", "transparency_required": "true"}, "required_controls": ["C_TRANSPARENCY", "C_ACCESS_LOGGING"]} + } + ] + }, + { + "id": "MC-AIA-HR-002", + "obligation_id": "AIACT-OBL-HR-002", + "regulation": "AI_Act", + "article_ref": "Annex III Nr. 4 AI Act", + "title": "HR-Ranking und Bewerberauswahl als Hochrisiko", + "description": "KI fuer Bewerber-Ranking oder -Klassifikation muss als Hochrisiko bewertet werden", + "rule_type": "classification_rule", + "constraints": [ + { + "if": {"domain": "hr", "decision_impact": "medium"}, + "then": {"set_risk_classification": "high", "required_values": {"logging_required": "true"}} + } + ] + }, + { + "id": "MC-AIA-HIGHRISK-001", + "obligation_id": "AIACT-OBL-OVERSIGHT", + "regulation": "AI_Act", + "article_ref": "Art. 14 AI Act", + "title": "Hochrisiko-KI erfordert Human Oversight", + "description": "Hochrisiko-KI-Systeme muessen wirksame menschliche Aufsicht ermoeglichen", + "rule_type": "requirement", + "constraints": [ + { + "if": {"risk_classification": "high"}, + "then": {"required_values": {"human_in_loop": "required"}, "required_controls": ["C_CONTESTATION"]} + } + ] + }, + { + "id": "MC-AIA-HIGHRISK-002", + "obligation_id": "AIACT-OBL-LOGGING", + "regulation": "AI_Act", + "article_ref": "Art. 12 AI Act", + "title": "Hochrisiko-KI erfordert Logging", + "description": "Hochrisiko-KI-Systeme muessen Betrieb und Vorfaelle protokollieren koennen", + "rule_type": "requirement", + "constraints": [ + { + "if": {"risk_classification": "high"}, + "then": {"required_values": {"logging_required": "true"}, "required_controls": ["C_ACCESS_LOGGING"]} + } + ] + }, + { + "id": "MC-AIA-HIGHRISK-003", + "obligation_id": "AIACT-OBL-TRANSPARENCY", + "regulation": "AI_Act", + "article_ref": "Art. 13 AI Act", + "title": "Hochrisiko-KI erfordert Transparenz", + "description": "Hochrisiko-KI-Systeme muessen Transparenzanforderungen erfuellen", + "rule_type": "requirement", + "constraints": [ + { + "if": {"risk_classification": "high"}, + "then": {"required_values": {"transparency_required": "true"}, "required_controls": ["C_TRANSPARENCY"]} + } + ] + }, + { + "id": "MC-AIA-HIGHRISK-004", + "obligation_id": "AIACT-OBL-EXPLAIN", + "regulation": "AI_Act", + "article_ref": "Art. 13 AI Act", + "title": "Hochrisiko-KI erfordert Mindest-Erklaerbarkeit", + "description": "Hochrisiko-Systeme muessen ein Mindestmass an Erklaerbarkeit bieten", + "rule_type": "requirement", + "constraints": [ + { + "if": {"risk_classification": "high", "explainability": "none"}, + "then": {"required_values": {"explainability": "basic"}} + } + ] + }, + { + "id": "MC-AIA-TRANS-001", + "obligation_id": "AIACT-OBL-TRANS-USER", + "regulation": "AI_Act", + "article_ref": "Art. 52 AI Act", + "title": "KI-Interaktion erfordert Nutzerbenachrichtigung", + "description": "Nutzer muessen ueber die KI-Interaktion informiert werden", + "rule_type": "requirement", + "constraints": [ + { + "if": {"deployment_scope": "external"}, + "then": {"required_values": {"transparency_required": "true"}} + }, + { + "if": {"deployment_scope": "public"}, + "then": {"required_values": {"transparency_required": "true"}} + } + ] + }, + { + "id": "MC-GDPR-PRINCIPLES-001", + "obligation_id": "DSGVO-OBL-005", + "regulation": "DSGVO", + "article_ref": "Art. 5 DSGVO", + "title": "Datenminimierung bei personenbezogenen Daten", + "description": "Personenbezogene Datenverarbeitung erfordert Datenminimierungsmassnahmen", + "rule_type": "requirement", + "constraints": [ + { + "if": {"data_type": "personal"}, + "then": {"required_controls": ["C_RETENTION_POLICY"]} + }, + { + "if": {"data_type": "sensitive"}, + "then": {"required_controls": ["C_RETENTION_POLICY", "C_ENCRYPTION"]} + }, + { + "if": {"data_type": "biometric"}, + "then": {"required_controls": ["C_RETENTION_POLICY", "C_ENCRYPTION"]} + } + ] + }, + { + "id": "MC-GDPR-INFO-001", + "obligation_id": "DSGVO-OBL-013", + "regulation": "DSGVO", + "article_ref": "Art. 13-14 DSGVO", + "title": "Informationspflicht bei personenbezogenen Daten", + "description": "Betroffene muessen ueber die Verarbeitung personenbezogener Daten informiert werden", + "rule_type": "requirement", + "constraints": [ + { + "if": {"data_type": "personal", "transparency_required": "false"}, + "then": {"required_values": {"transparency_required": "true"}} + } + ] + }, + { + "id": "MC-GDPR-RIGHTS-001", + "obligation_id": "DSGVO-OBL-015", + "regulation": "DSGVO", + "article_ref": "Art. 15 DSGVO", + "title": "Erklaerbarkeit bei hoher Auswirkung", + "description": "Bei hoher Entscheidungswirkung muss die Verarbeitung erklaerbar sein", + "rule_type": "requirement", + "constraints": [ + { + "if": {"decision_impact": "high", "explainability": "none"}, + "then": {"required_values": {"explainability": "basic"}} + } + ] + }, + { + "id": "MC-GDPR-DPIA-001", + "obligation_id": "DSGVO-OBL-035", + "regulation": "DSGVO", + "article_ref": "Art. 35 DSGVO", + "title": "DSFA bei hohem Risiko", + "description": "Hohe Entscheidungswirkung mit personenbezogenen Daten erfordert DSFA-Screening", + "rule_type": "requirement", + "constraints": [ + { + "if": {"decision_impact": "high", "data_type": "personal"}, + "then": {"required_controls": ["C_DSFA"]} + }, + { + "if": {"decision_impact": "high", "data_type": "sensitive"}, + "then": {"required_controls": ["C_DSFA"]} + } + ] + }, + { + "id": "MC-GDPR-SEC-001", + "obligation_id": "DSGVO-OBL-032", + "regulation": "DSGVO", + "article_ref": "Art. 32 DSGVO", + "title": "Sicherheitsmassnahmen bei personenbezogenen Daten", + "description": "Personenbezogene Daten erfordern angemessene technische und organisatorische Massnahmen", + "rule_type": "requirement", + "constraints": [ + { + "if": {"data_type": "personal"}, + "then": {"required_controls": ["C_ENCRYPTION"]} + } + ] + }, + { + "id": "MC-GDPR-SEC-002", + "obligation_id": "DSGVO-OBL-032", + "regulation": "DSGVO", + "article_ref": "Art. 32 DSGVO", + "title": "Audit-Logging bei mittlerer bis hoher Auswirkung", + "description": "Entscheidungen mit mittlerer oder hoher Auswirkung muessen protokolliert werden", + "rule_type": "requirement", + "constraints": [ + { + "if": {"decision_impact": "medium"}, + "then": {"required_values": {"logging_required": "true"}} + }, + { + "if": {"decision_impact": "high"}, + "then": {"required_values": {"logging_required": "true"}} + } + ] + }, + { + "id": "MC-GDPR-PBD-001", + "obligation_id": "DSGVO-OBL-025", + "regulation": "DSGVO", + "article_ref": "Art. 25 DSGVO", + "title": "Privacy by Design bei personenbezogenen Daten", + "description": "KI-Systeme mit personenbezogenen Daten muessen Privacy by Design implementieren", + "rule_type": "requirement", + "constraints": [ + { + "if": {"data_type": "personal", "deployment_scope": "public"}, + "then": {"required_patterns": ["P_PRE_ANON", "P_NAMESPACE_ISOLATION"]} + } + ] + }, + { + "id": "MC-AIA-EDUCATION-001", + "obligation_id": "AIACT-OBL-EDU-001", + "regulation": "AI_Act", + "article_ref": "Annex III Nr. 3 AI Act", + "title": "KI im Bildungsbereich mit hoher Auswirkung ist Hochrisiko", + "description": "KI-Systeme im Bildungsbereich mit hoher Entscheidungswirkung erfordern Hochrisiko-Einstufung", + "rule_type": "classification_rule", + "constraints": [ + { + "if": {"domain": "education", "decision_impact": "high"}, + "then": {"set_risk_classification": "high", "required_values": {"logging_required": "true", "transparency_required": "true"}} + } + ] + }, + { + "id": "MC-AIA-FINANCE-001", + "obligation_id": "AIACT-OBL-FIN-001", + "regulation": "AI_Act", + "article_ref": "Annex III Nr. 5 AI Act", + "title": "KI fuer Kreditvergabe und Versicherung ist Hochrisiko", + "description": "KI-Systeme fuer wesentliche Dienste wie Kreditvergabe oder Versicherung erfordern Hochrisiko-Einstufung", + "rule_type": "classification_rule", + "constraints": [ + { + "if": {"domain": "finance", "decision_impact": "high"}, + "then": {"set_risk_classification": "high", "required_values": {"human_in_loop": "required", "logging_required": "true", "transparency_required": "true"}} + } + ] + }, + { + "id": "MC-AIA-HEALTH-001", + "obligation_id": "AIACT-OBL-HEALTH-001", + "regulation": "AI_Act", + "article_ref": "Annex III Nr. 5 AI Act", + "title": "KI im Gesundheitsbereich mit hoher Auswirkung ist Hochrisiko", + "description": "KI-Systeme im Gesundheitsbereich mit hoher Auswirkung erfordern Hochrisiko-Einstufung", + "rule_type": "classification_rule", + "constraints": [ + { + "if": {"domain": "health", "decision_impact": "high"}, + "then": {"set_risk_classification": "high", "required_values": {"human_in_loop": "required", "logging_required": "true", "explainability": "high"}} + } + ] + }, + { + "id": "MC-GDPR-LAWFULNESS-001", + "obligation_id": "DSGVO-OBL-006", + "regulation": "DSGVO", + "article_ref": "Art. 6 DSGVO", + "title": "Vollautomatisierung mit personenbezogenen Daten erfordert Einwilligung oder Vertrag", + "description": "Vollautomatisierte Verarbeitung personenbezogener Daten muss auf Einwilligung oder Vertrag basieren", + "rule_type": "requirement", + "constraints": [ + { + "if": {"automation_level": "full", "data_type": "personal"}, + "then": {"required_controls": ["C_EXPLICIT_CONSENT"]} + } + ] + }, + { + "id": "MC-AIA-BLACKBOX-001", + "obligation_id": "AIACT-OBL-EXPLAIN-002", + "regulation": "AI_Act", + "article_ref": "Art. 13 AI Act", + "title": "Blackbox-Modelle bei hoher Auswirkung erfordern erhoehte Erklaerbarkeit", + "description": "Blackbox-LLM-Modelle mit hoher Entscheidungswirkung muessen mindestens Basic-Erklaerbarkeit bieten", + "rule_type": "requirement", + "constraints": [ + { + "if": {"model_type": "blackbox_llm", "decision_impact": "high", "explainability": "none"}, + "then": {"required_values": {"explainability": "basic"}} + } + ] + }, + { + "id": "MC-GDPR-PROFILING-001", + "obligation_id": "DSGVO-OBL-PROFILING", + "regulation": "EDPB_Guidelines", + "article_ref": "Art. 22 DSGVO / EDPB Profiling Guidelines", + "title": "Profiling mit erheblicher Wirkung erfordert Transparenz und Fairness", + "description": "Ranking- und Klassifikationssysteme mit hoher Auswirkung muessen Fairness- und Transparenzpruefung bestehen", + "rule_type": "requirement", + "constraints": [ + { + "if": {"decision_impact": "high", "deployment_scope": "external"}, + "then": {"required_values": {"transparency_required": "true", "explainability": "basic"}, "required_controls": ["C_CONTESTATION"]} + } + ] + }, + { + "id": "MC-OPT-META-001", + "obligation_id": "OPT-DERIVED-001", + "regulation": "AI_Act", + "article_ref": "Abgeleitet aus AI Act + DSGVO", + "title": "Optimierungsregel: Vollautomatisierung auf Assistenz reduzieren", + "description": "Wenn Vollautomatisierung bei hoher Wirkung blockiert ist, naechste konforme Konfiguration vorschlagen", + "rule_type": "optimizer_rule", + "constraints": [ + { + "if": {"automation_level": "full", "decision_impact": "high"}, + "then": {"required_values": {"automation_level": "assistive", "human_in_loop": "required", "decision_binding": "human_review_required"}} + } + ] + }, + { + "id": "MC-OPT-META-002", + "obligation_id": "OPT-DERIVED-002", + "regulation": "DSGVO", + "article_ref": "Abgeleitet aus DSGVO Grundsaetze", + "title": "Optimierungsregel: Datensensitivitaet reduzieren", + "description": "Wenn sensible Daten ohne Notwendigkeitsnachweis vorgeschlagen werden, geringere Datentiefe empfehlen", + "rule_type": "optimizer_rule", + "constraints": [ + { + "if": {"data_type": "sensitive", "decision_impact": "low"}, + "then": {"required_values": {"data_type": "personal"}} + } + ] + }, + { + "id": "MC-OPT-META-003", + "obligation_id": "OPT-DERIVED-003", + "regulation": "AI_Act", + "article_ref": "Abgeleitet aus AI Act + DSGVO", + "title": "Optimierungsregel: Maximale Contestability bei Profiling", + "description": "Wenn Profiling nicht vermeidbar ist, Contestability und Transparenz maximieren", + "rule_type": "optimizer_rule", + "constraints": [ + { + "if": {"decision_impact": "high", "deployment_scope": "public"}, + "then": {"required_values": {"transparency_required": "true", "explainability": "high"}, "required_controls": ["C_CONTESTATION"]} + } + ] + } + ] +}