Skip to content

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

TierWhereAudienceDeployment
User docsfrontend/src/docs/*.md + registered in frontend/src/docs/index.tsCustomers — simple, "what's possible", how each part worksShip with the app at /docs (public)
Dev docsdocs/ (this VitePress site)The team — architecture, internals, every optionRepo-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:

  1. 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).

  2. It doesn't (internals-only — the common case for user docs) → acknowledge the review:

    sh
    cd docs && npm run check -- --ack frontend/src/docs/orbit-agent.md

    This 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 sync

npm 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 .md under the right section, give it sources: frontmatter, add it to the sidebar in docs/.vitepress/config.mjs.
  • User docs: add the .md in frontend/src/docs/ with sources: frontmatter, register it in frontend/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 run cd docs && npm run sync so 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:

PrimitiveMarkdownRenders as
Callout> [!TIP] / > [!NOTE] / > [!WARNING] as the blockquote's first lineColored callout with icon
Ask block> [!ASK] + a bullet list of questionsCopyable "Ask Orbit" question chips
App block> [!APP] + click path / bulletsAn "In the app" card (where to click)
Card grid### Title + body under an H2 whose slug is in the doc's cardGrid opt-inResponsive card grid
StepsOrdered list in a doc registered with steps: trueNumbered step blocks

Pipeline rules the primitives depend on (breaking one is silent):

  • Every H2 keeps ≥1 line of its own proseextractSections drops 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.
  • toPlain strips the [!TYPE] markers so they never leak into search snippets or read time.
  • The backend's get_product_docs index view truncates each page's headings (see its MAX_* constants) and hardcodes a prose list of doc areas in the tool description — adding a page means updating that string.