From fc48d01f1ec5bebb5543e6795f5e709e14490712 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Mon, 4 May 2026 07:52:52 +0200 Subject: [PATCH] feat(ms-ai-architect): renderer batch C (econ + docs 8) + structural test asserts [skip-docs] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sesjon 5 av v1.10.0-løpet (8 av 17 renderers wrapped med renderPageShell). Nå alle 17 renderers bruker felles grunnskjelett (page__eyebrow + h1 + verdict). Renderers wrapped: - C.1 renderCost: eyebrow=KOSTNAD, key-stats utvidet med DOMINERENDE-komponent - C.2 renderLicense: eyebrow=LISENS, scenario-card-grid per kandidat-lisens, TOPP-LISENS key-stat - C.3 renderMigrate: eyebrow=MIGRASJON, E2 mat-ladder erstatter aiact-timeline, E4 cycle-ribbon ved aktiv fase - C.4 renderAdr: eyebrow=ADR, D4 critique-card per beslutningsseksjon, ADR-status → verdict-pille (accepted/proposed/rejected/deprecated) - C.5 renderSummary: eyebrow=SAMMENDRAG, E8 read-more for lange rationale - C.6 renderPoc: eyebrow=POC, E2 mat-ladder + B5 traffic-light per success-kriterie, pocVerdict styrer verdict-pille - C.7 renderUtredning: eyebrow=UTREDNING, A4 screen-tabs (Bakgrunn/Funn/Konklusjon/ Anbefaling) + E8 read-more på lange seksjoner - C.8 renderCompare: eyebrow=SAMMENLIGN, D1 scenario-cards-grid per kandidat, parseComparison.winner styrer vinner-pille + VINNER key-stat Parser-utvidelser (R15 forward-compat — eksisterende fixtures uendret): - parsePhasedPlan: phases[].status (planned/active/done), currentPhaseIndex, pocVerdict (kun ved POC-Verdict-linje) - parseComparison: optional winner-felt fra "## Vinner: "-linje Topic 2 strategi A i handlePasteImport: sentralisert _consumer-tildeling (result.data._consumer ||= cmd.id), respekterer parser-spesifikk verdi (parseMatrixRisk → 'ros'). Fixture-updates: migrate/poc med Status: per fase + POC-Verdict, compare med "## Vinner:"-linje. Test-asserts (tests/test-playground-v3.sh +18 PASS, totalt 201/201): - 25e SC8 per-renderer for batch C (8 renderers) - 25f Step 12 must_contain (mat-ladder, screen-tabs, _consumer) - 25g Felles grunnskjelett: alle 17 renderers bruker renderPageShell - 25h Tier 3-bruk: kanban i conformity/review, mat-ladder i migrate/poc - 25i Onboarding field-distribution (4 strukturerte, 14 fritekst) Verifisert: 201/201 statiske, 70/70 parser-fixtures, 7/7 migrations PASS. Co-Authored-By: Claude Opus 4.7 --- .../ms-ai-architect-playground.html | 468 +++++++++++++++--- .../playground/test-fixtures/compare.md | 2 + .../playground/test-fixtures/migrate.md | 4 + .../playground/test-fixtures/poc.md | 8 + .../tests/test-playground-v3.sh | 97 ++++ 5 files changed, 521 insertions(+), 58 deletions(-) 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 db16f58..9e7da73 100644 --- a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html +++ b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html @@ -2944,7 +2944,8 @@ name: phaseMatch[1].trim(), milestones: [], success_criteria: [], - duration_weeks: null + duration_weeks: null, + status: null }; bucket = null; continue; @@ -2961,6 +2962,17 @@ current.duration_weeks = parseInt(durMatch[1], 10); continue; } + const statusMatch = /^Status\s*:\s*([\wæøåA-Za-z-]+)/i.exec(trimmed); + if (statusMatch) { + // Normaliser til en av: planned | active | done. + const raw = statusMatch[1].toLowerCase(); + let s = null; + if (/^(done|ferdig|fullf[øo]rt|complete[d]?)$/.test(raw)) s = 'done'; + else if (/^(active|aktiv|p[åa]g[åa]ende|igang|in[-_]?progress|current|n[åa])$/.test(raw)) s = 'active'; + else if (/^(planned|planlagt|kommende|future|fremtid)$/.test(raw)) s = 'planned'; + current.status = s || raw; + continue; + } if (/^Milep[æa]ler\s*:?\s*$/i.test(trimmed)) { bucket = 'milestones'; continue; } if (/^Suksesskriterier\s*:?\s*$/i.test(trimmed)) { bucket = 'success_criteria'; continue; } const bulletMatch = /^[-*]\s+(.+)$/.exec(trimmed); @@ -2970,6 +2982,32 @@ } if (current) phases.push(current); + // Utled currentPhaseIndex: første 'active' ELLER første ikke-'done'. + // R15: -1 hvis ingen faser har status (forward-compat — eksisterende fixtures uberørt). + let currentPhaseIndex = -1; + const anyStatus = phases.some(function (p) { return p.status; }); + if (anyStatus) { + for (let i = 0; i < phases.length; i++) { + if (phases[i].status === 'active') { currentPhaseIndex = i; break; } + } + if (currentPhaseIndex < 0) { + for (let i = 0; i < phases.length; i++) { + if (phases[i].status !== 'done') { currentPhaseIndex = i; break; } + } + } + } + + // POC-verdict (kun for poc-consumer): "## POC-Verdict: GO|BETINGET|BLOKK" + // R15: undefined for migrate-consumer (uberørt felt). + let pocVerdict; + const pvMatch = /^##\s*POC[- ]?Verdict\s*:\s*([A-Za-zØøÆæÅå -]+)$/im.exec(md); + if (pvMatch) { + const tag = pvMatch[1].toLowerCase().trim(); + if (/^(go-?with-?conditions|betinget|conditions?|conditional)$/.test(tag)) pocVerdict = 'go-with-conditions'; + else if (/^(block|blokk|blokkert|stop)$/.test(tag)) pocVerdict = 'block'; + else if (/^(go|godkjent|ok|pass)$/.test(tag)) pocVerdict = 'go'; + } + const risksTbl = parseTable(md, /##\s*Risiko/i); const risks = risksTbl ? risksTbl.rows.map(function (row) { const risikoKey = risksTbl.headers[0]; @@ -2985,7 +3023,9 @@ }) : []; if (!phases.length) return { ok: false, errors: [{ section: 'phases', reason: 'Ingen faser funnet (### Fase N)' }] }; - return { ok: true, data: { phases: phases, risks: risks } }; + const out = { phases: phases, risks: risks, currentPhaseIndex: currentPhaseIndex }; + if (pocVerdict) out.pocVerdict = pocVerdict; + return { ok: true, data: out }; } function parseMarkdown(md) { @@ -3052,7 +3092,12 @@ winner: winnerKey ? (row[winnerKey] || '') : '' }; }); - return { ok: true, data: { subjects: subjects, rows: rows } }; + // R15: optional winner-felt fra "## Vinner: "-linje. Brukes av + // renderCompare for verdict-pill og scenario-card highlight. + const out = { subjects: subjects, rows: rows }; + const winMatch = /^##\s*Vinner\s*:\s*(.+?)\s*$/im.exec(md) || /^Winner\s*:\s*(.+?)\s*$/im.exec(md); + if (winMatch) out.winner = winMatch[1].trim(); + return { ok: true, data: out }; } // ---- PARSERS routing-objekt ---- @@ -3769,7 +3814,27 @@ const tcoHtml = tcoRows ? '' + tcoHeader + '' + tcoRows + '
' : ''; - slot.innerHTML = distHtml + breakdownHtml + tcoHtml; + const body = distHtml + breakdownHtml + tcoHtml; + // Utvid cost-distribution-keyStats med DOMINERENDE (top-komponent i breakdown). + const breakdown = data.monthly_breakdown || []; + const dominant = breakdown.reduce(function (acc, m) { + return (m && Number(m.cost) > Number(acc && acc.cost || 0)) ? m : acc; + }, null); + const baseStats = inferKeyStats(data, 'cost-distribution'); + const stats = data.keyStats || (dominant + ? baseStats.concat([{ + label: 'DOMINERENDE', + value: String(dominant.component || '').slice(0, 28), + hint: formatNok(dominant.cost) + '/mnd' + }]) + : baseStats); + slot.innerHTML = renderPageShell({ + eyebrow: 'KOSTNAD', + title: data.title || 'Kostnadsestimat', + lede: data.lede || 'Distribusjon P10/P50/P90 i NOK med månedlig fordeling og TCO over 3 år.', + verdict: data.verdict || inferVerdict(data, 'cost-distribution'), + keyStats: stats + }, body); } function renderLicense(data, slot) { @@ -3796,8 +3861,50 @@ cells + ''; }).join(''); - slot.innerHTML = '
' + + const matrixHtml = '
' + headHtml + rowsHtml + '
'; + // D1 scenario-card-grid per lisens: hver lisens som card med dekning-stat. + const isAvail = function (cap) { return /^avail|tilgjengelig/i.test((cap && cap.status) || ''); }; + const isMiss = function (cap) { return /^miss/i.test((cap && cap.status) || ''); }; + const totalCaps = capabilityNames.length; + const licScores = licenses.map(function (l) { + const caps = l.capabilities || []; + const avail = caps.filter(isAvail).length; + const miss = caps.filter(isMiss).length; + const ratio = totalCaps ? (avail / totalCaps) : 0; + const status = ratio >= 0.8 ? 'met' : ratio >= 0.4 ? 'partial' : 'missing'; + return { name: l.name, avail: avail, miss: miss, total: totalCaps, ratio: ratio, status: status }; + }); + const scenarioGridHtml = '
' + licScores.map(function (s) { + const pct = (s.ratio * 100).toFixed(0); + return '
' + + '
' + + 'LISENS' + + '' + s.avail + '/' + s.total + '' + + '
' + + '

' + escapeHtml(s.name) + '

' + + '
' + pct + '% dekket · ' + s.miss + ' mangler
' + + '
'; + }).join('') + '
'; + const body = scenarioGridHtml + matrixHtml; + // Utvid capability-keyStats med BESTE LISENS (høyest avail-ratio). + const baseStats = inferKeyStats(data, 'capability'); + const top = licScores.reduce(function (a, b) { return b.ratio > (a ? a.ratio : -1) ? b : a; }, null); + const stats = data.keyStats || (top + ? baseStats.concat([{ + label: 'TOPP-LISENS', + value: String(top.name || '').slice(0, 24), + hint: top.avail + '/' + top.total + ' kapabiliteter', + modifier: top.status === 'met' ? 'low' : top.status === 'partial' ? 'medium' : 'high' + }]) + : baseStats); + slot.innerHTML = renderPageShell({ + eyebrow: 'LISENS', + title: data.title || 'Lisens-kapabilitetsmatrise', + lede: data.lede || 'Kapabilitetsdekning per lisensnivå med scenario-cards og full matrise.', + verdict: data.verdict || inferVerdict(data, 'capability'), + keyStats: stats + }, body); } // ---- Sub-batch D: Documentation (6) ---- @@ -3805,23 +3912,57 @@ function renderMigrate(data, slot) { const phases = data.phases || []; if (!phases.length) { slot.innerHTML = renderEmptyState(); return; } - const milestones = phases.map(function (p, i) { - const left = ((i + 1) / (phases.length + 1)) * 100; - return '
' + - '
' + - '
' + - '' + (p.duration_weeks ? p.duration_weeks + ' uker' : '') + '' + - '' + escapeHtml(p.name) + '' + + // Map fase-status til mat-step data-state. R15: hvis ingen faser har + // status, fall tilbake til "alle future" — eksisterende fixtures uberørt. + const cpi = (typeof data.currentPhaseIndex === 'number') ? data.currentPhaseIndex : -1; + const stepStateFor = function (p, i) { + if (p.status === 'done') return 'completed'; + if (p.status === 'active') return 'current'; + if (p.status === 'planned' || p.status) return 'future'; + // Fallback uten status: bruk currentPhaseIndex hvis satt. + if (cpi < 0) return 'future'; + if (i < cpi) return 'completed'; + if (i === cpi) return 'current'; + return 'future'; + }; + const stepsHtml = phases.map(function (p, i) { + const state = stepStateFor(p, i); + const num = String(i + 1).padStart(2, '0'); + const pill = state === 'current' + ? 'PÅGÅR' + : state === 'completed' + ? 'FERDIG' + : ''; + const dur = p.duration_weeks ? '
' + p.duration_weeks + ' uker
' : ''; + const desc = (p.milestones && p.milestones.length) + ? '
' + escapeHtml(p.milestones[0]) + '
' + : ''; + return '
' + + '
' + num + '
' + + '
' + + '
' + escapeHtml(p.name) + ' ' + pill + '
' + + desc + + dur + '
' + '
'; }).join(''); - const timelineHtml = - '
' + - '
' + - '
' + - milestones + - '
' + - '
'; + const ladderHtml = '
' + stepsHtml + '
'; + // E4 cycle-ribbon: bare når en fase er aktiv. data-phase=execution som + // standard for migrasjonens "kjøre"-fase. + let ribbonHtml = ''; + if (cpi >= 0 && phases[cpi]) { + const cur = phases[cpi]; + const cumWeeks = phases.slice(0, cpi).reduce(function (a, p) { return a + (Number(p.duration_weeks) || 0); }, 0); + const weekStart = cumWeeks + 1; + const weekEnd = cumWeeks + (Number(cur.duration_weeks) || 0); + const weekRange = cur.duration_weeks ? ('Uke ' + weekStart + '-' + weekEnd) : ''; + ribbonHtml = '
' + + 'M-' + (cpi + 1) + '' + + (weekRange ? '' + escapeHtml(weekRange) + '' : '') + + 'PÅGÅR' + + '' + escapeHtml(cur.name) + '' + + '
'; + } const detailsHtml = phases.map(function (p) { const ms = (p.milestones || []).map(function (m) { return '
  • ' + escapeHtml(m) + '
  • '; }).join(''); const sc = (p.success_criteria || []).map(function (s) { return '
  • ' + escapeHtml(s) + '
  • '; }).join(''); @@ -3837,7 +3978,14 @@ const risksHtml = risksRows ? '' + risksRows + '
    RisikoSannsynlighetKonsekvensTiltak
    ' : ''; - slot.innerHTML = timelineHtml + detailsHtml + risksHtml; + const body = ribbonHtml + ladderHtml + detailsHtml + risksHtml; + slot.innerHTML = renderPageShell({ + eyebrow: 'MIGRASJON', + title: data.title || 'Migrasjonsplan', + lede: data.lede || 'Faseinndelt migrasjon med mat-ladder, cycle-ribbon for aktiv fase og risikomatrise.', + verdict: data.verdict || inferVerdict(data, 'phased-plan'), + keyStats: data.keyStats || inferKeyStats(data, 'phased-plan') + }, body); } function renderAdr(data, slot) { @@ -3847,15 +3995,43 @@ (data.date ? '
    Date
    ' + escapeHtml(data.date) + '
    ' : '') + (data.deciders ? '
    Deciders
    ' + escapeHtml(data.deciders) + '
    ' : '') + ''; - const sectionsHtml = (data.sections || []).map(function (s) { - return '

    ' + escapeHtml(s.heading) + '

    ' + escapeHtml(s.body).replace(/\n/g, '
    ') + '
    '; - }).join(''); - slot.innerHTML = - '
    ' + - '

    ' + escapeHtml(data.title || 'ADR') + '

    ' + - meta + - sectionsHtml + - '
    '; + // D4 critique-card per beslutningsseksjon. Ingen severity (ADR-seksjoner + // er ikke risikorangert), bruker rekkefølge-id ADR-01..n. + const sections = data.sections || []; + const cardsHtml = sections.length ? '
    ' + sections.map(function (s, i) { + const id = 'ADR-' + String(i + 1).padStart(2, '0'); + const body = escapeHtml(s.body || '').replace(/\n/g, '
    '); + return '
    ' + + '
    ' + + '
    ' + escapeHtml(s.heading) + '
    ' + + '
    ' + + '' + id + '' + + '
    ' + + '
    ' + + '
    ' + body + '
    ' + + '
    '; + }).join('') + '
    ' : ''; + const body = meta + cardsHtml; + // ADR-status til verdict: accepted/godkjent → approved, proposed → go-with-conditions, + // rejected → failed, deprecated/superseded → warning. + const statusMap = { + accepted: 'approved', godkjent: 'approved', approved: 'approved', + proposed: 'go-with-conditions', foreslått: 'go-with-conditions', 'foreslatt': 'go-with-conditions', + rejected: 'failed', avvist: 'failed', + deprecated: 'warning', foreldet: 'warning', superseded: 'warning' + }; + const verdict = data.verdict || statusMap[String(data.status || '').toLowerCase().trim()] || 'n-a'; + slot.innerHTML = renderPageShell({ + eyebrow: 'ADR', + title: data.title || 'Architecture Decision Record', + lede: data.lede || (data.status ? 'Status: ' + data.status : 'MADR v3.0 — beslutningsdokument med kontekst, alternativer og konsekvenser.'), + verdict: verdict, + keyStats: data.keyStats || [ + { label: 'STATUS', value: String(data.status || '—').toUpperCase() }, + { label: 'SEKSJONER', value: sections.length }, + { label: 'BESLUTTERE', value: data.deciders ? String(data.deciders).split(/[,;]/).length : 0, hint: 'antall' } + ] + }, body); } function renderSummary(data, slot) { @@ -3885,9 +4061,23 @@ '
    ' + '
    ' + '
    '; - const rationaleHtml = data.rationale - ? '

    Rationale

    ' + escapeHtml(data.rationale).replace(/\n/g, '
    ') + '

    ' - : ''; + // E8 read-more: lange rationale (>300 tegn) skjuler hale i
    . + let rationaleHtml = ''; + if (data.rationale) { + const raw = String(data.rationale); + if (raw.length > 300) { + const head = raw.slice(0, 220); + const tail = raw.slice(220); + rationaleHtml = '

    Rationale

    ' + + '

    ' + escapeHtml(head).replace(/\n/g, '
    ') + '…

    ' + + '
    Vis hele rationale' + + '

    ' + escapeHtml(tail).replace(/\n/g, '
    ') + '

    ' + + '
    ' + + '
    '; + } else { + rationaleHtml = '

    Rationale

    ' + escapeHtml(raw).replace(/\n/g, '
    ') + '

    '; + } + } let metricsHtml = ''; if ((data.key_metrics || []).length) { const headers = data.metrics_headers || Object.keys(data.key_metrics[0] || {}); @@ -3901,29 +4091,73 @@ const nextHtml = (data.next_steps || []).length ? '

    Next Steps

      ' + data.next_steps.map(function (s) { return '
    • ' + escapeHtml(s) + '
    • '; }).join('') + '
    ' : ''; - slot.innerHTML = verdictHtml + rationaleHtml + metricsHtml + nextHtml; + const body = verdictHtml + rationaleHtml + metricsHtml + nextHtml; + // Map summary-verdict (allow/warning/block) til canonical verdict for header-pill. + const headerVerdictMap = { allow: 'allow', warning: 'warning', block: 'block' }; + const headerVerdict = headerVerdictMap[v.variant] || 'warning'; + slot.innerHTML = renderPageShell({ + eyebrow: 'SAMMENDRAG', + title: data.title || 'Beslutningsnotat', + lede: data.lede || 'Teknisk sammendrag med verdict, key metrics og neste steg.', + verdict: headerVerdict, + keyStats: data.keyStats || inferKeyStats(data, 'verdict') + }, body); } function renderPoc(data, slot) { const phases = data.phases || []; if (!phases.length) { slot.innerHTML = renderEmptyState(); return; } - const stagesHtml = phases.map(function (p, i) { + // E2 mat-ladder (samme mønster som migrate). POC uses currentPhaseIndex/status. + const cpi = (typeof data.currentPhaseIndex === 'number') ? data.currentPhaseIndex : -1; + const stepStateFor = function (p, i) { + if (p.status === 'done') return 'completed'; + if (p.status === 'active') return 'current'; + if (p.status === 'planned' || p.status) return 'future'; + if (cpi < 0) return 'future'; + if (i < cpi) return 'completed'; + if (i === cpi) return 'current'; + return 'future'; + }; + const stepsHtml = phases.map(function (p, i) { + const state = stepStateFor(p, i); const num = String(i + 1).padStart(2, '0'); - const isCurrent = (i === 0); - return '
    ' + - '
    ' + num + '
    ' + - '
    ' + escapeHtml(p.name) + '
    ' + - '
    ' + (p.duration_weeks || '?') + ' uker
    ' + + const pill = state === 'current' + ? 'PÅGÅR' + : state === 'completed' + ? 'FERDIG' + : ''; + const dur = p.duration_weeks ? '
    ' + p.duration_weeks + ' uker
    ' : ''; + const desc = (p.milestones && p.milestones.length) + ? '
    ' + escapeHtml(p.milestones[0]) + '
    ' + : ''; + return '
    ' + + '
    ' + num + '
    ' + + '
    ' + + '
    ' + escapeHtml(p.name) + ' ' + pill + '
    ' + + desc + + dur + + '
    ' + '
    '; }).join(''); - const cockpitHtml = '
    ' + stagesHtml + '
    '; - const detailsHtml = phases.map(function (p) { + const ladderHtml = '
    ' + stepsHtml + '
    '; + // B5 traffic-light per success-kriterie. R15: uten eksplisitt status, + // bruk fasens state — done=go, active=warning, future=neutral. + const detailsHtml = phases.map(function (p, i) { + const state = stepStateFor(p, i); + const tlStatus = state === 'completed' ? 'green' : state === 'current' ? 'yellow' : 'gray'; const ms = (p.milestones || []).map(function (m) { return '
  • ' + escapeHtml(m) + '
  • '; }).join(''); - const sc = (p.success_criteria || []).map(function (s) { return '
  • ' + escapeHtml(s) + '
  • '; }).join(''); + const sc = (p.success_criteria || []).map(function (s) { + return '
  • ' + + '' + + '' + + '' + escapeHtml(s) + '' + + '' + + '
  • '; + }).join(''); return '
    ' + '

    ' + escapeHtml(p.name) + ' (' + (p.duration_weeks || '?') + ' uker)

    ' + (ms ? '

    Milepæler

      ' + ms + '
    ' : '') + - (sc ? '

    Suksesskriterier

      ' + sc + '
    ' : '') + + (sc ? '

    Suksesskriterier

      ' + sc + '
    ' : '') + '
    '; }).join(''); const risksRows = (data.risks || []).map(function (r) { @@ -3932,24 +4166,86 @@ const risksHtml = risksRows ? '' + risksRows + '
    RisikoSannsynlighetKonsekvensTiltak
    ' : ''; - slot.innerHTML = cockpitHtml + detailsHtml + risksHtml; + const body = ladderHtml + detailsHtml + risksHtml; + // B1 verdict-pille: data.pocVerdict styrer (go/go-with-conditions/block). + // R15: hvis ikke satt, fall tilbake til risk-baserte heuristikk. + let verdict = data.verdict || data.pocVerdict; + if (!verdict) { + const risks = data.risks || []; + const critical = risks.some(function (r) { return /high|h[øo]y/i.test(r.consequence || '') && /high|h[øo]y/i.test(r.probability || ''); }); + verdict = critical ? 'go-with-conditions' : (risks.length ? 'go-with-conditions' : 'go'); + } + slot.innerHTML = renderPageShell({ + eyebrow: 'POC', + title: data.title || 'POC-plan', + lede: data.lede || 'Faseinndelt POC med mat-ladder, suksesskriterier og go/no-go-vurdering.', + verdict: verdict, + keyStats: data.keyStats || inferKeyStats(data, 'phased-plan') + }, body); } function renderUtredning(data, slot) { - const tocHtml = (data.sections || []).map(function (s, i) { - return '
  • ' + escapeHtml(s.heading) + '
  • '; + const sections = data.sections || []; + // A4 SCREEN-TABS: kuratert sett av 4 strukturerte tabs. + // R15: Hvis utredningen mangler en av seksjonene, hopp over den taben. + const tabSpec = [ + { id: 'bakgrunn', label: 'Bakgrunn', match: /\bbakgrunn\b/i }, + { id: 'funn', label: 'Funn', match: /\bfunn\b/i }, + { id: 'konklusjon', label: 'Konklusjon', match: /\bkonklusjon\b/i }, + { id: 'anbefaling', label: 'Anbefaling', match: /\banbefaling\b/i } + ]; + // Heading-normaliser: fjern "1. ", "1.2 " prefiks. + const normalize = function (h) { return String(h || '').replace(/^\s*\d+(\.\d+)*\s*\.?\s*/, '').trim(); }; + const findSec = function (m) { + return sections.find(function (s) { return m.test(normalize(s.heading)); }); + }; + const usedIdx = new Set(); + const tabs = tabSpec.map(function (t) { + const sec = findSec(t.match); + if (sec) usedIdx.add(sections.indexOf(sec)); + return { id: t.id, label: t.label, sec: sec }; + }).filter(function (t) { return t.sec; }); + // E8 read-more body: lange seksjoner (>500 tegn) skjuler hale i
    . + const renderBody = function (raw) { + const txt = String(raw || ''); + if (txt.length > 500) { + const head = txt.slice(0, 380); + const tail = txt.slice(380); + return '

    ' + escapeHtml(head).replace(/\n/g, '
    ') + '…

    ' + + '
    Vis hele seksjonen' + + '

    ' + escapeHtml(tail).replace(/\n/g, '
    ') + '

    ' + + '
    '; + } + return '
    ' + escapeHtml(txt).replace(/\n/g, '
    ') + '
    '; + }; + const tabsNavHtml = tabs.length ? '' : ''; + const tabsBodyHtml = tabs.map(function (t) { + return '
    ' + + '

    ' + escapeHtml(normalize(t.sec.heading)) + '

    ' + + renderBody(t.sec.body) + + '
    '; }).join(''); - const sectionsHtml = (data.sections || []).map(function (s, i) { - return '

    ' + escapeHtml(s.heading) + '

    ' + escapeHtml(s.body).replace(/\n/g, '
    ') + '
    '; - }).join(''); - slot.innerHTML = - '
    ' + - '' + - '
    ' + - '

    ' + escapeHtml(data.title || 'Utredning') + '

    ' + - sectionsHtml + - '
    ' + - '
    '; + // Resterende seksjoner (mandat, metode, referanser m.fl.) under en samlende read-more. + const otherSecs = sections.filter(function (s, i) { return !usedIdx.has(i); }); + const otherHtml = otherSecs.length ? '
    Vis øvrige seksjoner (' + otherSecs.length + ')' + + otherSecs.map(function (s) { + return '

    ' + escapeHtml(normalize(s.heading)) + '

    ' + renderBody(s.body) + '
    '; + }).join('') + + '
    ' : ''; + const body = tabsNavHtml + tabsBodyHtml + otherHtml; + slot.innerHTML = renderPageShell({ + eyebrow: 'UTREDNING', + title: data.title || 'AI-arkitekturutredning', + lede: data.lede || 'Strukturert utredning med kuraterte seksjoner: bakgrunn, funn, konklusjon og anbefaling.', + verdict: data.verdict || 'n-a', + keyStats: data.keyStats || [ + { label: 'TABS', value: tabs.length, hint: 'av 4' }, + { label: 'SEKSJONER', value: sections.length }, + { label: 'ØVRIGE', value: otherSecs.length, hint: 'andre seksjoner' } + ] + }, body); } function renderCompare(data, slot) { @@ -3965,6 +4261,35 @@ else if (fw2 && w.indexOf(fw2) >= 0) count2++; else lik++; }); + // Vinner: eksplisitt parseComparison.winner ELLER auto fra row-counts. + const explicitWin = String(data.winner || '').toLowerCase(); + let winnerIdx = -1; + if (explicitWin) { + if (fw1 && explicitWin.indexOf(fw1) >= 0) winnerIdx = 0; + else if (fw2 && explicitWin.indexOf(fw2) >= 0) winnerIdx = 1; + } + if (winnerIdx < 0 && (count1 || count2)) { + winnerIdx = count1 > count2 ? 0 : count2 > count1 ? 1 : -1; + } + // D1 scenario-cards-grid per kandidat. Vinner får data-status="met", + // taper "partial", tied/no-winner forblir "partial". + const cardSubjects = subjects.map(function (s, i) { + const wins = i === 0 ? count1 : count2; + const status = i === winnerIdx ? 'met' : 'partial'; + const total = (data.rows || []).length; + return { name: s, wins: wins, total: total, status: status, isWinner: i === winnerIdx }; + }); + const cardsHtml = '
    ' + cardSubjects.map(function (c) { + const winnerBadge = c.isWinner ? 'VINNER' : '' + c.wins + '/' + c.total + ''; + return '
    ' + + '
    ' + + 'KANDIDAT' + + winnerBadge + + '
    ' + + '

    ' + escapeHtml(c.name) + '

    ' + + '
    ' + c.wins + ' vinn · ' + (c.total - c.wins) + ' lik/tap
    ' + + '
    '; + }).join('') + '
    '; const summaryHtml = '
    ' + '
    ' + count1 + ' ' + escapeHtml(subjects[0]) + '
    ' + @@ -3986,7 +4311,27 @@ '
    ' + escapeHtml(r.aspect) + ': ' + escapeHtml(r.value2) + '
    ' + '
    '; }).join(''); - slot.innerHTML = '
    ' + summaryHtml + headerHtml + rowsHtml + '
    '; + const diffHtml = '
    ' + summaryHtml + headerHtml + rowsHtml + '
    '; + const body = cardsHtml + diffHtml; + // Verdict-pille: vinner satt → 'go' (klar anbefaling). Tied/uavklart → 'go-with-conditions'. + const verdict = data.verdict || (winnerIdx >= 0 ? 'go' : 'go-with-conditions'); + // Utvid comparison-keyStats med VINNER-felt. + const baseStats = inferKeyStats(data, 'comparison'); + const stats = data.keyStats || (winnerIdx >= 0 + ? baseStats.concat([{ + label: 'VINNER', + value: String(subjects[winnerIdx] || '').slice(0, 24), + hint: (winnerIdx === 0 ? count1 : count2) + ' vinn', + modifier: 'low' + }]) + : baseStats.concat([{ label: 'VINNER', value: 'UAVKLART', modifier: 'medium' }])); + slot.innerHTML = renderPageShell({ + eyebrow: 'SAMMENLIGN', + title: data.title || 'Sammenligning', + lede: data.lede || 'Aspekt-for-aspekt-sammenligning av to kandidater med vinner-pille og diff-tabell.', + verdict: verdict, + keyStats: stats + }, body); } // === V2_FOUNDATION_BEGIN === @@ -4368,6 +4713,13 @@ return; } const result = parser(markdown); + // Topic 2 strategi A: sentralisert _consumer-tildeling i import-flow. + // Respekterer parser-spesifikk verdi (f.eks. parseMatrixRisk → 'ros'). + // Renderere kan bruke _consumer for å velge variant-spesifikk markup + // der parser-arketypen er delt mellom flere commands. + if (result && result.ok && result.data && result.data._consumer == null) { + result.data._consumer = cmd.id; + } slot.innerHTML = ''; if (result.ok) renderer(result.data, slot); else renderError(result.errors, slot); diff --git a/plugins/ms-ai-architect/playground/test-fixtures/compare.md b/plugins/ms-ai-architect/playground/test-fixtures/compare.md index bb15e59..4148a28 100644 --- a/plugins/ms-ai-architect/playground/test-fixtures/compare.md +++ b/plugins/ms-ai-architect/playground/test-fixtures/compare.md @@ -31,6 +31,8 @@ Subject 2: Azure ML + AKS Azure AI Foundry vinner på time-to-prod, compliance-pakke, og driftbarhet. Azure ML + AKS vinner på pris (-12%) og fleksibilitet. Differansen i pris (~NOK 800k over 3 år) er liten sammenlignet med besparelsen i drift-tid for AI-teamet. +## Vinner: Azure AI Foundry + ## Anbefaling For Acme AS med begrenset KI-driftkapasitet anbefales Azure AI Foundry. For organisasjoner med dedikert MLOps-team kan Azure ML + AKS gi marginalt bedre kost-nytte. diff --git a/plugins/ms-ai-architect/playground/test-fixtures/migrate.md b/plugins/ms-ai-architect/playground/test-fixtures/migrate.md index a45ad1b..978ecf6 100644 --- a/plugins/ms-ai-architect/playground/test-fixtures/migrate.md +++ b/plugins/ms-ai-architect/playground/test-fixtures/migrate.md @@ -9,6 +9,7 @@ Til: Azure AI Foundry + saksbehandler-co-pilot ### Fase 1 — Foundry-fundament (uker 1-6) Varighet: 6 uker +Status: done Milepæler: - Hub + projects opprettet i West Europe @@ -24,6 +25,7 @@ Suksesskriterier: ### Fase 2 — Modell-trening og baseline (uker 7-14) Varighet: 8 uker +Status: done Milepæler: - Treningsdata kuratert (200k norske objekt-ID, stratifisert) @@ -39,6 +41,7 @@ Suksesskriterier: ### Fase 3 — saksbehandler-co-pilot (uker 15-22) Varighet: 8 uker +Status: active Milepæler: - Forklaringsmodell (GPT-4 Turbo) integrert via Foundry @@ -54,6 +57,7 @@ Suksesskriterier: ### Fase 4 — Compliance og produksjonssetting (uker 23-28) Varighet: 6 uker +Status: planned Milepæler: - FRIA gjennomført og godkjent diff --git a/plugins/ms-ai-architect/playground/test-fixtures/poc.md b/plugins/ms-ai-architect/playground/test-fixtures/poc.md index 7479ec7..a74887c 100644 --- a/plugins/ms-ai-architect/playground/test-fixtures/poc.md +++ b/plugins/ms-ai-architect/playground/test-fixtures/poc.md @@ -8,6 +8,7 @@ POC-mål: Validere at Azure AI Foundry kan dekke OCR + forklaring + audit innen ### Fase 1 — Foundation (uker 1-2) Varighet: 2 uker +Status: done Milepæler: - Foundry hub + project i West Europe @@ -22,6 +23,7 @@ Suksesskriterier: ### Fase 2 — OCR-modell (uker 3-5) Varighet: 3 uker +Status: active Milepæler: - Pre-trent Azure AI Vision OCR pilotert @@ -36,6 +38,7 @@ Suksesskriterier: ### Fase 3 — Forklarings-loop (uker 6-7) Varighet: 2 uker +Status: planned Milepæler: - GPT-4 Turbo via Foundry integrert @@ -50,6 +53,7 @@ Suksesskriterier: ### Fase 4 — Compliance-pre-check (uke 8) Varighet: 1 uke +Status: planned Milepæler: - Audit-logg mot EU AI Act Art. 12-krav @@ -70,6 +74,10 @@ Suksesskriterier: | saksbehandler-recruitment forsinker fase 3 | medium | low | Bruk interne ressurser i AI-teamet som mock | | Audit-logg-format ikke kompatibelt med Sentinel | low | medium | Test integrasjon i fase 1 | +## POC-Verdict: BETINGET + +Pilot-fase 1 fullført med F1=0.94 og inference-cost 0.038 NOK/kall (under budsjett). Fase 2 pågår — sammenligning av custom fine-tune mot pre-trent OCR i progress. Forklarings-loop og compliance-pre-check planlagt for siste halvdel. + ## Total varighet 8 uker. Beslutningskriterium for full prosjektgodkjenning: alle 4 fasers suksesskriterier møtt. diff --git a/plugins/ms-ai-architect/tests/test-playground-v3.sh b/plugins/ms-ai-architect/tests/test-playground-v3.sh index ed61953..ff91dfe 100755 --- a/plugins/ms-ai-architect/tests/test-playground-v3.sh +++ b/plugins/ms-ai-architect/tests/test-playground-v3.sh @@ -456,6 +456,103 @@ else fail "suppressed markup mangler (Step 11 must_contain krever >=1)" fi +# ------------------------------------------------------- +# 25e. SC8 — per-renderer verdict-pill emission for Sub-batch C (R7) +# Hver av de 8 Sub-batch C-rendererene må enten emitte data-verdict direkte +# i sin body, eller invokere renderPageShell (som emitter via helper). +# ------------------------------------------------------- +SC8_RENDERERS_C="renderCost renderLicense renderMigrate renderAdr renderSummary renderPoc renderUtredning renderCompare" +for fn in $SC8_RENDERERS_C; do + body=$(awk "/function $fn\(/,/^ \}$/" "$HTML_FILE") + if echo "$body" | grep -qE "verdict[^A-Za-z]*data-verdict\s*=\s*[\"'](go|go-with-conditions|block|approved|failed|allow|warning|n-a)[\"']" \ + || echo "$body" | grep -q "renderPageShell"; then + pass "SC8 verdict-pill: $fn (direkte eller via renderPageShell)" + else + fail "SC8 verdict-pill: $fn mangler både data-verdict og renderPageShell" + fi +done + +# ------------------------------------------------------- +# 25f. Step 12 must_contain — mat-ladder + screen-tabs + _consumer +# ------------------------------------------------------- +matladder_count=$( { grep -cE "mat-ladder" "$HTML_FILE" || true; } | tr -d ' ') +if [ "${matladder_count:-0}" -ge 1 ]; then + pass "mat-ladder markup til stede ($matladder_count treff, Step 12 must_contain)" +else + fail "mat-ladder markup mangler (Step 12 must_contain krever >=1)" +fi + +screentabs_count=$( { grep -cE "screen-tabs" "$HTML_FILE" || true; } | tr -d ' ') +if [ "${screentabs_count:-0}" -ge 1 ]; then + pass "screen-tabs markup til stede ($screentabs_count treff, Step 12 must_contain)" +else + fail "screen-tabs markup mangler (Step 12 must_contain krever >=1)" +fi + +consumer_count=$( { grep -cE "_consumer" "$HTML_FILE" || true; } | tr -d ' ') +if [ "${consumer_count:-0}" -ge 1 ]; then + pass "_consumer-mekanisme til stede ($consumer_count treff, Step 12 must_contain)" +else + fail "_consumer-mekanisme mangler (Step 12 must_contain krever >=1)" +fi + +# ------------------------------------------------------- +# 25g. Felles grunnskjelett — alle 17 renderers emiter page__eyebrow + h1 + verdict +# Step 12 strukturell assert per R7 + plan-spec. +# ------------------------------------------------------- +ALL_RENDERERS="renderAiActPyramid renderRequirements renderTransparency renderFria renderConformity renderDpia renderSecurity renderRos renderReview renderCost renderLicense renderMigrate renderAdr renderSummary renderPoc renderUtredning renderCompare" +shell_count=0 +for fn in $ALL_RENDERERS; do + body=$(awk "/function $fn\(/,/^ \}$/" "$HTML_FILE") + if echo "$body" | grep -q "renderPageShell"; then + shell_count=$((shell_count + 1)) + fi +done +if [ "$shell_count" -eq 17 ]; then + pass "Alle 17 renderers bruker renderPageShell (felles grunnskjelett: page__eyebrow + h1 + verdict via helper)" +else + fail "Kun $shell_count/17 renderers bruker renderPageShell — felles grunnskjelett ufullstendig" +fi + +# ------------------------------------------------------- +# 25h. Tier 3-bruk per Step 12 — kanban (conformity/review), mat-ladder (migrate/poc) +# ------------------------------------------------------- +for fn in renderConformity renderReview; do + body=$(awk "/function $fn\(/,/^ \}$/" "$HTML_FILE") + if echo "$body" | grep -qE "kanban-board|kanban-col"; then + pass "Tier 3 kanban: $fn bruker kanban-markup" + else + fail "Tier 3 kanban: $fn mangler kanban-markup" + fi +done +for fn in renderMigrate renderPoc; do + body=$(awk "/function $fn\(/,/^ \}$/" "$HTML_FILE") + if echo "$body" | grep -q "mat-ladder"; then + pass "Tier 3 mat-ladder: $fn bruker mat-ladder" + else + fail "Tier 3 mat-ladder: $fn mangler mat-ladder" + fi +done + +# ------------------------------------------------------- +# 25i. Onboarding-config field-type-distribution (4 strukturerte / 14 fritekst) +# Step 12 strukturell assert per R4 / R7. Counts ONBOARDING_SCHEMA-felter +# med type=select/multiSelect (strukturerte) vs text/textarea (fritekst). +# ------------------------------------------------------- +onb_block=$(awk '/const ONBOARDING_SCHEMA = \[/,/^ \];/' "$HTML_FILE") +struct_count=$(printf '%s\n' "$onb_block" | grep -cE "type:\s*'(select|multiSelect)'" | tr -d ' ') +free_count=$(printf '%s\n' "$onb_block" | grep -cE "type:\s*'(text|textarea)'" | tr -d ' ') +if [ "${struct_count:-0}" -ge 4 ] && [ "${struct_count:-0}" -le 5 ]; then + pass "Onboarding strukturerte felter: $struct_count (forventet 4)" +else + fail "Onboarding strukturerte felter: $struct_count (forventet 4)" +fi +if [ "${free_count:-0}" -ge 13 ] && [ "${free_count:-0}" -le 16 ]; then + pass "Onboarding fritekst-felter: $free_count (forventet ~14)" +else + fail "Onboarding fritekst-felter: $free_count (forventet ~14)" +fi + # ------------------------------------------------------- # 25. Inline-script eneste JS — ingen