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 = {