/** * Constraint Enforcer - Hard Gate vor jedem Draft * * Stellt sicher, dass die Drafting Engine NIEMALS die deterministische * Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang: * * 1. Ist der Dokumenttyp in requiredDocuments? * 2. Passt die Draft-Tiefe zum Level? * 3. Ist eine DSFA erforderlich (Hard Trigger)? * 4. Werden Risiko-Flags beruecksichtigt? */ import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types' import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types' import type { ConstraintCheckResult, DraftContext } from './types' export class ConstraintEnforcer { /** * Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist. * Dies ist ein HARD GATE - bei Violation wird der Draft blockiert. */ check( documentType: ScopeDocumentType, decision: ScopeDecision | null, requestedDepthLevel?: ComplianceDepthLevel ): ConstraintCheckResult { const violations: string[] = [] const adjustments: string[] = [] const checkedRules: string[] = [] // Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben if (!decision) { checkedRules.push('RULE-NO-DECISION') if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') { violations.push( 'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.' ) } else { adjustments.push( 'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.' ) } return { allowed: violations.length === 0, violations, adjustments, checkedRules, } } const level = decision.determinedLevel const levelNumeric = getDepthLevelNumeric(level) // ----------------------------------------------------------------------- // Rule 1: Dokumenttyp in requiredDocuments? // ----------------------------------------------------------------------- checkedRules.push('RULE-DOC-REQUIRED') const isRequired = decision.requiredDocuments.some( d => d.documentType === documentType && d.required ) const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level] if (!isRequired && scopeReq && !scopeReq.required) { // Nicht blockieren, aber warnen adjustments.push( `Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` + `Entwurf ist moeglich, aber optional.` ) } // ----------------------------------------------------------------------- // Rule 2: Draft-Tiefe passt zum Level? // ----------------------------------------------------------------------- checkedRules.push('RULE-DEPTH-MATCH') if (requestedDepthLevel) { const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel) if (requestedNumeric > levelNumeric) { violations.push( `Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` + `Die Scope-Engine hat Level ${level} festgelegt. ` + `Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.` ) } else if (requestedNumeric < levelNumeric) { adjustments.push( `Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` + `Draft wird auf Level ${level} angehoben.` ) } } // ----------------------------------------------------------------------- // Rule 3: DSFA-Enforcement // ----------------------------------------------------------------------- checkedRules.push('RULE-DSFA-ENFORCEMENT') if (documentType === 'dsfa') { const dsfaRequired = decision.triggeredHardTriggers.some( t => t.rule.dsfaRequired ) if (!dsfaRequired && level !== 'L4') { adjustments.push( 'DSFA ist laut Scope-Engine nicht verpflichtend. ' + 'Entwurf wird als freiwillige Massnahme gekennzeichnet.' ) } } // Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen if (documentType !== 'dsfa') { const dsfaRequired = decision.triggeredHardTriggers.some( t => t.rule.dsfaRequired ) const dsfaInRequired = decision.requiredDocuments.some( d => d.documentType === 'dsfa' && d.required ) if (dsfaRequired && dsfaInRequired) { // Nur ein Hinweis, kein Block adjustments.push( 'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' + 'Bitte sicherstellen, dass auch eine DSFA erstellt wird.' ) } } // ----------------------------------------------------------------------- // Rule 4: Risiko-Flags beruecksichtigt? // ----------------------------------------------------------------------- checkedRules.push('RULE-RISK-FLAGS') const criticalRisks = decision.riskFlags.filter( f => f.severity === 'CRITICAL' || f.severity === 'HIGH' ) if (criticalRisks.length > 0) { adjustments.push( `${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` + `Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}` ) } // ----------------------------------------------------------------------- // Rule 5: Hard-Trigger Consistency // ----------------------------------------------------------------------- checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY') for (const trigger of decision.triggeredHardTriggers) { const mandatoryDocs = trigger.rule.mandatoryDocuments if (mandatoryDocs.includes(documentType)) { // Gut - wir erstellen ein mandatory document } else { // Pruefen ob die mandatory documents des Triggers vorhanden sind // (nur Hinweis, kein Block) } } return { allowed: violations.length === 0, violations, adjustments, checkedRules, } } /** * Convenience: Prueft aus einem DraftContext heraus. */ checkFromContext( documentType: ScopeDocumentType, context: DraftContext ): ConstraintCheckResult { // Reconstruct a minimal ScopeDecision from context const pseudoDecision: ScopeDecision = { id: 'projected', determinedLevel: context.decisions.level, scores: context.decisions.scores, triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({ rule: { id: t.id, label: t.label, description: '', conditionField: '', conditionOperator: 'EQUALS' as const, conditionValue: null, minimumLevel: context.decisions.level, mandatoryDocuments: [], dsfaRequired: false, legalReference: t.legalReference, }, matchedValue: null, explanation: '', })), requiredDocuments: context.decisions.requiredDocuments.map(d => ({ documentType: d.documentType, label: d.documentType, required: true, depth: d.depth, detailItems: d.detailItems, estimatedEffort: '', triggeredBy: [], })), riskFlags: context.constraints.riskFlags.map(f => ({ id: `rf-${f.title}`, severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL', title: f.title, description: '', recommendation: f.recommendation, })), gaps: [], nextActions: [], reasoning: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), } return this.check(documentType, pseudoDecision) } } /** Singleton-Instanz */ export const constraintEnforcer = new ConstraintEnforcer()