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>
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
/**
|
|
* 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()
|