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 three apps that consume the kit (walmart-mvp, speedway, and the forge console versable-forge-v6) live in sibling checkouts next to this repo, not inside it; their pins are stated once, in the first paragraph of CLAUDE.md. Two more things sit in the tree: packages/qsync, the URL half of the table sync seam, and contract/, the foundry contract, a tenant here until the org split moves it to its own repo.

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; USAGE.md is the consumer's door
packages/toolkit/ @versable-git/toolkit, zero React, published, no app consumer yet
packages/qsync/ @versable-git/qsync, the URL half of the table sync seam
docs/ the design canon, app patterns, ADRs, the board, the inbox, plan history
contract/ the foundry contract, a tenant until the org split; nothing in CI reads it

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 in this environment, so it may already be up: curl -s -o /dev/null -w '%{http_code}' http://localhost:5104/ answers 200 when it is. Two shapes share the port, never both at once:

  • playground-dev, the hot-reloading dev server, for editing. Turbopack's memory grows with every route visited and never releases (2.2 GB after a session; scripts/dev-guard.md is the cap for long sessions).
  • playground-prod, a production build served by next start, for review and testing sessions (about 190 MB). No hot reload; a change means a rebuild. The filesystem-reading routes (docs browser, pattern source) keep working because it is still a server; a true static export is not possible for this app.

When pm2 says neither exists (they are not saved across daemon restarts), start one from cold, from apps/playground:

pm2 start "fnm exec --using 24 -- pnpm dev -p 5104" --name playground-dev
pnpm build && pm2 start "fnm exec --using 24 -- pnpm start" --name playground-prod

Never run pnpm build while playground-dev is up; the build clobbers .next under it. pm2 delete the one you are replacing first.

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.

The bar for done#

This is the one list. CLAUDE.md and AGENTS.md point here rather than restating it, so it cannot drift between doors. It is what .github/workflows/ci.yml runs on every push, in this order, after an install with a frozen lockfile; run it locally before a commit, through fnm (fnm exec --using 24 -- <command>):

pnpm -r typecheck
pnpm lint:tokens
pnpm -r test
pnpm build
node scripts/ci-citations.mjs
node scripts/build-do-nots.mjs && git diff --exit-code -- docs/DO-NOTS.md
node scripts/sync-method-docs.mjs --check --drift-only

lint:tokens fails on errors and only warns on the tint-ladder nudge. The last two are the ones a docs change most often trips: the citation ratchet (docs-wide BROKEN and OUT-OF-RANGE may not rise above the baseline; the governing layers stay at zero) and the do-nots freshness check (a Banned section edited without regenerating the page fails the push). CI runs one more step that is advisory on a laptop, node scripts/check-notebook.mjs, the canon notebook's lag check. A UI change is done only after all of this AND the page has been looked at on :5104 in both themes. 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).

Changing what an existing component looks like by default#

A default change reaches every screen in every app on the next upgrade, so it has three extra gates. Why they exist: docs/plan/90-defect-prevention.md.

  1. Quote the ruling. The commit and the changelog row quote the owner's verbatim line that asks for the change. A paraphrase is not a ruling, and a change with no quote does not ship.
  2. Show it once. The owner sees one before and after image of the change and agrees before it merges. One image per change, not one per screen.
  3. Count the call sites. Grep each app for the usages the change alters (for a flipped Button default, every Button with no variant) and put the counts in the changelog row, so an upgrader knows what to check.

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 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. The check that catches a forgotten entry is node scripts/sync-method-docs.mjs --check, which besides the method copies now lists every doc in the four published trees that the nav does not name (run by hand before a docs commit; deliberately not a CI gate, owner ruling D7a 2026-09-03). Seventeen docs were missing from the nav when the check was written.

A component is described once, and that once is the contract doc's frontmatter subtitle. node scripts/sync-showcase-subtitles.mjs writes it into the showcase registry, and --check fails when the two have drifted. Edit the doc, never the registry entry. Two kinds of page keep their own sentence, because no single contract describes them: a page with no contract doc, and one of several pages sharing one (the table family reads table.md, chip-compare reads chip.md). The script names them when it runs, so the exception is visible rather than silent. Owner ruling D4a, decision page vb-rewrites-s01, 2026-09-04.

The same sync now rewrites paths on the way into the package: a canon doc citing docs/design-language/… is copied with method/design-language/…, because that is where the file actually sits inside an installed kit. A path this package does not carry, such as docs/plan/ or docs/evidence/, is left as the repo wrote it, so it reads as provenance rather than as a broken link. Owner ruling D1a, same page, whose note reads "Nonsense question, it should not have to be a trade-off".

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. Filing what an app sends back#

A defect, a proposal or a question about the kit that surfaced in an app is one entry in docs/plan/INBOX.md, in the shape that file gives, and then you go back to your task. Nothing is built from the inbox directly; a review run the owner calls reads it whole and promotes what earns it. Grep the file for the surface's name before adding, and never delete an entry. Deferrals of work this repo already knows about go to docs/plan/BACKLOG.md; a helper that might belong in the toolkit goes to packages/toolkit/PROPOSALS.md.

10. Where to look next#

  • README.md, who reads what, and the map by what you came to do.
  • packages/ui/USAGE.md, the door for anyone putting the kit in an app: what you came for in one hop, the six steps, the scope rule.
  • 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