ADR-018: The codebase map on the docs site¶
Status: Accepted (2026-10-18) · Builds on ADR-015 (the map) and respects ADR-014 (bun is the package manager only)
Context¶
ADR-015 put the one picture of the whole system
at /map in the web GUI — which means it exists only while the stack is up.
The place that explains the system, this site (golden-docs/, published
to GitHub Pages), had no picture at all. The ask was the map as a docs page
with its animations intact: the entrance choreography, the agent-card
beam, and the traveling comets are the point of the foglamp treatment, not
decoration, and a screenshot or a Mermaid redraw would have lost the trace,
the popovers, and the motion together.
Four constraints shaped the answer. The published site must keep working
offline and never phone home (the privacy-plugin precedent in mkdocs.yml).
The Python docs job in CI must not grow a JavaScript toolchain. ADR-014's
line — bun installs, Node executes — stands. And there must remain exactly
one implementation of the map: a port that could drift from /map would
be a second thing to keep true.
Decision¶
-
Same component, second build.
web/docs-map/holds a page shell (index.html,main.tsx,map.css,fonts.css) that mounts the app's ownCodebaseMapViewoutside the Next shell, plus its Vite config and the manifest helper.vite build(Vite with@tailwindcss/vite, run under Node bybun run build:docs-map) emits a self-contained bundle intogolden-docs/assets/map/: one page, one script, one stylesheet, the vendored favicons, the fonts stylesheet, and a manifest.map.cssimports the app'sglobals.csswhole, so the docs page and/mapcannot drift apart on colour or motion; Tailwind's source detection runs from the Vite root and the map's component files are named explicitly, so only the map's utilities are emitted. -
A committed, drift-guarded artifact — the
openapi.jsonpattern (golden-docs/openapi.json,web/lib/codebase-map.layout.json). The bundle is checked in;manifest.jsonrecords a sha256 over its inputs (the map components, data, layout snapshot,globals.css, the icons, andweb/docs-map/itself);web/lib/__tests__/docs-map.test.tsrecomputes that hash and fails CI when the committed bundle was built from anything else. Inputs rather than output bytes, because minifier output is not byte-stable across tool versions; dependency versions deliberately excluded, because a React patch changes the bytes but not the picture. The same test pins the properties the site relies on: relative URLs, the icons beside the page, one script and no ELK, and nothing in the bundle for the privacy plugin to fetch except the fonts it is meant to localize. -
Framed, not inlined.
golden-docs/map.mdembeds the page in a same-origin<iframe>(plus a full-screen link). Theme follows the site: an inline script inindex.htmlreads Material's<body data-md-color-scheme>through the parent, mirrors it onto the frame's.darkclass, and watches it with aMutationObserver; opened standalone (or framed by a page it cannot read) it follows the OS. It runs ahead of the stylesheet, so the first paint is already the right scheme. -
Relative everything. GitHub Pages serves the site under
/varagity/, so the build usesbase: "./"and the favicons gained an indirection:MapIconBaseContext(web/components/map/icon-base.ts) defaults to the app's absolute/map-icons, and the docs entry providesmap-icons, resolved beside the page. Nothing else in the map knows the path. -
Fonts through the privacy plugin.
next/fontis not available outside Next, sofonts.csscarries one@import url(https://fonts.googleapis.com/…)for Geist, Geist Mono, and Instrument Serif. Material's privacy plugin rewrites externalurl()s it finds in static stylesheets and downloads the files at build time, exactly as it does for the site's ownextra_cssentry — so the published page never contacts Google Fonts. A<link>in the static HTML would not be rewritten (the plugin processes rendered pages and static.css/.js, not static.html), which is why the import lives in a stylesheet.
Rationale¶
The alternatives, each rejected for a reason worth keeping:
- Frame the live app (
<iframe src="http://localhost:3000/map">). Needs the stack; every Pages reader gets a dead frame. - Next static export.
outputis config-global (the Docker image needsstandalone),/c/[id]cannot be enumerated, and the export would drag the sidebar shell along. - Mount the bundle inline in the Material page. Tailwind's preflight and
Material's cascade would fight across the whole page —
varagity.css's.md-typeset h1/h2rules alone would restyle the side panel — andnavigation.instant's XHR page swaps would need remount choreography. The iframe is an isolation boundary that costs nothing here. - A static SVG, a Mermaid redraw, or a screenshot. Loses the trace, the detail popovers, and the motion — the explicit ask.
- A vanilla-JS port. A second implementation of the map to keep true.
- Build at docs time (a MkDocs hook running the bundler). Puts bun and Node in the Python CI job, and misbehaves whenever they are absent.
bun build. Already in the binary and fast, but it is the bun runtime bundling — the line ADR-014 drew. Vite was already in the tree as Vitest's peer; promoting it to a declared devDependency cost nothing.
Consequences¶
- A third generated artifact and its command.
bun run build:docs-mapjoinsscripts/export_openapi.pyandbun run test -u; the update rule in Architecture → The codebase map has a second step. Sizes at adoption: ≈290 KB of script (88 KB gzipped — React is in it), ≈49 KB of stylesheet (9 KB gzipped), 46 KB of icons; a regeneration adds on the order of 100 KB compressed to history, and regenerations track map changes (three in the map's first three months). viteand@tailwindcss/viteare devDependencies. The Python docs job is unchanged:mkdocs build --strictcopies the committed bundle verbatim, and the Vitest guard runs in the web job.- The in-app map is functionally untouched — the icon path moved behind
a context with the old default. Playwright's
/mapspecs still apply. - Known behaviours, by design. The wheel pans inside the frame rather than scrolling the page (standard embedded-map behaviour, said on the page; the frame is bounded and the full-screen link exists). And the guard shares ADR-015's residual risk: it cannot tell whether an edge still tells the truth — edge semantics stay a human-maintained property.