diff --git a/playground/ms-ai-architect-playground.html b/playground/ms-ai-architect-playground.html index 8c5a93e..dc01074 100644 --- a/playground/ms-ai-architect-playground.html +++ b/playground/ms-ai-architect-playground.html @@ -210,6 +210,34 @@ .read-more-block { margin: var(--space-2) 0; } .read-more-block summary { cursor: pointer; color: var(--color-text-link); font-weight: var(--font-weight-medium); } + /* Renderer-batch B inlines (v1.10.0 Sesjon 4). Klasser ikke i vendor — + inline her per scope-regel (plugin standalone). Kandidat for hoisting + til shared/playground-design-system/ i Sesjon 6 visual QA. */ + .top-risks { margin: var(--space-4) 0; display: flex; flex-direction: column; gap: var(--space-2); } + .top-risks__heading { margin: 0 0 var(--space-2) 0; font-size: var(--font-size-md); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.06em; font-weight: var(--font-weight-semibold); } + .top-risk { display: grid; grid-template-columns: 28px 1fr auto; gap: var(--space-3); align-items: center; padding: var(--space-2) var(--space-3); background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); } + .top-risk__rank { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-text-tertiary); font-weight: var(--font-weight-bold); } + .top-risk__desc { font-size: var(--font-size-sm); color: var(--color-text-primary); } + .top-risk__score { font-family: var(--font-family-mono); font-size: var(--font-size-md); font-weight: var(--font-weight-bold); padding: 2px 10px; border-radius: var(--radius-pill); background: var(--color-bg-soft); color: var(--color-text-primary); } + .top-risk[data-severity="critical"] { border-left: 4px solid var(--color-severity-critical); } + .top-risk[data-severity="critical"] .top-risk__score { background: var(--color-severity-critical); color: var(--color-severity-critical-on); } + .top-risk[data-severity="high"] { border-left: 4px solid var(--color-severity-high); } + .top-risk[data-severity="high"] .top-risk__score { background: var(--color-severity-high-soft); color: var(--color-severity-high-on); } + .top-risk[data-severity="medium"] { border-left: 4px solid var(--color-severity-medium); } + .top-risk[data-severity="medium"] .top-risk__score { background: var(--color-severity-medium-soft); color: var(--color-severity-medium-on); } + .top-risk[data-severity="low"] { border-left: 4px solid var(--color-severity-low); } + + .recommendation-card { margin: var(--space-4) 0 0 0; padding: var(--space-4); background: var(--color-bg-soft); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); border-left: 4px solid var(--color-scope-architect); display: flex; flex-direction: column; gap: var(--space-2); } + .recommendation-card__label { font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); } + .recommendation-card__body { font-size: var(--font-size-md); color: var(--color-text-primary); line-height: var(--line-height-snug); } + + .suppressed-panel { margin: var(--space-4) 0 0 0; padding: var(--space-3) var(--space-4); background: var(--color-bg-soft); border: 1px dashed var(--color-border-subtle); border-radius: var(--radius-md); opacity: 0.85; } + .suppressed-panel summary { cursor: pointer; color: var(--color-text-secondary); font-weight: var(--font-weight-medium); font-size: var(--font-size-sm); } + .suppressed-panel[open] summary { margin-bottom: var(--space-2); } + .suppressed-panel__list { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-2) 0 0 0; } + .suppressed-panel__item { padding: var(--space-2) var(--space-3); background: var(--color-surface); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-sm); font-size: var(--font-size-sm); color: var(--color-text-secondary); display: flex; gap: var(--space-3); align-items: baseline; } + .suppressed-panel__id { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-text-tertiary); } + /* Tier 3 A4 — Screen-tabs (segmented). Inline her per scope-regel (plugin standalone). Kandidat for hoisting til shared/ i Sesjon 6. */ .screen-tabs { display: flex; gap: var(--space-1); padding: 4px; background: var(--color-bg-soft); border-radius: var(--radius-md); width: fit-content; margin: 0 0 var(--space-4) 0; } @@ -2650,13 +2678,92 @@ recommendation: row[recKey] || '' }; }) : []; + // topRisks: prøv ## Top-risikoer-tabell først, ellers fall tilbake til + // matrix_cells sortert desc på score. + const topRisksTbl = parseTable(md, /##\s*Top.?risikoer/i); + let topRisks = []; + if (topRisksTbl && topRisksTbl.rows.length) { + const idKey = topRisksTbl.headers[0]; + const descKey = topRisksTbl.headers.find(function (h) { return /risiko|trussel|description|beskrivelse/i.test(h); }) || topRisksTbl.headers[1]; + const scKey = topRisksTbl.headers.find(function (h) { return /score/i.test(h); }); + const sevKey = topRisksTbl.headers.find(function (h) { return /severity|alvorlighet|nivå/i.test(h); }); + topRisks = topRisksTbl.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 (matrix_cells.length) { + topRisks = matrix_cells.slice().sort(function (a, b) { + if (b.score !== a.score) return b.score - a.score; + return String(a.label || '').localeCompare(String(b.label || '')); + }).slice(0, 5).map(function (c) { + return { + id: '', + description: c.label || '', + score: c.score, + severity: '' + }; + }); + } + // categoryGrades: prøv ## Kategori-snitt-tabell først, ellers utled + // fra dimensions[]. Score → letter-grade A-F (5→A, 4→B, 3→C, 2→D, ≤1→F). + const gradeFor = function (sc) { + const n = Number(sc) || 0; + if (n >= 5) return 'A'; + if (n >= 4) return 'B'; + if (n >= 3) return 'C'; + if (n >= 2) return 'D'; + return 'F'; + }; + const catTbl = parseTable(md, /##\s*Kategori.snitt/i); + let categoryGrades = []; + if (catTbl && catTbl.rows.length) { + const nameKey = catTbl.headers[0]; + const scKey = catTbl.headers.find(function (h) { return /score|snitt/i.test(h); }) || catTbl.headers[1]; + categoryGrades = catTbl.rows.map(function (row) { + const sc = intOrZero(row[scKey] || '0'); + return { name: row[nameKey] || '', score: sc, grade: gradeFor(sc) }; + }); + } else { + categoryGrades = dimensions.map(function (d) { + return { name: d.name, score: d.score, grade: gradeFor(d.score) }; + }); + } + // residualPair: same syntax som parseMatrixRisk. + let residualPair = null; + const rrMatch = md.match(/^Restrisiko\s*:\s*([^\n]+)$/im); + if (rrMatch) { + const txt = rrMatch[1]; + const num = /(\d+)\s*[×x*]\s*(\d+)\s*(?:[-=]?[>→]|->)\s*(\d+)\s*[×x*]\s*(\d+)/.exec(txt); + if (num) { + const b1 = +num[1], b2 = +num[2], a1 = +num[3], a2 = +num[4]; + residualPair = { + before: { prob: b1, cons: b2, score: b1 * b2 }, + after: { prob: a1, cons: a2, score: a1 * a2 } + }; + } else { + const parts = txt.split(/(?:[-=]?[>→]|->)/); + if (parts.length === 2) { + residualPair = { + before: { label: parts[0].trim() }, + after: { label: parts[1].trim() } + }; + } + } + } return { ok: true, data: { dimensions: dimensions, matrix_cells: matrix_cells, findings: findings, - scores: dimensions.map(function (d) { return d.score; }) + scores: dimensions.map(function (d) { return d.score; }), + topRisks: topRisks, + categoryGrades: categoryGrades, + residualPair: residualPair } }; } @@ -3323,10 +3430,86 @@ // ---- Sub-batch B: Security (3) ---- function renderSecurity(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, 6); const radarHtml = renderRadarSvg(data.dimensions || []); + // C7 small-multiples per OWASP-kategori (driver: categoryGrades). + const cats = data.categoryGrades || []; + const smallMultiplesHtml = cats.length ? '