From 1fe40fe88650efb63662337780c08d58552c221d Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Mon, 4 May 2026 03:10:39 +0200 Subject: [PATCH] feat(ms-ai-architect): add renderPageShell + verdict + keyStats helpers (v2 foundation) --- .../ms-ai-architect-playground.html | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) 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 cdb5714..cc30447 100644 --- a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html +++ b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html @@ -148,6 +148,34 @@ .catalog-card__hint { font-size: var(--font-size-xs); color: var(--color-text-tertiary); font-family: var(--font-family-mono); } .catalog-card__actions { display: flex; gap: var(--space-2); margin-top: auto; padding-top: var(--space-2); } .catalog-tool-notice { padding: var(--space-2) var(--space-3); background: var(--color-bg-soft); border-left: 3px solid var(--color-primary-500); border-radius: var(--radius-sm); font-size: var(--font-size-xs); color: var(--color-text-secondary); } + + /* Foundation page-shell helpers (v1.10.0 Sesjon 1). + Felles header + verdict-pille + key-stats-grid for alle 17 renderers. + Kandidater for hoisting til shared/playground-design-system/ i en + senere iterasjon (Sesjon 6 visual QA). */ + .page__header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-6); margin: 0 0 var(--space-6) 0; } + .page__title { flex: 1 1 auto; min-width: 0; } + .page__eyebrow { display: block; font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-architect); margin-bottom: var(--space-2); } + .page__title h1 { margin: 0 0 var(--space-3) 0; font-size: var(--font-size-3xl); line-height: var(--line-height-tight); color: var(--color-text-primary); } + .page__lede { margin: 0; max-width: var(--measure); font-size: var(--font-size-lg); color: var(--color-text-secondary); line-height: var(--line-height-snug); } + .verdict-pill { display: inline-flex; align-items: center; padding: var(--space-2) var(--space-4); border-radius: var(--radius-pill); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; flex-shrink: 0; } + .verdict-pill[data-verdict="go"], + .verdict-pill[data-verdict="approved"], + .verdict-pill[data-verdict="allow"] { background: var(--color-state-success); color: #fff; } + .verdict-pill[data-verdict="go-with-conditions"], + .verdict-pill[data-verdict="warning"] { background: var(--color-severity-medium); color: var(--color-severity-medium-on); } + .verdict-pill[data-verdict="block"], + .verdict-pill[data-verdict="failed"] { background: var(--color-severity-critical); color: var(--color-severity-critical-on); } + .verdict-pill[data-verdict="n-a"] { background: var(--color-bg-soft); color: var(--color-text-secondary); border: 1px solid var(--color-border-subtle); } + .key-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-4); margin: 0 0 var(--space-6) 0; padding: var(--space-4); background: var(--color-bg-soft); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); } + .key-stat { display: flex; flex-direction: column; gap: 2px; } + .key-stat__label { font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.06em; } + .key-stat__value { font-size: var(--font-size-2xl); font-weight: var(--font-weight-bold); color: var(--color-text-primary); font-variant-numeric: tabular-nums; line-height: var(--line-height-tight); } + .key-stat__hint { font-size: var(--font-size-xs); color: var(--color-text-tertiary); } + .key-stat[data-modifier="critical"] .key-stat__value { color: var(--color-severity-critical); } + .key-stat[data-modifier="high"] .key-stat__value { color: var(--color-severity-high); } + .key-stat[data-modifier="medium"] .key-stat__value { color: var(--color-severity-medium); } + .key-stat[data-modifier="low"] .key-stat__value { color: var(--color-severity-low); } @@ -3209,6 +3237,288 @@ slot.innerHTML = '
' + summaryHtml + headerHtml + rowsHtml + '
'; } + // ============================================================ + // FOUNDATION HELPERS (v1.10.0 Sesjon 1) + // ============================================================ + // + // Felles grunnskjelett for alle 17 renderers. Sesjon 3-5 wrapper hver + // renderer med renderPageShell({...}, bodyHtml) — body forblir mest + // uendret, header/verdict/keyStats kommer fra denne foundation-laget. + // + // V2-data-shape (parser-output utvides — beholder v1-felter): + // data.verdict?: 'go'|'go-with-conditions'|'block'|'approved'|'failed'| + // 'allow'|'warning'|'n-a' + // data.keyStats?: Array<{label, value, hint?, modifier?}> + // + // MIGRATIONS v1->v2 i bootstrap (se migrateDataVersion under) utleder + // verdict + keyStats fra v1-felter idempotent for eksisterende state. + + const VERDICT_NORMAL = { + 'go': 'go', 'godkjent': 'approved', 'approved': 'approved', + 'go-with-conditions': 'go-with-conditions', 'conditions': 'go-with-conditions', 'betinget': 'go-with-conditions', + 'block': 'block', 'blokkert': 'block', 'forbudt': 'block', 'forbidden': 'block', + 'failed': 'failed', 'feilet': 'failed', 'underkjent': 'failed', + 'allow': 'allow', 'tillatt': 'allow', + 'warning': 'warning', 'advarsel': 'warning', + 'n-a': 'n-a', 'na': 'n-a' + }; + + function normalizeVerdict(raw) { + if (raw == null) return 'n-a'; + const k = String(raw).toLowerCase().trim(); + return VERDICT_NORMAL[k] || 'n-a'; + } + + function riskLevelModifier(level) { + const k = String(level || '').toLowerCase(); + if (k === 'forbudt' || k === 'forbidden') return 'critical'; + if (k === 'høy' || k === 'high') return 'high'; + if (k === 'begrenset' || k === 'limited') return 'medium'; + if (k === 'minimal' || k === 'low') return 'low'; + return undefined; + } + + function formatNok(n) { + if (n == null) return '—'; + const num = Number(n); + if (!isFinite(num)) return String(n); + if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; + if (num >= 1000) return Math.round(num / 1000) + 'k'; + return String(num); + } + + // Per-archetype default keyStats utledere. Hver tar v2-data, returnerer + // Array<{label, value, hint?, modifier?}>. Tom array hvis archetype ikke + // har et naturlig keyStats-aggregat (transparency, plain markdown). + const KEY_STATS_CONFIG = { + 'aiact': function (d) { + return [ + { label: 'RISIKONIVÅ', value: d.risk_level || '—', modifier: riskLevelModifier(d.risk_level) }, + { label: 'ROLLE', value: d.role || '—' }, + { label: 'FORPLIKTELSER', value: (d.obligations || []).length, hint: 'antall' } + ]; + }, + 'requirements-list': function (d) { + const items = d.items || []; + const required = items.filter(function (i) { return /påkrev|required/i.test(i.status || ''); }).length; + return [ + { label: 'KRAV', value: items.length }, + { label: 'PÅKREVD', value: required, modifier: required ? 'high' : 'low' } + ]; + }, + 'text-document': function () { return []; }, + 'fria': function (d) { + const rights = d.rights || []; + return [ + { label: 'BERØRTE GRUPPER', value: rights.length }, + { label: 'MITIGERENDE', value: rights.filter(function (r) { return r.mitigation; }).length, hint: 'tiltak' } + ]; + }, + 'conformity-checklist': function (d) { + const cl = d.checklist || []; + const passed = cl.filter(function (c) { return /pass|bestått|ok/i.test(c.status || ''); }).length; + return [ + { label: 'KRITERIER', value: cl.length }, + { label: 'BESTÅTT', value: passed, modifier: passed === cl.length ? 'low' : 'medium' }, + { label: 'FRISTER', value: (d.deadlines || []).length, hint: 'kommende' } + ]; + }, + 'matrix-risk': function (d) { + const threats = d.threats || []; + const high = threats.filter(function (t) { + const s = String(t.severity || '').toLowerCase(); + return s === 'høy' || s === 'high' || s === 'kritisk' || s === 'critical'; + }).length; + return [ + { label: 'TRUSLER', value: threats.length }, + { label: 'HØY/KRITISK', value: high, modifier: high ? 'high' : 'low' }, + { label: 'CELLER', value: (d.matrix_cells || []).length, hint: 'i matrise' } + ]; + }, + 'matrix-risk-6x5': function (d) { + const findings = d.findings || []; + const dims = d.dimensions || []; + const sum = dims.reduce(function (a, b) { return a + (Number(b.score) || 0); }, 0); + const avg = dims.length ? (sum / dims.length).toFixed(1) : '—'; + return [ + { label: 'DIMENSJONER', value: dims.length }, + { label: 'SNITT', value: avg, hint: 'av 5' }, + { label: 'FUNN', value: findings.length, modifier: findings.length > 5 ? 'high' : 'medium' } + ]; + }, + 'findings': function (d) { + const f = d.findings || []; + const crit = f.filter(function (x) { return /crit|kritisk/i.test(x.severity || ''); }).length; + return [ + { label: 'FUNN', value: f.length }, + { label: 'KRITISKE', value: crit, modifier: crit ? 'critical' : 'low' } + ]; + }, + 'cost-distribution': function (d) { + return [ + { label: 'P50', value: formatNok(d.p50), hint: 'median' }, + { label: 'P90', value: formatNok(d.p90), hint: 'pessimistisk', modifier: 'high' }, + { label: 'KOMPONENTER', value: (d.monthly_breakdown || []).length } + ]; + }, + 'capability': function (d) { + const lic = d.licenses || []; + const totalCaps = lic.reduce(function (a, l) { return a + ((l.capabilities || []).length); }, 0); + return [ + { label: 'LISENSER', value: lic.length }, + { label: 'KAPABILITETER', value: totalCaps } + ]; + }, + 'phased-plan': function (d) { + const phases = d.phases || []; + const totalWeeks = phases.reduce(function (a, p) { return a + (Number(p.duration_weeks) || 0); }, 0); + const risks = d.risks || []; + return [ + { label: 'FASER', value: phases.length }, + { label: 'VARIGHET', value: totalWeeks || '—', hint: 'uker totalt' }, + { label: 'RISIKOER', value: risks.length, modifier: risks.length > 3 ? 'high' : 'medium' } + ]; + }, + 'markdown': function (d) { + const sec = d.sections || []; + return sec.length ? [{ label: 'SEKSJONER', value: sec.length }] : []; + }, + 'verdict': function (d) { + const km = d.key_metrics || []; + return km.slice(0, 4).map(function (m) { + return { + label: String(m.label || m.name || '').toUpperCase(), + value: m.value != null ? m.value : '—', + hint: m.unit || undefined + }; + }); + }, + 'comparison': function (d) { + return [ + { label: 'KANDIDATER', value: (d.subjects || []).length }, + { label: 'DIMENSJONER', value: (d.rows || []).length } + ]; + } + }; + + function inferVerdict(data, archetype) { + if (!data) return 'n-a'; + // Eksplisitt verdict tar prioritet uansett kilde. + if (data.verdict) return normalizeVerdict(data.verdict); + switch (archetype) { + case 'aiact': { + const lvl = String(data.risk_level || '').toLowerCase(); + if (lvl === 'forbudt' || lvl === 'forbidden') return 'block'; + if (lvl === 'høy' || lvl === 'high') return 'warning'; + if (lvl === 'begrenset' || lvl === 'limited') return 'go-with-conditions'; + if (lvl === 'minimal' || lvl === 'low') return 'go'; + return 'n-a'; + } + case 'matrix-risk': + case 'matrix-risk-6x5': { + const threats = data.threats || data.findings || []; + const hasCritical = threats.some(function (t) { return /crit|kritisk/i.test(t.severity || ''); }); + if (hasCritical) return 'block'; + if (threats.length) return 'warning'; + return 'n-a'; + } + case 'conformity-checklist': { + const cl = data.checklist || []; + if (!cl.length) return 'n-a'; + const anyFailed = cl.some(function (c) { return /fail|underkjent/i.test(c.status || ''); }); + if (anyFailed) return 'failed'; + const allPassed = cl.every(function (c) { return /pass|bestått|ok/i.test(c.status || ''); }); + if (allPassed) return 'approved'; + return 'go-with-conditions'; + } + case 'findings': { + const fs = data.findings || []; + if (!fs.length) return 'allow'; + const crit = fs.some(function (f) { return /crit|kritisk/i.test(f.severity || ''); }); + return crit ? 'block' : 'warning'; + } + case 'cost-distribution': { + if (data.p90 != null && data.p50 != null) { + const ratio = Number(data.p90) / Math.max(Number(data.p50), 1); + return ratio > 2 ? 'warning' : 'go'; + } + return 'n-a'; + } + default: + return 'n-a'; + } + } + + function inferKeyStats(data, archetype) { + if (!data) return []; + // Eksplisitt keyStats tar prioritet + if (Array.isArray(data.keyStats)) return data.keyStats; + const fn = KEY_STATS_CONFIG[archetype]; + if (typeof fn !== 'function') return []; + try { + const out = fn(data); + return Array.isArray(out) ? out : []; + } catch (e) { + return []; + } + } + + function renderVerdictPill(verdict) { + const v = String(verdict || 'n-a').toLowerCase(); + const labels = { + 'go': 'GO', + 'go-with-conditions': 'BETINGET', + 'block': 'BLOKKERT', + 'approved': 'GODKJENT', + 'failed': 'UNDERKJENT', + 'allow': 'TILLATT', + 'warning': 'ADVARSEL', + 'n-a': 'IKKE VURDERT' + }; + return '' + + escapeHtml(labels[v] || v.toUpperCase()) + + ''; + } + + function renderKeyStatsGrid(stats) { + if (!stats || !stats.length) return ''; + const items = stats.map(function (s) { + const mod = s.modifier ? ' data-modifier="' + escapeAttr(s.modifier) + '"' : ''; + const hint = s.hint ? '' + escapeHtml(s.hint) + '' : ''; + return '
' + + '' + escapeHtml(s.label || '') + '' + + '' + escapeHtml(String(s.value)) + '' + + hint + + '
'; + }).join(''); + return '
' + items + '
'; + } + + function renderPageShell(opts, bodyHtml) { + opts = opts || {}; + const eyebrow = opts.eyebrow ? '' + escapeHtml(opts.eyebrow) + '' : ''; + const title = '

' + escapeHtml(opts.title || '') + '

'; + const lede = opts.lede ? '

' + escapeHtml(opts.lede) + '

' : ''; + const verdict = (opts.verdict && opts.verdict !== 'n-a') ? renderVerdictPill(opts.verdict) : ''; + const stats = renderKeyStatsGrid(opts.keyStats); + return ( + '' + + stats + + (bodyHtml || '') + ); + } + + // Eksponer for tester og fremtidig renderer-iterasjon (Sesjon 3-5) + window.__renderPageShell = renderPageShell; + window.__renderVerdictPill = renderVerdictPill; + window.__renderKeyStatsGrid = renderKeyStatsGrid; + window.__inferVerdict = inferVerdict; + window.__inferKeyStats = inferKeyStats; + window.__KEY_STATS_CONFIG = KEY_STATS_CONFIG; + // ---- RENDERERS routing-objekt (17 commands) ---- const RENDERERS = {