diff --git a/scripts/okf-index.mjs b/scripts/okf-index.mjs index 81f5a61..5a79657 100644 --- a/scripts/okf-index.mjs +++ b/scripts/okf-index.mjs @@ -26,6 +26,13 @@ import { parseFrontmatter } from '../lib/frontmatter.mjs'; export const OKF_VERSION = 'kb-layout-2026-06'; +// Drop-zone (raa innboks-filer) + skjulte kataloger (.cache osv.) er ikke nivaaer: +// de skal verken faa egen index.md eller listes som underkatalog-peker. Maa +// filtreres i BEGGE enumererings-steder (subdir-listing + rekursjon). +function isWalkableDir(name) { + return !name.startsWith('.') && name !== 'innboks'; +} + // "strategisk-kontekst" -> "Strategisk kontekst" function titleFromName(name) { const spaced = name.replace(/[-_]+/g, ' ').trim(); @@ -49,16 +56,37 @@ function parseExistingIndex(path) { return result; } -// Bygg en enkelt entry-linje paa OKF-form. Tom beskrivelse -> dropp ` - d`. +// Saner en frontmatter-avledet tittel/beskrivelse for trygg, idempotent emit: +// noytraliser markdown-lenker (RAG-injeksjon), strip kontrolltegn + strooe ]/) +// som ville korrumpert round-trip-parsen (parseExistingIndex), kollaps whitespace, +// og cap lengden. Idempotent: sanitizeEntry(sanitizeEntry(x)) === sanitizeEntry(x). +function sanitizeEntry(s) { + if (!s) return ''; + return String(s) + .replace(/[\x00-\x1f\x7f]/g, ' ') // kontrolltegn -> mellomrom + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // noytraliser markdown-lenker (behold tekst) + .replace(/[\])]/g, '') // strip strooe ] ) som brekker round-trip + .replace(/\s+/g, ' ') + .trim() + .slice(0, 200); +} + +// Bygg en enkelt entry-linje paa OKF-form. Tittel/beskrivelse saneres (link +// forblir uroert -- kontrollert filnavn). Tom beskrivelse -> dropp ` - d`. function entryLine(title, link, desc) { - return desc ? `* [${title}](${link}) - ${desc}` : `* [${title}](${link})`; + const t = sanitizeEntry(title); + const d = sanitizeEntry(desc); + return d ? `* [${t}](${link}) - ${d}` : `* [${t}](${link})`; } // Generer og skriv index.md for EN katalog (ikke rekursivt). isRoot styrer okf_version. function writeIndexFor(dir, isRoot, okfVersion) { const existing = parseExistingIndex(join(dir, 'index.md')); const dirents = readdirSync(dir, { withFileTypes: true }); - const subdirs = dirents.filter((e) => e.isDirectory()).map((e) => e.name).sort(); + const subdirs = dirents + .filter((e) => e.isDirectory() && isWalkableDir(e.name)) + .map((e) => e.name) + .sort(); const concepts = dirents .filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') .map((e) => e.name) @@ -95,7 +123,7 @@ export function generateIndexes(root, opts = {}) { const walk = (dir, isRoot) => { writeIndexFor(dir, isRoot, okfVersion); for (const e of readdirSync(dir, { withFileTypes: true })) { - if (e.isDirectory()) walk(join(dir, e.name), false); + if (e.isDirectory() && isWalkableDir(e.name)) walk(join(dir, e.name), false); } }; if (!existsSync(root)) throw new Error(`Bundle-rot finnes ikke: ${root}`); diff --git a/tests/okf-check.test.mjs b/tests/okf-check.test.mjs index daee49a..887fac5 100644 --- a/tests/okf-check.test.mjs +++ b/tests/okf-check.test.mjs @@ -65,6 +65,16 @@ function allDirs(root) { return out; } +// Byte-snapshot av alle index.md under root (for idempotens-sammenligning). +function snapshotIndexes(root) { + return allDirs(root) + .map((d) => join(d, 'index.md')) + .filter((p) => existsSync(p)) + .sort() + .map((p) => `${p}\n${readFileSync(p, 'utf8')}`) + .join('\n=====\n'); +} + // --- okf-index --- test('okf-index: genererer index.md per nivaa (hver katalog) i temp-kopi', () => { @@ -254,3 +264,49 @@ test('okf-check: raa filer i innboks/ og .cache/ ignoreres av walk (ingen falsk rmSync(dir, { recursive: true, force: true }); } }); + +// --- okf-index herding (Step 9): saner title/desc + skip innboks/dot --- + +test('okf-index: tittel/beskrivelse med ]/) + lenke saneres -> 2x byte-identisk + round-trip-trygg', () => { + const dir = tmpRoot(); + try { + mkdirSync(join(dir, 'dokumenter'), { recursive: true }); + writeFileSync(join(dir, 'index.md'), '# Bundle\n\nokf_version: kb-layout-2026-06\n'); + writeFileSync( + join(dir, 'dokumenter', 'kr.md'), + '---\ntype: OKR\ntitle: "KR1 [resultat] (maal) [lenke](http://x)"\n' + + 'description: "Status (delvis) [ref](http://y)"\n---\n# KR\n', + ); + generateIndexes(dir); + const run1 = snapshotIndexes(dir); + generateIndexes(dir); + const run2 = snapshotIndexes(dir); + assert.equal(run1, run2, 'generateIndexes skal vaere byte-idempotent'); + const idx = readFileSync(join(dir, 'dokumenter', 'index.md'), 'utf8'); + const entry = idx.split('\n').find((l) => l.startsWith('* ')); + assert.ok(entry, 'kr.md skal ha en entry'); + assert.match(entry, /^\* \[[^\]]*\]\([^)]+\)/, 'entry skal vaere round-trip-trygg OKF-form'); + assert.ok(!entry.includes('http'), 'injisert lenke skal vaere noytralisert'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('okf-index: innboks/ + dot-katalog -> ingen egen index.md OG ikke listet i rot-index', () => { + const dir = tmpRoot(); + try { + cpSync(REALISTIC, dir, { recursive: true }); + mkdirSync(join(dir, 'innboks'), { recursive: true }); + mkdirSync(join(dir, '.cache'), { recursive: true }); + writeFileSync(join(dir, 'innboks', 'raa.md'), '# Raa\n'); + writeFileSync(join(dir, '.cache', 'c.md'), '# Cache\n'); + generateIndexes(dir); + assert.ok(!existsSync(join(dir, 'innboks', 'index.md')), 'innboks/ skal ikke faa index.md'); + assert.ok(!existsSync(join(dir, '.cache', 'index.md')), 'dot-katalog skal ikke faa index.md'); + const root = readFileSync(join(dir, 'index.md'), 'utf8'); + assert.ok(!root.includes('innboks/index.md'), 'rot-index skal ikke liste innboks/ som peker'); + assert.ok(!root.includes('.cache/index.md'), 'rot-index skal ikke liste dot-katalog som peker'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +});