feat(ms-ai-architect): renderer B.2 ros adopt page-header + top-risks + recommendation-card

ros = REFERENCE STANDARD (mot Plugin Playground-run2/scenarios/ros-lier-kommune.html)

- parseMatrixRisk utvidet med _consumer-detection (ros når ## Top-risikoer eller ## Anbefaling), topRisks[] (max 5, fallback til threats sortert på severity-rank med alfabetisk tie-breaker), og recommendation (første avsnitt under ## Anbefaling)
- R15 regression: hasTopRisks/hasAnbefal-detection er ikke-invasiv; Dpia-fixturer har ingen av disse seksjonene → _consumer=null, topRisks=[], recommendation='' (alle felt forblir uendret for dpia-rendereren)
- renderRos wrapper renderPageShell med eyebrow=ROS; behold matrix 5x5 + radar 7-akser + threats; legg til top-risks-list, residual-pair og recommendation-card
- ros.md fixture utvidet med ## Top-risikoer (5 trusler), Restrisiko: 4x3 to 2x2, og ## Anbefaling
- RESTRISIKO key-stat utledes når residualPair finnes (samme monster som Dpia og Security)

Sesjon 6 (v1.10.0) gjor en samlet README/CLAUDE/CHANGELOG-oppdatering for hele v1.10.0-loypet.

[skip-docs]
This commit is contained in:
Kjell Tore Guttormsen 2026-05-04 06:33:06 +02:00
commit 20717102aa
2 changed files with 147 additions and 5 deletions

View file

@ -2632,10 +2632,65 @@
}
}
}
// _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 } };
// _consumer-diskriminator (R15): Settes til 'ros' når Ros-spesifikk
// markdown oppdages (## Top-risikoer eller ## Anbefaling). Dpia-fixturer
// har ingen av disse seksjonene → forblir null.
const hasTopRisks = /^##\s*Top.?risikoer\b/im.test(md);
const hasAnbefal = /^##\s*Anbefaling\b/im.test(md);
const consumer = (hasTopRisks || hasAnbefal) ? 'ros' : null;
// topRisks (R15, Ros-only): parse explicit ## Top-risikoer table, eller
// fallback til threats sortert på severity-rank (kritisk>høy>medium>lav).
// Felt er optional og brukes ikke av renderDpia. Tie-breaker: alfabetisk
// på description.
const sevRank = function (s) {
const v = String(s || '').toLowerCase();
if (/crit|kritisk/.test(v)) return 4;
if (/høy|high/.test(v)) return 3;
if (/medium|moderat/.test(v)) return 2;
if (/lav|low/.test(v)) return 1;
return 0;
};
let topRisks = [];
if (consumer === 'ros') {
const trTbl = parseTable(md, /##\s*Top.?risikoer/i);
if (trTbl && trTbl.rows.length) {
const idKey = trTbl.headers[0];
const descKey = trTbl.headers.find(function (h) { return /trussel|risiko|description|beskrivelse/i.test(h); }) || trTbl.headers[1];
const scKey = trTbl.headers.find(function (h) { return /score/i.test(h); });
const sevKey = trTbl.headers.find(function (h) { return /severity|alvorlighet|nivå/i.test(h); });
topRisks = trTbl.rows.map(function (row) {
return {
id: row[idKey] || '',
description: row[descKey] || row[idKey] || '',
score: scKey ? intOrZero(row[scKey] || '0') : 0,
severity: (sevKey && (row[sevKey] || '').toLowerCase().trim()) || ''
};
}).slice(0, 5);
} else if (threats.length) {
topRisks = threats.slice().sort(function (a, b) {
const r = sevRank(b.severity) - sevRank(a.severity);
if (r !== 0) return r;
return String(a.description || '').localeCompare(String(b.description || ''));
}).slice(0, 5).map(function (t) {
return { id: t.id, description: t.description, score: 0, severity: t.severity };
});
}
}
// recommendation (Ros-only): første avsnitt under ## Anbefaling.
let recommendation = '';
if (consumer === 'ros' && hasAnbefal) {
const m = md.match(/^##\s*Anbefaling\s*\n+([\s\S]*?)(?=\n##\s|\n$|$)/im);
if (m) recommendation = m[1].replace(/\n+$/, '').trim();
}
return { ok: true, data: {
matrix_cells: matrix_cells,
threats: threats,
radar_axes: radar_axes,
residualPair: residualPair,
topRisks: topRisks,
recommendation: recommendation,
_consumer: consumer
} };
}
function parseMatrixRisk6x5(md) {
@ -3513,9 +3568,80 @@
}
function renderRos(data, slot) {
const sevForScore = function (s) {
const n = Number(s) || 0;
if (n >= 16) return 'critical';
if (n >= 9) return 'high';
if (n >= 4) return 'medium';
return 'low';
};
const matrixHtml = renderMatrixHtml(data, 5);
const radarHtml = renderRadarSvg(data.radar_axes || []);
slot.innerHTML = matrixHtml + radarHtml + renderThreatsTable(data.threats);
// C4 top-risks-list (max 5).
const topRisks = (data.topRisks || []).slice(0, 5);
const topRisksHtml = topRisks.length ? '<section class="top-risks">' +
'<h4 class="top-risks__heading">Top-risikoer</h4>' +
topRisks.map(function (r, i) {
const sev = r.severity || sevForScore(r.score);
const scoreLabel = r.score ? String(r.score) : (r.severity || '—').toUpperCase();
return '<div class="top-risk" data-severity="' + escapeAttr(sev) + '">' +
'<span class="top-risk__rank">' + (i + 1) + '</span>' +
'<span class="top-risk__desc">' + escapeHtml(r.description || '') + '</span>' +
'<span class="top-risk__score">' + escapeHtml(scoreLabel) + '</span>' +
'</div>';
}).join('') + '</section>' : '';
// B6 residual-pair (gjenbruker mønster fra Dpia / Security).
const rp = data.residualPair;
let residualHtml = '';
if (rp && rp.before && rp.after) {
const labelOf = function (cell) {
if (cell.score != null) return cell.prob + '×' + cell.cons + ' = ' + cell.score;
return cell.label || '—';
};
const sevBefore = rp.before.score != null ? sevForScore(rp.before.score) : '';
const sevAfter = rp.after.score != null ? sevForScore(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>';
}
// D5 recommendation-card.
const rec = data.recommendation || '';
const recommendationHtml = rec ? '<aside class="recommendation-card">' +
'<span class="recommendation-card__label">Anbefaling</span>' +
'<p class="recommendation-card__body">' + escapeHtml(rec).replace(/\n/g, '<br>') + '</p>' +
'</aside>' : '';
const threatsHtml = renderThreatsTable(data.threats);
const body = matrixHtml + radarHtml + topRisksHtml + residualHtml + threatsHtml + recommendationHtml;
// Utvid matrix-risk-keyStats med RESTRISIKO når residualPair finnes
// (samme mønster som renderDpia).
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: 'ROS',
title: data.title || 'ROS-analyse (5×5)',
lede: data.lede || 'Risiko- og sårbarhetsanalyse iht. NS 5814 / ISO 31000 med AI-trusselbibliotek.',
verdict: data.verdict || inferVerdict(data, 'matrix-risk'),
keyStats: stats
}, body);
}
function renderReview(data, slot) {

View file

@ -48,6 +48,22 @@ Metodikk: NS 5814 / ISO 31000 + AI-trusselbibliotek
| M-104 | Multi-region failover testet | done | Drift |
| M-105 | Batching-logikk implementert | in-progress | Tech Lead |
## Top-risikoer
| ID | Trussel | Score | Severity |
|----|---------|-------|----------|
| T-101 | Modell-drift over tid | 12 | high |
| T-105 | Saksbehandlings-overbelastning | 12 | high |
| T-107 | Misbruk av AI-forklaring som bevis | 12 | high |
| T-108 | Kjedevirkning ved feil i objektregister | 10 | high |
| T-103 | Bias mot småbiler/MC | 9 | medium |
Restrisiko: 4×3 → 2×2
## Anbefaling
ROS godkjent av seksjonsleder 2026-04-25 forutsatt at M-103 (robusthetstest) ferdigstilles innen 2026-06-15. Re-evaluering ved hver modell-release eller ved endring i sak-volum > 20%.
## Konklusjon
Restrisiko etter tiltak: medium. ROS godkjent av seksjonsleder 2026-04-25.