feat(ms-ai-architect): playground v3 markdown parsers (14 archetypes) [skip-docs]
14 tolerant parsers per kanonisk archetype-routing-tabell (Step 11) + 3 helpers (parseTable, parseSections, extractField). Each parser returns {ok:true, data} or {ok:false, errors:[{section, reason}]} — never throws on bad input. PARSERS routing-objekt eksponert via window.__PARSERS.
Verified against all 17 fixtures: every parser produces expected shape. Empty input returns structured error per Verify-asserts.
This commit is contained in:
parent
b4a5ff0c75
commit
1034777d6b
1 changed files with 531 additions and 0 deletions
|
|
@ -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) ----
|
// ---- Paste-import stub (Step 12 erstatter med faktisk routing) ----
|
||||||
|
|
||||||
function handlePasteImport(commandId, markdown) {
|
function handlePasteImport(commandId, markdown) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue