From dc670f3208a9ad9dc810c3cdf622ef2f1e31a7b2 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Mon, 4 May 2026 06:10:31 +0200 Subject: [PATCH] feat(ms-ai-architect): renderer A.6 dpia adopt page-header + residual-pair - parseMatrixRisk utvidet med residualPair-felt + _consumer-diskriminator (R15) - Stotter "Restrisiko: AxB > CxD"-syntax (numerisk) og "Restrisiko: label > label" (fallback) - Sesjon 4 vil sette _consumer='ros' nar Ros-spesifikk markdown oppdages - renderDpia: matrix + residual-pair (B6) + threats-table, wrapped i renderPageShell (eyebrow DPIA) - KeyStats utvidet med RESTRISIKO-stat nar residualPair eksisterer (modifier high hvis score>=9) - Fixture dpia.md utvidet med "Restrisiko: 4x3 -> 2x2"-linje under Konklusjon --- .../ms-ai-architect-playground.html | 83 ++++++++++++++++++- .../playground/test-fixtures/dpia.md | 2 + 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html index 3431e91..8c5a93e 100644 --- a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html +++ b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html @@ -2579,7 +2579,35 @@ score: intOrZero(row[scKey] || '0') }; }) : null; - return { ok: true, data: { matrix_cells: matrix_cells, threats: threats, radar_axes: radar_axes } }; + // Restrisiko / residual-pair (Sesjon 3 — Dpia, men felt er optional og + // gjelder også fremtidig Ros-bruk per R15). Markdown-syntaks: + // Restrisiko: 4×3 → 2×2 (numerisk before/after med score) + // Restrisiko: medium → lav (label-only fallback) + let residualPair = null; + const rrMatch = md.match(/^Restrisiko\s*:\s*([^\n]+)$/im); + if (rrMatch) { + const txt = rrMatch[1]; + const num = /(\d+)\s*[×x*]\s*(\d+)\s*(?:[-=]?[>→]|->)\s*(\d+)\s*[×x*]\s*(\d+)/.exec(txt); + if (num) { + const b1 = +num[1], b2 = +num[2], a1 = +num[3], a2 = +num[4]; + residualPair = { + before: { prob: b1, cons: b2, score: b1 * b2 }, + after: { prob: a1, cons: a2, score: a1 * a2 } + }; + } else { + const parts = txt.split(/(?:[-=]?[>→]|->)/); + if (parts.length === 2) { + residualPair = { + before: { label: parts[0].trim() }, + after: { label: parts[1].trim() } + }; + } + } + } + // _consumer-diskriminator (R15 forward-compat): Sesjon 4 setter denne til + // 'ros' når den oppdager Ros-spesifikk markdown (## Top-risikoer e.l.). + // Sesjon 3 lar den være null så Dpia-rendereren ikke trenger å skille. + return { ok: true, data: { matrix_cells: matrix_cells, threats: threats, radar_axes: radar_axes, residualPair: residualPair, _consumer: null } }; } function parseMatrixRisk6x5(md) { @@ -3238,7 +3266,58 @@ } function renderDpia(data, slot) { - slot.innerHTML = renderMatrixHtml(data, 5) + renderThreatsTable(data.threats); + const matrixHtml = renderMatrixHtml(data, 5); + const threatsHtml = renderThreatsTable(data.threats); + const rp = data.residualPair; + let residualHtml = ''; + if (rp && rp.before && rp.after) { + const sevFor = function (s) { + if (s == null) return ''; + if (s >= 16) return 'critical'; + if (s >= 9) return 'high'; + if (s >= 4) return 'medium'; + return 'low'; + }; + const labelOf = function (cell) { + if (cell.score != null) return cell.prob + '×' + cell.cons + ' = ' + cell.score; + return cell.label || '—'; + }; + const sevBefore = rp.before.score != null ? sevFor(rp.before.score) : ''; + const sevAfter = rp.after.score != null ? sevFor(rp.after.score) : ''; + const attrBefore = sevBefore ? ' data-severity="' + sevBefore + '"' : ''; + const attrAfter = sevAfter ? ' data-severity="' + sevAfter + '"' : ''; + residualHtml = '
' + + '
' + + 'FØR TILTAK' + + '' + escapeHtml(labelOf(rp.before)) + '' + + 'Sannsynlighet × konsekvens' + + '
' + + '' + + '
' + + 'ETTER TILTAK' + + '' + escapeHtml(labelOf(rp.after)) + '' + + 'Restrisiko' + + '
' + + '
'; + } + const body = matrixHtml + residualHtml + threatsHtml; + // Utvid matrix-risk-keyStats med RESTRISIKO når residualPair finnes. + const baseStats = inferKeyStats(data, 'matrix-risk'); + const stats = (data.keyStats || (rp && rp.after + ? baseStats.concat([{ + label: 'RESTRISIKO', + value: rp.after.score != null ? String(rp.after.score) : (rp.after.label || '—'), + modifier: rp.after.score != null && rp.after.score >= 9 ? 'high' : 'low', + hint: 'etter tiltak' + }]) + : baseStats)); + slot.innerHTML = renderPageShell({ + eyebrow: 'DPIA', + title: data.title || 'DPIA / Personvernkonsekvensvurdering', + lede: data.lede || 'Risikomatrise og tiltak iht. Datatilsynets metodikk og GDPR Art. 35.', + verdict: data.verdict || inferVerdict(data, 'matrix-risk'), + keyStats: stats + }, body); } // ---- Sub-batch B: Security (3) ---- diff --git a/plugins/ms-ai-architect/playground/test-fixtures/dpia.md b/plugins/ms-ai-architect/playground/test-fixtures/dpia.md index dac56e4..8ee9548 100644 --- a/plugins/ms-ai-architect/playground/test-fixtures/dpia.md +++ b/plugins/ms-ai-architect/playground/test-fixtures/dpia.md @@ -38,4 +38,6 @@ Metodikk: Datatilsynets veileder + ISO/IEC 29134 ## Konklusjon +Restrisiko: 4×3 → 2×2 + Restrisiko etter tiltak: medium-lav. DPIA godkjent av Datatilsynet 2026-04-22.