# Cendry — Coding Rules

## Project

Cendry is a read-only Firestore ODM for Python >= 3.13, built on `google-cloud-firestore` and `anyio`.

## Commands

```bash
uv run pytest                              # run tests (integration auto-skipped without emulator)
uv run pytest -q                           # run tests (quiet)
uv run coverage run -m pytest && uv run coverage report --show-missing  # coverage (must be 100%)
uv run mypy src/cendry/ tests/             # type check with mypy (strict on src, relaxed on tests)
uv run ty check src/cendry/ tests/         # type check with ty (must also pass)
uv run ruff check src/ tests/              # lint
uv run ruff format src/ tests/             # format
uv run ruff check --fix src/ tests/        # lint + auto-fix
uv run pytest tests/integration/ -v        # integration tests (auto-starts Firestore emulator via testcontainers)
```

## Architecture

- `src/cendry/` layout with `hatchling` build backend
- `Model` and `Map` use a `_MapMeta` metaclass with `@dataclass_transform(kw_only_default=True)` that rewrites `Field[T]` annotations into dataclass fields and installs `FieldDescriptor` instances
- `Field[T]` has an overloaded `__get__` descriptor so type checkers see `FieldDescriptor` on class access and `T` on instance access
- `Cendry` (sync) and `AsyncCendry` share logic via `_BaseCendry` base class
- `Query[T]` / `AsyncQuery[T]` wrap Firestore queries with convenience methods
- Deserialization logic lives in `serialize.py` (standalone functions, not methods)
- Type validation lives in `types.py` (`TypeRegistry` with extensible checkers)
- `Batch` / `AsyncBatch` and `Txn` / `AsyncTxn` share write methods via `WritesMixin` in `_writes.py`
- `metadata.py` tracks `update_time`/`create_time` per instance via `WeakKeyDictionary` (keyed by `id()`)
- Tests use `unittest.mock` to mock the Firestore client — no real Firestore needed
- Integration tests use `testcontainers` to auto-start a Firestore emulator (requires Docker)

## Design Principles

- **Standalone functions over methods on Model** — `from_dict()`, `to_dict()` are standalone, NOT classmethods or instance methods. Keep `Model` lean.
- **No Django-style patterns** — no `Model.objects`, no `Model.select()`, no queryset managers bound to models. The context (`Cendry`) is the entry point for all queries.
- **Thin wrapper over Firestore** — don't abstract away Firestore's API. `FieldFilter` is Firestore's own class re-exported. Query semantics match Firestore (streaming, collection groups, subcollections).
- **Immutable query objects** — `Query.filter()` returns a new `Query`, never mutates.
- **Explicit over implicit** — `collection=` is required, no magic name derivation. Fields are `kw_only`.
- **Copy-pasteable repr** — all `__repr__` must return valid Python expressions that can be pasted back. Use the most natural form: `City.state == 'CA'` over `FieldFilter("state", "==", "CA")`.
- **Minimize imports** — prefer methods on existing objects over standalone classes. `City.population.asc()` over `Asc(City.population)`. `City.state == "CA"` over `FieldFilter("state", "==", "CA")`. Keep the public API importable from `cendry` but don't force users to import things they can access via methods.
- **Explicit type + handler registration** — `register_type(MyType, handler=MyHandler())` is the clean pattern. The handler doesn't know which type it handles — the registry maps types to handlers. Use kwargs (`serialize=`, `deserialize=`) for simple cases. `deserialize` is required; `serialize` is optional (defaults to identity).

## Code Style

- **Python >= 3.13** — use modern syntax: `X | Y` unions, PEP 695 type params (`class Foo[T]:`), no `from __future__ import annotations`
- **Relative imports** inside `src/cendry/` — use `from .module import X`, never `from cendry.module import X`
- **Absolute imports** in `tests/` — use `from cendry import X`
- **ruff** enforces linting and formatting — run `ruff check` and `ruff format` before committing
- **No `pass`** in empty class bodies — leave the body empty after the docstring
- **No `elif` after `return`/`raise`** — use flat `if` chains
- **Exception names** end with `Error` (ruff N818)
- **Line length** 99 chars
- **Don't shadow builtins** — use `doc_id` not `id` as parameter names

## Testing

- **100% coverage** required — `pyproject.toml` has `fail_under = 100`
- Use `pragma: no cover` only for truly unreachable defensive branches
- Tests use `pytest`, `pytest-bdd` for BDD scenarios, and `anyio` for async
- BDD feature files live in `tests/features/`
- Shared fixtures and model definitions live in `tests/conftest.py`
- Extract common test data into constants (e.g. `SF_DATA`) to avoid repetition

### Integration tests

- Live in `tests/integration/` — test against a real Firestore emulator
- Use **testcontainers** to auto-start `google/cloud-sdk:emulators` (requires Docker)
- Session-scoped `firestore_emulator` fixture starts one container per test session
- `clean_collection` fixture cleans up documents after each test
- Run separately: `uv run pytest tests/integration/ -v`
- Not included in coverage (`fail_under = 100` applies to unit tests only)
- Integration tests do **not** auto-run in CI — add a separate CI job if needed

## Type Checking

Both mypy and ty must pass on `src/cendry/` and `tests/`.

- **mypy** — strict on `src/cendry/`, relaxed on `tests/` (untyped defs allowed, metaclass false positives suppressed via `disable_error_code`)
- **ty** — strict everywhere, metaclass false positives suppressed on `tests/` via `[[tool.ty.overrides]]`, `unused-type-ignore-comment` globally ignored (mypy needs `type: ignore` comments that ty doesn't)
- When adding `type: ignore` comments for mypy, include the specific error code (e.g. `type: ignore[arg-type]`)
- `@dataclass_transform` eliminates most `type: ignore` needs — prefer fixing the root cause over adding ignores
- `ClassVar` for class-level attributes that aren't init params (e.g. `__collection__`)

## Errors to Avoid

- **Don't use `from __future__ import annotations`** — breaks runtime type introspection needed by the metaclass and `get_type_hints()`
- **Don't use `TypeVar` when PEP 695 syntax works** — prefer `class Foo[T]:` and `def bar[T]():`
- **Don't add `async def` to methods that just return an async wrapper** — e.g. `AsyncCendry.select()` returns `AsyncQuery`, not an async generator. It's a regular `def`.
- **Don't use `id` as a parameter name** — shadows the builtin. Use `doc_id`.
- **Don't put conversion/serialization logic on the context class** — extract to standalone functions in `serialize.py`
- **Don't add `Model.objects` or similar manager patterns** — the context is the query entry point
- **Don't return bare `Iterator[T]`** from queries — return `Query[T]` for the chainable API
- **Don't forget to run both mypy AND ty** — they catch different things and have different ignore needs

## Documentation

Documentation uses **Zensical** (successor to Material for MkDocs) with **Diátaxis** structure and **mkdocstrings/griffe** for auto-generated API reference. Config is in `mkdocs.yml` (Zensical reads it natively).

```bash
uv run zensical serve                      # local preview at http://127.0.0.1:8000
uv run zensical build --strict             # build static site (must pass with no warnings)
```

### Structure (Diátaxis)

- **`docs/tutorials/`** — learning-oriented, step-by-step. "Follow along to learn."
- **`docs/how-to/`** — task-oriented, practical recipes. "How do I do X?"
- **`docs/reference/`** — information-oriented, every class/method/param. "What does X do?"
- **`docs/explanation/`** — understanding-oriented, design and architecture. "Why does X work this way?"

### Style rules

- **Lead with the value** — open each page with what the reader will accomplish, not background
- **Code first** — show a working example before explaining it
- **Use admonitions** — `!!! tip` for best practices, `!!! warning` for pitfalls, `!!! info` for context, `!!! note` for subtleties. Don't overdo it.
- **Tables for options/parameters** — scannable, not prose
- **Tabbed examples** — use `=== "Tab"` for alternative approaches (pip/uv, sync/async)
- **No walls of text** — if a section is more than 5 lines of prose, break it up with code, tables, or admonitions
- **Link between sections** — tutorials link to how-to guides, how-to guides link to reference, explanation links back to both

### Auto-generated vs hand-written reference

- **Auto-generated** (via mkdocstrings/griffe): `Cendry`, `AsyncCendry`, `Query`, `AsyncQuery`, `TypeRegistry`, `from_dict`, `to_dict`, `Filter`, `And`, `Or`, exceptions. Uses `::: cendry.module.Class` directives.
- **Hand-written**: `Model`, `Map`, `Field[T]`, `FieldDescriptor`, `field()`. The metaclass makes auto-generation misleading — these need narrative explanation.
- **Docstrings**: Google style. Add `Args:`, `Returns:`, `Raises:` sections for public methods — griffe renders them.

### Deployment

- Docs deploy to GitHub Pages via `.github/workflows/docs.yml` on push to main
- Published at `https://johnnoone.github.io/cendry/`

## Releases

- **Version** lives in `pyproject.toml` `[project].version` — single source of truth
- **CHANGELOG.md** — update for every release. Use [Keep a Changelog](https://keepachangelog.com/) format with `Added`, `Changed`, `Fixed`, `Removed` sections.
- **Tagging** — `git tag v0.x.0` after the version bump commit
- **LICENSE** — MIT. The `LICENSE` file must exist at root (PyPI and GitHub require it).
- **CI/CD** — `.github/workflows/ci.yml` runs tests/lint/types on push. `.github/workflows/docs.yml` deploys docs on push to main.

## Git

- **Never add `Co-Authored-By` trailers** — do not mention Claude, Anthropic, or any AI assistant in commits
- **Never commit CLAUDE files** — `CLAUDE.local.md`, `CLAUDE.md`, and `docs/superpowers/` are gitignored and must stay out of the repository

## Public API

Everything is exported from `cendry.__init__` and listed in `__all__`:
`Model`, `Map`, `Field`, `field`, `FieldDescriptor`, `Cendry`, `AsyncCendry`, `Query`, `AsyncQuery`, `FieldFilter`, `And`, `Or`, `Asc`, `Desc`, `CendryError`, `DocumentNotFoundError`, `TypeHandler`, `BaseTypeHandler`, `TypeRegistry`, `register_type`, `from_dict`, `to_dict`
