feat(okr): 1.8.1 okf_version/okf_layout-splitt (OKF-spec 12)

Rot-index.md bar en markoer som dekket to urelaterte konsepter: den upstream
OKF-versjonen bundelen sikter mot OG pluginens egen layout-revisjon. Specen
(catalog/docs/okf-second-brain/spec.md 12, log.md 2026-07-23) skiller dem i to
markoerer. Denne releasen migrerer emitteren og sjekkeren over.

- okf-index.mjs: emitterer okf_version: 0.1 (upstream, paakrevd per 3) +
  okf_layout: kb-layout-2026-06 (vaar revisjon, valgfri per 12). Ny konstant
  OKF_LAYOUT; OKF_VERSION baerer naa upstream-verdien. Begge rot-eksklusive.
- Migrasjonssti: en ikke-upstream verdi i okf_version FLYTTES verbatim til
  okf_layout ved neste kjoering. Spec-konform okf_version roeres aldri;
  eksisterende okf_layout bevares. Byte-idempotent.
- okf-check.mjs: ekkoer begge markoerene (fravaerende -> MANGLER). Rent ekko,
  ingen ny haandheving -- spec 3 er ikke haandhevende paa form ennaa.
- CLI: --okf-layout er kanon. --okf-version beholdt som deprecated alias
  (verdien var alltid en layout-revisjon) m/ varsel til stderr, ALDRI stdout.
  Samme aliasing for generateIndexes({ okfLayout }) mot { okfVersion }.
- Doc-flater (CLAUDE.md, second-brain SKILL.md, commands/oppsett.md) beskriver
  naa to markoerer der de beskrev en.

Suite 167 -> 179, alle groenne. Versjonssync 1.8.1 over alle shippede flater.

Laaser opp catalog + llm-ingestion-okf, som begge ventet paa denne migrasjonen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pUfkQ1YyH75z9y6RBaBHG
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 15:33:30 +02:00
commit 0059da7bef
16 changed files with 333 additions and 59 deletions

View file

@ -5,8 +5,10 @@
// - >= 1 fil uten type -> exit 1 + teller + navngir filene.
// - 0 filer uten type -> exit 0.
// Anbefalte felt (resource/title/description/timestamp) rapporteres som ADVARSEL,
// ikke feil. Rotens `okf_version` ekkoes for menneskelig sammenligning mot
// gjeldende standard (ingen auto-fetch — hooks/scripts er no-network; SC7 myket).
// ikke feil. Rotens to markoerer (`okf_version` = upstream OKF-versjon, spec §3;
// `okf_layout` = emitterens egen layout-revisjon, spec §12) ekkoes for menneskelig
// sammenligning mot gjeldende standard (ingen auto-fetch — hooks/scripts er
// no-network; SC7 myket).
//
// Kjoeres PER ROT (prosjekt `.claude/okr/` + home `~/.claude/okr/org/`).
// Zero npm dependencies (node:-builtins).
@ -55,12 +57,19 @@ function walkConcepts(root) {
return out;
}
// Les rotens okf_version (markdown-tekst i index.md, ikke frontmatter). null hvis fravaerende.
function rootOkfVersion(root) {
// Les rotens to markoerer (markdown-tekst i index.md, ikke frontmatter).
// `okf_version` = upstream OKF-versjon (spec §3), `okf_layout` = emitterens egen
// layout-revisjon (spec §12, valgfri). Fravaerende markoer -> null. Ren ekko:
// verdiene valideres ikke (spec §3 er ikke haandhevende paa form ennaa).
function rootMarkers(root) {
const idx = join(root, 'index.md');
if (!existsSync(idx)) return null;
const m = readFileSync(idx, 'utf8').match(/^okf_version:\s*(.+)$/m);
return m ? m[1].trim() : null;
if (!existsSync(idx)) return { okfVersion: null, okfLayout: null };
const raw = readFileSync(idx, 'utf8');
const pick = (key) => {
const m = raw.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
return m ? m[1].trim() : null;
};
return { okfVersion: pick('okf_version'), okfLayout: pick('okf_layout') };
}
// strictIngest (default AV): paa skrivestien handheves det lukkede vokabularet
@ -117,7 +126,7 @@ export function checkBundle(root, { strictIngest = false, files } = {}) {
warnings,
strictErrors,
strictIngest,
okfVersion: rootOkfVersion(root),
...rootMarkers(root),
};
}
@ -147,6 +156,7 @@ if (isMain) {
for (const e of r.strictErrors) out.push(` x ${e}`);
}
out.push(` okf_version: ${r.okfVersion || 'MANGLER (rot-index uten okf_version)'}`);
out.push(` okf_layout: ${r.okfLayout || 'MANGLER (valgfri markoer)'}`);
out.push(` Advarsler (anbefalte felt): ${r.warnings.length}`);
for (const w of r.warnings) out.push(` ! ${w}`);
const failed = r.missingType.length > 0 || (strictIngest && r.strictErrors.length > 0);

View file

@ -5,7 +5,8 @@
// Layout»-index-form:
// # Overskrift
//
// okf_version: <ver> (KUN rot-index)
// okf_version: <upstream> (KUN rot-index -- OKF-versjonen bundelen sikter mot)
// okf_layout: <revisjon> (KUN rot-index -- vaar egen layout-revisjon, valgfri)
//
// * [title](relativ.md) - description
// Ingen frontmatter paa index.md (OKF-reservert). Konsept-filers `title`/
@ -13,7 +14,7 @@
// 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-
// 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.
//
@ -24,7 +25,18 @@ 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';
// 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
@ -39,10 +51,12 @@ function titleFromName(name) {
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.
// 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, descByLink: {} };
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('# ')) {
@ -50,12 +64,33 @@ function parseExistingIndex(path) {
}
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,
@ -83,10 +118,11 @@ function entryLine(title, link, 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) {
// 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
@ -103,7 +139,8 @@ function writeIndexFor(dir, isRoot, okfVersion, explicitVersion) {
const lines = [`# ${heading}`, ''];
if (isRoot) {
lines.push(`okf_version: ${explicitVersion ? okfVersion : (existing.okfVersion || okfVersion)}`, '');
const { version, layout } = resolveMarkers(existing, explicitLayout);
lines.push(`okf_version: ${version}`, `okf_layout: ${layout}`, '');
}
for (const sd of subdirs) {
@ -124,11 +161,13 @@ function writeIndexFor(dir, isRoot, okfVersion, explicitVersion) {
}
// 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 explicitVersion = typeof opts.okfVersion === 'string' && opts.okfVersion !== '';
const okfVersion = explicitVersion ? opts.okfVersion : OKF_VERSION;
const raw = opts.okfLayout ?? opts.okfVersion;
const explicitLayout = typeof raw === 'string' && raw !== '' ? raw : undefined;
const walk = (dir, isRoot) => {
writeIndexFor(dir, isRoot, okfVersion, explicitVersion);
writeIndexFor(dir, isRoot, explicitLayout);
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory() && isWalkableDir(e.name)) walk(join(dir, e.name), false);
}
@ -141,22 +180,32 @@ export function generateIndexes(root, opts = {}) {
const isMain = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
// B2: flagg-tolerant parsing -- `--okf-version <ver>` godtas foer ELLER etter
// rot-argumentet, og manglende flagg-verdi er en bruksfeil (exit 2), ikke krasj.
// 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 <bundle-rot> [--okf-version <ver>]\n');
process.stderr.write('Bruk: node okf-index.mjs <bundle-rot> [--okf-layout <ver>]\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);
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, { okfVersion });
generateIndexes(root, { okfLayout });
process.stdout.write(`OKF-index generert for ${root}\n`);
}