project-context.md 17 KB


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-17' 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…14 (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 <session-id>).


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-15 (reconciled with 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 ✅
12 Bug currentGame.wordId not saved until first guess ✅
13 Feature Compact EN/RU language switch position (switch onto the title row) ✅
14 Bug Fresh guess screen when the daily word changes — clear stale previous-word guesses (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-17)

raw/11–raw/13 were committed 2026-08-14 (585a6f0 language switch, bf6470d daily-word replay + store-wordId-before-first-guess). raw/14 sits on top of them:

  • Modified: client/src/App.tsx — raw/14: call resetForNewGame() in both new-word branches so a changed daily word shows a fresh guess screen (the useGame hook seeds its internal guesses once at mount, so clearing currentGame alone left stale guesses).
  • Untracked: raw/14-new-word-clean.md (spec).
  • Also modified (tooling, left uncommitted): .claude/settings.local.json, .gitignore.

Latest landed commit is bf6470d "feat: restore same-day daily game on return".


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.