All checks were successful
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) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 23s
- DOCUMENT_SDK_STEP_MAP: 12 kaputte URLs korrigiert (z.B. /sdk/loeschkonzept → /sdk/loeschfristen) - Go Backend: iace_ce_assessment zur validTypes-Whitelist hinzugefuegt - SOUL-Datei: von 17 auf ~80 Zeilen erweitert (18 draftbare Typen, Redirects, operative Module) - Intent Classifier: 10 fehlende Dokumenttyp-Patterns + 5 Redirect-Patterns (Impressum/AGB/Widerruf → Document Generator) - State Projector: getExistingDocumentTypes von 6 auf 11 Checks erweitert (risks, escalations, iace, obligations, dsr) - DraftingEngineWidget: Gap-Banner fuer kritische Luecken mit Analysieren-Button - Cross-Validation: 4 neue deterministische Regeln (DSFA-NO-VVT, DSFA-NO-TOM, DSI-NO-LF, AV-NO-VVT) - Prose Blocks: 5 neue Dokumenttypen (av_vertrag, betroffenenrechte, risikoanalyse, notfallplan, iace_ce_assessment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
343 lines
11 KiB
TypeScript
343 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')
|
|
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()
|