Skip to content

Latest commit

 

History

History
74 lines (51 loc) · 7.31 KB

File metadata and controls

74 lines (51 loc) · 7.31 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

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-product02-science03-architecture04-games05-design06-roadmap07-acceptance08-social.
  • docs/DECISIONS.md logs every place the code resolved a spec gap (with TODO(spec) markers). When the spec is ambiguous or silent, pick the conservative option, record it there, and proceed — do not invent mechanics or numbers.

Commands

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.

Hard rules (from AGENTS.md — these gate review)

  1. Never change "brain benefit" wording. UI claims about cognition come only from docs/01-product.md (раздел «Словарь формулировок») and docs/02-science.md. Forbidden: «повысит интеллект», «сделает умнее», «предотвратит деменцию», etc.
  2. All game numbers are fixed in docs/04-games.md — timings, level tables, scoring formulas. Do not alter them.
  3. Measurement accuracy over polish. Reaction time = performance.now() - stimulusShownAt, where stimulusShownAt is captured inside a double-rAF callback. Stimulus exposure runs on a rAF loop comparing against a deadline (setTimeout only as fallback). During a trial: no foreign re-renders, no network, no localStorage writes (persist between trials / at session end). See src/engine/timing.ts and docs/03-architecture.md §6.
  4. Stage discipline. Don't start a later roadmap stage before the current stage's DoD passes (docs/06-roadmap.md, docs/07-acceptance.md).
  5. Each game is an isolated module implementing GameModule. The engine must never know about a specific game (no gameId === "n-back" branching in runtime — use the GameModule hooks instead).

Conventions

  • 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 in src/content/science.ts.
  • TypeScript strict + noUncheckedIndexedAccess. any is an ESLint error — use unknown + 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). seedrandom is forbidden — the PRNG is hand-written. Any new dependency requires a DECISIONS.md entry.

Architecture

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.

src/engine/ — game-agnostic core (types.ts is the contract)

  • GameModule<TTrial, TAnswer> — every game implements this: generateTrial(level, rng) (pure, deterministic), pure evaluate(...), and a Board React component for presentation. Optional hooks getTrialRecords / getNextLevel let block-style games (n-back) expand one "trial" into many TrialRecords 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-1a hashStringToSeed. Session seed = hash(profileId:gameId:startedAt), stored in SessionResult for reproducibility. All generators must be pure (level, rng) => trial.
  • scheduler.tsbuildDailyTrain(...) deterministically picks 4 games across 4 domains (weakest domain always included).
  • session.tsbuildSessionResult(...) assembles the final SessionResult (accuracy/median RT over non-warmup trials, peak level, score).
  • timing.tsrAF timing utilities and the visibilitychange pause listener.

src/games/<game>/ — one isolated module per game

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.

src/components/GameSession.tsx — the runtime that drives a session

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.

src/data/ — persistence behind a repository interface

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.