5.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
pnpm install # pnpm workspaces; pnpm only
docker compose up -d # postgres (5432), pipeline postgres (5433), valkey (6379)
pnpm dev # api (:3000) + web (:5173) concurrently
pnpm build # shared → db → api, in dependency order
pnpm typecheck # tsc --build --noEmit across all projects
pnpm lint # eslint .
pnpm format # prettier --write .
pnpm test # vitest watch, all workspace projects
pnpm test:run # single run (also what the pre-commit hook runs)
pnpm vitest run apps/api/src/services/gameService.test.ts # single file
pnpm vitest run -t "returns 400" # single test by name
pnpm --filter @lila/db generate # drizzle-kit generate (after editing schema.ts)
pnpm --filter @lila/db migrate # apply migrations (uses DATABASE_URL_LOCAL)
pnpm --filter @lila/pipeline pipeline:run
packages/shared and packages/db are consumed as built dist/ output — rebuild them (pnpm --filter @lila/shared build) after changing them, or downstream packages will typecheck against stale types. pnpm --filter @lila/api dev does this automatically.
Husky pre-commit runs lint-staged (prettier + eslint --fix) then the full test suite.
Architecture
Monorepo: apps/api (Express + ws), apps/web (React 19, Vite, TanStack Router, Tailwind 4), packages/shared (Zod contract), packages/db (Drizzle), data-pipeline (vocabulary ETL). Docs live in documentation/ — ARCHITECTURE.md, DECISIONS.md, DATA_PIPELINE.md, STATUS.md, BACKLOG.md.
Strict layering in apps/api: router → controller → service → model (packages/db) → PostgreSQL. Each layer talks only to the one below it. Controllers do HTTP only (Zod safeParse, then next(error)); services hold business logic and never read req; models hold queries and no domain semantics. apps/api must never import drizzle-orm — all queries live in packages/db/src/models/.
Errors: AppError subclasses carry their own statusCode; a single errorHandler middleware maps them. ValidationError is thrown in controllers, NotFoundError in services.
packages/shared is the single source of truth for every shape crossing the API boundary (schemas/game.ts, schemas/lobby.ts, schemas/auth.ts, constants.ts). Changing a schema breaks compilation in both api and web simultaneously — that's intended. Supported languages (en/it/de/es/fr) and POS values are constants here and are CHECK-constrained in the DB schema.
WebSocket: upgrades on /ws on the same HTTP server. Better Auth cookie is validated at upgrade (ws/auth.ts), then ws/router.ts dispatches on the type field of a Zod discriminated union to ws/handlers/. Lobby membership is persisted in PostgreSQL; live game/room state lives in InMemoryGameSessionStore / InMemoryLobbyGameStore behind interfaces, so it is lost on restart (Valkey swap is planned — keep the interface boundary intact).
Answer evaluation is server-side. The correct answer is never included in what is sent to the client.
Data model: two vocabulary schemas coexist. The app reads vocabulary_entries + entry_translations (one row per word sense); the new pipeline targets words → senses → translations, which is migrated but empty. packages/db/src/models/termModel.ts is still on the live pair. Adding a language is rows, not schema changes. Auth tables (user, session, account, verification) are owned by Better Auth.
Data pipeline
data-pipeline/ is mid-rewrite on this branch (refactor/gemini-only-pipeline): the old local-LLM/adapter architecture is gone, and pipeline.ts currently holds the staged design as pseudocode comments — there is no executable pipeline yet. Current pieces: source-data/{lang}/{pos} wordlists, prompt (the Gemini system prompt, a plain UTF-8 file), db/schema.sql (SQLite staging: words → senses → translations), and a dedicated pipeline PostgreSQL on port 5433.
Read documentation/DATA_PIPELINE.md for orientation and current phase status, documentation/pipeline/design-doc.md for the schema/difficulty model/Gemini JSON contract, and documentation/pipeline/roadmap.md for the phase plan. Everything in documentation/archive/ is superseded — data-pipeline-local-llm.md, llm-setup-local.md, and model-strategy-cefr-voters.md describe the removed local-LLM / CEFR-voter pipeline and are historical only.
The sense-based words/senses/translations schema is what the pipeline targets; the app is not migrated to it yet and still queries vocabulary_entries/entry_translations.
Conventions
- TypeScript is maximally strict (
tsconfig.base.json):exactOptionalPropertyTypes,noUncheckedIndexedAccess,noPropertyAccessFromIndexSignature,verbatimModuleSyntax,erasableSyntaxOnly. Index access yieldsT | undefined— handle it rather than loosening the config. Env vars are read asprocess.env["KEY"]. - The base tsconfig deliberately omits
lib/module/moduleResolution; each package sets its own (apiNodeNext, webESNext/bundler). - Tests are co-located (
gameService.test.tsbesidegameService.ts), use vitest globals, and mock@lila/dbwithvi.mock— no test database. Endpoint tests use supertest against thecreateApp()factory without starting a server. - All env config lives in the single root
.env(see.env.example);packages/dband the pipeline both resolve it from the repo root.