1,149 words
Status: accepted · 27 Aug 2026
Document and startup ownership is superseded by ADR-0039.
Context
docs/ is a hundred and twenty thousand words across seventy markdown files —
the design bible, ten concept pages, fifteen decision records, the guides and
the agent handbook — with eighty-three Mermaid diagrams in them. packages/*
carries another eight hundred and twenty exports whose doc comments are, in
places, the most carefully written prose in the repository. All of it was
readable only on GitHub, by somebody who already knew the repository existed.
The obvious thing is a documentation site: Docusaurus, VitePress, Starlight, or TypeDoc's own HTML. Any of them would be running the same afternoon.
What none of them can do is the thing this project has and they do not. The
front door is a menu over a running simulation, framed on Earth, and the pitch
PRODUCT.md makes is that it is the only pitch a screenshot cannot fake. A
documentation site published beside the application cannot put the engine at the
top of a page about the engine — it can put a picture of it there, which is a
different claim.
There is a second force, and it is the one that decides between the remaining options. The URL is this product's public surface (ADR-0011): everything addressable is reachable and everything reachable is addressable, and a navigation is a single router. A themed TypeDoc output embedded in the page would satisfy the first force and break this one — it arrives with its own navigation, its own search, its own page shell and its own idea of what a link is, so a reader crossing from a concept page into the reference leaves the masthead, the rail and the router behind. That is two sites wearing one palette.
Decision
/docs is a mode, like the planetarium and the player. Its content is
rendered at build time and fetched at runtime, and the reference is drawn from
TypeDoc's reflection tree by this application's own components.
- A mode, not a dialog and not a second site.
modeForPathanswersdocs; the route renders inside.hud-layeras a sibling of the canvas, so the engine is running behind it and no navigation can rebuild the renderer. It is a mode rather than a dialog because a dialog opens over a mode, and two of them driving the observatory is the fourth camera producer AGENTS.md forbids. pnpm docs:buildrenders every page. Markdown throughmarked, listings through Shiki against a theme written from this system's palette, links resolved against a route table that mirrors the repository's own paths. The output is JSON underapps/game/public/doc-content/, gitignored, staged intodistby the same build that produces the bundle.- TypeDoc's JSON, not its HTML. The serialized reflection tree is the
complete model — every signature, every comment part, every resolved
{@link}target, every source position — soscripts/docs/api.mjsprints types itself and turns a reference to a documented symbol into a link into this site. An API page and a concept page are then the same page with different content in it. - Fetched, not bundled. The manifest, the search index and every page body
are same-origin
GETs.public/sw.jsalready serves that directory stale-while-revalidate, so the documentation is available offline, which is this project's base case rather than its degraded one. - The build refuses to publish quietly. A markdown file under
docs/that no wing lists fails the build, and TypeDoc'svalidation.invalidLinkfails it on a cross-reference to a renamed symbol.
Alternatives considered
A separate static site — Docusaurus or VitePress at docs. or /docs on
another origin. Cheapest by a wide margin, and it loses the one thing worth
having: the masthead cannot be the engine, because the engine is a WebGPU
renderer in the application it would be published beside. It also splits the
URL surface in two, which ADR-0011 spent its whole argument on.
TypeDoc's HTML with a custom theme, embedded. The shortest road to a reference that is nearly the right color, and the one that produces two sites wearing one palette: separate navigation, separate search, separate shell, and links that leave the router. Rejected on structure rather than on looks.
typedoc-plugin-markdown, feeding the same pipeline as the prose. Genuinely
attractive — one renderer for everything — and it discards the model on the way
through. A signature becomes a string of backticks, so a type in it cannot be
made a link, and cross-references become markdown links this build would have to
re-resolve against a name table TypeDoc had already resolved correctly.
A custom TypeDoc output plugin, emitting our page JSON from inside TypeDoc's
renderer. The same result as what was built, reached by depending on
app.renderer's internals — which are not the stable half of that library. The
serializer is, and the transform is then ours and testable in Node.
Bundling the pages through import.meta.glob. Vite would content-hash them
and the service worker's cache-first branch would cover them for nothing. At
nine hundred pages it is nine hundred dynamic imports, which is a chunk manifest
larger than most of the pages it indexes.
Server-side rendering, for crawlers. The right answer eventually and not
today: it needs the Worker on every navigation, which turns a free static asset
request into a billed invocation — the same trade src/site.ts records for the
Open Graph tags. Only /docs is in sitemap.xml until that changes.
Consequences
The documentation is offline, addressable, and inside the product. A link to
/docs/concepts/frames#the-frame-chain opens the reading room over a live
simulation of the thing being described, from a cold tab, with no network.
The reference can be followed rather than only read. UniverseVector in a
signature is a link to its page, across package boundaries, because the address
of every export was resolved before anything was rendered.
A page is a fetch, so the first paint of an article is not instant. The manifest arrives before the navigation can be drawn, and the mode shows the shape of a paragraph while it does. Offline and on a second visit it is a cache hit.
The build is slower by about eight seconds, almost all of it TypeDoc
converting twelve packages under the root tsconfig.json. It runs on every
pnpm build, because a documentation site that is one forgotten command away
from being stale is a stale documentation site.
Mermaid ships to the client. Laying out a graph needs a DOM with real fonts, so the alternative is a headless browser in the toolchain. It is a lazy import, only on a page that has a diagram, and the source is what is on screen until it resolves — and forever if it never does.
Nothing may hand-edit apps/game/public/doc-content/. It is derived, the
same way the brand artifacts are, and the markdown is the source. See
development.