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. */ ikke skubber innhold utenfor viewport. */
.recommendation-card__body { overflow-wrap: anywhere; word-break: break-word; } .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 /* 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- <section class="report-meta"> som outer-wrapper i flere rendrere. Browser-
defaults for <dl><dd><ul> gir uforutsigbare indents (Forpliktelser-bullet- 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- /* v1.13.1 fix (B12, B13, B15): defensive block-layout for sibling-rapport-
seksjoner som kan ha overflow-issues mot foregående grid-elementer seksjoner som kan ha overflow-issues mot foregående grid-elementer
(.small-multiples, .kanban-board, .mat-ladder). Sikrer eksplisitt (.small-multiples, .kanban-board, .mat-ladder). Sikrer eksplisitt
block-flow + clear så de ikke leker grid-celler eller flyter. */ block-flow + clear så de ikke leker grid-celler eller flyter.
.top-risks, v1.14.0 sesjon 3: .top-risks fjernet (ligger nå i .card med riktig DS block-flow). */
.suppressed-panel, .suppressed-panel,
.phase-detail, .phase-detail,
.aiact-timeline, .aiact-timeline,
.small-multiples + .top-risks,
.kanban-board + .report-meta, .kanban-board + .report-meta,
.mat-ladder + .phase-detail, .mat-ladder + .phase-detail,
.phase-detail + .phase-detail { display: block; clear: both; width: 100%; } .phase-detail + .phase-detail { display: block; clear: both; width: 100%; }
@ -3327,9 +3335,11 @@
} }
function renderFindingsBlock(findings, label) { function renderFindingsBlock(findings, label) {
// v1.13.0 fix (B1): DS' .findings er grid 360px+1fr (list+detail-panel). // v1.14.0 sesjon 3: refaktorert fra .report-meta-band-aid til standalone
// Vi har bare list-kolonnen; bruk <section class="report-meta"> som outer // findings-section. DS' .findings er grid 360px+1fr (list+detail-panel) —
// for å beholde findings__list-strukturen uten å bli klemt til 360px. // 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) { const items = findings.map(function (f) {
return '<li class="findings__item">' + return '<li class="findings__item">' +
'<span class="findings__item-severity-dot" data-severity="' + escapeAttr(f.severity || 'info') + '"></span>' + '<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>' + '<span class="findings__item-meta">Lokasjon: ' + escapeHtml(f.location || '—') + ' · Severity: ' + escapeHtml(f.severity || '—') + '</span>' +
'</li>'; '</li>';
}).join(''); }).join('');
return '<section class="report-meta">' + return '<section class="findings-section">' +
'<h4>' + escapeHtml(label) + '</h4>' + '<h3>' + escapeHtml(label) + '</h3>' +
'<div class="findings__list">' + '<ul class="findings__items findings__items--standalone">' + items + '</ul>' +
'<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>' +
'</section>'; '</section>';
} }
@ -3671,7 +3676,12 @@
} }
function renderDpia(data, slot) { 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 threatsHtml = renderThreatsTable(data.threats);
const rp = data.residualPair; const rp = data.residualPair;
let residualHtml = ''; let residualHtml = '';
@ -3735,34 +3745,51 @@
if (n >= 4) return 'medium'; if (n >= 4) return 'medium';
return 'low'; return 'low';
}; };
const matrixHtml = renderMatrixHtml(data, 6); // v1.14.0 sesjon 3: matrix + radar i .ros-layout (1fr 320px) per Anthropic-ref
const radarHtml = renderRadarSvg(data.dimensions || []); // ros-lier-scenario. Matrix står i .card, radar i <aside class="card">.
// C7 small-multiples per OWASP-kategori (driver: categoryGrades). 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 cats = data.categoryGrades || [];
const smallMultiplesHtml = cats.length ? '<div class="small-multiples">' + cats.map(function (c) { const smallMultiplesHtml = cats.length ? '<section class="small-multiples-section">' +
const grade = c.grade || ''; '<h3>Posture per OWASP-kategori</h3>' +
const fillPct = Math.max(0, Math.min(100, ((Number(c.score) || 0) / 5) * 100)); '<div class="small-multiples">' + cats.map(function (c) {
return '<div class="sm-card">' + const grade = c.grade || '';
'<div class="sm-card__header">' + const fillPct = Math.max(0, Math.min(100, ((Number(c.score) || 0) / 5) * 100));
'<span class="sm-card__name">' + escapeHtml(c.name || '') + '</span>' + return '<div class="sm-card">' +
'<span class="sm-card__grade" data-grade="' + escapeAttr(grade) + '">' + escapeHtml(grade) + '</span>' + '<div class="sm-card__header">' +
'</div>' + '<span class="sm-card__name">' + escapeHtml(c.name || '') + '</span>' +
'<div class="sm-card__bar"><div class="sm-card__bar-fill" style="width: ' + fillPct.toFixed(0) + '%"></div></div>' + '<span class="sm-card__grade" data-grade="' + escapeAttr(grade) + '">' + escapeHtml(grade) + '</span>' +
'<span class="sm-card__status">Score ' + escapeHtml(String(c.score || 0)) + ' / 5</span>' + '</div>' +
'</div>'; '<div class="sm-card__bar"><div class="sm-card__bar-fill" style="width: ' + fillPct.toFixed(0) + '%"></div></div>' +
}).join('') + '</div>' : ''; '<span class="sm-card__status">Score ' + escapeHtml(String(c.score || 0)) + ' / 5</span>' +
// 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>' +
'</div>'; '</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). // B6 residual-pair (når data.residualPair finnes).
const rp = data.residualPair; const rp = data.residualPair;
let residualHtml = ''; let residualHtml = '';
@ -3790,7 +3817,7 @@
'</div>'; '</div>';
} }
const findingsHtml = renderFindingsBlock(data.findings || [], 'Sikkerhetsfunn'); 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. // Utvid matrix-risk-6x5-keyStats med RESTRISIKO når residualPair finnes.
const baseStats = inferKeyStats(data, 'matrix-risk-6x5'); const baseStats = inferKeyStats(data, 'matrix-risk-6x5');
const stats = (data.keyStats || (rp && rp.after const stats = (data.keyStats || (rp && rp.after
@ -3818,21 +3845,34 @@
if (n >= 4) return 'medium'; if (n >= 4) return 'medium';
return 'low'; return 'low';
}; };
const matrixHtml = renderMatrixHtml(data, 5); // v1.14.0 sesjon 3: speil renderSecurity-pattern. Matrix + radar i .ros-layout
const radarHtml = renderRadarSvg(data.radar_axes || []); // (1fr 320px), top-risks + recommendation i .summary-grid (1.4fr 1fr).
// C4 top-risks-list (max 5). 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 topRisks = (data.topRisks || []).slice(0, 5);
const topRisksHtml = topRisks.length ? '<section class="top-risks">' + const topRisksCardHtml = topRisks.length ? '<div class="card">' +
'<h4 class="top-risks__heading">Top-risikoer</h4>' + '<h2>Topp-risikoer</h2>' +
topRisks.map(function (r, i) { '<ol class="top-risks">' +
const sev = r.severity || sevForScore(r.score); topRisks.map(function (r, i) {
const scoreLabel = r.score ? String(r.score) : (r.severity || '—').toUpperCase(); const sev = r.severity || sevForScore(r.score);
return '<div class="top-risk" data-severity="' + escapeAttr(sev) + '">' + const scoreLabel = r.score ? String(r.score) : (r.severity || '—').toUpperCase();
'<span class="top-risk__rank">' + (i + 1) + '</span>' + return '<li class="top-risk" data-severity="' + escapeAttr(sev) + '">' +
'<span class="top-risk__desc">' + escapeHtml(r.description || '') + '</span>' + '<span class="top-risk__rank">' + (i + 1) + '</span>' +
'<span class="top-risk__score">' + escapeHtml(scoreLabel) + '</span>' + '<span class="top-risk__desc">' + escapeHtml(r.description || '') + '</span>' +
'</div>'; '<span class="top-risk__score">' + escapeHtml(scoreLabel) + '</span>' +
}).join('') + '</section>' : ''; '</li>';
}).join('') +
'</ol>' +
'</div>' : '';
// B6 residual-pair (gjenbruker mønster fra Dpia / Security). // B6 residual-pair (gjenbruker mønster fra Dpia / Security).
const rp = data.residualPair; const rp = data.residualPair;
let residualHtml = ''; let residualHtml = '';
@ -3861,12 +3901,18 @@
} }
// D5 recommendation-card. // D5 recommendation-card.
const rec = data.recommendation || ''; 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>' + '<span class="recommendation-card__label">Anbefaling</span>' +
'<p class="recommendation-card__body">' + escapeHtml(rec).replace(/\n/g, '<br>') + '</p>' + '<p class="recommendation-card__body">' + escapeHtml(rec).replace(/\n/g, '<br>') + '</p>' +
'</aside>' : ''; '</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 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 // Utvid matrix-risk-keyStats med RESTRISIKO når residualPair finnes
// (samme mønster som renderDpia). // (samme mønster som renderDpia).
const baseStats = inferKeyStats(data, 'matrix-risk'); const baseStats = inferKeyStats(data, 'matrix-risk');

View file

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