/** * State Projector - Token-budgetierte Projektion des SDK-State * * Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die * relevanten Slices fuer den jeweiligen Agent-Modus. * * Token-Budgets: * - Draft: ~1500 Tokens * - Ask: ~600 Tokens * - Validate: ~2000 Tokens */ import type { SDKState, CompanyProfile } from '../types' import type { ComplianceScopeState, ScopeDecision, ScopeDocumentType, ScopeGap, RequiredDocument, RiskFlag, DOCUMENT_SCOPE_MATRIX, DocumentDepthRequirement, } from '../compliance-scope-types' import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types' import type { DraftContext, GapContext, ValidationContext, } from './types' // ============================================================================ // State Projector // ============================================================================ export class StateProjector { /** * Projiziert den SDKState fuer Draft-Operationen. * Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints. * * ~1500 Tokens */ projectForDraft( state: SDKState, documentType: ScopeDocumentType ): DraftContext { const decision = state.complianceScope?.decision ?? null const level = decision?.determinedLevel ?? 'L1' const depthReq = DOC_MATRIX[documentType]?.[level] ?? { required: false, depth: 'Basis', detailItems: [], estimatedEffort: 'N/A', } return { decisions: { level, scores: decision?.scores ?? { risk_score: 0, complexity_score: 0, assurance_need: 0, composite_score: 0, }, hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({ id: t.rule.id, label: t.rule.label, legalReference: t.rule.legalReference, })), requiredDocuments: (decision?.requiredDocuments ?? []) .filter(d => d.required) .map(d => ({ documentType: d.documentType, depth: d.depth, detailItems: d.detailItems, })), }, companyProfile: this.projectCompanyProfile(state.companyProfile), constraints: { depthRequirements: depthReq, riskFlags: (decision?.riskFlags ?? []).map(f => ({ severity: f.severity, title: f.title, recommendation: f.recommendation, })), boundaries: this.deriveBoundaries(decision, documentType), }, existingDocumentData: this.extractExistingDocumentData(state, documentType), } } /** * Projiziert den SDKState fuer Ask-Operationen. * Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente. * * ~600 Tokens */ projectForAsk(state: SDKState): GapContext { const decision = state.complianceScope?.decision ?? null // Fehlende Pflichtdokumente ermitteln const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required) const existingDocTypes = this.getExistingDocumentTypes(state) const missingDocuments = requiredDocs .filter(d => !existingDocTypes.includes(d.documentType)) .map(d => ({ documentType: d.documentType, label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType, depth: d.depth, estimatedEffort: d.estimatedEffort, })) // Gaps aus der Scope-Decision const gaps = (decision?.gaps ?? []).map(g => ({ id: g.id, severity: g.severity, title: g.title, description: g.description, relatedDocuments: g.relatedDocuments, })) // Unbeantwortete Fragen (aus dem Scope-Profiling) const answers = state.complianceScope?.answers ?? [] const answeredIds = new Set(answers.map(a => a.questionId)) return { unansweredQuestions: [], // Populated dynamically from question catalog gaps, missingDocuments, } } /** * Projiziert den SDKState fuer Validate-Operationen. * Fokus: Cross-Dokument-Konsistenz, Scope-Compliance. * * ~2000 Tokens */ projectForValidate( state: SDKState, documentTypes: ScopeDocumentType[] ): ValidationContext { const decision = state.complianceScope?.decision ?? null const level = decision?.determinedLevel ?? 'L1' // Dokument-Zusammenfassungen sammeln const documents = documentTypes.map(type => ({ type, contentSummary: this.summarizeDocument(state, type), structuredData: this.extractExistingDocumentData(state, type), })) // Cross-Referenzen extrahieren const crossReferences = { vvtCategories: (state.vvt ?? []).map(v => typeof v === 'object' && v !== null && 'name' in v ? String((v as Record).name) : '' ).filter(Boolean), dsfaRisks: state.dsfa ? ['DSFA vorhanden'] : [], tomControls: (state.toms ?? []).map(t => typeof t === 'object' && t !== null && 'name' in t ? String((t as Record).name) : '' ).filter(Boolean), retentionCategories: (state.retentionPolicies ?? []).map(p => typeof p === 'object' && p !== null && 'name' in p ? String((p as Record).name) : '' ).filter(Boolean), } // Depth-Requirements fuer alle angefragten Typen const depthRequirements: Record = {} for (const type of documentTypes) { depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? { required: false, depth: 'Basis', detailItems: [], estimatedEffort: 'N/A', } } return { documents, crossReferences, scopeLevel: level, depthRequirements: depthRequirements as Record, } } // ========================================================================== // Private Helpers // ========================================================================== private projectCompanyProfile( profile: CompanyProfile | null ): DraftContext['companyProfile'] { if (!profile) { return { name: 'Unbekannt', industry: 'Unbekannt', employeeCount: 0, businessModel: 'Unbekannt', isPublicSector: false, } } return { name: profile.companyName ?? profile.name ?? 'Unbekannt', industry: profile.industry ?? 'Unbekannt', employeeCount: typeof profile.employeeCount === 'number' ? profile.employeeCount : parseInt(String(profile.employeeCount ?? '0'), 10) || 0, businessModel: profile.businessModel ?? 'Unbekannt', isPublicSector: profile.isPublicSector ?? false, ...(profile.dataProtectionOfficer ? { dataProtectionOfficer: { name: profile.dataProtectionOfficer.name ?? '', email: profile.dataProtectionOfficer.email ?? '', }, } : {}), } } /** * Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf. */ private deriveBoundaries( decision: ScopeDecision | null, documentType: ScopeDocumentType ): string[] { const boundaries: string[] = [] const level = decision?.determinedLevel ?? 'L1' // Grundregel: Scope-Engine ist autoritativ boundaries.push( `Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})` ) // DSFA-Boundary if (documentType === 'dsfa') { const dsfaRequired = decision?.triggeredHardTriggers?.some( t => t.rule.dsfaRequired ) ?? false if (!dsfaRequired && level !== 'L4') { boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.') } } // Dokument nicht in requiredDocuments? const isRequired = decision?.requiredDocuments?.some( d => d.documentType === documentType && d.required ) ?? false if (!isRequired) { boundaries.push( `Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.` ) } return boundaries } /** * Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State. */ private extractExistingDocumentData( state: SDKState, documentType: ScopeDocumentType ): Record | undefined { switch (documentType) { case 'vvt': return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined case 'tom': return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined case 'lf': return state.retentionPolicies?.length ? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length } : undefined case 'dsfa': return state.dsfa ? { assessment: state.dsfa } : undefined case 'dsi': return state.documents?.length ? { entries: state.documents.slice(0, 3), totalCount: state.documents.length } : undefined case 'einwilligung': return state.consents?.length ? { entries: state.consents.slice(0, 5), totalCount: state.consents.length } : undefined default: return undefined } } /** * Ermittelt welche Dokumenttypen bereits im State vorhanden sind. */ private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] { const types: ScopeDocumentType[] = [] if (state.vvt?.length) types.push('vvt') if (state.toms?.length) types.push('tom') if (state.retentionPolicies?.length) types.push('lf') if (state.dsfa) types.push('dsfa') if (state.documents?.length) types.push('dsi') if (state.consents?.length) types.push('einwilligung') if (state.cookieBanner) types.push('einwilligung') return types } /** * Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung. */ private summarizeDocument( state: SDKState, documentType: ScopeDocumentType ): string { switch (documentType) { case 'vvt': return state.vvt?.length ? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst` : 'Keine VVT-Eintraege vorhanden' case 'tom': return state.toms?.length ? `${state.toms.length} TOM-Massnahmen definiert` : 'Keine TOM-Massnahmen vorhanden' case 'lf': return state.retentionPolicies?.length ? `${state.retentionPolicies.length} Loeschfristen definiert` : 'Keine Loeschfristen vorhanden' case 'dsfa': return state.dsfa ? 'DSFA vorhanden' : 'Keine DSFA vorhanden' default: return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}` } } } /** Singleton-Instanz */ export const stateProjector = new StateProjector()