Agent docs

Bringing an existing app onto the kit

Bringing an app that already works onto the kit without breaking what it does today.

An app that already works is a harder migration target than an empty one, because everything it does today is a claim someone will notice if you break it, and almost none of those claims are written down anywhere.

Audience: an agent (or person) migrating an existing app onto the kit.

So this guide is ordered by risk, not by effort. The cheap mechanical steps come last on purpose.

Step 0: write the parity ledger, before you replace anything#

This step is not optional and it does not come second.

A parity ledger is a list of behaviours the app has today that must still be true when you are done, each with the check that would catch its loss. It is written before the first replacement, reviewed by whoever cares about the behaviour, and it is the artifact every later gate runs against.

Why this is step zero. In July 2026 this codebase rebuilt a set of owner-reviewed surfaces across two rounds and purged three days of accumulated UX in the process. The plan had not asked for a rebuild. It repeatedly said "stays" and "threading" (docs/plan/archive/22-job-siloed-catalog.md:26-27, :38, :163), meaning the existing surfaces were to be threaded through, not replaced. Nothing failed, no gate went red, and every gate passed, because every gate verified the new flows. No artifact represented the old behaviour, so nothing could fail on its absence.

That is the whole mechanism, and it is worth stating plainly: a rebuild does not announce itself. Summarisation preserves task momentum and drops constraints, so "thread the existing surface" becomes "build the surface" by the second checkpoint, and by the third it is what the plan says. The ledger exists because a sentence in a plan binds nobody and a checklist item that fails does.

What goes in it. One row per behaviour, and a behaviour is anything a user would notice and complain about:

BehaviourWhere it lives nowThe check that catches its loss

No data

Fill the third column with something runnable or observable: a named flow somebody clicks, a baseline screenshot, a test, a count. "Looks the same" is not a check. If you cannot write the third column, you do not understand the behaviour well enough to preserve it, and that row is the most dangerous one on the page.

A rebuild is allowed. This is not a ban. It is a requirement that the choice be explicit and that the person who owns the UX has seen the list of what they are giving up. Deciding to drop a behaviour is fine. Discovering afterwards that it is gone is not.

Step 1: resolve name collisions#

Before deciding what to keep or replace, find every local component sharing a name with a kit export. Until that is resolved, no other decision can be made reliably, because a call site reading <StatusDot ...> does not tell you which component it means.

Walmart is the worked example, and the numbers are the point. Seven local components shadow kit export names: StatusDot, DataTable, EmptyState, Tabs, Timestamp, SidePanel and a local CopyButton (walmart-mvp/docs/04-technical-debt.md, and frontend/src/features/catalog/WalmartSubmit.tsx:511 for the last). StatusDot shows the cost. Inside one frontend/src, pages/Home.tsx:3 imports the kit's from @versable-git/ui, while pages/Workspace.tsx:9 and features/catalog/CatalogDetail.tsx:30 import a local one from features/catalog/pipeline with an unrelated signature. Reading <StatusDot /> at a call site tells you nothing; you have to go read the import first, every time.

None of this is visible in a diff, which is exactly how it accumulated, and it is why an adoption grep must match the import rather than the symbol. A grep for a bare component or helper name in this codebase reliably returns the wrong number, in either direction. It has produced both a false zero and a false 35 in the space of one afternoon.

Resolve each one by renaming the local component, not the kit import. The local one is the exception and should read as one.

The rule that falls out of it: no local component is exported under a kit export's name. A local copy that has to stay for now takes a name that says so (walmart's local table became PlainTable on 2026-08-18, so DataTable means the kit's everywhere in that repo), and its file header cites the kit component it stands in for and the release that retires it (packages/ui/CHANGELOG.md, the Retires lines). Walmart's drift ledger is walmart-mvp/docs/technical/06-kit-drift.md.

Step 2: check what your pin can actually reach#

A canon rule that says "as of kit 0.0.N" is unreachable if the app pins below N, and a caret does not save you: under npm semver a caret on a 0.0.x version is a patch pin, so ^0.0.14 resolves to exactly 0.0.14.

The two current consumers differ. Walmart pins ^0.0.24, speedway declares ^0.0.14 against a kit at 0.0.25. Speedway's dev tree hides its own pin behind a symlink into packages/ui, so it runs whatever is local while a clean install would not, which means "it works on my machine" is load-bearing there in the literal sense.

Decide the target version before you start, bump it deliberately, and re-read the canon knowing which rules that version can honour.

Step 3: adopt in dependency order, not in enthusiasm order#

Work outward from the shell, because a page migrated inside an unmigrated shell inherits two conflicting layout owners and you will debug the seam instead of the page.

  1. AppShell, Sidebar, Topbar. One owner of the frame.
  2. Card, PageTitle, page composition. One owner of horizontal inset per framed container (05 §A4).
  3. States, PageInfo, EmptyState, PageLoading, PageError, skeletons. Doing these early is what makes the later pages cheap, because every subsequent surface needs the same three-way branch.
  4. Table and DataTable, per surface, following AP-10.
  5. Overlays, following AP-11, starting with the row-zero decision rather than porting whatever shape exists today.

At each step, run the ledger. Not at the end.

Step 4: the things that will surprise you#

Collected from the two apps that have done this.

A second implementation is cheaper to write than to find. That is the whole reason step 1 exists. Assume the app already contains one of whatever you are about to build.

The kit component may have no callers. FilterBar is complete, shipped, and used by nobody; both apps built their own toolbars. A kit export existing is not evidence it is the proven path, and packages/ui/docs/ says plainly which components have shipped usage and which do not. Read that before adopting one.

Some rules stop being your problem. Each component doc has a "What this saves you" section naming the canon rules that component retires, and 12-primitives-and-rules.md sorts every rule by whether a primitive already handles it. Migrating is the moment that list pays: rules you were enforcing by hand can be deleted from your review checklist, not just satisfied.

Some never will. One meaning per row click cannot be enforced by any primitive, because the kit cannot see selection state it does not own. Those stay in the checklist permanently.

Done means the ledger passes, not that the diff is merged#

Every row checked, by the check written in its third column, by someone who did not write the migration. Rows that were deliberately dropped are marked dropped, with who agreed. A ledger with unchecked rows is an unfinished migration, whatever the pull request says.

Then delete the orphaned code, and not before. Quarantine first, prove parity against the ledger, then remove. Deleting zero-importer code that this same work stream orphaned is how the July purge finished the job.

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