98 lines
5.0 KiB
Markdown
98 lines
5.0 KiB
Markdown
# 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 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.
|