fix(okr): okf-index saner title/desc + escape klammer + skip innboks/dot (SC index-integritet)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_012HzGPGJ81k1BkUC6UvgX4Y
This commit is contained in:
Kjell Tore Guttormsen 2026-06-30 14:01:19 +02:00
commit dec8a13952
2 changed files with 88 additions and 4 deletions

View file

@ -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}`);

View file

@ -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 });
}
});