#!/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, resolve } 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. Testes ALLTID mot en // unquotet verdi -- se unquote(). const UPSTREAM_VERSION_RE = /^\d+(?:\.\d+)+$/; // En markoerverdi kan vaere sitert: upstreams eget kanoniske eksempel // (`okf/SPEC.md:773`) skriver `okf_version: "0.2"`. Anfoerselstegnene er // YAML-strengsyntaks, ikke del av verdien, saa de maa vekk FOER verdien tolkes // (UPSTREAM_VERSION_RE) og foer den emitteres -- ellers leses en gyldig // upstream-versjon som en layout-verdi og migreres til feil markoer. // KUN et matchende par strippes: en halv anfoerselstegn-sekvens er en ugyldig // verdi som skal bevares uroert, ikke gjettes paa. function unquote(s) { const q = s[0]; if ((q === '"' || q === "'") && s.length >= 2 && s.endsWith(q)) return s.slice(1, -1); return s; } // 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, // beskrivelser pr. lenke (for underkatalog-pekere), og lenkenes LINJEREKKEFOELGE. // `linkOrder` er eksplisitt og ikke utledet av descByLink-noekkelrekkefoelgen: // akse B gjoer rekkefoelgen til en observerbar egenskap, og da skal den ikke // hvile paa JS-objekters innsettingsrekkefoelge. Kaster aldri. function parseExistingIndex(path) { const result = { heading: null, okfVersion: null, okfLayout: null, descByLink: {}, linkOrder: [], }; 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 = unquote(ver[1].trim()); const lay = line.match(/^okf_layout:\s*(.+)$/); if (lay) result.okfLayout = unquote(lay[1].trim()); const entry = line.match(/^\*\s*\[([^\]]*)\]\(([^)]+)\)(?:\s*-\s*(.*))?$/); if (entry) { if (!(entry[2] in result.descByLink)) result.linkOrder.push(entry[2]); 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})`; } // AKSE B (ingest-spec.md:178-181 + §6): rekkefoelgen paa genererte index-lenker // er kallerens ekstraksjonsrekkefoelge -- "never filesystem enumeration order". // MEDLEMSKAPET (hvilke navn som er med) kommer fortsatt fra disk; det er akse A // (`:175-177`) og skal IKKE ta imot en kaller-liste. Denne funksjonen ordner kun // et sett som allerede er lest fra disk: // 1. navn som alt staar i indeksen -> beholder sin linjerekkefoelge (§6: // "every line it does not itself manage is preserved byte for byte"); // 2. nye navn med kaller-rang -> kallerens ekstraksjonsrekkefoelge; // 3. nye navn UTEN kaller-rang -> alfabetisk. // (3) er en dokumentert genesis-fallback, ikke en B-etterlevelse: naar det ikke // finnes noen kaller (CLI-en tar kun en katalog) finnes det ingen ekstraksjons- // rekkefoelge aa tre gjennom, og alternativet -- raa readdirSync-rekkefoelge -- // er nettopp det B forbyr. Fallbacken gjelder bare ved genesis: saa snart en // indeks finnes, vinner (1), saa et alfabetisk valg overstyrer aldri en // rekkefoelge en kaller har etablert. function orderEntries(names, linkOf, existing, callerRank, dir) { const pos = new Map(existing.linkOrder.map((l, i) => [l, i])); const known = names.filter((n) => pos.has(linkOf(n))); const fresh = names.filter((n) => !pos.has(linkOf(n))); known.sort((a, b) => pos.get(linkOf(a)) - pos.get(linkOf(b))); // Urangert sorteres sist. Sentinelen er et ENDELIG tall, ikke Infinity: to // urangerte ville gitt Infinity - Infinity = NaN, som bare "virker" fordi NaN // er falsy og faller gjennom til den alfabetiske sammenligneren. Det er en // korrekt-ved-uhell-konstruksjon, og neste leser skal ikke behoeve aa se den. const UNRANKED = Number.MAX_SAFE_INTEGER; const rankOf = (n) => { const r = callerRank.get(resolve(dir, n)); return r === undefined ? UNRANKED : r; }; fresh.sort((a, b) => (rankOf(a) - rankOf(b)) || (a < b ? -1 : a > b ? 1 : 0)); return [...known, ...fresh]; } // 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). callerRank: absolutt sti -> ekstraksjonsrang (akse B). function writeIndexFor(dir, isRoot, explicitLayout, callerRank) { const existing = parseExistingIndex(join(dir, 'index.md')); const dirents = readdirSync(dir, { withFileTypes: true }); const subdirs = orderEntries( dirents.filter((e) => e.isDirectory() && isWalkableDir(e.name)).map((e) => e.name), (n) => `${n}/index.md`, existing, callerRank, dir, ); const concepts = orderEntries( dirents .filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') .map((e) => e.name), (n) => n, existing, callerRank, dir, ); 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). // opts.order (akse B): kallerens ekstraksjonsrekkefoelge som stier -- absolutte, // eller relative til `root`. Kun RANGERING; den kan aldri utvide eller innskrenke // medlemskapet (akse A leser det fra disk), saa en sti som ikke finnes paa disk // er en no-op, ikke en oppretting. export function generateIndexes(root, opts = {}) { const raw = opts.okfLayout ?? opts.okfVersion; const explicitLayout = typeof raw === 'string' && raw !== '' ? raw : undefined; const callerRank = new Map( (Array.isArray(opts.order) ? opts.order : []).map((p, i) => [resolve(root, p), i]), ); const walk = (dir, isRoot) => { writeIndexFor(dir, isRoot, explicitLayout, callerRank); 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`); }