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 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-11 12:37:18 +01:00
parent f927c0c205
commit 206183670d
26 changed files with 4639 additions and 1 deletions

View File

@@ -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> = {}): 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)
})
})
})

View File

@@ -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')
})
})
})

View File

@@ -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> = {}): 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
})
})
})

View File

@@ -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()

View File

@@ -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()

View File

@@ -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`
}

View File

@@ -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.`
}

View File

@@ -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}.`
}

View File

@@ -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}.`
}

View File

@@ -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}.`
}

View File

@@ -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}).`
}

View File

@@ -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'

View File

@@ -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": [...]
}`
}

View File

@@ -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<string, unknown>).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<string, unknown>).name) : ''
).filter(Boolean),
retentionCategories: (state.retentionPolicies ?? []).map(p =>
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
).filter(Boolean),
}
// Depth-Requirements fuer alle angefragten Typen
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
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<ScopeDocumentType, DocumentDepthRequirement>,
}
}
// ==========================================================================
// 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<string, unknown> | 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()

View File

@@ -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<string, unknown>
}
// ============================================================================
// 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<string, unknown>
}>
/** 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<ScopeDocumentType, DocumentDepthRequirement>
}
// ============================================================================
// 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,
}

View File

@@ -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<void>
requestDraft: (instructions?: string) => Promise<void>
validateDraft: () => Promise<void>
acceptDraft: () => void
stopGeneration: () => void
clearMessages: () => void
}
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
const { state, dispatch } = useSDK()
const abortControllerRef = useRef<AbortController | null>(null)
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
const [isTyping, setIsTyping] = useState(false)
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
const [error, setError] = useState<string | null>(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,
}
}