Published

HANDOFF · 2026-08-16 — la ingesta gobernada, y el día que NACER volvió a funcionar

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

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-01/1 Running con spark-k8s:4.1.2-carbon1
spark-bridge1/1 Ready · /readyz 88 ms en caliente
jars en /stackable/spark/jarspostgresql-42.7.4.jar · carbon-jdbc-provider-1.0.0.jar
E·3 canary del editorVERDE — 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 gobernanzael editor dejó de tener lista propiagrant-sql-resaltado.test.ts
🏁 La identidad, unificada hacia la SESIÓNel canje RFC 7523 queda inertesujeto.test.ts
🏁 El alta perdía a los creadores de organización5 de 8 workspaces con 0 miembros, reparadost3-restaurar-membresias.ts
🏁 NACER vuelve a funcionarel namespace multinivel era la causadesplegado e453987
🏁 C·0 censo del tubo14 capacidades con destinoc0-censo-del-tubo.ts
🏁 C·1 la conexión, un securablegate VERDE en vivoc1-gate-conexion-securable.ts
🏁 C·2 la gramática de conexiones22 testsconnection-sql.test.ts
🏁 C·3 el vending de la credencialgate VERDE en vivoc3-gate-vending.ts
🏁 C·4 la introspección del origenVERDE contra un Postgres realc4-gate-introspeccion.ts
🏁 C·5·0 el proveedor JDBC, desplegado y cargandoel motor ya habla JDBCc5-0-probe-motor.ts
🏁 C·5·1 el cableado del SPISpark instancia el proveedor y le pasa carbon.*c5-1-probe-spi.ts
🏁 C·5·② la gramática de la copia16 tests, con el test de la ambigüedadcopy-sql.test.ts
🟡 C·5·③ la copiafalta 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 createTableimportTableloadTable 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_stampque 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.mdel suelo: el mapa medido, lo que hacen Snowflake/Databricks/Iceberg REST, los 5 pilares, las trampas de medición
ingesta-gobernada-approach.mdel 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.mdgenerado: 14 capacidades del tubo con destino (CUBRE/POSPONE/ABANDONA)
handoff-2026-08-15b-gobernanza-en-sql.mdel 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). Backup pre-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-clone encendido, 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.sql aplicada.
  • ~55 ficheros sin commitear del owner (rediseño de Warehouse, sidebar, integraciones). No se tocaron.