---
paths:
  - backend/migrations/**
  - backend/core_migrations.py
  - backend/thesis_seed.py
---

# Migrations & seeders

Read this before adding or editing a schema migration or a one-time seed/backfill.

## How they run

- Migrations apply automatically at startup via `backend/core_migrations.py`, reading `backend/migrations/NNNN_*.sql` in order and tracking applied files in a `schema_migrations` ledger.
- One-time seeds/backfills live in `backend/thesis_seed.py` (the `ensure_*` functions), wired into `server.init_db()` and run on every boot.

## Rules

- **Additive + reversible only.** Numbered `NNNN_*.sql` with a paired `NNNN_*.down.sql` — ship the `.down.sql` with every new migration. SQLite `ALTER` is add-column / rename only; no drop-column, no type change.
- **Seeds/backfills must be idempotent** via `interaction_log` sentinels (the `ensure_*` pattern) — safe to re-run on every boot.
- **Make migrations/seeders deployment-state-invariant.** Target rows **structurally**, not by transient text the same change mutates; capture prior state so a revert is exact.
  - *Learned the hard way:* matching old nodes by a body string the same changeset deleted broke fresh DBs. A migration must produce the same end state whether the box is empty, mid-version, or fully seeded.
- **Soft-delete only** — `deleted_at` and/or `status='retired'`; never hard-delete CRM records or thesis history.

## Verify before shipping

- `python3 -m py_compile` the edited Python.
- For any DB logic, run the change against a **copy** of `data/crm.db`, never production. Confirm the paired `.down.sql` cleanly reverts.
