HANDOFF · 2026-08-17 — la federación por la cara, y el día que el tubo se cortó
Para qué es esto. La foto autosuficiente para retomar. Supersede a
handoff-2026-08-16-ingesta-gobernada.md,
cuyo §0 (el editor caído) está cerrado.Regla de la casa: cada hecho lleva el comando que lo demuestra.
0 · 🏁 DESPLEGADO (lo hizo el owner)
Prod está al día y Vercel salió verde. La causa del build roto era la hipótesis de
este §0: el worktree despliega HEAD, y HEAD tenía media pareja de un cambio —
WarehouseClient.tsx ya pasaba onNewConnection y WarehouseTreeSidebarProps aún no lo
declaraba. Cerrado en b4bd4d5.
⭐ La lección que deja, y vale para cualquier deploy futuro: desplegar desde worktree
limpio protege de embarcar el árbol sucio, pero crea el riesgo simétrico — que HEAD
tenga sólo la mitad de un cambio repartido entre ficheros de dos manos. tsc en el árbol
local no lo ve, porque allí están las dos mitades.
⇒ Antes de desplegar, comprobar HEAD en el worktree, no en el árbol:
git worktree add --detach /c/tmp/carbon-deploy HEAD
cp -r .vercel /c/tmp/carbon-deploy/.vercel
# y ANTES de `vercel --prod`, un typecheck contra ESE árbol
1 · Lo cerrado hoy
| evidencia | ||
|---|---|---|
| 🏁 §0 del 08-16 · el SQL Editor, en pie | la SA del handoff era la equivocada | pod 1/1, E·3 verde |
| 🏁 C·5·0 el motor carga el driver JDBC | con control negativo | c5-0-probe-motor.ts |
| 🏁 C·5·1 el SPI, cableado | medido sin un solo Postgres | c5-1-probe-spi.ts |
| 🏁 N·4 un solo régimen de namespace | 110 tablas · trinquete · desplegado | check:regimen-namespace |
| 🏁 C·5 la copia | 250 filas + suma en destino, fallo sin huérfanas | c5-gate-copia.ts |
🏁 C·5 idempotencia · lotes · fan-out · LIKE | dos pasadas siguen en 250 · 4 lectores | idem |
| 🏁 C·6·0 la medida del tubo | 59 fich. / 9.711 líneas | c6-0-medir-el-tubo.ts |
| 🏁 C·6 el tubo, cortado | trinquete verde sobre 3.985 ficheros | check:sin-tubo |
| 🏁 F·1 consultar sin copiar | zero-copy medido + JOIN mixto | f1-gate-federacion.ts |
| 🏁 W·1 el asistente crea el securable | 6 orígenes con backfill | w1-backfill-conexiones.ts |
Commits: eeecfac · f1a62b1 · 84917c1 · fa6c0bd · cc46ed5 · 4cfc6d8 ·
82e2667 · c9e304c (+ 095da54, 8f00b1d, 3dfdace, 2791760, 8460992 ya en prod).
2 · ⭐⭐⭐ EL VIRAJE DE NORTE (decisión del owner)
Federación + metadata-only + zero-copy writes + vistas pasan a ser EL FUERTE. Copiar es un recurso aceptado, no la apuesta.
⚠️ Esto revierte el punto 1 de las decisiones del 08-11 («la federación NO vuelve al
norte»). Queda marcado como virado en carbon-sql-parser-decision y escrito en
platform-thesis como cuña técnica.
⛔ Con una restricción medida y no negociable: la federación va POR la cara, nunca
alrededor. Un FOREIGN CATALOG real sería un catálogo que el motor ve directamente,
y eso lo deja fuera del punto de aplicación — la federación no se debilitaría,
desaparecería.
3 · ⭐⭐ Los hallazgos que ordenan lo que queda
① ⭐⭐⭐ La primitiva de C·5 ERA la de la federación. La vista JDBC temporal sirve
para copiar, consultar sin copiar, LIKE y (mañana) vistas: en los cuatro casos lo que
hace falta es que el motor lea del origen bajo una autorización nuestra; sólo cambia
qué se hace después. Vive en lib/governance/jdbc-vista.ts, una sola vez.
② ⛔⛔ La puerta y el motor NO tienen la misma red. Leer el esquema conectándose al
origen desde la puerta murió con timeout expired contra un Cloud SQL de IP privada
que el clúster sí alcanza — y en prod la puerta corre en Vercel. ⇒ el esquema lo da
quien va a mover los datos (DESCRIBE sobre la vista JDBC), y de paso desapareció
un mapa de tipos PG→Spark hecho a mano.
③ ⭐⭐ La desambiguación es de EXISTENCIA — tercera vez (C·4, C·5, F·1). Ya no es
casualidad: en una puerta que habla el mismo dialecto que el motor, nunca puede ser
sintáctica. SELECT * FROM main.default.t y SELECT * FROM conexion.esq.t son la
misma forma.
④ El tubo no estaba dormido: estaba FALLANDO. 55 datasets con datos (el último del 11-ago) y 7 fantasmas, el último del 16-ago. El último fallo era más reciente que la última escritura buena: se invocaba, fallaba en silencio, ocupaba el nombre y le enseñaba al usuario un dataset vacío como si hubiera funcionado.
⑤ El CDC nunca corrió. cdc_syncs y cdc_sync_runs a CERO: 3.283 líneas
(Oracle LogMiner, parser de redo, snapshots) sin ejecutar ni una vez.
⑥ connections y user_credentials eran dos mundos. El Catalog Explorer lista desde
integraciones; el carril gobernado cuelga de connections, que tenía 0 filas. El
origen «se veía en el catálogo» y no se podía consultar. ⇒ W·1.
4 · 🪤 Trampas nuevas
| ⛔⛔ | Un binding de IAM a la SA equivocada responde «Updated IAM policy». La identidad de los nodos se le pregunta al node-pool (gke-node-carbon@, no la compute default) |
| ⛔⛔ | connectionProvider 'carbon' es obligatorio: el proveedor básico de Spark responde a todo, así que en cuanto el nuestro dice que sí hay DOS y Spark aborta. ⭐ Ese error es la prueba de que el plugin se descubrió |
| ⛔⛔ | fetchsize no es optimización: es el techo de RAM. pgjdbc materializa el resultado entero antes de la primera fila — y no salta en pruebas porque las tablas de prueba son pequeñas |
| ⛔ | El puente RECORTA EL CENTRO de la traza: buscar ClassNotFoundException da NO_MEDIDO con el motor sano. Y topa wait_timeout_s en 50 (422) |
| ⛔ | El guardián del editor mata CREATE OR REPLACE por el TEXTO — también una vista TEMPORAL, que ni es tabla ni tiene datos |
| ⛔ | La puerta NO admite INSERT, y no por descuido: es decisión pendiente en el contrato ⇒ la carga usa MERGE (que además es la reserva del §7) |
| ⛔ | Una relación de la sesión del motor hay que DECLARARLA (RunQueryOpts.fuentesDeMotor), como writeTarget |
| ⛔ | SEGMENTO no casaba el backtick doble aunque descitar sí lo deshacía ⇒ un nombre con backtick partía la referencia. Lo cazó un test de ida y vuelta |
| ⚠️ | import type no cuenta para alcanzabilidad: se borra al compilar |
| ⚠️ | lib/triggers/polling-* NO es el tubo (es la cola de workflows del canvas) — inflaba el censo en 2.348 líneas |
⛔⛔⛔ Y las trampas de MEDICIÓN, que son las peores
| ① | El censo dijo «0 vivas de 131» — cero errores, número absurdo: preguntaba por el warehouse compartido cuando el inquilino tiene el suyo. El prefijo se resuelve por inquilino |
| ② | TRES estados, no dos: un timeout contado como fantasma daba 107/3 y 108/2 en dos corridas. Transporte fallido = indeterminado ⇒ abortar |
| ③ | Contar ficheros con find mete los worktrees de .claude/ (triplicaba); y un pipe a grep/wc dentro de execSync devuelve 0 en Windows sin un solo error |
| ④ | ⭐ Dos falsos negativos MÍOS al comprobar permisos: usar created_by (que en orígenes viejos es un id DEV de Clerk, no miembro), y usar el workspace de la primera conexión para todas cuando están en dos. Daba 5 de 6 denegadas — muy convincente y falso. Cada securable se decide en SU workspace o no se decide nada |
⭐ Y la lección de método que vale para todo lo demás: un censo que dice «lo cubre la fase X» hay que releerlo cuando X se cierra. C·0 daba por cubiertas la idempotencia y los lotes, y ninguna estaba cuando C·5 se cerró.
5 · La arquitectura, como ha quedado
el usuario / la UI / un notebook
│ (la UI genera la MISMA sentencia)
▼
┌──────────────────────────────┐
│ LA PUERTA (runQuery) │ decidePrivilege · plan · ledger
└──────────────────────────────┘
│ │ │
gramáticas federación catálogo
de la puerta (F·1) (warehouse)
─────────── ────────── ──────────
CREATE CONNECTION SELECT/CTAS sobre
DROP / SHOW CONNECTIONS tablas Iceberg
SHOW SCHEMAS/TABLES IN <c>
CREATE TABLE … AS SELECT * FROM <c>.<e>.<t> (copia)
CREATE TABLE … LIKE <c>.<e>.<t> (metadata-only)
SELECT … FROM <c>.<e>.<t> (zero-copy)
│
▼
lib/governance/jdbc-vista.ts ← LA PRIMITIVA, una sola vez
vista TEMPORAL en la sesión del motor · sin secreto en el SQL
│
▼
CarbonJdbcConnectionProvider (Spark)
canjea `carbon.face.token` → /api/connections/credential
│
▼
el PostgreSQL del cliente
El régimen del catálogo, consolidado y desplegado:
warehouse w_<tenant> › namespace {env} › tabla ds_<hex>
el inquilino, una vez el entorno, una vez la identidad física
Lo que ya NO existe: el tubo (polling workers + CDC + warehouse-writer) no es
alcanzable ni desde el lanzador ni desde el camino del producto.
6 · ⏭️ Los horizontes
0. 🏁 desplegado por el owner (§0)
1. El botón que plasma el `SELECT` — REHACERLO bien ← medir el traspaso primero
2. Los 7 FANTASMAS — pero CORTANDO primero quien los crea ← ver §7
2. «Explorar» → derivar al carril gobernado (C·4) ← ⚠️ mide antes: pierde conteos
3. La VISTA persistente sobre el origen ← la pregunta interesante
5. Orígenes que no sean PostgreSQL ← cada uno es un adaptador
6. Escribir en el origen ← decisión NO tomada
7. Borrar las 9.711 líneas del tubo ← otra entrega; el trinquete ya protege
8. Gravitino 1.3.0 (ORTOGONAL) ← exige adaptar la CARA
⏭️ Y el horizonte que los REORDENA — abierto el 2026-08-17
📎 fd-foreign-database-approach.md · FD · LA BASE
FORÁNEA, el siguiente paso de esta misma iteración.
El inventario O·1 dejó un solo rojo estructural —sólo el owner alcanza una
conexión— y resultó no ser un problema de reparto: falta el OBJETO. Hoy la lectura
federada se autoriza con CONNECTION_USE, o sea contra la conexión, porque no hay una
tabla foránea donde colgar el SELECT. En SQL/MED y en Databricks eso se resuelve con
un contenedor espejado; aquí se adopta la forma de Snowflake: FOREIGN DATABASE, con
database.schema.object.
⭐ Y absorbe tres de estos horizontes: el ③ (la vista persistente pasa a ser un
CREATE VIEW normal), el ② («Explorar» tendría por fin metadata catalogada que enseñar)
y el ∅ DESCRIBE del inventario.
⛔ Sin revertir el veto de §2: el espejo es metadata en Index —el punto de
aplicación—, y los datos se siguen resolviendo por la vista JDBC. El motor no ve ninguna
base foránea. Va con trinquete propio (FD·6).
7 · ⚠️ Los sesgos y deudas que quedan abiertos
① Los 7 fantasmas NO se han reconciliado, y a propósito. Son filas de datasets con
0 filas y iceberg_namespace null — tablas que nunca existieron, que ocupan el
nombre. ⛔ Los crea la UI, no el worker: cortar el tubo no cerró esa entrada.
Reconciliar antes de cerrarla es barrer con el grifo abierto. Y la entrada vive en
las rutas de integraciones, donde el owner tiene trabajo sin commitear.
②·bis ⛔ El botón que plasmaba el SELECT NO funcionaba, y está SUPRIMIDO. Se
entregó como hecho —con la coletilla «no verificado en navegador», que no basta— y en
pantalla no hacía nada. ⇒ retirado entero (el prop initialQuery del editor, el paso por
WorkspaceShell, el generador y su test) en vez de dejarlo a medias.
⭐ Lo único que sobrevive, porque era un bug real e independiente: SEGMENTO no
casaba el backtick doble aunque descitar sí lo deshacía, así que un nombre con
backtick partía la coordenada. Lo encontró el test de ida y vuelta del generador; su
cobertura se movió a refs-cualificadas.test.ts para que no se fuera con el borrado.
⚠️ Y queda sin diagnosticar por qué no llegaba la query. La sospecha barata: el
traspaso escribe la pestaña en sessionStorage y WorkspaceShell la restaura al montar —
habría que comprobar si esa restauración conserva query. Medir antes de reescribir.
② «Explorar» sigue en el camino viejo. Llama a /api/integrations/[service]/tables,
que devuelve filas y columnas estimadas (lo que el árbol enseña: 112.650 rows · 7 cols). C·4 devuelve nombres y tipos, no los conteos ⇒ un cambio a ciegas empobrece
la UI. Medir antes de migrar.
③ Hay un SEGUNDO camino que inserta credenciales: POST /api/credentials, que usan
el editor de linaje y el grafo. No crea securable. No es el del asistente, así que no
afecta a lo que se está probando — pero un origen creado por ahí nacería sin gobernar.
④ El SELECT federado desde el editor NO está verificado en navegador. El gate lo
prueba llamando al ejecutor; la ruta HTTP necesita una sesión de Clerk. Falta esa vuelta.
⑤ 4 tests rojos PRE-EXISTENTES en lib/lakehouse/read-client.test.ts y
namespace-resolution.test.ts. Comprobado con los cambios guardados: no son de esta
sesión.
⑥ Deuda con fecha: subir a org:admin en Clerk LIVE a lunacorunaspain y
facebookdeveloper98, o el webhook los bajará a viewer.
⑦ La autoría histórica sigue en ids DEV (110 columnas / 57.392 filas) — y es lo que
hizo que created_by diera falsos negativos aquí.
8 · 💰 Estado del entorno · el clúster está ENCENDIDO
- GKE
trino-compute-clone: 1 nodo, encendido. Apagarlo:⚠️ Quitar el autoscaling ANTES o el autoscaler lo vuelve a levantar.gcloud container clusters update trino-compute-clone --no-enable-autoscaling \ --node-pool main-pool-4 --region europe-west1 --project trino-k8s gcloud container clusters resize trino-compute-clone --node-pool main-pool-4 \ --num-nodes 0 --region europe-west1 --project trino-k8s --quiet - Cloud SQL
carbon-catalog: se le añadió la basec5_origeny el usuarioc5_lector(250 filas enpublic.ventas, suma 423562.50) para los gates. Si no se quieren ahí:gcloud sql databases delete c5_origen --instance=carbon-catalog. La contraseña no está en disco: se regenera congcloud sql users set-password c5_lector --instance=carbon-catalog --password=…. - Prod (Vercel): al día y verde — desplegado por el owner (§0).
- Spark:
spark-k8s:4.1.2-carbon1con driver Postgres +CarbonJdbcConnectionProvider.
9 · Comandos que se corren
# los gates de la ingesta y la federación (necesitan el port-forward)
kubectl port-forward -n spark svc/spark-bridge 18080:80
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c5-0-probe-motor.ts
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c5-1-probe-spi.ts
C5_FANOUT_MIN_RANGO=1 SPARK_BRIDGE_URL=http://127.0.0.1:18080 C5_PG_PASSWORD=… \
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c5-gate-copia.ts
SPARK_BRIDGE_URL=http://127.0.0.1:18080 C5_PG_PASSWORD=… \
npx dotenv -e .env.local -- npx tsx scripts/ingesta/f1-gate-federacion.ts
# el estado del sustrato
npm run check:regimen-namespace # un solo régimen de namespace
npm run check:sin-tubo # el tubo no es alcanzable
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c6-0-medir-el-tubo.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/w1-backfill-conexiones.ts
# en frío
npx vitest run lib/governance lib/warehouse # 842
NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit
10 · 📎 Los documentos de esta sesión
fd-foreign-database-approach.md | ⏭️ lo que se abre: la BASE FORÁNEA — el objeto que falta, y por qué no revierte el veto |
f1-federacion-por-la-cara.md | la cuña: consultar sin copiar, y por qué un FOREIGN CATALOG no |
n4-regimen-de-namespace.md | el régimen consolidado + las trampas de censo |
ingesta-c6-medida-del-tubo.md | cuánto tubo había, y que no estaba dormido |
ingesta-gobernada-approach.md | C·0-C·6, actualizado con lo entregado de verdad |
handoff-2026-08-16-ingesta-gobernada.md | el anterior — su §0 está cerrado |