--- name: 'Wordle — Project Codebase & Docbase Map' type: project-context purpose: orientation altitude: system paradigm: 'SPA + stateless REST API' scope: 'Wordle clone — bilingual (EN/RU) game, solver engine, word bank' status: current created: '2026-08-14' updated: '2026-08-14' sources: - '_bmad-output/planning-artifacts/prds/prd-wordle-2026-07-07/prd.md' - '_bmad-output/planning-artifacts/architecture/architecture-wordle-2026-07-07/ARCHITECTURE-SPINE.md' - '_bmad-output/planning-artifacts/epics.md' - 'raw/01…13 (feature/bug change log)' --- # Project Map — Wordle > **How to use this map:** read §1–§3 to orient, §4 for the codebase, §5 for the > docbase, §6 for rules that are easy to get wrong. This file is regenerated when > the code drifts; it is the single orientation artifact for future changes. --- ## 1. What this project is A **bilingual (English + Russian) Wordle clone** — a learning project built end-to-end with the BMad method. The gameplay is standard Wordle (5 letters, 6 attempts, green/yellow/gray feedback) plus three extras the original lacks: 1. **Computer word ranking** — every target word is pre-solved by 4 algorithms to produce an objective difficulty score (used to rank words and drive skill-based word selection). 2. **Solver replays** — on the results screen the player sees how each solver cracked the word. 3. **A player-facing solver tool** at `/solver` and `/ru/solver` — filter the dictionary by entering guess→feedback rows. The codebase doubles as a **reference implementation** for future BMad projects. **Stack:** TypeScript ^6 · Node ^24 LTS · React ^19 · Vite ^7 · Express ^5. Monorepo (npm workspaces: `client`, `server`, `shared`, `solver`). --- ## 2. Top-level layout | Path | What it is | | --- | --- | | `client/` | React + Vite SPA (screens, components, hooks, i18n) | | `server/` | Express REST API (stateless; EN + RU mounts) | | `shared/` | **Single source of truth** — types + solver algorithms, imported by all packages | | `solver/` | Offline CLI script: validates every target is solvable in ≤6 | | `data/` | Static word banks: `word-bank.json` (EN), `word-bank-ru.json` (RU) | | `raw/` | **Docbase** — numbered feature/bug change log + word-bank generation tooling | | `docs/` | `project_knowledge` (currently only budget screenshots) | | `_bmad-output/` | BMad artifacts: planning (brief/PRD/architecture/epics), implementation, brainstorming | | `design-artifacts/` | WDS scaffolding (empty subdirs, not yet populated) | | `dist/`, `dist-package.json` | Deploy bundle assembly target (built, not edited) | | `.claude/skills/` | Installed BMad skill set | | `_bmad/` | BMad runtime config + `10-feature-wordle-solver.md` (copy of raw/10) | `resume.sh` is a convenience wrapper (`claude --resume `). --- ## 3. Architecture & data flow ``` Browser (React SPA) ── REST ──> Express (stateless) │ localStorage │ └── state persistence ├── data/word-bank*.json (loaded once at startup) ├── in-memory solver cache (lazy + background fork) └── solver-worker (child process, full 4-solver run) ``` **Paradigm (AD-1):** the server holds **no session state**. Every request carries its own context (`wordId`, `tryNo`, `skillMetric`). Deterministic computation replaces stored state where possible (daily word = pure function of the date). **Layer responsibilities:** - **`shared/`** — TypeScript interfaces (API contracts, game types, word-bank shape) **and** the solver algorithms. The `TargetWord`/`WordBank`/`GuessResponse` types are defined once here. No package defines its own copy of a shared type (AD-3, AD-9, AD-10). - **`server/`** — validates guesses, computes feedback, selects words, runs the solver cache. The guessable word list **never leaves the server** (AD-5); only a single-word reveal on loss. - **`client/`** — owns all game state: current attempt, guess history, game-over detection, statistics, skill metric. Persists to `localStorage` (AD-7). Renders colors the server sends. - **`solver/`** — offline CLI, **not part of the runtime** (AD-4 note). **Feedback scoring rule (AD-6, AD-11):** greens first (exact position), then yellows allocated up to remaining letter count, excess instances gray. Wire format is a 5-element array of single-char `'g'` / `'y'` / `'x'`. --- ## 4. Codebase map ### 4.1 `shared/src/` — the contract layer | File | Contents | | --- | --- | | `index.ts` | Re-exports `api`, `game`, `word-bank`, `solvers/index` | | `api.ts` | All HTTP request/response interfaces (`DailyResponse`, `GuessRequest/Response`, `PlayAgain*`, `ValidateResponse`, `SolverRequest/Response`, `ApiError`) | | `game.ts` | `GuessEntry`, `GameStats`, `PersistedState`, `defaultPersistedState()` | | `word-bank.ts` | `WordBank`, `TargetWord { word, hint? }`, `SolverReplay { solver, steps }` | | `solvers/index.ts` | `CachedResult`, `computeReplays` (3 fast solvers), `computeReplaysFull` (4 incl. entropy); difficulty = average attempts, **failed ⇒ counts as 10**, rounded to 2 dp | | `solvers/feedback.ts` | `getFeedback()` (returns `'ggggg'` string) + `matchesFeedback()` | | `solvers/entropy.ts` | Entropy solver with base-3 feedback encoding + cached best-first-guess | | `solvers/frequency.ts` | Letter-frequency solver | | `solvers/vowel-first.ts` | Vowel-first solver (handles Cyrillic vowels too) | | `solvers/greedy.ts` | Greedy solver with hardcoded starters `crane/slate/arise/stare` | **Key type facts:** - `wordId` is the **0-indexed array index** into `targets` (raw/08 removed the `id` field). - `TargetWord` has **no `difficulty`** — difficulty and replays are **computed at runtime** and cached (raw/01, raw/02 refactors). `hint` (synonym) is optional. - `PersistedState.currentGame` holds `wordId, guesses, hint?, revealedWord?, outcome?`. ### 4.2 `server/src/` — the REST API | File | Role | | --- | --- | | `index.ts` | App entry. Loads EN + RU banks, creates one cache per language, mounts routers at `/api/*` and `/ru/api/*`, serves SPA static + SPA fallback | | `feedback.ts` | `computeFeedback(target, guess)` → `string[]` of `'g'/'y'/'x'` (two-pass) | | `validate.ts` | `isValidGuess()` (length 5 + in `guessable`), `getTargetWord()` (index lookup) | | `daily-word.ts` | `getDailyWordId(count, now)` = `daysSinceEpoch % count` (0-indexed). Timezone America/New_York, epoch 2026-01-01. Uses `Intl.DateTimeFormat('en-CA')` for tz-safe dates | | `play-again.ts` | `selectPlayAgainWord()` — Gaussian-like weighting `1/(1+(diff-skill)^2)`, uncached words default difficulty 3.0 | | `solver-pool.ts` | `runSolverInWorker()` — forks a child process with `tsx --import` (must be a child process, not worker_threads, so tsx can load TS) | | `solver-worker.ts` | Child process: on message, runs `computeReplays` or `computeReplaysFull` | | `routes/daily.ts` | `GET /daily` → `{ wordId }`, pre-warms cache + background full compute | | `routes/guess.ts` | `POST /guess` → validates, computes feedback, reveals word on loss, adds hint on 5th guess, returns `difficulty`+`replays` on final guess | | `routes/play-again.ts` | `POST /play-again` → `{ wordId }` (weighted selection) | | `routes/validate.ts` | `GET /validate/:word` → `{ word, valid }` | | `routes/solver.ts` | `POST /solver` → filters dictionary from `{ rows: [{guess,result}] }`, returns candidates + suggested guess + best letters + optional hint on empty board | | `__tests__/` | **Empty** — no server tests exist yet | **Solver cache behavior (important):** cache starts empty. On first hit for a word it computes with **3 fast solvers (~20 ms)** synchronously, then fires the **full 4-solver run (~14 s, includes entropy) in a forked child** and upgrades the cache entry when done. `GET /daily` and `POST /play-again` pre-warm the cache the same way. ### 4.3 `client/src/` — the SPA | File | Role | | --- | --- | | `main.tsx` | React root (`StrictMode`) | | `App.tsx` | Top-level state machine: routes `/solver`, `/ru/solver` → `ScreenSolver`; else `rules`/`game`/`results` screens. Owns init-from-persistence, stat computation, skill-metric running average | | `api.ts` | Fetch wrappers for `/daily`, `/guess`, `/play-again`, `/validate`, `/solver` | | `hooks/useGame.ts` | Guess state, `submitGuess`, `deriveStatus` (won/lost/playing) | | `hooks/usePersistence.ts` | Single-owner localStorage read/write; key = `wordle-state-{lang}` | | `messages/index.ts` | `Messages` interface, `detectLang()` (URL `/ru` → ru), `getMessages()`, `getApiBase()` (`/ru/api` vs `/api`) | | `messages/en.ts` / `messages/ru.ts` | All UI strings + per-language keyboard layouts (ru has Cyrillic layout) | | `components/Grid.tsx` | 6×5 guess grid | | `components/Keyboard.tsx` | QWERTY/Cyrillic keyboard + nav row (Enter/Left/Right/Backspace); reused by solver | | `components/LangSwitch.tsx` | EN↔RU toggle | | `components/SolverBoard.tsx` | Editable 6×5 board with cursor + color cycling (solver screen) | | `screens/Screen1Rules.tsx` | Rules (first visit) | | `screens/Screen2Game.tsx` | Game play surface | | `screens/Screen3Results.tsx` | Results + stats + solver replays + Play Again | | `screens/ScreenSolver.tsx` | Solver tool | **State/persistence facts:** - `localStorage` key is **`wordle-state-en`** / **`wordle-state-ru`** (NOT `wordle-state` as the architecture spine says — language-suffixed since RU was added). - `detectLang()` reads `window.location.pathname`; language is derived from URL, not a toggle state that persists independently. - Skill metric is a client-side running average of difficulty, starts at 3.0, sent to `/play-again`. ### 4.4 `solver/` & `data/` - `solver/src/index.ts` — `npm run solve`: runs all 4 solvers against every target in both banks and reports solvability (a word unsolvable by **all** algorithms is flagged). It **validates**, it does **not** write `data/` (difficulty is runtime-computed now). - `data/word-bank.json` — EN: **5965** guessable, **994** targets `{word, hint}`. - `data/word-bank-ru.json` — RU: **2866** guessable, **992** targets `{word, hint}`. - `raw/` holds the generation scripts and frequency source lists (`generate-banks*.py`, `add-synonyms.py`, `eng5letter.py`, `noun5letter.py`, `*.txt` frequency lists, etc.). --- ## 5. Docbase map ### 5.1 Planning artifacts (`_bmad-output/planning-artifacts/`) | Path | Contents | | --- | --- | | `briefs/brief-wordle-2026-07-07/brief.md` | Product brief (v1 scope, "computer ranking + guessing" as differentiators) | | `briefs/…/addendum.md` | Parked post-v1 features (tiered by solver dependency) + rejected ideas | | `prds/prd-wordle-2026-07-07/prd.md` | v1 PRD: FR-1…FR-19, user journeys UJ-1…5, glossary, non-goals | | `architecture/…/ARCHITECTURE-SPINE.md` | Architecture decisions AD-1…AD-12 (⚠ partly stale vs code — see §6) | | `epics.md` | Epic 1 (play the game) + Epic 2 (solver engine), stories 1.1–1.3, 2.1 | | `implementation-readiness-report-2026-07-07.md` | Pre-impl readiness check | ### 5.2 Implementation artifacts (`_bmad-output/implementation-artifacts/`) | File | Story | | --- | --- | | `1-1-project-foundation-daily-word-setup.md` | Foundation + daily word | | `1-2-guess-validation-feedback.md` | Guess validation + feedback | | `1-3-play-again-skill-tracking.md` | Play Again + skill tracking | | `2-1-solver-script-word-ranking.md` | Solver script + ranking | | `sprint-status.yaml` | Status: epic-2 done; epic-1 + stories 1-1/1-2/1-3 in `review` | ### 5.3 Change log (`raw/` — numbered, chronological) This is the **living docbase of post-v1 work**. Read these before touching a related area. | # | Type | Subject | | --- | --- | --- | | 01 | Refactor | Move `difficulty`/`replays` out of the JSON → compute at runtime ✅ | | 02 | Refactor | Compute difficulty/replays lazily from cache + background thread (not at startup) ✅ | | 03 | Feature | Rebuild dictionaries from frequency-sorted lists; ~1000 targets via Gaussian pick ✅ | | 04 | Feature | Editable guess row: blinking cursor, nav row (Enter/Left/Right/Backspace), Enter disabled on invalid ✅ | | 05 | Feature | Tap a letter in current guess moves cursor to it ✅ | | 06 | Feature | Synonym hint on 5th unsuccessful guess (`hint` key) ✅ | | 07 | Feature | Extend targets to first 1000 words + add hints ✅ | | 08 | Refactor | Remove `id` key from targets; use array index ✅ | | 09 | Bug | Histogram green bars show empty-or-full instead of proportional ⚠ | | 10 | Feature | Wordle Solver at `/solver` + `/ru/solver` (server-side filtering) ✅ | | 11 | Feature | **Daily-word replay**: restore same-day game / show results instead of blank board (in flight) | | 12 | Bug | `currentGame.wordId` not saved until first guess (in flight) | | 13 | Feature | Compact EN/RU language switch position (in flight) | `_bmad/10-feature-wordle-solver.md` is a duplicate copy of `raw/10`. ### 5.4 Other docbase - `docs/` (`project_knowledge`): only `budget *.png` screenshots currently. - `design-artifacts/` (WDS): empty `A-Product-Brief`…`E-Development` folders — scaffolding only. - `_bmad-output/brainstorming/`: initial brainstorm (html, intent, task-list). --- ## 6. Rules & gotchas (easy to get wrong) ### 6.1 Architecture spine is reconciled with code `ARCHITECTURE-SPINE.md` was updated 2026-08-14 to match the implementation, so docs and code are back in sync. Non-obvious facts that are easy to get wrong (now documented in the spine as AD-2/AD-4/AD-9/AD-12/AD-13/AD-14): - Daily word ID is **0-indexed** (`days % count`), used directly as `targets[i]` — no `+1`. - Word-bank targets are `{ word, hint? }` — no `id`, no `difficulty`; the array index is the id, and difficulty is runtime-computed + cached. - Solvers live in `shared/src/solvers/` (entropy, frequency, vowel-first, greedy — no minimax); `solver/` is an offline validation CLI. - The app is bilingual EN/RU; `localStorage` key is `wordle-state-{lang}`. ### 6.2 Hard rules that still hold - **Stateless server** (AD-1): no server-side session/game tracking. - **Shared types are single source of truth** (AD-3/AD-9/AD-10): define API types in `shared/src/api.ts` before implementing an endpoint. - **Word bank never leaves the server** (AD-5): validate server-side; only reveal the word on loss. The solver endpoint returns *filtered candidates* (this is a deliberate post-v1 exception — capped at 200). - **Feedback is server-computed** (AD-6): client only renders `'g'/'y'/'x'`. There are **two** feedback implementations that must stay in sync: `server/src/feedback.ts` (array) and `shared/src/solvers/feedback.ts` (string + `matchesFeedback`). - **Client owns game state** (AD-7): attempt, history, stats, skill all client-side. ### 6.3 Conventions - Naming: camelCase functions/vars, PascalCase types/components, kebab-case files. - Import style: ES modules with explicit `.js` extensions (`import { x } from './foo.js'`), even for TypeScript. Package-boundary imports use `@wordle/shared`. - Errors: `{ error: string }` with 400 (bad input) / 404 (unknown wordId). - No router library — screen switching is React state + a pathname check in `App.tsx`. - Deploy rewrites `@wordle/shared` imports to relative paths via `sed` in the bundle step (see root `package.json` → `build:bundle`). ### 6.4 Build / run / deploy | Command | Effect | | --- | --- | | `npm run dev` / `npm start` | Run server (`node --import tsx server/src/index.ts`), port 3000 | | `npm run build` | `build:client` (vite) + `build:bundle` (assemble `dist/` for deploy) | | `npm run solve` | Run solver validation CLI against both banks | | `npm run deploy` | Build + `rsync dist/ → helg.com:/var/www/wordle/` | | per-package `npm run typecheck` | `tsc --noEmit` | --- ## 7. Current in-flight work (uncommitted, as of 2026-08-14) Git `status` shows these modified/untracked — they correspond to `raw/11`, `raw/12`, `raw/13`: - **Modified:** `shared/src/game.ts`, `client/src/App.tsx`, `client/src/hooks/useGame.ts`, `client/src/screens/Screen3Results.tsx` — daily-word replay + store-wordId-before-first-guess (raw/11 + raw/12). - **Modified:** `data/word-bank-ru.json` — RU word bank edits (hints / plurals). - **Untracked:** `raw/11-feature-daily-word-replay.md`, `raw/12-bug-store-current-word-before-first-guess.md`, `raw/13-ui-language-switch-compact.txt`. - Also modified: `.claude/settings.local.json`, `.gitignore` (tooling). Recent commit `3ef3d49` "fix: replace plurals in word-bank targets and add hints" is the last landed change; the daily-replay work sits on top of it. --- ## 8. Notable design decisions / open threads - **Difficulty model:** average of solver attempt counts; a solver that fails (>6) counts as 10 attempts. This means difficulty ∈ roughly [1, 10], not [1, 6]. - **Play Again** has no repeat-word prevention (AD-8 — target set is large enough). - **Solver endpoint is the only place the dictionary is partially exposed** (candidate list), deliberately, to keep the SPA lean (raw/10). - **`server/src/__tests__/` is empty** — no automated tests exist yet despite the testing skill set being installed. Future test work starts from scratch.