# Colnade Development Rules

## Before Every Commit

You MUST run **all** of the following checks before committing and ensure they all pass:

```bash
uv run ruff check .
uv run ruff format --check .
uv run pytest tests/ -v
uv run ty check tests/typing/ --error-on-warning
uv run python scripts/check_api_docs.py
uv run mkdocs build --strict
```

If any check fails, fix the issue before committing. Do not commit with known failures.

**Important:** Run `ruff check .` and `ruff format --check .` on the entire repo (`.`), not just specific directories. CI runs these against the whole repo.

## Updating Documentation (MANDATORY)

**Any change to a public-facing API MUST include corresponding documentation updates in the same commit.** This is not optional — CI will fail if documentation is missing or inconsistent.

When you add, remove, or rename any public API symbol (anything in `__all__`):

1. **API reference** (auto-generated) — Add/update/remove the `::: module.Symbol` directive in the appropriate `docs/api/*.md` file. Run `uv run python scripts/check_api_docs.py` to verify coverage. **This check runs in CI and will block merging if it fails.**
2. **User guide** (manual) — Update the relevant page in `docs/user-guide/` to document new features or reflect removed ones.
3. **Tutorials** (manual) — Update any affected tutorials in `docs/tutorials/` if they reference changed APIs.
4. **README.md** — Update if the change affects the quick start, feature list, or examples shown to new users.
5. **mkdocs build** — Run `uv run mkdocs build --strict` to verify docs build without warnings.

## Updating the Changelog

When a PR adds user-visible changes (new features, bug fixes, breaking changes, deprecations), add an entry to `CHANGELOG.md` under an `## [Unreleased]` section at the top. Use [Keep a Changelog](https://keepachangelog.com/) categories: Added, Changed, Deprecated, Removed, Fixed, Security.

When cutting a release, rename `[Unreleased]` to the version and date, add a new empty `[Unreleased]` section above it, and add a link reference at the bottom.

## Issue and PR Workflow

Issues are the coordination mechanism for all work — both internal and external contributions.

### Labels

| Label | Meaning |
|-------|---------|
| `triage` | Needs maintainer review — not yet confirmed |
| `approved` | Confirmed worth doing, ready for someone to pick up |
| `good first issue` | Scoped and newcomer-friendly |
| `bug` / `feature` / `documentation` | Type labels (combine with triage/approved) |

### Triage process

- External issues from contributors start with `triage`. Review and either close or move to `approved`.
- Internal issues (filed by maintainer or agent) skip triage — label `approved` immediately.
- The working backlog is `label:approved`. Use this filter to find what to work on next.

### Filing issues

When filing issues, include:
- Clear title describing the desired outcome
- Enough context for someone else to understand and implement
- Type label (`bug`, `feature`, `documentation`)
- `approved` label for internal issues

### PRs

- Always open PRs against `main` — do not commit directly to `main`
- Reference the GitHub issue in the PR description
- Keep PRs focused — one feature or fix per PR

## Architectural Decisions

### Column[DType] Annotation Pattern

Schema column annotations use `Column[DType]` instead of bare dtype annotations:

```python
# Correct — type checker sees Column methods
class Users(Schema):
    id: Column[UInt64]
    name: Column[Utf8]
    age: Column[UInt8 | None]

# WRONG — type checker sees bare dtype, no Column methods visible
class Users(Schema):
    age: UInt8
```

**Rationale:** All Python type checkers (ty, mypy, pyright) read static annotations, not runtime metaclass replacements. With bare `age: UInt8`, the type checker thinks `Users.age` is `UInt8` — a sentinel class with no `.sum()`, `.mean()`, etc. The `Column[DType]` pattern (inspired by SQLAlchemy 2.0's `Mapped[T]`) makes the annotation _be_ the descriptor type.

### Self Narrowing

Column methods are restricted to appropriate dtypes via `self:` type annotations (self-narrowing). This catches misuse like `Users.name.sum()` (string column) at type-check time. Uses explicit union types (not TypeVar bounds) because Column is invariant and nullable columns (`Column[UInt8 | None]`) must be included.

**Remaining limitation:** `.list` property self-narrowing does not work in ty (property self types are not enforced). `.list` returns `ListAccessor[Any]` — `ListAccessor` methods like `.get()`, `.sum()` return `ListOp[Any]`. The annotations are in place and will become precise when ty adds property self-narrowing support.
