import type { ComplianceDepthLevel, ComplianceScores, ScopeProfilingAnswer, ScopeDecision, HardTriggerRule, TriggeredHardTrigger, RequiredDocument, RiskFlag, ScopeGap, NextAction, ScopeReasoning, } from './compliance-scope-types' import type { CompanyProfile, MachineBuilderProfile } from './types' import { getDepthLevelNumeric, maxDepthLevel, createEmptyScopeDecision, } from './compliance-scope-types' // Re-export data constants so existing barrel imports keep working export { QUESTION_SCORE_WEIGHTS, ANSWER_MULTIPLIERS } from './compliance-scope-data' export { HARD_TRIGGER_RULES } from './compliance-scope-triggers' import { QUESTION_SCORE_WEIGHTS, ANSWER_MULTIPLIERS } from './compliance-scope-data' import { HARD_TRIGGER_RULES } from './compliance-scope-triggers' import { parseEmployeeCount, getLevelFromScore, getMaxTriggerLevel, buildDocumentScope, evaluateRiskFlags, calculateGaps, buildNextActions, buildReasoning, } from './compliance-scope-documents' // ============================================================================ // COMPLIANCE SCOPE ENGINE // ============================================================================ export class ComplianceScopeEngine { /** * Haupteinstiegspunkt: Evaluiert alle Profiling-Antworten und produziert eine ScopeDecision * Optional: companyProfile fuer machineBuilder-basierte IACE Triggers */ evaluate(answers: ScopeProfilingAnswer[], companyProfile?: CompanyProfile | null): ScopeDecision { const decision = createEmptyScopeDecision() // 1. Scores berechnen decision.scores = this.calculateScores(answers) // 2. Hard Triggers prüfen (inkl. IACE machineBuilder Triggers) decision.triggeredHardTriggers = this.evaluateHardTriggers(answers, companyProfile) // 3. Finales Level bestimmen decision.determinedLevel = this.determineLevel( decision.scores, decision.triggeredHardTriggers ) // 4. Dokumenten-Scope aufbauen decision.requiredDocuments = this.buildDocumentScope( decision.determinedLevel, decision.triggeredHardTriggers, answers ) // 5. Risk Flags ermitteln decision.riskFlags = this.evaluateRiskFlags(answers, decision.determinedLevel) // 6. Gaps berechnen decision.gaps = this.calculateGaps(answers, decision.determinedLevel) // 7. Next Actions ableiten decision.nextActions = this.buildNextActions( decision.requiredDocuments, decision.gaps ) // 8. Reasoning (Audit Trail) aufbauen decision.reasoning = this.buildReasoning( decision.scores, decision.triggeredHardTriggers, decision.determinedLevel, decision.requiredDocuments ) decision.evaluatedAt = new Date().toISOString() return decision } /** * Berechnet Risk-, Complexity- und Assurance-Scores aus den Profiling-Antworten */ calculateScores(answers: ScopeProfilingAnswer[]): ComplianceScores { let riskSum = 0 let complexitySum = 0 let assuranceSum = 0 let riskWeightSum = 0 let complexityWeightSum = 0 let assuranceWeightSum = 0 for (const answer of answers) { const weights = QUESTION_SCORE_WEIGHTS[answer.questionId] if (!weights) continue const multiplier = this.getAnswerMultiplier(answer) riskSum += weights.risk * multiplier complexitySum += weights.complexity * multiplier assuranceSum += weights.assurance * multiplier riskWeightSum += weights.risk complexityWeightSum += weights.complexity assuranceWeightSum += weights.assurance } const riskScore = riskWeightSum > 0 ? (riskSum / riskWeightSum) * 10 : 0 const complexityScore = complexityWeightSum > 0 ? (complexitySum / complexityWeightSum) * 10 : 0 const assuranceScore = assuranceWeightSum > 0 ? (assuranceSum / assuranceWeightSum) * 10 : 0 const composite = riskScore * 0.4 + complexityScore * 0.3 + assuranceScore * 0.3 return { risk_score: Math.round(riskScore * 10) / 10, complexity_score: Math.round(complexityScore * 10) / 10, assurance_need: Math.round(assuranceScore * 10) / 10, composite_score: Math.round(composite * 10) / 10, } } /** * Bestimmt den Multiplikator für eine Antwort (0.0 - 1.0) */ private getAnswerMultiplier(answer: ScopeProfilingAnswer): number { const { questionId, value } = answer // Boolean if (typeof value === 'boolean') { return value ? 1.0 : 0.0 } // Number if (typeof value === 'number') { return this.normalizeNumericAnswer(questionId, value) } // Single choice if (typeof value === 'string') { const multipliers = ANSWER_MULTIPLIERS[questionId] if (multipliers && multipliers[value] !== undefined) { return multipliers[value] } return 0.5 // Fallback } // Multi choice if (Array.isArray(value)) { if (value.length === 0) return 0.0 return Math.min(value.length / 5, 1.0) } return 0.0 } /** * Normalisiert numerische Antworten */ private normalizeNumericAnswer(_questionId: string, value: number): number { if (value <= 0) return 0.0 if (value >= 1000) return 1.0 return Math.log10(value + 1) / 3 } /** * Evaluiert Hard Trigger Rules * Optional: companyProfile fuer machineBuilder-basierte IACE Triggers */ evaluateHardTriggers(answers: ScopeProfilingAnswer[], companyProfile?: CompanyProfile | null): TriggeredHardTrigger[] { const triggered: TriggeredHardTrigger[] = [] const answerMap = new Map(answers.map((a) => [a.questionId, a.value])) for (const rule of HARD_TRIGGER_RULES) { const isTriggered = this.checkTriggerCondition(rule, answerMap, answers, companyProfile) if (isTriggered) { triggered.push({ ruleId: rule.id, category: rule.category, description: rule.description, legalReference: rule.legalReference, minimumLevel: rule.minimumLevel, requiresDSFA: rule.requiresDSFA, mandatoryDocuments: rule.mandatoryDocuments, }) } } return triggered } /** * Liest einen Wert aus dem MachineBuilderProfile anhand eines Feldnamens */ private getMachineBuilderValue(mb: MachineBuilderProfile, field: string): unknown { return (mb as Record)[field] } /** * Prüft, ob eine Trigger-Regel erfüllt ist */ checkTriggerCondition( rule: HardTriggerRule, answerMap: Map, answers: ScopeProfilingAnswer[], companyProfile?: CompanyProfile | null, ): boolean { // IACE machineBuilder-basierte Triggers if (rule.questionId.startsWith('machineBuilder.')) { const mb = companyProfile?.machineBuilder if (!mb) return false const fieldName = rule.questionId.replace('machineBuilder.', '') const fieldValue = this.getMachineBuilderValue(mb, fieldName) if (fieldValue === undefined) return false let baseCondition = false switch (rule.condition) { case 'EQUALS': baseCondition = fieldValue === rule.conditionValue break case 'CONTAINS': if (Array.isArray(fieldValue)) { baseCondition = fieldValue.includes(rule.conditionValue) } break default: baseCondition = fieldValue === rule.conditionValue } if (!baseCondition) return false const combine = (rule as any).combineWithMachineBuilder if (combine) { const combineVal = this.getMachineBuilderValue(mb, combine.field) if (combine.value !== undefined && combineVal !== combine.value) return false if (combine.includes !== undefined) { if (!Array.isArray(combineVal) || !combineVal.includes(combine.includes)) return false } } return true } // Standard answer-based triggers const value = answerMap.get(rule.questionId) if (value === undefined) return false let baseCondition = false switch (rule.condition) { case 'EQUALS': baseCondition = value === rule.conditionValue break case 'CONTAINS': if (Array.isArray(value)) { baseCondition = value.includes(rule.conditionValue) } else if (typeof value === 'string') { baseCondition = value.includes(rule.conditionValue) } break case 'IN': if (Array.isArray(rule.conditionValue)) { baseCondition = rule.conditionValue.includes(value) } break case 'GREATER_THAN': if (typeof value === 'number' && typeof rule.conditionValue === 'number') { baseCondition = value > rule.conditionValue } else if (typeof value === 'string') { const parsed = parseEmployeeCount(value) baseCondition = parsed > (rule.conditionValue as number) } break case 'NOT_EQUALS': baseCondition = value !== rule.conditionValue break } if (!baseCondition) return false // Exclude-Bedingung if (rule.excludeWhen) { const exVal = answerMap.get(rule.excludeWhen.questionId) if (Array.isArray(rule.excludeWhen.value) ? rule.excludeWhen.value.includes(exVal) : exVal === rule.excludeWhen.value) { return false } } // Require-Bedingung if (rule.requireWhen) { const reqVal = answerMap.get(rule.requireWhen.questionId) if (Array.isArray(rule.requireWhen.value) ? !rule.requireWhen.value.includes(reqVal) : reqVal !== rule.requireWhen.value) { return false } } // Combined checks if (rule.combineWithArt9) { const art9 = answerMap.get('data_art9') if (!art9 || (Array.isArray(art9) && art9.length === 0)) return false } if (rule.combineWithMinors) { const minors = answerMap.get('data_minors') if (minors !== true) return false } if (rule.combineWithAI) { const ai = answerMap.get('proc_ai_usage') if (!ai || (Array.isArray(ai) && (ai.length === 0 || ai.includes('keine')))) { return false } } if (rule.combineWithEmployeeMonitoring) { const empMon = answerMap.get('proc_employee_monitoring') if (empMon !== true) return false } if (rule.combineWithADM) { const adm = answerMap.get('proc_adm_scoring') if (adm !== true) return false } return true } /** * Bestimmt das finale Compliance-Level basierend auf Scores und Triggers */ determineLevel( scores: ComplianceScores, triggers: TriggeredHardTrigger[] ): ComplianceDepthLevel { const levelFromScore = getLevelFromScore(scores.composite_score) const maxTriggerLevel = getMaxTriggerLevel(triggers) return maxDepthLevel(levelFromScore, maxTriggerLevel) } // Delegate to extracted helpers (keep public API surface identical) buildDocumentScope( level: ComplianceDepthLevel, triggers: TriggeredHardTrigger[], answers: ScopeProfilingAnswer[] ): RequiredDocument[] { return buildDocumentScope(level, triggers, answers) } evaluateRiskFlags( answers: ScopeProfilingAnswer[], level: ComplianceDepthLevel ): RiskFlag[] { return evaluateRiskFlags(answers, level, (rule, answerMap, ans) => this.checkTriggerCondition(rule, answerMap, ans)) } calculateGaps( answers: ScopeProfilingAnswer[], level: ComplianceDepthLevel ): ScopeGap[] { return calculateGaps(answers, level) } buildNextActions( requiredDocuments: RequiredDocument[], gaps: ScopeGap[] ): NextAction[] { return buildNextActions(requiredDocuments, gaps) } buildReasoning( scores: ComplianceScores, triggers: TriggeredHardTrigger[], level: ComplianceDepthLevel, docs: RequiredDocument[] ): ScopeReasoning[] { return buildReasoning(scores, triggers, level, docs) } } // ============================================================================ // SINGLETON EXPORT // ============================================================================ export const complianceScopeEngine = new ComplianceScopeEngine()