This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
BrainTone («Тонус») is a mobile-first Next.js web app for short adaptive cognitive tasks. It is spec-driven: the full product, science, and implementation contract lives in docs/ and is the source of truth, not the code. Your job is to implement the spec exactly, not to design new behavior.
AGENTS.md(Russian) is the primary agent brief — read it. It defines the hard rules below.docs/holds the spec. Read in order:01-product→02-science→03-architecture→04-games→05-design→06-roadmap→07-acceptance→08-social.docs/DECISIONS.mdlogs every place the code resolved a spec gap (withTODO(spec)markers). When the spec is ambiguous or silent, pick the conservative option, record it there, and proceed — do not invent mechanics or numbers.
npm run dev # Next dev server (http://127.0.0.1:3030)
npm run build # production build — must pass with zero type warnings
npm run lint # eslint . --max-warnings=0
npm run typecheck # tsc --noEmit
npm run test # vitest run (unit)
npm run test:coverage # vitest with coverage; engine/ thresholds enforced (see vitest.config.ts)
npm run test:watch # vitest watch
npm run e2e # playwright (chromium-mobile / Pixel 5); auto-starts dev server
npm run format # prettier --write (printWidth 100)Run a single test file: npx vitest run src/engine/staircase.test.ts
Run a single test by name: npx vitest run -t "name fragment"
"Done" means: npm run build clean, all tests green, and the current stage's DoD in docs/07-acceptance.md satisfied.
- Never change "brain benefit" wording. UI claims about cognition come only from
docs/01-product.md(раздел «Словарь формулировок») anddocs/02-science.md. Forbidden: «повысит интеллект», «сделает умнее», «предотвратит деменцию», etc. - All game numbers are fixed in
docs/04-games.md— timings, level tables, scoring formulas. Do not alter them. - Measurement accuracy over polish. Reaction time =
performance.now() - stimulusShownAt, wherestimulusShownAtis captured inside a double-rAFcallback. Stimulus exposure runs on arAFloop comparing against a deadline (setTimeoutonly as fallback). During a trial: no foreign re-renders, no network, no localStorage writes (persist between trials / at session end). Seesrc/engine/timing.tsanddocs/03-architecture.md§6. - Stage discipline. Don't start a later roadmap stage before the current stage's DoD passes (
docs/06-roadmap.md,docs/07-acceptance.md). - Each game is an isolated module implementing
GameModule. The engine must never know about a specific game (nogameId === "n-back"branching in runtime — use theGameModulehooks instead).
- Code, identifiers, and commit messages in English; UI text in Russian, centralized in
src/content/strings.ts(no hardcoded UI strings in components). Science copy lives insrc/content/science.ts. - TypeScript strict +
noUncheckedIndexedAccess.anyis an ESLint error — useunknown+ narrowing. Unused vars must be_-prefixed. - Path alias
@/*→src/*. - Conventional Commits (
feat:,fix:,test:,docs:,chore:), one logical change per commit. - Dependencies are restricted to the table in
docs/03-architecture.md§1 (Next 15 / React 19 / Tailwind 3.4 / Zustand 5 / Zod 3 / date-fns).seedrandomis forbidden — the PRNG is hand-written. Any new dependency requires aDECISIONS.mdentry.
Single Next.js App Router app (Vercel target). Stages 0–2 are local-first (no backend); stage 3 adds Route Handlers under src/app/api/*, Neon/Drizzle, and Auth.js. DB access only ever happens in Route Handlers / server actions, never in client components. Games are client components.
The core abstraction is a clean split: a game-agnostic engine/, per-game games/ modules, and a GameSession runtime that wires them together.
GameModule<TTrial, TAnswer>— every game implements this:generateTrial(level, rng)(pure, deterministic), pureevaluate(...), and aBoardReact component for presentation. Optional hooksgetTrialRecords/getNextLevellet block-style games (n-back) expand one "trial" into manyTrialRecords and override the staircase — this is how the engine stays game-agnostic.staircase.ts— single weighted up-down adaptive staircase for all games (3-up / 1-down with buffer; calibration mode is 2-up / 1-down).createStaircase(opts) → { level(), register(outcome) }.scoring.ts— pure scoring: session score 0..1000 (levelComponent + accuracyComponent), domain index (EMA), overall Тонус-индекс.rng.ts— hand-written mulberry32 PRNG + FNV-1ahashStringToSeed. Session seed =hash(profileId:gameId:startedAt), stored inSessionResultfor reproducibility. All generators must be pure(level, rng) => trial.scheduler.ts—buildDailyTrain(...)deterministically picks 4 games across 4 domains (weakest domain always included).session.ts—buildSessionResult(...)assembles the finalSessionResult(accuracy/median RT over non-warmup trials, peak level, score).timing.ts—rAFtiming utilities and thevisibilitychangepause listener.
Layout per game: index.ts (exports the GameModule), generator.ts (pure trial generation + evaluate), Board.tsx (presentation; emits answer + rtMs), params.ts (level tables from docs/04-games.md), *.test.ts (deterministic-by-seed tests). Implemented: reaction-field, stroop, n-back, switcher, patterns. Stubs (.gitkeep): memory-matrix, go-nogo, associations. catalog.ts lists playable games with their domain + UI copy.
Generic component that takes a GameModule and runs the full loop: rules → countdown → trial → feedback → intertrial → … → result. It owns the Rng, staircase, and TrialRecord accumulation via refs (to avoid re-renders during a trial), routes answers through module.evaluate and the staircase (or the getNextLevel/getTrialRecords hooks), handles warmup trials, pause-on-tab-hidden, and finally calls buildSessionResult + onComplete. PlayableGameSession.tsx wires a catalog game into it for /play/[gameId]. Transient UI state lives in Zustand stores (src/stores/gameRuntime.ts, trainSession.ts); persistent data goes through the repository.
repository.ts defines ProfileRepository; local-repository.ts is the localStorage implementation (key braintone:v1, whole state as one Zod-validated JSON blob; invalid data → onInvalidData callback or reset). Stage 3 adds an api-repository.ts that keeps local as an offline-first cache. All persisted shapes are Zod schemas in schemas.ts. Persistent vs. runtime state is a deliberate boundary — Zustand is runtime only.