# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commands ```bash 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 yields `T | undefined` — handle it rather than loosening the config. Env vars are read as `process.env["KEY"]`. - The base tsconfig deliberately omits `lib`/`module`/`moduleResolution`; each package sets its own (api `NodeNext`, web `ESNext`/`bundler`). - Tests are co-located (`gameService.test.ts` beside `gameService.ts`), use vitest globals, and mock `@lila/db` with `vi.mock` — no test database. Endpoint tests use supertest against the `createApp()` factory without starting a server. - All env config lives in the single root `.env` (see `.env.example`); `packages/db` and the pipeline both resolve it from the repo root.