From 206183670d1eadb4b449b6f58543180cbf840911 Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Wed, 11 Feb 2026 12:37:18 +0100 Subject: [PATCH] feat(sdk): Add Drafting Engine with 4-mode agent system (Explain/Ask/Draft/Validate) Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine that can generate, validate, and refine compliance documents within Scope Engine constraints. Includes intent classifier, state projector, constraint enforcer, SOUL templates, Go backend endpoints, and React UI components. Co-Authored-By: Claude Opus 4.6 --- .../app/api/sdk/drafting-engine/chat/route.ts | 184 ++++++++ .../api/sdk/drafting-engine/draft/route.ts | 168 +++++++ .../api/sdk/drafting-engine/validate/route.ts | 188 ++++++++ .../sdk/ComplianceAdvisorWidget.tsx | 9 +- admin-v2/components/sdk/DraftEditor.tsx | 300 +++++++++++++ .../components/sdk/DraftingEngineWidget.tsx | 417 ++++++++++++++++++ admin-v2/components/sdk/ValidationReport.tsx | 220 +++++++++ .../__tests__/constraint-enforcer.test.ts | 224 ++++++++++ .../__tests__/intent-classifier.test.ts | 153 +++++++ .../__tests__/state-projector.test.ts | 311 +++++++++++++ .../drafting-engine/constraint-enforcer.ts | 221 ++++++++++ .../sdk/drafting-engine/intent-classifier.ts | 241 ++++++++++ .../prompts/ask-gap-analysis.ts | 49 ++ .../sdk/drafting-engine/prompts/draft-dsfa.ts | 91 ++++ .../prompts/draft-loeschfristen.ts | 78 ++++ .../prompts/draft-privacy-policy.ts | 102 +++++ .../sdk/drafting-engine/prompts/draft-tom.ts | 99 +++++ .../sdk/drafting-engine/prompts/draft-vvt.ts | 109 +++++ .../lib/sdk/drafting-engine/prompts/index.ts | 11 + .../prompts/validate-cross-check.ts | 66 +++ .../sdk/drafting-engine/state-projector.ts | 337 ++++++++++++++ admin-v2/lib/sdk/drafting-engine/types.ts | 279 ++++++++++++ .../drafting-engine/use-drafting-engine.ts | 343 ++++++++++++++ agent-core/soul/drafting-agent.soul.md | 95 ++++ ai-compliance-sdk/cmd/server/main.go | 10 + .../api/handlers/drafting_handlers.go | 335 ++++++++++++++ 26 files changed, 4639 insertions(+), 1 deletion(-) create mode 100644 admin-v2/app/api/sdk/drafting-engine/chat/route.ts create mode 100644 admin-v2/app/api/sdk/drafting-engine/draft/route.ts create mode 100644 admin-v2/app/api/sdk/drafting-engine/validate/route.ts create mode 100644 admin-v2/components/sdk/DraftEditor.tsx create mode 100644 admin-v2/components/sdk/DraftingEngineWidget.tsx create mode 100644 admin-v2/components/sdk/ValidationReport.tsx create mode 100644 admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/intent-classifier.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/index.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/state-projector.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/types.ts create mode 100644 admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts create mode 100644 agent-core/soul/drafting-agent.soul.md create mode 100644 ai-compliance-sdk/internal/api/handlers/drafting_handlers.go diff --git a/admin-v2/app/api/sdk/drafting-engine/chat/route.ts b/admin-v2/app/api/sdk/drafting-engine/chat/route.ts new file mode 100644 index 0000000..e24b211 --- /dev/null +++ b/admin-v2/app/api/sdk/drafting-engine/chat/route.ts @@ -0,0 +1,184 @@ +/** + * Drafting Engine Chat API + * + * Verbindet das DraftingEngineWidget mit dem LLM Backend. + * Unterstuetzt alle 4 Modi: explain, ask, draft, validate. + * Nutzt State-Projection fuer token-effiziente Kontextgabe. + */ + +import { NextRequest, NextResponse } from 'next/server' + +const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086' +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +// SOUL System Prompt (from agent-core/soul/drafting-agent.soul.md) +const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf + +## Identitaet +Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK, +DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und +Konsistenz zwischen Dokumenten sicherzustellen. + +## Strikte Constraints +- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen +- Das bestimmte Level ist bindend fuer die Dokumenttiefe +- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung +- Kommuniziere auf Deutsch, sachlich und verstaendlich +- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung + +## Kompetenzbereich +DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248` + +/** + * Query the RAG corpus for relevant documents + */ +async function queryRAG(query: string): Promise { + try { + const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=3` + const res = await fetch(url, { + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }) + + if (!res.ok) return '' + + const data = await res.json() + if (data.results?.length > 0) { + return data.results + .map( + (r: { source_name?: string; source_code?: string; content?: string }, i: number) => + `[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}` + ) + .join('\n\n---\n\n') + } + return '' + } catch { + return '' + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { + message, + history = [], + sdkStateProjection, + mode = 'explain', + documentType, + } = body + + if (!message || typeof message !== 'string') { + return NextResponse.json({ error: 'Message is required' }, { status: 400 }) + } + + // 1. Query RAG for legal context + const ragContext = await queryRAG(message) + + // 2. Build system prompt with mode-specific instructions + state projection + let systemContent = DRAFTING_SYSTEM_PROMPT + + // Mode-specific instructions + const modeInstructions: Record = { + explain: '\n\n## Aktueller Modus: EXPLAIN\nBeantworte Fragen verstaendlich mit Quellenangaben.', + ask: '\n\n## Aktueller Modus: ASK\nAnalysiere Luecken und stelle gezielte Fragen. Eine Frage pro Antwort.', + draft: `\n\n## Aktueller Modus: DRAFT\nEntwirf strukturierte Dokument-Sections. Dokumenttyp: ${documentType || 'nicht spezifiziert'}.\nAntworte mit JSON wenn ein Draft angefragt wird.`, + validate: '\n\n## Aktueller Modus: VALIDATE\nPruefe Cross-Dokument-Konsistenz. Gib Errors, Warnings und Suggestions zurueck.', + } + systemContent += modeInstructions[mode] || modeInstructions.explain + + // Add state projection context + if (sdkStateProjection) { + systemContent += `\n\n## SDK-State Projektion (${mode}-Kontext)\n${JSON.stringify(sdkStateProjection, null, 0).slice(0, 3000)}` + } + + // Add RAG context + if (ragContext) { + systemContent += `\n\n## Relevanter Rechtskontext\n${ragContext}` + } + + // 3. Build messages array + const messages = [ + { role: 'system', content: systemContent }, + ...history.slice(-10).map((h: { role: string; content: string }) => ({ + role: h.role === 'user' ? 'user' : 'assistant', + content: h.content, + })), + { role: 'user', content: message }, + ] + + // 4. Call LLM with streaming + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages, + stream: true, + options: { + temperature: mode === 'draft' ? 0.2 : 0.3, + num_predict: mode === 'draft' ? 16384 : 8192, + }, + }), + signal: AbortSignal.timeout(120000), + }) + + if (!ollamaResponse.ok) { + const errorText = await ollamaResponse.text() + console.error('LLM error:', ollamaResponse.status, errorText) + return NextResponse.json( + { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, + { status: 502 } + ) + } + + // 5. Stream response back + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + const reader = ollamaResponse.body!.getReader() + const decoder = new TextDecoder() + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n').filter((l) => l.trim()) + + for (const line of lines) { + try { + const json = JSON.parse(line) + if (json.message?.content) { + controller.enqueue(encoder.encode(json.message.content)) + } + } catch { + // Partial JSON, skip + } + } + } + } catch (error) { + console.error('Stream error:', error) + } finally { + controller.close() + } + }, + }) + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }) + } catch (error) { + console.error('Drafting engine chat error:', error) + return NextResponse.json( + { error: 'Verbindung zum LLM fehlgeschlagen.' }, + { status: 503 } + ) + } +} diff --git a/admin-v2/app/api/sdk/drafting-engine/draft/route.ts b/admin-v2/app/api/sdk/drafting-engine/draft/route.ts new file mode 100644 index 0000000..c7a4a32 --- /dev/null +++ b/admin-v2/app/api/sdk/drafting-engine/draft/route.ts @@ -0,0 +1,168 @@ +/** + * Drafting Engine - Draft API + * + * Erstellt strukturierte Compliance-Dokument-Entwuerfe. + * Baut dokument-spezifische Prompts aus SOUL-Template + State-Projection. + * Gibt strukturiertes JSON zurueck. + */ + +import { NextRequest, NextResponse } from 'next/server' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +// Import prompt builders +import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt' +import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom' +import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa' +import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy' +import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen' +import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer' + +const constraintEnforcer = new ConstraintEnforcer() + +const DRAFTING_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe. +Du MUSST immer im JSON-Format antworten mit einem "sections" Array. +Jede Section hat: id, title, content, schemaField. +Halte die Tiefe strikt am vorgegebenen Level. +Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung]. +Sprache: Deutsch.` + +function buildPromptForDocumentType( + documentType: ScopeDocumentType, + context: DraftContext, + instructions?: string +): string { + switch (documentType) { + case 'vvt': + return buildVVTDraftPrompt({ context, instructions }) + case 'tom': + return buildTOMDraftPrompt({ context, instructions }) + case 'dsfa': + return buildDSFADraftPrompt({ context, instructions }) + case 'dsi': + return buildPrivacyPolicyDraftPrompt({ context, instructions }) + case 'lf': + return buildLoeschfristenDraftPrompt({ context, instructions }) + default: + return `## Aufgabe: Entwurf fuer ${documentType} + +### Level: ${context.decisions.level} +### Tiefe: ${context.constraints.depthRequirements.depth} +### Erforderliche Inhalte: +${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${instructions ? `### Anweisungen: ${instructions}` : ''} + +Antworte als JSON mit "sections" Array.` + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { documentType, draftContext, instructions, existingDraft } = body + + if (!documentType || !draftContext) { + return NextResponse.json( + { error: 'documentType und draftContext sind erforderlich' }, + { status: 400 } + ) + } + + // 1. Constraint Check (Hard Gate) + const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) + + if (!constraintCheck.allowed) { + return NextResponse.json({ + draft: null, + constraintCheck, + tokensUsed: 0, + error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), + }, { status: 403 }) + } + + // 2. Build document-specific prompt + const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) + + // 3. Build messages + const messages = [ + { role: 'system', content: DRAFTING_SYSTEM_PROMPT }, + ...(existingDraft ? [{ + role: 'assistant', + content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`, + }] : []), + { role: 'user', content: draftPrompt }, + ] + + // 4. Call LLM (non-streaming for structured output) + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages, + stream: false, + options: { + temperature: 0.15, + num_predict: 16384, + }, + format: 'json', + }), + signal: AbortSignal.timeout(180000), + }) + + if (!ollamaResponse.ok) { + return NextResponse.json( + { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, + { status: 502 } + ) + } + + const result = await ollamaResponse.json() + const content = result.message?.content || '' + + // 5. Parse JSON response + let sections: DraftSection[] = [] + try { + const parsed = JSON.parse(content) + sections = (parsed.sections || []).map((s: Record, i: number) => ({ + id: String(s.id || `section-${i}`), + title: String(s.title || ''), + content: String(s.content || ''), + schemaField: s.schemaField ? String(s.schemaField) : undefined, + })) + } catch { + // If not JSON, wrap raw content as single section + sections = [{ + id: 'raw', + title: 'Entwurf', + content: content, + }] + } + + const draft: DraftRevision = { + id: `draft-${Date.now()}`, + content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'), + sections, + createdAt: new Date().toISOString(), + instruction: instructions, + } + + const response: DraftResponse = { + draft, + constraintCheck, + tokensUsed: result.eval_count || 0, + } + + return NextResponse.json(response) + } catch (error) { + console.error('Draft generation error:', error) + return NextResponse.json( + { error: 'Draft-Generierung fehlgeschlagen.' }, + { status: 503 } + ) + } +} diff --git a/admin-v2/app/api/sdk/drafting-engine/validate/route.ts b/admin-v2/app/api/sdk/drafting-engine/validate/route.ts new file mode 100644 index 0000000..521f5fe --- /dev/null +++ b/admin-v2/app/api/sdk/drafting-engine/validate/route.ts @@ -0,0 +1,188 @@ +/** + * 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.', + }) + } + + 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, 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 } + ) + } +} diff --git a/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx b/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx index 650fb63..65b1eab 100644 --- a/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx +++ b/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx @@ -15,6 +15,7 @@ interface Message { interface ComplianceAdvisorWidgetProps { currentStep?: string + enableDraftingEngine?: boolean } // ============================================================================= @@ -58,7 +59,13 @@ const EXAMPLE_QUESTIONS: Record = { // COMPONENT // ============================================================================= -export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) { +export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftingEngine = false }: ComplianceAdvisorWidgetProps) { + // Feature-flag: If Drafting Engine enabled, render DraftingEngineWidget instead + if (enableDraftingEngine) { + const { DraftingEngineWidget } = require('./DraftingEngineWidget') + return + } + const [isOpen, setIsOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [messages, setMessages] = useState([]) diff --git a/admin-v2/components/sdk/DraftEditor.tsx b/admin-v2/components/sdk/DraftEditor.tsx new file mode 100644 index 0000000..d459780 --- /dev/null +++ b/admin-v2/components/sdk/DraftEditor.tsx @@ -0,0 +1,300 @@ +'use client' + +/** + * DraftEditor - Split-Pane Editor fuer Compliance-Dokument-Entwuerfe + * + * Links (2/3): Gerenderter Draft mit Section-Headern + * Rechts (1/3): Chat-Panel fuer iterative Verfeinerung + * Oben: Document-Type Label, Depth-Level Badge, Constraint-Compliance + */ + +import { useState, useRef, useCallback } from 'react' +import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import type { + DraftRevision, + ConstraintCheckResult, + ValidationResult, +} from '@/lib/sdk/drafting-engine/types' + +interface DraftEditorProps { + draft: DraftRevision + documentType: ScopeDocumentType | null + constraintCheck: ConstraintCheckResult | null + validationResult: ValidationResult | null + isTyping: boolean + onAccept: () => void + onValidate: () => void + onClose: () => void + onRefine: (instruction: string) => void +} + +export function DraftEditor({ + draft, + documentType, + constraintCheck, + validationResult, + isTyping, + onAccept, + onValidate, + onClose, + onRefine, +}: DraftEditorProps) { + const [refineInput, setRefineInput] = useState('') + const [activeSection, setActiveSection] = useState(null) + const contentRef = useRef(null) + + const handleRefine = useCallback(() => { + if (!refineInput.trim() || isTyping) return + onRefine(refineInput.trim()) + setRefineInput('') + }, [refineInput, isTyping, onRefine]) + + const handleRefineKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleRefine() + } + } + + const docLabel = documentType + ? DOCUMENT_TYPE_LABELS[documentType]?.split(' (')[0] || documentType + : 'Dokument' + + return ( +
+
+ {/* Header */} +
+
+ + + +
+
{docLabel} - Entwurf
+
+ {draft.sections.length} Sections | Erstellt {new Date(draft.createdAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} +
+
+
+ +
+ {/* Constraint Badge */} + {constraintCheck && ( + + {constraintCheck.allowed ? 'Constraints OK' : 'Constraint-Verletzung'} + + )} + + {/* Validation Badge */} + {validationResult && ( + + {validationResult.passed ? 'Validiert' : `${validationResult.errors.length} Fehler`} + + )} + + +
+
+ + {/* Adjustment Warnings */} + {constraintCheck && constraintCheck.adjustments.length > 0 && ( +
+ {constraintCheck.adjustments.map((adj, i) => ( +

+ + + + {adj} +

+ ))} +
+ )} + + {/* Main Content: 2/3 Editor + 1/3 Chat */} +
+ {/* Left: Draft Content (2/3) */} +
+ {/* Section Navigation */} +
+ {draft.sections.map((section) => ( + + ))} +
+ + {/* Sections */} +
+ {draft.sections.map((section) => ( +
+
+

{section.title}

+ {section.schemaField && ( + {section.schemaField} + )} +
+
+
+ {section.content} +
+
+
+ ))} +
+
+ + {/* Right: Refinement Chat (1/3) */} +
+
+

Verfeinerung

+

Geben Sie Anweisungen zur Verbesserung

+
+ + {/* Validation Summary (if present) */} + {validationResult && ( +
+
+ {validationResult.errors.length > 0 && ( +
+ + {validationResult.errors.length} Fehler +
+ )} + {validationResult.warnings.length > 0 && ( +
+ + {validationResult.warnings.length} Warnungen +
+ )} + {validationResult.suggestions.length > 0 && ( +
+ + {validationResult.suggestions.length} Vorschlaege +
+ )} +
+
+ )} + + {/* Refinement Area */} +
+
+

+ Beschreiben Sie, was geaendert werden soll. Der Agent erstellt eine ueberarbeitete Version unter Beachtung der Scope-Constraints. +

+ + {/* Quick Refinement Buttons */} +
+ {[ + 'Mehr Details hinzufuegen', + 'Platzhalter ausfuellen', + 'Rechtliche Referenzen ergaenzen', + 'Sprache vereinfachen', + ].map((suggestion) => ( + + ))} +
+
+
+ + {/* Refinement Input */} +
+
+ setRefineInput(e.target.value)} + onKeyDown={handleRefineKeyDown} + placeholder="Anweisung eingeben..." + disabled={isTyping} + className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> + +
+
+
+
+ + {/* Footer Actions */} +
+
+ +
+ +
+ + +
+
+
+
+ ) +} diff --git a/admin-v2/components/sdk/DraftingEngineWidget.tsx b/admin-v2/components/sdk/DraftingEngineWidget.tsx new file mode 100644 index 0000000..0079c6b --- /dev/null +++ b/admin-v2/components/sdk/DraftingEngineWidget.tsx @@ -0,0 +1,417 @@ +'use client' + +/** + * DraftingEngineWidget - Erweitert den ComplianceAdvisor um 4 Modi + * + * Mode-Indicator Pills: Explain / Ask / Draft / Validate + * Document-Type Selector aus requiredDocuments der ScopeDecision + * Feature-Flag enableDraftingEngine fuer schrittweises Rollout + */ + +import { useState, useEffect, useRef, useCallback } from 'react' +import { useSDK } from '@/lib/sdk/context' +import { useDraftingEngine } from '@/lib/sdk/drafting-engine/use-drafting-engine' +import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' +import type { AgentMode } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { DraftEditor } from './DraftEditor' +import { ValidationReport } from './ValidationReport' + +interface DraftingEngineWidgetProps { + currentStep?: string + enableDraftingEngine?: boolean +} + +const MODE_CONFIG: Record = { + explain: { label: 'Explain', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-purple-100 text-purple-700 ring-1 ring-purple-300', icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, + ask: { label: 'Ask', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-amber-100 text-amber-700 ring-1 ring-amber-300', icon: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, + draft: { label: 'Draft', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-blue-100 text-blue-700 ring-1 ring-blue-300', icon: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z' }, + validate: { label: 'Validate', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-green-100 text-green-700 ring-1 ring-green-300', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' }, +} + +const EXAMPLE_QUESTIONS: Record = { + explain: [ + 'Was ist ein Verarbeitungsverzeichnis?', + 'Wann brauche ich eine DSFA?', + 'Was sind TOM nach Art. 32 DSGVO?', + ], + ask: [ + 'Welche Luecken hat mein Compliance-Profil?', + 'Was fehlt noch fuer die Zertifizierung?', + 'Welche Dokumente muss ich noch erstellen?', + ], + draft: [ + 'Erstelle einen VVT-Eintrag fuer unseren Hauptprozess', + 'Erstelle TOM fuer unsere Cloud-Infrastruktur', + 'Erstelle eine Datenschutzerklaerung', + ], + validate: [ + 'Pruefe die Konsistenz meiner Dokumente', + 'Stimmen VVT und TOM ueberein?', + 'Gibt es Luecken bei den Loeschfristen?', + ], +} + +export function DraftingEngineWidget({ + currentStep = 'default', + enableDraftingEngine = true, +}: DraftingEngineWidgetProps) { + const { state } = useSDK() + const engine = useDraftingEngine() + const [isOpen, setIsOpen] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + const [inputValue, setInputValue] = useState('') + const [showDraftEditor, setShowDraftEditor] = useState(false) + const [showValidationReport, setShowValidationReport] = useState(false) + const messagesEndRef = useRef(null) + + // Available document types from scope decision + const availableDocumentTypes: ScopeDocumentType[] = + state.complianceScope?.decision?.requiredDocuments + ?.filter(d => d.required) + .map(d => d.documentType as ScopeDocumentType) ?? ['vvt', 'tom', 'lf'] + + // Auto-scroll + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [engine.messages]) + + // Open draft editor when a new draft arrives + useEffect(() => { + if (engine.currentDraft) { + setShowDraftEditor(true) + } + }, [engine.currentDraft]) + + // Open validation report when new results arrive + useEffect(() => { + if (engine.validationResult) { + setShowValidationReport(true) + } + }, [engine.validationResult]) + + const handleSendMessage = useCallback( + (content: string) => { + if (!content.trim()) return + setInputValue('') + engine.sendMessage(content) + }, + [engine] + ) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSendMessage(inputValue) + } + } + + const exampleQuestions = EXAMPLE_QUESTIONS[engine.currentMode] + + if (!isOpen) { + return ( + + ) + } + + // Draft Editor full-screen overlay + if (showDraftEditor && engine.currentDraft) { + return ( + { + engine.acceptDraft() + setShowDraftEditor(false) + }} + onValidate={() => { + engine.validateDraft() + }} + onClose={() => setShowDraftEditor(false)} + onRefine={(instruction: string) => { + engine.requestDraft(instruction) + }} + validationResult={engine.validationResult} + isTyping={engine.isTyping} + /> + ) + } + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+
Drafting Engine
+
Compliance-Dokumententwurf
+
+
+
+ + +
+
+ + {/* Mode Pills */} +
+ {(Object.keys(MODE_CONFIG) as AgentMode[]).map((mode) => { + const config = MODE_CONFIG[mode] + const isActive = engine.currentMode === mode + return ( + + ) + })} +
+ + {/* Document Type Selector (visible in draft/validate mode) */} + {(engine.currentMode === 'draft' || engine.currentMode === 'validate') && ( +
+
+ Dokument: + +
+
+ )} + + {/* Error Banner */} + {engine.error && ( +
+ {engine.error} + +
+ )} + + {/* Validation Report Inline */} + {showValidationReport && engine.validationResult && ( +
+ setShowValidationReport(false)} + compact + /> +
+ )} + + {/* Messages Area */} +
+ {engine.messages.length === 0 ? ( +
+
+ + + +
+

+ {engine.currentMode === 'explain' && 'Fragen beantworten'} + {engine.currentMode === 'ask' && 'Luecken erkennen'} + {engine.currentMode === 'draft' && 'Dokumente entwerfen'} + {engine.currentMode === 'validate' && 'Konsistenz pruefen'} +

+

+ {engine.currentMode === 'explain' && 'Stellen Sie Fragen zu DSGVO, AI Act und Compliance.'} + {engine.currentMode === 'ask' && 'Identifiziert Luecken in Ihrem Compliance-Profil.'} + {engine.currentMode === 'draft' && 'Erstellt strukturierte Compliance-Dokumente.'} + {engine.currentMode === 'validate' && 'Prueft Cross-Dokument-Konsistenz.'} +

+ +
+

Beispiele:

+ {exampleQuestions.map((q, idx) => ( + + ))} +
+ + {/* Quick Actions for Draft/Validate */} + {engine.currentMode === 'draft' && engine.activeDocumentType && ( + + )} + {engine.currentMode === 'validate' && ( + + )} +
+ ) : ( + <> + {engine.messages.map((message, idx) => ( +
+
+

+ {message.content} +

+ + {/* Draft ready indicator */} + {message.metadata?.hasDraft && engine.currentDraft && ( + + )} + + {/* Validation ready indicator */} + {message.metadata?.hasValidation && engine.validationResult && ( + + )} +
+
+ ))} + + {engine.isTyping && ( +
+
+
+
+
+
+
+
+
+ )} + +
+ + )} +
+ + {/* Input Area */} +
+
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={ + engine.currentMode === 'draft' + ? 'Anweisung fuer den Entwurf...' + : engine.currentMode === 'validate' + ? 'Validierungsfrage...' + : 'Frage eingeben...' + } + disabled={engine.isTyping} + className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50" + /> + {engine.isTyping ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/admin-v2/components/sdk/ValidationReport.tsx b/admin-v2/components/sdk/ValidationReport.tsx new file mode 100644 index 0000000..abe4a79 --- /dev/null +++ b/admin-v2/components/sdk/ValidationReport.tsx @@ -0,0 +1,220 @@ +'use client' + +/** + * ValidationReport - Strukturierte Anzeige von Validierungsergebnissen + * + * Errors (Scope-Violations) in Rot + * Warnings (Inkonsistenzen) in Amber + * Suggestions in Blau + */ + +import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' +import type { ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types' + +interface ValidationReportProps { + result: ValidationResult + onClose: () => void + /** Compact mode for inline display in widget */ + compact?: boolean +} + +const SEVERITY_CONFIG = { + error: { + bg: 'bg-red-50', + border: 'border-red-200', + text: 'text-red-700', + icon: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z', + label: 'Fehler', + dotColor: 'bg-red-500', + }, + warning: { + bg: 'bg-amber-50', + border: 'border-amber-200', + text: 'text-amber-700', + icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z', + label: 'Warnungen', + dotColor: 'bg-amber-500', + }, + suggestion: { + bg: 'bg-blue-50', + border: 'border-blue-200', + text: 'text-blue-700', + icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', + label: 'Vorschlaege', + dotColor: 'bg-blue-500', + }, +} + +function FindingCard({ finding, compact }: { finding: ValidationFinding; compact?: boolean }) { + const config = SEVERITY_CONFIG[finding.severity] + const docLabel = DOCUMENT_TYPE_LABELS[finding.documentType]?.split(' (')[0] || finding.documentType + + if (compact) { + return ( +
+ +
+

{finding.title}

+

{finding.description}

+
+
+ ) + } + + return ( +
+
+ + + +
+
+

{finding.title}

+ {docLabel} +
+

{finding.description}

+ + {finding.crossReferenceType && ( +

+ Cross-Referenz: {DOCUMENT_TYPE_LABELS[finding.crossReferenceType]?.split(' (')[0] || finding.crossReferenceType} +

+ )} + + {finding.legalReference && ( +

{finding.legalReference}

+ )} + + {finding.suggestion && ( +
+ + + +

{finding.suggestion}

+
+ )} +
+
+
+ ) +} + +export function ValidationReport({ result, onClose, compact }: ValidationReportProps) { + const totalFindings = result.errors.length + result.warnings.length + result.suggestions.length + + if (compact) { + return ( +
+
+
+ + + {result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'} + + + ({totalFindings} {totalFindings === 1 ? 'Fund' : 'Funde'}) + +
+ +
+
+ {result.errors.map((f) => )} + {result.warnings.map((f) => )} + {result.suggestions.map((f) => )} +
+
+ ) + } + + return ( +
+ {/* Summary Header */} +
+
+
+
+ + + +
+
+

+ {result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'} +

+

+ Level {result.scopeLevel} | {new Date(result.timestamp).toLocaleString('de-DE')} +

+
+
+ + {/* Stats */} +
+ {result.errors.length > 0 && ( +
+ + {result.errors.length} +
+ )} + {result.warnings.length > 0 && ( +
+ + {result.warnings.length} +
+ )} + {result.suggestions.length > 0 && ( +
+ + {result.suggestions.length} +
+ )} + + +
+
+
+ + {/* Errors */} + {result.errors.length > 0 && ( +
+

+ Fehler ({result.errors.length}) +

+
+ {result.errors.map((f) => )} +
+
+ )} + + {/* Warnings */} + {result.warnings.length > 0 && ( +
+

+ Warnungen ({result.warnings.length}) +

+
+ {result.warnings.map((f) => )} +
+
+ )} + + {/* Suggestions */} + {result.suggestions.length > 0 && ( +
+

+ Vorschlaege ({result.suggestions.length}) +

+
+ {result.suggestions.map((f) => )} +
+
+ )} +
+ ) +} diff --git a/admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts b/admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts new file mode 100644 index 0000000..929c40c --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts @@ -0,0 +1,224 @@ +import { ConstraintEnforcer } from '../constraint-enforcer' +import type { ScopeDecision } from '../../compliance-scope-types' + +describe('ConstraintEnforcer', () => { + const enforcer = new ConstraintEnforcer() + + // Helper: minimal valid ScopeDecision + function makeDecision(overrides: Partial = {}): ScopeDecision { + return { + id: 'test-decision', + determinedLevel: 'L2', + scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 }, + triggeredHardTriggers: [], + requiredDocuments: [ + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] }, + { documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '3h', triggeredBy: [] }, + { documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] }, + ], + riskFlags: [], + gaps: [], + nextActions: [], + reasoning: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, + } as ScopeDecision + } + + describe('check - no decision', () => { + it('should allow basic documents (vvt, tom, dsi) without decision', () => { + const result = enforcer.check('vvt', null) + expect(result.allowed).toBe(true) + expect(result.adjustments.length).toBeGreaterThan(0) + expect(result.checkedRules).toContain('RULE-NO-DECISION') + }) + + it('should allow tom without decision', () => { + const result = enforcer.check('tom', null) + expect(result.allowed).toBe(true) + }) + + it('should allow dsi without decision', () => { + const result = enforcer.check('dsi', null) + expect(result.allowed).toBe(true) + }) + + it('should block non-basic documents without decision', () => { + const result = enforcer.check('dsfa', null) + expect(result.allowed).toBe(false) + expect(result.violations.length).toBeGreaterThan(0) + }) + + it('should block av_vertrag without decision', () => { + const result = enforcer.check('av_vertrag', null) + expect(result.allowed).toBe(false) + }) + }) + + describe('check - RULE-DOC-REQUIRED', () => { + it('should allow required documents', () => { + const decision = makeDecision() + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + }) + + it('should warn but allow optional documents', () => { + const decision = makeDecision({ + requiredDocuments: [ + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] }, + ], + }) + const result = enforcer.check('dsfa', decision) + expect(result.allowed).toBe(true) // Only warns, does not block + expect(result.adjustments.some(a => a.includes('nicht als Pflicht'))).toBe(true) + }) + }) + + describe('check - RULE-DEPTH-MATCH', () => { + it('should block when requested depth exceeds determined level', () => { + const decision = makeDecision({ determinedLevel: 'L2' }) + const result = enforcer.check('vvt', decision, 'L4') + expect(result.allowed).toBe(false) + expect(result.violations.some(v => v.includes('ueberschreitet'))).toBe(true) + }) + + it('should allow when requested depth matches level', () => { + const decision = makeDecision({ determinedLevel: 'L2' }) + const result = enforcer.check('vvt', decision, 'L2') + expect(result.allowed).toBe(true) + }) + + it('should adjust when requested depth is below level', () => { + const decision = makeDecision({ determinedLevel: 'L3' }) + const result = enforcer.check('vvt', decision, 'L1') + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('angehoben'))).toBe(true) + }) + + it('should allow without requested depth level', () => { + const decision = makeDecision({ determinedLevel: 'L3' }) + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + }) + }) + + describe('check - RULE-DSFA-ENFORCEMENT', () => { + it('should note when DSFA is not required but requested', () => { + const decision = makeDecision({ determinedLevel: 'L2' }) + const result = enforcer.check('dsfa', decision) + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('nicht verpflichtend'))).toBe(true) + }) + + it('should allow DSFA when hard triggers require it', () => { + const decision = makeDecision({ + determinedLevel: 'L3', + triggeredHardTriggers: [{ + rule: { + id: 'HT-ART9', + label: 'Art. 9 Daten', + description: '', + conditionField: '', + conditionOperator: 'EQUALS' as const, + conditionValue: null, + minimumLevel: 'L3', + mandatoryDocuments: ['dsfa'], + dsfaRequired: true, + legalReference: 'Art. 35 DSGVO', + }, + matchedValue: null, + explanation: 'Art. 9 Daten verarbeitet', + }], + }) + const result = enforcer.check('dsfa', decision) + expect(result.allowed).toBe(true) + }) + + it('should warn about DSFA when drafting non-DSFA but DSFA is required', () => { + const decision = makeDecision({ + determinedLevel: 'L3', + triggeredHardTriggers: [{ + rule: { + id: 'HT-ART9', + label: 'Art. 9 Daten', + description: '', + conditionField: '', + conditionOperator: 'EQUALS' as const, + conditionValue: null, + minimumLevel: 'L3', + mandatoryDocuments: ['dsfa'], + dsfaRequired: true, + legalReference: 'Art. 35 DSGVO', + }, + matchedValue: null, + explanation: '', + }], + requiredDocuments: [ + { documentType: 'dsfa', label: 'DSFA', required: true, depth: 'Vollstaendig', detailItems: [], estimatedEffort: '8h', triggeredBy: [] }, + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] }, + ], + }) + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('DSFA') && a.includes('verpflichtend'))).toBe(true) + }) + }) + + describe('check - RULE-RISK-FLAGS', () => { + it('should note critical risk flags', () => { + const decision = makeDecision({ + riskFlags: [ + { id: 'rf-1', severity: 'CRITICAL', title: 'Offene Art. 9 Verarbeitung', description: '', recommendation: 'DSFA durchfuehren' }, + { id: 'rf-2', severity: 'HIGH', title: 'Fehlende Verschluesselung', description: '', recommendation: 'TOM erstellen' }, + { id: 'rf-3', severity: 'LOW', title: 'Dokumentation unvollstaendig', description: '', recommendation: '' }, + ], + }) + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('2 kritische/hohe Risiko-Flags'))).toBe(true) + }) + + it('should not flag when no risk flags present', () => { + const decision = makeDecision({ riskFlags: [] }) + const result = enforcer.check('vvt', decision) + expect(result.adjustments.every(a => !a.includes('Risiko-Flags'))).toBe(true) + }) + }) + + describe('check - checkedRules tracking', () => { + it('should track all checked rules', () => { + const decision = makeDecision() + const result = enforcer.check('vvt', decision) + expect(result.checkedRules).toContain('RULE-DOC-REQUIRED') + expect(result.checkedRules).toContain('RULE-DEPTH-MATCH') + expect(result.checkedRules).toContain('RULE-DSFA-ENFORCEMENT') + expect(result.checkedRules).toContain('RULE-RISK-FLAGS') + expect(result.checkedRules).toContain('RULE-HARD-TRIGGER-CONSISTENCY') + }) + }) + + describe('checkFromContext', () => { + it('should reconstruct decision from DraftContext and check', () => { + const context = { + decisions: { + level: 'L2' as const, + scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 }, + hardTriggers: [], + requiredDocuments: [ + { documentType: 'vvt' as const, depth: 'Standard', detailItems: [] }, + ], + }, + companyProfile: { name: 'Test GmbH', industry: 'IT', employeeCount: 50, businessModel: 'SaaS', isPublicSector: false }, + constraints: { + depthRequirements: { required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h' }, + riskFlags: [], + boundaries: [], + }, + } + const result = enforcer.checkFromContext('vvt', context) + expect(result.allowed).toBe(true) + expect(result.checkedRules.length).toBeGreaterThan(0) + }) + }) +}) diff --git a/admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts b/admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts new file mode 100644 index 0000000..521ea1c --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts @@ -0,0 +1,153 @@ +import { IntentClassifier } from '../intent-classifier' + +describe('IntentClassifier', () => { + const classifier = new IntentClassifier() + + describe('classify - Draft mode', () => { + it.each([ + ['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'], + ['Generiere eine TOM-Dokumentation', 'draft'], + ['Schreibe eine Datenschutzerklaerung', 'draft'], + ['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'], + ['Create a DSFA document', 'draft'], + ['Draft a privacy policy for us', 'draft'], + ['Neues VVT anlegen', 'draft'], + ])('"%s" should classify as %s', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + expect(result.confidence).toBeGreaterThan(0.7) + }) + }) + + describe('classify - Validate mode', () => { + it.each([ + ['Pruefe die Konsistenz meiner Dokumente', 'validate'], + ['Ist mein VVT korrekt?', 'validate'], + ['Validiere die TOM gegen das VVT', 'validate'], + ['Check die Vollstaendigkeit', 'validate'], + ['Stimmt das mit der DSFA ueberein?', 'validate'], + ['Cross-Check VVT und TOM', 'validate'], + ])('"%s" should classify as %s', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + expect(result.confidence).toBeGreaterThan(0.7) + }) + }) + + describe('classify - Ask mode', () => { + it.each([ + ['Was fehlt noch in meinem Profil?', 'ask'], + ['Zeige mir die Luecken', 'ask'], + ['Welche Dokumente fehlen noch?', 'ask'], + ['Was ist der naechste Schritt?', 'ask'], + ['Welche Informationen brauche ich noch?', 'ask'], + ])('"%s" should classify as %s', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + expect(result.confidence).toBeGreaterThan(0.6) + }) + }) + + describe('classify - Explain mode (fallback)', () => { + it.each([ + ['Was ist DSGVO?', 'explain'], + ['Erklaere mir Art. 30', 'explain'], + ['Hallo', 'explain'], + ['Danke fuer die Hilfe', 'explain'], + ])('"%s" should classify as %s (fallback)', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + }) + }) + + describe('classify - confidence thresholds', () => { + it('should have high confidence for clear draft intents', () => { + const result = classifier.classify('Erstelle ein neues VVT') + expect(result.confidence).toBeGreaterThanOrEqual(0.85) + }) + + it('should have lower confidence for ambiguous inputs', () => { + const result = classifier.classify('Hallo') + expect(result.confidence).toBeLessThan(0.6) + }) + + it('should boost confidence with document type detection', () => { + const withDoc = classifier.classify('Erstelle VVT') + const withoutDoc = classifier.classify('Erstelle etwas') + expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence) + }) + + it('should boost confidence with multiple pattern matches', () => { + const single = classifier.classify('Erstelle Dokument') + const multi = classifier.classify('Erstelle und generiere ein neues Dokument') + expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence) + }) + }) + + describe('detectDocumentType', () => { + it.each([ + ['VVT erstellen', 'vvt'], + ['Verarbeitungsverzeichnis', 'vvt'], + ['Art. 30 Dokumentation', 'vvt'], + ['TOM definieren', 'tom'], + ['technisch organisatorische Massnahmen', 'tom'], + ['Art. 32 Massnahmen', 'tom'], + ['DSFA durchfuehren', 'dsfa'], + ['Datenschutz-Folgenabschaetzung', 'dsfa'], + ['Art. 35 Pruefung', 'dsfa'], + ['DPIA erstellen', 'dsfa'], + ['Datenschutzerklaerung', 'dsi'], + ['Privacy Policy', 'dsi'], + ['Art. 13 Information', 'dsi'], + ['Loeschfristen definieren', 'lf'], + ['Loeschkonzept erstellen', 'lf'], + ['Retention Policy', 'lf'], + ['Auftragsverarbeitung', 'av_vertrag'], + ['AVV erstellen', 'av_vertrag'], + ['Art. 28 Vertrag', 'av_vertrag'], + ['Einwilligung einholen', 'einwilligung'], + ['Consent Management', 'einwilligung'], + ['Cookie Banner', 'einwilligung'], + ])('"%s" should detect document type %s', (input, expectedType) => { + const result = classifier.detectDocumentType(input) + expect(result).toBe(expectedType) + }) + + it('should return undefined for unrecognized types', () => { + expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined() + expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined() + }) + }) + + describe('classify - Umlaut handling', () => { + it('should handle German umlauts correctly', () => { + // With actual umlauts (ä, ö, ü) + const result1 = classifier.classify('Prüfe die Vollständigkeit') + expect(result1.mode).toBe('validate') + + // With ae/oe/ue substitution + const result2 = classifier.classify('Pruefe die Vollstaendigkeit') + expect(result2.mode).toBe('validate') + }) + + it('should handle ß correctly', () => { + const result = classifier.classify('Schließe Lücken') + // Should still detect via normalized patterns + expect(result).toBeDefined() + }) + }) + + describe('classify - combined mode + document type', () => { + it('should detect both mode and document type', () => { + const result = classifier.classify('Erstelle ein VVT fuer unsere Firma') + expect(result.mode).toBe('draft') + expect(result.detectedDocumentType).toBe('vvt') + }) + + it('should detect validate + document type', () => { + const result = classifier.classify('Pruefe mein TOM auf Konsistenz') + expect(result.mode).toBe('validate') + expect(result.detectedDocumentType).toBe('tom') + }) + }) +}) diff --git a/admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts b/admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts new file mode 100644 index 0000000..20425f6 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts @@ -0,0 +1,311 @@ +import { StateProjector } from '../state-projector' +import type { SDKState } from '../../types' + +describe('StateProjector', () => { + const projector = new StateProjector() + + // Helper: minimal SDKState + function makeState(overrides: Partial = {}): SDKState { + return { + version: '1.0.0', + lastModified: new Date(), + tenantId: 'test', + userId: 'user1', + subscription: 'PROFESSIONAL', + customerType: null, + companyProfile: null, + complianceScope: null, + currentPhase: 1, + currentStep: 'company-profile', + completedSteps: [], + checkpoints: {}, + importedDocuments: [], + gapAnalysis: null, + useCases: [], + activeUseCase: null, + screening: null, + modules: [], + requirements: [], + controls: [], + evidence: [], + checklist: [], + risks: [], + aiActClassification: null, + obligations: [], + dsfa: null, + toms: [], + retentionPolicies: [], + vvt: [], + documents: [], + cookieBanner: null, + consents: [], + dsrConfig: null, + escalationWorkflows: [], + preferences: { + language: 'de', + theme: 'light', + compactMode: false, + showHints: true, + autoSave: true, + autoValidate: true, + allowParallelWork: true, + }, + ...overrides, + } as SDKState + } + + function makeDecisionState(level: string = 'L2'): SDKState { + return makeState({ + companyProfile: { + companyName: 'Test GmbH', + industry: 'IT-Dienstleistung', + employeeCount: 50, + businessModel: 'SaaS', + isPublicSector: false, + } as any, + complianceScope: { + decision: { + id: 'dec-1', + determinedLevel: level, + scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 }, + triggeredHardTriggers: [], + requiredDocuments: [ + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] }, + { documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] }, + { documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] }, + ], + riskFlags: [ + { id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' }, + ], + gaps: [ + { id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] }, + ], + nextActions: [], + reasoning: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + answers: [], + } as any, + vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[], + toms: [], + retentionPolicies: [], + }) + } + + describe('projectForDraft', () => { + it('should return a DraftContext with correct structure', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result).toHaveProperty('decisions') + expect(result).toHaveProperty('companyProfile') + expect(result).toHaveProperty('constraints') + expect(result.decisions.level).toBe('L2') + }) + + it('should project company profile', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.companyProfile.name).toBe('Test GmbH') + expect(result.companyProfile.industry).toBe('IT-Dienstleistung') + expect(result.companyProfile.employeeCount).toBe(50) + }) + + it('should provide defaults when no company profile', () => { + const state = makeState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.companyProfile.name).toBe('Unbekannt') + expect(result.companyProfile.industry).toBe('Unbekannt') + expect(result.companyProfile.employeeCount).toBe(0) + }) + + it('should extract constraints and depth requirements', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.constraints.depthRequirements).toBeDefined() + expect(result.constraints.boundaries.length).toBeGreaterThan(0) + }) + + it('should extract risk flags', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.constraints.riskFlags.length).toBe(1) + expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung') + }) + + it('should include existing document data when available', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.existingDocumentData).toBeDefined() + expect((result.existingDocumentData as any).totalCount).toBe(1) + }) + + it('should return undefined existingDocumentData when none exists', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'tom') + + expect(result.existingDocumentData).toBeUndefined() + }) + + it('should filter required documents', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.decisions.requiredDocuments.length).toBe(3) + expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true) + }) + + it('should handle empty state gracefully', () => { + const state = makeState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.decisions.level).toBe('L1') + expect(result.decisions.hardTriggers).toEqual([]) + expect(result.decisions.requiredDocuments).toEqual([]) + }) + }) + + describe('projectForAsk', () => { + it('should return a GapContext with correct structure', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + + expect(result).toHaveProperty('unansweredQuestions') + expect(result).toHaveProperty('gaps') + expect(result).toHaveProperty('missingDocuments') + }) + + it('should identify missing documents', () => { + const state = makeDecisionState() + // vvt exists, tom and lf are missing + const result = projector.projectForAsk(state) + + expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true) + expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true) + }) + + it('should not list existing documents as missing', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + + // vvt exists in state + expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false) + }) + + it('should include gaps from scope decision', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + + expect(result.gaps.length).toBe(1) + expect(result.gaps[0].title).toBe('TOM fehlt') + }) + + it('should handle empty state', () => { + const state = makeState() + const result = projector.projectForAsk(state) + + expect(result.gaps).toEqual([]) + expect(result.missingDocuments).toEqual([]) + }) + }) + + describe('projectForValidate', () => { + it('should return a ValidationContext with correct structure', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + + expect(result).toHaveProperty('documents') + expect(result).toHaveProperty('crossReferences') + expect(result).toHaveProperty('scopeLevel') + expect(result).toHaveProperty('depthRequirements') + }) + + it('should include all requested document types', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom']) + + expect(result.documents.length).toBe(2) + expect(result.documents.map(d => d.type)).toContain('vvt') + expect(result.documents.map(d => d.type)).toContain('tom') + }) + + it('should include cross-references', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + + expect(result.crossReferences).toHaveProperty('vvtCategories') + expect(result.crossReferences).toHaveProperty('tomControls') + expect(result.crossReferences).toHaveProperty('retentionCategories') + expect(result.crossReferences.vvtCategories.length).toBe(1) + expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung') + }) + + it('should include scope level', () => { + const state = makeDecisionState('L3') + const result = projector.projectForValidate(state, ['vvt']) + + expect(result.scopeLevel).toBe('L3') + }) + + it('should include depth requirements per document type', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom']) + + expect(result.depthRequirements).toHaveProperty('vvt') + expect(result.depthRequirements).toHaveProperty('tom') + }) + + it('should summarize documents', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom']) + + expect(result.documents[0].contentSummary).toContain('1') + expect(result.documents[1].contentSummary).toContain('Keine TOM') + }) + + it('should handle empty state', () => { + const state = makeState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + + expect(result.scopeLevel).toBe('L1') + expect(result.crossReferences.vvtCategories).toEqual([]) + expect(result.crossReferences.tomControls).toEqual([]) + }) + }) + + describe('token budget estimation', () => { + it('projectForDraft should produce compact output', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + const json = JSON.stringify(result) + + // Rough token estimation: ~4 chars per token + const estimatedTokens = json.length / 4 + expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500 + }) + + it('projectForAsk should produce very compact output', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + const json = JSON.stringify(result) + + const estimatedTokens = json.length / 4 + expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600 + }) + + it('projectForValidate should stay within budget', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + const json = JSON.stringify(result) + + const estimatedTokens = json.length / 4 + expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000 + }) + }) +}) diff --git a/admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts b/admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts new file mode 100644 index 0000000..dace188 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts @@ -0,0 +1,221 @@ +/** + * Constraint Enforcer - Hard Gate vor jedem Draft + * + * Stellt sicher, dass die Drafting Engine NIEMALS die deterministische + * Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang: + * + * 1. Ist der Dokumenttyp in requiredDocuments? + * 2. Passt die Draft-Tiefe zum Level? + * 3. Ist eine DSFA erforderlich (Hard Trigger)? + * 4. Werden Risiko-Flags beruecksichtigt? + */ + +import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types' +import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types' +import type { ConstraintCheckResult, DraftContext } from './types' + +export class ConstraintEnforcer { + + /** + * Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist. + * Dies ist ein HARD GATE - bei Violation wird der Draft blockiert. + */ + check( + documentType: ScopeDocumentType, + decision: ScopeDecision | null, + requestedDepthLevel?: ComplianceDepthLevel + ): ConstraintCheckResult { + const violations: string[] = [] + const adjustments: string[] = [] + const checkedRules: string[] = [] + + // Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben + if (!decision) { + checkedRules.push('RULE-NO-DECISION') + if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') { + violations.push( + 'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.' + ) + } else { + adjustments.push( + 'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.' + ) + } + return { + allowed: violations.length === 0, + violations, + adjustments, + checkedRules, + } + } + + const level = decision.determinedLevel + const levelNumeric = getDepthLevelNumeric(level) + + // ----------------------------------------------------------------------- + // Rule 1: Dokumenttyp in requiredDocuments? + // ----------------------------------------------------------------------- + checkedRules.push('RULE-DOC-REQUIRED') + const isRequired = decision.requiredDocuments.some( + d => d.documentType === documentType && d.required + ) + const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level] + + if (!isRequired && scopeReq && !scopeReq.required) { + // Nicht blockieren, aber warnen + adjustments.push( + `Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` + + `Entwurf ist moeglich, aber optional.` + ) + } + + // ----------------------------------------------------------------------- + // Rule 2: Draft-Tiefe passt zum Level? + // ----------------------------------------------------------------------- + checkedRules.push('RULE-DEPTH-MATCH') + if (requestedDepthLevel) { + const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel) + + if (requestedNumeric > levelNumeric) { + violations.push( + `Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` + + `Die Scope-Engine hat Level ${level} festgelegt. ` + + `Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.` + ) + } else if (requestedNumeric < levelNumeric) { + adjustments.push( + `Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` + + `Draft wird auf Level ${level} angehoben.` + ) + } + } + + // ----------------------------------------------------------------------- + // Rule 3: DSFA-Enforcement + // ----------------------------------------------------------------------- + checkedRules.push('RULE-DSFA-ENFORCEMENT') + if (documentType === 'dsfa') { + const dsfaRequired = decision.triggeredHardTriggers.some( + t => t.rule.dsfaRequired + ) + + if (!dsfaRequired && level !== 'L4') { + adjustments.push( + 'DSFA ist laut Scope-Engine nicht verpflichtend. ' + + 'Entwurf wird als freiwillige Massnahme gekennzeichnet.' + ) + } + } + + // Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen + if (documentType !== 'dsfa') { + const dsfaRequired = decision.triggeredHardTriggers.some( + t => t.rule.dsfaRequired + ) + const dsfaInRequired = decision.requiredDocuments.some( + d => d.documentType === 'dsfa' && d.required + ) + + if (dsfaRequired && dsfaInRequired) { + // Nur ein Hinweis, kein Block + adjustments.push( + 'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' + + 'Bitte sicherstellen, dass auch eine DSFA erstellt wird.' + ) + } + } + + // ----------------------------------------------------------------------- + // Rule 4: Risiko-Flags beruecksichtigt? + // ----------------------------------------------------------------------- + checkedRules.push('RULE-RISK-FLAGS') + const criticalRisks = decision.riskFlags.filter( + f => f.severity === 'CRITICAL' || f.severity === 'HIGH' + ) + + if (criticalRisks.length > 0) { + adjustments.push( + `${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` + + `Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}` + ) + } + + // ----------------------------------------------------------------------- + // Rule 5: Hard-Trigger Consistency + // ----------------------------------------------------------------------- + checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY') + for (const trigger of decision.triggeredHardTriggers) { + const mandatoryDocs = trigger.rule.mandatoryDocuments + if (mandatoryDocs.includes(documentType)) { + // Gut - wir erstellen ein mandatory document + } else { + // Pruefen ob die mandatory documents des Triggers vorhanden sind + // (nur Hinweis, kein Block) + } + } + + return { + allowed: violations.length === 0, + violations, + adjustments, + checkedRules, + } + } + + /** + * Convenience: Prueft aus einem DraftContext heraus. + */ + checkFromContext( + documentType: ScopeDocumentType, + context: DraftContext + ): ConstraintCheckResult { + // Reconstruct a minimal ScopeDecision from context + const pseudoDecision: ScopeDecision = { + id: 'projected', + determinedLevel: context.decisions.level, + scores: context.decisions.scores, + triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({ + rule: { + id: t.id, + label: t.label, + description: '', + conditionField: '', + conditionOperator: 'EQUALS' as const, + conditionValue: null, + minimumLevel: context.decisions.level, + mandatoryDocuments: [], + dsfaRequired: false, + legalReference: t.legalReference, + }, + matchedValue: null, + explanation: '', + })), + requiredDocuments: context.decisions.requiredDocuments.map(d => ({ + documentType: d.documentType, + label: d.documentType, + required: true, + depth: d.depth, + detailItems: d.detailItems, + estimatedEffort: '', + triggeredBy: [], + })), + riskFlags: context.constraints.riskFlags.map(f => ({ + id: `rf-${f.title}`, + severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL', + title: f.title, + description: '', + recommendation: f.recommendation, + })), + gaps: [], + nextActions: [], + reasoning: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + + return this.check(documentType, pseudoDecision) + } +} + +/** Singleton-Instanz */ +export const constraintEnforcer = new ConstraintEnforcer() diff --git a/admin-v2/lib/sdk/drafting-engine/intent-classifier.ts b/admin-v2/lib/sdk/drafting-engine/intent-classifier.ts new file mode 100644 index 0000000..41be915 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/intent-classifier.ts @@ -0,0 +1,241 @@ +/** + * Intent Classifier - Leichtgewichtiger Pattern-Matcher + * + * Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call. + * Deutsche und englische Muster werden unterstuetzt. + * + * Confidence-Schwellen: + * - >0.8: Hohe Sicherheit, automatisch anwenden + * - 0.6-0.8: Mittel, Nutzer kann bestaetigen + * - <0.6: Fallback zu 'explain' + */ + +import type { AgentMode, IntentClassification } from './types' +import type { ScopeDocumentType } from '../compliance-scope-types' + +// ============================================================================ +// Pattern Definitions +// ============================================================================ + +interface ModePattern { + mode: AgentMode + patterns: RegExp[] + /** Base-Confidence wenn ein Pattern matched */ + baseConfidence: number +} + +const MODE_PATTERNS: ModePattern[] = [ + { + mode: 'draft', + baseConfidence: 0.85, + patterns: [ + /\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i, + /\b(draft|create|generate|write|compose)\b/i, + /\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i, + /\b(vorlage|template)\s+(erstell|generier)/i, + /\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i, + ], + }, + { + mode: 'validate', + baseConfidence: 0.80, + patterns: [ + /\b(pruef|validier|check|kontrollier|ueberpruef)\b/i, + /\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i, + /\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i, + /\b(validate|verify|check|review)\b/i, + /\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i, + /\bcross[\s-]?check\b/i, + /\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i, + ], + }, + { + mode: 'ask', + baseConfidence: 0.75, + patterns: [ + /\bwas\s+fehlt\b/i, + /\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i, + /\b(unvollstaendig|unfertig|offen)\b/i, + /\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i, + /\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i, + /\bworan\s+(muss|soll)\b/i, + ], + }, +] + +/** Dokumenttyp-Erkennung */ +const DOCUMENT_TYPE_PATTERNS: Array<{ + type: ScopeDocumentType + patterns: RegExp[] +}> = [ + { + type: 'vvt', + patterns: [ + /\bv{1,2}t\b/i, + /\bverarbeitungsverzeichnis\b/i, + /\bverarbeitungstaetigkeit/i, + /\bprocessing\s+activit/i, + /\bart\.?\s*30\b/i, + ], + }, + { + type: 'tom', + patterns: [ + /\btom\b/i, + /\btechnisch.*organisatorisch.*massnahm/i, + /\bart\.?\s*32\b/i, + /\bsicherheitsmassnahm/i, + ], + }, + { + type: 'dsfa', + patterns: [ + /\bdsfa\b/i, + /\bdatenschutz[\s-]?folgenabschaetzung\b/i, + /\bdpia\b/i, + /\bart\.?\s*35\b/i, + /\bimpact\s+assessment\b/i, + ], + }, + { + type: 'dsi', + patterns: [ + /\bdatenschutzerklaerung\b/i, + /\bprivacy\s+policy\b/i, + /\bdsi\b/i, + /\bart\.?\s*13\b/i, + /\bart\.?\s*14\b/i, + ], + }, + { + type: 'lf', + patterns: [ + /\bloeschfrist/i, + /\bloeschkonzept/i, + /\bretention/i, + /\baufbewahr/i, + ], + }, + { + type: 'av_vertrag', + patterns: [ + /\bavv?\b/i, + /\bauftragsverarbeit/i, + /\bdata\s+processing\s+agreement/i, + /\bart\.?\s*28\b/i, + ], + }, + { + type: 'betroffenenrechte', + patterns: [ + /\bbetroffenenrecht/i, + /\bdata\s+subject\s+right/i, + /\bart\.?\s*15\b/i, + /\bauskunft/i, + ], + }, + { + type: 'einwilligung', + patterns: [ + /\beinwillig/i, + /\bconsent/i, + /\bcookie/i, + ], + }, +] + +// ============================================================================ +// Classifier +// ============================================================================ + +export class IntentClassifier { + + /** + * Klassifiziert die Nutzerabsicht anhand des Inputs. + * + * @param input - Die Nutzer-Nachricht + * @returns IntentClassification mit Mode, Confidence, Patterns + */ + classify(input: string): IntentClassification { + const normalized = this.normalize(input) + let bestMatch: IntentClassification = { + mode: 'explain', + confidence: 0.3, + matchedPatterns: [], + } + + for (const modePattern of MODE_PATTERNS) { + const matched: string[] = [] + + for (const pattern of modePattern.patterns) { + if (pattern.test(normalized)) { + matched.push(pattern.source) + } + } + + if (matched.length > 0) { + // Mehr Matches = hoehere Confidence (bis zum Maximum) + const matchBonus = Math.min(matched.length - 1, 2) * 0.05 + const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99) + + if (confidence > bestMatch.confidence) { + bestMatch = { + mode: modePattern.mode, + confidence, + matchedPatterns: matched, + } + } + } + } + + // Dokumenttyp erkennen + const detectedDocType = this.detectDocumentType(normalized) + if (detectedDocType) { + bestMatch.detectedDocumentType = detectedDocType + // Dokumenttyp-Erkennung erhoeht Confidence leicht + bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99) + } + + // Fallback: Bei Confidence <0.6 immer 'explain' + if (bestMatch.confidence < 0.6) { + bestMatch.mode = 'explain' + } + + return bestMatch + } + + /** + * Erkennt den Dokumenttyp aus dem Input. + */ + detectDocumentType(input: string): ScopeDocumentType | undefined { + const normalized = this.normalize(input) + + for (const docPattern of DOCUMENT_TYPE_PATTERNS) { + for (const pattern of docPattern.patterns) { + if (pattern.test(normalized)) { + return docPattern.type + } + } + } + + return undefined + } + + /** + * Normalisiert den Input fuer Pattern-Matching. + * Ersetzt Umlaute, entfernt Sonderzeichen. + */ + private normalize(input: string): string { + return input + .replace(/ä/g, 'ae') + .replace(/ö/g, 'oe') + .replace(/ü/g, 'ue') + .replace(/ß/g, 'ss') + .replace(/Ä/g, 'Ae') + .replace(/Ö/g, 'Oe') + .replace(/Ü/g, 'Ue') + } +} + +/** Singleton-Instanz */ +export const intentClassifier = new IntentClassifier() diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts b/admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts new file mode 100644 index 0000000..6a60543 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts @@ -0,0 +1,49 @@ +/** + * Gap Analysis Prompt - Lueckenanalyse und gezielte Fragen + */ + +import type { GapContext } from '../types' + +export interface GapAnalysisInput { + context: GapContext + instructions?: string +} + +export function buildGapAnalysisPrompt(input: GapAnalysisInput): string { + const { context, instructions } = input + + return `## Aufgabe: Compliance-Lueckenanalyse + +### Identifizierte Luecken: +${context.gaps.length > 0 + ? context.gaps.map(g => `- [${g.severity}] ${g.title}: ${g.description}`).join('\n') + : '- Keine Luecken identifiziert'} + +### Fehlende Pflichtdokumente: +${context.missingDocuments.length > 0 + ? context.missingDocuments.map(d => `- ${d.label} (Tiefe: ${d.depth}, Aufwand: ${d.estimatedEffort})`).join('\n') + : '- Alle Pflichtdokumente vorhanden'} + +### Unbeantwortete Fragen: +${context.unansweredQuestions.length > 0 + ? context.unansweredQuestions.map(q => `- [${q.blockId}] ${q.question}`).join('\n') + : '- Alle Fragen beantwortet'} + +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Aufgabe: +Analysiere den Stand und stelle EINE gezielte Frage, die die wichtigste Luecke adressiert. +Priorisiere nach: +1. Fehlende Pflichtdokumente +2. Kritische Luecken (HIGH/CRITICAL severity) +3. Unbeantwortete Pflichtfragen +4. Mittlere Luecken + +### Antwort-Format: +Antworte in dieser Struktur: +1. **Statusuebersicht**: Kurze Zusammenfassung des Compliance-Stands (2-3 Saetze) +2. **Wichtigste Luecke**: Was fehlt am dringendsten? +3. **Gezielte Frage**: Eine konkrete Frage an den Nutzer +4. **Warum wichtig**: Warum muss diese Luecke geschlossen werden? +5. **Empfohlener naechster Schritt**: Link/Verweis zum SDK-Modul` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts new file mode 100644 index 0000000..7e1e898 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts @@ -0,0 +1,91 @@ +/** + * DSFA Draft Prompt - Datenschutz-Folgenabschaetzung (Art. 35 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface DSFADraftInput { + context: DraftContext + processingDescription?: string + instructions?: string +} + +export function buildDSFADraftPrompt(input: DSFADraftInput): string { + const { context, processingDescription, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + const hardTriggers = context.decisions.hardTriggers + + return `## Aufgabe: DSFA entwerfen (Art. 35 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Hard Triggers (Gruende fuer DSFA-Pflicht): +${hardTriggers.length > 0 + ? hardTriggers.map(t => `- ${t.id}: ${t.label} (${t.legalReference})`).join('\n') + : '- Keine Hard Triggers (DSFA auf Wunsch)'} + +### Erforderliche Inhalte: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${processingDescription ? `### Beschreibung der Verarbeitung: ${processingDescription}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "beschreibung", + "title": "Systematische Beschreibung der Verarbeitung", + "content": "...", + "schemaField": "processingDescription" + }, + { + "id": "notwendigkeit", + "title": "Notwendigkeit und Verhaeltnismaessigkeit", + "content": "...", + "schemaField": "necessityAssessment" + }, + { + "id": "risikobewertung", + "title": "Bewertung der Risiken fuer die Rechte und Freiheiten", + "content": "...", + "schemaField": "riskAssessment" + }, + { + "id": "massnahmen", + "title": "Massnahmen zur Eindaemmung der Risiken", + "content": "...", + "schemaField": "mitigationMeasures" + }, + { + "id": "stellungnahme_dsb", + "title": "Stellungnahme des Datenschutzbeauftragten", + "content": "...", + "schemaField": "dpoOpinion" + }, + { + "id": "standpunkt_betroffene", + "title": "Standpunkt der betroffenen Personen", + "content": "...", + "schemaField": "dataSubjectView" + }, + { + "id": "ergebnis", + "title": "Ergebnis und Empfehlung", + "content": "...", + "schemaField": "conclusion" + } + ] +} + +Halte die Tiefe exakt auf Level ${level}. +Nutze WP248-Kriterien als Leitfaden fuer die Risikobewertung.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts new file mode 100644 index 0000000..ddf5d8e --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts @@ -0,0 +1,78 @@ +/** + * Loeschfristen Draft Prompt - Loeschkonzept + */ + +import type { DraftContext } from '../types' + +export interface LoeschfristenDraftInput { + context: DraftContext + instructions?: string +} + +export function buildLoeschfristenDraftPrompt(input: LoeschfristenDraftInput): string { + const { context, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: Loeschkonzept / Loeschfristen entwerfen + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${context.existingDocumentData ? `### Bestehende Loeschfristen: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "grundsaetze", + "title": "Grundsaetze der Datenlöschung", + "content": "...", + "schemaField": "principles" + }, + { + "id": "kategorien", + "title": "Datenkategorien und Loeschfristen", + "content": "Tabellarische Uebersicht...", + "schemaField": "retentionSchedule" + }, + { + "id": "gesetzliche_fristen", + "title": "Gesetzliche Aufbewahrungsfristen", + "content": "HGB, AO, weitere...", + "schemaField": "legalRetention" + }, + { + "id": "loeschprozess", + "title": "Technischer Loeschprozess", + "content": "...", + "schemaField": "deletionProcess" + }, + { + "id": "verantwortlichkeiten", + "title": "Verantwortlichkeiten", + "content": "...", + "schemaField": "responsibilities" + }, + { + "id": "ausnahmen", + "title": "Ausnahmen und Sonderfaelle", + "content": "...", + "schemaField": "exceptions" + } + ] +} + +Halte die Tiefe exakt auf Level ${level}. +Beruecksichtige branchenspezifische Aufbewahrungsfristen fuer ${context.companyProfile.industry}.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts new file mode 100644 index 0000000..a417d42 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts @@ -0,0 +1,102 @@ +/** + * Privacy Policy Draft Prompt - Datenschutzerklaerung (Art. 13/14 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface PrivacyPolicyDraftInput { + context: DraftContext + websiteUrl?: string + instructions?: string +} + +export function buildPrivacyPolicyDraftPrompt(input: PrivacyPolicyDraftInput): string { + const { context, websiteUrl, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: Datenschutzerklaerung entwerfen (Art. 13/14 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : ''} +${websiteUrl ? `- Website: ${websiteUrl}` : ''} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "verantwortlicher", + "title": "Verantwortlicher", + "content": "...", + "schemaField": "controller" + }, + { + "id": "dsb", + "title": "Datenschutzbeauftragter", + "content": "...", + "schemaField": "dpo" + }, + { + "id": "verarbeitungen", + "title": "Verarbeitungstaetigkeiten und Zwecke", + "content": "...", + "schemaField": "processingPurposes" + }, + { + "id": "rechtsgrundlagen", + "title": "Rechtsgrundlagen der Verarbeitung", + "content": "...", + "schemaField": "legalBases" + }, + { + "id": "empfaenger", + "title": "Empfaenger und Datenweitergabe", + "content": "...", + "schemaField": "recipients" + }, + { + "id": "drittland", + "title": "Uebermittlung in Drittlaender", + "content": "...", + "schemaField": "thirdCountryTransfers" + }, + { + "id": "speicherdauer", + "title": "Speicherdauer", + "content": "...", + "schemaField": "retentionPeriods" + }, + { + "id": "betroffenenrechte", + "title": "Ihre Rechte als betroffene Person", + "content": "...", + "schemaField": "dataSubjectRights" + }, + { + "id": "cookies", + "title": "Cookies und Tracking", + "content": "...", + "schemaField": "cookies" + }, + { + "id": "aenderungen", + "title": "Aenderungen dieser Datenschutzerklaerung", + "content": "...", + "schemaField": "changes" + } + ] +} + +Halte die Tiefe exakt auf Level ${level}.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts new file mode 100644 index 0000000..5f97d52 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts @@ -0,0 +1,99 @@ +/** + * TOM Draft Prompt - Technische und Organisatorische Massnahmen (Art. 32 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface TOMDraftInput { + context: DraftContext + focusArea?: string + instructions?: string +} + +export function buildTOMDraftPrompt(input: TOMDraftInput): string { + const { context, focusArea, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: TOM-Dokument entwerfen (Art. 32 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte fuer Level ${level}: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +### Constraints +${context.constraints.boundaries.map(b => `- ${b}`).join('\n')} + +${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags +${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}`).join('\n')}` : ''} + +${focusArea ? `### Fokusbereich: ${focusArea}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +${context.existingDocumentData ? `### Bestehende TOM: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "zutrittskontrolle", + "title": "Zutrittskontrolle", + "content": "Massnahmen die unbefugten Zutritt zu Datenverarbeitungsanlagen verhindern...", + "schemaField": "accessControl" + }, + { + "id": "zugangskontrolle", + "title": "Zugangskontrolle", + "content": "Massnahmen gegen unbefugte Systemnutzung...", + "schemaField": "systemAccessControl" + }, + { + "id": "zugriffskontrolle", + "title": "Zugriffskontrolle", + "content": "Massnahmen zur Sicherstellung berechtigter Datenzugriffe...", + "schemaField": "dataAccessControl" + }, + { + "id": "weitergabekontrolle", + "title": "Weitergabekontrolle / Uebertragungssicherheit", + "content": "Massnahmen bei Datenuebertragung und -transport...", + "schemaField": "transferControl" + }, + { + "id": "eingabekontrolle", + "title": "Eingabekontrolle", + "content": "Nachvollziehbarkeit von Dateneingaben...", + "schemaField": "inputControl" + }, + { + "id": "auftragskontrolle", + "title": "Auftragskontrolle", + "content": "Massnahmen zur weisungsgemaessen Auftragsverarbeitung...", + "schemaField": "orderControl" + }, + { + "id": "verfuegbarkeitskontrolle", + "title": "Verfuegbarkeitskontrolle", + "content": "Schutz gegen Datenverlust...", + "schemaField": "availabilityControl" + }, + { + "id": "trennungsgebot", + "title": "Trennungsgebot", + "content": "Getrennte Verarbeitung fuer verschiedene Zwecke...", + "schemaField": "separationControl" + } + ] +} + +Fuelle fehlende Informationen mit [PLATZHALTER: ...]. +Halte die Tiefe exakt auf Level ${level}.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts new file mode 100644 index 0000000..bf1aa75 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts @@ -0,0 +1,109 @@ +/** + * VVT Draft Prompt - Verarbeitungsverzeichnis (Art. 30 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface VVTDraftInput { + context: DraftContext + activityName?: string + activityPurpose?: string + instructions?: string +} + +export function buildVVTDraftPrompt(input: VVTDraftInput): string { + const { context, activityName, activityPurpose, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: VVT-Eintrag entwerfen (Art. 30 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} +- Geschaeftsmodell: ${context.companyProfile.businessModel} +${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : '- DSB: Nicht benannt'} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte fuer Level ${level}: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +### Constraints +${context.constraints.boundaries.map(b => `- ${b}`).join('\n')} + +${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags +${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}: ${f.recommendation}`).join('\n')}` : ''} + +${activityName ? `### Gewuenschte Verarbeitungstaetigkeit: ${activityName}` : ''} +${activityPurpose ? `### Zweck: ${activityPurpose}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +${context.existingDocumentData ? `### Bestehende VVT-Eintraege: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "bezeichnung", + "title": "Bezeichnung der Verarbeitungstaetigkeit", + "content": "...", + "schemaField": "name" + }, + { + "id": "verantwortlicher", + "title": "Verantwortlicher", + "content": "...", + "schemaField": "controller" + }, + { + "id": "zweck", + "title": "Zweck der Verarbeitung", + "content": "...", + "schemaField": "purpose" + }, + { + "id": "rechtsgrundlage", + "title": "Rechtsgrundlage", + "content": "...", + "schemaField": "legalBasis" + }, + { + "id": "betroffene", + "title": "Kategorien betroffener Personen", + "content": "...", + "schemaField": "dataSubjects" + }, + { + "id": "datenkategorien", + "title": "Kategorien personenbezogener Daten", + "content": "...", + "schemaField": "dataCategories" + }, + { + "id": "empfaenger", + "title": "Empfaenger", + "content": "...", + "schemaField": "recipients" + }, + { + "id": "speicherdauer", + "title": "Speicherdauer / Loeschfristen", + "content": "...", + "schemaField": "retentionPeriod" + }, + { + "id": "tom_referenz", + "title": "TOM-Referenz", + "content": "...", + "schemaField": "tomReference" + } + ] +} + +Fuelle fehlende Informationen mit [PLATZHALTER: Beschreibung was hier eingetragen werden muss]. +Halte die Tiefe exakt auf Level ${level} (${context.constraints.depthRequirements.depth}).` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/index.ts b/admin-v2/lib/sdk/drafting-engine/prompts/index.ts new file mode 100644 index 0000000..dde2726 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/index.ts @@ -0,0 +1,11 @@ +/** + * Drafting Engine Prompts - Re-Exports + */ + +export { buildVVTDraftPrompt, type VVTDraftInput } from './draft-vvt' +export { buildTOMDraftPrompt, type TOMDraftInput } from './draft-tom' +export { buildDSFADraftPrompt, type DSFADraftInput } from './draft-dsfa' +export { buildPrivacyPolicyDraftPrompt, type PrivacyPolicyDraftInput } from './draft-privacy-policy' +export { buildLoeschfristenDraftPrompt, type LoeschfristenDraftInput } from './draft-loeschfristen' +export { buildCrossCheckPrompt, type CrossCheckInput } from './validate-cross-check' +export { buildGapAnalysisPrompt, type GapAnalysisInput } from './ask-gap-analysis' diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts b/admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts new file mode 100644 index 0000000..825ae60 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts @@ -0,0 +1,66 @@ +/** + * Cross-Document Validation Prompt + */ + +import type { ValidationContext } from '../types' + +export interface CrossCheckInput { + context: ValidationContext + focusDocuments?: string[] + instructions?: string +} + +export function buildCrossCheckPrompt(input: CrossCheckInput): string { + const { context, focusDocuments, instructions } = input + + return `## Aufgabe: Cross-Dokument-Konsistenzpruefung + +### Scope-Level: ${context.scopeLevel} + +### Vorhandene Dokumente: +${context.documents.map(d => `- ${d.type}: ${d.contentSummary}`).join('\n')} + +### Cross-Referenzen: +- VVT-Kategorien: ${context.crossReferences.vvtCategories.join(', ') || 'Keine'} +- DSFA-Risiken: ${context.crossReferences.dsfaRisks.join(', ') || 'Keine'} +- TOM-Controls: ${context.crossReferences.tomControls.join(', ') || 'Keine'} +- Loeschfristen-Kategorien: ${context.crossReferences.retentionCategories.join(', ') || 'Keine'} + +### Tiefenpruefung pro Dokument: +${context.documents.map(d => { + const req = context.depthRequirements[d.type] + return req ? `- ${d.type}: Erforderlich=${req.required}, Tiefe=${req.depth}` : `- ${d.type}: Keine Requirements` + }).join('\n')} + +${focusDocuments ? `### Fokus auf: ${focusDocuments.join(', ')}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Pruefkriterien: +1. Jede VVT-Taetigkeit muss einen TOM-Verweis haben +2. Jede VVT-Kategorie muss eine Loeschfrist haben +3. Bei DSFA-pflichtigen Verarbeitungen muss eine DSFA existieren +4. TOM-Massnahmen muessen zum Risikoprofil passen +5. Loeschfristen duerfen gesetzliche Minima nicht unterschreiten +6. Dokument-Tiefe muss Level ${context.scopeLevel} entsprechen + +### Antwort-Format +Antworte als JSON: +{ + "passed": true/false, + "errors": [ + { + "id": "ERR-001", + "severity": "error", + "category": "scope_violation|inconsistency|missing_content|depth_mismatch|cross_reference", + "title": "...", + "description": "...", + "documentType": "vvt|tom|dsfa|...", + "crossReferenceType": "...", + "legalReference": "Art. ... DSGVO", + "suggestion": "..." + } + ], + "warnings": [...], + "suggestions": [...] +}` +} diff --git a/admin-v2/lib/sdk/drafting-engine/state-projector.ts b/admin-v2/lib/sdk/drafting-engine/state-projector.ts new file mode 100644 index 0000000..67e5866 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/state-projector.ts @@ -0,0 +1,337 @@ +/** + * State Projector - Token-budgetierte Projektion des SDK-State + * + * Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die + * relevanten Slices fuer den jeweiligen Agent-Modus. + * + * Token-Budgets: + * - Draft: ~1500 Tokens + * - Ask: ~600 Tokens + * - Validate: ~2000 Tokens + */ + +import type { SDKState, CompanyProfile } from '../types' +import type { + ComplianceScopeState, + ScopeDecision, + ScopeDocumentType, + ScopeGap, + RequiredDocument, + RiskFlag, + DOCUMENT_SCOPE_MATRIX, + DocumentDepthRequirement, +} from '../compliance-scope-types' +import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types' +import type { + DraftContext, + GapContext, + ValidationContext, +} from './types' + +// ============================================================================ +// State Projector +// ============================================================================ + +export class StateProjector { + + /** + * Projiziert den SDKState fuer Draft-Operationen. + * Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints. + * + * ~1500 Tokens + */ + projectForDraft( + state: SDKState, + documentType: ScopeDocumentType + ): DraftContext { + const decision = state.complianceScope?.decision ?? null + const level = decision?.determinedLevel ?? 'L1' + const depthReq = DOC_MATRIX[documentType]?.[level] ?? { + required: false, + depth: 'Basis', + detailItems: [], + estimatedEffort: 'N/A', + } + + return { + decisions: { + level, + scores: decision?.scores ?? { + risk_score: 0, + complexity_score: 0, + assurance_need: 0, + composite_score: 0, + }, + hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({ + id: t.rule.id, + label: t.rule.label, + legalReference: t.rule.legalReference, + })), + requiredDocuments: (decision?.requiredDocuments ?? []) + .filter(d => d.required) + .map(d => ({ + documentType: d.documentType, + depth: d.depth, + detailItems: d.detailItems, + })), + }, + companyProfile: this.projectCompanyProfile(state.companyProfile), + constraints: { + depthRequirements: depthReq, + riskFlags: (decision?.riskFlags ?? []).map(f => ({ + severity: f.severity, + title: f.title, + recommendation: f.recommendation, + })), + boundaries: this.deriveBoundaries(decision, documentType), + }, + existingDocumentData: this.extractExistingDocumentData(state, documentType), + } + } + + /** + * Projiziert den SDKState fuer Ask-Operationen. + * Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente. + * + * ~600 Tokens + */ + projectForAsk(state: SDKState): GapContext { + const decision = state.complianceScope?.decision ?? null + + // Fehlende Pflichtdokumente ermitteln + const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required) + const existingDocTypes = this.getExistingDocumentTypes(state) + const missingDocuments = requiredDocs + .filter(d => !existingDocTypes.includes(d.documentType)) + .map(d => ({ + documentType: d.documentType, + label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType, + depth: d.depth, + estimatedEffort: d.estimatedEffort, + })) + + // Gaps aus der Scope-Decision + const gaps = (decision?.gaps ?? []).map(g => ({ + id: g.id, + severity: g.severity, + title: g.title, + description: g.description, + relatedDocuments: g.relatedDocuments, + })) + + // Unbeantwortete Fragen (aus dem Scope-Profiling) + const answers = state.complianceScope?.answers ?? [] + const answeredIds = new Set(answers.map(a => a.questionId)) + + return { + unansweredQuestions: [], // Populated dynamically from question catalog + gaps, + missingDocuments, + } + } + + /** + * Projiziert den SDKState fuer Validate-Operationen. + * Fokus: Cross-Dokument-Konsistenz, Scope-Compliance. + * + * ~2000 Tokens + */ + projectForValidate( + state: SDKState, + documentTypes: ScopeDocumentType[] + ): ValidationContext { + const decision = state.complianceScope?.decision ?? null + const level = decision?.determinedLevel ?? 'L1' + + // Dokument-Zusammenfassungen sammeln + const documents = documentTypes.map(type => ({ + type, + contentSummary: this.summarizeDocument(state, type), + structuredData: this.extractExistingDocumentData(state, type), + })) + + // Cross-Referenzen extrahieren + const crossReferences = { + vvtCategories: (state.vvt ?? []).map(v => + typeof v === 'object' && v !== null && 'name' in v ? String((v as Record).name) : '' + ).filter(Boolean), + dsfaRisks: state.dsfa + ? ['DSFA vorhanden'] + : [], + tomControls: (state.toms ?? []).map(t => + typeof t === 'object' && t !== null && 'name' in t ? String((t as Record).name) : '' + ).filter(Boolean), + retentionCategories: (state.retentionPolicies ?? []).map(p => + typeof p === 'object' && p !== null && 'name' in p ? String((p as Record).name) : '' + ).filter(Boolean), + } + + // Depth-Requirements fuer alle angefragten Typen + const depthRequirements: Record = {} + for (const type of documentTypes) { + depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? { + required: false, + depth: 'Basis', + detailItems: [], + estimatedEffort: 'N/A', + } + } + + return { + documents, + crossReferences, + scopeLevel: level, + depthRequirements: depthRequirements as Record, + } + } + + // ========================================================================== + // Private Helpers + // ========================================================================== + + private projectCompanyProfile( + profile: CompanyProfile | null + ): DraftContext['companyProfile'] { + if (!profile) { + return { + name: 'Unbekannt', + industry: 'Unbekannt', + employeeCount: 0, + businessModel: 'Unbekannt', + isPublicSector: false, + } + } + + return { + name: profile.companyName ?? profile.name ?? 'Unbekannt', + industry: profile.industry ?? 'Unbekannt', + employeeCount: typeof profile.employeeCount === 'number' + ? profile.employeeCount + : parseInt(String(profile.employeeCount ?? '0'), 10) || 0, + businessModel: profile.businessModel ?? 'Unbekannt', + isPublicSector: profile.isPublicSector ?? false, + ...(profile.dataProtectionOfficer ? { + dataProtectionOfficer: { + name: profile.dataProtectionOfficer.name ?? '', + email: profile.dataProtectionOfficer.email ?? '', + }, + } : {}), + } + } + + /** + * Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf. + */ + private deriveBoundaries( + decision: ScopeDecision | null, + documentType: ScopeDocumentType + ): string[] { + const boundaries: string[] = [] + const level = decision?.determinedLevel ?? 'L1' + + // Grundregel: Scope-Engine ist autoritativ + boundaries.push( + `Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})` + ) + + // DSFA-Boundary + if (documentType === 'dsfa') { + const dsfaRequired = decision?.triggeredHardTriggers?.some( + t => t.rule.dsfaRequired + ) ?? false + if (!dsfaRequired && level !== 'L4') { + boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.') + } + } + + // Dokument nicht in requiredDocuments? + const isRequired = decision?.requiredDocuments?.some( + d => d.documentType === documentType && d.required + ) ?? false + if (!isRequired) { + boundaries.push( + `Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.` + ) + } + + return boundaries + } + + /** + * Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State. + */ + private extractExistingDocumentData( + state: SDKState, + documentType: ScopeDocumentType + ): Record | undefined { + switch (documentType) { + case 'vvt': + return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined + case 'tom': + return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined + case 'lf': + return state.retentionPolicies?.length + ? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length } + : undefined + case 'dsfa': + return state.dsfa ? { assessment: state.dsfa } : undefined + case 'dsi': + return state.documents?.length + ? { entries: state.documents.slice(0, 3), totalCount: state.documents.length } + : undefined + case 'einwilligung': + return state.consents?.length + ? { entries: state.consents.slice(0, 5), totalCount: state.consents.length } + : undefined + default: + return undefined + } + } + + /** + * Ermittelt welche Dokumenttypen bereits im State vorhanden sind. + */ + private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] { + const types: ScopeDocumentType[] = [] + if (state.vvt?.length) types.push('vvt') + if (state.toms?.length) types.push('tom') + if (state.retentionPolicies?.length) types.push('lf') + if (state.dsfa) types.push('dsfa') + if (state.documents?.length) types.push('dsi') + if (state.consents?.length) types.push('einwilligung') + if (state.cookieBanner) types.push('einwilligung') + return types + } + + /** + * Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung. + */ + private summarizeDocument( + state: SDKState, + documentType: ScopeDocumentType + ): string { + switch (documentType) { + case 'vvt': + return state.vvt?.length + ? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst` + : 'Keine VVT-Eintraege vorhanden' + case 'tom': + return state.toms?.length + ? `${state.toms.length} TOM-Massnahmen definiert` + : 'Keine TOM-Massnahmen vorhanden' + case 'lf': + return state.retentionPolicies?.length + ? `${state.retentionPolicies.length} Loeschfristen definiert` + : 'Keine Loeschfristen vorhanden' + case 'dsfa': + return state.dsfa + ? 'DSFA vorhanden' + : 'Keine DSFA vorhanden' + default: + return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}` + } + } +} + +/** Singleton-Instanz */ +export const stateProjector = new StateProjector() diff --git a/admin-v2/lib/sdk/drafting-engine/types.ts b/admin-v2/lib/sdk/drafting-engine/types.ts new file mode 100644 index 0000000..80d7276 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/types.ts @@ -0,0 +1,279 @@ +/** + * Drafting Engine - Type Definitions + * + * Typen fuer die 4 Agent-Rollen: Explain, Ask, Draft, Validate + * Die Drafting Engine erweitert den Compliance Advisor um aktive Dokumententwurfs- + * und Validierungsfaehigkeiten, stets unter Beachtung der deterministischen Scope-Engine. + */ + +import type { + ComplianceDepthLevel, + ComplianceScores, + ScopeDecision, + ScopeDocumentType, + ScopeGap, + RequiredDocument, + RiskFlag, + DocumentDepthRequirement, + ScopeProfilingQuestion, +} from '../compliance-scope-types' +import type { CompanyProfile } from '../types' + +// ============================================================================ +// Agent Mode +// ============================================================================ + +/** Die 4 Agent-Rollen */ +export type AgentMode = 'explain' | 'ask' | 'draft' | 'validate' + +/** Confidence-Score fuer Intent-Erkennung */ +export interface IntentClassification { + mode: AgentMode + confidence: number + matchedPatterns: string[] + /** Falls Draft oder Validate: erkannter Dokumenttyp */ + detectedDocumentType?: ScopeDocumentType +} + +// ============================================================================ +// Draft Context (fuer Draft-Mode) +// ============================================================================ + +/** Projizierter State fuer Draft-Operationen (~1500 Tokens) */ +export interface DraftContext { + /** Scope-Entscheidung (Level, Scores, Hard Triggers) */ + decisions: { + level: ComplianceDepthLevel + scores: ComplianceScores + hardTriggers: Array<{ id: string; label: string; legalReference: string }> + requiredDocuments: Array<{ + documentType: ScopeDocumentType + depth: string + detailItems: string[] + }> + } + /** Firmenprofil-Auszug */ + companyProfile: { + name: string + industry: string + employeeCount: number + businessModel: string + isPublicSector: boolean + dataProtectionOfficer?: { name: string; email: string } + } + /** Constraints aus der Scope-Engine */ + constraints: { + depthRequirements: DocumentDepthRequirement + riskFlags: Array<{ severity: string; title: string; recommendation: string }> + boundaries: string[] + } + /** Optional: bestehende Dokumentdaten aus dem SDK-State */ + existingDocumentData?: Record +} + +// ============================================================================ +// Gap Context (fuer Ask-Mode) +// ============================================================================ + +/** Projizierter State fuer Ask-Operationen (~600 Tokens) */ +export interface GapContext { + /** Noch unbeantwortete Fragen aus dem Scope-Profiling */ + unansweredQuestions: Array<{ + id: string + question: string + type: string + blockId: string + }> + /** Identifizierte Luecken */ + gaps: Array<{ + id: string + severity: string + title: string + description: string + relatedDocuments: ScopeDocumentType[] + }> + /** Fehlende Pflichtdokumente */ + missingDocuments: Array<{ + documentType: ScopeDocumentType + label: string + depth: string + estimatedEffort: string + }> +} + +// ============================================================================ +// Validation Context (fuer Validate-Mode) +// ============================================================================ + +/** Projizierter State fuer Validate-Operationen (~2000 Tokens) */ +export interface ValidationContext { + /** Zu validierende Dokumente */ + documents: Array<{ + type: ScopeDocumentType + /** Zusammenfassung/Auszug des Inhalts */ + contentSummary: string + /** Strukturierte Daten falls vorhanden */ + structuredData?: Record + }> + /** Cross-Referenzen zwischen Dokumenten */ + crossReferences: { + /** VVT Kategorien (Verarbeitungstaetigkeiten) */ + vvtCategories: string[] + /** DSFA Risiken */ + dsfaRisks: string[] + /** TOM Controls */ + tomControls: string[] + /** Loeschfristen-Kategorien */ + retentionCategories: string[] + } + /** Scope-Level fuer Tiefenpruefung */ + scopeLevel: ComplianceDepthLevel + /** Relevante Depth-Requirements */ + depthRequirements: Record +} + +// ============================================================================ +// Validation Result +// ============================================================================ + +export type ValidationSeverity = 'error' | 'warning' | 'suggestion' + +export interface ValidationFinding { + id: string + severity: ValidationSeverity + category: 'scope_violation' | 'inconsistency' | 'missing_content' | 'depth_mismatch' | 'cross_reference' + title: string + description: string + /** Betroffenes Dokument */ + documentType: ScopeDocumentType + /** Optional: Referenz zu anderem Dokument */ + crossReferenceType?: ScopeDocumentType + /** Rechtsgrundlage falls relevant */ + legalReference?: string + /** Vorschlag zur Behebung */ + suggestion?: string + /** Kann automatisch uebernommen werden */ + autoFixable?: boolean +} + +export interface ValidationResult { + passed: boolean + timestamp: string + scopeLevel: ComplianceDepthLevel + errors: ValidationFinding[] + warnings: ValidationFinding[] + suggestions: ValidationFinding[] +} + +// ============================================================================ +// Draft Session +// ============================================================================ + +export interface DraftRevision { + id: string + content: string + sections: DraftSection[] + createdAt: string + instruction?: string +} + +export interface DraftSection { + id: string + title: string + content: string + /** Mapping zum Dokumentschema (z.B. VVT-Feld) */ + schemaField?: string +} + +export interface DraftSession { + id: string + mode: AgentMode + documentType: ScopeDocumentType + /** Aktueller Draft-Inhalt */ + currentDraft: DraftRevision | null + /** Alle bisherigen Revisionen */ + revisions: DraftRevision[] + /** Validierungszustand */ + validationState: ValidationResult | null + /** Constraint-Check Ergebnis */ + constraintCheck: ConstraintCheckResult | null + createdAt: string + updatedAt: string +} + +// ============================================================================ +// Constraint Check (Hard Gate) +// ============================================================================ + +export interface ConstraintCheckResult { + /** Darf der Draft erstellt werden? */ + allowed: boolean + /** Verletzungen die den Draft blockieren */ + violations: string[] + /** Anpassungen die vorgenommen werden sollten */ + adjustments: string[] + /** Gepruefte Regeln */ + checkedRules: string[] +} + +// ============================================================================ +// Chat / API Types +// ============================================================================ + +export interface DraftingChatMessage { + role: 'user' | 'assistant' | 'system' + content: string + /** Metadata fuer Agent-Nachrichten */ + metadata?: { + mode: AgentMode + documentType?: ScopeDocumentType + hasDraft?: boolean + hasValidation?: boolean + } +} + +export interface DraftingChatRequest { + message: string + history: DraftingChatMessage[] + sdkStateProjection: DraftContext | GapContext | ValidationContext + mode?: AgentMode + documentType?: ScopeDocumentType +} + +export interface DraftRequest { + documentType: ScopeDocumentType + draftContext: DraftContext + instructions?: string + existingDraft?: DraftRevision +} + +export interface DraftResponse { + draft: DraftRevision + constraintCheck: ConstraintCheckResult + tokensUsed: number +} + +export interface ValidateRequest { + documentType: ScopeDocumentType + draftContent: string + validationContext: ValidationContext +} + +// ============================================================================ +// Feature Flag +// ============================================================================ + +export interface DraftingEngineConfig { + /** Feature-Flag: Drafting Engine aktiviert */ + enableDraftingEngine: boolean + /** Verfuegbare Modi (fuer schrittweises Rollout) */ + enabledModes: AgentMode[] + /** Max Token-Budget fuer State-Projection */ + maxProjectionTokens: number +} + +export const DEFAULT_DRAFTING_ENGINE_CONFIG: DraftingEngineConfig = { + enableDraftingEngine: false, + enabledModes: ['explain'], + maxProjectionTokens: 4096, +} diff --git a/admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts b/admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts new file mode 100644 index 0000000..d8de534 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts @@ -0,0 +1,343 @@ +'use client' + +/** + * useDraftingEngine - React Hook fuer die Drafting Engine + * + * Managed: currentMode, activeDocumentType, draftSessions, validationState + * Handled: State-Projection, API-Calls, Streaming + * Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft() + */ + +import { useState, useCallback, useRef } from 'react' +import { useSDK } from '../context' +import { stateProjector } from './state-projector' +import { intentClassifier } from './intent-classifier' +import { constraintEnforcer } from './constraint-enforcer' +import type { + AgentMode, + DraftSession, + DraftRevision, + DraftingChatMessage, + ValidationResult, + ConstraintCheckResult, + DraftContext, + GapContext, + ValidationContext, +} from './types' +import type { ScopeDocumentType } from '../compliance-scope-types' + +export interface DraftingEngineState { + currentMode: AgentMode + activeDocumentType: ScopeDocumentType | null + messages: DraftingChatMessage[] + isTyping: boolean + currentDraft: DraftRevision | null + validationResult: ValidationResult | null + constraintCheck: ConstraintCheckResult | null + error: string | null +} + +export interface DraftingEngineActions { + setMode: (mode: AgentMode) => void + setDocumentType: (type: ScopeDocumentType) => void + sendMessage: (content: string) => Promise + requestDraft: (instructions?: string) => Promise + validateDraft: () => Promise + acceptDraft: () => void + stopGeneration: () => void + clearMessages: () => void +} + +export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions { + const { state, dispatch } = useSDK() + const abortControllerRef = useRef(null) + + const [currentMode, setCurrentMode] = useState('explain') + const [activeDocumentType, setActiveDocumentType] = useState(null) + const [messages, setMessages] = useState([]) + const [isTyping, setIsTyping] = useState(false) + const [currentDraft, setCurrentDraft] = useState(null) + const [validationResult, setValidationResult] = useState(null) + const [constraintCheck, setConstraintCheck] = useState(null) + const [error, setError] = useState(null) + + // Get state projection based on mode + const getProjection = useCallback(() => { + switch (currentMode) { + case 'draft': + return activeDocumentType + ? stateProjector.projectForDraft(state, activeDocumentType) + : null + case 'ask': + return stateProjector.projectForAsk(state) + case 'validate': + return activeDocumentType + ? stateProjector.projectForValidate(state, [activeDocumentType]) + : stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf']) + default: + return activeDocumentType + ? stateProjector.projectForDraft(state, activeDocumentType) + : null + } + }, [state, currentMode, activeDocumentType]) + + const setMode = useCallback((mode: AgentMode) => { + setCurrentMode(mode) + }, []) + + const setDocumentType = useCallback((type: ScopeDocumentType) => { + setActiveDocumentType(type) + }, []) + + const sendMessage = useCallback(async (content: string) => { + if (!content.trim() || isTyping) return + setError(null) + + // Auto-detect mode if needed + const classification = intentClassifier.classify(content) + if (classification.confidence > 0.7 && classification.mode !== currentMode) { + setCurrentMode(classification.mode) + } + if (classification.detectedDocumentType && !activeDocumentType) { + setActiveDocumentType(classification.detectedDocumentType) + } + + const userMessage: DraftingChatMessage = { + role: 'user', + content: content.trim(), + } + setMessages(prev => [...prev, userMessage]) + setIsTyping(true) + + abortControllerRef.current = new AbortController() + + try { + const projection = getProjection() + const response = await fetch('/api/sdk/drafting-engine/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message: content.trim(), + history: messages.map(m => ({ role: m.role, content: m.content })), + sdkStateProjection: projection, + mode: currentMode, + documentType: activeDocumentType, + }), + signal: abortControllerRef.current.signal, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' })) + throw new Error(errorData.error || `Server-Fehler (${response.status})`) + } + + const agentMessageId = `msg-${Date.now()}-agent` + setMessages(prev => [...prev, { + role: 'assistant', + content: '', + metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined }, + }]) + + // Stream response + const reader = response.body!.getReader() + const decoder = new TextDecoder() + let accumulated = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + accumulated += decoder.decode(value, { stream: true }) + const text = accumulated + setMessages(prev => + prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m) + ) + } + + setIsTyping(false) + } catch (err) { + if ((err as Error).name === 'AbortError') { + setIsTyping(false) + return + } + setError((err as Error).message) + setMessages(prev => [...prev, { + role: 'assistant', + content: `Fehler: ${(err as Error).message}`, + }]) + setIsTyping(false) + } + }, [isTyping, messages, currentMode, activeDocumentType, getProjection]) + + const requestDraft = useCallback(async (instructions?: string) => { + if (!activeDocumentType) { + setError('Bitte waehlen Sie zuerst einen Dokumenttyp.') + return + } + setError(null) + setIsTyping(true) + + try { + const draftContext = stateProjector.projectForDraft(state, activeDocumentType) + + const response = await fetch('/api/sdk/drafting-engine/draft', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + documentType: activeDocumentType, + draftContext, + instructions, + existingDraft: currentDraft, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error || 'Draft-Generierung fehlgeschlagen') + } + + setCurrentDraft(result.draft) + setConstraintCheck(result.constraintCheck) + + setMessages(prev => [...prev, { + role: 'assistant', + content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`, + metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true }, + }]) + + setIsTyping(false) + } catch (err) { + setError((err as Error).message) + setIsTyping(false) + } + }, [activeDocumentType, state, currentDraft]) + + const validateDraft = useCallback(async () => { + setError(null) + setIsTyping(true) + + try { + const docTypes: ScopeDocumentType[] = activeDocumentType + ? [activeDocumentType] + : ['vvt', 'tom', 'lf'] + const validationContext = stateProjector.projectForValidate(state, docTypes) + + const response = await fetch('/api/sdk/drafting-engine/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + documentType: activeDocumentType || 'vvt', + draftContent: currentDraft?.content || '', + validationContext, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error || 'Validierung fehlgeschlagen') + } + + setValidationResult(result) + + const summary = result.passed + ? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.` + : `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.` + + setMessages(prev => [...prev, { + role: 'assistant', + content: summary, + metadata: { mode: 'validate', hasValidation: true }, + }]) + + setIsTyping(false) + } catch (err) { + setError((err as Error).message) + setIsTyping(false) + } + }, [activeDocumentType, state, currentDraft]) + + const acceptDraft = useCallback(() => { + if (!currentDraft || !activeDocumentType) return + + // Dispatch the draft data into SDK state + switch (activeDocumentType) { + case 'vvt': + dispatch({ + type: 'ADD_PROCESSING_ACTIVITY', + payload: { + id: `draft-vvt-${Date.now()}`, + name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag', + ...Object.fromEntries( + currentDraft.sections + .filter(s => s.schemaField) + .map(s => [s.schemaField!, s.content]) + ), + }, + }) + break + case 'tom': + dispatch({ + type: 'ADD_TOM', + payload: { + id: `draft-tom-${Date.now()}`, + name: 'TOM-Entwurf', + ...Object.fromEntries( + currentDraft.sections + .filter(s => s.schemaField) + .map(s => [s.schemaField!, s.content]) + ), + }, + }) + break + default: + dispatch({ + type: 'ADD_DOCUMENT', + payload: { + id: `draft-${activeDocumentType}-${Date.now()}`, + type: activeDocumentType, + content: currentDraft.content, + sections: currentDraft.sections, + }, + }) + } + + setMessages(prev => [...prev, { + role: 'assistant', + content: `Draft wurde in den SDK-State uebernommen.`, + }]) + setCurrentDraft(null) + }, [currentDraft, activeDocumentType, dispatch]) + + const stopGeneration = useCallback(() => { + abortControllerRef.current?.abort() + setIsTyping(false) + }, []) + + const clearMessages = useCallback(() => { + setMessages([]) + setCurrentDraft(null) + setValidationResult(null) + setConstraintCheck(null) + setError(null) + }, []) + + return { + currentMode, + activeDocumentType, + messages, + isTyping, + currentDraft, + validationResult, + constraintCheck, + error, + setMode, + setDocumentType, + sendMessage, + requestDraft, + validateDraft, + acceptDraft, + stopGeneration, + clearMessages, + } +} diff --git a/agent-core/soul/drafting-agent.soul.md b/agent-core/soul/drafting-agent.soul.md new file mode 100644 index 0000000..c5ceac1 --- /dev/null +++ b/agent-core/soul/drafting-agent.soul.md @@ -0,0 +1,95 @@ +# Drafting Agent - Compliance-Dokumententwurf + +## Identitaet +Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK, +DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und +Konsistenz zwischen Dokumenten sicherzustellen. + +Du arbeitest in 4 Modi: +- **Explain**: Erklaere rechtliche Konzepte (Standard-Modus) +- **Ask**: Erkenne proaktiv Luecken im Compliance-Stand und stelle gezielte Fragen +- **Draft**: Entwirf Compliance-Dokumente innerhalb der Scope-Engine-Constraints +- **Validate**: Pruefe Cross-Dokument-Konsistenz (VVT ↔ DSFA ↔ TOM ↔ Loeschfristen) + +## Strikte Constraints (NICHT VERHANDELBAR) + +### Scope-Engine ist autoritativ +- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen +- Das bestimmte Level (L1-L4) ist bindend fuer die Dokumenttiefe +- Pflichtdokumente werden ausschliesslich durch die Scope-Engine bestimmt +- Hard Triggers (z.B. Art. 9 Daten → DSFA Pflicht) sind unumgaenglich + +### Dokumenttiefe einhalten +- Deine Entwuerfe MUESSEN die Tiefenstufe {level} einhalten +- L1 = Lean/Basis: Kurz, pragmatisch, Checklisten-Format +- L2 = Standard: Strukturiert, alle Pflichtfelder, moderate Tiefe +- L3 = Erweitert: Detailliert, Versionierung, Freigabeprozesse +- L4 = Audit-Ready: Vollstaendig, Nachweisketten, externe Audit-Faehigkeit +- Erstelle KEINE L4-Dokumente bei L2-Decision (Overengineering) +- Erstelle KEINE L1-Dokumente bei L3-Decision (Underengineering) + +### Keine Rechtsberatung +- Du gibst praxisnahe Hinweise, KEINE konkrete Rechtsberatung +- "Es empfiehlt sich..." statt "Sie muessen..." +- Bei komplexen Faellen: Verweis auf Fachanwalt oder DSB + +## Modus-spezifisches Verhalten + +### Explain-Modus +- Antworte in verstaendlichem Deutsch +- Strukturiert mit Ueberschriften +- Immer Quellenangaben (DSGVO-Artikel, BDSG-Paragraph) +- Format: Zusammenfassung → Erklaerung → Praxishinweise → Quellen + +### Ask-Modus +- Analysiere den aktuellen State auf Luecken +- Stelle EINE gezielte Frage pro Nachricht (nicht ueberfordern) +- Erklaere warum die Information wichtig ist +- Verlinke auf relevante SDK-Schritte +- Format: Luecken-Hinweis → Frage → Warum wichtig → Empfohlener Schritt + +### Draft-Modus +- Erstelle strukturierte Dokumente mit Sections +- Jede Section hat: Titel, Inhalt, Schema-Referenz +- Verwende die detailItems aus der DOCUMENT_SCOPE_MATRIX als Checkliste +- Nutze existierende Daten aus dem SDK-State als Grundlage +- Format: JSON mit sections[] Array, jede Section hat id, title, content, schemaField +- Fuege Platzhalter fuer fehlende Daten ein: [PLATZHALTER: Beschreibung] + +### Validate-Modus +- Stufe 1: Deterministische Pruefung (Scope-Compliance) +- Stufe 2: Inhaltliche Konsistenz-Pruefung +- Pruefe Cross-Referenzen zwischen Dokumenten +- Format: Checkliste mit Errors (rot), Warnings (gelb), Suggestions (blau) +- Errors blockieren, Warnings informieren, Suggestions sind optional + +## Kompetenzbereich +- DSGVO Art. 1-99 + Erwaegsgruende +- BDSG (Bundesdatenschutzgesetz) +- AI Act (EU KI-Verordnung) +- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz) +- DSK-Kurzpapiere (Nr. 1-20) +- SDM (Standard-Datenschutzmodell) V3.0 +- BSI-Grundschutz (Basis-Kenntnisse) +- ISO 27001/27701 (Ueberblick) +- EDPB Guidelines +- WP29/WP248 Arbeitspapiere +- Bundes- und Laender-Muss-Listen (DSFA) + +## Kommunikationsstil +- Sachlich, verstaendlich, kein Juristendeutsch +- Deutsch als Hauptsprache +- Strukturierte Antworten +- Quellenangaben bei rechtlichen Aussagen +- Kurze, praegnante Saetze + +## Eskalation +- Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen +- Widerspruechliche Rechtslagen: Beide Positionen darstellen +- Dringende Datenpannen: Auf 72-Stunden-Frist hinweisen + +## Metrik-Ziele +- Quellenangabe in > 95% der Draft-Sections +- Constraint-Compliance: 100% (kein Overriding der Scope-Engine) +- Cross-Reference-Konsistenz > 90% +- Platzhalter-Markierung fuer alle fehlenden Daten diff --git a/ai-compliance-sdk/cmd/server/main.go b/ai-compliance-sdk/cmd/server/main.go index 4a772e9..1b52d5e 100644 --- a/ai-compliance-sdk/cmd/server/main.go +++ b/ai-compliance-sdk/cmd/server/main.go @@ -97,6 +97,7 @@ func main() { roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore) workshopHandlers := handlers.NewWorkshopHandlers(workshopStore) portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore) + draftingHandlers := handlers.NewDraftingHandlers(accessGate, providerRegistry, piiDetector, auditStore, trailBuilder) // Initialize middleware rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine) @@ -425,6 +426,15 @@ func main() { portfolioRoutes.POST("/merge", portfolioHandlers.MergePortfolios) portfolioRoutes.POST("/compare", portfolioHandlers.ComparePortfolios) } + + // Drafting Engine routes - Compliance Document Drafting & Validation + draftingRoutes := v1.Group("/drafting") + draftingRoutes.Use(rbacMiddleware.RequireLLMAccess()) + { + draftingRoutes.POST("/draft", draftingHandlers.DraftDocument) + draftingRoutes.POST("/validate", draftingHandlers.ValidateDocument) + draftingRoutes.GET("/history", draftingHandlers.GetDraftHistory) + } } // Create HTTP server diff --git a/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go b/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go new file mode 100644 index 0000000..bc2fb27 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go @@ -0,0 +1,335 @@ +package handlers + +import ( + "fmt" + "net/http" + "time" + + "github.com/breakpilot/ai-compliance-sdk/internal/audit" + "github.com/breakpilot/ai-compliance-sdk/internal/llm" + "github.com/breakpilot/ai-compliance-sdk/internal/rbac" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// DraftingHandlers handles Drafting Engine API endpoints +type DraftingHandlers struct { + accessGate *llm.AccessGate + registry *llm.ProviderRegistry + piiDetector *llm.PIIDetector + auditStore *audit.Store + trailBuilder *audit.TrailBuilder +} + +// NewDraftingHandlers creates new Drafting Engine handlers +func NewDraftingHandlers( + accessGate *llm.AccessGate, + registry *llm.ProviderRegistry, + piiDetector *llm.PIIDetector, + auditStore *audit.Store, + trailBuilder *audit.TrailBuilder, +) *DraftingHandlers { + return &DraftingHandlers{ + accessGate: accessGate, + registry: registry, + piiDetector: piiDetector, + auditStore: auditStore, + trailBuilder: trailBuilder, + } +} + +// --------------------------------------------------------------------------- +// Request/Response Types +// --------------------------------------------------------------------------- + +// DraftDocumentRequest represents a request to generate a compliance document draft +type DraftDocumentRequest struct { + DocumentType string `json:"document_type" binding:"required"` + ScopeLevel string `json:"scope_level" binding:"required"` + Context map[string]interface{} `json:"context"` + Instructions string `json:"instructions"` + Model string `json:"model"` +} + +// ValidateDocumentRequest represents a request to validate document consistency +type ValidateDocumentRequest struct { + DocumentType string `json:"document_type" binding:"required"` + DraftContent string `json:"draft_content"` + ValidationContext map[string]interface{} `json:"validation_context"` + Model string `json:"model"` +} + +// DraftHistoryEntry represents a single audit trail entry for drafts +type DraftHistoryEntry struct { + ID string `json:"id"` + UserID string `json:"user_id"` + TenantID string `json:"tenant_id"` + DocumentType string `json:"document_type"` + ScopeLevel string `json:"scope_level"` + Operation string `json:"operation"` + ConstraintsRespected bool `json:"constraints_respected"` + TokensUsed int `json:"tokens_used"` + CreatedAt time.Time `json:"created_at"` +} + +// --------------------------------------------------------------------------- +// Handlers +// --------------------------------------------------------------------------- + +// DraftDocument handles document draft generation via LLM with constraint validation +func (h *DraftingHandlers) DraftDocument(c *gin.Context) { + var req DraftDocumentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + tenantID := rbac.GetTenantID(c) + namespaceID := rbac.GetNamespaceID(c) + + if userID == uuid.Nil || tenantID == uuid.Nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"}) + return + } + + // Validate scope level + validLevels := map[string]bool{"L1": true, "L2": true, "L3": true, "L4": true} + if !validLevels[req.ScopeLevel] { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid scope_level, must be L1-L4"}) + return + } + + // Validate document type + validTypes := map[string]bool{ + "vvt": true, "tom": true, "dsfa": true, "dsi": true, "lf": true, + "av_vertrag": true, "betroffenenrechte": true, "einwilligung": true, + "daten_transfer": true, "datenpannen": true, "vertragsmanagement": true, + "schulung": true, "audit_log": true, "risikoanalyse": true, + "notfallplan": true, "zertifizierung": true, "datenschutzmanagement": true, + } + if !validTypes[req.DocumentType] { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid document_type"}) + return + } + + // Build system prompt for drafting + systemPrompt := fmt.Sprintf( + `Du bist ein DSGVO-Compliance-Experte. Erstelle einen strukturierten Entwurf fuer Dokument "%s" auf Level %s. +Antworte NUR im JSON-Format mit einem "sections" Array. +Jede Section hat: id, title, content, schemaField. +Halte die Tiefe strikt am vorgegebenen Level. +Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung]. +Sprache: Deutsch.`, + req.DocumentType, req.ScopeLevel, + ) + + userPrompt := "Erstelle den Dokumententwurf." + if req.Instructions != "" { + userPrompt = req.Instructions + } + + // Detect PII in context + contextStr := fmt.Sprintf("%v", req.Context) + dataCategories := h.piiDetector.DetectDataCategories(contextStr) + + // Process through access gate + chatReq := &llm.ChatRequest{ + Model: req.Model, + Messages: []llm.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: userPrompt}, + }, + MaxTokens: 16384, + Temperature: 0.15, + } + + gatedReq, err := h.accessGate.ProcessChatRequest( + c.Request.Context(), + userID, tenantID, namespaceID, + chatReq, dataCategories, + ) + if err != nil { + h.logDraftAudit(c, userID, tenantID, req.DocumentType, req.ScopeLevel, "draft", false, 0, err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": "access_denied", + "message": err.Error(), + }) + return + } + + // Execute the request + resp, err := h.accessGate.ExecuteChat(c.Request.Context(), gatedReq) + if err != nil { + h.logDraftAudit(c, userID, tenantID, req.DocumentType, req.ScopeLevel, "draft", false, 0, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "llm_error", + "message": err.Error(), + }) + return + } + + tokensUsed := 0 + if resp.Usage.TotalTokens > 0 { + tokensUsed = resp.Usage.TotalTokens + } + + // Log successful draft + h.logDraftAudit(c, userID, tenantID, req.DocumentType, req.ScopeLevel, "draft", true, tokensUsed, "") + + c.JSON(http.StatusOK, gin.H{ + "document_type": req.DocumentType, + "scope_level": req.ScopeLevel, + "content": resp.Message.Content, + "model": resp.Model, + "provider": resp.Provider, + "tokens_used": tokensUsed, + "pii_detected": gatedReq.PIIDetected, + }) +} + +// ValidateDocument handles document cross-consistency validation +func (h *DraftingHandlers) ValidateDocument(c *gin.Context) { + var req ValidateDocumentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := rbac.GetUserID(c) + tenantID := rbac.GetTenantID(c) + namespaceID := rbac.GetNamespaceID(c) + + if userID == uuid.Nil || tenantID == uuid.Nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"}) + return + } + + // Build validation prompt + systemPrompt := `Du bist ein DSGVO-Compliance-Validator. Pruefe die Konsistenz und Vollstaendigkeit. +Antworte NUR im JSON-Format: +{ + "passed": boolean, + "errors": [{"id": string, "severity": "error", "title": string, "description": string, "documentType": string, "legalReference": string}], + "warnings": [{"id": string, "severity": "warning", "title": string, "description": string}], + "suggestions": [{"id": string, "severity": "suggestion", "title": string, "description": string, "suggestion": string}] +}` + + validationPrompt := fmt.Sprintf("Validiere Dokument '%s'.\nInhalt:\n%s\nKontext:\n%v", + req.DocumentType, req.DraftContent, req.ValidationContext) + + // Detect PII + dataCategories := h.piiDetector.DetectDataCategories(req.DraftContent) + + chatReq := &llm.ChatRequest{ + Model: req.Model, + Messages: []llm.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: validationPrompt}, + }, + MaxTokens: 8192, + Temperature: 0.1, + } + + gatedReq, err := h.accessGate.ProcessChatRequest( + c.Request.Context(), + userID, tenantID, namespaceID, + chatReq, dataCategories, + ) + if err != nil { + h.logDraftAudit(c, userID, tenantID, req.DocumentType, "", "validate", false, 0, err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": "access_denied", + "message": err.Error(), + }) + return + } + + resp, err := h.accessGate.ExecuteChat(c.Request.Context(), gatedReq) + if err != nil { + h.logDraftAudit(c, userID, tenantID, req.DocumentType, "", "validate", false, 0, err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "llm_error", + "message": err.Error(), + }) + return + } + + tokensUsed := 0 + if resp.Usage.TotalTokens > 0 { + tokensUsed = resp.Usage.TotalTokens + } + + h.logDraftAudit(c, userID, tenantID, req.DocumentType, "", "validate", true, tokensUsed, "") + + c.JSON(http.StatusOK, gin.H{ + "document_type": req.DocumentType, + "validation": resp.Message.Content, + "model": resp.Model, + "provider": resp.Provider, + "tokens_used": tokensUsed, + "pii_detected": gatedReq.PIIDetected, + }) +} + +// GetDraftHistory returns the audit trail of all drafting operations for a tenant +func (h *DraftingHandlers) GetDraftHistory(c *gin.Context) { + userID := rbac.GetUserID(c) + tenantID := rbac.GetTenantID(c) + + if userID == uuid.Nil || tenantID == uuid.Nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"}) + return + } + + // Query audit store for drafting operations + entries, _, err := h.auditStore.QueryGeneralAuditEntries(c.Request.Context(), &audit.GeneralAuditFilter{ + TenantID: tenantID, + ResourceType: "compliance_document", + Limit: 50, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to query draft history"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "history": entries, + "total": len(entries), + }) +} + +// --------------------------------------------------------------------------- +// Audit Logging +// --------------------------------------------------------------------------- + +func (h *DraftingHandlers) logDraftAudit( + c *gin.Context, + userID, tenantID uuid.UUID, + documentType, scopeLevel, operation string, + constraintsRespected bool, + tokensUsed int, + errorMsg string, +) { + newValues := map[string]any{ + "document_type": documentType, + "scope_level": scopeLevel, + "constraints_respected": constraintsRespected, + "tokens_used": tokensUsed, + } + if errorMsg != "" { + newValues["error"] = errorMsg + } + + entry := h.trailBuilder.NewGeneralEntry(). + WithTenant(tenantID). + WithUser(userID). + WithAction("drafting_engine." + operation). + WithResource("compliance_document", nil). + WithNewValues(newValues). + WithClient(c.ClientIP(), c.GetHeader("User-Agent")) + + go func() { + entry.Save(c.Request.Context()) + }() +}