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

@ -1,6 +1,6 @@
{
"name": "okr",
"version": "1.8.0",
"version": "1.8.1",
"description": "Expert OKR guidance for Norwegian public sector. Write, review, cascade, track and govern OKR based on Google/Doerr methodology adapted for 4-month tertial cycles.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.8.1] - 2026-07-25
Patch-release: **`okf_version` / `okf_layout`-splitt**. Rot-`index.md` bar én markør som dekket to urelaterte konsepter — den upstream OKF-versjonen bundelen sikter mot *og* pluginens egen layout-revisjon. OKF-specen §12 (katalog-eid) skiller dem i to markører; denne releasen migrerer emitteren og sjekkeren over. Ingen nye kommandoer, agenter eller referansefiler.
### Changed
- **`scripts/okf-index.mjs` emitterer to markør-linjer på rot-`index.md`** — `okf_version: 0.1` (upstream Google OKF-versjonen, verdisett eid av Google, påkrevd per spec §3) og `okf_layout: kb-layout-2026-06` (pluginens egen layout-revisjon, valgfri per §12, verdisett eid av emitteren; trigger ingen kryss-plugin-rekjekk). Konstanten `OKF_VERSION` bærer nå upstream-verdien; ny `OKF_LAYOUT` bærer layout-revisjonen. Undernivå-index er uendret — begge markørene er rot-eksklusive.
- **`scripts/okf-check.mjs` ekkoer begge markørene** (`okf_version` + `okf_layout`, fraværende → `MANGLER`). Rent ekko: verdiene valideres fortsatt ikke, siden spec §3 ennå ikke er håndhevende på form. Retur-objektet fra `checkBundle()` bærer `okfLayout` ved siden av `okfVersion`.
- **`--okf-layout <ver>` er det kanoniske CLI-flagget** for å bumpe layout-revisjonen. `--okf-version` beholdes som **deprecated alias** — verdien kallere sendte inn der var alltid en layout-revisjon — og skriver et deprecation-varsel til **stderr**, aldri stdout (scriptets maskinlesbare flate). Samme aliasing gjelder `generateIndexes(root, { okfLayout })` mot det gamle `{ okfVersion }`. Manglende flagg-verdi er fortsatt bruksfeil (exit 2, ingen skriving).
### Fixed
- **Migrasjonssti for eksisterende bundles** — en rot-`index.md` som bærer en ikke-upstream verdi i `okf_version` (formen fra før 1.8.1) får verdien **flyttet verbatim** til `okf_layout`, og `okf_version` settes til upstream-verdien. Den funne verdien erstattes aldri av vår egen konstant, en allerede spec-konform `okf_version` (`0.1`, `0.2`, …) røres aldri, og en eksisterende `okf_layout` bevares. Migrasjonen skjer ved neste `okf-index`-kjøring; ingen manuell handling kreves.
### Added
- **Testsuite 167 → 179 cases**, alle grønne. 12 nye i `tests/okf-check.test.mjs`: begge markørene på rot og ingen på undernivå (inkl. at markør-blokken ikke etterlater en tom linje der), migrasjon av legacy-form, verbatim-bevaring av en *fremmed* layout-verdi, ikke-migrering av spec-konform `okf_version`, verbatim-bevaring av allerede splittet form, `--okf-layout`-bump + idempotent vedlikehold, deprecated-alias-atferd, stderr-vs-stdout-separasjon for deprecation-varselet, bruksfeil ved manglende flagg-verdi, og begge markørene i `okf-check`-ekkoet inkludert `MANGLER` for utelatt layout.
### Documentation
- `CLAUDE.md`, `skills/okr-second-brain-search/SKILL.md` og `commands/oppsett.md` beskriver nå to markører der de tidligere beskrev én.
## [1.8.0] - 2026-07-25
Minor-release **«Én kanon»**: metodekonsolidering av MAJOR-/metode-funnene fra dyp review 2026-07-16 (§3). Ingen nye kommandoer, agenter eller referansefiler — prinsippet er ÉN kanonisk definisjon per metodebegrep, der konsumentflater (kommandoer, agenter, øvrige referansefiler) refererer kanon i stedet for å redefinere den. Konsolideringen er låst av en ny strukturell konsistensvakt, ikke bare av prosa.

View file

@ -1,4 +1,4 @@
# OKR Offentlig Sektor v1.8.0
# OKR Offentlig Sektor v1.8.1
Expert OKR guidance for Norwegian public sector. Google/Doerr methodology adapted for 4-month tertial cycles.
@ -64,11 +64,11 @@ 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 an `okf_version` marker. `okf-index`/`okf-check` run per root; retrieval Globs both (project preferred, else home).
**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 (project preferred, else home).
- `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).
- `scripts/okf-check.mjs` — validate each concept file carries `type:` (exit 1 + count otherwise); report `okf_version` per root.
- `scripts/okf-check.mjs` — validate each concept file carries `type:` (exit 1 + count otherwise); echo `okf_version` + `okf_layout` per root (pure echo, no value validation).
- `scripts/compose-org-profile.mjs` — build the nested org-profile YAML with top-level OKF keys (`type`/`resource`/`timestamp`) before piping to `write-org-profile.mjs`.
- `scripts/innboks-ingest.mjs` — inbox-ingestion orchestrator (`/okr:innboks`): walks `.claude/okr/innboks/`, converts (txt/md/docx/eml/pdf via `lib/convert/index.mjs`), heading-splits, stamps OKF frontmatter + `kilde: innboks`, routes by type, then gates each document with `okf-check --strict-ingest` — a failing document is discarded alone (staging + per-document rollback), survivors get relations and index entries. Non-destructive (originals stay in the inbox), idempotent by construction (timestamp = source mtime), path-confined (realpath). Binary formats need `npm install --ignore-scripts` (4 exact-pinned pure-JS deps); txt/md run dependency-free.

View file

@ -6,7 +6,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-1.8.0-blue)
![Version](https://img.shields.io/badge/version-1.8.1-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Agents](https://img.shields.io/badge/agents-7-orange)
![Commands](https://img.shields.io/badge/commands-14-blue)
@ -301,6 +301,7 @@ The plugin core is zero-dependency (`node:` builtins only). The inbox-ingestion
| Version | Date | Highlights |
|---------|------|------------|
| **1.8.1** | 2026-07-25 | Patch: `okf_version`/`okf_layout`-splitt (OKF-spec §12) — rot-`index.md` bærer nå to distinkte markører i stedet for én som bar to urelaterte konsepter: `okf_version` = upstream OKF-versjonen (`0.1`, verdisett eid av Google), `okf_layout` = pluginens egen layout-revisjon (`kb-layout-2026-06`). Automatisk migrasjonssti for eksisterende bundles (layout-verdi i `okf_version` flyttes verbatim); `okf-check` ekkoer begge; `--okf-layout` erstatter `--okf-version` (beholdt som deprecated alias m/ stderr-varsel) |
| **1.8.0** | 2026-07-25 | «Én kanon» — metodekonsolidering etter dyp review: én kanonisk confidence-tabell og én kadens-doktrine i `okr-framework.md` med konsument-dedup på tvers av referanser, kommandoer og agenter; score→modenhet-avledningen fjernet; `committed` = forpliktelse OG påvirkbarhet; kvalitetsvurdering ankret i rubrikken (alle 10 dimensjoner, ingen parallelle scorebånd); ekte antipattern-kategorier; milepæl-/binær-unntak; «align, don't cascade» i kaskadeflatene; markedsclaims kildebelagt + nye kilder (NCT, EBM 2024); ny konsistensvakt `tests/canon-consistency.test.mjs` (suite 149 → 167 cases) |
| **1.7.1** | 2026-07-17 | Patch: restsanering etter dyp review — KB/doc-hygiene med referanse-integritetstest (B1) + ingestion-kode-hygiene (B2): æ/ø-translitterering i slugify, ikke-destruktiv circuit-breaker (merge, config overlever), sanitizeEntry mot bidi/zero-width/Unicode-tag, `--okf-version`-bump, BOM/CRLF-toleranse, presis at-risk-telling, bøyningsformer i topic-guard |
| **1.7.0** | 2026-07-17 | Innboks-ingestion: `/okr:innboks` med deterministisk pipeline (convert/split/frontmatter/gate/relasjoner/indeks), per-dokument sikkerhetsgate mot RAG-poisoning, `kilde: innboks`-provenans + untrusted-envelope i retrieval-skillen, 4 exact-pinnede pure-JS-avhengigheter (eneste zero-dep-unntak) |

View file

@ -180,7 +180,8 @@ node ${CLAUDE_PLUGIN_ROOT}/scripts/okf-index.mjs .claude/okr
```
(Idempotent: bygger `index.md`-entries fra konsept-filenes `title`/`description`,
bevarer menneske-skrevne overskrifter og rotens `okf_version`.)
bevarer menneske-skrevne overskrifter og rotens markører `okf_version` +
`okf_layout`.)
### Fase 4 — Struktur (3 min)

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "okr-offentlig-sektor",
"version": "1.8.0",
"version": "1.8.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "okr-offentlig-sektor",
"version": "1.8.0",
"version": "1.8.1",
"dependencies": {
"mammoth": "1.12.0",
"postal-mime": "2.7.5",

View file

@ -1,6 +1,6 @@
{
"name": "okr-offentlig-sektor",
"version": "1.8.0",
"version": "1.8.1",
"private": true,
"type": "module",
"description": "OKR-plugin for norsk offentlig sektor. Pure-JS konverterings-deps for innboks-ingestion (bevisst zero-dep-brudd, se README).",

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`);
}

View file

@ -2,7 +2,7 @@
name: okr-offentlig-sektor
description: >-
OKR (Objectives and Key Results) for Norwegian public sector: writing OKR, reviewing OKR quality, cascading OKR from strategy to team, tracking progress, running OKR meetings, translating tildelingsbrev to OKR. Also CFR, OKR antipatterns, scoring, Oboard. Triggers on: "OKR", "skriv OKR", "vurder OKR", "OKR-scoring", "kaskadere OKR", "tildelingsbrev til OKR", "OKR for offentlig sektor".
version: "1.8.0"
version: "1.8.1"
---
# OKR Skill for Offentlig Sektor (Norge)

View file

@ -10,7 +10,7 @@ description: >-
prompt. Triggers on: "våre mål", "overordnede mål", "mål dette tertialet",
"tertialmål", "tildelingsbrev", "hva sier OKR-ene våre", "forrige syklus",
"strategi", "styringssignaler".
version: "1.8.0"
version: "1.8.1"
---
# OKR Second-Brain Search
@ -41,8 +41,9 @@ both, project first:
2. **Home bundle**`~/.claude/okr/org/` (organization identity, survives
reinstall).
Each root carries its own `index.md` per level and an `okf_version` marker on its
root `index.md`. Project content overrides home content on conflict
Each root carries its own `index.md` per level, and its root `index.md` carries two
markers: `okf_version` (the upstream OKF version targeted) and `okf_layout` (this
plugin's own layout revision). Project content overrides home content on conflict
(most-specific-wins, mirroring the hook's resolution).
## OKF layout (what you are searching)

View file

@ -1,5 +1,6 @@
# OKF second brain (minimal testfixtur)
okf_version: kb-layout-2026-06
okf_version: 0.1
okf_layout: kb-layout-2026-06
* [Tildelingsbrev](tildelingsbrev.md) - Styringssignal med unik testtoken.

View file

@ -1,6 +1,7 @@
# OKF second brain - Vegdirektoratet (realistisk testfixtur)
okf_version: kb-layout-2026-06
okf_version: 0.1
okf_layout: kb-layout-2026-06
* [Strategisk kontekst](strategisk-kontekst/index.md) - Tildelingsbrev, virksomhetsplan og profil.
* [Syklus T1-2026](syklus/T1-2026/index.md) - Aktive OKR og statusrapport.

View file

@ -41,7 +41,7 @@ function makeProjectTree(work, count) {
mkdirSync(lvl, { recursive: true });
writeFileSync(
join(work, '.claude', 'okr', 'index.md'),
'# OKR-rot\n\nokf_version: kb-layout-2026-06\n',
'# OKR-rot\n\nokf_version: 0.1\nokf_layout: kb-layout-2026-06\n',
);
for (let i = 0; i < count; i++) {
writeFileSync(join(lvl, `konseptfil-${i}.md`), `---\ntype: OKR\n---\n# K${i}\n`);
@ -52,7 +52,7 @@ function makeHomeBundle(home, navn = 'HomeCapOrg') {
const org = join(home, '.claude', 'okr', 'org');
mkdirSync(org, { recursive: true });
writeFileSync(join(org, 'profil.md'), `---\nnavn: "${navn}"\n---\n`);
writeFileSync(join(org, 'index.md'), '# Org-rot\n\nokf_version: kb-layout-2026-06\n');
writeFileSync(join(org, 'index.md'), '# Org-rot\n\nokf_version: 0.1\nokf_layout: kb-layout-2026-06\n');
}
function runHook(cwd, home, input = OKR_PROMPT) {

View file

@ -8,7 +8,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { execFileSync, spawnSync } from 'node:child_process';
import {
mkdtempSync, cpSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync, mkdirSync,
} from 'node:fs';
@ -122,6 +122,23 @@ test('okf-index: rot-index baerer okf_version, undernivaa gjoer ikke', () => {
}
});
test('okf-index: undernivaa-index har verken markoer eller tom markoer-linje', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const sub = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.ok(!/okf_layout/.test(sub), 'undernivaa skal IKKE ha okf_layout');
// Markoer-blokken er rot-eksklusiv: undernivaaet gaar rett fra H1 til entries.
const lines = sub.split('\n');
assert.match(lines[0], /^# /, 'linje 1 = overskrift');
assert.equal(lines[1], '', 'linje 2 = blank');
assert.match(lines[2], /^\* \[/, 'linje 3 = foerste entry (ingen markoer-blokk)');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: konsept-tittel/beskrivelse hentes fra frontmatter', () => {
const dir = tmpRoot();
try {
@ -165,7 +182,7 @@ test('okf-check: konsept-fil uten type: -> exit != 0 + teller > 0 + navngir file
test('okf-check: rapporterer okf_version fra rot-index', () => {
const { stdout } = runCheck(REALISTIC);
assert.match(stdout, /okf_version:\s*kb-layout-2026-06/, 'skal ekko okf_version for menneskelig sammenligning');
assert.match(stdout, /okf_version:\s*0\.1/, 'skal ekko okf_version for menneskelig sammenligning');
});
// --- okf-check --strict-ingest (Step 8): lukket vokab + lenke-allow-liste ---
@ -316,17 +333,20 @@ test('okf-index: innboks/ + dot-katalog -> ingen egen index.md OG ikke listet i
const INDEX_CLI = join(ROOT, 'scripts', 'okf-index.mjs');
test('okf-index CLI: eksplisitt --okf-version bumper eksisterende rot-index (flagg foer rot)', () => {
// 1.8.1: verdien flagget baerer er en LAYOUT-revisjon (den var det ogsaa foer
// splitten) -- flagget godtas fortsatt her (deprecated alias), men landet den
// skriver er `okf_layout`. Se alias-testene i 1.8.1-seksjonen nederst.
test('okf-index CLI: eksplisitt layout-flagg bumper eksisterende rot-index (flagg foer rot)', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir); // rot-index har allerede okf_version: kb-layout-2026-06
execFileSync('node', [INDEX_CLI, '--okf-version', 'kb-layout-2027-01', dir], { encoding: 'utf8' });
execFileSync('node', [INDEX_CLI, '--okf-layout', 'kb-layout-2027-01', dir], { encoding: 'utf8' });
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version: kb-layout-2027-01$/m, 'eksplisitt versjon skal vinne over eksisterende');
// Uten flagg bevares den bumpede versjonen (idempotent vedlikehold, som foer).
assert.match(root, /^okf_layout: kb-layout-2027-01$/m, 'eksplisitt layout skal vinne over eksisterende');
// Uten flagg bevares den bumpede verdien (idempotent vedlikehold, som foer).
execFileSync('node', [INDEX_CLI, dir], { encoding: 'utf8' });
const root2 = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root2, /^okf_version: kb-layout-2027-01$/m, 'implisitt kjoering bevarer eksisterende versjon');
assert.match(root2, /^okf_layout: kb-layout-2027-01$/m, 'implisitt kjoering bevarer eksisterende layout');
} finally {
rmSync(dir, { recursive: true, force: true });
}
@ -456,3 +476,175 @@ test('checkBundle { files }: strict validerer KUN oppgitte filer, aldri hele rot
rmSync(dir, { recursive: true, force: true });
}
});
// --- 1.8.1: okf_version / okf_layout-splitt (OKF-spec §12) ---
// `okf_version` = upstream Google OKF-versjon ALENE (verdisett eid av Google,
// enkeltverdi). `okf_layout` = vaar EGEN layout-revisjon (valgfri, verdisett eid
// av emitteren). Rot-index baerer BEGGE. Migrasjonssti: en eksisterende rot-index
// som baerer en layout-verdi i `okf_version` (vaar form foer 1.8.1) splittes.
// Kilde: catalog/docs/okf-second-brain/spec.md §3 + §12, log.md 2026-07-23.
// Skriv en rot-index med et vilkaarlig sett markoer-linjer (legacy/splittet/tom).
function writeRootIndex(dir, markerLines) {
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'index.md'), `# Bundle\n\n${markerLines.join('\n')}\n`);
}
test('okf-index: rot-index baerer BEGGE markoerene, undernivaa ingen av dem', () => {
const dir = tmpRoot();
try {
cpSync(REALISTIC, dir, { recursive: true });
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version: 0\.1$/m, 'rot skal baere upstream-versjonen alene');
assert.match(root, /^okf_layout: kb-layout-2026-06$/m, 'rot skal baere vaar layout-revisjon');
const sub = readFileSync(join(dir, 'strategisk-kontekst', 'index.md'), 'utf8');
assert.ok(!/^okf_version:/m.test(sub), 'undernivaa skal IKKE ha okf_version');
assert.ok(!/^okf_layout:/m.test(sub), 'undernivaa skal IKKE ha okf_layout');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: legacy rot-index (layout-verdi i okf_version) migreres til splittet form', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: kb-layout-2026-06']);
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version: 0\.1$/m, 'okf_version skal settes til upstream-verdien');
assert.match(root, /^okf_layout: kb-layout-2026-06$/m, 'layout-verdien skal flyttes hit, ikke tapes');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: en FREMMED layout-verdi i okf_version bevares gjennom migrasjonen', () => {
const dir = tmpRoot();
try {
// Migrasjonen skal flytte verdien, ikke erstatte den med vaar egen konstant.
writeRootIndex(dir, ['okf_version: kb-layout-2025-01']);
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version: 0\.1$/m);
assert.match(root, /^okf_layout: kb-layout-2025-01$/m, 'den FUNNE verdien skal flyttes verbatim');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: spec-konform okf_version (upstream-form) migreres IKKE til okf_layout', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: 0.2']);
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version: 0\.2$/m, 'upstream-verdi skal bevares, ikke overskrives');
assert.match(root, /^okf_layout: kb-layout-2026-06$/m, 'layout fylles fra konstant');
assert.ok(!/^okf_layout: 0\.2$/m.test(root), 'upstream-verdien skal ALDRI havne i okf_layout');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: eksisterende splittet form bevares verbatim (idempotent vedlikehold)', () => {
const dir = tmpRoot();
try {
writeRootIndex(dir, ['okf_version: 0.3', 'okf_layout: kb-layout-2099-12']);
generateIndexes(dir);
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_version: 0\.3$/m, 'eksisterende okf_version bevares');
assert.match(root, /^okf_layout: kb-layout-2099-12$/m, 'eksisterende okf_layout bevares');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index CLI: --okf-layout bumper layout og lar okf_version staa', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir); // legacy rot-index: okf_version: kb-layout-2026-06
execFileSync('node', [INDEX_CLI, '--okf-layout', 'kb-layout-2027-01', dir], { encoding: 'utf8' });
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_layout: kb-layout-2027-01$/m, 'eksplisitt layout skal vinne');
assert.match(root, /^okf_version: 0\.1$/m, 'okf_version skal vaere upstream-verdien');
// Uten flagg bevares den bumpede layouten (idempotent vedlikehold).
execFileSync('node', [INDEX_CLI, dir], { encoding: 'utf8' });
const root2 = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root2, /^okf_layout: kb-layout-2027-01$/m, 'implisitt kjoering bevarer bumpet layout');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index CLI: --okf-version er deprecated alias for --okf-layout (m/ stderr-varsel)', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
// Aliaset maa fortsatt sette LAYOUT -- det var alltid det verdien betydde.
execFileSync('node', [INDEX_CLI, '--okf-version', 'kb-layout-2027-05', dir], {
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
});
const root = readFileSync(join(dir, 'index.md'), 'utf8');
assert.match(root, /^okf_layout: kb-layout-2027-05$/m, 'aliaset skal sette layout');
assert.match(root, /^okf_version: 0\.1$/m, 'aliaset skal IKKE sette okf_version');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index CLI: --okf-version skriver deprecation-varsel til stderr, ikke stdout', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
const res = spawnSync(
'node', [INDEX_CLI, '--okf-version', 'kb-layout-2027-06', dir],
{ encoding: 'utf8' },
);
assert.equal(res.status, 0, 'aliaset skal fortsatt lykkes');
assert.match(res.stderr, /--okf-version/, 'varselet skal navngi det utgaaende flagget');
assert.match(res.stderr, /--okf-layout/, 'varselet skal peke paa erstatteren');
assert.ok(!/--okf-layout/.test(res.stdout), 'varselet skal IKKE forurense stdout');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index CLI: --okf-layout uten verdi -> bruksfeil exit 2, ingen skriving', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
const before = readFileSync(join(dir, 'index.md'), 'utf8');
let status = 0;
try {
execFileSync('node', [INDEX_CLI, dir, '--okf-layout'], { encoding: 'utf8', stdio: 'pipe' });
} catch (e) {
status = e.status;
}
assert.equal(status, 2, 'manglende flagg-verdi skal gi bruksfeil exit 2');
assert.equal(readFileSync(join(dir, 'index.md'), 'utf8'), before, 'rot-index skal vaere uendret');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-check: ekkoer BEGGE markoerene fra rot-index', () => {
const { stdout } = runCheck(REALISTIC);
assert.match(stdout, /okf_version:\s*0\.1/, 'skal ekko upstream-versjonen');
assert.match(stdout, /okf_layout:\s*kb-layout-2026-06/, 'skal ekko vaar layout-revisjon');
});
test('okf-check: rot-index uten okf_layout ekkoer MANGLER for den, ikke for okf_version', () => {
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeRootIndex(dir, ['okf_version: 0.1']); // splittet form, layout utelatt (lovlig: valgfri)
const r = checkBundle(dir);
assert.equal(r.okfVersion, '0.1', 'okf_version skal parses');
assert.equal(r.okfLayout, null, 'fravaerende okf_layout skal vaere null, ikke undefined');
const { stdout } = runCheck(dir);
assert.match(stdout, /okf_layout: MANGLER/, 'ekkoet skal si fra at layout-markoeren mangler');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

View file

@ -2,7 +2,7 @@
// Step 10 (A1): package.json-kontrakten for det bevisste zero-dep-bruddet.
// Verifiserer at dep-laget er EXACT-pinnet (ingen ^/~/*), at engines-gulvet er
// satt (unpdf krever node >= 22), at pakken er ESM (type: module), at versjonen
// er 1.8.0 (minor-lane C: En kanon) paa ALLE shippede flater, og at .npmrc slaar av
// er 1.8.1 (patch-lane: okf_layout-migrasjon) paa ALLE shippede flater, og at .npmrc slaar av
// install-scripts (Shai-Hulud / supply-chain). Zero npm deps i selve testen.
// Moenster: tests/frontmatter.test.mjs (les fil, assert struktur).
@ -24,10 +24,10 @@ function readPkg() {
return JSON.parse(readFileSync(PKG, 'utf8'));
}
test('package.json: type module + version 1.8.0 (minor-lane)', () => {
test('package.json: type module + version 1.8.1 (patch-lane)', () => {
const pkg = readPkg();
assert.equal(pkg.type, 'module');
assert.equal(pkg.version, '1.8.0');
assert.equal(pkg.version, '1.8.1');
});
test('package.json: engines.node-gulv satt (unpdf krever >= 22)', () => {