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 dc01074..00643d7 100644
--- a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html
+++ b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html
@@ -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 ? 'Top-risikoer
' +
+ topRisks.map(function (r, i) {
+ const sev = r.severity || sevForScore(r.score);
+ const scoreLabel = r.score ? String(r.score) : (r.severity || '—').toUpperCase();
+ return '