feat(okr): okf_version emitteres i rot-indeksens frontmatter

This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 21:15:50 +02:00
commit 1da9dd878d
4 changed files with 144 additions and 12 deletions

View file

@ -66,7 +66,7 @@ Cycle archival: `/okr:oppsett arkiver` — moves `syklus/` to `historikk/`, gene
Context files carry OKF v0.1 Documents-style frontmatter (conforms to the OKF v0.1 minimal contract, canonized in `catalog/docs/okf-second-brain/spec.md`): a Title-Case `type` (`Organisasjonsprofil`, `Tildelingsbrev`, `Virksomhetsplan`, `Overordnede OKR`, `OKR`, `Retrospektiv`, `Status`) plus recommended `resource`/`title`/`description`/`tags`/`timestamp`. Only `type` is de-facto required. The flat frontmatter parser reads only single-line keys (`type`, profile/config fields); tree files may carry multi-line `tags` lists without breaking it. Files written by inbox ingestion additionally carry the provenance extension key `kilde: innboks`.
**Two bundle roots** with distinct lifecycles: project `.claude/okr/` (cwd-bound cycle/work) and home `~/.claude/okr/org/` (reinstall-surviving org identity). Each root carries its own `index.md` (no frontmatter; `# H1` + `* [title](link) - description`) and, on the root index, two distinct markers (OKF spec §12): `okf_version` (the upstream OKF version targeted, currently `0.1`) and `okf_layout` (this plugin's own layout revision, currently `kb-layout-2026-06`). `okf-index`/`okf-check` run per root; retrieval Globs both roots as a union — never a short-circuit — with project content winning on conflict (`skills/okr-second-brain-search/SKILL.md:35,46,95` is the authoritative procedure). Only the org *profile* resolves by precedence (`hooks/scripts/inject-okr-context.mjs:54-56` short-circuits on the project hit).
**Two bundle roots** with distinct lifecycles: project `.claude/okr/` (cwd-bound cycle/work) and home `~/.claude/okr/org/` (reinstall-surviving org identity). Each root carries its own `index.md` (`# H1` + `* [title](link) - description`) and, on the root index, two distinct markers (OKF spec §12): `okf_version` (the upstream OKF version targeted, currently `0.1`) and `okf_layout` (this plugin's own layout revision, currently `kb-layout-2026-06`). The two markers sit on **different surfaces** by decision 6: `okf_version` in the root index's frontmatter (the machine-readable contract consumers outside the plugin read), `okf_layout` in its body. Sub-level indexes carry no frontmatter and no markers. `okf-check` reads `okf_version` from **both** placements — frontmatter first, body as fallback — since bundles on disk do not migrate simultaneously. `okf-index`/`okf-check` run per root; retrieval Globs both roots as a union — never a short-circuit — with project content winning on conflict (`skills/okr-second-brain-search/SKILL.md:35,46,95` is the authoritative procedure). Only the org *profile* resolves by precedence (`hooks/scripts/inject-okr-context.mjs:54-56` short-circuits on the project hit).
- `lib/frontmatter.mjs` — shared frontmatter parser/writer; both hooks route through it (strips trailing ` #…` only from unquoted values).
- `scripts/okf-index.mjs` — regenerate a root's per-level `index.md` (verbatim OKF index format).

View file

@ -2,16 +2,25 @@
// 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:
// Layout»-index-form. ROT-index:
// ---
// okf_version: <upstream> (frontmatter -- OKF-versjonen bundelen sikter mot)
// ---
//
// # Overskrift
//
// okf_version: <upstream> (KUN rot-index -- OKF-versjonen bundelen sikter mot)
// okf_layout: <revisjon> (KUN rot-index -- vaar egen layout-revisjon, valgfri)
// okf_layout: <revisjon> (broedtekst -- 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).
// UNDERKATALOG-index: samme, men uten frontmatter og uten markoerer.
//
// De to markoerene ligger paa ULIKE flater med vilje (beslutning 6, D8 steg 21):
// `okf_version` er upstream-eid og den maskinlesbare kontrakten konsumenter
// utenfor pluginen leser -> frontmatter; `okf_layout` er vaar egen revisjon ->
// broedtekst. Frontmatter paa index.md gjelder DERFOR kun roten -- underkatalog-
// indekser er fortsatt frontmatter-frie. 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-
@ -23,7 +32,7 @@
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';
import { parseFrontmatter, writeFrontmatter } 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
@ -198,11 +207,22 @@ function writeIndexFor(dir, isRoot, explicitLayout, callerRank) {
const heading = existing.heading
|| (isRoot ? 'OKF second brain' : titleFromName(basename(dir)));
const lines = [`# ${heading}`, ''];
// Beslutning 6: de to markoerene har ulike eiere og ulik livssyklus, saa de
// faar ulike flater. `okf_version` (upstream, Google-eid) er den maskinlesbare
// kontrakten konsumenter utenfor pluginen leser -> frontmatter. `okf_layout`
// (vaar egen revisjon) blir staaende i broedteksten. KUN rot-indeksen; en
// underkatalog-index faar aldri frontmatter.
const lines = [];
let rootLayout = null;
if (isRoot) {
const { version, layout } = resolveMarkers(existing, explicitLayout);
lines.push(`okf_version: ${version}`, `okf_layout: ${layout}`, '');
rootLayout = layout;
// Siste element av split er '' (writeFrontmatter avslutter med \n) -- droppes
// her og erstattes av den bevisste blanklinja mellom blokk og overskrift.
lines.push(...writeFrontmatter({ okf_version: version }).split('\n').slice(0, -1), '');
}
lines.push(`# ${heading}`, '');
if (isRoot) lines.push(`okf_layout: ${rootLayout}`, '');
for (const sd of subdirs) {
const link = `${sd}/index.md`;

View file

@ -75,14 +75,33 @@ in their frontmatter. These files passed a deterministic conformance gate but
**no human review** — they are the least-trusted tier of the bundle (see
ranking and the security envelope below).
Each level has an **`index.md`** with **no frontmatter**, formatted as a heading
plus one bullet per concept file:
Each level has an **`index.md`** formatted as a heading plus one bullet per
concept file. Sub-level indexes carry **no frontmatter**:
```
# Heading
* [title](relative.md) - description
```
A **root** index additionally carries the two bundle markers, deliberately on
different surfaces: `okf_version` (the upstream OKF version) in frontmatter, and
`okf_layout` (this plugin's own layout revision) in the body:
```
---
okf_version: 0.1
---
# Heading
okf_layout: kb-layout-2026-06
* [title](relative.md) - description
```
These markers are metadata about the bundle, not retrievable content — skip them
when ranking.
The `index.md` is the curated table of contents for its level — use it to
navigate and to break ranking ties (below).

View file

@ -17,6 +17,7 @@ import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { generateIndexes } from '../scripts/okf-index.mjs';
import { checkBundle } from '../scripts/okf-check.mjs';
import { parseFrontmatter } from '../lib/frontmatter.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CHECK = join(ROOT, 'scripts', 'okf-check.mjs');
@ -1015,3 +1016,95 @@ test('(20f) okf-check: frontmatter-markoer tolkes som YAML -- trailing kommentar
}
});
// --- D8 steg 21: okf_version emitteres i rot-indeksens frontmatter ---
// Laast arkitektur-beslutning 6: `okf_version` -> frontmatter, `okf_layout` ->
// broedtekst. De to markoerene har ulike eiere (Google vs. oss) og ulik
// livssyklus; frontmatter er den maskinlesbare flaten konsumenter utenfor
// pluginen leser, broedteksten er vaar egen. Gjelder KUN rot-indeksen --
// underkatalog-indekser forblir uten frontmatter.
//
// PRESENCE haandheves fortsatt IKKE: `okf_version` er MAY i specen, og dette
// steget flytter plassering, det innfoerer ikke et krav.
//
// Emisjon og lesing testes HVER FOR SEG (S38-S40-laerdommen: samme fil, samme
// markoer, to lesninger). (21e) er den ene bevisste integrasjonen: den beviser
// at steg 20s toleranse faktisk moeter det steg 21 skriver.
test('(21a) okf-index: rot-index baerer okf_version i FRONTMATTER, okf_layout i broedteksten', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.ok(root.startsWith('---\n'), 'rot-index skal aapne med en frontmatter-blokk');
const { get } = parseFrontmatter(root);
assert.equal(get('okf_version'), '0.1', 'okf_version skal ligge i frontmatter');
assert.equal(get('okf_layout'), null, 'okf_layout skal IKKE ligge i frontmatter');
const body = root.slice(root.indexOf('\n---', 3) + 4);
assert.match(body, /^okf_layout: kb-layout-2026-06$/m, 'okf_layout skal staa i broedteksten');
assert.ok(!/^okf_version:/m.test(body), 'okf_version skal ikke ogsaa staa i broedteksten');
assert.match(body, /^# /m, 'overskriften skal fortsatt finnes, under frontmatteren');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21b) okf-index: underkatalog-index faar ALDRI frontmatter', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const sub = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.ok(!sub.startsWith('---'), 'undernivaa skal IKKE ha frontmatter');
assert.equal(sub.split('\n')[0].slice(0, 2), '# ', 'undernivaa starter fortsatt paa overskriften');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21c) okf-index: to kjoeringer gir BYTE-IDENTISK rot-index (idempotens over ny plassering)', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const first = readFileSync(join(dir, 'index.md'), 'utf8');
generateIndexes(dir);
const second = readFileSync(join(dir, 'index.md'), 'utf8');
// Fanger at parseExistingIndex ikke forstaar den nye plasseringen:
// duplisert markoer, forskjoevet overskrift, eller markoer re-utledet til default.
assert.equal(second, first, 'andre kjoering skal ikke endre en byte');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21d) okf-index: umigrert broedtekst-form loeftes til frontmatter, verdiene bevart', () => {
const dir = tmpRoot();
try {
// Bundle skrevet av en eldre okf-index: begge markoerene i broedteksten.
writeRootIndex(dir, ['okf_version: 0.3', 'okf_layout: kb-layout-2099-12']);
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.equal(parseFrontmatter(root).get('okf_version'), '0.3', 'den FUNNE versjonen skal loeftes, ikke erstattes av konstanten');
const body = root.slice(root.indexOf('\n---', 3) + 4);
assert.match(body, /^okf_layout: kb-layout-2099-12$/m, 'layout skal bli staaende i broedteksten, verbatim');
assert.ok(!/^okf_version:/m.test(body), 'den gamle broedtekst-linja skal ikke bli liggende igjen');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(21e) okf-index -> okf-check: det emitteren skriver, leser checkeren', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
generateIndexes(dir);
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.1', 'checkeren skal finne den frontmatter-plasserte versjonen');
assert.equal(r.okfLayout, 'kb-layout-2026-06', 'og den broedtekst-plasserte layouten');
assert.equal(runCheck(dir).status, 0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});