# llm-ingestion-okf — fase 4-kartlegging (okr-siden) **Status:** `planned` — kartlegging og kravgrunnlag. Ingen kode endret, ingenting wiret. **Dato:** 2026-07-20. **Grunnlag:** ground-truth-lesing av all OKF-kode i dette repoet (suite 149/149 grønn ved kartleggingstidspunkt). Node-halvdelen av `llm-ingestion-okf` finnes ikke ennå. Dette dokumentet er forarbeid: hva okr faktisk har, hva som er generelt vs. okr-spesifikt, og hva et delt bibliotek må oppfylle før okr kan vendore det i stedet for å eie egen kode. --- ## 1. Kartlegging — hva finnes i dag Zero-runtime-dependency Node/ESM. Alle moduler er både importerbare (named exports) og CLI-kjørbare (`import.meta.url === process.argv[1]`-vakt). Node >= 22. | Modul | Linjer | Rolle | Løftbarhet | |---|---|---|---| | `lib/frontmatter.mjs` | 70 | Flat FM-parse/skriv. BOM+CRLF-tolerant, siterte verdier bevarer intern `#`, multi-linje list-verdier tolereres | **Generell** | | `lib/okf-links.mjs` | 39 | Den ENE lenke-allow-listen: trygg bundle-lenke = leading `/`, `.md`, ingen scheme/`..`/backslash/NUL. `resolveBundleLink` re-sjekker confinement | **Generell** | | `lib/okf-vocab.mjs` | 73 | Lukket TYPE/TAGS-vokab + `routeLevel(type)` | **okr-spesifikk data, generell mekanisme** | | `lib/innboks-split.mjs` | 100 | Ren funksjon: `#`/`##`-heading-split → konsepter. Preamble bevares, flat-fallback | **Generell** (én no-detalj) | | `lib/innboks-frontmatter.mjs` | 96 | Regelbasert FM-projeksjon. `timestamp` = kilde-mtime (idempotens by construction) | **Blandet** | | `lib/innboks-relations.mjs` | 49 | Relasjoner innen kjøringens sett, exact-title-substring → root-lenker. Assert zero-dangling | **Generell** | | `lib/innboks-write.mjs` | 186 | Atomisk skriv (temp+rename), path-confinement, realpath/symlink-guard, reservert-navn-guard, kuratert-fil-vern (`kilde: innboks`), kryss-kilde-`claimed`-register | **Generell** (én okr-konstant) | | `lib/convert/index.mjs` | 132 | docx/eml/pdf/txt/md → md. Lenke-nøytralisering i alle fem lenkeformer | **Generell** | | `scripts/okf-check.mjs` | 160 | Bundle-validering. Default tolerant (kun `type` kreves); `--strict-ingest` = vokab + lenke-allow-liste + `files`-scoping | **Generell** | | `scripts/okf-index.mjs` | 162 | Per-nivå `index.md`, idempotent, `sanitizeEntry` (C0/C1, zero-width, bidi, Unicode-tags, link-nøytralisering), atomisk | **Generell** | | `scripts/innboks-ingest.mjs` | 240 | Orkestrator: discover → per-dok staging → gate → discard-on-fail → relasjoner → index → sluttsjekk | **Generell arkitektur** | **Arkitektur-invarianter verdt å bevare i et løft:** - Ingen LLM. Fil-settet er en funksjon av (drop-zone-innhold + mtime) alene → run1 og run2 byte-identisk. Veggklokke brukes aldri. - Per-dokument-isolasjon (Map-Reduce): ett fiendtlig dokument discardes alene og forgifter ikke de andre. Relasjoner emitteres ETTER gaten, så hvert mål garantert er på disk. - Strict-gaten scopes til kjøringens skrevne filer, ALDRI hele roten — ellers feller den legitimt håndkuratert innhold med lovlige eksterne lenker. - Ikke-destruktiv: originalene blir liggende i drop-zonen; hver original får en peker-fil. --- ## 2. Hva er okr-spesifikt vs. generelt ### Generelt (kan løftes tilnærmet uendret) `frontmatter.mjs`, `okf-links.mjs`, `innboks-split.mjs`, `innboks-relations.mjs`, `okf-index.mjs`, `okf-check.mjs`-kjernen, `convert/`-adapterne, og hele skrive-/ confinement-maskineriet i `innboks-write.mjs`. Ingenting her vet at domenet er OKR. ### okr-spesifikt (må parametriseres før løft) 1. **`TYPE_VOCAB`/`TAGS_VOCAB`/`LEVEL_BY_TYPE`** (`okf-vocab.mjs`) — norske, lukkede, OKR-domene. Mekanismen (lukket vokab + type→nivå-ruting) er generell; *innholdet* er ikke. Må inn som konfigurasjon, ikke kode. Merk at spec-en er engelskspråklig konvensjon mens vokabet er norsk — eierskapet (bibliotek-default vs. konsument-profil) må avklares. 2. **`HOME_ORG`-konstanten** (`innboks-write.mjs:44`, `~/.claude/okr/org`) — okrs to-rot- modell. Generaliseres til en «forbudte skriverøtter»-liste fra kaller. 3. **Type-utledningen** (`deriveType`, `innboks-frontmatter.mjs`) — matcher vokab-termer som helt ord i tittel + kilde-basename, med `\p{L}\p{N}`-lookaround fordi `\b` ikke håndterer æ/ø/å. Regelen er generell, vokabet ikke. 4. **`slugify`s æ→ae/ø→oe-translitterering** (`innboks-split.mjs:15-25`) — nordisk, ikke universelt. NFKD dekomponerer *ikke* æ/ø, så uten dette faller bokstaven bort. Et delt bibliotek trenger en pluggbar translitterasjonstabell, ellers regresserer norsk input. 5. **Faste nivånavn** (`strategisk-kontekst`/`historikk`/`dokumenter`) og `.claude/okr/innboks/` som drop-zone. Rene konfig-verdier. 6. **Norske feilmeldinger og `kilde: innboks`-provenansnøkkelen.** Meldingsspråk må være pluggbart hvis biblioteket skal tjene ikke-norske konsumenter; `kilde`-nøkkelen er en OKF-extension key okr er avhengig av for kuratert-fil-vernet. ### `lib/convert/` spesielt Adapterne er **domene-agnostiske** og det mest direkte gjenbrukbare i repoet. Men: - Fire exact-pinnede deps (mammoth 1.12.0, turndown 7.2.4, postal-mime 2.7.5, unpdf 1.6.2). okrs zero-dep-posisjon er bevart ved at de er lazy-importert med norsk installasjonshint, og at txt/md kjører helt dependency-fritt. **Et delt bibliotek må bevare denne egenskapen** — deps må være valgfrie (peer/optional), aldri obligatoriske. Vendored-vs-peer er en åpen beslutning som må tas sammen, ikke arves. - `turndown` er pinnet til `headingStyle: 'atx'`. Default er setext, som heading-splitten aldri ser. Denne koblingen mellom konverter og splitter må dokumenteres i kontrakten, ellers reintroduseres bugen i en delt utgave. - `unpdf` kalles med eksplisitt `isEvalSupported: false` (CVE-2024-4367), selv om det er default i 1.6.2. Belte + seler — må overleve løftet. - PDF gir flat tekst uten headings (kjent v1-caveat): PDF-dokumenter blir alltid ett konsept. - **`neutralizeExternalLinks` er sikkerhetskritisk** og hører sammen med `okf-links.mjs`. Emit-siden og validerings-siden deler allow-liste nettopp for at de aldri skal divergere. Splittes de i et løft, er divergens et spørsmål om tid. --- ## 3. Forutsetninger for at okr kan vendore en delt utgave Rangert, alle må være oppfylt: 1. **Vokabet er data, ikke kode.** Bibliotekets API tar TYPE/TAGS/`routeLevel` som argument. Uten dette må okr forke uansett. 2. **Zero-dep-kjerne bevart.** txt/md-stien må kjøre uten npm-installasjon. Binærformat-deps optional/lazy med samme feilmeldings-kvalitet (klar hint, aldri traceback). 3. **Sikkerhetsegenskapene er testbart bevart.** Path-confinement, realpath/symlink-guard, reservert-navn-guard, kuratert-fil-vern, `sanitizeEntry`-settet (C0/C1, ZWSP, bidi, Unicode tag-blokk), lenke-nøytralisering i alle fem former, `--strict-ingest`-scoping. Alle 149 testene her er tilgjengelige som seed; et løft som ikke porter dem er ikke ferdig. Særlig: gate-scoping (`files`) og discard-on-fail-isolasjon er lærte lekser fra B5/B2 — de er ikke kosmetikk. 4. **Idempotens by construction.** mtime-basert timestamp, deterministisk sortering, stabil kryss-kilde-disambiguering. Et bibliotek som introduserer veggklokke er ubrukelig for okr. 5. **Pluggbar slugify-translitterering** (æ/ø/å), ellers regresjon på norsk input. 6. **Pluggbar meldingsstreng/språk**, eller minst en `onNotice`-seam som i dag. 7. **Versjonspinning etter polyrepo-disiplinen.** okr vendorer mot en tag, ikke mot main. 8. **Guard-grensen respekteres.** Dør A er ugatet; `llm-ingestion-okf` er plumbing. okrs egen strict-gate er *ikke* en sikkerhetsguard i guard-repoets forstand og skal ikke forveksles med en. Vil vi ha guard-gating, går det via `llm-ingestion-guard` på kallstedet — og det er en separat beslutning, ikke en del av fase 4. --- ## 4. `okf_version` — hvordan okr bruker feltet i dag (kjent avvik) **Faktisk bruk, verifisert:** - Verdien er `kb-layout-2026-06` (`scripts/okf-index.mjs:27`, eksportert som `OKF_VERSION`). - Den bor som **markdown-tekstlinje i rot-`index.md`**, ikke i frontmatter — `index.md` er OKF-reservert og bærer aldri frontmatter. Kun rot-index; undernivåer har den aldri (testet eksplisitt). - `okf-check.mjs` **ekkoer** verdien for menneskelig sammenligning. Den validerer den ikke, sammenligner den ikke mot noe, og feiler aldri på den. Hooks/scripts er no-network, så auto-fetch mot en standard er utelukket by design. - `okf-index.mjs` **bevarer** eksisterende verdi ved re-kjøring; en eksplisitt `--okf-version ` vinner (bump-mekanismen). Begge grener er testet. - Feltet er per rot: prosjekt-`.claude/okr/` og home-`~/.claude/okr/org/` har hver sin. **Avviket:** spec-en sier `0.1`; okr sier `kb-layout-2026-06`. Dette er ikke bare ulik verdi — det er to ulike *typer* felt. okrs verdi navngir hvilket **layout-mønster** treet følger (datert konvensjons-snapshot); spec-ens `0.1` er et **spec-versjonsnummer**. De kan begge være riktige og likevel uforenlige i ett felt. **okrs posisjon:** dette avgjøres av catalog som konvensjonseier, ikke av okr og ikke av biblioteket. okr har lav byttekostnad — feltet er ren ekko-tekst uten validerings-semantikk, så en verdiendring koster én konstant + fixture-oppdateringer. Men hvis begge betydningene skal bæres, trenger vi to felt (f.eks. `okf_version` = spec-versjon, `okf_layout` = layout-snapshot), og det er en spec-endring som må gå via commons/catalog. --- ## 5. Hva som aldri bør flyttes til biblioteket - **`okf-vocab.mjs`s innhold** — norsk OKR-domenevokab hører hjemme i konsumenten eller i en profil, aldri som bibliotek-default. - **Orkestratoren `innboks-ingest.mjs` som helhet** — fase-rekkefølgen er generell og verdt å dele som *mønster*, men den konkrete drop-zone-plasseringen, to-rot-modellen og peker-filkonvensjonen er okrs. Del kjeden, ikke policyen. - **Hook-integrasjonen** (`inject-okr-context.mjs`, `coaching-hook.mjs`) og `okr-second-brain-search`-skillen — ren plugin-UX. - **Alt sikkerhetsansvar.** Guard-grensen står: `llm-ingestion-guard` eier sikkerhet, `llm-ingestion-okf` er plumbing, og okr reimplementerer ingen av delene. --- ## 6. Tillegg fra adopsjonsrunden 2026-07-20 (trinn C) Skrevet etter deltakelse i den koordinerte runden (ni repo, postkasse `~/repos/_okf-interim/`, midlertidig). Svaret vårt ligger i `svar/okr.md` der; det varige innholdet er dette: ### 6.1 Primitiv-modellen er riktig — men orkestreringen er den dyre delen okr er allerede primitiver + én komponerende kaller: `splitConcepts`, `projectFrontmatter`, `resolveRelations`, `writeConcepts`, `generateIndexes`, `checkBundle`, `convert`, `isSafeBundleLink` — og `innboks-ingest.mjs` som gjør ingenting primitivene ikke eksponerer. Fire orkestrerings-invarianter er ikke-åpenbare og ble kjøpt dyrt (B5, B2). De må følge med et delt bibliotek som referanse-orkestrator, ikke bare som primitiver: 1. Gate FØR relasjoner (ellers dangling lenker fra discardede dokumenter). 2. Gate scopet til kjøringens skrevne filer (ellers felles håndkuratert innhold). 3. Kryss-kilde-`claimed`-register (kollisjon er en egenskap ved kjøringen, ikke en primitiv). 4. Per-dokument-rollback (derfor returnerer `writeConcepts` `{concepts, pointers}`). Begrensning i vår stemme: okr har **ingen pull-sti** (no-network by design), så vi kan ikke uttale oss om henting-som-primitiv. ### 6.2 Flat vs. hierarkisk indeks — format deles, kontrakt gjør det ikke Dør A produserer flate bundles; vår form er hierarkisk. Forskjellen er ikke kosmetisk: - **To lenkekonvensjoner i samme bundle:** index-entries er nivå-relative, body-relasjoner er bundle-rot-relative (leading `/`). I en flat bundle kollapser de til det samme — en port fra en flat kontrakt får dette stille galt. - `okf_version` kun i rot-index forutsetter en rot distinkt fra andre nivåer. - `routeLevel(type)` gjør nivået semantisk avledet; flat materialisering har ingen tilsvarende operasjon. **Posisjon:** flat = degenerert hierarkisk (dybde 1). Kontrakten må formuleres som «én `index.md` per nivå», ellers kan ikke Node konformere uten å brekke okr. Krav til cross-runtime-fixtures: minst én hierarkisk fixture, ellers beviser parity-testing ingenting. ### 6.3 Konvergens med to andre repo på writer-primitivet portfolio-optimiser-claude og ms-ai-architect ber begge om frontmatter-som-input uten connector. **Vi har det bygget:** `writeConcepts` tar `concept.frontmatter` som ferdig serialisert streng og skriver verbatim (`fileContent` = `frontmatter + body`, ingen re-serialisering — dokumentert designvalg). Tre av ni konvergerer; løses F1 som en markdown-connector i dør A, får ingen av de tre noe. ### 6.4 Bundle-plassering — vi er compliant, men cwd-binding er en svak garanti Ingen bruker-eid bundle ligger i plugin-treet (`.claude/okr/` er cwd-relativ i brukerens eget prosjekt; `~/.claude/okr/org/` er home; KB-referansene er plugin-eide og blir). Org-profil- migreringen i 1.6.x er vår egen referanse for konfigurerbar sti-oppløsning. **Åpent, meldt videre:** cwd-binding flytter lekkasjeflaten, den fjerner den ikke — `.claude/okr/` lander i hvilket som helst repo brukeren står i, inkludert offentlige. **Grensetilfelle:** tillitsmodellen vår er asymmetrisk *innenfor* én bundle (innboks fiendtlig, resten kuratert — derfor `kilde: innboks`-guarden). Blir hele bundlen dør C-«eksternt innhold», må den grensen tegnes på nytt. ## 7. Neste steg (ikke utført) 1. Operatør melder kartleggingen inn til biblioteket som kravgrunnlag (§3 er kravlisten). 2. Catalog avklarer `okf_version`-semantikken (§4) — blokkerer løftet. 3. Parser-sett + vendored-vs-peer avgjøres i samråd (§2, `lib/convert/`). 4. Først når Node-halvdelen finnes med §3 oppfylt: vurder adopsjon i egen sesjon. Ingen av disse stegene endrer kode i okr. Markørlinjen i STATE.md står på `planned`.