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 -vcat .node-versionInstall:
git clone https://github.com/versable-git/versable-builder.gitcd versable-builderpnpm installThis 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 demoapps/_templates/ what you copy to start a new consuming app, one folder per shapepackages/ui/ @versable-git/ui, published, raw TypeScriptpackages/toolkit/ @versable-git/toolkit, zero React, privatedocs/ the design canon, app patterns, ADRs, plan history3. 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 playgroundIt 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 packagepnpm lint:tokens # eslint ., the real lint: bans raw palette/arbitrary colorspnpm test # vitest in packages/ui (render smoke, SchemaForm, sidebar), packages/toolkit and packages/qsyncpnpm build # next build in apps/playground; the kit has no build step, it ships raw TSTwo 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 docsIt 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:
- 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 examplepackages/ui/src/breadcrumbs/breadcrumbs.tsxandbreadcrumbs.types.ts. Presets, if the component has shaped variants, are aRecord<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). - 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-54for the Breadcrumbs block). - Contract doc:
packages/ui/docs/<name>.md, one file per component. Frontmatter carrieskit_icon:(noticon:; mkdocs-material claimsicon:for its own paths and the build breaks otherwise,apps/playground/src/lib/docs-registry.ts:375-377) andsubtitle:, 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 thedata-toneshape, doc 13 §A13), "Travels with", "Snippet". Seepackages/ui/docs/breadcrumbs.md:1-39for a complete instance andpackages/ui/docs/app-shell.mdfor the checklist filled in. - Playground page:
apps/playground/src/app/components/<name>/page.tsxplus agallery.tsx.page.tsxstays a thin server module: it setsmetadatafromshowcaseMetadata(href)and passesreadExampleSources(name)into the gallery (apps/playground/src/app/components/breadcrumbs/page.tsx:1-8). Anexamples/subfolder of small named.tsxsnippets is common but not universal. It exists when the gallery pulls printable source strings for a CodeBlock (seeapps/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). - Registry entry: add a
ShowcaseEntryto the right group inCOMPONENT_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), andrelated(sibling routes worth a jump from this page's header); see the Breadcrumbs entry atapps/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. - 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 thekitroot inapps/playground/src/lib/docs-registry.ts:206-212, so a new file appears in/docs/kit/<name>automatically. - 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 tolocalStorage.pg-theme,docs/index.md:59-61), and runpnpm --filter @versable-git/ui typecheck(or the rootpnpm typecheck). - 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 tomain.publish-kit.ymldoes 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/).
example.tsx: the composite itself,"use client", importing only from@versable-git/ui(hereCard,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).page.tsx: reads that same file's text viareadExampleSource("compact-dropzone")(apps/playground/src/app/patterns/read-source.ts:7-12) and hands it toPatternShellalongside 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: aProvenanceRow[], each a realcite(repo-relativefile:line, resolvable in the sibling checkouts) and anote. Seeapps/playground/src/app/patterns/compact-dropzone/page.tsx:31-40citing into bothspeedway/app/components/NewJobForm.tsxandwalmart-mvp/frontend/src/pages/ImportParts.tsx.appSpecific: what a real page adds that the fixture deliberately leaves out.
- Registry entry: add it to
PATTERN_GROUPSinapps/playground/src/app/showcase-registry.ts:413-469, same shape as a component entry, withrelatedpointing at the components it composes (compact-dropzone names/components/dropzone,/components/tabs,/components/card, line 434). - 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/*.mdtrait doc, cited back to the pattern'sexample.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
/docsroute, 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.