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:
parent
bbe7971d01
commit
20717102aa
2 changed files with 147 additions and 5 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue