refactor(ms-ai-architect): playground v1.14.0 sesjon 3 — risk-rapporter til DS-konvensjon

- renderDpia: matrix wrappet i .card med h2
- renderSecurity: ros-layout (matrix+radar), small-multiples-section, top-risks som <ol> i .card
- renderRos: speil renderSecurity (5x5) + summary-grid for top-risks+recommendation
- renderFindingsBlock: fjern .report-meta-band-aid, bruk findings-section + findings__items--standalone
- Legg til .ros-layout, .summary-grid, .findings-section, .small-multiples-section i lokal CSS
- Fjern .top-risks fra defensive layout-block
- test-playground-v3.sh: bytt .findings__list → .findings__items i DS-klasse-asserts
- Style-blokk: 182 → 188 linjer (mål ≤195 nådd)

Refs V1.14.0-AUDIT.local.md sub-batch B + helper-section.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-08 20:13:00 +02:00
commit d117bea219
2 changed files with 109 additions and 63 deletions

View file

@ -185,6 +185,15 @@
ikke skubber innhold utenfor viewport. */
.recommendation-card__body { overflow-wrap: anywhere; word-break: break-word; }
/* v1.14.0 sesjon 3: layout-utilities for risk-rapporter (renderDpia, renderSecurity, renderRos).
Speiler Anthropic-ref ros-lier-scenario. Hoist til DS i v1.15.0 hvis andre plugins trenger samme. */
.ros-layout { display: grid; grid-template-columns: 1fr 320px; gap: var(--space-6); align-items: start; margin-block: var(--space-5); }
.summary-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: var(--space-5); align-items: start; margin-block: var(--space-5); }
@media (max-width: 980px) { .ros-layout, .summary-grid { grid-template-columns: 1fr; } }
.findings-section, .small-multiples-section { margin-block: var(--space-5); }
.findings-section > h3, .small-multiples-section > h3 { margin: 0 0 var(--space-3); font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); }
.findings__items--standalone { list-style: none; margin: 0; padding: 0; border: 1px solid var(--color-border-subtle); border-radius: var(--radius-lg); background: var(--color-surface); overflow: hidden; }
/* v1.13.1 fix (B7): .report-meta er ikke definert i vendored DS — vi bruker
<section class="report-meta"> som outer-wrapper i flere rendrere. Browser-
defaults for <dl><dd><ul> gir uforutsigbare indents (Forpliktelser-bullet-
@ -209,12 +218,11 @@
/* v1.13.1 fix (B12, B13, B15): defensive block-layout for sibling-rapport-
seksjoner som kan ha overflow-issues mot foregående grid-elementer
(.small-multiples, .kanban-board, .mat-ladder). Sikrer eksplisitt
block-flow + clear så de ikke leker grid-celler eller flyter. */
.top-risks,
block-flow + clear så de ikke leker grid-celler eller flyter.
v1.14.0 sesjon 3: .top-risks fjernet (ligger nå i .card med riktig DS block-flow). */
.suppressed-panel,
.phase-detail,
.aiact-timeline,
.small-multiples + .top-risks,
.kanban-board + .report-meta,
.mat-ladder + .phase-detail,
.phase-detail + .phase-detail { display: block; clear: both; width: 100%; }
@ -3327,9 +3335,11 @@
}
function renderFindingsBlock(findings, label) {
// v1.13.0 fix (B1): DS' .findings er grid 360px+1fr (list+detail-panel).
// Vi har bare list-kolonnen; bruk <section class="report-meta"> som outer
// for å beholde findings__list-strukturen uten å bli klemt til 360px.
// v1.14.0 sesjon 3: refaktorert fra .report-meta-band-aid til standalone
// findings-section. DS' .findings er grid 360px+1fr (list+detail-panel) —
// siden vi ikke har detail-panel, bruker vi en standalone container med
// .findings__items--standalone-modifier som styles lokalt.
if (!findings || !findings.length) return '';
const items = findings.map(function (f) {
return '<li class="findings__item">' +
'<span class="findings__item-severity-dot" data-severity="' + escapeAttr(f.severity || 'info') + '"></span>' +
@ -3338,14 +3348,9 @@
'<span class="findings__item-meta">Lokasjon: ' + escapeHtml(f.location || '—') + ' · Severity: ' + escapeHtml(f.severity || '—') + '</span>' +
'</li>';
}).join('');
return '<section class="report-meta">' +
'<h4>' + escapeHtml(label) + '</h4>' +
'<div class="findings__list">' +
'<div class="findings__group">' +
'<div class="findings__group-header"><span>' + escapeHtml(label) + '</span><span>' + findings.length + '</span></div>' +
'<ul class="findings__items">' + items + '</ul>' +
'</div>' +
'</div>' +
return '<section class="findings-section">' +
'<h3>' + escapeHtml(label) + '</h3>' +
'<ul class="findings__items findings__items--standalone">' + items + '</ul>' +
'</section>';
}
@ -3671,7 +3676,12 @@
}
function renderDpia(data, slot) {
const matrixHtml = renderMatrixHtml(data, 5);
// v1.14.0 sesjon 3: matrix wrappet i .card med h2 for visuell separasjon
// fra residual-pair og threats-tabell (per Anthropic-ref ros-lier-pattern).
const matrixHtml = '<div class="card" style="padding: var(--space-6)">' +
'<h2>5×5 Risikomatrise</h2>' +
renderMatrixHtml(data, 5) +
'</div>';
const threatsHtml = renderThreatsTable(data.threats);
const rp = data.residualPair;
let residualHtml = '';
@ -3735,34 +3745,51 @@
if (n >= 4) return 'medium';
return 'low';
};
const matrixHtml = renderMatrixHtml(data, 6);
const radarHtml = renderRadarSvg(data.dimensions || []);
// C7 small-multiples per OWASP-kategori (driver: categoryGrades).
// v1.14.0 sesjon 3: matrix + radar i .ros-layout (1fr 320px) per Anthropic-ref
// ros-lier-scenario. Matrix står i .card, radar i <aside class="card">.
const layoutHtml = '<div class="ros-layout">' +
'<div class="card" style="padding: var(--space-6)">' +
'<h2>6×5 Sikkerhetsmatrise</h2>' +
renderMatrixHtml(data, 6) +
'</div>' +
'<aside class="card">' +
'<h3>Dimensjons-radar</h3>' +
renderRadarSvg(data.dimensions || []) +
'</aside>' +
'</div>';
// C7 small-multiples per OWASP-kategori (driver: categoryGrades) — egen seksjon
// i full bredde under layout.
const cats = data.categoryGrades || [];
const smallMultiplesHtml = cats.length ? '<div class="small-multiples">' + cats.map(function (c) {
const grade = c.grade || '';
const fillPct = Math.max(0, Math.min(100, ((Number(c.score) || 0) / 5) * 100));
return '<div class="sm-card">' +
'<div class="sm-card__header">' +
'<span class="sm-card__name">' + escapeHtml(c.name || '') + '</span>' +
'<span class="sm-card__grade" data-grade="' + escapeAttr(grade) + '">' + escapeHtml(grade) + '</span>' +
'</div>' +
'<div class="sm-card__bar"><div class="sm-card__bar-fill" style="width: ' + fillPct.toFixed(0) + '%"></div></div>' +
'<span class="sm-card__status">Score ' + escapeHtml(String(c.score || 0)) + ' / 5</span>' +
'</div>';
}).join('') + '</div>' : '';
// 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);
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(String(r.score || 0)) + '</span>' +
const smallMultiplesHtml = cats.length ? '<section class="small-multiples-section">' +
'<h3>Posture per OWASP-kategori</h3>' +
'<div class="small-multiples">' + cats.map(function (c) {
const grade = c.grade || '';
const fillPct = Math.max(0, Math.min(100, ((Number(c.score) || 0) / 5) * 100));
return '<div class="sm-card">' +
'<div class="sm-card__header">' +
'<span class="sm-card__name">' + escapeHtml(c.name || '') + '</span>' +
'<span class="sm-card__grade" data-grade="' + escapeAttr(grade) + '">' + escapeHtml(grade) + '</span>' +
'</div>' +
'<div class="sm-card__bar"><div class="sm-card__bar-fill" style="width: ' + fillPct.toFixed(0) + '%"></div></div>' +
'<span class="sm-card__status">Score ' + escapeHtml(String(c.score || 0)) + ' / 5</span>' +
'</div>';
}).join('') + '</section>' : '';
}).join('') + '</div>' +
'</section>' : '';
// C4 top-risks-list (max 5) — som <ol class="top-risks"> inne i .card.
const topRisks = (data.topRisks || []).slice(0, 5);
const topRisksHtml = topRisks.length ? '<div class="card">' +
'<h2>Topp-risikoer</h2>' +
'<ol class="top-risks">' +
topRisks.map(function (r, i) {
const sev = r.severity || sevForScore(r.score);
return '<li 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(String(r.score || 0)) + '</span>' +
'</li>';
}).join('') +
'</ol>' +
'</div>' : '';
// B6 residual-pair (når data.residualPair finnes).
const rp = data.residualPair;
let residualHtml = '';
@ -3790,7 +3817,7 @@
'</div>';
}
const findingsHtml = renderFindingsBlock(data.findings || [], 'Sikkerhetsfunn');
const body = matrixHtml + radarHtml + smallMultiplesHtml + topRisksHtml + residualHtml + findingsHtml;
const body = layoutHtml + smallMultiplesHtml + topRisksHtml + residualHtml + findingsHtml;
// Utvid matrix-risk-6x5-keyStats med RESTRISIKO når residualPair finnes.
const baseStats = inferKeyStats(data, 'matrix-risk-6x5');
const stats = (data.keyStats || (rp && rp.after
@ -3818,21 +3845,34 @@
if (n >= 4) return 'medium';
return 'low';
};
const matrixHtml = renderMatrixHtml(data, 5);
const radarHtml = renderRadarSvg(data.radar_axes || []);
// C4 top-risks-list (max 5).
// v1.14.0 sesjon 3: speil renderSecurity-pattern. Matrix + radar i .ros-layout
// (1fr 320px), top-risks + recommendation i .summary-grid (1.4fr 1fr).
const layoutHtml = '<div class="ros-layout">' +
'<div class="card" style="padding: var(--space-6)">' +
'<h2>5×5 Risikomatrise</h2>' +
renderMatrixHtml(data, 5) +
'</div>' +
'<aside class="card">' +
'<h3>Risiko-radar</h3>' +
renderRadarSvg(data.radar_axes || []) +
'</aside>' +
'</div>';
// C4 top-risks-list (max 5) — som <ol class="top-risks"> inne i .card.
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>' : '';
const topRisksCardHtml = topRisks.length ? '<div class="card">' +
'<h2>Topp-risikoer</h2>' +
'<ol class="top-risks">' +
topRisks.map(function (r, i) {
const sev = r.severity || sevForScore(r.score);
const scoreLabel = r.score ? String(r.score) : (r.severity || '—').toUpperCase();
return '<li 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>' +
'</li>';
}).join('') +
'</ol>' +
'</div>' : '';
// B6 residual-pair (gjenbruker mønster fra Dpia / Security).
const rp = data.residualPair;
let residualHtml = '';
@ -3861,12 +3901,18 @@
}
// D5 recommendation-card.
const rec = data.recommendation || '';
const recommendationHtml = rec ? '<aside class="recommendation-card">' +
const recommendationCardHtml = 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>' : '';
// Top-risks + recommendation i .summary-grid (1.4fr 1fr) per Anthropic-ref ros-lier.
// Hvis bare en av delene finnes, fyll andre kolonne med tom div for å bevare grid.
const summaryGridHtml = (topRisksCardHtml || recommendationCardHtml) ? '<div class="summary-grid">' +
(topRisksCardHtml || '<div></div>') +
(recommendationCardHtml || '<div></div>') +
'</div>' : '';
const threatsHtml = renderThreatsTable(data.threats);
const body = matrixHtml + radarHtml + topRisksHtml + residualHtml + threatsHtml + recommendationHtml;
const body = layoutHtml + summaryGridHtml + residualHtml + threatsHtml;
// Utvid matrix-risk-keyStats med RESTRISIKO når residualPair finnes
// (samme mønster som renderDpia).
const baseStats = inferKeyStats(data, 'matrix-risk');

View file

@ -198,11 +198,11 @@ fi
# -------------------------------------------------------
# 12. Design-system CSS-klasse-bruk (Tier 1+2+3)
# -------------------------------------------------------
# v1.13.0 fix (B1): .findings outer-wrapper bevisst fjernet — DS' .findings er
# grid 360px+1fr (list+detail-panel), men playground bruker bare list-kolonnen.
# Asserterer .findings__list (BEM-list) i stedet for å bekrefte at findings-
# strukturen fortsatt er i bruk uten at vi misbruker grid-containeren.
DS_CLASSES=".pyramide .matrix .radar .findings__list .distribution .critique-card .capability-matrix .aiact-timeline .tracks .error-summary .guide-panel .expansion .form-progress"
# v1.14.0 sesjon 3: .findings__list-wrapper fjernet sammen med .report-meta-band-aid.
# renderFindingsBlock bruker nå <section class="findings-section"> + <ul class="findings__items
# findings__items--standalone">. Asserterer .findings__items (BEM-list-items) i stedet —
# bekrefter at findings-strukturen fortsatt er i bruk uten at vi misbruker grid-containeren.
DS_CLASSES=".pyramide .matrix .radar .findings__items .distribution .critique-card .capability-matrix .aiact-timeline .tracks .error-summary .guide-panel .expansion .form-progress"
for cls in $DS_CLASSES; do
# Match som klassen i class="..." eller selektor — søk på klassenavnet uten leading dot
bare="${cls#.}"