diff --git a/playground/ms-ai-architect-v3.html b/playground/ms-ai-architect-v3.html
index 660ad56..bd93c02 100644
--- a/playground/ms-ai-architect-v3.html
+++ b/playground/ms-ai-architect-v3.html
@@ -2080,6 +2080,537 @@
);
}
+ // ============================================================
+ // MARKDOWN PARSERS (Step 11)
+ // ============================================================
+ //
+ // 14 parser-arketyper per kanonisk routing-tabell. Hver parser tar
+ // markdown-streng og returnerer { ok: true, data: {...} } eller
+ // { ok: false, errors: [{section, reason}] }. Parsers er tolerante
+ // (kaster aldri unntak) — tom/uventet input gir strukturert feil.
+ //
+ // Routing: PARSERS[archetype] for oppslag i handlePasteImport.
+
+ // ---- Felles helpers ----
+
+ function parseTableRow(line) {
+ const inner = line.replace(/^\|/, '').replace(/\|$/, '');
+ return inner.split('|').map(function (c) { return c.trim(); });
+ }
+
+ function parseTable(md, anchorRegex) {
+ if (typeof md !== 'string') return null;
+ let body = md;
+ if (anchorRegex) {
+ const m = anchorRegex.exec(md);
+ if (!m) return null;
+ body = md.slice(m.index + m[0].length);
+ }
+ const lines = body.split(/\r?\n/);
+ for (let i = 0; i < lines.length - 1; i++) {
+ const line = lines[i].trim();
+ const next = (lines[i + 1] || '').trim();
+ if (line.indexOf('|') === 0 && /^\|[\s\-:|]+\|$/.test(next)) {
+ const headers = parseTableRow(line);
+ const rows = [];
+ for (let j = i + 2; j < lines.length; j++) {
+ const rowLine = lines[j].trim();
+ if (rowLine.indexOf('|') !== 0) break;
+ const cells = parseTableRow(rowLine);
+ if (cells.length === 0) break;
+ const row = {};
+ for (let k = 0; k < headers.length; k++) {
+ row[headers[k]] = (cells[k] || '').trim();
+ }
+ rows.push(row);
+ }
+ return { headers: headers, rows: rows };
+ }
+ }
+ return null;
+ }
+
+ function parseSections(md) {
+ if (typeof md !== 'string') return [];
+ const sections = [];
+ const lines = md.split(/\r?\n/);
+ let current = null;
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const m = /^##\s+(.+)$/.exec(line);
+ if (m && line.charAt(2) === ' ') { // exactly two #
+ if (current) sections.push(current);
+ current = { heading: m[1].trim(), body: '' };
+ } else if (current) {
+ current.body += (current.body ? '\n' : '') + line;
+ }
+ }
+ if (current) sections.push(current);
+ return sections.map(function (s) {
+ return { heading: s.heading, body: s.body.trim() };
+ });
+ }
+
+ function extractField(md, label) {
+ if (typeof md !== 'string') return null;
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const re = new RegExp('^\\s*' + escaped + '\\s*:\\s*(.+)$', 'mi');
+ const m = re.exec(md);
+ return m ? m[1].trim() : null;
+ }
+
+ function intOrZero(s) {
+ if (typeof s !== 'string') return 0;
+ const v = parseInt(s.replace(/[^\d-]/g, ''), 10);
+ return isNaN(v) ? 0 : v;
+ }
+
+ function emptyInput(md) {
+ return !md || typeof md !== 'string' || !md.trim();
+ }
+
+ // ---- 14 archetype parsers ----
+
+ function parseAiAct(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const errors = [];
+ const sections = parseSections(md);
+
+ let risk_level = extractField(md, 'Risk-level') || extractField(md, 'Risikonivå');
+ if (!risk_level) {
+ const sec = sections.find(function (s) { return /risikoniv|risk.level/i.test(s.heading); });
+ if (sec) {
+ const firstLine = sec.body.split(/\r?\n/)[0] || '';
+ risk_level = firstLine.replace(/^Risk-level:\s*/i, '').replace(/^Risikonivå:\s*/i, '').trim();
+ }
+ }
+ if (!risk_level) errors.push({ section: 'risk_level', reason: 'Fant ikke risikonivå' });
+
+ const role = extractField(md, 'Rolle') || extractField(md, 'Role') || '';
+ if (!role) errors.push({ section: 'role', reason: 'Fant ikke rolle' });
+
+ let reasoning = extractField(md, 'Reasoning') || extractField(md, 'Begrunnelse') || '';
+ if (!reasoning) {
+ const sec = sections.find(function (s) { return /begrunnelse|reasoning/i.test(s.heading); });
+ if (sec) reasoning = sec.body;
+ }
+
+ const obligations = [];
+ const oblSec = sections.find(function (s) { return /forpliktelser|obligations/i.test(s.heading); });
+ if (oblSec) {
+ oblSec.body.split(/\r?\n/).forEach(function (line) {
+ const m = /^[-*]\s+(.+)$/.exec(line.trim());
+ if (m) obligations.push(m[1].trim());
+ });
+ }
+
+ if (errors.length > 0) return { ok: false, errors: errors };
+ return {
+ ok: true,
+ data: {
+ risk_level: (risk_level || '').toLowerCase(),
+ role: role,
+ reasoning: reasoning,
+ obligations: obligations
+ }
+ };
+ }
+
+ function parseRequirements(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const tbl = parseTable(md);
+ if (!tbl) return { ok: false, errors: [{ section: 'table', reason: 'Ingen krav-tabell funnet' }] };
+ const reqKey = tbl.headers.find(function (h) { return /krav|requirement/i.test(h); }) || tbl.headers[0];
+ const statusKey = tbl.headers.find(function (h) { return /status/i.test(h); }) || tbl.headers[1];
+ const sourceKey = tbl.headers.find(function (h) { return /kilde|source|art/i.test(h); }) || tbl.headers[2];
+ const items = tbl.rows.map(function (row) {
+ return {
+ requirement: row[reqKey] || '',
+ status: (row[statusKey] || '').toLowerCase().trim(),
+ source_article: row[sourceKey] || ''
+ };
+ });
+ return { ok: true, data: { items: items } };
+ }
+
+ function parseTextDocument(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const sections = parseSections(md);
+ if (!sections.length) {
+ return { ok: true, data: { sections: [{ heading: 'Innhold', body: md.trim() }] } };
+ }
+ return { ok: true, data: { sections: sections } };
+ }
+
+ function parseFria(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const tbl = parseTable(md);
+ if (!tbl) return { ok: false, errors: [{ section: 'table', reason: 'Ingen rettighet-tabell funnet' }] };
+ const nameKey = tbl.headers.find(function (h) { return /rettighet|right/i.test(h); }) || tbl.headers[0];
+ const impactKey = tbl.headers.find(function (h) { return /impact|påvirkning/i.test(h); }) || tbl.headers[1];
+ const mitigKey = tbl.headers.find(function (h) { return /tiltak|mitigation/i.test(h); }) || tbl.headers[2];
+ const rights = tbl.rows.map(function (row) {
+ return {
+ name: row[nameKey] || '',
+ impact: intOrZero(row[impactKey] || '0'),
+ mitigation: row[mitigKey] || ''
+ };
+ });
+ return { ok: true, data: { rights: rights } };
+ }
+
+ function parseConformityChecklist(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const checklistTbl = parseTable(md, /##\s*Sjekkliste/i) || parseTable(md);
+ if (!checklistTbl) return { ok: false, errors: [{ section: 'checklist', reason: 'Ingen sjekkliste-tabell funnet' }] };
+ const reqKey = checklistTbl.headers.find(function (h) { return /krav|requirement/i.test(h); }) || checklistTbl.headers[0];
+ const statusKey = checklistTbl.headers.find(function (h) { return /status/i.test(h); }) || checklistTbl.headers[1];
+ const evidKey = checklistTbl.headers.find(function (h) { return /bevis|evidence/i.test(h); }) || checklistTbl.headers[2];
+ const checklist = checklistTbl.rows.map(function (row) {
+ return {
+ requirement: row[reqKey] || '',
+ status: (row[statusKey] || '').toLowerCase().trim(),
+ evidence: row[evidKey] || ''
+ };
+ });
+ const deadlinesTbl = parseTable(md, /##\s*Frister/i);
+ const deadlines = deadlinesTbl ? deadlinesTbl.rows.map(function (row) {
+ const dateKey = deadlinesTbl.headers.find(function (h) { return /dato|date/i.test(h); }) || deadlinesTbl.headers[0];
+ const mileKey = deadlinesTbl.headers.find(function (h) { return /milepæl|milestone/i.test(h); }) || deadlinesTbl.headers[1];
+ const stKey = deadlinesTbl.headers.find(function (h) { return /status/i.test(h); }) || deadlinesTbl.headers[2];
+ return {
+ date: row[dateKey] || '',
+ milestone: row[mileKey] || '',
+ status: (row[stKey] || '').toLowerCase().trim()
+ };
+ }) : [];
+ return { ok: true, data: { checklist: checklist, deadlines: deadlines } };
+ }
+
+ function parseMatrixRisk(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const matrixTbl = parseTable(md, /Risikomatrise.*5/i) || parseTable(md);
+ if (!matrixTbl) return { ok: false, errors: [{ section: 'matrix', reason: 'Ingen risikomatrise funnet' }] };
+ const labelKey = matrixTbl.headers[0];
+ const sannKey = matrixTbl.headers.find(function (h) { return /sannsynlig/i.test(h); });
+ const konsKey = matrixTbl.headers.find(function (h) { return /konsekvens/i.test(h); });
+ const scoreKey = matrixTbl.headers.find(function (h) { return /score/i.test(h); });
+ const matrix_cells = matrixTbl.rows.map(function (row) {
+ return {
+ label: row[labelKey] || '',
+ prob: intOrZero(row[sannKey] || '0'),
+ cons: intOrZero(row[konsKey] || '0'),
+ score: intOrZero(row[scoreKey] || '0')
+ };
+ });
+ const threatsTbl = parseTable(md, /##\s*Trusler/i);
+ const threats = threatsTbl ? threatsTbl.rows.map(function (row) {
+ const idKey = threatsTbl.headers[0];
+ const descKey = threatsTbl.headers.find(function (h) { return /beskrivelse|description/i.test(h); }) || threatsTbl.headers[1];
+ const sevKey = threatsTbl.headers.find(function (h) { return /severity|alvorlighet/i.test(h); });
+ const mitKey = threatsTbl.headers.find(function (h) { return /tiltak|mitigation/i.test(h); });
+ return {
+ id: row[idKey] || '',
+ description: row[descKey] || '',
+ severity: (row[sevKey] || '').toLowerCase().trim(),
+ mitigation: row[mitKey] || ''
+ };
+ }) : [];
+ const radarTbl = parseTable(md, /Radar.akser/i);
+ const radar_axes = radarTbl ? radarTbl.rows.map(function (row) {
+ const akseKey = radarTbl.headers.find(function (h) { return /akse|axis/i.test(h); }) || radarTbl.headers[0];
+ const scKey = radarTbl.headers.find(function (h) { return /score/i.test(h); }) || radarTbl.headers[1];
+ return {
+ name: row[akseKey] || '',
+ score: intOrZero(row[scKey] || '0')
+ };
+ }) : null;
+ return { ok: true, data: { matrix_cells: matrix_cells, threats: threats, radar_axes: radar_axes } };
+ }
+
+ function parseMatrixRisk6x5(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const dimsTbl = parseTable(md, /Score per dimensjon/i);
+ if (!dimsTbl) return { ok: false, errors: [{ section: 'dimensions', reason: 'Ingen dimensjon-tabell funnet' }] };
+ const dimNameKey = dimsTbl.headers.find(function (h) { return /dimensjon/i.test(h); }) || dimsTbl.headers[0];
+ const dimScoreKey = dimsTbl.headers.find(function (h) { return /score/i.test(h); }) || dimsTbl.headers[1];
+ const dimVurdKey = dimsTbl.headers.find(function (h) { return /vurdering/i.test(h); });
+ const dimensions = dimsTbl.rows.map(function (row) {
+ return {
+ name: row[dimNameKey] || '',
+ score: intOrZero(row[dimScoreKey] || '0'),
+ assessment: row[dimVurdKey] || ''
+ };
+ });
+ const matrixTbl = parseTable(md, /Risikomatrise.*6/i);
+ const matrix_cells = matrixTbl ? matrixTbl.rows.map(function (row) {
+ const labelKey = matrixTbl.headers[0];
+ const sannKey = matrixTbl.headers.find(function (h) { return /sannsynlig/i.test(h); });
+ const konsKey = matrixTbl.headers.find(function (h) { return /konsekvens/i.test(h); });
+ const scoreKey = matrixTbl.headers.find(function (h) { return /score/i.test(h); });
+ return {
+ label: row[labelKey] || '',
+ prob: intOrZero(row[sannKey] || '0'),
+ cons: intOrZero(row[konsKey] || '0'),
+ score: intOrZero(row[scoreKey] || '0')
+ };
+ }) : [];
+ const findingsTbl = parseTable(md, /##\s*Funn/i);
+ const findings = findingsTbl ? findingsTbl.rows.map(function (row) {
+ const idKey = findingsTbl.headers[0];
+ const sevKey = findingsTbl.headers.find(function (h) { return /severity|alvorlighet/i.test(h); });
+ const locKey = findingsTbl.headers.find(function (h) { return /lokasjon|location/i.test(h); });
+ const recKey = findingsTbl.headers.find(function (h) { return /anbefaling|recommendation/i.test(h); });
+ return {
+ id: row[idKey] || '',
+ severity: (row[sevKey] || '').toLowerCase().trim(),
+ location: row[locKey] || '',
+ recommendation: row[recKey] || ''
+ };
+ }) : [];
+ return {
+ ok: true,
+ data: {
+ dimensions: dimensions,
+ matrix_cells: matrix_cells,
+ findings: findings,
+ scores: dimensions.map(function (d) { return d.score; })
+ }
+ };
+ }
+
+ function parseFindings(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const tbl = parseTable(md, /##\s*Funn/i) || parseTable(md);
+ if (!tbl) return { ok: false, errors: [{ section: 'table', reason: 'Ingen funn-tabell funnet' }] };
+ const idKey = tbl.headers[0];
+ const sevKey = tbl.headers.find(function (h) { return /severity|alvorlighet/i.test(h); });
+ const locKey = tbl.headers.find(function (h) { return /lokasjon|location/i.test(h); });
+ const recKey = tbl.headers.find(function (h) { return /anbefaling|recommendation/i.test(h); });
+ const findings = tbl.rows.map(function (row) {
+ return {
+ id: row[idKey] || '',
+ severity: (row[sevKey] || '').toLowerCase().trim(),
+ location: row[locKey] || '',
+ recommendation: row[recKey] || ''
+ };
+ });
+ return { ok: true, data: { findings: findings } };
+ }
+
+ function parseCostDistribution(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const distTbl = parseTable(md, /Distribusjon/i);
+ if (!distTbl) return { ok: false, errors: [{ section: 'distribution', reason: 'Ingen distribusjons-tabell funnet' }] };
+ const persKey = distTbl.headers.find(function (h) { return /persentil|percentile/i.test(h); }) || distTbl.headers[0];
+ const monthlyKey = distTbl.headers.find(function (h) { return /månedlig|monthly/i.test(h); }) || distTbl.headers[1];
+ const yearlyKey = distTbl.headers.find(function (h) { return /årlig|yearly/i.test(h); });
+ let p10 = null, p50 = null, p90 = null;
+ distTbl.rows.forEach(function (row) {
+ const monthly = intOrZero(row[monthlyKey] || '0');
+ const yearly = yearlyKey ? intOrZero(row[yearlyKey] || '0') : null;
+ const entry = { monthly: monthly, yearly: yearly };
+ const tag = (row[persKey] || '').toUpperCase();
+ if (/P10|P\.10|P 10/.test(tag)) p10 = entry;
+ else if (/P50|P\.50|P 50/.test(tag)) p50 = entry;
+ else if (/P90|P\.90|P 90/.test(tag)) p90 = entry;
+ });
+ const monthlyTbl = parseTable(md, /Månedlig fordeling/i);
+ const monthly_breakdown = monthlyTbl ? monthlyTbl.rows.map(function (row) {
+ const compKey = monthlyTbl.headers[0];
+ const costKey = monthlyTbl.headers[1];
+ return {
+ component: row[compKey] || '',
+ cost: intOrZero(row[costKey] || '0')
+ };
+ }) : [];
+ const tcoTbl = parseTable(md, /TCO/i);
+ const tco_table = tcoTbl ? tcoTbl.rows : [];
+ return {
+ ok: true,
+ data: {
+ p10: p10, p50: p50, p90: p90,
+ monthly_breakdown: monthly_breakdown,
+ tco_table: tco_table,
+ tco_headers: tcoTbl ? tcoTbl.headers : []
+ }
+ };
+ }
+
+ function parseCapabilityMatrix(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const tbl = parseTable(md, /##\s*Matrise/i) || parseTable(md);
+ if (!tbl) return { ok: false, errors: [{ section: 'matrix', reason: 'Ingen matrise funnet' }] };
+ const capKey = tbl.headers[0];
+ const licenseNames = tbl.headers.slice(1);
+ const licenses = licenseNames.map(function (name) {
+ return { name: name, capabilities: [] };
+ });
+ tbl.rows.forEach(function (row) {
+ const capName = row[capKey];
+ licenseNames.forEach(function (licName, i) {
+ licenses[i].capabilities.push({
+ name: capName,
+ status: (row[licName] || '').toLowerCase().trim()
+ });
+ });
+ });
+ return { ok: true, data: { licenses: licenses } };
+ }
+
+ function parsePhasedPlan(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const phases = [];
+ const lines = md.split(/\r?\n/);
+ let current = null;
+ let bucket = null;
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const phaseMatch = /^###\s+(?:Fase\s+\d+\s*[—-]\s*)?(.+?)\s*(?:\(.*\))?\s*$/i.exec(line.trim());
+ const isH3 = /^###\s+/.test(line);
+ const isH2 = /^##\s+/.test(line) && !isH3;
+ if (isH3 && phaseMatch) {
+ if (current) phases.push(current);
+ current = {
+ name: phaseMatch[1].trim(),
+ milestones: [],
+ success_criteria: [],
+ duration_weeks: null
+ };
+ bucket = null;
+ continue;
+ }
+ if (isH2) {
+ if (current) { phases.push(current); current = null; }
+ bucket = null;
+ continue;
+ }
+ if (!current) continue;
+ const trimmed = line.trim();
+ const durMatch = /^Varighet:\s*(\d+)\s*uke/i.exec(trimmed);
+ if (durMatch) {
+ current.duration_weeks = parseInt(durMatch[1], 10);
+ 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);
+ if (bulletMatch && bucket && current[bucket]) {
+ current[bucket].push(bulletMatch[1].trim());
+ }
+ }
+ if (current) phases.push(current);
+
+ const risksTbl = parseTable(md, /##\s*Risiko/i);
+ const risks = risksTbl ? risksTbl.rows.map(function (row) {
+ const risikoKey = risksTbl.headers[0];
+ const sannKey = risksTbl.headers.find(function (h) { return /sannsynlig/i.test(h); });
+ const konsKey = risksTbl.headers.find(function (h) { return /konsekvens/i.test(h); });
+ const tiltakKey = risksTbl.headers.find(function (h) { return /tiltak|mitigation/i.test(h); });
+ return {
+ risk: row[risikoKey] || '',
+ probability: row[sannKey] || '',
+ consequence: row[konsKey] || '',
+ mitigation: row[tiltakKey] || ''
+ };
+ }) : [];
+
+ if (!phases.length) return { ok: false, errors: [{ section: 'phases', reason: 'Ingen faser funnet (### Fase N)' }] };
+ return { ok: true, data: { phases: phases, risks: risks } };
+ }
+
+ function parseMarkdown(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const titleMatch = /^#\s+(.+)$/m.exec(md);
+ const title = titleMatch ? titleMatch[1].trim() : '';
+ const sections = parseSections(md);
+ // Frontmatter-style fields (Status, Date, Deciders) — typisk i ADR
+ const status = extractField(md, 'Status') || '';
+ const date = extractField(md, 'Date') || extractField(md, 'Dato') || '';
+ const deciders = extractField(md, 'Deciders') || extractField(md, 'Beslutningstakere') || '';
+ return { ok: true, data: { title: title, sections: sections, raw: md, status: status, date: date, deciders: deciders } };
+ }
+
+ function parseVerdict(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const verdictRaw = extractField(md, 'Verdict') || '';
+ const verdict = verdictRaw.toLowerCase().trim();
+ const sub = extractField(md, 'Sub') || '';
+ const sections = parseSections(md);
+ const ratSec = sections.find(function (s) { return /rationale|begrunnelse/i.test(s.heading); });
+ const rationale = ratSec ? ratSec.body : '';
+ const metricsTbl = parseTable(md, /Key Metrics|Nøkkelmetrikker/i);
+ const key_metrics = metricsTbl ? metricsTbl.rows : [];
+ const metrics_headers = metricsTbl ? metricsTbl.headers : [];
+ const nextSec = sections.find(function (s) { return /next steps|neste steg/i.test(s.heading); });
+ const next_steps = [];
+ if (nextSec) {
+ nextSec.body.split(/\r?\n/).forEach(function (line) {
+ const m = /^[-*]\s+(.+)$/.exec(line.trim());
+ if (m) next_steps.push(m[1].trim());
+ });
+ }
+ if (!verdict) return { ok: false, errors: [{ section: 'verdict', reason: 'Fant ikke "Verdict:"-linje' }] };
+ return {
+ ok: true,
+ data: {
+ verdict: verdict,
+ sub: sub,
+ rationale: rationale,
+ key_metrics: key_metrics,
+ metrics_headers: metrics_headers,
+ next_steps: next_steps
+ }
+ };
+ }
+
+ function parseComparison(md) {
+ if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] };
+ const subject1 = extractField(md, 'Subject 1') || '';
+ const subject2 = extractField(md, 'Subject 2') || '';
+ const tbl = parseTable(md, /##\s*Sammenligning|##\s*Comparison/i) || parseTable(md);
+ if (!tbl) return { ok: false, errors: [{ section: 'table', reason: 'Ingen sammenligningstabell funnet' }] };
+ const aspectKey = tbl.headers[0];
+ const v1Key = tbl.headers[1];
+ const v2Key = tbl.headers[2];
+ const winnerKey = tbl.headers[3];
+ const subjects = [subject1 || v1Key || '', subject2 || v2Key || ''];
+ const rows = tbl.rows.map(function (row) {
+ return {
+ aspect: row[aspectKey] || '',
+ value1: row[v1Key] || '',
+ value2: row[v2Key] || '',
+ winner: winnerKey ? (row[winnerKey] || '') : ''
+ };
+ });
+ return { ok: true, data: { subjects: subjects, rows: rows } };
+ }
+
+ // ---- PARSERS routing-objekt ----
+
+ const PARSERS = {
+ 'aiact': parseAiAct,
+ 'requirements-list': parseRequirements,
+ 'text-document': parseTextDocument,
+ 'fria': parseFria,
+ 'conformity-checklist': parseConformityChecklist,
+ 'matrix-risk': parseMatrixRisk,
+ 'matrix-risk-6x5': parseMatrixRisk6x5,
+ 'findings': parseFindings,
+ 'cost-distribution': parseCostDistribution,
+ 'capability': parseCapabilityMatrix,
+ 'phased-plan': parsePhasedPlan,
+ 'markdown': parseMarkdown,
+ 'verdict': parseVerdict,
+ 'comparison': parseComparison
+ };
+
+ // Eksponer for Verify-asserts og Step 12.
+ window.__PARSERS = PARSERS;
+ window.__parseTable = parseTable;
+ window.__parseSections = parseSections;
+ window.__extractField = extractField;
+
// ---- Paste-import stub (Step 12 erstatter med faktisk routing) ----
function handlePasteImport(commandId, markdown) {