feat: Compliance Maximizer — Regulatory Optimization Engine
Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 1m45s
Build + Deploy / build-backend-compliance (push) Successful in 4m42s
Build + Deploy / build-ai-sdk (push) Successful in 46s
Build + Deploy / build-developer-portal (push) Successful in 1m6s
Build + Deploy / build-tts (push) Successful in 1m14s
Build + Deploy / build-document-crawler (push) Successful in 31s
Build + Deploy / build-dsms-gateway (push) Successful in 24s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 15s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m27s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 37s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 18s
Build + Deploy / trigger-orca (push) Successful in 4m35s
Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 1m45s
Build + Deploy / build-backend-compliance (push) Successful in 4m42s
Build + Deploy / build-ai-sdk (push) Successful in 46s
Build + Deploy / build-developer-portal (push) Successful in 1m6s
Build + Deploy / build-tts (push) Successful in 1m14s
Build + Deploy / build-document-crawler (push) Successful in 31s
Build + Deploy / build-dsms-gateway (push) Successful in 24s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 15s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m27s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 37s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 18s
Build + Deploy / trigger-orca (push) Successful in 4m35s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> {
|
||||
const headers: Record<string, string> = { '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')
|
||||
}
|
||||
151
admin-compliance/app/sdk/compliance-optimizer/[id]/page.tsx
Normal file
151
admin-compliance/app/sdk/compliance-optimizer/[id]/page.tsx
Normal file
@@ -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<any>(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 <div className="max-w-6xl mx-auto p-6 text-gray-500">Laden...</div>
|
||||
if (!data) return <div className="max-w-6xl mx-auto p-6 text-red-600">Optimierung nicht gefunden.</div>
|
||||
|
||||
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 (
|
||||
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Link href="/sdk/compliance-optimizer" className="text-sm text-blue-600 hover:underline">← Zurueck</Link>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mt-1">{data.title || 'Optimierung'}</h1>
|
||||
<p className="text-sm text-gray-500">{new Date(data.created_at).toLocaleString('de-DE')} — v{data.constraint_version}</p>
|
||||
</div>
|
||||
<ZoneBadge zone={data.is_compliant ? 'SAFE' : 'FORBIDDEN'} />
|
||||
</div>
|
||||
|
||||
{/* 3-Zone Summary */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">3-Zonen-Analyse</h2>
|
||||
<DimensionZoneTable zoneMap={zones} />
|
||||
</div>
|
||||
|
||||
{/* Optimization Result */}
|
||||
{maxSafe && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">Optimierte Konfiguration</h2>
|
||||
<OptimizationScoreCard
|
||||
safetyScore={maxSafe.safety_score}
|
||||
utilityScore={maxSafe.utility_score}
|
||||
compositeScore={maxSafe.composite_score}
|
||||
deltaCount={maxSafe.delta_count}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<ConfigComparison deltas={maxSafe.deltas || []} />
|
||||
</div>
|
||||
{maxSafe.rationale && (
|
||||
<p className="mt-3 text-sm text-gray-600 italic">{maxSafe.rationale}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alternative Variants */}
|
||||
{variants.length > 1 && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">Alternative Varianten ({variants.length})</h2>
|
||||
<div className="flex gap-2 mb-3">
|
||||
{variants.map((v: any, i: number) => (
|
||||
<button key={i} onClick={() => setActiveVariant(i)}
|
||||
className={`px-3 py-1 text-sm rounded ${i === activeVariant ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>
|
||||
Variante {i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{variants[activeVariant] && (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-2 text-sm text-gray-600">
|
||||
<span>Sicherheit: {variants[activeVariant].safety_score}</span>
|
||||
<span>Nutzen: {variants[activeVariant].utility_score}</span>
|
||||
<span>Gesamt: {Math.round(variants[activeVariant].composite_score)}</span>
|
||||
</div>
|
||||
<ConfigComparison deltas={variants[activeVariant].deltas || []} />
|
||||
{variants[activeVariant].rationale && (
|
||||
<p className="mt-2 text-sm text-gray-500 italic">{variants[activeVariant].rationale}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Required Controls & Patterns */}
|
||||
{(controls.length > 0 || patterns.length > 0) && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">Erforderliche Massnahmen</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{controls.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Controls</h4>
|
||||
<ul className="space-y-1">
|
||||
{controls.map((c: string, i: number) => (
|
||||
<li key={i} className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full" />{c}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{patterns.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Architektur-Patterns</h4>
|
||||
<ul className="space-y-1">
|
||||
{patterns.map((p: string, i: number) => (
|
||||
<li key={i} className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-purple-500 rounded-full" />{p}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Triggered Rules (Audit Trail) */}
|
||||
{triggered.length > 0 && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">Ausgeloeste Regeln ({triggered.length})</h2>
|
||||
<div className="space-y-2">
|
||||
{triggered.map((r: any, i: number) => (
|
||||
<div key={i} className="flex items-start gap-3 text-sm border-b border-gray-100 pb-2">
|
||||
<span className="font-mono text-xs text-gray-400 min-w-[120px]">{r.rule_id}</span>
|
||||
<span className="text-gray-700">{r.title}</span>
|
||||
<span className="text-gray-400 ml-auto text-xs">{r.article_ref}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
163
admin-compliance/app/sdk/compliance-optimizer/new/page.tsx
Normal file
163
admin-compliance/app/sdk/compliance-optimizer/new/page.tsx
Normal file
@@ -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<Record<string, string>>({
|
||||
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<Record<string, { zone: string }> | 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 (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-1">Neue Optimierung</h1>
|
||||
<p className="text-sm text-gray-500 mb-6">Konfigurieren Sie Ihren KI-Use-Case und finden Sie den maximalen regulatorischen Spielraum.</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Titel</label>
|
||||
<input type="text" value={title} onChange={(e) => 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" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{DIMENSIONS.map((dim) => (
|
||||
<div key={dim.key}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{dim.label}
|
||||
{preview && preview[dim.key] && (
|
||||
<span className="ml-2"><ZoneBadge zone={preview[dim.key].zone as 'FORBIDDEN' | 'RESTRICTED' | 'SAFE'} /></span>
|
||||
)}
|
||||
</label>
|
||||
<select
|
||||
value={config[dim.key]}
|
||||
onChange={(e) => { setConfig({ ...config, [dim.key]: e.target.value }); setPreview(null) }}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white"
|
||||
>
|
||||
{dim.options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{TOGGLE_DIMENSIONS.map((dim) => (
|
||||
<div key={dim.key} className="flex items-center gap-3">
|
||||
<input type="checkbox" checked={config[dim.key] === 'true'}
|
||||
onChange={(e) => { setConfig({ ...config, [dim.key]: String(e.target.checked) }); setPreview(null) }}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600" />
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
{dim.label}
|
||||
{preview && preview[dim.key] && (
|
||||
<span className="ml-2"><ZoneBadge zone={preview[dim.key].zone as 'FORBIDDEN' | 'RESTRICTED' | 'SAFE'} /></span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button onClick={handlePreview} className="border border-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-50 text-sm">
|
||||
Vorschau (3-Zonen-Check)
|
||||
</button>
|
||||
<button onClick={handleSubmit} disabled={submitting}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 text-sm font-medium disabled:opacity-50">
|
||||
{submitting ? 'Optimiere...' : 'Optimieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
124
admin-compliance/app/sdk/compliance-optimizer/page.tsx
Normal file
124
admin-compliance/app/sdk/compliance-optimizer/page.tsx
Normal file
@@ -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<string, { zone: 'FORBIDDEN' | 'RESTRICTED' | 'SAFE' }>
|
||||
max_safe_config?: { safety_score: number; utility_score: number }
|
||||
}
|
||||
|
||||
function countZones(zoneMap: Record<string, { zone: string }>) {
|
||||
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<OptimizationSummary[]>([])
|
||||
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 (
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Compliance Optimizer</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Regulatorischen Spielraum maximieren — KI-Use-Cases optimal konfigurieren
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/sdk/compliance-optimizer/new"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 text-sm font-medium"
|
||||
>
|
||||
Neue Optimierung
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-500">Laden...</div>
|
||||
) : optimizations.length === 0 ? (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<p className="text-gray-600 mb-2">Noch keine Optimierungen durchgefuehrt.</p>
|
||||
<Link href="/sdk/compliance-optimizer/new" className="text-blue-600 hover:underline text-sm">
|
||||
Erste Optimierung starten
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Titel</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Zonen</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Datum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{optimizations.map((o) => {
|
||||
const zones = countZones(o.zone_map)
|
||||
return (
|
||||
<tr key={o.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<Link href={`/sdk/compliance-optimizer/${o.id}`} className="text-blue-600 hover:underline font-medium text-sm">
|
||||
{o.title || 'Ohne Titel'}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<ZoneBadge zone={o.is_compliant ? 'SAFE' : 'FORBIDDEN'} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">
|
||||
{zones.forbidden > 0 && <span className="text-red-600 mr-2">{zones.forbidden} verboten</span>}
|
||||
{zones.restricted > 0 && <span className="text-yellow-600 mr-2">{zones.restricted} eingeschraenkt</span>}
|
||||
<span className="text-green-600">{zones.safe} erlaubt</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500">
|
||||
{new Date(o.created_at).toLocaleDateString('de-DE')}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{total > 20 && (
|
||||
<div className="px-4 py-3 bg-gray-50 text-sm text-gray-500">
|
||||
{total} Optimierungen insgesamt
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
|
||||
<AdditionalModuleItem href="/sdk/use-cases" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>} label="Use Cases" isActive={pathname?.startsWith('/sdk/use-cases') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||
<AdditionalModuleItem href="/sdk/ai-act" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>} label="AI Act" isActive={pathname?.startsWith('/sdk/ai-act') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||
<AdditionalModuleItem href="/sdk/ai-registration" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>} label="EU Registrierung" isActive={pathname?.startsWith('/sdk/ai-registration') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||
<AdditionalModuleItem href="/sdk/compliance-optimizer" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>} label="Compliance Optimizer" isActive={pathname?.startsWith('/sdk/compliance-optimizer') ?? false} collapsed={collapsed} projectId={projectId} />
|
||||
</div>
|
||||
|
||||
{/* Payment / Terminal */}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
interface DimensionDelta {
|
||||
dimension: string
|
||||
from: string
|
||||
to: string
|
||||
impact: string
|
||||
}
|
||||
|
||||
const DIMENSION_LABELS: Record<string, string> = {
|
||||
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 (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 text-green-700 text-sm">
|
||||
Keine Aenderungen noetig — Ihre Konfiguration ist bereits konform.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">Empfohlene Aenderungen ({deltas.length})</h4>
|
||||
<div className="space-y-1">
|
||||
{deltas.map((d, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-blue-50 border border-blue-200 rounded px-3 py-2 text-sm">
|
||||
<span className="font-medium text-gray-800 min-w-[160px]">
|
||||
{DIMENSION_LABELS[d.dimension] || d.dimension}
|
||||
</span>
|
||||
<span className="text-red-600 font-mono line-through">{d.from}</span>
|
||||
<span className="text-gray-400">→</span>
|
||||
<span className="text-green-700 font-mono font-bold">{d.to}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
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<string, ZoneInfo> }) {
|
||||
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 (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Dimension</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aktueller Wert</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Zone</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Regelgrund</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Rechtsgrundlage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{dimensions.map(([dim, info]) => (
|
||||
<tr key={dim} className={info.zone === 'FORBIDDEN' ? 'bg-red-50' : info.zone === 'RESTRICTED' ? 'bg-yellow-50' : ''}>
|
||||
<td className="px-4 py-2 text-sm font-medium text-gray-900">
|
||||
{DIMENSION_LABELS[dim] || dim}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600 font-mono">
|
||||
{info.current_value}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<ZoneBadge zone={info.zone} />
|
||||
</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-600">
|
||||
{info.reason || '-'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-sm text-gray-500">
|
||||
{info.obligation_refs?.join(', ') || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="relative w-16 h-16">
|
||||
<svg viewBox="0 0 36 36" className="w-16 h-16">
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none" stroke="#e5e7eb" strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none" stroke={color} strokeWidth="3"
|
||||
strokeDasharray={`${value}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-sm font-bold text-gray-800">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function OptimizationScoreCard({ safetyScore, utilityScore, compositeScore, deltaCount }: ScoreCardProps) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">Bewertung der optimierten Konfiguration</h4>
|
||||
<div className="flex items-center gap-6">
|
||||
<ScoreGauge value={safetyScore} label="Sicherheit" color="#22c55e" />
|
||||
<ScoreGauge value={utilityScore} label="Business-Nutzen" color="#3b82f6" />
|
||||
<ScoreGauge value={Math.round(compositeScore)} label="Gesamt" color="#8b5cf6" />
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-2xl font-bold text-gray-800">{deltaCount}</span>
|
||||
<span className="text-xs text-gray-500">Aenderungen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border ${style.bg} ${style.text} ${style.border}`}>
|
||||
{style.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
155
ai-compliance-sdk/internal/api/handlers/maximizer_handlers.go
Normal file
155
ai-compliance-sdk/internal/api/handlers/maximizer_handlers.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
52
ai-compliance-sdk/internal/maximizer/constraint_loader.go
Normal file
52
ai-compliance-sdk/internal/maximizer/constraint_loader.go
Normal file
@@ -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 "."
|
||||
}
|
||||
76
ai-compliance-sdk/internal/maximizer/constraints.go
Normal file
76
ai-compliance-sdk/internal/maximizer/constraints.go
Normal file
@@ -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
|
||||
}
|
||||
306
ai-compliance-sdk/internal/maximizer/dimensions.go
Normal file
306
ai-compliance-sdk/internal/maximizer/dimensions.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
201
ai-compliance-sdk/internal/maximizer/dimensions_test.go
Normal file
201
ai-compliance-sdk/internal/maximizer/dimensions_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
218
ai-compliance-sdk/internal/maximizer/evaluator.go
Normal file
218
ai-compliance-sdk/internal/maximizer/evaluator.go
Normal file
@@ -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
|
||||
}
|
||||
229
ai-compliance-sdk/internal/maximizer/evaluator_test.go
Normal file
229
ai-compliance-sdk/internal/maximizer/evaluator_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
189
ai-compliance-sdk/internal/maximizer/intake_mapper.go
Normal file
189
ai-compliance-sdk/internal/maximizer/intake_mapper.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
291
ai-compliance-sdk/internal/maximizer/optimizer.go
Normal file
291
ai-compliance-sdk/internal/maximizer/optimizer.go
Normal file
@@ -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
|
||||
}
|
||||
300
ai-compliance-sdk/internal/maximizer/optimizer_test.go
Normal file
300
ai-compliance-sdk/internal/maximizer/optimizer_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
84
ai-compliance-sdk/internal/maximizer/scoring.go
Normal file
84
ai-compliance-sdk/internal/maximizer/scoring.go
Normal file
@@ -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)
|
||||
}
|
||||
88
ai-compliance-sdk/internal/maximizer/scoring_test.go
Normal file
88
ai-compliance-sdk/internal/maximizer/scoring_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
144
ai-compliance-sdk/internal/maximizer/service.go
Normal file
144
ai-compliance-sdk/internal/maximizer/service.go
Normal file
@@ -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
|
||||
}
|
||||
209
ai-compliance-sdk/internal/maximizer/store.go
Normal file
209
ai-compliance-sdk/internal/maximizer/store.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Keep imports used by DecisionTreeResult.
|
||||
var (
|
||||
_ uuid.UUID
|
||||
_ time.Time
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Constants / Enums
|
||||
// ============================================================================
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
41
ai-compliance-sdk/migrations/026_maximizer_schema.sql
Normal file
41
ai-compliance-sdk/migrations/026_maximizer_schema.sql
Normal file
@@ -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);
|
||||
461
ai-compliance-sdk/policies/maximizer_constraints_v1.json
Normal file
461
ai-compliance-sdk/policies/maximizer_constraints_v1.json
Normal file
@@ -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"]}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user