From cf60c39658d18936b38813a2549a452eec559d4b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 12 Mar 2026 09:39:31 +0100 Subject: [PATCH] fix(scope-engine): Normalize UPPERCASE trigger docs to lowercase ScopeDocumentType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical bug fix: mandatoryDocuments in Hard-Trigger-Rules used UPPERCASE names (VVT, TOM, DSE) that never matched lowercase ScopeDocumentType keys (vvt, tom, dsi). This meant no trigger documents were ever recognized as mandatory in buildDocumentScope(). - Add normalizeDocType() mapping function with alias support (DSE→dsi, LOESCHKONZEPT→lf, DSR_PROZESS→betroffenenrechte, etc.) - Fix buildDocumentScope() to use normalized doc types - Fix estimateEffort() to use lowercase keys matching ScopeDocumentType - Add 2 tests for UPPERCASE normalization and alias resolution Co-Authored-By: Claude Opus 4.6 --- .../__tests__/compliance-scope-engine.test.ts | 36 +++++++ .../lib/sdk/compliance-scope-engine.ts | 94 ++++++++++++++----- 2 files changed, 105 insertions(+), 25 deletions(-) diff --git a/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts b/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts index 33d9e40..872736f 100644 --- a/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts +++ b/admin-compliance/lib/sdk/__tests__/compliance-scope-engine.test.ts @@ -265,6 +265,42 @@ describe('buildDocumentScope', () => { }) }) + it('normalizes UPPERCASE trigger doc names to lowercase ScopeDocumentType', () => { + const t = trigger('HT-test', 'L2', { + category: 'test', + mandatoryDocuments: ['VVT', 'TOM', 'DSFA'], + }) + const docs = complianceScopeEngine.buildDocumentScope('L2', [t], []) + const vvt = docs.find((d: any) => d.documentType === 'vvt') + const tom = docs.find((d: any) => d.documentType === 'tom') + const dsfa = docs.find((d: any) => d.documentType === 'dsfa') + expect(vvt).toBeDefined() + expect(vvt!.requirement).toBe('mandatory') + expect(vvt!.triggeredBy).toContain('HT-test') + expect(tom).toBeDefined() + expect(tom!.requirement).toBe('mandatory') + expect(dsfa).toBeDefined() + expect(dsfa!.requirement).toBe('mandatory') + }) + + it('normalizes aliased doc names (DSE→dsi, LOESCHKONZEPT→lf)', () => { + const t = trigger('HT-alias', 'L2', { + category: 'test', + mandatoryDocuments: ['DSE', 'LOESCHKONZEPT', 'DSR_PROZESS'], + }) + const docs = complianceScopeEngine.buildDocumentScope('L2', [t], []) + const dsi = docs.find((d: any) => d.documentType === 'dsi') + const lf = docs.find((d: any) => d.documentType === 'lf') + const betroffenenrechte = docs.find((d: any) => d.documentType === 'betroffenenrechte') + expect(dsi).toBeDefined() + expect(dsi!.requirement).toBe('mandatory') + expect(dsi!.triggeredBy).toContain('HT-alias') + expect(lf).toBeDefined() + expect(lf!.requirement).toBe('mandatory') + expect(betroffenenrechte).toBeDefined() + expect(betroffenenrechte!.requirement).toBe('mandatory') + }) + it('documents sorted: mandatory first', () => { const decision = complianceScopeEngine.evaluate([ ans('data_art9', ['gesundheit']), diff --git a/admin-compliance/lib/sdk/compliance-scope-engine.ts b/admin-compliance/lib/sdk/compliance-scope-engine.ts index 7fb0a17..ef0b98b 100644 --- a/admin-compliance/lib/sdk/compliance-scope-engine.ts +++ b/admin-compliance/lib/sdk/compliance-scope-engine.ts @@ -1328,6 +1328,38 @@ export class ComplianceScopeEngine { return maxDepthLevel(levelFromScore, maxTriggerLevel) } + /** + * Normalisiert UPPERCASE Dokumenttyp-Bezeichner aus den Hard-Trigger-Rules + * auf die lowercase ScopeDocumentType-Schlüssel. + */ + private normalizeDocType(raw: string): ScopeDocumentType | null { + const mapping: Record = { + VVT: 'vvt', + TOM: 'tom', + DSFA: 'dsfa', + DSE: 'dsi', + AGB: 'vertragsmanagement', + AVV: 'av_vertrag', + COOKIE_BANNER: 'einwilligung', + EINWILLIGUNGEN: 'einwilligung', + TRANSFER_DOKU: 'daten_transfer', + AUDIT_CHECKLIST: 'audit_log', + VENDOR_MANAGEMENT: 'vertragsmanagement', + LOESCHKONZEPT: 'lf', + DSR_PROZESS: 'betroffenenrechte', + NOTFALLPLAN: 'notfallplan', + AI_ACT_DOKU: 'ai_act_doku', + WIDERRUFSBELEHRUNG: 'widerrufsbelehrung', + PREISANGABEN: 'preisangaben', + FERNABSATZ_INFO: 'fernabsatz_info', + STREITBEILEGUNG: 'streitbeilegung', + PRODUKTSICHERHEIT: 'produktsicherheit', + } + // Falls raw bereits ein gueltiger ScopeDocumentType ist + if (raw in DOCUMENT_SCOPE_MATRIX) return raw as ScopeDocumentType + return mapping[raw] ?? null + } + /** * Baut den Dokumenten-Scope basierend auf Level und Triggers */ @@ -1338,11 +1370,18 @@ export class ComplianceScopeEngine { ): RequiredDocument[] { const requiredDocs: RequiredDocument[] = [] const mandatoryFromTriggers = new Set() + // Mapping: normalisierter DocType → original Rule-Strings (fuer triggeredBy Lookup) + const triggerDocOrigins = new Map() - // Sammle mandatory docs aus Triggern + // Sammle mandatory docs aus Triggern (normalisiert) for (const trigger of triggers) { for (const doc of trigger.mandatoryDocuments) { - mandatoryFromTriggers.add(doc as ScopeDocumentType) + const normalized = this.normalizeDocType(doc) + if (normalized) { + mandatoryFromTriggers.add(normalized) + if (!triggerDocOrigins.has(normalized)) triggerDocOrigins.set(normalized, []) + triggerDocOrigins.get(normalized)!.push(doc) + } } } @@ -1352,6 +1391,7 @@ export class ComplianceScopeEngine { const isMandatoryFromTrigger = mandatoryFromTriggers.has(docType) if (requirement === 'mandatory' || isMandatoryFromTrigger) { + const originDocs = triggerDocOrigins.get(docType) ?? [] requiredDocs.push({ documentType: docType, label: DOCUMENT_TYPE_LABELS[docType], @@ -1361,7 +1401,7 @@ export class ComplianceScopeEngine { sdkStepUrl: DOCUMENT_SDK_STEP_MAP[docType], triggeredBy: isMandatoryFromTrigger ? triggers - .filter((t) => t.mandatoryDocuments.includes(docType as any)) + .filter((t) => t.mandatoryDocuments.some((d) => originDocs.includes(d))) .map((t) => t.ruleId) : [], }) @@ -1410,29 +1450,33 @@ export class ComplianceScopeEngine { * Schätzt den Aufwand für ein Dokument (in Stunden) */ private estimateEffort(docType: ScopeDocumentType): number { - const effortMap: Record = { - VVT: 8, - TOM: 12, - DSFA: 16, - AVV: 4, - DSE: 6, - EINWILLIGUNGEN: 6, - LOESCHKONZEPT: 10, - TRANSFER_DOKU: 8, - DSR_PROZESS: 8, - NOTFALLPLAN: 12, - COOKIE_BANNER: 4, - AGB: 6, - WIDERRUFSBELEHRUNG: 3, - PREISANGABEN: 2, - FERNABSATZ_INFO: 4, - STREITBEILEGUNG: 1, - PRODUKTSICHERHEIT: 8, - AI_ACT_DOKU: 12, - AUDIT_CHECKLIST: 8, - VENDOR_MANAGEMENT: 10, + const effortMap: Partial> = { + vvt: 8, + tom: 12, + dsfa: 16, + av_vertrag: 4, + dsi: 6, + einwilligung: 6, + lf: 10, + daten_transfer: 8, + betroffenenrechte: 8, + notfallplan: 12, + vertragsmanagement: 10, + audit_log: 8, + risikoanalyse: 6, + schulung: 4, + datenpannen: 6, + zertifizierung: 8, + datenschutzmanagement: 12, + iace_ce_assessment: 8, + widerrufsbelehrung: 3, + preisangaben: 2, + fernabsatz_info: 4, + streitbeilegung: 1, + produktsicherheit: 8, + ai_act_doku: 12, } - return effortMap[docType] || 6 + return effortMap[docType] ?? 6 } /**