Litestore · Developer guide
Testing

Harness

This page explains the checks that keep your fork of Litestore healthy and how to run them.

The harness is a set of checks and guides that keeps your fork of Litestore healthy as it grows. The store itself is free and open source under the MIT license. The harness is the paid layer around it.

Each check is a scripts/check-*.ts file that reads the source code in your repository. The checks run on your machine using ripgrep and the TypeScript AST. Nothing is uploaded, and you don't sign in to anything.

Terminal
bun run verify           # biome, the checks, vitest run, typecheck — sequential
bun run verify:parallel  # the same 15 legs through scripts/verify.ts, concurrent

How it runs

HarnessHow it runs
Commandbun run verify runs the steps one after another. bun run verify:parallel runs the same 15 legs at the same time, up to one fewer than your number of CPU cores.
ChecksThere are 25 scripts in scripts/check-*.ts. 24 of them run inside verify.
Outside the gateOne check, bun run db:guarantees:audit, is left out because it needs a live DATABASE_URL, and CI doesn't have one.
Rules8 guides in .claude/guides/, about 1,800 lines in total, linked from .claude/CLAUDE.md.
BaselinesProblems that already exist are recorded in a baseline instead of blocking you. A check fails when a count goes up, not when it is above zero.
NetworkNone. Nothing phones home.
ScopeYour fork, including modules you add.

Because existing problems go into a baseline, you can adopt the harness on a large codebase without the build failing on day one. The baseline can't grow. You can add code, but you can't add new violations.

Each check with a baseline can update it with --update-baseline. Running that flag is a deliberate step, and it shows up in your diff where reviewers can see it.

verify names eleven checks one by one and runs the other thirteen through the lint:cleanliness aggregate.

bun run lint is a shorter loop for everyday work. It runs Biome with --write, six of the named checks and the same lint:cleanliness. It skips the theme boundaries, event completeness and three visibility checks, so it doesn't replace the full verify.

The 25 Checks

Cache and Freshness

These checks stop pages from showing old data after a write.

  • Cache invalidation (check-cache-invalidation.ts): Every server file that runs Prisma mutations directly must import an invalidation helper from ~/lib/cache, or revalidateTag/revalidatePath from next/cache. Without this, an admin table can show old rows until the 60-second TTL runs out. BaseCRUD subclasses and write-only activity tracking are allowlisted.
  • Cache tags (check-cache-tags.ts): Fails when the first argument to revalidateTag, invalidateTag or invalidateTags is a string or template literal. It also fails on a protected cacheTag = "…" declaration on a CRUD class. Tags must come from CACHE_TAGS in lib/cache.ts, because a mistyped tag invalidates nothing and raises no error.
  • Public feed visibility (check-public-feed-visibility.ts): Reads the sitemap, RSS and llm.txt routes and flags any db.product or db.collection query whose where never mentions isHidden. This keeps hidden content out of what search engines and crawlers see. You can mark a line with // visibility-ok to skip it.

Database Guarantees

These checks catch a migration that weakens one of the database's promises.

  • DB guarantees (check-db-guarantees.ts): Connects to DATABASE_URL and confirms that 20 CHECK constraints, 4 triggers, 11 unique indexes and 6 views exist in the live schema. It then looks for duplicates in the normalized uniqueness keys, and fails if any of the three drift views returns a row. This is the one check verify doesn't run.
  • Guarantee contract (check-guarantees-contract.ts): Reads the migration SQL and needs no database. It fails if db:migrate or db:deploy stop using Prisma Migrate, if a script uses prisma db push, if amountBase stops being a non-nullable Decimal(12,2), or if a named constraint, trigger or view disappears from the migrations.
  • Nested soft delete (check-nested-soft-delete.ts): The Prisma soft-delete extension only adds deletedAt: null to top-level reads. This check looks for a nested include or select of a soft-deletable to-many relation that has no deletedAt filter of its own.
  • Nested hidden (check-nested-hidden.ts): Does the same for isHidden, which nothing adds automatically. It only scans server/web, components/web and the non-admin app routes, because the admin is supposed to see hidden rows.

Dead Code

These checks find work that was built but never connected.

  • Dead components (check-dead-components.ts): Flags files under components/ that no other file imports. These are usually a second version of something that already exists.
  • Orphan exports (check-orphan-exports.ts): Works on single exports instead of whole files. Every exported PascalCase component must be rendered as JSX somewhere, so it catches a dead export inside a file that is otherwise in use.
  • Dead server actions (check-dead-server-actions.ts): Finds exported createServerAction definitions that no code outside their own file uses. That shows you a permission-wrapped action that no button ever calls.
  • Dead Prisma fields (check-dead-prisma-fields.ts): Flags models and fields in prisma/schema.prisma whose names appear in no .ts or .tsx file.
  • Duplicate utilities (check-duplicate-utils.ts): Flags an exported function or const name that is defined in more than one file under lib/ and utils/. That's how two versions of slugify drift apart without anyone noticing.
  • Unused dependencies (check-unused-deps.ts): Flags runtime dependencies in package.json that nothing imports. Packages used only in config are allowlisted, and ones that were already unused are in the baseline.

Type and Boundary Safety

These checks catch runtime crashes that the compiler should have caught.

  • No as any (check-no-as-any.ts): Scans the whole source tree for as any and as unknown as. It records today's count and fails on any new one. These casts are most dangerous in money, order and inventory code.
  • Client and server boundary (check-client-server-boundary.ts): Catches a server file that imports and calls a plain lowercase function exported from a "use client" module. Next registers that export as a client reference, so the call throws when a request comes in instead of failing the build.
  • Client bundle isolation (check-client-bundle-server-only.ts): Follows the static imports from every "use client" module and fails if any path reaches a server-only file or services/db. It stops at "use server" files, because those are RPC boundaries. It takes under a second, while next build takes minutes to tell you the same thing.
  • Prisma field balance (check-prisma-field-balance.ts): Catches a scalar field that the code writes or selects as an object key but never reads as .field. Dead columns usually start out this way, stored but never used.

Convention Discipline

These checks keep one standard way of doing each job.

  • Discipline (check-discipline.ts): Runs 69 named structural rules, each with its own baseline file under scripts/discipline-baselines/. Examples include using the sprite for icons, the factory for tables and the CRUD classes for mutations. Others require permission-wrapped actions and forbid export *, broad revalidatePath calls and money on a major-unit field.
  • Discipline hygiene (check-discipline-hygiene.ts): Checks the harness itself. It fails on a baseline entry whose file no longer exists, and on an allowlist regex that matches nothing. Both kinds of leftover only pile up, and each dead exemption stays a hole forever. This check has no baseline of its own.
  • Raw flex (check-raw-flex.ts): Flags a className with a standalone flex, which makes a display:flex container, where the shared Stack component should be used. It ignores flex-1, flex-col, flex-wrap and inline-flex, because those modify a container rather than create one.
  • Theme boundaries (check-theme-boundaries.ts): Keeps theme-specific CSS selectors and theme ids inside config/theme.ts and config/themes/. Global CSS, admin code and shared components must not change a theme by name. They use theme.target and the helper APIs instead.
  • Hack budget (check-hacks.ts): Counts setTimeout, inline style={{, !important, hardcoded high z-[…], as any and @ts-ignore/@ts-expect-error in the source. It fails when any of these counts rises above the number in scripts/hacks-baseline.json.

Events and Tests

These checks protect the code where a bug costs real money.

  • Critical tests (check-critical-tests.ts): Every file in the money and inventory paths must be imported by at least one *.test.ts. That covers checkout, payments, pricing, cart, tax, orders, inventory, shipping, refunds and returns. The check looks at imports, not file names, so it catches a test that only checks its own stub.
  • Event completeness (check-event-completeness.ts): Every member of DOMAIN_EVENT_TYPES must have a createDomainEventHandler in functions/domain-events.ts and be registered in the domainEventFunctions array. Without a registered handler, an event goes to Inngest and nothing picks it up, so subscribed webhooks silently get nothing.
  • Analytics events (check-events.ts): Fails when the first argument to capture() is a string literal. Event names must come from ANALYTICS_EVENTS in lib/analytics/events.ts, so a capture("search") can't drift away from the "search_executed" that the funnel looks for.

The Rule Set

The rule set is eight guides in .claude/guides/, about 1,800 lines in total, linked from the root file .claude/CLAUDE.md. They are written for an AI agent working in the repository, so that Claude Code makes changes that follow the conventions already in place.

GuideWhat is in it
design-review.mdA checklist to go through before you build anything non-trivial. It covers the judgement calls the checks can't make.
before-building.mdBefore you write a new page, model or module, name the existing one you're copying, or stop and ask.
code-style.mdThe conventions the codebase follows, with the reason for each one.
ui-components.mdWhich shared component to use, and when a new one is justified.
data-api.mdHow queries, server actions and cache tags are structured, and where each one goes.
new-entity.mdEvery step for adding a model, from the schema through the admin table to activity logging.
platform-primitives.mdA table of the standard patterns for lifecycle, queues, publish gates, registries and alerting.
project-specific.mdThe rules that only apply to Litestore, including the money and inventory constraints.

The checks catch a convention someone forgot. The guides catch a more expensive mistake, which is building the wrong thing correctly.

Get the harness

See the harness page.

On this page