Files
mnemosyne/CLAUDE.md
2026-05-22 21:17:01 -04:00

188 lines
8.7 KiB
Markdown
Raw 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.
## 🐾 Red Panda Approval™
The standard every change is judged against. Don't satisfy a checklist —
satisfy the red pandas. Ask of each change: *does this earn approval?*
1. **Fresh Migration Test** — migrations apply cleanly from an empty database.
2. **Elegant Simplicity** — no unnecessary complexity; the obvious solution, done well.
3. **Observable & Debuggable** — proper logging; failures say what broke and why.
4. **Consistent Patterns** — follows Django conventions and the patterns already in this repo.
5. **Actually Works** — passes all checks *and* serves a real user need.
Criteria 1 and 5 are **externally verifiable** — migrations apply or they
don't; checks pass or they don't. Verify them, don't assert them. Criteria
24 are judgement calls: when in doubt, match what the repo already does
rather than grading your own elegance.
> If a paw print isn't leading the response, the rest of this file probably
> isn't being honoured either. Lead with one. 🐾
---
## Conventions (always-on)
These are the rubric made concrete for the common case — writing models,
views, forms, templates, and queries.
### Models
- Names: singular PascalCase (`User`, `BlogPost`, `OrderItem`).
- Every model defines `__str__` and `get_absolute_url`.
- Every model has `created_at = DateTimeField(auto_now_add=True)` and
`updated_at = DateTimeField(auto_now=True)`.
- `TextChoices` for status fields.
- `related_name` on every `ForeignKey`; plural snake_case with correct
English pluralisation.
- Public-facing models: consider `UUIDField` primary key and
`is_active` for soft deletes.
### Field naming
- Foreign keys: singular, no `_id` suffix (`author`, `category`, `parent`).
- Booleans: prefixed (`is_active`, `has_permission`, `can_edit`).
- Dates: suffixed (`created_at`, `updated_at`, `published_on`).
- No abbreviations (`description`, not `desc`).
### Views
- **Function-based views exclusively.** Explicit logic over implicit
inheritance. Extract shared logic into utility functions.
- Business logic lives in service functions, not views and not `save()`.
### Forms
- `ModelForm` with an explicit `fields` list — never `__all__`, never `exclude`.
- Validate at the boundary; never trust client-side validation alone.
### Queries
- `select_related()` for FKs; `prefetch_related()` for reverse and M2M.
- No queries inside loops (N+1). No `.all()` when you need a subset.
- `.only()` / `.defer()` for large models. Comment non-obvious querysets.
### URLs & identifiers
- Public URLs use 12-char short UUIDs via `shortuuid`. Never expose
sequential IDs (enumeration risk). Internal refs may use PKs.
- Resource-based, namespaced URL names per app, trailing slashes, flat
structure preferred.
### Docstrings
- **Google style.** Document public classes, functions, methods, modules.
- Imperative one-line summary. `Args:`/`Returns:`/`Raises:` only when the
signature doesn't already convey it. Don't restate type hints in prose.
- Skip obvious one-liners and standard Django overrides.
### Code organisation
- PEP 8 import ordering (stdlib, third-party, local). Type hints on params.
- CSS and JS in external files only — no inline styles, `<style>`,
inline handlers, or `<script>` blocks.
- File length: split by domain concept past ~500 lines; hard ceiling 1000.
### Testing
- Django `TestCase` (not pytest). Separate files per module:
`test_models.py`, `test_views.py`, `test_forms.py`.
An app isn't done until it's reachable
django-admin startapp builds an island. A complete-from-its-own-boundary
app — models, views, urls, templates, tests all present and passing — is
# Add to always-on Django CLAUDE.md — Conventions section
Insert this block under "Conventions (always-on)", as its own subsection.
It is the universal Django definition-of-done. It fires for *every* app,
not just registered tools.
### An app isn't done until it's reachable
`django-admin startapp` builds an **island**. A complete-from-its-own-boundary
app — models, views, urls, templates, tests all present and passing — is
still *unfinished* if nothing in the running site links to it. "It works in
isolation" is not done; **"a user can reach it from the running site" is done.**
Before reporting a new app complete, wire it into the site:
1. **`INSTALLED_APPS`** — add the app's config.
2. **Root URLconf**`include()` the app's `urls.py` in `config/urls.py`.
An app whose URLconf isn't included has unreachable views, full stop.
3. **Navigation / discovery** — register the app so it surfaces wherever
this project expects apps to appear. This project uses an **app
registry** (see Project Setup): the app registers itself in its own
`apps.py.ready()` and the navigation template tag picks it up. Do **not**
hand-edit nav templates or central list views — they read from the
registry.
4. **Verify reachability** — confirm the app's main page actually loads
from the running site (not just that its tests pass). Per Red Panda
criterion 5, this is externally verifiable: load the page, don't assert
it works.
Why this rule exists: an LLM reasons locally and closes the visible task at
the app's own boundary. The wiring that makes an app reachable lives in
*other* files (`config/urls.py`, `INSTALLED_APPS`, the registry) with no
signal inside the new app pointing to them. Without this rule, the
near-certain result is a fully-built, completely inaccessible app. The
registry exists precisely so that "surface it" happens *inside* the app's
own boundary (a `register()` call in `ready()`) — collapsing the wiring
into the one place local reasoning will actually look.
> The same principle generalises beyond Django: a new route that isn't
> mounted, a CLI subcommand not added to the dispatcher, a handler not
> registered — all the same failure. Done means *connected*, not *written*.
---
## Always-on anti-patterns
The cross-cutting tripwires worth carrying everywhere. File-specific
landmines (nginx, compose, broker) are in path-scoped rules.
- **Models:** no `.get()` without handling `DoesNotExist`; no `null=True`
on `CharField`/`TextField` (use `blank=True, default=""`); always specify
`on_delete`; don't override `save()` for business logic; no
`Meta.ordering` on large tables.
- **Security:** secrets via env vars, never in `settings.py`; never commit
`.env`; never `DEBUG=True` in production; never `mark_safe()` on
user-supplied content; never disable CSRF.
- **Templates:** `{% url %}` not `{{ variable }}` for URLs; no logic in
templates; `{% csrf_token %}` in every form.
- **Imports/style:** no `import *`; no mutable default args; no bare
`except:`; don't silence linter warnings without a documented reason.
---
## Environment
- Virtual environment: `~/env/PROJECT/bin/activate` (replace PROJECT).
- `pyproject.toml` for config — no `setup.py`, no `requirements.txt`.
- Dependencies floor-pinned with ceiling (`Django>=5.2,<6.0`). Exact `==`
pins only in application lock files, never in reusable packages.
- Dev DB: SQLite. Production DB: PostgreSQL.
---
## Path-scoped rules to create (`.claude/rules/`)
These hold the landmines extracted from the standards doc. Each loads only
when its `paths` match, keeping this file lean. Frontmatter shown.
- **`nginx.md`** — `paths: ["nginx/**", "**/*.conf"]` — reverse-proxy
reference config: Docker DNS resolver + variable `proxy_pass`,
`$proxy_x_forwarded_proto` map, access-log filtering, RFC1918 allowlists
(all four ranges), `always` security headers.
- **`docker-compose.md`** — `paths: ["docker-compose*.y*ml", ".env*"]`
per-service `environment:` scoping (no shared `env_file:`), `${VAR}`
interpolation, `.env.example` annotation convention, the `repr()` parse
diagnostic.
- **`celery-tasks.md`** — `paths: ["**/tasks.py"]` — idempotency, retry
logic, pass IDs not instances, synchronous-by-default, broker URL
percent-encoding, progress pattern `{app}:task:{task_id}:progress`.
- **`migrations.md`** — `paths: ["**/migrations/**"]` — never edit deployed
migrations; `RunPython` needs a reverse; no non-nullable field without a
default; meaningful `--name`; test forward and backward.
- **`memcached.md`** — `paths: ["**/settings.py", ".env*"]` — bind
`0.0.0.0` not localhost; container can't reach `127.0.0.1`; LAN hostname
in `KVDB_LOCATION`; key pattern `{app}:{model}:{identifier}:{field}`.
- **`frontend.md`** — `paths: ["**/templates/**", "**/static/**"]` — DaisyUI+
Tailwind for new projects / Bootstrap 5 for existing; extend
`themis/base.html`; no inline styles or scripts.
## Reference docs (consult on demand, don't inline)
- `docs/` gotcha writeups: broker-URL/Kombu parsing, env-file parsing
differences, nginx IP-caching. State the rule in the rule file; link the
*why* here.
- Preferred-packages list and per-app architecture: keep in `docs/`, not in
this always-on file.