All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 23s
- DOCUMENT_SDK_STEP_MAP: 12 kaputte URLs korrigiert (z.B. /sdk/loeschkonzept → /sdk/loeschfristen) - Go Backend: iace_ce_assessment zur validTypes-Whitelist hinzugefuegt - SOUL-Datei: von 17 auf ~80 Zeilen erweitert (18 draftbare Typen, Redirects, operative Module) - Intent Classifier: 10 fehlende Dokumenttyp-Patterns + 5 Redirect-Patterns (Impressum/AGB/Widerruf → Document Generator) - State Projector: getExistingDocumentTypes von 6 auf 11 Checks erweitert (risks, escalations, iace, obligations, dsr) - DraftingEngineWidget: Gap-Banner fuer kritische Luecken mit Analysieren-Button - Cross-Validation: 4 neue deterministische Regeln (DSFA-NO-VVT, DSFA-NO-TOM, DSI-NO-LF, AV-NO-VVT) - Prose Blocks: 5 neue Dokumenttypen (av_vertrag, betroffenenrechte, risikoanalyse, notfallplan, iace_ce_assessment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
249 lines
9.5 KiB
TypeScript
249 lines
9.5 KiB
TypeScript
/**
|
|
* Drafting Engine - Validate API
|
|
*
|
|
* Stufe 1: Deterministische Pruefung gegen DOCUMENT_SCOPE_MATRIX
|
|
* Stufe 2: LLM Cross-Consistency Check
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { DOCUMENT_SCOPE_MATRIX, DOCUMENT_TYPE_LABELS, getDepthLevelNumeric } from '@/lib/sdk/compliance-scope-types'
|
|
import type { ScopeDocumentType, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
|
|
import type { ValidationContext, ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
|
|
import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validate-cross-check'
|
|
|
|
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
|
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
|
|
|
/**
|
|
* Stufe 1: Deterministische Pruefung
|
|
*/
|
|
function deterministicCheck(
|
|
documentType: ScopeDocumentType,
|
|
validationContext: ValidationContext
|
|
): ValidationFinding[] {
|
|
const findings: ValidationFinding[] = []
|
|
const level = validationContext.scopeLevel
|
|
const levelNumeric = getDepthLevelNumeric(level)
|
|
const req = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
|
|
|
|
// Check 1: Ist das Dokument auf diesem Level erforderlich?
|
|
if (req && !req.required && levelNumeric < 3) {
|
|
findings.push({
|
|
id: `DET-OPT-${documentType}`,
|
|
severity: 'suggestion',
|
|
category: 'scope_violation',
|
|
title: `${DOCUMENT_TYPE_LABELS[documentType] ?? documentType} ist optional`,
|
|
description: `Auf Level ${level} ist dieses Dokument nicht verpflichtend.`,
|
|
documentType,
|
|
})
|
|
}
|
|
|
|
// Check 2: VVT vorhanden wenn erforderlich?
|
|
const vvtReq = DOCUMENT_SCOPE_MATRIX.vvt[level]
|
|
if (vvtReq.required && validationContext.crossReferences.vvtCategories.length === 0) {
|
|
findings.push({
|
|
id: 'DET-VVT-MISSING',
|
|
severity: 'error',
|
|
category: 'missing_content',
|
|
title: 'VVT fehlt',
|
|
description: `Auf Level ${level} ist ein VVT Pflicht, aber keine Eintraege vorhanden.`,
|
|
documentType: 'vvt',
|
|
legalReference: 'Art. 30 DSGVO',
|
|
})
|
|
}
|
|
|
|
// Check 3: TOM vorhanden wenn VVT existiert?
|
|
if (validationContext.crossReferences.vvtCategories.length > 0
|
|
&& validationContext.crossReferences.tomControls.length === 0) {
|
|
findings.push({
|
|
id: 'DET-TOM-MISSING-FOR-VVT',
|
|
severity: 'warning',
|
|
category: 'cross_reference',
|
|
title: 'TOM fehlt bei vorhandenem VVT',
|
|
description: 'VVT-Eintraege existieren, aber keine TOM-Massnahmen sind definiert.',
|
|
documentType: 'tom',
|
|
crossReferenceType: 'vvt',
|
|
legalReference: 'Art. 32 DSGVO',
|
|
suggestion: 'TOM-Massnahmen erstellen, die die VVT-Taetigkeiten absichern.',
|
|
})
|
|
}
|
|
|
|
// Check 4: Loeschfristen fuer VVT-Kategorien
|
|
if (validationContext.crossReferences.vvtCategories.length > 0
|
|
&& validationContext.crossReferences.retentionCategories.length === 0) {
|
|
findings.push({
|
|
id: 'DET-LF-MISSING-FOR-VVT',
|
|
severity: 'warning',
|
|
category: 'cross_reference',
|
|
title: 'Loeschfristen fehlen',
|
|
description: 'VVT-Eintraege existieren, aber keine Loeschfristen sind definiert.',
|
|
documentType: 'lf',
|
|
crossReferenceType: 'vvt',
|
|
legalReference: 'Art. 17 DSGVO',
|
|
suggestion: 'Loeschfristen fuer alle VVT-Datenkategorien definieren.',
|
|
})
|
|
}
|
|
|
|
// Check 5: DSFA ohne VVT-Grundlage
|
|
if (documentType === 'dsfa' && validationContext.crossReferences.vvtCategories.length === 0) {
|
|
findings.push({
|
|
id: 'DET-DSFA-NO-VVT',
|
|
severity: 'error',
|
|
category: 'cross_reference',
|
|
title: 'DSFA ohne VVT-Grundlage',
|
|
description: 'Eine DSFA setzt ein Verarbeitungsverzeichnis voraus. Ohne VVT fehlt die Uebersicht ueber die betroffenen Verarbeitungstaetigkeiten.',
|
|
documentType: 'dsfa',
|
|
crossReferenceType: 'vvt',
|
|
legalReference: 'Art. 35 i.V.m. Art. 30 DSGVO',
|
|
suggestion: 'Zuerst ein VVT erstellen, dann die DSFA darauf aufbauen.',
|
|
})
|
|
}
|
|
|
|
// Check 6: DSFA ohne TOM-Massnahmen
|
|
if (documentType === 'dsfa' && validationContext.crossReferences.tomControls.length === 0) {
|
|
findings.push({
|
|
id: 'DET-DSFA-NO-TOM',
|
|
severity: 'error',
|
|
category: 'cross_reference',
|
|
title: 'DSFA ohne TOM-Massnahmen',
|
|
description: 'Eine DSFA muss Abhilfemassnahmen enthalten. Ohne TOM-Katalog koennen keine Schutzmassnahmen referenziert werden.',
|
|
documentType: 'dsfa',
|
|
crossReferenceType: 'tom',
|
|
legalReference: 'Art. 35 Abs. 7d DSGVO',
|
|
suggestion: 'TOM-Massnahmen definieren, bevor die DSFA erstellt wird.',
|
|
})
|
|
}
|
|
|
|
// Check 7: Datenschutzerklaerung ohne Loeschfristen
|
|
if (documentType === 'dsi' && validationContext.crossReferences.retentionCategories.length === 0) {
|
|
findings.push({
|
|
id: 'DET-DSI-NO-LF',
|
|
severity: 'warning',
|
|
category: 'cross_reference',
|
|
title: 'Datenschutzerklaerung ohne Loeschfristen',
|
|
description: 'Die Datenschutzerklaerung muss Angaben zur Speicherdauer enthalten. Ohne definierte Loeschfristen fehlt diese Information.',
|
|
documentType: 'dsi',
|
|
crossReferenceType: 'lf',
|
|
legalReference: 'Art. 13 Abs. 2a DSGVO',
|
|
suggestion: 'Loeschfristen definieren und in der Datenschutzerklaerung referenzieren.',
|
|
})
|
|
}
|
|
|
|
// Check 8: AVV ohne VVT-Kontext
|
|
if (documentType === 'av_vertrag' && validationContext.crossReferences.vvtCategories.length === 0) {
|
|
findings.push({
|
|
id: 'DET-AV-NO-VVT',
|
|
severity: 'warning',
|
|
category: 'cross_reference',
|
|
title: 'AVV ohne VVT-Kontext',
|
|
description: 'Ein Auftragsverarbeitungsvertrag sollte auf den im VVT dokumentierten Verarbeitungstaetigkeiten basieren.',
|
|
documentType: 'av_vertrag',
|
|
crossReferenceType: 'vvt',
|
|
legalReference: 'Art. 28 Abs. 3 i.V.m. Art. 30 DSGVO',
|
|
suggestion: 'VVT erstellen, um die betroffenen Verarbeitungstaetigkeiten fuer den AVV zu identifizieren.',
|
|
})
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const { documentType, draftContent, validationContext } = body
|
|
|
|
if (!documentType || !validationContext) {
|
|
return NextResponse.json(
|
|
{ error: 'documentType und validationContext sind erforderlich' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stufe 1: Deterministische Pruefung
|
|
// ---------------------------------------------------------------
|
|
const deterministicFindings = deterministicCheck(documentType, validationContext)
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stufe 2: LLM Cross-Consistency Check
|
|
// ---------------------------------------------------------------
|
|
let llmFindings: ValidationFinding[] = []
|
|
|
|
try {
|
|
const crossCheckPrompt = buildCrossCheckPrompt({
|
|
context: validationContext,
|
|
})
|
|
|
|
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: LLM_MODEL,
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: 'Du bist ein DSGVO-Compliance-Validator. Antworte NUR im JSON-Format.',
|
|
},
|
|
{ role: 'user', content: crossCheckPrompt },
|
|
],
|
|
stream: false,
|
|
options: { temperature: 0.1, num_predict: 8192 },
|
|
format: 'json',
|
|
}),
|
|
signal: AbortSignal.timeout(120000),
|
|
})
|
|
|
|
if (ollamaResponse.ok) {
|
|
const result = await ollamaResponse.json()
|
|
try {
|
|
const parsed = JSON.parse(result.message?.content || '{}')
|
|
llmFindings = [
|
|
...(parsed.errors || []),
|
|
...(parsed.warnings || []),
|
|
...(parsed.suggestions || []),
|
|
].map((f: Record<string, unknown>, i: number) => ({
|
|
id: String(f.id || `LLM-${i}`),
|
|
severity: (String(f.severity || 'suggestion')) as 'error' | 'warning' | 'suggestion',
|
|
category: (String(f.category || 'inconsistency')) as ValidationFinding['category'],
|
|
title: String(f.title || ''),
|
|
description: String(f.description || ''),
|
|
documentType: (String(f.documentType || documentType)) as ScopeDocumentType,
|
|
crossReferenceType: f.crossReferenceType ? String(f.crossReferenceType) as ScopeDocumentType : undefined,
|
|
legalReference: f.legalReference ? String(f.legalReference) : undefined,
|
|
suggestion: f.suggestion ? String(f.suggestion) : undefined,
|
|
}))
|
|
} catch {
|
|
// LLM response not parseable, skip
|
|
}
|
|
}
|
|
} catch {
|
|
// LLM unavailable, continue with deterministic results only
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Combine results
|
|
// ---------------------------------------------------------------
|
|
const allFindings = [...deterministicFindings, ...llmFindings]
|
|
const errors = allFindings.filter(f => f.severity === 'error')
|
|
const warnings = allFindings.filter(f => f.severity === 'warning')
|
|
const suggestions = allFindings.filter(f => f.severity === 'suggestion')
|
|
|
|
const result: ValidationResult = {
|
|
passed: errors.length === 0,
|
|
timestamp: new Date().toISOString(),
|
|
scopeLevel: validationContext.scopeLevel,
|
|
errors,
|
|
warnings,
|
|
suggestions,
|
|
}
|
|
|
|
return NextResponse.json(result)
|
|
} catch (error) {
|
|
console.error('Validation error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Validierung fehlgeschlagen.' },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
}
|