#!/usr/bin/env node // okf-index.mjs // Genererer OKF-kompatible `index.md` per nivaa i en bundle-rot (prosjekt // `.claude/okr/` eller home `~/.claude/okr/org/`). Verbatim OKF-«Documents/kb // Layout»-index-form: // # Overskrift // // okf_version: (KUN rot-index) // // * [title](relativ.md) - description // Ingen frontmatter paa index.md (OKF-reservert). Konsept-filers `title`/ // `description` leses via lib/frontmatter.mjs. Underkataloger faar en peker til // sin egen index.md. Kjoeres PER ROT (de to bundlene har ulik livssyklus). // // Idempotens / vedlikehold (NFR): en eksisterende index.md sin `# overskrift`, // rotens `okf_version`-verdi, og menneske-skrevne beskrivelser for underkatalog- // pekere bevares; konsept-entries regenereres alltid fra frontmatter (autoritativ // kilde). Skriving er atomisk (temp + renameSync), jf. write-org-profile.mjs. // // Zero npm dependencies (node:-builtins). import { readdirSync, readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs'; import { join, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; 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(); return spaced.charAt(0).toUpperCase() + spaced.slice(1); } // Parse en eksisterende index.md for bevaring: overskrift, okf_version, og // beskrivelser pr. lenke (for underkatalog-pekere). Kaster aldri. function parseExistingIndex(path) { const result = { heading: null, okfVersion: null, descByLink: {} }; if (!existsSync(path)) return result; for (const line of readFileSync(path, 'utf8').split('\n')) { if (result.heading === null && line.startsWith('# ')) { result.heading = line.slice(2).trim(); } const ver = line.match(/^okf_version:\s*(.+)$/); if (ver) result.okfVersion = ver[1].trim(); const entry = line.match(/^\*\s*\[([^\]]*)\]\(([^)]+)\)(?:\s*-\s*(.*))?$/); if (entry) result.descByLink[entry[2]] = { title: entry[1], desc: (entry[3] || '').trim() }; } return result; } // 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\u0080-\u009f]/g, ' ') // C0 + C1 kontrolltegn -> mellomrom .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // noytraliser markdown-lenker (behold tekst) // B2: fjern usynlige styringstegn -- zero-width (ZWSP/ZWNJ/ZWJ/LRM/RLM), // bidi-embedding/-override/-isolater (spoofing av leseretning), BOM, og // Unicode tag-blokken (usynlig smugle-kanal for instruksjonstekst). .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]|[\u{E0000}-\u{E007F}]/gu, '') .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) { 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. // explicitVersion (B2): en eksplisitt oppgitt versjon VINNER over eksisterende // rot-verdi (bump-mekanisme); ellers bevares eksisterende (idempotent vedlikehold). function writeIndexFor(dir, isRoot, okfVersion, explicitVersion) { const existing = parseExistingIndex(join(dir, 'index.md')); const dirents = readdirSync(dir, { withFileTypes: true }); 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) .sort(); const heading = existing.heading || (isRoot ? 'OKF second brain' : titleFromName(basename(dir))); const lines = [`# ${heading}`, '']; if (isRoot) { lines.push(`okf_version: ${explicitVersion ? okfVersion : (existing.okfVersion || okfVersion)}`, ''); } for (const sd of subdirs) { const link = `${sd}/index.md`; const prev = existing.descByLink[link]; lines.push(entryLine(prev?.title || titleFromName(sd), link, prev?.desc || 'Underkatalog.')); } for (const c of concepts) { const { get } = parseFrontmatter(readFileSync(join(dir, c), 'utf8')); const title = get('title') || titleFromName(basename(c, '.md')); lines.push(entryLine(title, c, get('description') || '')); } const content = `${lines.join('\n')}\n`; const tmp = join(dir, `.index.md.${process.pid}.tmp`); writeFileSync(tmp, content); renameSync(tmp, join(dir, 'index.md')); } // Generer index.md for rot + alle underkataloger, rekursivt. export function generateIndexes(root, opts = {}) { const explicitVersion = typeof opts.okfVersion === 'string' && opts.okfVersion !== ''; const okfVersion = explicitVersion ? opts.okfVersion : OKF_VERSION; const walk = (dir, isRoot) => { writeIndexFor(dir, isRoot, okfVersion, explicitVersion); for (const e of readdirSync(dir, { withFileTypes: true })) { if (e.isDirectory() && isWalkableDir(e.name)) walk(join(dir, e.name), false); } }; if (!existsSync(root)) throw new Error(`Bundle-rot finnes ikke: ${root}`); walk(root, true); } // --- CLI --- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isMain) { // B2: flagg-tolerant parsing -- `--okf-version ` godtas foer ELLER etter // rot-argumentet, og manglende flagg-verdi er en bruksfeil (exit 2), ikke krasj. const usage = () => { process.stderr.write('Bruk: node okf-index.mjs [--okf-version ]\n'); process.exit(2); }; const args = process.argv.slice(2); let okfVersion; const vi = args.indexOf('--okf-version'); if (vi !== -1) { okfVersion = args[vi + 1]; if (!okfVersion || okfVersion.startsWith('--')) usage(); args.splice(vi, 2); } const root = args[0]; if (!root) usage(); generateIndexes(root, { okfVersion }); process.stdout.write(`OKF-index generert for ${root}\n`); }