diff --git a/lib/okf-vocab.mjs b/lib/okf-vocab.mjs new file mode 100644 index 0000000..7798f87 --- /dev/null +++ b/lib/okf-vocab.mjs @@ -0,0 +1,73 @@ +// okf-vocab.mjs +// Lukket kontrollert vokabular for innboks-ingestion (type + tags) og +// type->nivaa-ruting. Zero npm dependencies. +// +// Net-new (premiss #1): handheves KUN paa ingestion-skrivestien (snapType/ +// snapTags) og av okf-check++ --strict-ingest. Lese-siden (okr-second-brain- +// search, okf-check default) behandler fortsatt ukjente typer som gyldige -- +// denne modulen er den ENESTE strenge porten, aktiv kun ved ingestion. + +// Title-Case kanoniske OKF-typer (jf. catalog-spec Documents/kb Layout). +export const TYPE_VOCAB = [ + 'Organisasjonsprofil', + 'Tildelingsbrev', + 'Virksomhetsplan', + 'Overordnede OKR', + 'OKR', + 'Retrospektiv', + 'Status', + 'Notat', + 'Dokument', +]; + +// Safe default naar raw type ikke matcher vokabularet exact (premiss #1). +const DEFAULT_TYPE = 'Dokument'; + +// Lukket start-sett av Title-Case tags for ingestion-snap. +export const TAGS_VOCAB = [ + 'Strategi', + 'Tildelingsbrev', + 'Virksomhetsplan', + 'OKR', + 'Styring', + 'Governance', + 'Retrospektiv', + 'Status', + 'Maal', + 'Risiko', +]; + +// type -> bundle-nivaa (katalog under bundle-rot). Organisasjonsprofil rutes +// til dokumenter/ (IKKE strategisk-kontekst/) for aa ikke konkurrere med den +// home-kanoniske ~/.claude/okr/org/profil.md (Revisions #25). +const LEVEL_BY_TYPE = { + Tildelingsbrev: 'strategisk-kontekst', + Virksomhetsplan: 'strategisk-kontekst', + 'Overordnede OKR': 'strategisk-kontekst', + Retrospektiv: 'historikk', + OKR: 'dokumenter', + Status: 'dokumenter', + Notat: 'dokumenter', + Dokument: 'dokumenter', + Organisasjonsprofil: 'dokumenter', +}; + +const DEFAULT_LEVEL = 'dokumenter'; + +// Exact-match -> kanonisk type; ellers safe default Dokument. +export function snapType(raw) { + return TYPE_VOCAB.includes(raw) ? raw : DEFAULT_TYPE; +} + +// Behold kun tags i det lukkede vokabularet (dropp ukjente); bevar rekkefolge. +export function snapTags(rawList) { + if (!Array.isArray(rawList)) return []; + return rawList.filter((t) => TAGS_VOCAB.includes(t)); +} + +// type -> bundle-nivaa; ukjent/ikke-vokab -> default dokumenter/. Plassert her +// i Session 1 slik at relasjons-steget (Step 6) kan resolvere konseptets +// mal-sti FOR skriv (Pass-2 ordering-fiks, Revisions #21). +export function routeLevel(type) { + return LEVEL_BY_TYPE[type] ?? DEFAULT_LEVEL; +} diff --git a/tests/okf-vocab.test.mjs b/tests/okf-vocab.test.mjs new file mode 100644 index 0000000..98f20db --- /dev/null +++ b/tests/okf-vocab.test.mjs @@ -0,0 +1,65 @@ +// okf-vocab.test.mjs +// Tester det lukkede OKF-vokabularet (type+tags) og type->nivaa-ruting for +// innboks-ingestion (Step 2). snapType: exact-match -> kanonisk, ellers safe +// default Dokument; snapTags dropper ukjente; routeLevel mapper type til +// bundle-nivaa. Direkte import (zero npm deps). Moenster: tests/frontmatter.test.mjs. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + TYPE_VOCAB, + TAGS_VOCAB, + snapType, + snapTags, + routeLevel, +} from '../lib/okf-vocab.mjs'; + +test('snapType: kjent type beholdes kanonisk', () => { + assert.equal(snapType('OKR'), 'OKR'); + assert.equal(snapType('Tildelingsbrev'), 'Tildelingsbrev'); +}); + +test('snapType: ukjent type -> safe default Dokument', () => { + assert.equal(snapType('ukjent'), 'Dokument'); + assert.equal(snapType(''), 'Dokument'); + assert.equal(snapType(undefined), 'Dokument'); +}); + +test('snapTags: dropper ukjente, beholder kjente (bevart rekkefolge)', () => { + assert.deepEqual(snapTags(['Strategi', 'xyz']), ['Strategi']); + assert.deepEqual(snapTags(['xyz', 'abc']), []); + assert.deepEqual(snapTags([]), []); +}); + +test('snapTags: ikke-array -> tom liste (ingen krasj)', () => { + assert.deepEqual(snapTags(undefined), []); + assert.deepEqual(snapTags(null), []); +}); + +test('routeLevel: strategisk-kontekst for foringsdokumenter', () => { + assert.equal(routeLevel('Tildelingsbrev'), 'strategisk-kontekst'); + assert.equal(routeLevel('Virksomhetsplan'), 'strategisk-kontekst'); + assert.equal(routeLevel('Overordnede OKR'), 'strategisk-kontekst'); +}); + +test('routeLevel: historikk for Retrospektiv', () => { + assert.equal(routeLevel('Retrospektiv'), 'historikk'); +}); + +test('routeLevel: dokumenter for OKR/Status/Notat/Dokument/Organisasjonsprofil', () => { + assert.equal(routeLevel('OKR'), 'dokumenter'); + assert.equal(routeLevel('Status'), 'dokumenter'); + assert.equal(routeLevel('Notat'), 'dokumenter'); + assert.equal(routeLevel('Dokument'), 'dokumenter'); + assert.equal(routeLevel('Organisasjonsprofil'), 'dokumenter'); +}); + +test('routeLevel: ukjent type -> default dokumenter', () => { + assert.equal(routeLevel('Whatever'), 'dokumenter'); +}); + +test('vokabular non-tomt + default i settet', () => { + assert.ok(TYPE_VOCAB.length > 0, 'TYPE_VOCAB non-tomt'); + assert.ok(TAGS_VOCAB.length > 0, 'TAGS_VOCAB non-tomt'); + assert.ok(TYPE_VOCAB.includes('Dokument'), 'default Dokument er i vokabularet'); +});