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
This commit is contained in:
Kjell Tore Guttormsen 2026-05-04 06:10:31 +02:00
commit dc670f3208
2 changed files with 83 additions and 2 deletions

View file

@ -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 = '<div class="residual-pair">' +
'<div class="residual-pair__cell"' + attrBefore + '>' +
'<span class="residual-pair__cell-label">FØR TILTAK</span>' +
'<span class="residual-pair__cell-value">' + escapeHtml(labelOf(rp.before)) + '</span>' +
'<span class="residual-pair__cell-meta">Sannsynlighet × konsekvens</span>' +
'</div>' +
'<div class="residual-pair__arrow" aria-hidden="true"></div>' +
'<div class="residual-pair__cell"' + attrAfter + '>' +
'<span class="residual-pair__cell-label">ETTER TILTAK</span>' +
'<span class="residual-pair__cell-value">' + escapeHtml(labelOf(rp.after)) + '</span>' +
'<span class="residual-pair__cell-meta">Restrisiko</span>' +
'</div>' +
'</div>';
}
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) ----

View file

@ -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.