WI-016: Generate the client contract
WI-016: Generate the client contract
Implements ADR-0022, option C, chosen after WI-023 measured the cost.
Problem
Importing one runtime value from @tabletop/shared put 660KB of drizzle into the mobile bundle — pg-core, drizzle-zod and every table definition, to validate a slug.
Two things hid it. import type is erased before bundling, so type-only use costs nothing and revealed nothing. And the guard that should have caught it — a test asserting packages/shared has no postgres or pg dependency — passed, because what it actually depends on is @tabletop/db and drizzle-orm.
In scope
src/schemas.ts→src/derived.ts: generator input, still the drizzle-zod source of truth.scripts/generate-schemas.ts→src/generated.ts, committed, importing onlyzod.src/index.tsexports the generated contract.- Drizzle moves to
devDependencies;zodis the only runtime dependency. - Subpath exports on
packages/dbso the contract imports@tabletop/db/schema, not the index (the cheap half of ADR-0022 option A). scripts/check-generated.tsin the gate.- An equivalence test, and a replacement for the guard that could not fail.
Out of scope
zod/mini. It would cut into the remaining ~396KB but has API differences and deserves its own decision.- Runtime validation in the API. WI-012.
Definition of done
-
src/generated.tsimports onlyzod— asserted by reading the file, not by inspectingpackage.json. -
src/index.tsreaches nothing drizzle-shaped — asserted. -
packages/shared’s runtime dependencies are exactly["zod"]. - Changing the Drizzle schema without regenerating fails the gate, naming the first differing line.
- The generated and derived schemas agree on accept/reject across mutations and all 333 fixture games — because the diff check cannot catch a serialiser that is uniformly wrong.
- The generator throws on any construct it does not recognise rather than emitting a guess.
- The mobile bundle drops, and the budget drops with it so the saving cannot erode.
- Each new check proven able to fail.
- Gate green.
Verification
pnpm --filter @tabletop/shared generatepnpm --filter @tabletop/shared testpnpm --filter @tabletop/mobile buildpnpm gateNotes
The two guards answer different questions and both are needed. check-generated.ts proves the committed file is current. The equivalence test proves the serialiser is right — a serialiser that dropped every .min() would regenerate identically for ever and sail through the diff check. That was seeded: it fails eight tests.