Lenient KB-atom reader in @dossier/okf-view (readKbAtoms) — faithful getCollection reproduction for the SvelteKit app

0044-okf-view-lenient-kb-atom-reader

decision read as Explain confidence verified status active 2026-06-17 owner sveltekit-engineer
Reversibility
two-way door

DEC-0044 — Lenient KB-atom reader in @dossier/okf-view (readKbAtoms)

Decided + built this turn by the Principal SvelteKit Engineer while executing DEC-0043 Phase 2 (port /graph + shared chrome to @dossier/app). Recorded automatically in the same turn (the standing rule). confidence: verified — built, and the reproduction was measured against the live KB.

Context

DEC-0043 says the SvelteKit app loads its data via +page.server.ts load() calling @dossier/okf + @dossier/okf-view with prerender = true, reproducing "the same static output profile as Astro's getCollection" — noting "the content collection was always a thin layer over a filesystem reader we own."

Porting /graph exposed a gap in which reader. The Astro docs collection (content.config.ts) used a lenient schema: Starlight's docsSchema extended with an all-optional okfFields block, unknown keys stripped. It therefore surfaced every id-bearing .md atom to buildGraph. The strict @dossier/okf parse() (full per-type Zod validate()) is a different contract: it rejects atoms that don't conform to a type's exact schema. Measured on Dossier's own KB: 87 of 87 id-bearing atoms parse leniently, but only 56 pass strict parse()31 are rejected (almost all authored decision ADRs, including DEC-0043 itself). The existing getRouteMap reader in @dossier/okf-view is regex-only (id/title/type, no body, no full frontmatter), so it can't feed buildGraph either.

Using strict parse() in the graph loader would have silently dropped ~⅓ of the graph — the opposite of "reproduce the Astro profile exactly."

Options considered

  1. Strict @dossier/okf parse() in the loader. Rejected: drops 31/87 atoms — a visible, wrong regression vs. the live Astro graph.
  2. Re-implement a frontmatter splitter + YAML parse inside @dossier/app. Rejected: forks the KB read path (DEC-0043's whole point of @dossier/okf-view is one shared reader, never forked).
  3. Add a lenient readKbAtoms() to @dossier/okf-view (CHOSEN). The shared leaf is the correct SSOT home — both the app (graph/board) and the docs surface (when it ports) consume one reader. Reuses the yaml parser @dossier/okf already uses, and the same frontmatter-split + slug-derivation as the Astro loader / getRouteMap, so the readers can never drift.

Decision

Add readKbAtoms(): KbAtom[] (KbAtom = { id, data, body }) to @dossier/okf-view (src/kb.ts, exported from the index). It walks knowledgeDir() for **/*.md, strips a BOM, splits the leading --- … --- block, YAML.parses it without Zod validation, normalizes bare-Date frontmatter fields (timestamp/decided_on → ISO date, lease_expires → ISO datetime) the way the Astro dateish transforms did, and derives the id slug identically to the Astro loader's generateId (index.mdknowledge, foo/index.mdfoo). Crash-safe (missing/unreadable/frontmatter-less → skipped, never thrown) and KB-agnostic (DOSSIER_KB/knowledgeDir()).

This adds yaml@^2 (already in the workspace lockfile via @dossier/okf) as the first declared dependency of @dossier/okf-view; the shared vite lib build externalizes it.

Rationale

  • Faithful, not lax. "Lenient" reproduces what Astro's collection actually fed renderers (the superset), which is the literal requirement of DEC-0043's data-profile promise. Verified: readKbAtomsbuildGraph yields 87 nodes, 439 live edges, 7 concept types, with DEC-0043 present and carrying its description + body — matching the live Astro graph, not the 56-atom strict subset.
  • Single source of truth for the KB read (DEC-0043 + the atomic SSOT bar). One reader in the shared leaf, consumed by both surfaces; the regex getRouteMap and this full reader split the frontmatter the same way, so they can't drift.
  • One parser across the repo. Reuses @dossier/okf's yaml, rather than inventing a second YAML path.
  • Sovereignty (DEC-0001) preserved. Read-only, derived; the git KB stays the source of truth.

Consequences

  • @dossier/okf-view gains one runtime dep (yaml), losing its prior zero-dep status — bought back by not forking a YAML reader and by keeping the surface read faithful. Still a leaf (no @dossier/* runtime deps; node builtins + yaml).
  • Atoms that fail strict OKF validation still appear in the graph/board (as they did under Astro). This is intentional for surface fidelity; strict conformance is the extraction/validation layer's job (DEC-0008), not the renderer's.
  • The board port (Phase 3) and a future docs-side use can consume readKbAtoms directly — no second reader.

Reversibility

Two-way door: readKbAtoms is an additive, pure read function in a shared leaf; nothing existing changed (the route map / sidebar / board view-model are untouched). A future caller wanting strict conformance can layer @dossier/okf validate() on top per atom; if the lenient profile is ever wrong it is one function to adjust. The durable commitment is the principle — the surface read must reproduce what Astro's collection surfaced (the lenient superset), not the strict schema subset — and the SSOT placement (one KB reader in @dossier/okf-view, consumed by both surfaces, never forked). Removing the function reverts to no shared full-atom reader; the yaml dep is trivially dropped with it.

Review

confidence: verified: built and exercised — pnpm --filter @dossier/app check is 0/0, the workspace test suite is green (385 passed), and the dev-server + prerendered /graph both emit the 87-node / 439-edge payload. See Migrate chrome-light app surfaces to SvelteKit; docs stay on Astro/Starlight (two apps, one origin) and Execute the SvelteKit app migration (DEC-0043) — phased, no big-bang, apex domain moved only at final cutover.