# Cairn — Conventions

## Code Style

- **Type hints everywhere** — `from __future__ import annotations` at top of most files
- **Pydantic v2** — `BaseModel`, `Field`, `field_validator`, `model_validator`, `ConfigDict`
- **No ORM** — raw SQL via `sqlite3` module, `conn.execute()` with parameterized queries
- **Named access** — `conn.row_factory = sqlite3.Row`, then `row["column_name"]`

## Naming

| Pattern | Convention | Example |
|---------|-----------|---------|
| Router functions | snake_case verb_noun | `create_project`, `heartbeat_project_reason` |
| Pydantic models | PascalCase | `CreateProjectRequest`, `ProjectDetail` |
| Config types | PascalCase + Config suffix | `DispatchConfig`, `WorkerConfig`, `RuntimeConfig` |
| DB tables | snake_case plural | `projects`, `intent_sources`, `scoped_counters` |
| Task types | lowercase literal | `"reason"`, `"explore"`, `"bootstrap"` |
| Worker types | lowercase literal | `"claudecode"`, `"codex"`, `"pi"`, `"mock"` |

## Error Handling

- **HTTP errors** — `HTTPException(status_code, detail)` in routers
- **Validation** — Pydantic `@field_validator` with `ValueError`; `@model_validator` for cross-field
- **DB errors** — No custom exception classes; let SQLite errors propagate to FastAPI's default handler
- **Worker errors** — Outcome strings (`"unhealthy"`, `"rejected"`, `"invalid_json"`, `"command_fail"`) returned to scheduler, never exceptions

## Patterns

### Lease-based claiming
Intents and reason slots use heartbeat-based leases. Worker must send heartbeat within `intent_timeout`/`reason_timeout` seconds or lease expires. `expire_workers()` and `expire_reason_leases()` called on every read.

### Stigmergy coordination
Workers never communicate directly. All coordination through the shared graph. Read graph → decide → write result. No RPC between workers.

### Structured output parsing
Workers return JSON. `contracts.py` validates payload structure. Three outcome paths: `complete`, `intents`, `noop` (for reason); `fact`, `rejected` (for explore/bootstrap).

### Config-driven workers
Worker behavior determined entirely by `dispatch.yaml` config: type, env vars, task types, max_running, priority. Mock workers use `MOCK_*` env vars for probabilistic outcome control.

## Anti-patterns found

- No TODO/FIXME/HACK comments in source (clean codebase)
- No linter/formatter config (no ruff/black/mypy configured)
- No type checking enforcement (no mypy/pyright in CI)

## Database Conventions

- **ID generation** — Scoped counters per project: `next_fact_id(conn, project_id)` returns `"fact-{n}"`
- **Cascade deletes** — All child tables use `ON DELETE CASCADE` from projects
- **Timestamps** — ISO 8601 strings via `utcnow()`, no timezone-aware datetime objects
- **Migrations** — Manual `_ensure_project_columns()` in `db.py` for schema evolution
