HANDOFF · 2026-08-16 — la ingesta gobernada, y el día que NACER volvió a funcionar
Para qué es esto. La foto autosuficiente para retomar. Supersede a
handoff-2026-08-15b-gobernanza-en-sql.md,
cuyos ①②③ están cerrados (ver sus §8, §9, §10 y §11).Regla de la casa: cada hecho lleva el comando que lo demuestra.
0 · 🏁 CERRADO (2026-08-16, tarde) — EL EDITOR ESTÁ EN PIE
El pod de Spark Connect estaba en ImagePullBackOff por un permiso de lectura en el
Artifact Registry. Resuelto, con el motor ya cargando el driver JDBC.
# ⛔ la SA CORRECTA — ver la trampa de abajo
gcloud artifacts repositories add-iam-policy-binding carbon --location=europe-west1 \
--member="serviceAccount:gke-node-carbon@trino-k8s.iam.gserviceaccount.com" \
--role="roles/artifactregistry.reader"
kubectl delete pod carbon-connect-server-0 -n spark
Estado medido tras el arreglo:
carbon-connect-server-0 | 1/1 Running con spark-k8s:4.1.2-carbon1 |
spark-bridge | 1/1 Ready · /readyz 88 ms en caliente |
jars en /stackable/spark/jars | postgresql-42.7.4.jar · carbon-jdbc-provider-1.0.0.jar |
| E·3 canary del editor | VERDE — lee por Gravitino, la huérfana no aparece |
⛔⛔ La trampa: la SA del comando anterior era la EQUIVOCADA
Este handoff traía 883735825877-compute@developer.gserviceaccount.com (la compute
default). Los nodos no corren con ésa:
gcloud container node-pools describe main-pool-4 --cluster=trino-compute-clone \
--region=europe-west1 --format="value(config.serviceAccount)"
# → gke-node-carbon@trino-k8s.iam.gserviceaccount.com
⭐ El binding a la SA equivocada responde Updated IAM policy for repository [carbon]
— un éxito perfecto que no arregla nada. Es el hallazgo ⑤ del día otra vez (§2): el
estado dice que está bien. ⇒ Un binding de IAM no se da por bueno sin preguntarle al
node-pool con qué identidad corre.
🏁 Y el bloqueante de C·5 CAYÓ — medido, no razonado
kubectl port-forward -n spark svc/spark-bridge 18080:80
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c5-0-probe-motor.ts
✅ ① el motor contesta SELECT 1 → 1
✅ ② driver de PostgreSQL carga · class org.postgresql.Driver
✅ ③ proveedor de Carbon carga · class io.paladio.spark.jdbc.CarbonJdbcConnectionProvider
✅ ④ control negativo una clase inexistente SÍ falla ⇒ la sonda distingue
🏁 VERDE · el bloqueante de C·5 CAYÓ.
⚠️ Alcance dicho para que el verde no se lea de más: esto es el classpath. Que el
SPI de Spark descubra al proveedor y que la credencial llegue sólo lo demuestra una
lectura JDBC real con carbon.connection — y eso es el gate de C·5.
1 · Lo cerrado hoy
| evidencia | ||
|---|---|---|
| 🏁 ① El RESALTADO conoce los verbos de gobernanza | el editor dejó de tener lista propia | grant-sql-resaltado.test.ts |
| 🏁 ② La identidad, unificada hacia la SESIÓN | el canje RFC 7523 queda inerte | sujeto.test.ts |
| 🏁 ③ El alta perdía a los creadores de organización | 5 de 8 workspaces con 0 miembros, reparados | t3-restaurar-membresias.ts |
| 🏁 NACER vuelve a funcionar | el namespace multinivel era la causa | desplegado e453987 |
| 🏁 C·0 censo del tubo | 14 capacidades con destino | c0-censo-del-tubo.ts |
| 🏁 C·1 la conexión, un securable | gate VERDE en vivo | c1-gate-conexion-securable.ts |
| 🏁 C·2 la gramática de conexiones | 22 tests | connection-sql.test.ts |
| 🏁 C·3 el vending de la credencial | gate VERDE en vivo | c3-gate-vending.ts |
| 🏁 C·4 la introspección del origen | VERDE contra un Postgres real | c4-gate-introspeccion.ts |
| 🏁 C·5·0 el proveedor JDBC, desplegado y cargando | el motor ya habla JDBC | c5-0-probe-motor.ts |
| 🏁 C·5·1 el cableado del SPI | Spark instancia el proveedor y le pasa carbon.* | c5-1-probe-spi.ts |
| 🏁 C·5·② la gramática de la copia | 16 tests, con el test de la ambigüedad | copy-sql.test.ts |
| 🟡 C·5·③ la copia | falta el ejecutor + su gate | §4 |
Commits: e453987 · fa95a6d · 76144c5 · 00a722b · 45e1b3c · aaa3efb · 71482c2
2 · ⭐ Los hallazgos que ordenan lo que queda
① ⛔⛔ El namespace MULTINIVEL era lo que impedía nacer. El hook createTable →
importTable → loadTable de Gravitino lo aplana —crea en prod.w_<hex>.t y relee en
prod.t— y devuelve 500 con la tabla ya creada. Y el 504 de 30 s del editor era el
MISMO fallo: un 5xx es reintentable para el cliente Iceberg, que reintentaba hasta que el
proxy cortaba. Una causa, dos síntomas. Arreglado con {env}_w_{tenant} (un nivel), sin
migración: el namespace se pinea por tabla.
② ⭐⭐ La bifurcación «descomponer el CTAS vs pivotar a 1.3.0» era FALSA. Ninguna
desbloquea C·5. 1.3.0 arregla el stage-create (CTAS entre tablas Iceberg); el
problema de la ingesta es leer de PostgreSQL, y para eso faltaba el driver JDBC en
el clúster. Hicieron falta las dos mediciones para verlo. ⇒ 1.3.0 es ORTOGONAL: merece
la pena por el camino Iceberg↔Iceberg, no es prerrequisito de la ingesta.
③ Añadir un securable toca CUATRO sitios en la base, y ninguno avisa del otro. ① el
CHECK del nombre del privilegio, ② el CASE del trigger privilege_grant_stamp —que es
quien impide el cruce entre inquilinos, así que olvidarlo deja el securable sin
aislamiento—, ③ el CHECK de securable_kind, y ④ el de access_events, particionado por
mes (las constraints de las particiones son heredadas: se toca la padre) y sólo visible
con ENABLE_ACCESS_EVENTS=true.
④ El invariante del secreto no se pide: se hace imposible. SecretoDeConexion no se
serializa (JSON.stringify, interpolación, console.log, spread → [secreto]); sólo
.revelar(), que se encuentra con un grep.
⑤ El estado dice que está bien — cuatro veces hoy. Un job con ✅ 100 filas y cero
escritas · un alta active sin miembros · un trinquete verde sobre un alta rota · un
kubectl patch que responde «patched» y no se aplica. Es el patrón del día.
3 · 🪤 Trampas nuevas
| ⛔⛔ | --packages NO sirve para drivers JDBC — el JVM los resuelve al arrancar. Lo documenta el propio operador. ⇒ imagen custom |
| ⛔⛔ | El operador de Spark llevaba en bucle desde las 08:56 (96 errores) — volumeClaimTemplates es inmutable. Ningún cambio del CR se aplicaba, en silencio |
| ⛔ | Extender el sql de serie de Monaco pasa los tests y NO funciona en pantalla: su cargador perezoso pisa lo que se le registre antes ⇒ lenguaje propio |
| ⛔ | Un banco de pruebas que no incluye al que TRADUCE no prueba el camino: el probe de 1.3.0 medía el catálogo aislado, y el fallo estaba en cara↔catálogo |
| ⚠️ | ENABLE_ACCESS_EVENTS apagado hace que un gate cuente 0 y no distinga «apagado» de «roto» |
| ⚠️ | Cloud Build ya no crea la SA legacy: hay que pasar --service-account explícito |
| ⚠️ | --chmod en ADD/COPY exige BuildKit; el builder de Cloud Build usa Docker clásico |
| ⛔⛔ | Un binding de IAM a la SA equivocada responde «Updated IAM policy» — y no arregla nada. La identidad de los nodos se le pregunta al node-pool, no se supone (§0) |
| ⛔ | El puente RECORTA EL CENTRO de la traza (…[N caracteres omitidos]…): la causa raíz de un SparkException cae justo ahí. Un gate que busque ClassNotFoundException da NO_MEDIDO con el motor sano — el recorte del transporte leído como fallo del motor |
| ⚠️ | El puente topa wait_timeout_s en 50 y responde 422, que tampoco es un fallo del motor |
| ⛔⛔ | Un JdbcConnectionProvider selectivo NO basta: el básico de Spark responde a todo, así que en cuanto el tuyo dice que sí hay DOS y Spark aborta en vez de elegir ⇒ hace falta connectionProvider 'carbon' en cada sentencia. ⭐ Y ese error es la prueba de que Spark sí descubrió el plugin, no de que falte |
4 · ⏭️ Los horizontes
0. 🏁 LEVANTAR EL EDITOR — el permiso del registry (§0) ← HECHO
1. C·5 · la copia: descomposición + compensación ← AHORA, sin bloqueantes
2. C·6 · suprimir el tubo de Railway ← C·0 dice qué no perder
3. La UI de Roles: atar/desatar paquetes ← verificable en pantalla
4. Gravitino 1.3.0 (ORTOGONAL): arregla el CTAS del editor ← exige adaptar la CARA
5. Reconciliar la base de control ← desbloquea separar entornos
Lo que C·5 necesita ahora, de sus tres prerrequisitos quedan cero bloqueantes:
| ① el driver en el clúster | 🏁 | c5-0-probe-motor.ts VERDE |
| ② el transporte de la credencial sin ponerla en el SQL | 🏁 | lo resuelve CarbonJdbcConnectionProvider: por las opciones viaja carbon.face.token (corto, revocable, auditable), no la contraseña |
③ la descomposición CREATE TABLE + carga con compensación | 🟡 | la gramática está (copy-sql.ts, 16 tests); falta el ejecutor y el gate |
⭐ Y el cableado del SPI está medido sin necesitar un Postgres (c5-1-probe-spi.ts):
al proveedor se le manda carbon.connection sin token contra una url muerta, y
lanza su propio mensaje antes de conectar. Eso separa el fallo del cableado del fallo de
la conexión, que es la distinción que un gate de punta a punta no sabe hacer.
⚠️ El gate de C·5 sí necesita un Postgres alcanzable DESDE EL CLÚSTER. El de C·4 es un Docker en local, y GKE no lo ve. Es el primer paso del siguiente tramo.
⛔ Y el modo de fallo de ③ sigue igual de vivo: la issue de Gravitino documenta que un
INSERT fallido tras un CREATE deja una tabla que ni se puede borrar. La regla es la
de siempre: la duda no borra — sólo se compensa cuando el catálogo confirma que no
hay nada.
⚠️ Deuda con fecha: subir a org:admin en Clerk LIVE a lunacorunaspain y
facebookdeveloper98; si no, el webhook los bajará a viewer en cuanto alguien toque su
membresía.
5 · 📎 Los documentos de esta sesión
ingesta-gobernada-fundamentos.md | el suelo: el mapa medido, lo que hacen Snowflake/Databricks/Iceberg REST, los 5 pilares, las trampas de medición |
ingesta-gobernada-approach.md | el plan: C·0-C·6, la definición de listo (7 criterios, 5 salidos de errores medidos) y el estado de cada fase |
ingesta-c0-censo-del-tubo.md | generado: 14 capacidades del tubo con destino (CUBRE/POSPONE/ABANDONA) |
handoff-2026-08-15b-gobernanza-en-sql.md | el anterior — sus §8-§11 son de hoy |
6 · Comandos que se corren
# los gates de la ingesta
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c0-censo-del-tubo.ts
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c1-gate-conexion-securable.ts
ENABLE_ACCESS_EVENTS=true npx dotenv -e .env.local -- npx tsx scripts/ingesta/c3-gate-vending.ts
docker run -d --name pg-c4 -e POSTGRES_PASSWORD=c4secret -e POSTGRES_DB=ventas -p 55433:5432 postgres:16
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c4-gate-introspeccion.ts
# ⭐ C·5 · el motor y el cableado (los dos 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 # ¿carga el driver?
npx dotenv -e .env.local -- npx tsx scripts/ingesta/c5-1-probe-spi.ts # ¿lo USA Spark?
# el catálogo, por la CARA (nunca por el upstream: da vacío sin un solo error)
E3_TABLA=p0_bucket_clientes npx dotenv -e .env.local -- npx tsx scripts/warehouse/e3-canary-editor-gravitino.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/g13-gate-gravitino-13.ts # diferencial 1.2 vs 1.3
# en frío
npx vitest run lib/governance # 489
NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit
# la imagen del motor
gcloud builds submit --service-account=projects/trino-k8s/serviceAccounts/883735825877-compute@developer.gserviceaccount.com \
--default-buckets-behavior=regional-user-owned-bucket \
--tag europe-west1-docker.pkg.dev/trino-k8s/carbon/spark-k8s:4.1.2-carbon1 services/spark-carbon-jdbc
7 · Estado del entorno
- Prod (Vercel):
e453987— con el namespace de un nivel. Lo posterior (C·0-C·5) no está desplegado: es código de la puerta, no toca el runtime hasta que se despliegue. - Gravitino:
1.2.0. El esquema de su base ya está migrado a 1.3.0 (aditivo y compatible, verificado tras dos rollbacks). Backuppre-upgrade-gravitino-1.3.0. - Spark: CR apuntando a
spark-k8s:4.1.2-carbon1(driver Postgres + proveedor). Pod EN PIE y ambas clases cargando — §0. - GKE: clúster
trino-compute-cloneencendido, 1 nodo. ⚠️ El owner lo apaga de noche: 0 nodos ⇒503 unconditional drop overload. .env.local:LAKEHOUSE_OPAQUE_NS_WORKSPACES=*(estaba vacío y nada podía nacer en local, fail-closed) ·LAKEHOUSE_ENV=test.- Migración
20261290_connections_securable.sqlaplicada. - ~55 ficheros sin commitear del owner (rediseño de Warehouse, sidebar, integraciones). No se tocaron.