diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e00009c..fefd4cf 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7700d94..b2382f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` 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. diff --git a/CLAUDE.md b/CLAUDE.md index e486f82..1404ec8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index 1e87a91..3cd0ad6 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/commands/oppsett.md b/commands/oppsett.md index a006729..d49edcf 100644 --- a/commands/oppsett.md +++ b/commands/oppsett.md @@ -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) diff --git a/package-lock.json b/package-lock.json index 4598af2..f739d5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 162e9c0..709a366 100644 --- a/package.json +++ b/package.json @@ -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).", diff --git a/scripts/okf-check.mjs b/scripts/okf-check.mjs index 20bc4e3..1d31b85 100644 --- a/scripts/okf-check.mjs +++ b/scripts/okf-check.mjs @@ -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); diff --git a/scripts/okf-index.mjs b/scripts/okf-index.mjs index 48302cf..12fca1d 100644 --- a/scripts/okf-index.mjs +++ b/scripts/okf-index.mjs @@ -5,7 +5,8 @@ // Layout»-index-form: // # Overskrift // -// okf_version: (KUN rot-index) +// 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`/ @@ -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 ` 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 [--okf-version ]\n'); + process.stderr.write('Bruk: node okf-index.mjs [--okf-layout ]\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`); } diff --git a/skills/okr-offentlig-sektor/SKILL.md b/skills/okr-offentlig-sektor/SKILL.md index 55f5f4e..99d78b6 100644 --- a/skills/okr-offentlig-sektor/SKILL.md +++ b/skills/okr-offentlig-sektor/SKILL.md @@ -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) diff --git a/skills/okr-second-brain-search/SKILL.md b/skills/okr-second-brain-search/SKILL.md index 751bd02..f01458e 100644 --- a/skills/okr-second-brain-search/SKILL.md +++ b/skills/okr-second-brain-search/SKILL.md @@ -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) diff --git a/tests/fixtures/okf-minimal/index.md b/tests/fixtures/okf-minimal/index.md index 1294af6..ab72ea7 100644 --- a/tests/fixtures/okf-minimal/index.md +++ b/tests/fixtures/okf-minimal/index.md @@ -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. diff --git a/tests/fixtures/okf-realistic/index.md b/tests/fixtures/okf-realistic/index.md index 5198037..9197277 100644 --- a/tests/fixtures/okf-realistic/index.md +++ b/tests/fixtures/okf-realistic/index.md @@ -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. diff --git a/tests/inject-core-cap.test.mjs b/tests/inject-core-cap.test.mjs index c84e98e..eb3c207 100644 --- a/tests/inject-core-cap.test.mjs +++ b/tests/inject-core-cap.test.mjs @@ -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) { diff --git a/tests/okf-check.test.mjs b/tests/okf-check.test.mjs index 81d8e76..a01fde3 100644 --- a/tests/okf-check.test.mjs +++ b/tests/okf-check.test.mjs @@ -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 }); + } +}); diff --git a/tests/package-shape.test.mjs b/tests/package-shape.test.mjs index 8491391..268b653 100644 --- a/tests/package-shape.test.mjs +++ b/tests/package-shape.test.mjs @@ -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)', () => {