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
- 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.sveltegained a generic, optional, production-emptyafterMain?: Snippetprop, rendered between</main>and<SiteFooter>. It is empty for the Dossier landing (/) and every shipped client instance — the template still renders everyLandingContentvalue identically (no client content drives structure), so DEC-0037 invariant #1 holds in spirit: whenafterMainis undefined the render is byte-identical to before. The dev-only preview passes the client graph intoafterMain, 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-emptyafterMainextension 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/(afterMainundefined → identical render);pnpm checkclean; 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
/graphlink 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
LandingPagetemplate stays structurally neutral via a generic, production-emptyafterMainslot (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_KBto the client dir for the preview request. Rejected:DOSSIER_KB/knowledgeDir()is process-wide. Mutating it would corrupt the prerendered/graphand 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_KBalone (chosen). Add an optionaldirparam toreadKbAtoms; the preview passes the resolved client dir. The configured KB and the prerendered/graphare completely unaffected.
3. How to feed the inline graph — duplicate /graph's logic/markup vs. extract shared builders.
- (a) Copy the
/graphpayload-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)./graphbecomes 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):
readKbAtoms(dir?: string)(packages/okf-view/src/kb.ts) — added an optionaldirparam, defaulting toknowledgeDir(). A caller can now read a SPECIFIC client's served OKF directory; slugs derive relative to whicheverdiris 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 -bdeclarations because@dossier/appconsumes the builtdist, not source.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/graphloader is now a thin caller (buildGraphView(readKbAtoms(), routes)).<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.csstravels with the component. Both/graphand 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/rbanow 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
LandingPagetemplate gains only a generic, production-empty extension point. The template did NOT stay literally untouched: it gained an optionalafterMain?: Snippetprop 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 everyLandingContentvalue identically and the/render is byte-identical whenafterMainis undefined. The dev-only preview passes the client graph intoafterMain, 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. TheGraphExplorer+buildGraphViewextractions exist precisely so the preview reuses/graph's logic and markup rather than duplicating them — and they leave/graphcleaner (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/graphand 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 →
readKbAtomsreturns[]→buildGraphViewyields the explorer's honest empty state, never an error. - DEV-ONLY by construction. The preview route already 404s in production (
prerender = false+ theimport.meta.env.DEV404 guard) and imports the registry only inside theimport.meta.env.DEVgate, 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
readKbAtomsis 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/graphand/boardloaders) 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/graphloader and page shrank to thin callers.- The preview proves the surfaces over the client's ACTUAL atoms.
/preview/rbais 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
/boardin the monorepo dev server. The samereadKbAtoms(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/rbainline 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 extractedbuildGraphView.pnpm check(svelte-check): 0 errors / 0 warnings;tsc -bclean; full vitest suite 389 passed / 1 skipped.
- Two-way door. Additive and revertible: drop the
okfDirfield + the inline band and the preview reverts to the landing alone;readKbAtoms'sdirparam is a backward-compatible default-arg;buildGraphView/GraphExplorercould be re-inlined into/graph. The durable commitments are the no-env-mutation principle (read the client dir directly, never perturb the process-wideDOSSIER_KBor the prerendered/graph) and the SSOT placements (one graph recipe inbuildGraphView, one explorer inGraphExplorer, one KB reader inreadKbAtoms, 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.