Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine that can generate, validate, and refine compliance documents within Scope Engine constraints. Includes intent classifier, state projector, constraint enforcer, SOUL templates, Go backend endpoints, and React UI components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
222 lines
7.5 KiB
TypeScript
222 lines
7.5 KiB
TypeScript
/**
|
|
* 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()
|