Runbook · EL RELEASE — 142 commits sin desplegar
Abierto: 2026-08-13. Este runbook existe porque todo lo que se construye hoy
se queda en el repo:Carbon Jobscorre código del 08-09 y Vercel sale de un
árbol local. Cada avance —I·1·b, la contracción de D·5·b, T·1— cuelga del mismo
clavo.Regla de la casa: cada hecho lleva el comando que lo demuestra.
1 · La topología, y por qué esto no es «un deploy»
Vercel (app.paladio.io) ← el ÁRBOL LOCAL. `vercel --prod` embarca lo que haya,
incluido lo sucio ⇒ se despliega desde worktree limpio
Carbon Jobs (Railway) ← GitHub, branch `main`, hoy en 76cb381 (2026-08-09)
warehouse-writer ← imagen propia + variables de Railway
Supabase (control-plane) ← migraciones aplicadas A MANO, sin tracker
⭐⭐ Son cuatro sustratos con cuatro relojes distintos, y ninguno se entera de
los otros. La misma línea de código llega a Vercel y no al worker; la misma
migración llega a la BD y no al código. Los dos incidentes de hoy son eso: la
ingesta con el token viejo (I·1) y la cara caída por contraer antes de desplegar
(§7·decies del approach de tenencia).
2 · Qué hay en el delta — medido
git rev-list --count origin/main..HEAD # → 142
git rev-list --count HEAD..origin/main # → 0 (main no tiene nada que no esté aquí)
Reparto por área (ficheros tocados):
app/(app) 124 lib/compute 68 scripts/governance 52
docs/architecture 116 docs/piezas 55 lib/warehouse 39
⇒ No es una feature: son cuatro días de trabajo en cinco frentes. Por eso no se despliega «para arreglar algo»: se despliega como release, con su verificación.
Migraciones dentro del delta
20261272_file_items.sql 20261275_datasets_project_opcional.sql
20261273_file_items_sources.sql 20261276_idempotency_scope_sql_dml.sql
20261274_datasets_sync_file_id_sin_fk.sql
🏁 20261278_tenencia_env_derivada.sql ← YA APLICADA (13-08)
🏁 20261279_tenencia_columnas_generadas.sql ← YA APLICADA (13-08)
⚠️ Y una que ni siquiera está commiteada: 20261277_file_items_lineage_exploration.sql
aparece como untracked. Una migración fuera de git es una que nadie puede saber si
está aplicada.
3 · ⛔ El estado de la BD — 20 tablas y 37 funciones ausentes
npx dotenv -e .env.local -- npx tsx scripts/dataspaces/audit-migration-drift.ts
Migraciones en repo: 259 · declarado: 123 tablas · 101 funciones
tablas ausentes: 20 · funciones ausentes: 37 · funciones driftadas: 1 · columnas: 2
schema_migrations: NO EXISTE (este proyecto no usa el tracker de supabase CLI)
Y hay que leerlo con cuidado: no todo lo ausente es un pendiente. Se separa en tres clases, y confundirlas sería aplicar migraciones que revertirían trabajo hecho:
| Clase | Qué es | Ejemplos | Acción |
|---|---|---|---|
| ✅ Retirado a propósito | Lo demolió una fase, y la migración que lo creaba sigue en el repo | dataset_row_staging · dataset_branch_rows · lakehouse_read_flags · lakehouse_write_flags · lakehouse_read_parity · iceberg_sync_cursor · los RPC sdk_*_dataset_rows | NADA. Reaplicar resucitaría el plano PG |
| ⛔ Nunca aplicado | Migración real que no llegó | ⭐ 20261265_semantic_context_graph (6 tablas + 3 funciones) · ml_* (5 tablas + 11 funciones) · notebook_cell_snapshots · graph_saved_views · page_shares | Decidir: aplicar o retirar la migración |
| ⚠️ Driftado | El cuerpo vivo ≠ el del repo | acknowledge_schema_dependency | Investigar cuál manda |
⭐
20261265es de la serie RECIENTE —la misma en la que estamos trabajando— y
no está aplicada. Es la prueba de que el backlog no es sólo histórico.
⛔ Sin schema_migrations, «aplicada» no es una consulta: es una inferencia. Este
runbook usa el auditor porque es lo único que hay; poner un tracker es trabajo de
después del release, y va en §6.
4 · El orden, y por qué cada paso va donde va
expand → migrar el código → contract. Los dos incidentes de hoy son el precio
de saltarse el paso de en medio.
| Paso | Por qué aquí | Verificación | |
|---|---|---|---|
| R·0 | Commitear 20261277 y decidir si se aplica | Una migración untracked no se puede razonar | git status limpio en supabase/ |
| R·1 | Decidir sobre las «nunca aplicadas» (§3, clase ⛔) | Desplegar código que las llama contra una BD que no las tiene rompe en runtime, no en CI | por cada una: aplicar, o borrar la migración con su motivo |
| R·2 | Aplicar las migraciones pendientes del delta (20261272-77) | El esquema va SIEMPRE por delante del código que lo usa | audit-migration-drift sin nuevos ausentes |
| R·3 | Desplegar Vercel desde un worktree LIMPIO | El árbol local tiene cambios sin commitear que no deben viajar | /api/diag/motor responde y declara los interruptores esperados |
| R·4 | Llevar HEAD a main → redespliega Carbon Jobs | Es el único camino al worker de ingesta | railway deployment list -s "Carbon Jobs" en el commit nuevo |
| R·5 | Contraer: soltar las dos columnas generadas + barrer los lectores de scripts/ y los dos n3a | Sólo cuando NADIE las lea | d5-dependencias-de-columna.ts → 0 |
| R·6 | I·1·b: apuntar LAKEHOUSE_REST_URI de warehouse-writer a /api/iceberg | Necesita R·4: el derivador vive en el worker | una ingesta con asiento en access_events y una tabla NUEVA nacida por la cara |
⚠️ R·3 y R·4 no son intercambiables ni simultáneos. Entre uno y otro hay una ventana en la que la app nueva habla con workers viejos. Todo lo desplegado hoy debe tolerarlo — y la forma de saberlo es que ningún paso del release dependa de que los dos estén a la vez, que es justo lo que R·6 sí exige y por eso va al final.
5 · Los rojos conocidos que el release NO arregla
Para que nadie los lea como regresiones del despliegue:
· 4 tests preexistentes en lib/lakehouse (read-client · namespace-resolution)
— sustrato-07 §7, verificados por stash el 13-08
· 26 errores de tsc, TODOS de la clase «scripts sin import comparten scope global
y colisionan en main()» — ninguno en código de producción
· T·1 en rojo: la cara no sirve el CTAS atómico, y sus dos agujeros
(materializa sin compensar · `DROP` no reconcilia Index)
5·bis · ⭐ EL RESCOLDO DE dataset_rows — y por qué NO se barre entero
npm run check:dataset-rows-seam
# → allowlist VACÍA · 18 ficheros con accesos crudos, con su cuenta pineada
La tabla no existe desde el 2026-07-31. Así que ninguno de esos 43 accesos es «legacy por migrar»: es código que no puede ejecutarse. Pero antes de tocarlos hay que preguntarse de quién son, y ahí la mitad del trabajo desaparece:
| Clase | Ficheros | Qué hacer |
|---|---|---|
| 🏁 | lib/workers/polling/dataset-writer.ts (9 → 0) | RETIRADO el 08-13. Ver la corrección de abajo |
⚠️ Alcanzable y roto — puerto con maxMode: 'pg' | app/api/datasets/ingest (ingest.stream)app/api/datasets/rows (row.edit) | NINGUNO lo estaba: row.edit cerrado (409) e ingest.stream cerrado (410) desde D·2. Queda el .delete() mudo de handleAbortTransaction |
| ✅ Inalcanzable — la rama existe pero el puerto resuelve a iceberg | parse · manual-table · model-node-materialiser · cdc/sink | Borrar la rama muerta |
| 📖 Sólo lectores — leen una tabla vacía y devuelven nada | studio/digest-worker · studio/training-pair-worker · table-export/run-export · datasets/route | Miden en falso: un muestreo sobre 0 filas dice «no hay datos», no «miré donde no era» |
| 🗂️ Ni siquiera son filas | fileBackedDatasetUpload (6) · sdk/v1/.../files · media-set/item · notebooks/.../upload | Son manifiestos de fichero — ya mudados a dataset_file_manifest. Menciones residuales |
| 💬 Sólo un comentario | lib/lakehouse/read-router.ts | Una línea |
⭐⭐ La lección, y me costó una edición revertida: iba a limpiar con cuidado la
rama PG dedataset-writer.ts—comentario explicando por qué es inalcanzable,
throwpara el futuro— en un fichero que el approach de la ingesta retira
entero. Pulir lo que estás a punto de borrar no es rigor: es trabajo que se tira,
y encima entrena a leerlo como si fuera a quedarse.Antes de limpiar un rescoldo hay que preguntar de quién es. Los que pertenecen
a una pieza que va a desaparecer no se limpian: se esperan.
⚠️ Corrección (2026-08-13): la pregunta era buena, la RESPUESTA estaba incompleta
Se preguntó «¿de quién es?» y se contestó «del tubo». Falso: los 9 accesos tenían
dos dueños. writeDatasetRowsPg no lo llamaba sólo el sync — lo llamaba la
puerta (junction.ts, brazo postgres de write()), y la puerta no desaparece con
la Fase A. Esperar a que el tubo muriera habría dejado un plano de datos inexistente
colgando de una pieza que sobrevive.
⇒ Se retiró entero el 08-13: PostgresBackend + writeDatasetRowsPg + el brazo de la
puerta que delegaba en él. −808 líneas, trinquete 9 → 0, 48 tests verdes.
⭐ Y la lección original SOBREVIVE intacta, sólo que afilada: lo que se retiró no
es un pulido —comentario ythrowsobre código que se iba a borrar— sino el borrado
mismo. La regla no era «espera»: era «no pulas lo que vas a borrar; bórralo». Lo
que sí hay que esperar es el camino VIVO, que necesita sustituto.Y la pregunta «¿de quién es?» sólo sirve si se contesta buscando llamantes, no
leyendo el nombre del fichero.
⇒ El barrido real son las clases ⚠️, ✅, 📖, 🗂️ y 💬 — 9 accesos de los 43. El resto se va solo.
6 · Lo que hay que arreglar para que no vuelva a pasar
| Qué | Por qué | |
|---|---|---|
| ⛔⛔ | Un tracker de migraciones | Hoy «¿está aplicada?» se contesta infiriendo del esquema. Con 259 ficheros, eso no escala y ya falló |
| ⛔ | Que Carbon Jobs y Vercel salgan del MISMO commit | Dos relojes es lo que produjo los dos incidentes de hoy |
| ⚠️ | Un gate que impida contraer antes de desplegar | 20261278 llevaba la advertencia escrita en su cabecera y no impidió nada: un comentario no es un gate |
7 · La frase que resume
El problema no es que haya 142 commits: es que hay cuatro sustratos con cuatro
relojes y ninguno se entera de los otros. El release no es empujar código — es
volver a poner los cuatro en hora, y en el orden que no rompe.