8 hygiene-fikser fra review 2026-07-16 par.4 kode-lista, TDD roed-groenn (suite 138 -> 149): - innboks-split: slugify translittererer ae/oe (datatap-fiks) - innboks-frontmatter: beskrivende feil ved manglende sourceMtime - okf-index: --okf-version bumper eksisterende rot, flagg-tolerant CLI (exit 2 ved manglende verdi), sanitizeEntry strip C1/zero-width/bidi/ Unicode-tag - write-org-profile: circuit-breaker MERGER i stedet for aa overskrive full config (M4); test beviser at eksisterende config overlever - compose-org-profile: intern ----linje trunkerer ikke blokken - coaching-hook: at-risk teller status-markerte tabellrader (M1/m1) - inject-okr-context: topic-guard treffer boeyningsformer (maalene) - frontmatter: BOM/CRLF-toleranse (falsk mangler-type-fiks) Versjonsflater bumpet til 1.7.1 (package/plugin/lock/CLAUDE/README/ SKILL x2/package-shape-test) + CHANGELOG 1.7.1-seksjon (B1+B2). Release-tag + katalog-ref venter paa [G-B] operatoer-go. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
3.3 KiB
JavaScript
100 lines
3.3 KiB
JavaScript
// 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, translitterer ae/oe (B2 -- NFKD dekomponerer ikke
|
|
// disse, saa uten dette droppes bokstaven: "OEkonomi" -> "konomi"), strip
|
|
// diakritika defensivt, ikke-alfanum -> '-', trim.
|
|
// Never-empty fallback 'konsept' (deterministisk; aldri tomt filnavn).
|
|
function slugify(text) {
|
|
const s = String(text)
|
|
.toLowerCase()
|
|
.replace(/\u00e6/g, 'ae')
|
|
.replace(/\u00f8/g, 'oe')
|
|
.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;
|
|
}
|