// innboks-split.mjs // Step 4: deterministisk heading-split av (allerede konvertert) markdown til // konsepter. REN funksjon -- fil-settet er en funksjon av input alene (INGEN // LLM, ingen veggklokke), saa run1 og run2 gir byte-identisk resultat (SC // idempotens by construction). Split paa #/## (MarkdownHeaderTextSplitter- // moenster): hver #/##-heading starter et nytt konsept; ### og dypere forblir // body-innhold. Dokument uten brukbare overskrifter -> fallback "ett dokument = // ett konsept". Innhold foer foerste heading bevares som ledende konsept (ingen // datatap). Zero npm dependencies. // Kebab-slug: lowercase, strip diakritika defensivt, ikke-alfanum -> '-', trim. // Never-empty fallback 'konsept' (deterministisk; aldri tomt filnavn). function slugify(text) { const s = String(text) .toLowerCase() .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); return s || 'konsept'; } // #/## heading-linje -> { level, title }. ### og dypere matcher IKKE: '#{1,2}' // kan ikke etterfoelges av '\s+' naar tredje tegn er '#', saa de forblir body. const HEADING_RE = /^(#{1,2})\s+(.+?)\s*$/; // markdown -> Concept[]; opts.sourceSlug identifiserer kilde-dokumentet (brukt // som tittel/slug for flat-fallback + preamble). Concept-form: // { sourceSlug, title, slug, level, body } export function splitConcepts(markdown, { sourceSlug } = {}) { const src = typeof sourceSlug === 'string' && sourceSlug.trim() !== '' ? sourceSlug : 'konsept'; const normalized = String(markdown).replace(/\r\n/g, '\n'); const lines = normalized.split('\n'); // Segmenter: ett per #/##-heading. Linjer foer foerste heading -> preamble. const segments = []; const preambleLines = []; let current = null; for (const line of lines) { const m = line.match(HEADING_RE); if (m) { current = { title: m[2].trim(), level: m[1].length, lines: [] }; segments.push(current); } else if (current) { current.lines.push(line); } else { preambleLines.push(line); } } const concepts = []; const seen = new Map(); const uniqueSlug = (base) => { const n = seen.get(base) || 0; seen.set(base, n + 1); return n === 0 ? base : `${base}-${n + 1}`; }; // Reelt innhold foer foerste heading -> ledende konsept (titulert av kilde). if (preambleLines.join('').trim() !== '') { concepts.push({ sourceSlug: src, title: src, slug: uniqueSlug(slugify(src)), level: 1, body: preambleLines.join('\n').trim(), }); } // Flat dokument (ingen #/##): hele dokumentet = ett konsept. if (segments.length === 0) { if (concepts.length === 0) { concepts.push({ sourceSlug: src, title: src, slug: uniqueSlug(slugify(src)), level: 1, body: normalized.trim(), }); } return concepts; } for (const seg of segments) { concepts.push({ sourceSlug: src, title: seg.title, slug: uniqueSlug(slugify(seg.title)), level: seg.level, body: seg.lines.join('\n').trim(), }); } return concepts; }