El PIPELINE, de punta a punta — dónde estamos, medido
Para qué es esto. Levantar la vista después de tres frentes seguidos (el cutover del
catálogo, la política en el plan, la robustez de escritura) y contestar una sola pregunta:
¿qué partes del camino de lectura y de escritura están cerradas, y cuáles siguen
abiertas?Regla de la casa: cada hecho lleva el comando que lo demuestra. Lo que no lo lleva va
marcado ⏳ u opinión. Y lo que aquí se afirma se volvió a medir el 08-14/15 — no se
copió de los approaches, que es de donde vienen los tres errores corregidos abajo.
0 · Lo que hay que saber primero
- 🏁 El build de producción, arreglado (
e656679): faltaba importarSecurityenPlatformSidebar.tsx. Una línea, cuatro deploys enErrory un diagnóstico largo — la lección de método, en §3. ⚠️ Producción sólo sirve lo nuevo cuando hay un deploy de Production en verde: unReadyde Preview no cuenta. - 🏁 La lectura está gobernada de punta a punta: el motor delega el plan en el catálogo, la política viaja dentro, y la credencial ya no la esquiva.
- 🏁 La escritura resultó estar más entera de lo que decía su inventario: el reintento OCC existía —heredado— y ahora está atado y declarado.
- ⛔ Pero la autorización no llega al OBJETO, y eso es lo que encarece todo lo demás.
- ⛔ La Action atómica sigue siendo imposible: sin commit multi-tabla y sin transacción multi-sentencia, «escribe A y B, o nada» no se puede sostener.
1 · El camino de LECTURA
| evidencia | ||
|---|---|---|
| 🏁 | El motor delega la planificación en el catálogo (scan-planning-mode: server, canary por workspace) | s2-gate-delegacion.ts · el contador de Plan table scan sube |
| 🏁 | La cara completa el plan (plan-tasks → file-scan-tasks), todo-o-nada | scan-plan.ts · 17 tests |
| 🏁 | La política de fila viaja en el plan y cambia lo que el usuario ve | s3-gate-politica-en-el-plan.ts |
| 🏁 | El vending está conmutado: sólo recibe llave quien pasa por el plan | s4-gate-vending-conmutado.ts |
| 🏁 | El requisito de partición es conocido y comprobable, y hay censo | p0/p0b/p1/p2 |
| ⛔ | La máscara de columna NO es aplicable en el plan ⇒ deniega | medido: el select no recorta el esquema |
| 🏁 | La puerta contra un tercero, CERRADA en vivo: con política, una persona no recibe credencial y el motor sí | k3-gate-s4-en-vivo.ts |
Lo que falta en la lectura, por orden de daño:
- ⭐⭐ La máscara de columna no tiene camino. Hoy una tabla con
column_masksimplemente no se sirve. Es la mitad de la política de celda que sigue sin punto de aplicación, y la única salida es aplicarla en el motor (la «salida A»). - ⭐ Un segundo inquilino con datos. Todo el aislamiento está probado con uno solo: el control cruzado sólo puede decir «no existe ahí», no «existe y no lo ves».
- ⭐⭐ La identidad de Keycloak no está unida a la de Clerk: el
subdel realm no es eluser_…de los miembros de workspace. Una persona autenticada es alguien REAL y sin grants — sirve para medir la puerta, todavía no para que vea sus tablas.
2 · El camino de ESCRITURA
2·1 · Lo que está cerrado
| evidencia | ||
|---|---|---|
| 🏁 | Grants de usuario derivados de la membresía, USER_AUTHZ=enforce vivo | /api/diag/motor · medido hoy |
| 🏁 | El reintento OCC funciona — heredado del cliente Iceberg, la cara no lo rompe | 12 escritores · 34 conflictos reales · 12/12 |
| 🏁 | Las 7 propiedades de robustez, declaradas en la tabla (4 de reintento + 3 de aislamiento) | 105/107 tablas · las nuevas nacen con ellas |
| 🏁 | createTable arreglado (G·1) — crea, sin huérfanas, y admite datos | w8-create-sin-ctas.ts |
| 🏁 | El destino viaja aparte del SQL (writeTarget autorizado fuera de banda) | — |
| 🏁 | Idempotencia con huella de SQL+destino, y ledger con el snapshot observado | lo mejor construido del repo |
2·2 · Lo que sigue abierto, por orden de daño
| estado | ||
|---|---|---|
| ⛔⛔ | La autorización no llega al OBJETO | Medido hoy: principal_grants = 389 workspace + 24 schema + 0 de tabla. Hay 16 TABLE_WRITE_DATA… todos de ámbito workspace. ⇒ la política discrimina por rol de workspace, no por persona sobre tabla |
| ⛔⛔ | No hay Action atómica | Verificado hoy: /transactions/commit no existe (404 en 3 variantes · 0 endpoints con «transaction»). Y una sentencia por ejecución. ⇒ «escribe A y B, o nada» es imposible |
| ⛔ | El CTAS sigue roto (stage-create + hook importTable) | Salida decidida: CREATE + relleno. Se pierde la atomicidad |
| ⛔ | S·5·③: el reintento tras un 500 no tiene gate | La cara ya pregunta al catálogo en vez de inferir del !res.ok, pero no está medido que no duplique |
| ⚠️ | El fallo parcial se reconcilia A MANO (ControlPlaneUnrecordedError) | compensate es corrección visible, no reintento automático |
| ⚠️ | El bridge comparte UNA sesión de Spark Connect | Observado: al romperse se llevó 5 escrituras en el mismo milisegundo, y una de ellas había escrito pese a reportar error. No reproducible en 3 intentos |
| 🟡 | INSERT … VALUES vetado con motivo caducado | Su fuente sí se resuelve, medido en frío |
| 🟡 | dmlVerb decide por la primera palabra del texto | Ya se sortea preguntando al plan, pero la función sigue decidiendo en el resto de caminos |
3 · ⭐ Las tres cosas que este repaso CORRIGIÓ
Las tres estaban escritas en documentos vigentes, y las tres eran falsas o imprecisas. No es casualidad: es el coste de arrastrar approaches escritos antes de poder medir.
| decía | lo medido | |
|---|---|---|
| ⭐⭐⭐ | «No hay ningún bucle de reintento ⇒ una de dos escrituras concurrentes se la come el usuario» | El reintento existe, heredado del cliente Iceberg, y la cara pasa el 409 íntegro. Se estuvo a punto de construir uno al lado |
| ⭐⭐ | «La política sólo protege si el filtro se resuelve por poda — tabla particionada u ordenada» | Podar no es lo que protege. Sin partición se poda igual y queda residuo; bucket también. Sólo identity resuelve |
| ✅ | «Se perdió el commit multi-tabla» | Cierto — verificado contra el servidor, no contra la lista de endpoints, que ya mintió una vez con /scan |
🪤 Y una cuarta, de método, que costó cuatro deploys
El build de producción moría con ReferenceError: Security is not defined mientras
npm run build en local PASABA. Se persiguió durante cuatro intentos —se descartó la
versión del paquete, el lock, ficheros que faltaran, la caché de webpack, el prerender— y la
causa era un import que faltaba en PlatformSidebar.tsx. Una línea.
⭐⭐ El build local mentía porque el árbol local tenía trabajo SIN COMMITEAR que ya lo arreglaba, y el deploy sale de git. «En local pasa y en Vercel no», con el árbol sucio, no es una paradoja: es que no se estaba construyendo lo mismo.
🪤 Y lo que lo alargó: se miró git status | grep -v "^ M components" para quitar ruido, y
ese filtro escondió justo el fichero culpable. Filtrar la lista de cambios mientras
buscas una diferencia entre dos árboles es quitarte la respuesta de delante.
⇒ Regla: ante «en local funciona y en el deploy no», lo primero es git status, entero.
4 · ⏭️ El orden que se deduce AHORA
🏁 arreglar el BUILD ← hecho (e656679)
🏁 el SUJETO humano existe ← hecho: Keycloak respalda, la cara distingue persona
de servicio, y S·4 quedó cerrado en vivo
① FEDERAR Keycloak ↔ Clerk ← sin esto, una persona autenticada no es NADIE conocido
② alcance de OBJETO en los grants ← depende de ①: hoy no discriminaría a nadie (G·0)
③ la máscara de columna, en el motor ← la mitad de la política de celda que no tiene camino
④ S·5·③: reintento tras 500 sin duplicar
⑤ la sesión compartida del bridge ← un fallo suyo se lleva TODAS las escrituras en vuelo
⑥ Action atómica ← ya no es «estrenar» una capacidad: hay que reconstruirla
⚠️ ② dejó de ser el primero, y por una medición. G·0 midió que el ámbito de objeto no
discriminaría a nadie hoy: los grants se derivan de workspace_members (12 sujetos de
Clerk) y el único creador humano es el owner, que ya lo recibe todo. Sigue siendo la
condición que abarata el futuro —mientras la autorización sea de workspace, cada verbo que se
abra se abre para todo el mundo a la vez—, pero primero hace falta ①, o se construye
una retícula sin nadie dentro.
⭐ Y ① no es fontanería de identidad: es lo que convierte a una persona en sujeto de
gobierno. Hoy el sub de Keycloak es alguien real y sin grants; el user_… de Clerk tiene
membresía y no puede hablarle al catálogo. Unirlos es lo que hace que todo lo construido
—política en el plan, vending conmutado, partición por columna— le aplique a una persona.
⚠️ ⑥ cambió de naturaleza con el cutover. Cuando el inventario lo listó, la capacidad
estaba «comprada y sin estrenar» (Lakekeeper exponía /transactions/commit). Gravitino no la
expone ⇒ ahora hay que construirla o suplirla, y eso es otro tamaño de problema.
5 · Comandos que se corren
# el runtime — NUNCA la consola de Vercel
curl -H "x-diag-key: $SPARK_BRIDGE_JWT_SECRET" https://app.paladio.io/api/diag/motor
# la lectura gobernada
npx dotenv -e .env.local -- npx tsx scripts/warehouse/s3-gate-politica-en-el-plan.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/p1-gate-particion-gobernada.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/p2-censo-servibilidad.ts
# la escritura robusta (el gate necesita kubectl para contar los conflictos)
S5_ESCRITORES=12 npx dotenv -e .env.local -- npx tsx scripts/warehouse/s5-gate-reintento-occ.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/s5d-barrido-robustez.ts # --aplicar
# en frío
npx vitest run lib/governance # 331 verdes
npx tsc --noEmit # limpio