The dev-only client landing preview renders the client's REAL OKF knowledge graph inline — via three atomic no-copy-paste extractions (readKbAtoms(dir), buildGraphView, GraphExplorer) that read the client's served OKF directly without perturbing the process-wide DOSSIER_KB or the prerendered /graph

0047-client-preview-renders-real-okf-graph-inline

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

DEC-0047 — The client landing preview renders the client's REAL OKF graph inline

Decided + built this turn by the Principal SvelteKit Engineer, on top of The marketing landing becomes a tailorable per-client template — a typed LandingContent model rendered by LandingPage.astro; the Dossier render stays byte-for-byte identical; client instances are values of the same type, canonical in the client's own repo, generated by the generate-landing skill (the tailorable preview) and Lenient KB-atom reader in @dossier/okf-view (readKbAtoms) — faithful getCollection reproduction for the SvelteKit app (the lenient KB reader). Recorded automatically in the same turn (the standing rule). confidence: verified — built, type-checked, and the two graphs were measured against each other (RBA's real atoms vs. Dossier's own).

Placement correction (2026-06-17, same-session follow-up — not a re-decision). This record originally stated the inline graph lived in the dev-only wrapper below <LandingPage> with the shared template left "untouched." That placement stranded the graph BELOW the landing's footer — the footer reads as "end of page," so the operator reported "i dont see it." Fix shipped: LandingPage.svelte gained a generic, optional, production-empty afterMain?: Snippet prop, rendered between </main> and <SiteFooter>. It is empty for the Dossier landing (/) and every shipped client instance — the template still renders every LandingContent value identically (no client content drives structure), so DEC-0037 invariant #1 holds in spirit: when afterMain is undefined the render is byte-identical to before. The dev-only preview passes the client graph into afterMain, so it now sits in the page flow ABOVE the footer, not appended after it. The accurate statement is therefore: "the shared template gains a generic, production-empty afterMain extension point; the graph is slotted there (above the footer), not appended after it." All "template untouched" / below-the-template phrasings below should be read through this correction. Verified live: graph at y=4887, footer at y=5899 (graph above footer); 53 RBA nodes render; no regression to / (afterMain undefined → identical render); pnpm check clean; full suite 389 passed. (Files: packages/app/src/lib/components/LandingPage.svelte.)

Context

The dev-only client landing preview (/preview/<slug>, The marketing landing becomes a tailorable per-client template — a typed LandingContent model rendered by LandingPage.astro; the Dossier render stays byte-for-byte identical; client instances are values of the same type, canonical in the client's own repo, generated by the generate-landing skill) renders a tailored LandingContent instance. That instance's "live surfaces" showcase beat (DEC-0037's content-contract extension) cards /board and /graph and links into them. But in the monorepo dev server those links resolve to Dossier's own KB: DOSSIER_KB is unset, so /graph reads knowledgeDir() = Dossier's own knowledge base. So viewing /preview/rba and clicking through showed Dossier's graph, not RBA's — the opposite of what the tailored page promises ("both surfaces read your OKF and render it").

The operator's ask was direct: "show the rba graph when i view them on the preview." The chosen UX (the operator picked it): render the client's real graph inline on the preview page, beneath the landing, so the proof is right there — no navigation, no perturbing the process-wide KB.

The constraint that shaped the whole mechanism: do this without touching the process-wide DOSSIER_KB env or the prerendered /graph (which is built statically over the configured KB, DEC-0043). Mutating DOSSIER_KB to point at the client mid-request would be a process-global side effect that would corrupt /graph and every other reader.

Options considered

1. How the preview shows the client graph — link out vs. render inline.

  • (a) Leave the surfaces-card /graph link as-is. Rejected: in the monorepo dev server it shows Dossier's own graph, contradicting the tailored page's claim that the surfaces read the client's OKF. The exact gap the operator flagged.
  • (b) Render the client's real graph INLINE beneath the landing, in the dev-only preview wrapper (chosen). The operator picked this. The proof is on the page; the shared LandingPage template stays structurally neutral via a generic, production-empty afterMain slot (DEC-0037 invariant #1 — one template, geometry/structure never tailored per client; the slot is empty for / and every shipped instance, so the render is byte-identical when unset); the graph the dev preview passes into that slot lives only in the dev-only path. (See the Placement correction note above: the graph is slotted in the page flow ABOVE the footer, not stranded below it.)

2. How to point the graph at the client's KB — mutate DOSSIER_KB vs. read the client dir directly.

  • (a) Set DOSSIER_KB to the client dir for the preview request. Rejected: DOSSIER_KB / knowledgeDir() is process-wide. Mutating it would corrupt the prerendered /graph and every other KB reader — a process-global side effect masquerading as a local one.
  • (b) Read the client's served OKF directory DIRECTLY, leaving DOSSIER_KB alone (chosen). Add an optional dir param to readKbAtoms; the preview passes the resolved client dir. The configured KB and the prerendered /graph are completely unaffected.

3. How to feed the inline graph — duplicate /graph's logic/markup vs. extract shared builders.

  • (a) Copy the /graph payload-building + markup into the preview page. Rejected: forks the graph recipe and the explorer markup — every future graph fix or polish pass would have to be applied twice, and the two could drift. Violates the standing fully-open-and-atomic rule (feedback-fully-open-and-atomic) and the single-source-of-truth bar.
  • (b) Extract one shared builder + one shared component, both callers consume them (chosen). buildGraphView(entries, routes) (the payload + SSR-fallback recipe) and <GraphExplorer> (the stage + no-JS fallback + island mount). /graph becomes a thin caller; the preview is a second caller. One recipe, one component, never copy-pasted.

Decision

Render a client's REAL OKF knowledge graph inline beneath their tailored landing preview (/preview/<slug>), dev-only, by reading the client's served OKF directory directly through three atomic, no-copy-paste extractions — never by perturbing the process-wide DOSSIER_KB or the prerendered /graph.

The three extractions (the load-bearing part):

  1. readKbAtoms(dir?: string) (packages/okf-view/src/kb.ts) — added an optional dir param, defaulting to knowledgeDir(). A caller can now read a SPECIFIC client's served OKF directory; slugs derive relative to whichever dir is read, so a passed dir yields that KB's own clean slugs. Backward-compatible (existing zero-arg callers unchanged). Required rebuilding the package's dist + tsc -b declarations because @dossier/app consumes the built dist, not source.
  2. buildGraphView(entries, routes) (packages/app/src/lib/server/graph-view.ts, server-only) — the graph-payload + SSR-fallback recipe extracted verbatim out of (app)/graph/+page.server.ts. ONE recipe feeds both /graph (over the configured KB) and the preview (over the client dir). The /graph loader is now a thin caller (buildGraphView(readKbAtoms(), routes)).
  3. <GraphExplorer> (packages/app/src/lib/components/GraphExplorer.svelte) — the explorer stage + the no-JS summary/index fallback + the empty state + the #graph-data {@html} injection + the d3 island mount extracted out of (app)/graph/+page.svelte; graph.css travels with the component. Both /graph and the preview render it; each page still owns its own head/shell/footer.

Wiring. The preview registry entry (packages/app/src/lib/content/landing/examples/registry.ts) gained an optional okfDir (repo-relative path to the client's served OKF; RBA → clients/rba/tenants/rba-consulting/okf). The preview loader (packages/app/src/routes/preview/[client]/+page.server.ts) resolves it from the repo root (the same ../../ anchor knowledgeDir() uses), calls buildGraphView(readKbAtoms(dir), getRouteMap()), and passes the result to +page.svelte, which renders <GraphExplorer> inside a bordered "Dev preview · live surface · {label}" band passed into <LandingPage>'s production-empty afterMain slot — so it sits in the page flow above the footer (see the Placement correction note; the earlier "below <LandingPage>" placement stranded it below the footer).

Rationale

  • It closes exactly the gap the operator flagged. /preview/rba now shows RBA's own graph inline (53 nodes, 143 edges), making the tailored page's "both surfaces read your OKF" claim true on the page, not just in copy.
  • DEC-0037 invariant #1 is honored in spirit — the shared LandingPage template gains only a generic, production-empty extension point. The template did NOT stay literally untouched: it gained an optional afterMain?: Snippet prop rendered between </main> and <SiteFooter>. But that slot is empty for / and every shipped client instance (no client content drives structure), so the template renders every LandingContent value identically and the / render is byte-identical when afterMain is undefined. The dev-only preview passes the client graph into afterMain, so it sits in the page flow above the footer (not stranded below it — the bug the original "untouched / banded appendage below <LandingPage>" placement caused; see the Placement correction note). The template's geometry/structure is still not tailored per client.
  • The standing fully-open-and-atomic rule is honored (feedback-fully-open-and-atomic): shared component/builder, never copy-paste. The GraphExplorer + buildGraphView extractions exist precisely so the preview reuses /graph's logic and markup rather than duplicating them — and they leave /graph cleaner (a thin caller) as a side benefit.
  • Sovereignty (DEC-0001) + KB-agnostic (Astro Starlight as the docs-site generator + the product-owner, starlight-engineer, and documentation-engineer functions / DEC-0026 lineage via Lenient KB-atom reader in @dossier/okf-view (readKbAtoms) — faithful getCollection reproduction for the SvelteKit app). The preview reads the client's OWN git OKF, READ-ONLY and derived; it reads the client dir directly instead of mutating the global DOSSIER_KB, so the prerendered /graph and the process-wide KB are unaffected. Route deep-links use the host route map (getRouteMap()), so client atom ids absent from it yield null links — the honest state (those atoms have no reading surface in this app), never a fabricated link or a crash.
  • Crash-safe by construction. A missing/unreadable client dir → readKbAtoms returns []buildGraphView yields the explorer's honest empty state, never an error.
  • DEV-ONLY by construction. The preview route already 404s in production (prerender = false + the import.meta.env.DEV 404 guard) and imports the registry only inside the import.meta.env.DEV gate, so a production bundle never pulls in the client copy — the one visibility gate DEC-0037 keeps. No client graph ships to the public site.

Consequences

  • readKbAtoms is now KB-targetable per call. Any caller can read a specific KB dir without env mutation — useful beyond the preview (e.g. a future multi-tenant comparison surface). The default-arg keeps every existing caller (the /graph and /board loaders) byte-identical.
  • /graph's payload recipe and explorer markup now have exactly one home each (buildGraphView + GraphExplorer). A future graph fix or polish pass lands once and both surfaces get it. The /graph loader and page shrank to thin callers.
  • The preview proves the surfaces over the client's ACTUAL atoms. /preview/rba is now an end-to-end demonstration that the tailored landing AND the derived graph both read the client's own OKF — a stronger onboarding artifact.
  • A follow-up is now visible: the same preview's surfaces-card board link still points at Dossier's /board in the monorepo dev server. The same readKbAtoms(dir) + a shared board view-model would let the board render inline too. Filed as Render the client's REAL board inline on the dev-only preview (the board sibling of DEC-0047's inline graph) (backlog, p2, Principal SvelteKit Engineer) so it doesn't evaporate.
  • Verification (reproduced this turn, real numbers):
    • /preview/rba inline graph: 53 nodes, 143 edges, types {capability, client, concept, process, workflow} — RBA's real atoms (e.g. rba-project-delivery-lifecycle, rba-lifecycle-align). No Dossier-own ids present.
    • /graph (Dossier own) unchanged: 95 nodes, 494 edges, over the extracted buildGraphView.
    • pnpm check (svelte-check): 0 errors / 0 warnings; tsc -b clean; full vitest suite 389 passed / 1 skipped.
  • Two-way door. Additive and revertible: drop the okfDir field + the inline band and the preview reverts to the landing alone; readKbAtoms's dir param is a backward-compatible default-arg; buildGraphView/GraphExplorer could be re-inlined into /graph. The durable commitments are the no-env-mutation principle (read the client dir directly, never perturb the process-wide DOSSIER_KB or the prerendered /graph) and the SSOT placements (one graph recipe in buildGraphView, one explorer in GraphExplorer, one KB reader in readKbAtoms, never forked).

Review

confidence: verified: built and exercised this turn — the two graphs were measured against each other (RBA 53/143 with no Dossier ids; /graph unchanged at 95/494 over the shared builder), pnpm check is 0/0, tsc -b is clean, and the full vitest suite is 389 passed / 1 skipped. No open gate on the core decision. The named follow-up (inline client board on the preview) is tracked separately by Render the client's REAL board inline on the dev-only preview (the board sibling of DEC-0047's inline graph) and is not a gate on this record. See The marketing landing becomes a tailorable per-client template — a typed LandingContent model rendered by LandingPage.astro; the Dossier render stays byte-for-byte identical; client instances are values of the same type, canonical in the client's own repo, generated by the generate-landing skill and Lenient KB-atom reader in @dossier/okf-view (readKbAtoms) — faithful getCollection reproduction for the SvelteKit app.