fix(quality): Ruff/CVE/TS-Fixes, 104 neue Tests, Complexity-Refactoring
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell - CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns) - TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes - Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed - Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A) - Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,153 +0,0 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,312 +0,0 @@
|
||||
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,
|
||||
sourcePolicy: 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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -95,11 +95,11 @@ export function buildAllowedFacts(
|
||||
const scope = state.complianceScope
|
||||
|
||||
return {
|
||||
companyName: profile?.name ?? 'Unbekannt',
|
||||
companyName: profile?.companyName ?? 'Unbekannt',
|
||||
legalForm: profile?.legalForm ?? '',
|
||||
industry: profile?.industry ?? '',
|
||||
location: profile?.location ?? '',
|
||||
employeeCount: profile?.employeeCount ?? 0,
|
||||
location: profile?.headquartersCity ?? '',
|
||||
employeeCount: parseEmployeeCount(profile?.employeeCount),
|
||||
|
||||
teamStructure: deriveTeamStructure(profile),
|
||||
itLandscape: deriveItLandscape(profile),
|
||||
@@ -213,11 +213,33 @@ export function checkForDisallowedContent(
|
||||
// Private Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parst den employeeCount-String (z.B. "1-9", "50-249", "1000+") in eine Zahl.
|
||||
* Verwendet den Mittelwert des Bereichs oder den unteren Wert bei "+".
|
||||
*/
|
||||
function parseEmployeeCount(value: string | undefined | null): number {
|
||||
if (!value) return 0
|
||||
// Handle "1000+" style
|
||||
const plusMatch = value.match(/^(\d+)\+$/)
|
||||
if (plusMatch) return parseInt(plusMatch[1], 10)
|
||||
// Handle "50-249" style ranges
|
||||
const rangeMatch = value.match(/^(\d+)-(\d+)$/)
|
||||
if (rangeMatch) {
|
||||
const low = parseInt(rangeMatch[1], 10)
|
||||
const high = parseInt(rangeMatch[2], 10)
|
||||
return Math.round((low + high) / 2)
|
||||
}
|
||||
// Try plain number
|
||||
const num = parseInt(value, 10)
|
||||
return isNaN(num) ? 0 : num
|
||||
}
|
||||
|
||||
function deriveTeamStructure(profile: CompanyProfile | null): string {
|
||||
if (!profile) return ''
|
||||
// Ableitung aus verfuegbaren Profildaten
|
||||
if (profile.employeeCount > 500) return 'Konzernstruktur'
|
||||
if (profile.employeeCount > 50) return 'mittelstaendisch'
|
||||
const count = parseEmployeeCount(profile.employeeCount)
|
||||
if (count > 500) return 'Konzernstruktur'
|
||||
if (count > 50) return 'mittelstaendisch'
|
||||
return 'Kleinunternehmen'
|
||||
}
|
||||
|
||||
@@ -225,15 +247,15 @@ function deriveItLandscape(profile: CompanyProfile | null): string {
|
||||
if (!profile) return ''
|
||||
return profile.businessModel?.includes('SaaS') ? 'Cloud-First' :
|
||||
profile.businessModel?.includes('Cloud') ? 'Cloud-First' :
|
||||
profile.isPublicSector ? 'On-Premise' : 'Hybrid'
|
||||
'Hybrid'
|
||||
}
|
||||
|
||||
function deriveSpecialFeatures(profile: CompanyProfile | null): string[] {
|
||||
if (!profile) return []
|
||||
const features: string[] = []
|
||||
if (profile.isPublicSector) features.push('Oeffentlicher Sektor')
|
||||
if (profile.employeeCount > 250) features.push('Grossunternehmen')
|
||||
if (profile.dataProtectionOfficer) features.push('Interner DSB benannt')
|
||||
const count = parseEmployeeCount(profile.employeeCount)
|
||||
if (count > 250) features.push('Grossunternehmen')
|
||||
if (profile.dpoName) features.push('Interner DSB benannt')
|
||||
return features
|
||||
}
|
||||
|
||||
@@ -253,5 +275,5 @@ function deriveTriggeredRegulations(
|
||||
|
||||
function derivePrimaryUseCases(state: SDKState): string[] {
|
||||
if (!state.useCases || state.useCases.length === 0) return []
|
||||
return state.useCases.slice(0, 3).map(uc => uc.name || uc.title || 'Unbenannt')
|
||||
return state.useCases.slice(0, 3).map(uc => uc.name || 'Unbenannt')
|
||||
}
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'datenpannen',
|
||||
patterns: [
|
||||
/\bdatenpanne/i,
|
||||
/\bdata\s*breach/i,
|
||||
/\bart\.?\s*33\b/i,
|
||||
/\bsicherheitsvorfall/i,
|
||||
/\bincident/i,
|
||||
/\bmelde.*vorfall/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'daten_transfer',
|
||||
patterns: [
|
||||
/\bdrittland/i,
|
||||
/\btransfer/i,
|
||||
/\bscc\b/i,
|
||||
/\bstandardvertragsklausel/i,
|
||||
/\bart\.?\s*44\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'vertragsmanagement',
|
||||
patterns: [
|
||||
/\bvertragsmanagement/i,
|
||||
/\bcontract\s*management/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'schulung',
|
||||
patterns: [
|
||||
/\bschulung/i,
|
||||
/\btraining/i,
|
||||
/\bawareness/i,
|
||||
/\bmitarbeiterschulung/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'audit_log',
|
||||
patterns: [
|
||||
/\baudit/i,
|
||||
/\blogging\b/i,
|
||||
/\bprotokollierung/i,
|
||||
/\bart\.?\s*5\s*abs\.?\s*2\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'risikoanalyse',
|
||||
patterns: [
|
||||
/\brisikoanalyse/i,
|
||||
/\brisk\s*assessment/i,
|
||||
/\brisikobewertung/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'notfallplan',
|
||||
patterns: [
|
||||
/\bnotfallplan/i,
|
||||
/\bkrisenmanagement/i,
|
||||
/\bbusiness\s*continuity/i,
|
||||
/\bnotfall/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'zertifizierung',
|
||||
patterns: [
|
||||
/\bzertifizierung/i,
|
||||
/\biso\s*27001\b/i,
|
||||
/\biso\s*27701\b/i,
|
||||
/\bart\.?\s*42\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'datenschutzmanagement',
|
||||
patterns: [
|
||||
/\bdsms\b/i,
|
||||
/\bdatenschutzmanagement/i,
|
||||
/\bpdca/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'iace_ce_assessment',
|
||||
patterns: [
|
||||
/\biace\b/i,
|
||||
/\bce[\s-]?kennzeichnung/i,
|
||||
/\bai\s*act\b/i,
|
||||
/\bki[\s-]?verordnung/i,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
// Redirect Patterns (nicht-draftbare Dokumente → Document Generator)
|
||||
// ============================================================================
|
||||
|
||||
const REDIRECT_PATTERNS: Array<{
|
||||
pattern: RegExp
|
||||
response: string
|
||||
}> = [
|
||||
{
|
||||
pattern: /\bimpressum\b/i,
|
||||
response: 'Impressum-Templates finden Sie unter /sdk/document-generator → Kategorie "Impressum". Der Drafting Agent erstellt keine Impressen, da diese nach DDG §5 unternehmensspezifisch sind.',
|
||||
},
|
||||
{
|
||||
pattern: /\b(agb|allgemeine.?geschaefts)/i,
|
||||
response: 'AGB-Vorlagen erstellen Sie im Document Generator unter /sdk/document-generator → Kategorie "AGB". Der Drafting Agent erstellt keine AGB, da diese nach BGB §305ff individuell gestaltet werden muessen.',
|
||||
},
|
||||
{
|
||||
pattern: /\bwiderruf/i,
|
||||
response: 'Widerrufs-Templates finden Sie unter /sdk/document-generator → Kategorie "Widerruf".',
|
||||
},
|
||||
{
|
||||
pattern: /\bnda\b/i,
|
||||
response: 'NDA-Vorlagen finden Sie unter /sdk/document-generator.',
|
||||
},
|
||||
{
|
||||
pattern: /\bsla\b/i,
|
||||
response: 'SLA-Vorlagen finden Sie unter /sdk/document-generator.',
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
// 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)
|
||||
|
||||
// Redirect-Check: Nicht-draftbare Dokumente → Document Generator
|
||||
for (const redirect of REDIRECT_PATTERNS) {
|
||||
if (redirect.pattern.test(normalized)) {
|
||||
return {
|
||||
mode: 'explain',
|
||||
confidence: 0.90,
|
||||
matchedPatterns: [redirect.pattern.source],
|
||||
suggestedResponse: redirect.response,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -243,7 +243,7 @@ function sanitizeAddress(
|
||||
*/
|
||||
export function validateNoRemainingPII(facts: SanitizedFacts): string[] {
|
||||
const warnings: string[] = []
|
||||
const allValues = extractAllStringValues(facts)
|
||||
const allValues = extractAllStringValues(facts as unknown as Record<string, unknown>)
|
||||
|
||||
for (const { path, value } of allValues) {
|
||||
if (path === '__sanitized') continue
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
/**
|
||||
* 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')
|
||||
if (state.risks?.length) types.push('risikoanalyse')
|
||||
if (state.escalationWorkflows?.length) types.push('datenpannen')
|
||||
if (state.iaceProjects?.length) types.push('iace_ce_assessment')
|
||||
if (state.obligations?.length) types.push('zertifizierung')
|
||||
if (state.dsrConfig) types.push('betroffenenrechte')
|
||||
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()
|
||||
@@ -1,343 +0,0 @@
|
||||
'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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user