Files
wordsearch/CLAUDE.md
2026-05-04 09:45:17 +01:00

5.0 KiB
Raw Permalink Blame History

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_length is clamped to grid_size before filtering.
  • All grid letters are uppercase AZ, 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.