Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
337
admin-compliance/lib/sdk/drafting-engine/state-projector.ts
Normal file
337
admin-compliance/lib/sdk/drafting-engine/state-projector.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 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()
|
||||
Reference in New Issue
Block a user