## Migraciones de BBDD: nunca perder datos

**Regla absoluta:** todo cambio de schema en SQLite (apps con `kanban.db`, `operations.db` propia, registry.db, etc.) DEBE ir en un archivo de migración versionado. Nunca borrar/recrear tablas, nunca cambiar tipos sin proceso seguro, nunca confiar en "borra el .db y vuelve a empezar".

### Por que

- Las apps almacenan **datos vivos** (cards, entities, executions, assertions, columns, sessions).
- Borrar = perder horas/dias/semanas de trabajo del usuario.
- Lo que es trivial en dev (`rm operations.db`) es destructivo en produccion (deploys + sync entre PCs).
- Sync entre PCs (`fn sync`, `/full-git-pull`) trae bases de datos de otros equipos: si tu schema asume tabla recreada, los datos del otro PC desaparecen.

### Patrones obligatorios

#### 1. Archivos numerados en `migrations/`

Cada cambio de schema = un archivo nuevo `migrations/NNN_<accion>.sql`. Numeracion zero-padded de 3 digitos. Nombre descriptivo.

```
apps/<app>/migrations/
  001_init.sql                      # CREATE TABLE inicial (no se modifica nunca)
  002_add_stickers.sql               # ALTER TABLE cards ADD COLUMN stickers
  003_add_assignees.sql              # ALTER TABLE cards ADD COLUMN assignee_id
  004_create_lock_history.sql        # CREATE TABLE card_lock_history
  ...
```

#### 2. Solo operaciones aditivas seguras

| Operacion | Seguro | Notas |
|---|---|---|
| `CREATE TABLE IF NOT EXISTS` | si | idempotente |
| `CREATE INDEX IF NOT EXISTS` | si | idempotente |
| `ALTER TABLE ... ADD COLUMN` | si | aditivo, default obligatorio |
| `INSERT INTO ... ON CONFLICT IGNORE` | si | seed data idempotente |
| `DROP TABLE` | NO | destructivo |
| `DROP COLUMN` | NO | destructivo (SQLite < 3.35 ni siquiera lo soporta) |
| `ALTER TABLE ... RENAME COLUMN` | precaucion | rompe codigo viejo si rollback |
| `ALTER TABLE ... DROP/ALTER constraint` | NO sin backup | requiere recreate-and-copy |

Si necesitas cambiar tipo, eliminar columna, o cambiar PK: hacer **migracion en pasos** (Branch by Abstraction):
1. Crear nueva columna/tabla con la forma deseada (migration N).
2. App escribe en ambas (migration N+1, codigo).
3. Backfill de datos viejos (migration N+2, script).
4. App lee solo de la nueva (migration N+3, codigo).
5. Eliminar la vieja (migration N+4, despues de tener backups verificados).

Cada paso = una rama TBD corta + commit + verificacion. Nunca un solo PR que rompa lectores.

#### 3. Aplicacion idempotente al arrancar

La app aplica todas las migraciones en orden al iniciar. Patron canonico (Go):

```go
//go:embed migrations/*.sql
var migrationsFS embed.FS

func applyMigrations(conn *sql.DB) error {
    files, err := fs.Glob(migrationsFS, "migrations/*.sql")
    if err != nil { return err }
    sort.Strings(files)
    for _, f := range files {
        b, err := migrationsFS.ReadFile(f)
        if err != nil { return err }
        if _, err := conn.Exec(string(b)); err != nil {
            // SQLite ALTER TABLE ADD COLUMN no es idempotente nativamente.
            // Si ya existe, ignorar el error de "duplicate column".
            if !strings.Contains(err.Error(), "duplicate column") &&
               !strings.Contains(err.Error(), "already exists") {
                return fmt.Errorf("%s: %w", f, err)
            }
        }
    }
    return nil
}
```

Alternativa: tabla `_migrations` con las versiones aplicadas (mas robusta para schemas grandes). Para apps pequeñas (kanban, operations.db), bastan los archivos numerados + `IF NOT EXISTS` / catch de "duplicate column".

#### 4. Migracion + cambios en codigo en el mismo commit

Cuando añades una columna:
- `migrations/NNN_<accion>.sql` (nueva)
- `db.go` (lee/escribe la columna)
- `types.ts` (frontend type)
- Tests

Todo en el mismo commit/rama. Si solo mergeas la migracion pero no el codigo, otros PCs aplican la migracion al sync y luego el codigo viejo no la usa. OK. Si mergeas el codigo sin la migracion, la app peta al arrancar en otros PCs. Mal. **Migracion antes que codigo en el orden de archivos** (no de tiempo).

#### 5. Tests sobre la migracion

Cada migracion debe tener test que:
- Arranca con DB vacia → aplica todas → verifica schema.
- Arranca con DB en estado N-1 (datos previos) → aplica migracion N → verifica que los datos se conservan.

Esto detecta migraciones destructivas antes de mergear.

### Que NO hacer

| Anti-patron | Consecuencia |
|---|---|
| Borrar `*.db` durante dev y commitear "schema actualizado" | Otros PCs pierden datos al sync. |
| Modificar `001_init.sql` para añadir columnas | Las DBs ya creadas no se actualizan. Datos divergentes. |
| `DROP TABLE x; CREATE TABLE x ...` | Borra todo lo que el usuario tenga. |
| Usar `ensureColumns` sin archivo SQL paralelo | El cambio de schema vive solo en codigo Go, no auditable, no migrable manualmente. |
| Cambiar tipo de columna in-place | SQLite necesita recreate-and-copy. Asume que pierde datos si no se hace bien. |
| "fn index" como solucion para regenerar registry.db | OK para `registry.db` (regenerable). NUNCA para `operations.db`, `kanban.db`, etc. |

### Casos especiales

#### registry.db (raiz del fn_registry)

`registry.db` SE PUEDE regenerar con `fn index` desde los `.go` y `.md`. Para cambios de schema del registry: actualizar `registry/migrations.go` o el codigo de creacion + `fn index`. NO hace falta archivo de migracion porque la fuente de verdad son los `.md`/`.go`. Excepcion: tablas con datos vivos (`proposals`, `pc_locations`) — esas SI requieren migracion preservando datos.

#### operations.db (por app)

Cada app tiene su `operations.db` con entities/relations/executions. Schema definido en `fn_operations/`. Cambios al schema → archivo de migracion en `fn_operations/migrations/` aplicado al abrir la BD. Idempotente.

#### apps con BD propia (kanban, etc.)

Mismo patron: `apps/<app>/migrations/NNN_*.sql`, embebido y aplicado al arrancar.

### Comandos utiles

```bash
# Ver schema actual
sqlite3 apps/kanban/operations.db ".schema"

# Ver columnas de una tabla
sqlite3 apps/kanban/operations.db "PRAGMA table_info(cards);"

# Backup antes de migracion arriesgada
sqlite3 apps/kanban/operations.db ".backup apps/kanban/operations.db.bak.$(date +%Y%m%d)"

# Aplicar una migracion manual (si la app no esta corriendo)
sqlite3 apps/kanban/operations.db < apps/kanban/migrations/00X_<accion>.sql

# Listar archivos de migracion en orden
ls apps/kanban/migrations/*.sql | sort
```

### Resumen

- Cada cambio de schema = archivo numerado nuevo en `migrations/`.
- Aditivo siempre que se pueda. Destructivo solo en pasos verificados con backup.
- App aplica migraciones al arrancar, idempotente.
- Migracion + codigo + tests en el mismo commit.
- Nunca borrar `.db` para "arreglar" schema. Nunca modificar migraciones existentes.

### Estado retroactivo (2026-05-09)

Inventario de BDs del ecosistema y conformidad con la regla:

| Repo / App | BD | `migrations/` | Estado |
|---|---|---|---|
| `registry/` | `registry.db` | si (11 archivos) | ✓ |
| `fn_operations/` | `operations.db` por app | si (4 archivos) | ✓ |
| `apps/kanban/` | `operations.db` (kanban) | si (5 archivos: 001 init, 002 stickers, 003 columns_extras, 004 cards_extras, 005 history_actor) | ✓ |
| `apps/deploy_server/` | `operations.db` (deploys) | si (2 archivos: 001 init, 002 target_extras) | ✓ |
| `apps/dag_engine/store/` | DB del dag_engine | si (001_init) | ✓ |
| `projects/element_agents/.../shell/memory/` | memoria del agente | si (001_init) | ✓ |
| `projects/osint_graph/apps/graph_explorer/` | DBs C++ inline (project_manager, layout_store, jobs, node_groups) | NO | **pendiente** — refactor C++ multi-archivo, mover schema inline a `migrations/*.sql` aplicado al abrir cada DB. |

Las apps marcadas ✓ usan el patron canonico `embed.FS + applyMigrations()` (Go) o equivalente. La C++ pendiente requiere ronda dedicada — tracker via issue cuando se aborde.

`apps/kanban/db.go::ensureColumns` se mantiene como **backstop idempotente** para DBs muy antiguas creadas antes del refactor de migraciones. NO añadir columnas nuevas alli — siempre via archivo SQL.
