CLAUDE.md added
This commit is contained in:
188
CLAUDE.md
Normal file
188
CLAUDE.md
Normal file
@@ -0,0 +1,188 @@
|
||||
## 🐾 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
|
||||
2–4 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.
|
||||
Reference in New Issue
Block a user