Published

Runbook · EL RELEASE — 142 commits sin desplegar

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

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 Jobs corre 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:

ClaseQué esEjemplosAcción
Retirado a propósitoLo demolió una fase, y la migración que lo creaba sigue en el repodataset_row_staging · dataset_branch_rows · lakehouse_read_flags · lakehouse_write_flags · lakehouse_read_parity · iceberg_sync_cursor · los RPC sdk_*_dataset_rowsNADA. Reaplicar resucitaría el plano PG
Nunca aplicadoMigració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_sharesDecidir: aplicar o retirar la migración
⚠️ DriftadoEl cuerpo vivo ≠ el del repoacknowledge_schema_dependencyInvestigar cuál manda

20261265 es 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.

PasoPor qué aquíVerificación
R·0Commitear 20261277 y decidir si se aplicaUna migración untracked no se puede razonargit status limpio en supabase/
R·1Decidir sobre las «nunca aplicadas» (§3, clase ⛔)Desplegar código que las llama contra una BD que no las tiene rompe en runtime, no en CIpor cada una: aplicar, o borrar la migración con su motivo
R·2Aplicar las migraciones pendientes del delta (20261272-77)El esquema va SIEMPRE por delante del código que lo usaaudit-migration-drift sin nuevos ausentes
R·3Desplegar Vercel desde un worktree LIMPIOEl árbol local tiene cambios sin commitear que no deben viajar/api/diag/motor responde y declara los interruptores esperados
R·4Llevar HEAD a main → redespliega Carbon JobsEs el único camino al worker de ingestarailway deployment list -s "Carbon Jobs" en el commit nuevo
R·5Contraer: soltar las dos columnas generadas + barrer los lectores de scripts/ y los dos n3aSólo cuando NADIE las lead5-dependencias-de-columna.ts → 0
R·6I·1·b: apuntar LAKEHOUSE_REST_URI de warehouse-writer a /api/icebergNecesita R·4: el derivador vive en el workeruna 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:

ClaseFicherosQué hacer
🏁 ES EL TUBOla clasificación estaba mallib/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 icebergparse · manual-table · model-node-materialiser · cdc/sinkBorrar la rama muerta
📖 Sólo lectores — leen una tabla vacía y devuelven nadastudio/digest-worker · studio/training-pair-worker · table-export/run-export · datasets/routeMiden en falso: un muestreo sobre 0 filas dice «no hay datos», no «miré donde no era»
🗂️ Ni siquiera son filasfileBackedDatasetUpload (6) · sdk/v1/.../files · media-set/item · notebooks/.../uploadSon manifiestos de fichero — ya mudados a dataset_file_manifest. Menciones residuales
💬 Sólo un comentariolib/lakehouse/read-router.tsUna línea

⭐⭐ La lección, y me costó una edición revertida: iba a limpiar con cuidado la
rama PG de dataset-writer.ts —comentario explicando por qué es inalcanzable,
throw para 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 y throw sobre 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 migracionesHoy «¿está aplicada?» se contesta infiriendo del esquema. Con 259 ficheros, eso no escala y ya falló
Que Carbon Jobs y Vercel salgan del MISMO commitDos relojes es lo que produjo los dos incidentes de hoy
⚠️Un gate que impida contraer antes de desplegar20261278 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.