Skip to content

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 only zod.
  • src/index.ts exports the generated contract.
  • Drizzle moves to devDependencies; zod is the only runtime dependency.
  • Subpath exports on packages/db so the contract imports @tabletop/db/schema, not the index (the cheap half of ADR-0022 option A).
  • scripts/check-generated.ts in 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.ts imports only zod — asserted by reading the file, not by inspecting package.json.
  • src/index.ts reaches 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

Terminal window
pnpm --filter @tabletop/shared generate
pnpm --filter @tabletop/shared test
pnpm --filter @tabletop/mobile build
pnpm gate

Notes

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.