5.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project status
The repo currently contains only wordsearch-specs.md — the implementation has not been written yet. The spec is the source of truth; read it before any change. When implementing, follow the directory layout, routes, and behaviours it prescribes rather than improvising.
Local development
Iterate on the Mac with a venv + uvicorn --reload; only use Docker when verifying the deploy. The Docker bind-mount (./themes) points at the same directory, so local and container runs share theme state.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
--reload restarts on .py changes; Jinja2 templates reload without a restart. .venv/ must be in .gitignore.
Dependency management
Use requirements.txt with pinned versions. Do not introduce Poetry, uv, Pipenv, or pyproject.toml. This is a homelab app — plain pip works identically in the venv and inside the Docker container with no extra tooling.
Initial pin set (don't drop any of these without checking why they're here):
fastapi==0.115.0
uvicorn[standard]==0.32.0
jinja2==3.1.4
reportlab==4.2.5
python-multipart==0.0.12
python-multipart is required for the form-encoded POST /generate and theme-editor submits — easy to omit until the form 500s.
What not to add
No test framework, no linter, no formatter, no build step is specified. Don't introduce one without asking. The project is deliberately minimal: single language, single process, no build pipeline, no DB.
Docker
Production runs the same code in a container — docker compose up mounts ./themes as a volume. The container has no venv (the container is the isolation); the Dockerfile pip installs into the system Python. Don't try to share a venv between host and container.
Architecture
Two-form word model (central design idea)
Every input word produces two strings, and they are not interchangeable:
- Display form — original string, untouched. Goes on the PDF word list.
- Grid form — stripped of leading prefix tokens, uppercased, whitespace/punctuation removed. Goes in the grid and is what length filters and placement operate on.
Prefix stripping (Mr, Mrs, Miss, Dr, Sir, Captain, Saint, etc. — full list in app/normaliser.py::PREFIXES) is token-based, case-insensitive, with optional trailing dot. "Misty" does NOT match "Miss". Multiple consecutive prefixes strip ("Mr Dr Strange" → STRANGE). A word that is only a prefix ("Mr") keeps as MR and logs a stderr warning. Empty grid form after stripping → skip and warn.
The PDF word list shows GRIDFORM (original prefix tokens) only when something was stripped, e.g. TICKLE (Mr). Bare GRIDFORM otherwise.
Direction set is computed, not picked
Don't expose every direction as an independent toggle. Horizontal (→) and vertical (↓) are always on. The two user toggles compose:
base = { →, ↓ }
if diagonal: base |= { ↘, ↗ }
if reversed: base |= { reverse(d) for d in base }
So reversed without diagonal gives → ↓ ← ↑ (no diagonals reversed because none were in base). diagonal + reversed gives all 8.
Placement: retry-then-skip, not backtrack
Per word: random direction + random start, validate collision (empty cells, or matching letters when overlap is on), commit or retry up to 200 times. Skip after 200 and warn — never block, never backtrack already-placed words. If fewer words placed than requested, generate the puzzle anyway and surface a warning to the result page.
No sticky state
All optional toggles (diagonal, reversed, overlap) default off on every / page load. Don't add cookies, localStorage, or session state to remember the last config. Each puzzle is configured explicitly.
Storage
Flat JSON files under themes/<slug>.json with { name, words: [...] }. No DB. The slug is the filename and is locked after creation. themes/ is the only persistence — generated PDFs are streamed straight to the browser as <slug>_<YYYY-MM-DD_HH-MM-SS>.pdf and never written to disk on the server.
Server-rendered, almost no JS
Jinja2 templates only. The single piece of vanilla JS lives in the theme editor: a debounced fetch to POST /api/normalise to render the live preview of grid forms. No bundler, no framework, no build artifacts in the repo.
Auth
None by design (homelab use). If exposing publicly, add HTTP basic auth as FastAPI middleware — don't introduce a user model.
Conventions worth preserving
- Validation errors (invalid slug, empty word list,
max_length < min_length) must render a clear message, never a stack trace. max_lengthis clamped togrid_sizebefore filtering.- All grid letters are uppercase A–Z, including the random fill.
- The PDF is plain: no footer, no timestamp, no branding, no toggle state printed. The filename carries the timestamp.
- v1 ships puzzle-only — no answer key PDF.