HANDOFF · 2026-08-14 (cierre) — el catálogo migrado, y lo que falta
Para qué es esto. La sesión del 08-14 cerró el cutover del catálogo de punta a punta:
el SQL Editor lee por Gravitino y el camino a Lakekeeper está suprimido. Este
documento es la foto autosuficiente para retomar.⚠️ SUPERSEDE a
handoff-2026-08-14.md, que era la foto de la
mañana. Aquél sigue valiendo para la ingesta y la gobernanza (§3 y §4); para el catálogo,
manda éste.Regla de la casa: cada hecho lleva el comando que lo demuestra. Lo que no lo lleva va
marcado ⏳ u opinión.Entregable vivo:
sql-editor-cutover-approach.md·
irc-cutover-approach.md.
0 · Lo que hay que saber en diez líneas
- 🏁 El SQL Editor lee por Gravitino. Verificado por el camino del producto
(
runQuery), con control negativo. - 🏁 El camino a Lakekeeper está SUPRIMIDO — no apagado: la bandera, las ramas y el canary de la bandera ya no existen.
- 🏁 104 tablas re-registradas (de 174 censadas). Las 70 restantes son huérfanas: están en el catálogo viejo y no en Index. No se migraron a propósito.
- 🏁 El vending temporal es nuestro: provider
r2-tokenpropio, JWT local, cero red, acotado al prefijo de la tabla. - 🏁 El aislamiento entre inquilinos, arreglado y comprobado EN CRUZ — y era falso antes.
- 🏁 El catálogo está expuesto y acotado:
catalog.paladio.io, 401 sin token, 200 la cara, 403 cualquier otro cliente. - ⚠️ Queda UNA dependencia de Lakekeeper, con nombre propio: el alta de inquilinos.
- ⚠️ Lo que está roto es la CREACIÓN de tablas, no la escritura (medido 08-14 tarde).
El CTAS falla con un 500 de Gravitino (su hook
importTable, §3③) — pero el DML sobre tabla existente FUNCIONA:UPDATEyDELETEcommitean de verdad contra el catálogo nuevo, firmados (carbon.operation-iden el summary) y con el CAS intacto. ⛔INSERTestá vetado por diseño en Junction, no por el catálogo. - ⚠️ Otros motores siguen en Lakekeeper: Trino, duck-server, warehouse-writer.
- ⏸️ TODO SUSPENDIDO al cerrar (catálogo incluido) ⇒ el SQL Editor NO funciona hasta
levantarlo:
catalog.paladio.ioda 503. Los comandos, en §6.
1 · El estado, exacto
1·1 · El catálogo
UNA pieza: gravitino-server 1/1 (el IRC PLEGADO dentro, como servicio auxiliar)
/api :8090 servicio gravitino-server
/iceberg :9001 servicio gravitino-irc ⇐ el NOMBRE viejo, repuntado
externo: https://catalog.paladio.io cert ACTIVE · HealthCheckPolicy TCP
almacén: carbon-catalog (Cloud SQL, IP privada 10.99.0.3) — entidades + iceberg_tables
metalake: `carbon`, dueño `carbon-catalog-face`
catálogos: 9 · todos con `credential-providers = r2-token` y `catalog-backend-name` propio
1·2 · ⏸️ TODO SUSPENDIDO al cerrar la sesión
gravitino-server (ns catalog) 0/0 ⇐ el catálogo, con el IRC dentro
spark-bridge (ns spark) 0/0
carbon-connect clusterOperation.stopped = true
nodo: 221m CPU (5%) · 3.129Mi (23%) ⇐ sólo operadores de Stackable y kube-system
⚠️⚠️ ESTO ROMPE EL SQL EDITOR, y ahora de dos maneras. Antes bastaba con que Spark
estuviera apagado; desde el cutover, el catálogo también está en el camino de
producción: con gravitino-server a 0, catalog.paladio.io devuelve 503 y la cara no
tiene a quién delegar. Levantar los dos (§6) antes de tocar el editor.
⚠️ Suspender NO ahorra dinero: el nodo se paga igual. Libera CPU y memoria.
🪤 Al medir justo después de suspender, el nodo marcaba 56 % de memoria — más que encendido. Era caché sin liberar: 20 s después, 23 %. Una métrica tomada en el instante del cambio mide el cambio, no el estado.
1·3 · Credenciales nuevas
Keycloak (realm lakehouse) | cliente carbon-catalog-face — client auth ON, standard flow OFF, service accounts ON |
Vercel (node-jqzh, Prod + Preview) | GRAVITINO_CATALOG_URI, GRAVITINO_CATALOG_CREDENTIAL |
Cluster (ns catalog) | Secret carbon-catalog-face (lo usan los Jobs de migración) |
| ⛔ Retirada | CATALOG_UPSTREAM — la bandera del cutover, borrada de código, Vercel y .env.local |
⚠️ El secreto del cliente se pegó en el chat de la sesión. Conviene rotarlo.
2 · Lo hecho, con su gate
| Gate | ||
|---|---|---|
| 🏁 B·0 | la temporal de R2 por JWT local | 200 con token · 403 fuera del prefijo · caducada rechazada |
| 🏁 B·1 | el CredentialProvider en Java (8 KB, cero dependencias) | 20/20 en frío, gate dentro del build |
| 🏁 B·2/3/4 | provider desplegado, Spark escribe y lee | s3.session-token · ventana 900 s · paridad superada |
| 🏁 C·1·b | el aislamiento, arreglado (catalog-backend-name) | control CRUZADO: crear en A → B da 404/404 |
| 🏁 C·3 | 104 tablas re-registradas | leídas por el IRC con credencial temporal · vecino 404 |
| 🏁 D·0…D·7 | servidor Gravitino, catálogos por API, IRC plegado, auth, exposición | 401 / 200 / 403 desde internet |
| 🏁 E·2/E·3 | la cara habla con Gravitino · el editor lee | SELECT … LIMIT 5 → 3 filas + control negativo |
| 🏁 E·4 | camino a Lakekeeper suprimido | 259 tests · tsc limpio · canary verde tras desplegar |
3 · ⏭️ LO SIGUIENTE — los tres que quedan
⭐⭐⭐ ① El ALTA por API — la última referencia a Lakekeeper
Dónde: lib/storage/tenant-warehouse.ts · lakekeeperManagementToken() +
provisionTenantWarehouse().
Hoy el alta de una organización crea su warehouse en Lakekeeper. Debe crear su catálogo en Gravitino:
POST /api/metalakes/carbon/catalogs
{ name: "w_<hex>", type: "RELATIONAL", provider: "lakehouse-iceberg", properties: {...} }
Ya está probado que funciona (D·3: catálogo creado por API → servido sin reiniciar, vendiendo temporal y aislado del vecino). Lo que falta es el llamante.
⚠️ Cuatro cosas que ya sabemos y evitan un día de depuración:
- Las propiedades se pueden clonar de un catálogo vecino, pero hay que quitar
in-use(derivada; el POST devuelve 400 sin mensaje) y forzarcredential-providers=r2-tokenycatalog-backend-name = <nombre>(sin lo segundo, se pierde el aislamiento). - El servidor exige token y sólo autoriza al dueño del metalake ⇒ el control plane
necesita la credencial de
carbon-catalog-face. ⛔ Vercel no alcanza el/api(8090): hoy sólo está expuesto el/iceberg. Hay que exponerlo también, con auth. - Un catálogo nuevo aparece al instante; uno modificado tarda hasta 1 h
(
expireAfterAccess). - Gravitino no borra un catálogo «en uso»: deshabilitar antes (
PATCH {inUse:false}).
⏭️ Decisión pendiente: ¿escritura doble (Lakekeeper y Gravitino) mientras haya motores en el viejo, o sólo Gravitino? Hoy Trino, duck-server y warehouse-writer siguen allí.
⭐⭐ ② Las 70 huérfanas
Están en Lakekeeper y no en Index (datasets). No se migraron: un re-registro es la
única ocasión barata de no arrastrar lo muerto. Listado con
c3d-reregistrar.ts (dry-run) o c3c-dueno-en-index.ts.
⏭️ Decidir: retirarlas, o investigarlas antes (fechas, tamaño, si alguien las usa). Ya hay
doctrina en orphaned-retirement-dedup.
⭐⭐⭐ ③ El camino de ESCRITURA
⛔ La escritura sigue SIN verificar contra el catálogo nuevo. Lo medido es lectura. Pero desde el 08-14 (tarde) el frente ya no está a oscuras: la pregunta previa está contestada y el gate está escrito y validado en seco. Falta correrlo.
✅ MEDIDO · por dónde obtiene sus bytes un escritor. Era la incógnita que bloqueaba empezar, y la respuesta estaba en el código, no en el aire:
- El escritor los obtiene por
loadTablecon delegación, como el lector. La spec de Iceberg REST no tiene forma de declarar intención de escritura, así que la cara pidevendCredential({mode: 'auto'})y el modo lo decide el PRIVILEGIO: quien tieneTABLE_WRITE_DATArecibeobject-read-write(route.ts:742-749,credential-vending.ts:50-71). No hay «otra vía» que buscar. - ⚠️ Lo que estaba rancio era el aviso:
mintTableCredentialno es que no venda para escribir — no tiene llamante en producción, para nada. La sustituyó la firma JWT local de P4-bis · F2. Comentario corregido enstorage-vending.ts. - El commit no lo ejecuta la cara:
updateTablese autoriza y se reenvía íntegro — el CAS es del catálogo. Con Gravitino sigue siéndolo. - El write path W0-W5 está desplegado y atraviesa la cara.
🏁 EL GATE, escrito y CORRIDO: scripts/warehouse/w6-canary-escritura-gravitino.ts —
CTAS por runQuery + lectura de vuelta + control CRUZADO. Lleva --dry, que valida el
instrumento sin escribir; en seco dio verde (103 tablas en el nuevo, 149 en el viejo).
⛔ EL RESULTADO: lo que no funciona es CREAR, no escribir
⚠️ Corrección de la primera lectura de esta sesión. Al ver el CTAS fallar se escribió
aquí que «la escritura no funciona». Es demasiado amplio, y lo desmintió la batería
w7-bateria-lectura-escritura.ts: sobre tabla existente, UPDATE y DELETE pasan por
runQuery y commitean de verdad contra Gravitino. El log del catálogo lo prueba —no el
código de estado, que aquí engaña:
16:29:04 updateTable … "operation":"overwrite" → Successfully committed
16:29:10 updateTable … "operation":"delete" → Successfully committed
Y trae lo que hacía falta comprobar: carbon.operation-id + carbon.principal en el
summary (el commit va FIRMADO, W2·D6) y assert-ref-snapshot-id con el entero de 64
bits sin mutilar (el parseLossless cumple).
⇒ El camino de escritura ATRAVIESA la cara y llega al catálogo nuevo. Lo que está roto
es exclusivamente createTable. Lo de abajo es ese defecto, y sólo ese.
⛔ Aparte, INSERT no llega ni al catálogo: lo veta Junction
(DML_VERBS_PERMITIDOS = {DELETE, MERGE, UPDATE}) por una limitación real —un
INSERT … SELECT cualifica el destino pero no la fuente, así que el motor recibiría un
nombre lógico—. ⚠️ Pero el veto es más ancho que su motivo: un INSERT … VALUES no
tiene fuente que resolver y cae igual.
🪤 Y el mensaje de rechazo mezcla dos causas: el prefijo «El DDL de esquema está
admitido, pero no se pudo aplicar en este despliegue» sale de diagAlter.porque, que tiene
valor siempre que PLAN_MANDA esté apagado, sea cual sea el verbo — así que un INSERT
recibe un texto sobre DDL de esquema. Justo lo que el comentario de al lado prohíbe: «un
mensaje sólo es honesto si su primera línea ya es la verdad» (junction.ts:2041).
El defecto de la CREACIÓN — y la culpa no es de la cara
El CTAS por el camino del producto falla:
petición → namespace: prod.w_<hex> · stageCreate=true
error → 500 NoSuchTableException: Table does not exist: prod.ds_<hex>
Dos pistas en esa línea: el namespace de la petición tiene dos niveles y el de la
resolución interna tiene uno, y quien falla no es el createTable sino lo que viene
después. El stack del servidor lo confirma:
IcebergTableHookDispatcher.createTable(:60) → importTable(:185) → loadTable → 💥
Gravitino crea la tabla y acto seguido la relee para importar la entidad al metalake.
Ese loadTable es el que revienta y convierte un éxito en un 500.
La matriz, medida (w6c-stagecreate-vs-multinivel.ts, reproducible y autolimpiante):
stage-create | namespace | HTTP | ¿queda tabla? |
|---|---|---|---|
| true | multinivel | 500 | no — es lo que hace el SQL Editor |
| false | multinivel | 500 | ⚠️ SÍ, se crea igual |
| true | un nivel | 500 | no |
| false | un nivel | ✅ 200 | sí |
⇒ DOS defectos independientes de Gravitino 1.2.0:
- A · el namespace multinivel se APLANA en el
loadTablede post-proceso: se pideprod.w_<hex>.<tabla>y se releeprod.<tabla>. La creación funciona igual ⇒ ⚠️ un 500 de este catálogo NO significa que no se haya escrito. Deja tablas detrás de un error. - B ·
stage-createes incompatible con el hook: una tabla staged no existe hasta el commit, así queimportTableno puede releerla jamás. Falla incluso con namespace de un solo nivel, donde no hay nada que aplanar.
El CTAS de Spark usa stage-create: true sobre el namespace opaco ⇒ choca con los dos a
la vez. Por eso ninguna cantidad de trabajo en la cara lo arreglaría.
✅ Lo que sí quedó probado: el fallo es limpio. No dejó fila fantasma en Index (el rollback W4·3 hizo su trabajo) ni tabla huérfana en el catálogo, verificado tras la corrida.
🏁 G·1 · el arreglo, implementado y sin desplegar
⛔ Antes, dos cosas medidas que descartan lo fácil: el hook no se puede desactivar
(RESTService lo cablea sin condición — comprobado abriendo el jar) y no era sólo el
CTAS: un CREATE TABLE (cols) pelado falla igual, y ⚠️ dejaba una HUÉRFANA por
intento (la tabla se creaba, la cara compensaba la fila de Index y quedaba sin dueño).
Lo que hace: cuando createTable falla, la cara le pregunta al catálogo si la tabla
existe en vez de compensar por !res.ok. Si existe → conserva el gobierno y responde
200 con el LoadTableResponse real. Si el catálogo confirma 404 → compensa como antes.
Si no se puede preguntar → no borra (la duda no borra).
⭐ Es la disciplina que T·1·a ya tenía escrita treinta líneas más abajo para updateTable:
«no se infiere del !res.ok: se PREGUNTA al catálogo». Estaba, sin aplicar aquí.
Gate w9-gate-del-arreglo-g1.ts VERDE, y valida la premisa de la que depende todo:
createTable → 500 · GET de esa tabla → 200 con metadata-location. Es decir,
loadTable no aplana. Si aplanara, la cara borraría el gobierno de tablas existentes.
⚠️ Sin desplegar (la cara corre en Vercel; Spark habla con app.paladio.io).
⛔ No arregla el CTAS: un stage-create no crea nada, así que no hay tabla que hallar.
⏭️ Las salidas para el CTAS, por coste creciente — ninguna medida todavía:
- Desactivar el hook de importación del IRC (si es configurable) — el
importTablees quien sobra; sin él, ② y ④ pasarían. - Evitar el
stage-createen el camino de escritura, si Iceberg/Spark permite un CTAS no atómico. ⚠️ Pierde atomicidad: un CTAS a medias deja tabla vacía. - Subir de versión Gravitino — pero ⚠️ el defecto A parece de diseño del hook, no un bug de esta build. Comprobar antes en su repo.
npx dotenv -e .env.local -- npx tsx scripts/warehouse/w6c-stagecreate-vs-multinivel.ts
4 · 🪤 Las trampas medidas — para no repetirlas
| ⛔⛔ | Encender la autenticación rompe TODO chequeo anónimo, y en cada piso: el pod se quedó 0/1 (readinessProbe a /api/version → 401) y el balanceador dio 503 (health check GET / anónimo). Los dos síntomas apuntan a la red y la causa es el permiso. Probar por TCP |
| ⛔⛔ | Un loadTable en 200 puede estar sirviendo la LLAVE LARGA. Síntoma mudo: el cliente funciona igual. Sólo el log lo decía (Generate credential: s3-secret-key) |
| ⛔⛔ | rewrite_gravitino_server_config.py CONSERVA lo que no conoce y PISA lo que sí. gravitino.authorization.* está en su env_map ⇒ las líneas del conf se reescribían con los defectos sin un solo error. Van por ENV |
| ⛔⛔ | Al plegar el IRC CAMBIA EL PREFIJO de config: suelto lee gravitino.iceberg-rest.*; plegado, sólo gravitino.auxService.*. Con el prefijo viejo arrancó con sus DEFECTOS (backend memory, warehouse /tmp) y devolvió 404 a todo |
| ⛔⛔ | Un 500 del catálogo NO significa que no se haya escrito. Gravitino crea la tabla y falla al RELEERLA (importTable), así que devuelve error habiendo persistido. Medido: stage-create:false en namespace multinivel → 500 y tabla creada. ⇒ Un reintento «porque falló» duplica; y una limpieza que borre sólo lo que devolvió 200 deja basura. Comprobar lo que EXISTE, no lo que contestó |
| ⛔⛔ | Un censo heredado puede devolver 0 SIN UN SOLO ERROR, y los dos catálogos no se dejan recorrer igual. El recorrido de c3c/c3d descubre namespaces con ?parent=<ns>: contra Lakekeeper saca 174 tablas; contra Gravitino devuelve el propio namespace (?parent=prod → [["prod"]]), así que nunca baja y cuenta 0. Se estrenó en el canary de escritura y el rojo parecía «la escritura va al viejo». Los namespaces no se descubren: se le preguntan a Index (datasets.iceberg_namespace), que es la coordenada con la que la cara reenvía |
| ⛔ | Las tablas de un inquilino NO viven todas en el mismo namespace: 7 en prod.w_<hex> (el paradigma opaco) y 84 en main.default, de antes. Un censo que sólo mire la coordenada nueva se deja fuera a la mayoría — incluida bronze_clientes, la tabla del canary de lectura |
| ⚠️ | El separador de niveles del namespace en la URL es 0x1F, no un punto — y es invisible al copiarlo. main.default viaja como main%1Fdefault; copiado a ojo, se convierte en la cadena vacía y el censo mira el namespace equivocado |
| ⛔ | El libs/ del servicio auxiliar NO es el del IRC suelto (falta gravitino-aws). Auth, authz y namespaces se veían perfectos; listar tablas reventaba |
| ⛔ | kubectl port-forward deploy/… durante un rollout se engancha al pod que aún TERMINA ⇒ mides la config vieja |
| ⛔ | La imagen del IRC no trae unzip; el sh de esa imagen no tiene /dev/tcp. Censar con el mecanismo del consumidor |
| ⚠️ | Un curl a la cara da 403 aunque todo esté bien: no reproduce su autorización por usuario. Canarizar con runQuery |
| ⚠️ | Clonar ≠ reenviar el GET: trae propiedades derivadas (in-use) que el POST rechaza con 400 sin mensaje |
| ⚠️ | El IRC cachea el wrapper por NOMBRE (1 h): nuevo = instantáneo, modificado = hasta una hora |
| ⚠️ | vercel --prod embarca el ÁRBOL, no el commit — el despliegue vivo salió de un árbol con 128 ficheros modificados, 99 ajenos |
| ⚠️ | Un · en un JSON por shell puede llegar como Invalid UTF-8 start byte |
5 · ⭐ Las reglas nuevas (sustrato-07 §13, nº 80-91)
- 80 Un control negativo que sólo mira «¿no fue 200?» no controla nada.
- 81 Cuando algo no pasa, separa tu mitad de la suya (pieza legítima ajena, maquinaria tuya).
- 83 Lo que despacha por CLASE no atiende a tu nombre — copia la clase que el consumidor reconoce.
- 84 Censa con el mecanismo del consumidor, no con el que tengas a mano.
- 85 «Desplegado» no es «sirviendo lo desplegado».
- 86 No confundas el ámbito que pones con el que te dan.
- 87 Enrutar no es aislar, y un 404 de control no distingue las dos cosas.
- 88 Antes de extender, pregunta cómo lo resuelve el sector — escribir la extensión era el rodeo.
- 89 Un 200 no dice qué lleva dentro.
- 90 La fuente que copias puede no haber tenido nunca el valor bueno.
- 91 Encender la autenticación rompe todo chequeo anónimo — y en cada piso.
6 · Comandos que se corren
# en frío
npx vitest run lib/governance lib/storage # 259 verdes
npx tsc --noEmit
# el gate del cutover (necesita Spark encendido)
E3_TABLA=bronze_clientes npx dotenv -e .env.local -- \
npx tsx scripts/warehouse/e3-canary-editor-gravitino.ts
# el catálogo, desde internet (401 sin token es la respuesta CORRECTA)
curl -s -o /dev/null -w '%{http_code}\n' \
'https://catalog.paladio.io/iceberg/v1/config?warehouse=lakehouse'
# censos de la migración (sólo leen)
npx dotenv -e .env.local -- npx tsx scripts/warehouse/c3-censo-lakekeeper.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/c3c-dueno-en-index.ts
# ⏸️→▶️ LEVANTAR lo suspendido — LOS DOS, o el editor no funciona
kubectl scale deploy/gravitino-server -n catalog --replicas=1 # el catálogo (503 sin esto)
kubectl scale deploy/spark-bridge -n spark --replicas=1
kubectl patch sparkconnectserver carbon-connect -n spark --type=merge \
-p '{"spec":{"clusterOperation":{"stopped":false}}}'
# y comprobar ANTES de dar nada por bueno (401 = vivo y pidiendo credencial):
curl -s -o /dev/null -w '%{http_code}\n' \
'https://catalog.paladio.io/iceberg/v1/config?warehouse=lakehouse'
# ▶️→⏸️ SUSPENDER (así quedó al cerrar la sesión)
kubectl scale deploy/gravitino-server -n catalog --replicas=0
kubectl scale deploy/spark-bridge -n spark --replicas=0
kubectl patch sparkconnectserver carbon-connect -n spark --type=merge \
-p '{"spec":{"clusterOperation":{"stopped":true}}}'
⚠️ El orden importa al levantar: el catálogo primero — Spark sin catálogo no resuelve nada y los errores apuntan al motor.
💰 Para AHORRAR de verdad: el pool a 0 (no basta con suspender)
⚠️⚠️ Suspender workloads NO ahorra un céntimo: el nodo se paga igual. Y escalar los
operadores a 0 tampoco sirve — medido: kube-system tiene 8 Deployments propios de
GKE (kube-dns, konnectivity-agent, metrics-server, l7-default-backend…) que anclan
el nodo igualmente, y gmp-operator vuelve solo porque es un addon del cluster.
⇒ El único mecanismo es forzar el pool. Y hay que quitar el autoscaling ANTES, o el
autoscaler ve los pods Pending y vuelve a levantar un nodo — deshaciendo el ahorro sin
avisar:
# APAGAR (≈150-160 $/mes del nodo)
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
# ENCENDER — los workloads siguen a 1, así que arrancan solos al haber nodo
gcloud container clusters resize trino-compute-clone --node-pool main-pool-4 \
--num-nodes 1 --region europe-west1 --project trino-k8s --quiet
gcloud container clusters update trino-compute-clone --enable-autoscaling \
--node-pool main-pool-4 --min-nodes 0 --max-nodes 3 --region europe-west1 --project trino-k8s
⚠️ currentNodeCount del cluster va con retraso: para saber si de verdad está a 0,
mirar kubectl get nodes o las instancias de Compute.
Lo que NO se ahorra (sigue facturando con el nodo a 0): la cuota de gestión del cluster
GKE (regional ⇒ ~73 $/mes), el load balancer + IP estática carbon-bridge-lb, el Cloud NAT
carbon-nat, y Cloud SQL carbon-catalog — donde vive el catálogo, que por eso
sobrevive al apagado. ⇒ Se va algo más de la mitad, no toda.
⏭️ Si molesta que el editor caiga con el cómputo: dos pools — uno pequeño siempre
encendido (e2-small) para catálogo y sistema, y el n2-standard-4 a 0 para Spark. Hoy es
todo-o-nada porque hay un solo pool.
7 · Los artefactos de esta sesión
Infra (infra/gke/): b2-r2-credential-provider · b3-vending-e2e · b3b-censo-catalog-name ·
d0-gravitino-server · d0b-gravitino-schema · d1-catalogos-por-api · d1b-forzar-provider ·
d2-irc-dinamico · d3-alta-sin-reinicio · d5-gravitino-plegado (el vivo) · d7-catalog-expuesto
El jar: infra/gravitino-r2-credential/ — provider de R2, con su gate en el build.
Sondas (scripts/warehouse/): b0-jwt-r2-temporal · b0b-que-vende-cloudflare ·
b1-vector-dorado · b2-vending-temporal · c1b-aislamiento-cruzado · c3-censo-lakekeeper ·
c3b-mapa-namespaces · c3c-dueno-en-index · c3d-reregistrar · e3-canary-editor-gravitino ·
w6-canary-escritura-gravitino (el gate de ③, con --dry) · w6b-sonda-prefijo (dónde
vive cada tabla, y por qué un censo «obvio» cuenta 0)
Código: lib/governance/catalog-upstream.ts (E·4) · lib/storage/tenant-warehouse.ts
(prefijo derivado + lakekeeperManagementToken) · lib/governance/storage-vending.ts (comentario
corregido) · tests de catalog-upstream.