## 🐾 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.