a28db8f8f0
Eliminate the pre-existing TS errors that were masked by next.config.js `typescript.ignoreBuildErrors: true`, then turn the flag OFF so the compiler is a real safety net for future changes. `next build` and `tsc --noEmit` now pass with 0 errors. The errors were not cosmetic — several exposed real latent bugs hidden by the flag, e.g. the drafting-engine ConstraintEnforcer read non-existent fields (`t.rule.dsfaRequired`, `d.required`, `r.title`), so its DSFA hard gate and risk-flag checks were silently no-ops; scopeDefaults read snake_case CompanyProfile fields that never matched the camelCase type (generator defaults never populated). Both fixed by aligning code to the current types. Highlights: - Vitest globals: add vitest-globals.d.ts (config already had globals:true) so the test files type-check; exclude Playwright specs from vitest. - Add a minimal ambient `pg` module declaration (no @types/pg installed). - Fix Next 15 route handlers to await Promise params. - Reconcile drifted types across loeschfristen, compliance-scope, document- generator, drafting-engine, vendor-compliance, agent and more. Pre-existing (NOT caused here, proven by stashing the diff): 3 vitest logic tests still fail — getNextStep (2) and buildDocumentScope priority (1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
215 lines
7.4 KiB
TypeScript
215 lines
7.4 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_CORE, 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.requirement === 'mandatory'
|
|
)
|
|
const scopeReq = DOCUMENT_SCOPE_MATRIX_CORE[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.requiresDSFA
|
|
)
|
|
|
|
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.requiresDSFA
|
|
)
|
|
const dsfaInRequired = decision.requiredDocuments.some(
|
|
d => d.documentType === 'dsfa' && d.requirement === 'mandatory'
|
|
)
|
|
|
|
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.message).join(', ')}`
|
|
)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Rule 5: Hard-Trigger Consistency
|
|
// -----------------------------------------------------------------------
|
|
checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY')
|
|
for (const trigger of decision.triggeredHardTriggers) {
|
|
const mandatoryDocs = trigger.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 => ({
|
|
ruleId: t.id,
|
|
category: '',
|
|
description: t.label,
|
|
legalReference: t.legalReference,
|
|
minimumLevel: context.decisions.level,
|
|
requiresDSFA: false,
|
|
mandatoryDocuments: [],
|
|
matchedValue: null,
|
|
explanation: '',
|
|
})),
|
|
requiredDocuments: context.decisions.requiredDocuments.map(d => ({
|
|
documentType: d.documentType,
|
|
label: d.documentType,
|
|
requirement: 'mandatory' as const,
|
|
priority: 'medium' as const,
|
|
estimatedEffort: 0,
|
|
triggeredBy: [],
|
|
})),
|
|
riskFlags: context.constraints.riskFlags.map(f => ({
|
|
severity: f.severity,
|
|
category: '',
|
|
message: f.title,
|
|
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()
|