Agent docs

Contributing

How to set up, run, use, and extend this repo (formerly USAGE.md): a component, a pattern, a doc, as the network of edits each is.

How to work on this repo: how it is put together, how to run it, and the exact sequence of files a change touches when you add or change a component, a pattern, or a doc, and contribute it back. README.md sells the repo, CLAUDE.md primes an agent's judgment, and this doc is the mechanical how-to underneath both.

1. What this repo is#

versable-builder is a shared UI system three things are built from: a component kit (packages/ui, published as @versable-git/ui), a framework-free utility toolkit (packages/toolkit), and a written design canon (docs/design-language, docs/app-patterns, packages/ui/docs) that says what the kit's components mean and when to reach for them (README.md:39-47). apps/playground is the fourth piece: a Next.js app that renders every component and pattern live, doubles as the docs browser, and is the only home of a few primitives that never shipped elsewhere. The two product apps that consume the kit, walmart-mvp and speedway, live in sibling checkouts next to this repo, not inside it (README.md:47, AGENTS.md:3-10).

2. Setup#

Prerequisites: pnpm, pinned to pnpm@11.10.0 in package.json:4, and Node >=24 (package.json:5-7), pinned exactly to 24 in the root .node-version file, which is also what every GitHub Actions workflow reads (.github/workflows/ci.yml:14-17). This matters concretely. A plain node or pnpm on a machine whose default Node is newer (Homebrew's is commonly 26) resolves the wrong runtime and breaks the build, which is why AGENTS.md:14-15 says to run everything through fnm exec --using 24 -- pnpm <cmd> if pnpm alone doesn't already land on Node 24. Check what you have before assuming:

node -v
cat .node-version

Install:

git clone https://github.com/versable-git/versable-builder.git
cd versable-builder
pnpm install

This is a pnpm workspace (pnpm-workspace.yaml:1-3): apps/* and packages/* are the member packages, installed together from one lockfile at the root. Always run pnpm, not npm, inside it. No environment variables are required; the playground ships fictional mock data (see "Deploying the playground to Vercel" under section 3).

Workspace layout:

apps/playground/ Next 16 app: the gallery, the docs browser, the demo
apps/_templates/ what you copy to start a new consuming app, one folder per shape
packages/ui/ @versable-git/ui, published, raw TypeScript
packages/toolkit/ @versable-git/toolkit, zero React, private
docs/ the design canon, app patterns, ADRs, plan history

3. Running things#

Every script under scripts/ is explained in scripts/README.md: what it is for, when to run it, and what it must never do. This section covers the ones you reach for while working.

The playground, the fastest way to see a component render, on port 5104 (apps/playground/package.json:6):

pnpm dev # from the repo root; filters to the playground

It also runs as a standing pm2 process named playground-dev in this environment, so it may already be up. Check before starting a second one. pnpm dev at the root maps to pnpm --filter @versable-git/playground dev (package.json:13). A memory-capped alternative exists, apps/playground: pnpm dev-safe (apps/playground/package.json:7), documented in scripts/dev-guard.md. It is opt-in; only run it if a human asks (scripts/dev-guard.md:5).

Everything else, from the root, in parallel across every workspace package (package.json:8-15):

pnpm typecheck # tsc -p tsconfig.json in each package
pnpm lint:tokens # eslint ., the real lint: bans raw palette/arbitrary colors
pnpm test # vitest in packages/ui (render smoke, SchemaForm, sidebar), packages/toolkit and packages/qsync
pnpm build # next build in apps/playground; the kit has no build step, it ships raw TS

Two things are worth knowing before you reach for these. pnpm lint (pnpm -r --parallel lint) currently runs a placeholder in every package (echo 'lint: wired in M0.4', e.g. packages/ui/package.json:11), so the lint that actually catches something is pnpm lint:tokens, run at the root with plain eslint . (package.json:11); it is what fails a raw bg-blue-500 or a bracket-arbitrary color (AGENTS.md:20-24). And packages/ui has no build script at all (packages/ui/package.json:9-12): the kit is shipped and consumed as source, so "does the kit build" is really "does the kit typecheck," which is exactly what CI and the publish workflow both run (.github/workflows/ci.yml:19, .github/workflows/publish-kit.yml:91).

Citation health, two scripts: the full report on demand, and the ratchet CI runs (scripts/check-citations.mjs, scripts/ci-citations.mjs):

node scripts/check-citations.mjs docs

It walks every backtick `file.ext:N` or `file.ext:N-M` citation across the doc corpus and reports files that are missing, line ranges past end-of-file, and citations pointing at a file committed after the doc that cites it (scripts/check-citations.mjs:1-9). Its default scan set is docs/design-language, docs/app-patterns, packages/ui/docs, and apps/playground (scripts/check-citations.mjs:20). That set does not include the top-level docs/ root, so a doc like docs/SCRIPTURE.md is only checked if you pass docs explicitly, as above (docs/SCRIPTURE.md:24-26). node scripts/ci-citations.mjs is the gate: it fails when the docs-wide BROKEN or OUT-OF-RANGE count rises above scripts/citations-baseline.json or when a governing doc (canon, contract docs, breakdowns) carries any BROKEN citation. Lowering the baseline once a count drops is a one-line edit to that file.

What CI does on push, in order (.github/workflows/ci.yml:18-26): install with a frozen lockfile, pnpm -r typecheck, pnpm lint:tokens (errors fail it; the tint-ladder nudge is warnings), pnpm -r test, pnpm build, the citation ratchet (node scripts/ci-citations.mjs), and a do-nots freshness check (node scripts/build-do-nots.mjs && git diff --exit-code -- docs/DO-NOTS.md, so a Banned-section edit without the regeneration fails the push). The two at the end are the ones a docs change most often trips. publish-kit.yml fires on a push to main touching packages/ui/**, packages/toolkit/** or packages/qsync/** and publishes each package whose package.json version the registry lacks; a commit that bumps nothing publishes nothing, and [skip kit] in the message holds all three (.github/workflows/publish-kit.yml:24-31). docs-publish.yml fires on a push to main touching docs/**, packages/ui/docs/**, or mkdocs.yml, and calls a reusable workflow in the sibling slack-automation repo that renders and publishes to tech.versable.ai (.github/workflows/docs-publish.yml:7-24). There is no documented local mkdocs build in this repo. The theme (.docs-kit/) is fetched by that CI job from slack-automation and is not vendored here (mkdocs.yml:5-7, pipelines/README.md:40-46). Verify a docs change with check-citations.mjs and a read of the rendered markdown, and trust the workflow for the actual render.

The live playground on Vercel#

The playground is deployed at https://versable-builder-playground.vercel.app/ (Vercel project versable-builder-playground in the Versable Team, Root Directory apps/playground, Node 24.x, framework Next.js, no env vars; the app is static with mock data). Vercel's GitHub integration redeploys it on every push to main; the git-main alias (versable-builder-playground-git-main-versable-team.vercel.app) tracks the branch and the bare domain follows the last successful production deploy. Nothing in the repo configures it (there is no vercel.json); the settings above live in the Vercel dashboard.

What the repo does carry: apps/playground/next.config.ts sets turbopack.root and outputFileTracingRoot to the repo root so the build traces the raw-TS transpilePackages workspace deps (@versable-git/ui, @versable-git/qsync), and the @source "../src" line in packages/ui/theme/index.css is load-bearing for kit classes on the deployed tree. Do not remove either.

After a push, verify at the live URL: /, /components/button, /components/data-table, /docs, /demo/review; click a row, flip the theme. Known noise: a push to the gh-pages branch (the mkdocs site) also triggers a Vercel preview build, which fails at once because that branch has no apps/playground; it does not touch production. An Ignored Build Step on the Vercel project would silence it.

4. How to add a kit component#

A component is never one file. Worked example: Breadcrumbs, added whole in one pass (task doc 53 G6.1, apps/playground/src/lib/docs-registry.ts:5-11 callout aside). The edits, in the order they need to happen:

  1. Source: a new directory under packages/ui/src/<name>/, at minimum <name>.tsx (the component) and <name>.types.ts (its props and any exported unions), for example packages/ui/src/breadcrumbs/breadcrumbs.tsx and breadcrumbs.types.ts. Presets, if the component has shaped variants, are a Record<Preset, Partial<Props>> merged with explicit props always winning (packages/ui/src/breadcrumbs/breadcrumbs.tsx:5-13); this is a repo-wide rule, not a Breadcrumbs-specific choice (AGENTS.md:38-45).
  2. Barrel export: add the component and its types to packages/ui/src/index.ts. This file is the authoritative inventory. If a thing is not exported there, it does not exist as far as any consumer or any doc is concerned (docs/index.md:36-37, packages/ui/src/index.ts:46-54 for the Breadcrumbs block).
  3. Contract doc: packages/ui/docs/<name>.md, one file per component. Frontmatter carries kit_icon: (not icon:; mkdocs-material claims icon: for its own paths and the build breaks otherwise, apps/playground/src/lib/docs-registry.ts:375-377) and subtitle:, a one-line summary index pages read instead of guessing from the body (apps/playground/src/lib/docs-registry.ts:378). The body follows the shape every kit doc uses: what it is, "When to reach for it", "Contract" (props), "Sanctioned combinations" (a table of combination, what it produces, where it's used, why), "Banned combinations", "Before you adopt this" (six questions with the failure each catches: does the shell or a parent already render this; does the app already ship a local one; does the kit pin reach the version; who supplies the accessible name and keyboard path; which canon §A rules bind it; does a stateful look read the platform's own selector or mint an attribute, and if it mints, does it follow the data-tone shape, doc 13 §A13), "Travels with", "Snippet". See packages/ui/docs/breadcrumbs.md:1-39 for a complete instance and packages/ui/docs/app-shell.md for the checklist filled in.
  4. Playground page: apps/playground/src/app/components/<name>/page.tsx plus a gallery.tsx. page.tsx stays a thin server module: it sets metadata from showcaseMetadata(href) and passes readExampleSources(name) into the gallery (apps/playground/src/app/components/breadcrumbs/page.tsx:1-8). An examples/ subfolder of small named .tsx snippets is common but not universal. It exists when the gallery pulls printable source strings for a CodeBlock (see apps/playground/src/app/components/field-chip/examples/ for the shape, seven small files, one behavior each); Breadcrumbs itself ships without one because its gallery composes everything inline (apps/playground/src/app/components/breadcrumbs/gallery.tsx).
  5. Registry entry: add a ShowcaseEntry to the right group in COMPONENT_GROUPS, apps/playground/src/app/showcase-registry.ts:31-411. The fields that matter: href, label, Icon, note (the landing-card tag), subtitle (one sentence, shown under the page title), doc (the repo-relative contract-doc path, wired into the page's "view contract" link), and related (sibling routes worth a jump from this page's header); see the Breadcrumbs entry at apps/playground/src/app/showcase-registry.ts:267-274. This one registry object is what derives the sidebar nav (toNav, line 472-477), the breadcrumb trail above the page (showcaseCrumbs, line 498-506), and the per-page AI-awareness prompt embedded in page metadata (awarenessPrompt, line 521-539). Nothing here is hand-wired twice.
  6. Docs browser exposure: nothing to do here if the doc lives under packages/ui/docs/. That whole directory is already swept on every request by the kit root in apps/playground/src/lib/docs-registry.ts:206-212, so a new file appears in /docs/kit/<name> automatically.
  7. Verify live in both themes: run the playground (already up as playground-dev), load the new page, flip the theme toggle in the top bar (apps/playground/src/app/theme-toggle-button.tsx, persisted to localStorage.pg-theme, docs/index.md:59-61), and run pnpm --filter @versable-git/ui typecheck (or the root pnpm typecheck).
  8. Release note: a kit change does not publish by itself. It rides the owner's next deliberate version bump to packages/ui/package.json, pushed to main. publish-kit.yml does the rest (README.md:170-179, CLAUDE.md, "Releasing the kit"). Never publish by hand from a laptop; it races the workflow (README.md:175-177).

5. How to add a pattern#

A pattern is a composite recipe, not a new primitive: real components composed the way an app actually ships them, with the provenance to prove it. Worked example: compact-dropzone (apps/playground/src/app/patterns/compact-dropzone/).

  1. example.tsx: the composite itself, "use client", importing only from @versable-git/ui (here Card, Dropzone, Tabs) plus React state to drive a knob through the real states the pattern has to handle: empty, populated, error, never a static screenshot (apps/playground/src/app/patterns/compact-dropzone/example.tsx:1-70).
  2. page.tsx: reads that same file's text via readExampleSource("compact-dropzone") (apps/playground/src/app/patterns/read-source.ts:7-12) and hands it to PatternShell alongside the rendered composite, so the printed source and the live render can never drift apart. PatternShell's props worth filling in (apps/playground/src/app/patterns/pattern-shell.tsx:22-48):
    • problem: what kind of problem this solves (visual, information, interaction, ease of use, discovery, gestalt hierarchy, self-explaining action container), explained in a sentence or two.
    • useWhen: one to three { when, example } situations that call for it.
    • provenance: a ProvenanceRow[], each a real cite (repo-relative file:line, resolvable in the sibling checkouts) and a note. See apps/playground/src/app/patterns/compact-dropzone/page.tsx:31-40 citing into both speedway/app/components/NewJobForm.tsx and walmart-mvp/frontend/src/pages/ImportParts.tsx.
    • appSpecific: what a real page adds that the fixture deliberately leaves out.
  3. Registry entry: add it to PATTERN_GROUPS in apps/playground/src/app/showcase-registry.ts:413-469, same shape as a component entry, with related pointing at the components it composes (compact-dropzone names /components/dropzone, /components/tabs, /components/card, line 434).
  4. Canon citation: if the pattern encodes a rule the design canon should know about (a spacing decision, a state-transition rule), that rule belongs in the relevant docs/design-language/*.md trait doc, cited back to the pattern's example.tsx, not restated only in the pattern page. The canon is the layer of record for anything a second, unrelated surface would also need (docs/design-language/00-overview.md:14-16).

6. How to propose a toolkit utility#

packages/toolkit/PROPOSALS.md is the backlog of helpers that might belong in the toolkit. Filing is deliberately cheaper than building: an agent who notices a generalizable helper writes six lines and moves on, and the whole list is processed at once at a wave boundary. The file's own header carries the entry shape and the four operations. Two rules bind a filer: grep for the name before adding, and never delete an entry, since a discarded proposal is the record that stops the next agent re-proposing it.

7. How to add or change a doc#

Three doc families, three different rules.

Canon docs (docs/design-language/*.md, docs/app-patterns/*.md): one pen. Every change is an owner-visible diff, never a bulk rewrite (docs/SCRIPTURE.md:22-26). Every trait doc keeps the same three-section shape: §A principles and mental models, §B presets and how-to, §C use cases through the apps, cited to file:line (docs/design-language/00-overview.md:12-16, CLAUDE.md, "The canon"). After any edit, run the citation checker with the docs argument as shown in §3 above. The checker's own default scope would silently skip a change to a file at the docs/ root, so pass it explicitly when in doubt.

Kit contract docs (packages/ui/docs/*.md): frontmatter needs kit_icon: and subtitle: (§4 above explains why the key isn't icon:), and the body follows the fixed shape: intro paragraph, "When to reach for it," "Contract," "Sanctioned combinations," "Banned combinations," "Before you adopt this," "Travels with," "Snippet" (packages/ui/docs/breadcrumbs.md; the checklist section is on packages/ui/docs/app-shell.md). Keep it current with the release it describes, per docs/index.md:36-38. Every Banned section feeds the generated docs/DO-NOTS.md; regenerate it with node scripts/build-do-nots.mjs after editing one.

Repo docs (README, this file, AGENTS.md, docs/SCRIPTURE.md, docs/index.md, ADRs): no fixed shape, but the same kit_icon: and subtitle: frontmatter convention if you want the doc to appear well-labeled in the in-app docs browser, since that browser is a fixed allowlist of files in three groups, Start here, Working here and Canon (apps/playground/src/lib/docs-registry.ts:77-181, 213-246). A brand-new top-level doc has to be added to one of those files: arrays by hand to be reachable at /docs/<group>/<slug>. A new file dropped into docs/design-language/, docs/app-patterns/ or docs/breakdowns/ needs no such registration, since those roots are swept by directory listing, with each tree's README as the group's first row (apps/playground/src/lib/docs-registry.ts:182-212).

One shared trap: mkdocs.yml's nav: block (mkdocs.yml:9-71) is a second, independent index of the same doc trees, used only by the published site, not by the in-app browser. A new canon or kit doc needs an entry there too if it should appear on tech.versable.ai. The in-app /docs route and the published site read two different lists, and neither derives the other.

8. Conventions that bite#

Zero em dashes in anything rendered or written. This is a standing, previously-breached constraint, not a suggestion. The canon calls it out by name (docs/app-patterns/00-data-loading.md:57), and the scripture's own honesty section records that the rule "is documented in this repo as having failed" once before, because the gate that catches it lives in the writing agent's own configuration, outside this repo, not in a repo-local hook (docs/plan/49-scripture-evidence/honesty-gate.md:85-91). Treat it as something to check by eye, not something CI guarantees for you.

Mono is for identifiers only, SKUs, ids, code, never decoration (docs/design-language/01-foundations.md:28).

Verify UI changes in both themes, not just one. The dark theme's literal name is versable-dark (packages/ui/theme/index.css:24), stamped as data-theme on <html>. The full pass, with the review skills that run each step and when each fires, is docs/app-patterns/14-validating-a-ui-change.md.

Never stage user_docs/speedway-todo.md or its sibling-repo counterpart, speedway/docs/user-notes.md. Both are owner-authored, and agents never commit them (CLAUDE.md, "Doc map").

Path-scoped git add, never git add -A, especially with more than one agent writing in parallel (AGENTS.md:34-35).

Never publish the kit from a laptop. A hand publish races the publish-kit workflow, which then finds the version already present and skips. [skip kit] in a commit message is the deliberate way to suppress a publish, not a hand npm publish (README.md:175-177).

The rendering judge, at batch boundaries only#

Ruled 2026-08-21 (doc 73 C1, which carries the full text and the evidence). A fresh seat that never authored the changes looks at the rendered result twice per cycle: once per review wave before the owner's sitting, once per release candidate over changed shipped exports. Cheapest instrument first (see --ui, then /ui-categorical-check, native vision only on a clean pass over a new surface); a canon edit to a visual rule sends the judge to the affected playground surfaces. Always advisory: findings land as /callouts rows citing a canon file:line, never as blocks. The kill condition rides the mechanism: two consecutive post-judge sittings without a lower render-visible defect count stops the judge. Per-turn telemetry notices must exist before its first run, so value per cost is measured rather than argued.

9. Where to look next#

  • README.md, who reads what, and the map by what you came to do.
  • CLAUDE.md, the doc map, what's authoritative for requirements vs. design vs. status, and the current product freeze.
  • docs/SCRIPTURE.md, why this repo exists, its goals, and an honest ledger of where it has and hasn't lived up to them.
  • docs/index.md, the docs map: what the docs can do, where kit truth lives, the two showcase surfaces.
  • packages/ui/README.md, installing and upgrading the kit in an app.
  • the playground's own /docs route, every doc above, rendered, searchable, and cross-linked, once the dev server is running.

Until 2026-08-19 this file was USAGE.md; the name changed, the sections did not.

@versable-git/ui · reference, canon, and method, read in place