Files
breakpilot-compliance/admin-compliance/lib/sdk/drafting-engine/state-projector.ts
Benjamin Admin 274dc68e24
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
feat: Drafting Agent Kompetenzbereich erweitert — alle 18 Dokumenttypen, Gap-Banner, Redirect-Logic
- 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>
2026-03-05 09:07:07 +01:00

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