#!/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 -- OKF-versjonen bundelen sikter mot) // okf_layout: (KUN rot-index -- vaar egen layout-revisjon, valgfri) // // * [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 markoer-verdier, 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'; // To distinkte markoerer (OKF-spec §12) -- ett felt skal ikke baere to urelaterte // konsepter. `okf_version` = upstream Google OKF-versjonen bundelen sikter mot // (verdisett eid av Google, enkeltverdi; paakrevd per §3). `okf_layout` = VAAR // egen layout-revisjon (valgfri; verdisett eid av emitteren, trigger ingen // kryss-plugin-rekjekk). Foer 1.8.1 baar `okf_version` layout-verdien alene -- // se resolveMarkers() for migrasjonsstien. export const OKF_VERSION = '0.1'; export const OKF_LAYOUT = 'kb-layout-2026-06'; // Upstream-versjoner er numerisk punktnotasjon (`0.1`). Alt annet i et // `okf_version`-felt er en layout-verdi fra foer splitten. const UPSTREAM_VERSION_RE = /^\d+(?:\.\d+)+$/; // 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, begge rot-markoerene, // og beskrivelser pr. lenke (for underkatalog-pekere). Kaster aldri. function parseExistingIndex(path) { const result = { heading: null, okfVersion: null, okfLayout: 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 lay = line.match(/^okf_layout:\s*(.+)$/); if (lay) result.okfLayout = lay[1].trim(); const entry = line.match(/^\*\s*\[([^\]]*)\]\(([^)]+)\)(?:\s*-\s*(.*))?$/); if (entry) result.descByLink[entry[2]] = { title: entry[1], desc: (entry[3] || '').trim() }; } return result; } // Avgjoer de to rot-markoerene fra en eksisterende index + en evt. eksplisitt // layout. MIGRASJONSSTI (1.8.1): baerer `okf_version` en ikke-upstream verdi og // `okf_layout` mangler, FLYTTES den funne verdien til `okf_layout` (verbatim -- // ikke erstattet av vaar konstant) og `okf_version` settes til upstream-verdien. // En allerede spec-konform `okf_version` (0.1, 0.2, ...) roeres aldri, og en // eksisterende `okf_layout` bevares (idempotent vedlikehold). function resolveMarkers(existing, explicitLayout) { let version = existing.okfVersion; let layout = existing.okfLayout; if (layout === null && version !== null && !UPSTREAM_VERSION_RE.test(version)) { layout = version; version = null; } return { version: version || OKF_VERSION, layout: explicitLayout || layout || OKF_LAYOUT, }; } // 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 de to // markoer-linjene. explicitLayout (B2): en eksplisitt oppgitt layout-revisjon // VINNER over eksisterende rot-verdi (bump-mekanisme); ellers bevares eksisterende // (idempotent vedlikehold). function writeIndexFor(dir, isRoot, explicitLayout) { 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) { const { version, layout } = resolveMarkers(existing, explicitLayout); lines.push(`okf_version: ${version}`, `okf_layout: ${layout}`, ''); } 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. // opts.okfVersion er et DEPRECATED alias for opts.okfLayout: verdien kallere har // sendt inn her var alltid en layout-revisjon, ogsaa foer splitten (§12). export function generateIndexes(root, opts = {}) { const raw = opts.okfLayout ?? opts.okfVersion; const explicitLayout = typeof raw === 'string' && raw !== '' ? raw : undefined; const walk = (dir, isRoot) => { writeIndexFor(dir, isRoot, explicitLayout); 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 -- flagget godtas foer ELLER etter rot-argumentet, // og manglende flagg-verdi er en bruksfeil (exit 2), ikke krasj. // 1.8.1: `--okf-layout` er kanon; `--okf-version` beholdes som deprecated alias // (verdien var alltid en layout-revisjon) og varsler paa stderr -- ALDRI stdout, // som er scriptets maskinlesbare flate. const usage = () => { process.stderr.write('Bruk: node okf-index.mjs [--okf-layout ]\n'); process.exit(2); }; const args = process.argv.slice(2); let okfLayout; for (const flag of ['--okf-layout', '--okf-version']) { const i = args.indexOf(flag); if (i === -1) continue; const val = args[i + 1]; if (!val || val.startsWith('--')) usage(); if (flag === '--okf-version') { process.stderr.write( 'Advarsel: --okf-version er utgaatt og setter layout-revisjonen. Bruk --okf-layout.\n', ); } if (okfLayout === undefined) okfLayout = val; args.splice(i, 2); } const root = args[0]; if (!root) usage(); generateIndexes(root, { okfLayout }); process.stdout.write(`OKF-index generert for ${root}\n`); }