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

98 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
```bash
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 install`s 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.