Appearance
Keeping these docs updated
The docs stay honest through three mechanisms: docs-as-code, a mechanical freshness gate, and the CLAUDE.md contract that makes doc updates part of every feature change.
The two tiers
| Tier | Where | Audience | Deployment |
|---|---|---|---|
| User docs | frontend/src/docs/*.md + registered in frontend/src/docs/index.ts | Customers — simple, "what's possible", how each part works | Ship with the app at /docs (public) |
| Dev docs | docs/ (this VitePress site) | The team — architecture, internals, every option | Repo-only; cd docs && npm run dev. Never deployed publicly |
What belongs in which tier
Dev docs document the technical reality — every meaningful technical change updates them. New service, changed contract, different concurrency model, new gate: the dev page moves with the code.
User docs change ONLY for user-visible behavior. A backend change that improves concurrency, refactors internals, or hardens security without changing what the user sees must NOT touch the user docs — it gets an acknowledgment instead (below). If the user experience changed (new button, different flow, new capability, changed limits), the user doc changes too, in user language.
The freshness gate
Every docs page declares the source paths it documents in frontmatter:
yaml
---
sources:
- backend/src/modules/performance/health
---cd docs && npm run check (or node docs/scripts/check-freshness.mjs) then reports:
- STALE — a source has a git commit newer than the page's last commit (committed drift) → exit 1.
- pending — a source has uncommitted changes → warn only (you're mid-work; update the page before committing).
- DEAD MAP — a
sources:entry matches no tracked file (the code moved) → exit 1. - UNMAPPED — a page without
sources:frontmatter → warn (it can silently rot).
Clearing a STALE flag — two paths, matching the tier policy:
The change affects what the page documents → update the page and commit it in the same commit as the source. That clears the gate on its own: when the source's newest commit also carries the page, the two shipped together, which is the review the gate exists to enforce. Nothing to ack (2026-09-01 — before that, even the ordinary "I updated the doc with my change" case needed one, and a PR whose merge commit touched a mapped source could not be cleared any other way).
It doesn't (internals-only — the common case for user docs) → acknowledge the review:
shcd docs && npm run check -- --ack frontend/src/docs/orbit-agent.mdThis records the reviewed source content (a hash over the source's blobs at HEAD, not a commit hash, so a squash merge or rebase of the PR cannot invalidate it, and a staged edit to the source is "pending" work rather than a voided ack) in
docs/.freshness-ack.json; commit that file with your change. The page's prose is untouched, and the review is auditable. A later source change makes the page stale again — acks cover exactly the content that was reviewed.
User docs carry the same sources: frontmatter; frontend/src/docs/index.ts strips it before rendering.
The coverage gate (absence, not staleness)
Freshness can't see a capability that never got a docs page. scripts/check-coverage.mjs (second half of npm run check, also the last verify gate) scans the backend source for every registered module id, agent tool, and agent skill, and fails unless each one is either mapped to an existing user-doc page in docs/coverage-map.json or listed there under exclusions with a reason (501 shells, agent-internal modules). Dead map entries (capability removed from code) fail too, so the map can't rot in either direction.
The map entry is the mechanical half. The content half is the CLAUDE.md contract: a new capability ships — same commit — with a section in its home page, a chip in what-to-ask.md if askable, and a card in what-orbit-can-do.md if it's a new area. CLAUDE.md files are excluded from doc scans and the snapshot (frontend/src/docs/CLAUDE.md is the authoring contract, not a doc).
The Vue-hazard gate (compilability, not content)
VitePress compiles every dev-docs page as a Vue template, so prose is not inert: a bare angle-bracket placeholder (<day>, <column>) parses as an unclosed element and fails the Dev docs CI build, and a bare double-brace expression evaluates as an interpolation during the SSG render (writing that literal here would trip the gate documenting it — inline code does not escape interpolation, only v-pre does). Neither freshness nor coverage parses prose, so this shape passed every local gate and broke CI twice in one day (2026-09-01, both in guide/frontend.md).
scripts/check-vue-hazards.mjs (third leg of npm run check, the last verify gate, and the pre-commit hook when a dev-docs page is staged) is the millisecond tripwire: it flags lowercase non-HTML tags and bare interpolations outside code fences, inline code, and comments. Backticking the placeholder is always the fix; deliberate escapes pass (\<date\> backslash-escaped, v-pre lines). It is a tripwire, not a compiler — CI's vitepress build stays the whole truth — but it catches the class that actually bites. User docs (frontend/src/docs/) are exempt: DocViewer renders plain markdown, not Vue.
The agent-docs snapshot
The get_product_docs agent tool serves the user docs from a committed snapshot at backend/src/modules/agent/product-docs/ (backend deploys don't have the frontend tree on disk). After changing any user doc, regenerate it:
sh
cd docs && npm run syncnpm run check fails with UNSYNCED when the snapshot drifts, so it can't silently go stale — commit the regenerated snapshot together with the doc change.
The snapshot directory is excluded from freshness-source comparisons (SNAPSHOT_EXCLUDE in check-freshness.mjs): it lives inside backend/src/modules/agent, and without the exclusion every mirror sync advanced that source dir and re-tripped orbit-agent.md into a stale/ack race with itself.
The working contract (from CLAUDE.md)
When a change alters behavior in an area a docs page maps (check its sources:), updating that page is part of the change — same commit. User-visible behavior changes also update the matching user doc. /docs-sync runs the checker and walks through updating whatever is stale.
Adding a page
- Dev docs: add the
.mdunder the right section, give itsources:frontmatter, add it to the sidebar indocs/.vitepress/config.mjs. - User docs: add the
.mdinfrontend/src/docs/withsources:frontmatter, register it infrontend/src/docs/index.ts(slug, group, title, description — the registry strips frontmatter and extracts headings for search/TOC automatically; an optional 6th arg opts into DocViewer layouts:{ cardGrid: ["h2-slug"], steps: true }), then runcd docs && npm run syncso the agent-docs snapshot picks the page up — the freshness gate fails UNSYNCED until it does.
Writing user docs: the block primitives
User docs are written in blocks, not prose — the viewer (DocViewer.tsx) renders a small set of primitives, all plain markdown so the agent's snapshot stays readable text:
| Primitive | Markdown | Renders as |
|---|---|---|
| Callout | > [!TIP] / > [!NOTE] / > [!WARNING] as the blockquote's first line | Colored callout with icon |
| Ask block | > [!ASK] + a bullet list of questions | Copyable "Ask Orbit" question chips |
| App block | > [!APP] + click path / bullets | An "In the app" card (where to click) |
| Card grid | ### Title + body under an H2 whose slug is in the doc's cardGrid opt-in | Responsive card grid |
| Steps | Ordered list in a doc registered with steps: true | Numbered step blocks |
Pipeline rules the primitives depend on (breaking one is silent):
- Every H2 keeps ≥1 line of its own prose —
extractSectionsdrops empty-bodied headings from the search index, so a section that is only chips/cards becomes unfindable. - Headings stay plain text — anchor ids are computed twice (registry from markdown, viewer from the rendered tree) and must agree.
toPlainstrips the[!TYPE]markers so they never leak into search snippets or read time.- The backend's
get_product_docsindex view truncates each page's headings (see itsMAX_*constants) and hardcodes a prose list of doc areas in the tool description — adding a page means updating that string.