/** * 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' /** * Anti-Fake-Evidence: Verbotene Formulierungen * * Flags formulations that falsely claim compliance without evidence. * Only allowed when: control_status=pass AND confidence >= E2 AND * truth_status in (validated_internal, accepted_by_auditor). */ interface EvidenceContext { controlStatus?: string confidenceLevel?: string truthStatus?: string } const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp label: string safeAlternative: string }> = [ { pattern: /ist\s+compliant/gi, label: 'ist compliant', safeAlternative: 'soll compliant sein' }, { pattern: /erfüllt\s+vollständig/gi, label: 'erfüllt vollständig', safeAlternative: 'soll vollständig erfüllt werden' }, { pattern: /wurde\s+geprüft/gi, label: 'wurde geprüft', safeAlternative: 'soll geprüft werden' }, { pattern: /wurde\s+umgesetzt/gi, label: 'wurde umgesetzt', safeAlternative: 'ist zur Umsetzung vorgesehen' }, { pattern: /ist\s+auditiert/gi, label: 'ist auditiert', safeAlternative: 'soll auditiert werden' }, { pattern: /vollständig\s+implementiert/gi, label: 'vollständig implementiert', safeAlternative: 'Implementierung ist vorgesehen' }, { pattern: /nachweislich\s+konform/gi, label: 'nachweislich konform', safeAlternative: 'Konformität ist nachzuweisen' }, ] const CONFIDENCE_ORDER: Record = { E0: 0, E1: 1, E2: 2, E3: 3, E4: 4 } const VALID_TRUTH_STATUSES = new Set(['validated_internal', 'accepted_by_auditor']) function checkForbiddenFormulations( content: string, evidenceContext?: EvidenceContext, ): ValidationFinding[] { const findings: ValidationFinding[] = [] if (!content) return findings // If evidence context shows sufficient proof, allow the formulations if (evidenceContext) { const { controlStatus, confidenceLevel, truthStatus } = evidenceContext const confLevel = CONFIDENCE_ORDER[confidenceLevel ?? 'E0'] ?? 0 if ( controlStatus === 'pass' && confLevel >= CONFIDENCE_ORDER.E2 && VALID_TRUTH_STATUSES.has(truthStatus ?? '') ) { return findings // Formulations are backed by real evidence } } for (const { pattern, label, safeAlternative } of FORBIDDEN_PATTERNS) { // Reset regex state for global patterns pattern.lastIndex = 0 if (pattern.test(content)) { findings.push({ id: `AFE-FORBIDDEN-${label.replace(/\s+/g, '_').toUpperCase()}`, severity: 'error', category: 'forbidden_formulation' as ValidationFinding['category'], title: `Verbotene Formulierung: "${label}"`, description: `Die Formulierung "${label}" impliziert eine nachgewiesene Compliance, die ohne ausreichenden Nachweis (Evidence >= E2, validiert) nicht verwendet werden darf.`, documentType: 'vvt' as ScopeDocumentType, suggestion: `Verwende stattdessen: "${safeAlternative}"`, }) } } return findings } /** * 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, think: false, options: { temperature: 0.1, num_predict: 8192, num_ctx: 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, 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 } // --------------------------------------------------------------- // Stufe 1b: Verbotene Formulierungen (Anti-Fake-Evidence) // --------------------------------------------------------------- const forbiddenFindings = checkForbiddenFormulations( draftContent || '', validationContext.evidenceContext, ) // --------------------------------------------------------------- // Combine results // --------------------------------------------------------------- const allFindings = [...deterministicFindings, ...forbiddenFindings, ...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 } ) } }