Quality Gates
The local and CI checks that protect generated data, tokens, builds, links, and tests.
Inner loop
Run focused checks while changing code:
pnpm check
pnpm test:unitpnpm check typechecks the root host, @zudo-sg/ui, and the demo app. pnpm test:unit runs Vitest unit tests.
For docs-only work, use the filtered docs checks:
pnpm --filter @zudo-sg/doc check
pnpm --filter @zudo-sg/doc build
node scripts/check-links.mjs --dist=doc/distGenerated-data drift
Generated files are checked in, but their source of truth lives elsewhere:
pnpm check:sg-registryverifies the styleguide story registries generated frompackages/.ui/ src/ **/ *. stories. tsx pnpm check:token-manifestverifies the UI token manifest generated frompackages/andui/ styles/ tokens. css packages/.ui/ styles/ colors. css
The docs workspace uses zudo-doc's package-owned default z-index tiers. It has no local z-index generator or drift check.
When a source file changes, run the matching generator before committing:
pnpm gen:sg-registry
pnpm gen:token-manifestToken and accessibility gates
pnpm lint:tokens enforces the tight-token strategy so components keep using approved semantic token utilities. pnpm contrast:audit checks configured color relationships for contrast regressions.
Story files are also covered by unit tests in packages/, including module-shape and source-drift checks.
Pre-push gate
The local pre-push command is:
pnpm b4pushIt runs formatting checks for MDX, token lint, codegen drift checks, typecheck, unit tests, root build, demo build, link checks, HTML validation, Playwright smoke tests, and a final manual interactive smoke prompt.
Use the full gate before pushing broad changes. For documentation-only changes, the required focused checks are usually the docs build, docs check, link check against doc/dist, and git diff --check.
Standalone provider package gate
packages/ui is dual-mode: a workspace member and, at the same time, its own standalone package with a separate pnpm-lock.yaml (it resolves a different Preact version than the root workspace does). Two gates verify the standalone side without disturbing the workspace: pnpm test:ui-provider-package (typecheck + Vitest, run on an isolated copy outside the repo) and pnpm verify:ui-provider-install -- --exact (installs the published package spec in a temp directory and proves it builds for a real consumer).
Canonical local sequence
Run in this order — root gates first, then the standalone provider gates:
pnpm install
pnpm check
pnpm test:unit
pnpm test:ui-provider-package
pnpm verify:ui-provider-install -- --exactEvery command above is safe to run from the repo root as written: pnpm check typechecks @zudo-sg/ui through --filter, which resolves against the root lockfile, and both provider gates install, typecheck, and test in a temp directory, so packages/ is never written. One command does run with its working directory inside the package — pnpm pack, in verify:ui-provider-install's local-package fallback, which --exact never reaches — and it is safe because pack is neither run nor exec, so verify-deps-before-run does not fire. Clean-workspace CI is still the source of truth; this sequence is a local convenience, not a substitute for it.
The one thing that breaks it: running pnpm directly inside packages/ui
Never run pnpm with your working directory inside packages/ui, and never pass --dir packages/ui — not even for a no-op command. Both forms trigger pnpm's verify-deps-before-run check, which compares against packages/ui's own lockfile (it declares packages: [], so it's its own workspace root) and reinstalls its standalone dependency set into packages/ before your command even runs. That replaces the workspace-linked preact symlink with a second, self-contained copy, and the repo ends up with two Preact module identities. This is not about install flags — no explicit pnpm ... install is required, and none of the commands in the canonical sequence above do this.
# Don't do this:
cd packages/ui && pnpm test
pnpm --dir packages/ui exec node -e "0"
# Do this instead:
pnpm test:ui-provider-package # isolated copy, same checks
pnpm --filter @zudo-sg/ui typecheck # runs in the root workspace contextRefreshing the standalone lockfile
When packages/'s dependencies change, regenerate its nested lockfile with --lockfile-only and install at the root last — this updates both lockfiles without ever materializing packages/, because --lockfile-only never writes node_modules at all:
pnpm --dir packages/ui install --lockfile-only
pnpm installThis is a lockfile-refresh procedure, not a way to avoid the hazard above — running pnpm --dir packages/ui <anything else> right after this sequence still contaminates the tree. What keeps the standalone gates safe is that they run on an isolated copy, not this ordering.
Recovery for an already-contaminated tree
If packages/ contains a nested preact@* entry, or packages/ is a real directory instead of a symlink, the tree is contaminated. Recover from the repo root:
rm -rf packages/ui/node_modules && pnpm installThe rm -rf is the load-bearing half. A plain pnpm install alone reports Already up to date and leaves the second Preact instance in place — that message prints identically whether the tree is actually healthy or still broken, so don't use it to judge success. Only a filesystem check (as above) tells you the recovery worked.
What a contaminated tree looks like
A developer who hits this without knowing the cause sees, in the root unit suite, roughly 76 failed tests across roughly 44 UI-related test files, with "Found multiple elements" errors — two Preact instances mean React Testing Library's element identity checks stop matching. The demo app's Playwright suite fails four tests: theme controls go missing, a Browse control stops updating aria-expanded, and a page error surfaces reading Preact's internal __H field. None of this points at the change that was actually made; if you see these specific symptoms, check for contamination first with the recovery above before debugging further.
CI gate
Pull requests target main or base/** branches. CI mirrors the b4push shape with separate jobs for token lint, codegen drift, typecheck, unit tests, root build, demo build, smoke E2E, and dist checks.
Scheduled rich CI is intentionally deferred until after public release. Visual and platform-specific checks are run ad hoc instead of as standing nightly infrastructure.