The docs home (/knowledge/) must resolve for any client KB — including one with no root index.md

0058-docs-home-must-resolve-without-root-index

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

DEC-0058 — The docs home (/knowledge/) must resolve for any client KB, even with no root index.md

Surfaced this turn by the Principal Forward Deployed Engineer during a spin-up of the RBA reference tenant (First live FirecrawlConnector run against a real client source — field evidence for the reserved web seam), verified by curl + screenshot against the running surfaces. Recorded automatically in the same turn (the standing rule). confidence: verified — the consequence chain is read directly off the actual source files and the live 404 was reproduced. This record holds the invariant and why it matters; the durable fix is owned by Docs home must resolve for every served KB (synthesize an overview when there is no root index).

Context

Dossier serves a client's OKF knowledge base as a derived, read-only docs surface (Adopt OKF as Dossier's canonical knowledge format, Astro Starlight as the docs-site generator + the product-owner, starlight-engineer, and documentation-engineer functions): the Astro/Starlight @dossier/docs app renders /knowledge/* + the atom routes, and the SvelteKit @dossier/app links to it (Migrate chrome-light app surfaces to SvelteKit; docs stay on Astro/Starlight (two apps, one origin), Link to the split @dossier/docs surface dev-origin-aware (same-origin in prod, docs dev origin in dev) via an SSOT $lib/docs module — NOT a Vite dev proxy, which two Vite dev servers cannot share one origin for). The app's primary "Docs" navigation — in the ONE shared SiteHeader/SiteFooter (Site chrome unified into shared SiteHeader + SiteFooter + ThemeToggle components, and the docs/KB surface made fully open — DEC-0022's DOCS_ENABLED landing-only gate is removed so /knowledge ships publicly alongside the landing/board/graph) — points at the docs HOME, /knowledge/.

A spin-up of the RBA reference tenant (the freshly-ingested Firecrawl KB at clients/rba/tenants-firecrawl/rba-consulting/okf, 440 atoms) exposed a real, generic seam: that KB had no root index.md, and as a direct consequence the app's Docs link was dead (404). The consequence chain, all read off the actual files this session:

  1. @dossier/okf-view routes.ts hasRootIndex() (routes.ts:187-190) does existsSync(join(root, 'index.md'))false for a KB with no root index.
  2. generateSidebar() (routes.ts:219-228) gates the /knowledge/ "Overview" link on hasRootIndex() (line 221) → the link is omitted (intentional, documented behavior).
  3. More consequentially: every docs page is content-collection-driven. The Astro glob loader (content.config.ts:137-142) globs **/*.md under knowledgeDir(), and generateId (content.config.ts:130-135) maps knowledge/index.md → slug knowledge (served at /knowledge/). There are no manual .astro pages under packages/site/src/pages/ (confirmed — none exist). So with no index.md in the source KB, no entry maps to slug knowledge, no /knowledge/ page is generated, and GET /knowledge/ returns 404.
  4. The app's shared chrome hardcodes the Docs target: SiteHeader.svelte:66 and SiteFooter.svelte:27 both ship { label: 'Docs', href: docsUrl('/knowledge/'), newTab: true }.

Net: for any client KB without a root index, the app's primary Docs nav lands on a 404.

Why it stayed invisible: Dossier's own KB has knowledge/index.md (the dossier-knowledge-root atom), so /knowledge/ always existed in dogfooding and every prior test/build. The gap is structural to client KBs, and a real client KB is what first served without one.

Why it is generic, not an RBA accident: a root index.md is not the default output of the ingest→extract pipeline, and neither the client-new (/client-new — a thin operator slash command over the control-plane CLI) nor the generate-landing flow emits one. So this hits every client whose KB lacks a root overview atom — which is the default — not just RBA.

Options considered

  1. Leave it — treat a missing /knowledge/ as the client's responsibility to author. Rejected: it makes the app's own header/footer ship a dead primary link by default, on every fresh client. That is a broken first impression of the product on day one of every engagement; the "Docs" nav is core chrome, not an optional surface.
  2. Renderer/okf-view SYNTHESIZES a /knowledge/ overview from the KB's structure (top-level dir groups + counts, à la topLevelDirs()/sidebarItemsForDir() which already exist) when no root index.md is present. Pro: robust, KB-agnostic, and writes nothing into the sovereign client repo (Adopt OKF as Dossier's canonical knowledge format — the KB is read-only; indexes are derived caches). The docs home is a derived view either way, so synthesizing one when the source lacks it is consistent with the existing "the nav is a faithful view of what exists" stance in routes.ts. Con: a synthesized page is less rich than a hand-authored overview, and the Astro side needs a generated route (not just the okf-view sidebar) so the page exists, not only the link.
  3. Ingest/extract (or client-new/generate-landing) EMITS a root index.md into every client KB at provision time. Pro: gives a real, editable overview atom the client owns. Con: it writes into the client's sovereign repo (Adopt OKF as Dossier's canonical knowledge format / Fix git-per-tenant isolation when a tenant root is nested inside another repo) — a generated file the client now owns and must maintain — which is less sovereignty-clean than synthesizing a derived view; and it is an inferred, possibly-stale artifact committed into the system of record (the same provenance/staleness concern Do not scan/mirror Anthropic's docs into the KB — distill curated references instead raised about absorbing what should be derived).
  4. De-hardcode the Docs link so the app points at the first available KB surface when /knowledge/ is absent. Rejected as the primary fix: it papers over the real gap (the docs HOME 404s) rather than guaranteeing a home, and it forks the SSOT Docs target out of the shared chrome into per-KB conditional logic.

Decision

Adopt the invariant: serving any client KB, the docs home (/knowledge/) MUST resolve to a faithful overview (HTTP 200), and the app's shared header/footer "Docs" link must never be dead — regardless of whether the source KB contains a root index.md.

This record fixes the invariant and names the seam (the @dossier/docs renderer / @dossier/okf-view boundary, with the ingest/generation pipeline as the alternative locus). It deliberately does not pick between Option 2 (synthesize, sovereignty-clean) and Option 3 (provision-time emit) — that implementation choice, with its sovereignty trade-off, is owned and tracked by Docs home must resolve for every served KB (synthesize an overview when there is no root index) (the FDE's read leans Option 2 as more sovereignty-clean, but the call is the owner's). The per-tenant stopgap below keeps RBA's reference tenant whole in the meantime.

Rationale

  • Sovereignty first. The KB is the client's owned git; the docs surface is a derived view (Adopt OKF as Dossier's canonical knowledge format, KB-agnostic @dossier/site (renders any tenant's OKF KB) + runtime-driven site rendering + the Node-26 Windows build fix). A missing root index is a property of the source, and the renderer's job is to render what exists faithfully — which argues for synthesizing the home from structure rather than writing a file into the sovereign repo. Hence the invariant lives at the renderer seam, and the FDE's lean is Option 2.
  • The default must be correct. Because no provisioning flow emits a root index today, "client KB without one" is the common case, not the edge case. Core chrome (the Docs link) must work on the default output of the pipeline, not only on the dogfood KB that happens to have one.
  • Faithfulness over fabrication. Any synthesized or emitted overview must report what the KB actually contains (dir groups + counts), never invented structure (Dossier — The Knowledge Model (v0) principle 8) — exactly as the per-tenant stopgap below was authored (real 440/12/819 figures, real linked atoms).

Consequences

  • Per-tenant stopgap applied this session (content fix, NOT the platform fix): authored clients/rba/tenants-firecrawl/rba-consulting/okf/index.md — a type: index root overview atom (id: rba-consulting-knowledge-root), faithful to the real KB (440 atoms / 12 types / 819 edges, links to the real atoms [[rba-consulting]] and [[rba-project-delivery-lifecycle]], both verified present), confidence: inferred, source: https://www.rbaconsulting.com/who-we-serve/. This lives in the gitignored clients/ sandbox (Fix git-per-tenant isolation when a tenant root is nested inside another repo) — it makes RBA's /knowledge/ resolve for this tenant, but does not fix the generic seam (the next client without a root index hits the same 404).
  • Durable platform fix is filed: Docs home must resolve for every served KB (synthesize an overview when there is no root index) (backlog, p1, owner/assignee Astro Starlight Engineer) — guarantee /knowledge/ resolves for any served KB with no root index, framing Option 2 (synthesize) vs Option 3 (emit) for the owner to decide.
  • Once the platform fix lands, the per-tenant stopgap index.md becomes redundant as a 404 guard — but it is a legitimately richer hand-authored overview than a synthesized one, so it can stay as RBA's real root atom (the platform fix is the floor, an authored index is the ceiling).
  • No change to the shared chrome's SSOT Docs target (it stays docsUrl('/knowledge/') in one place) — the fix makes the target resolve, rather than forking the link.

Review

This is a field-verified finding (confidence: verified): the consequence chain is read directly off routes.ts:187-190/219-228, content.config.ts:130-142, SiteHeader.svelte:66, SiteFooter.svelte:27, and the live 404 was reproduced by curl. The invariant it adopts is asserted until the fix ships. Promotion gate (asserted → verified on the invariant being upheld): serving a client KB with no root index.md, GET /knowledge/ returns 200 with a faithful overview, and the app's header/footer Docs link is never dead — the acceptance criteria of Docs home must resolve for every served KB (synthesize an overview when there is no root index). The stopgap is explicitly not that gate (it fixes one tenant, not the seam).