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:
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.
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:
/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).
| 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 <session-id>).
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'.
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?.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.
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./play-again.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.)._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 |
_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 |
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.
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).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):
days % count), used directly as targets[i] — no +1.{ word, hint? } — no id, no difficulty; the array index is the id, and difficulty is runtime-computed + cached.shared/src/solvers/ (entropy, frequency, vowel-first, greedy — no minimax); solver/ is an offline validation CLI.localStorage key is wordle-state-{lang}.shared/src/api.ts before implementing an endpoint.'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)..js extensions (import { x } from './foo.js'),
even for TypeScript. Package-boundary imports use @wordle/shared.{ error: string } with 400 (bad input) / 404 (unknown wordId).App.tsx.@wordle/shared imports to relative paths via sed in the bundle step
(see root package.json → build:bundle).| 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 |
Git status shows these modified/untracked — they correspond to raw/11, raw/12, raw/13:
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).data/word-bank-ru.json — RU word bank edits (hints / plurals).raw/11-feature-daily-word-replay.md, raw/12-bug-store-current-word-before-first-guess.md,
raw/13-ui-language-switch-compact.txt..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.
server/src/__tests__/ is empty — no automated tests exist yet despite the testing
skill set being installed. Future test work starts from scratch.