Published

⭐⭐⭐ El MAPA DE POSICIÓN — Index Catalog como plano de gobernanza unificado

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

⭐⭐⭐ El MAPA DE POSICIÓN — Index Catalog como plano de gobernanza unificado

Qué es este documento. No es un plan ni un approach: es una definición de
hecho
. Para cada concepto de gobernanza que la industria ya tiene resuelto,
dice cuatro cosas y siempre las mismas:

① EL MODELO      · quién es la referencia, qué vocabulario tiene, cuál es su madurez
② LA DERIVACIÓN  · de qué dato NUESTRO sale — si no sale de ninguno, es una declaración
③ DÓNDE ESTAMOS  · medido, con el comando que lo demuestra
④ LA AUTORIDAD   · quién puede decir que NO, y dónde

Abierto: 2026-08-12. Es la base sobre la que giran
plano-gobernado-approach.md (el plan) y
sustrato-07.md (la foto). Donde discrepen, el sustrato manda
en los hechos y esto manda en el encuadre
.


1 · La tesis, en tres frases

① Un solo sitio donde la gobernanza se DEFINE: Index.
② N sitios donde se APLICA: la cara, la puerta y el motor.
③ Cero sitios donde se COPIE.

Es literalmente el movimiento de Unity Catalog —govern once, protect everywhere— y también el de Lake Formation, Polaris y Gravitino. La diferencia no está en la idea: está en de qué deriva cada pieza. Y ahí es donde se gana o se pierde.

1·1 · ⚠️ Y una precisión que cambia lo que significa «copiar UC OSS»

Las políticas ABAC de Unity Catalog —row filter, column mask, governed tags— son del producto GESTIONADO de Databricks, no del proyecto open source. Toda su documentación vive en docs.databricks.com, no en el repo OSS.

No hay implementación que instalar. Lo que hay es un modelo público, maduro y GA (mayo 2026) que se puede estudiar entero. Es decir: el §2 del approach —se toma el modelo, se deriva el sustrato, se evita el producto— aquí no es una preferencia, es la única opción disponible.


2 · Los cinco PLANOS DE AUTORIDAD

La pregunta «¿cuál es la naturaleza de su autoridad?» no tiene una respuesta: tiene cinco, y confundirlas es el origen de la mayoría de los errores de diseño.

┌─ ① IDENTIDAD ─────────────────────────────────────────────────────────┐
│  QUIÉN ERES.  Clerk (humanos) · Keycloak (servicios)                   │
│  Autoridad EXTERNA y no negociable: el catálogo la referencia,         │
│  no la administra.  ⇒ «no modifiques los grupos aquí: usa tu IdP»      │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ② RETÍCULA (grants) ─────────▼───────────────────────────────────────┐
│  QUÉ OBJETOS PUEDES TOCAR.  Index · `privilege_grants`                 │
│  Autoridad DECLARATIVA y ADITIVA: concede, nunca deniega.              │
│  Grano GRUESO. Es la frontera del objeto, no de su contenido.          │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ③ POLÍTICA (celda) ──────────▼───────────────────────────────────────┐
│  QUÉ VES DENTRO.  filtro de fila · máscara · proyección                │
│  Autoridad RESTRICTIVA: sólo quita. Se define en Index y se APLICA     │
│  en el motor (o en el catálogo, si algún día planifica).               │
│  ⛔ NO EXISTE TODAVÍA                                                   │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ④ CONTRATO (ODCS) ───────────▼───────────────────────────────────────┐
│  QUÉ ES EL DATO Y QUÉ GARANTIZA.  semántica · calidad · clasificación  │
│  Autoridad del PRODUCTOR, y nace en la INGESTA. Es lo que ALIMENTA ③   │
│  (las etiquetas) y lo que define el TIPO de la ontología.              │
│  ⛔ NO EXISTE TODAVÍA                                                   │
└───────────────────────────────┬───────────────────────────────────────┘
┌─ ⑤ APLICACIÓN ────────────────▼───────────────────────────────────────┐
│  DÓNDE SE EJERCE.  la cara `/api/iceberg` · la puerta Junction · motor │
│  Autoridad OPERATIVA: es el único plano que puede decir «no» de verdad.│
│  Los otros cuatro sólo tienen razón; éste tiene el interruptor.        │
└───────────────────────────────────────────────────────────────────────┘

⭐⭐ Y el invariante que los une: los planos ①-④ definen; sólo el ⑤ impide. Un plano superior sin punto de aplicación es exactamente lo que privilege.ts avisa en su cabecera: «C3 sin C5 es decoración».

⚠️ Lo que hoy NO tiene autoridad, y hay que saberlo:

PiezaQué NO puede decir
Cloudflare R2Nada. Los bytes no saben de nadie, y está bien así
LakekeeperNada de nuestros grants: tiene los suyos y no los cruza
OpenMetadataNada: es productor de etiquetas, no fuente de verdad
OpenFGANada todavía: desplegado e inerte (AUTHZ_BACKEND=allowall)
⚠️ El vendado STSLo esquiva todo: mientras se venda credencial, la política de fila es decorativa

3 · LOS GRANTS QUE PROPONEMOS — qué son, dónde se definen, qué autoridad tienen

3·1 · El vocabulario: 40 privilegios en 6 familias

Definidos en lib/governance/privilege.ts — y esto es una decisión, no un accidente: el vocabulario vive en TypeScript porque es donde además se sabe DÓNDE puede concederse cada uno (grantableOn) y QUÉ implica (implies). El CHECK de la migración es sólo un patrón (^(WORKSPACE|CATALOG|SCHEMA|TABLE|VIEW|POLICY)_[A-Z_]+$): ataja la basura evidente y no repite la lista, porque el mismo conocimiento en dos lenguajes ya nos costó una fase.

FamiliaCuántosQué gobierna
WORKSPACE_*4El inquilino. Nuestro, no de Polaris — tenemos un nivel por encima del catálogo
CATALOG_*7Contenido, metadata, propiedades y adjuntar política
SCHEMA_*8El namespace lógico (≠ el físico, que es opaco desde N·3)
TABLE_*10Incluido ⭐ TABLE_READ_DATAleer FILAS, que NINGÚN *_METADATA implica
VIEW_*6Conviven con TABLE_* sobre el securable dataset
POLICY_*6La entidad de política — existe el vocabulario, no el objeto

3·2 · Dónde viven: Index, y sólo Index

principal_roles ──┬─ principal_role_members   (el sujeto: user | service | agent | job)
                  └─ catalog_role_bindings ──┬─ catalog_roles
                                             └─ privilege_grants  ← EL GRANT
                                                (privilege × securable_kind × securable_id)
                  principal_grants  (VISTA que aplana la cadena; no decide nada)

Cuatro securables — workspacecatalogschemadatasetel mismo eje que usan las políticas (C1) y el registro de uso (C2). Que los tres pilares compartan jerarquía no es elegancia: es lo que permite cruzar «qué se usó», «bajo qué política» y «con qué permiso» con un JOIN en vez de con una tabla de traducción.

⚠️ No hay tabla principals, y es deliberado. Un principal es un par (kind, id); crear un registro propio obligaría a sincronizarlo con Clerk, con las cuentas de servicio y con los agentes, y a decidir qué pasa cuando divergen. La identidad es de otro sistema; aquí sólo se referencia (plano ①).

3·3 · Quién los recibe: roles, nunca personas

SujetoDe dónde DERIVARolEstado
Humanosworkspace_members (que mantiene Clerk)ws-owner · ws-admin · ws-editor · ws-viewer · ws-guest🏁 12 sujetos, 08-12
ServiciosDeclarados: el client_id de Keycloak es el principal_idwarehouse-writer · query-engines · etl-engines · maintenance · sql-editor · catalog-profilers · operators🏁 368 grants
Agentes⛔ nada todavíael vocabulario existe ('agent'), el sujeto no
Jobs⛔ nada todavíaídem

Los humanos se DERIVAN y los servicios se DECLARAN, y no es incoherencia: un servicio no está escrito en ninguna otra parte, así que declararlo es la única opción honesta; una persona sí lo está, así que declararla sería duplicar la verdad. La regla no es «derivar siempre»: es «derivar cuando hay de qué».

3·4 · ⭐ La naturaleza de su autoridad — seis propiedades

PropiedadConsecuencia, dicha entera
ADITIVO, SIN denyIgual que Polaris. Un forbid explícito es competencia de Cedar/OpenFGA — meterlo aquí serían dos motores de política, que es lo que la decisión del owner descartó
HEREDA hacia abajo por la cadena de securables⭐⭐ Y es lo que hace que los future grants de Snowflake no hagan falta: un dataset nuevo en un contenedor concedido nace cubierto
IMPLICA transitivamente (CATALOG_MANAGE_CONTENT → 10 más)Se calcula con visitados: un ciclo accidental sería un error de datos, pero un bucle en el camino de autorización sería una caída
Gana el MÁS ESPECÍFICONo por precedencia (el modelo es aditivo, cualquiera bastaría) sino para explicar: el nivel más cercano al objeto es el que un humano espera ver
Devuelve el PORQUÉ, no un booleanoSin el grantedBy, una autorización no se puede auditar
⚠️ Grano GRUESO, y sólo esoDecide si tocas la tabla, nunca qué ves dentro. Eso es el plano ③, y no existe

3·5 · Dónde se APLICA hoy — dos puertas, dos preguntas distintas

PuntoMóduloPreguntaEstado
La cara Iceberg RESTcatalog-authz.ts«¿puede este MOTOR?»🏁 VIVO, fail-closed, con asiento en access_events
La puerta (Junction)user-authz.ts«¿puede esta PERSONA?»⚠️ shadow — mide y registra, no deniega

La decisión más importante de la cara, y merece repetirse: loadTable sin delegación pide TABLE_READ_PROPERTIES; con X-Iceberg-Access-Delegation pide además TABLE_READ_DATA. Porque son dos cosas: la primera devuelve dónde está la tabla; la segunda, la llave para leer sus bytes.

3·6 · ⛔ Los seis límites, sin maquillar

  1. No hay deny ⇒ no se puede expresar «todos menos éste».
  2. No hay etiquetas ⇒ cada política sería por objeto: N tablas = N políticas.
  3. No hay grano fino ⇒ ni fila, ni columna, ni proyección.
  4. Principal.purpose existe y no se usa ⇒ no hay acceso por propósito.
  5. ⚠️ Un grant de workspace alcanza al DATO. UC dice que los grants de metastore no se heredan al dato; nosotros heredamos desde el workspace. Divergencia consciente: nuestro workspace juega el papel de contenedor de catálogos, no de metastore. Queda anotada porque el día que exista un grant «de administración del inquilino» habrá que separar los dos ejes.
  6. Nadie puede conceder por API todavía salvo el operator: WORKSPACE_MANAGE_ACCESS está sólo en ws-owner. La superficie del cliente (F·5) no existe.

4 · ⭐⭐⭐ EL MAPA — dónde estamos, concepto a concepto

Leyenda: 🏁 hecho y medido · 🟡 parcial · ⛔ no existe · ⚠️ existe y engaña

#Concepto① El modelo (referencia · madurez)② De qué dato NUESTRO deriva③ Dónde estamos④ Quién puede decir NO
1Retícula de privilegios sobre securables con herenciaUC + Polaris. Maduro y estándar de factoprivilege_grants — ya existe🏁 40 privilegios · 368+12 sujetos · herencia y implicaciones probadasla cara (motor) 🏁 · la puerta (persona) 🟡 shadow
2Sujetos = roles del IdP, nunca personasSnowflake («no ata privilegios a usuarios») · UC (grupos vía SCIM)workspace_members ← Clerk🏁 derivado el 08-12 · reproyecta en los 3 handlers
3Future grants (que un objeto nazca con permisos)Snowflake ON FUTURE · LF por herencia de etiqueta🏁 ⭐⭐ INNECESARIO: nuestra herencia ya lo es (medido, 123/123)
4Propiedad de primera clase, y de un ROLSnowflake («los roles poseen objetos») · UC (el dueño tiene todo sobre ESE objeto)datasets.created_by es «quién lo tecleó», no el dueño (14 apuntan a servicios)aplazado con motivo medido
5Separación de deberes (conceder ≠ crear)Snowflake (SECURITYADMINSYSADMIN) · UC (MANAGE o propiedad)El propio enum de Clerk: owneradmin🏁 sólo ws-owner concede — salió gratisIndex (declarativo)
6⭐⭐ Etiquetas gobernadas (ABAC)UC, GA mayo 2026: pares clave-valor de nivel CUENTA sobre catálogos, esquemas, tablas, columnas, modelos y volúmenes · LF-Tags (el más antiguo y maduro)⛔ nadael multiplicador que falta: sin él, N tablas = N políticas
7Filtro de fila y máscara de columnaUC (política adjunta a catálogo/esquema/tabla, con WHEN y MATCH COLUMNS) · Snowflake · LF (cell-level)⛔ nadael MOTOR (§7·ter)
8Política de proyección (unir por email sin poder leerlo)Snowflake (objeto aparte)el plan ya distingue lo proyectado de lo usado en predicados🟡 casi cae sola — la ventaja que ellos dan por supuestael MOTOR
9Política de agregación (tamaño mínimo de grupo)Snowflake⏳ conocida y aplazada, no olvidada
10Composición de políticasUC/Snowflake: hay que satisfacerlas TODAS, y una fila excluida por el filtro tampoco cuenta en los agregados⛔ — y sin esa segunda mitad, la agregación es un canal lateral
11Server-side scan planning (planTableScan)Spec REST de Iceberg 1.11 (mayo 2026). ⭐⭐ Gravitino 1.2.0 YA LO IMPLEMENTA (marzo 2026); Polaris lo usa para la gobernanza de SnowflakeLa cara /api/iceberg/v1 ya es un Iceberg REST gobernado — es donde entraríaLakekeeper NO lo implementa (25 endpoints, ninguno de plan) · cliente 1.11.0 SÍ puestoel CATÁLOGO, para todos los motores
12⚠️ Vendado vs aplicaciónUC: «tablas con filtros o máscaras NO se sirven por credencial vendida» · Dremio lo declara arquitecturaGET …/tables/{t}/credentials es endpoint de primera clase de nuestro catálogo⚠️ VENDAMOS SIEMPRE ⇒ cualquier política de fila sería decorativanadie — es el agujero
13Contrato de activo (semántica + gobernanza + calidad)ODCS v3.1.0 (Bitol/LF) — validación JSON Schema estricta, de facto⛔ la ingesta escribe sin contrato y sin etiquetas⛔ — y es lo que define el TIPO de la ontologíael productor, en la ingesta
14Clasificación automáticaUC (agentic data classification, GA 05-2026) → produce las etiquetasOpenMetadata, degradado a productor— (necesita revisión humana)
15Un solo modelo de tipos y permisosUC · Foundry (Actions)⚠️ DOS planos que no se hablan: action_types tiene su propio permiso⚠️ se rehace (4 ejecuciones en toda su historia)dos motores ⇒ divergen siempre
16Policy as codeCedar / OPADecidido Cedar, sin activar · OpenFGA desplegado e inerte🟡 la pieza está, no está enchufada
17Registro de acceso (quién leyó qué, bajo qué política)UC (audit) · Snowflakeaccess_events con el mismo eje de securables🏁 la cara deja asiento en cada decisión
18Federación de catálogosPolaris 1.3 · Gravitino (multi-fuente)⛔ fuera del norte (decidido 08-11)

4·bis · ⭐⭐⭐ LA MALLA, CENSADA OBJETO A OBJETO (2026-08-13)

scripts/governance/m0-censo-de-la-malla.ts — contra la base viva. La sonda separa tres estados, y la distinción es la que decide el plan:

⛔ NO EXISTE   no hay tabla            ⇒ hay que CONSTRUIR la primitiva
🟡 VACÍA       la primitiva EXISTE     ⇒ hay que POBLARLA / CABLEARLA
🏁 POBLADA     hay filas               ⇒ queda saber si algo las APLICA (otra pregunta)
PlanoObjetoEstado
① identidadworkspace_members 12 · principal_roles 96 · principal_role_members 76🏁
② retículacatalog_roles 56 · catalog_role_bindings 120 · privilege_grants 248 (principal_grants aplana a 413)🏁
· taxonomíacatalogs 10 · schemas 16 · datasets 123 · tenant_warehouses 8🏁
③ políticapolicies 0 · policy_attachments 0🟡 existe y está vacía
③ políticalegacy: retention_policies 0 · egress_policies 0 · credential_egress_policies 0 · file_access_grants 2🟡/🏁
⭐ etiquetastags · tag_assignments
④ contratodata_contracts⛔ (lo más cercano: dataset_schema_versions 71)
⭐ conexiónconnections · secrets como securables⛔ (hay user_credentials 26 · api_connections 1)
⭐ sharingshares · share_recipients · dataspaces⛔ (sólo dataspace_assets, vacía)
⑤ usoaccess_events 4.614 · dataset_transactions 988🏁
⚠️ ontologíaobject_types 17 · action_types 4 · objects 27.042🏁 y se rehace
⛔ primitivas que NO EXISTEN (construir) ..... 11
🟡 primitivas VACÍAS (poblar / cablear) ......  6

⭐⭐⭐ ① La política NO hay que construirla: está construida y vacía

policies + policy_attachments existen desde la migración 20261266, con el invariante «un securable tiene como mucho UNA política de cada tipo» sellado en un trigger, y su resolución con herencia ya escrita y probada (lib/governance/policy.ts, 26 tests). Cero filas y cero consumidores.

⇒ El §7 de este mapa decía «4 · POLÍTICA como objeto». Ese paso no es construir: es extender un vocabulario. Los tipos declarados hoy son de ciclo de vida y egress —retention · snapshot_expiry · compaction · orphan_cleanup · egress · tier_expectation · custom—; falta row_filter y column_mask, que es un CHECK y un zod, no una entidad.

⭐⭐⭐ ② Hay CINCO vocabularios de «etiqueta» y ninguno es la etiqueta gobernada

Lo que el censo encontró sin buscarlo:

TablaFilasQué es
object_tags10object_id · tag · category · confidence · source — etiqueta sobre objects, la copia materializada del plano que se rehace. Tiene la forma de una clasificación automática… y se aplica al objeto equivocado
ml_run_tags · ml_model_version_tags · ml_registered_model_tags0Tags de MLflow. Otro dominio
graph_node_labels · graph_edge_labels0Del canvas
gmail_labels0De la integración

⭐⭐ Es exactamente el diagnóstico que la migración 20261266 hizo con las
políticas —«cinco superficies, cuatro vacías, ninguna aplicable ni heredable»
repetido con etiquetas, y nadie lo había contado.
La etiqueta gobernada no compite
con ninguna de las cinco: se aplica sobre securables (catálogo · esquema ·
dataset · columna), que es justo lo que ninguna hace.

⚠️ Y object_tags merece una nota aparte: su forma (confidence, source) ya anticipa la clasificación automática del concepto 14. El vocabulario se pensó una vez; lo que falla es de qué cuelga.

⭐⭐ ③ La conexión y el secreto: dos superficies, ninguna en la retícula

user_credentials  26   ← el alta de fuentes: encrypted_data + metadata.warehouse_destination
api_connections    1   ← OAuth de integraciones: access_token · refresh_token · bot_id · shop_domain

Ninguna de las dos es un securable: no se pueden conceder. ⇒ «¿quién puede ingerir de dónde?» y «¿quién puede usar esta credencial?» no son preguntas que este plano sepa contestar hoy — y el estándar las contesta con USE CONNECTION (UC) y READ sobre el secret (Snowflake). Ver ingesta-como-query-approach.md §4·sexies.

⛔ ④ SHARING no está en este mapa, y es una laguna del encuadre

Los 18 conceptos del §4 cubren cómo entra y cómo se lee el dato. No hay ni un concepto sobre cómo SALE gobernado hacia fuera — ni Delta Sharing, ni el SHARE de Snowflake, ni Gaia-X/EDC, del que sólo queda dataspace_assets vacía.

⇒ Se añaden abajo como conceptos 19-21. No para hacerlos ya: para que dejen de parecer resueltos por omisión.

#Concepto① El modelo② De qué deriva③ Dónde estamos④ Quién dice NO
19La FUENTE como securable (connection)UC CONNECTION + FOREIGN CATALOG · Snowflake CATALOG INTEGRATIONuser_credentials + api_connections, hoy fuera de la retícula
20El SECRETO como objeto con privilegio propioSnowflake SECRET (+READ) · UC credenciales en la connectionencrypted_data, hoy sin sujeto que lo pueda pedir
21SHARING gobernado hacia fueraDelta Sharing (protocolo abierto) · Snowflake SHARE/listing · Gaia-X/EDC⛔ nada — dataspace_assets vacíani concepto ni tabla

⇒ Lo que el censo le hace al orden del §7

ANTES                                   DESPUÉS (con lo medido)
1 · encender la aplicación              1 · encender la aplicación        (igual)
2 · etiquetas en Index                  2 · etiquetas en Index            ⭐ sigue siendo el multiplicador
3 · contrato en la ingesta              3 · ⭐ CONNECTION + SECRET como securables
4 · POLÍTICA como objeto                     ← el estándar lo exige y la ingesta lo necesita
5 · conmutar el vendado                 4 · 🟡 política: EXTENDER el tipo, no construir la entidad
6 · el aplicador en el motor            5 · contrato en la ingesta        (cuelga de 3)
                                        6 · conmutar el vendado           (igual, y sigue siendo el que decide)
                                        7 · el aplicador                  (igual)

Dos movimientos, y los dos salen de medir en vez de suponer: la política baja de rango porque ya está construida, y la conexión sube porque el estándar la puso en el centro y nosotros no la teníamos ni como concepto.


5 · Los jugadores, y dónde aplica cada uno

La pregunta que los ordena no es «qué features tiene» sino «dónde está su punto de aplicación» — porque eso decide si su gobernanza sobrevive a cambiar de motor.

QuiénDónde aplicaQué le copiamosQué NO
⭐⭐ Unity CatalogCatálogo + motor propioTodo el modelo: securables, herencia, governed tags, ABAC, la regla de composiciónEl producto: la parte ABAC no es OSS (§1·1)
Snowflake HorizonSu motor + Scan Plan API para los demásLos grants a roles, la propiedad de rol, la separación de deberes, la taxonomía de 5 políticas⚠️ «sólo gobierna dentro de Snowflake» — el modelo que evitamos
Apache Polaris (TLP feb-2026)El catálogoEl vocabulario de privilegios (ya calcado) y el policy store externalizadoSu grano fino: no tiene — manda a «la capa del motor»
⭐⭐ Apache GravitinoEl catálogo, y ⭐ planifica en servidor desde 1.2.0La prueba de que ⑪ es alcanzable hoy en OSSEl producto entero: es un metadata lake federado, otro problema
AWS Lake FormationCatálogo + motores AWSLF-Tags: el ABAC más maduro y la herencia de etiqueta que hace que un dataset nuevo herede políticasEl acoplamiento a IAM
DremioCatálogo grueso + motor finoEl encuadre de tres capas (metadata → catálogo → motor)
Starburst / TrinoEl motor (OPA/Ranger)El patrón «autorizar desde el plan analizado» — ya adoptado (plan-reader)El impuesto por motor
OpenMetadata / DataHubEn ningún sitio: describenEl sistema de tipos como referencia histórica · producir etiquetas⚠️ Ser fuente de verdad — describir no es gobernar
⭐⭐ Apache AtlasEn ningún sitio: describe. Aplica Ranger, y sobre Hive/Impala⭐⭐⭐ El PROCESO como entidad y ⭐⭐⭐ la propagación de clasificación por la arista del linaje (§5·bis)El despliegue (JanusGraph+HBase+Solr+Kafka+ZK) y su segundo modelo de tipos y de permisos
Immuta / PrivaceraSuperposición sobre el motorUna segunda fuente de política

⭐⭐ Y el patrón que comparten los cinco primeros, que es la conclusión del mapa:

La frontera GRUESA se aplica en el CATÁLOGO. El grano FINO se aplica donde el
catálogo llegue — y todos están MOVIÉNDOLO hacia el catálogo
(Snowflake con su
Scan Plan API, UC con ABAC cross-engine, Gravitino con el scan planning offload).
Nadie se está moviendo en la otra dirección.


5·bis · ⭐⭐⭐ ¿ABSORBER APACHE ATLAS? — el modelo SÍ, el sistema NO (2026-08-13)

La propuesta del owner es buena y señala una laguna real de este mapa: los 18+3 conceptos no tienen ni uno de linaje. Y el linaje es justo donde nuestros activos viven, porque —dicho por él— «cambian de estado y naturaleza mediante ETL, ELT, ML, analíticas y operaciones ad hoc».

⭐⭐⭐ Lo que Atlas tiene y nosotros no habíamos nombrado

QuéPor qué importa AQUÍ
⭐⭐⭐El PROCESO es una entidad de primera clase (Process, con inputs y outputs)Un ETL/ELT/ML no es una arista: es un objeto gobernable, con dueño, permiso y traza. Es exactamente lo que el owner describe, y hoy nosotros no lo tenemos como objeto
⭐⭐⭐La clasificación se PROPAGA por la arista del linaje — y se puede bloquear por aristaEl multiplicador del multiplicador: etiquetas el origen y la tabla derivada nace clasificada. Sin esto, cada tabla que produce un pipeline hay que etiquetarla a mano
⭐⭐Sistema de tipos EXTENSIBLE (typeDef/entityDef, con herencia)Tópicos · contenedores · volúmenes · imágenes · modelos · tablas. Es el problema del owner, literal

Los tres se adoptan. Como MODELO.

⛔ Y lo que hace que absorber el SISTEMA sea el camino equivocado

Medido
⛔⛔ No cabeAtlas necesita JanusGraph + HBase + Solr + Kafka + ZooKeeper; la propia guía de Solr pide ~32 GB de RAM. Nuestro techo es CPUS_ALL_REGIONS = 12 sobre un nodo al 82 %, y v2.5 aún «promete» un footprint más ligero. La absorción de Gravitino se pagó sacando Trino; para Atlas no hay presupuesto que sacar
⛔⛔ No aplica nadaAtlas describe; quien aplica es Ranger, y Ranger aplica en Hive/Impala (y Trino, que sale). Nuestro motor es Spark sobre Iceberg
La integración tag→policy es de CLOUDERAToda la documentación de «controlar el acceso con etiquetas» vive en docs.cloudera.com. Es el mismo patrón que el ABAC de UC (§1·1): modelo público, producto de un vendor
⛔⛔ Sería un SEGUNDO modelo de tipos y de permisosEs literalmente el defecto por el que el plano ontológico legacy se rehace (sustrato-07 §3·7) y lo que dice la regla 47. Adoptarlo entero sería repetirlo con mejor vocabulario
⚠️ Su tipo no DERIVA de nuestro catálogoRegla 46: un tipo que no deriva es una declaración, no un contrato

Apache Polaris 1.5.0 (mayo 2026) soporta Ranger como autorizador externo del catálogo Iceberg. ⇒ Si algún día quisiéramos Ranger, el camino es por el catálogo, no por Atlas — que es otra vez el patrón del §5: todos mueven la aplicación hacia el catálogo, nadie en la otra dirección.

🏁 Y lo que YA tenemos de esto, sin saberlo

lib/compute/junction.ts:350 · mergeUpstreamProvenance(sourceRefs), que corre en cada escritura por la puerta:

resuelve las fuentes → acumula `sourceDatasetIds`
                     → PROPAGA los campos de frontera (counterparty · edcTransferId · agreementId)
                     → encadena `upstreamOrigins`
                     → lo estampa en el metadata del ledger

Es el mecanismo de Atlas —propagar por la arista— aplicado a la gobernanza de frontera, y ya está vivo. Lo que le falta son tres cosas concretas, no un sistema:

  1. no propaga clasificaciones — porque no hay etiquetas (§4·bis·②);
  2. ⚠️ vive en el metadata de una TRANSACCIÓN, no en el objeto ⇒ es un rastro, no un estado consultable del dataset;
  3. ⚠️ una fuente irresoluble no bloquea (continue) ⇒ el linaje puede quedar incompleto en silencio.

⇒ Los dos conceptos que faltaban en el mapa

#Concepto① El modelo② De qué deriva③ Dónde estamos④ Quién dice NO
22El PROCESO como entidad de primera clase (ETL·ELT·ML·ad-hoc con inputs/outputs)Atlas Process · UC lineage de sistemadataset_transactions (988) + sourceRefs ya declara las entradas🟡 el rastro existe, el OBJETO no
23⭐⭐⭐ Propagación de clasificación por el linaje, con bloqueo por aristaAtlas (con Ranger aplicando)mergeUpstreamProvenance ya propaga — pero campos de frontera, no etiquetas🟡 el mecanismo está; le falta qué propagarel motor, vía política

⭐⭐ La conclusión, y es la regla 46 en su forma más útil: Atlas no nos sobra por
pequeño, nos sobra por entero. Lo que necesitábamos de él —el proceso como
objeto y la propagación por el linaje— son dos conceptos, y uno de los dos ya
está a medio construir en nuestra puerta
.


5·ter · ⭐⭐⭐ LA DERIVACIÓN — el paradigma de Atlas, traído a nuestro sustrato

Decisión del owner: «tomamos su paradigma como modelo y lo derivamos a nuestro
equivalente»
. De acuerdo — y la prueba del algodón es la de siempre (§2·4 del
approach): ¿de qué dato NUESTRO deriva cada pieza? Lo que no derive de una tabla
que ya existe, no entra.

AtlasNuestro equivalente② De qué dato NUESTRO derivaEstado
Referenceable.qualifiedNameLa coordenada (workspace, schema_id, name) + ds_<uuid>Ya sellada: índice único + trigger, y N·5 la hizo direccionable🏁
Entity / AssetEl securable (kind, id)Mismo eje que grants, políticas y access_events — los tres cruzan con un JOIN🏁
DataSetdatasets (123)🏁
⭐⭐⭐ Process (inputs/outputs)El proceso como securableSe PROYECTA del ledger, no se declara: dataset_transactions (988) ya guarda writer, motor, destino y metadata; sourceRefs ya declara las entradas🟡 el rastro está, el objeto no
LineageDerivado, nunca declarado — igual que en AtlasmergeUpstreamProvenance ya acumula sourceDatasetIds en cada escritura por la puerta🟡
⭐⭐⭐ Classification (tipada, propagable)tags + tag_assignments sobre securables y columnasDel contrato de la ingesta (concepto 13) y del clasificador (14)
Label (suelta, sin tipo)NO se deriva, y es deliberadoYa hay cinco vocabularios de etiqueta sin gobernar (§4·bis·②). Añadir un sexto sin tipo ni propagación empeora el problema que venimos a resolver⛔ a propósito
Relationship.propagateTags (bloqueo por arista)Un flag en el vínculo de linajeLa arista ya existe implícita en sourceDatasetIds
BusinessMetadatadatasets.metadata (jsonb)Ya existe🏁
Glossary / TermLa ontología proyectada — el norteDel catálogo, que es lo que la hace contrato y no declaración

⭐⭐⭐ La decisión de diseño que sale de aquí, y no es la que parecía

Lo más vistoso de Atlas es su sistema de tipos EXTENSIBLE en runtime (typeDef). Y es justo lo que no hay que derivar:

Atlas tiene tipos extensibles porque su oficio es DESCRIBIR — tiene que poder
representar cualquier sistema que exista. El nuestro es GOBERNAR, y un securable
que ningún punto de aplicación sabe aplicar no gobierna nada: es una fila.

Conjunto CERRADO y versionado de securables, que es lo que hacen UC (TABLE · VOLUME · MODEL · FUNCTION bajo el esquema) y Gravitino (filesets · models · topics). Regla 55: su problema no es el nuestro.

Pero el punto del owner sigue en pie, y es el que toca el código: tópicos, contenedores, volúmenes, imágenes y tablas no son cuatro niveles, son HERMANOS.

// hoy — lib/governance/policy.ts:29
SECURABLE_HIERARCHY = ['workspace','catalog','schema','dataset']  // lineal
securableDepth(kind) => SECURABLE_HIERARCHY.indexOf(kind)         // la profundidad ES el índice
lo que hace falta
workspace › catalog › schema › { dataset | volume | topic | model | image }
                                └── varios kinds a la MISMA profundidad

⇒ El cambio no es «un sistema de tipos»: es que la profundidad deje de ser el índice de un array lineal y pase a ser el nivel en un árbol, donde el nivel hoja admite hermanos. Acotado, pero toca las tres piezas que comparten eje —decidePrivilege, la resolución de políticas y access_events—, así que se hace una vez y con test de invariante, no por kind.

⚠️ Y con una condición que decide si esto gobierna o decora: un kind nuevo sólo entra con su punto de aplicación. Un topic gobernable cuya lectura no pasa por ninguna puerta es exactamente el plano ontológico otra vez (regla 47).

Las tres salvaguardas — para no repetir el plano ontológico con mejor vocabulario

  1. Cada entidad nueva deriva de una tabla que YA existe (Process ← el ledger; el linaje ← sourceDatasetIds) — o no entra.
  2. Ni un segundo modelo de permisos. El can_execute_action_type del plano legacy nació así. Todo lo nuevo se autoriza con privilege_grants y grantableOn.
  3. Ni un segundo almacén. El grafo de linaje se proyecta; no se materializa en paralelo. Los 27.042 objects son el recordatorio de lo que cuesta la copia.

5·quater · ⭐⭐⭐ ABSORBER EN VEZ DE RECONSTRUIR — la viabilidad, explorada (2026-08-13)

Propuesta del owner: «en vez de construir linaje nativo, tagging nativo y procesos
de primera clase, absorbemos lo que nos interesa y pasamos a tener estándares maduros
acoplados a gran escala; luego construimos alrededor»
.

Es viable. Y el candidato no es Atlas: son dos piezas, y una ya la tenemos.

⭐⭐ La distinción que lo desbloquea todo: hay DOS planos, y sólo uno se absorbe

PLANO DESCRIPTIVO   qué existe · de dónde viene · cómo se llama · qué contiene
                    ⇒ SE ABSORBE. Es estándar, es voluminoso y no decide nada
PLANO DE AUTORIDAD  quién puede · bajo qué política · con qué privilegio
                    ⇒ SE DERIVA. Es nuestro, es pequeño y es lo único que dice NO

Este mapa ya lo tenía escrito sin sacarle la consecuencia: «OpenMetadata / DataHub / Atlas — aplican en ningún sitio: describen» (§5). Eso no era una pega: era la señal de que se pueden absorber sin riesgo de retícula doble.

La cadena, y es toda de estándares

Spark (nuestro cómputo)  ──spark.extraListeners──▶  OpenLineage
   el linaje NO se construye: lo EMITE el motor, y a nivel de COLUMNA
                                     │  eventos (spec abierta, portable)
OpenMetadata  ──▶ catálogo · discovery · tags · glossary · lineage col-level
   entidades de primera clase: tabla · topic · container · pipeline · ML model · dashboard
PiezaQué aportaCoste real
⭐⭐⭐ OpenLineageEl productor. Un listener de Spark emite linaje a nivel de columna, con soporte de Iceberg. Es una spec, no un productoUn jar + config en el CR de Spark. No consume CPUS_ALL_REGIONS
⭐⭐ OpenMetadataEl consumidor. Catálogo, discovery, tags/classifications/glossary, contratos y data products; consume OpenLineage por Kafka/Kinesis y tiene agente de Spark⚠️ Servidor + base + OpenSearch. Fuera del cluster — ya vivía en su propia VM

🏁 Y lo que ya teníamos, medido hoy

VM carbon-openmetadata (34.168.67.167)   puerto 22 ⛔ · puerto 8585 ⛔   ⇒ CAÍDA

La absorción no empieza de cero: empieza por reencender lo que ya se pagó. Y este mapa lo tenía degradado a «productor de etiquetas» como si fuera un consuelo — es exactamente el papel correcto, y con OpenLineage delante deja de ser poco.

⭐⭐ Por qué esto NO repite el error del plano ontológico

El plano legacy falló por dos cosas (§3·7): escribía sin pasar por la puerta y tenía su propio modelo de permisos. La absorción sólo es sana con la dirección declarada:

Index  ──DERIVA──▶  OpenMetadata          (proyección descriptiva · NUNCA al revés)
Index  ──DECIDE──▶  la cara · la puerta · el motor     (la autoridad no se absorbe)

Las policies y los roles de OpenMetadata se quedan APAGADOS, igual que el OpenFGA de Lakekeeper. Encenderlos sería la segunda retícula, que es el error de siempre con mejor vocabulario.

⚠️ Y la excepción que hay que decidir a propósito: las etiquetas que OM produce (clasificación automática) vuelven hacia Index. Ése es el único flujo inverso legítimo, y tiene que entrar como propuesta, no como hecho — con revisión humana, que es lo que el propio estándar recomienda para clasificación automática (concepto 14).

La viabilidad, por pasos y con gate

PasoGate
🏁 L·0Estado del cluster y de las VMsMEDIDO (§5·quinquies) — ⛔ la VM de OM no existe
🏁 L·1⭐⭐ ¿emite el motor linaje a nivel de columna?VERDE (§5·quinquies)
L·2Levantar OM y conectarle el emisorUna tabla derivada por la puerta aparece con su origen sin que nadie lo declare
L·3⭐⭐⭐ La proyección Index → OM: securables, dueños y coordenada🟡 medio resuelto por el estándar: el symlinks ya trae la coordenada (§5·quinquies·③)
L·4Las etiquetas producidas vuelven como propuestaUna clasificación de OM llega a Index sin aplicarse sola
L·5(no se hace) encender policies/roles de OM

Lo que NO resuelve, y hay que decirlo

  1. Sigue sin aplicar nada. OM describe; el NO lo siguen diciendo la cara, la puerta y el motor. La absorción no adelanta la política de celda.
  2. ⚠️ El linaje que emite Spark cubre lo que pasa por Spark. La ingesta actual (Node + PyIceberg) no emite nada ⇒ otro motivo para que la ingesta sea una sentencia (ingesta-como-query-approach.md).
  3. ⚠️ Un segundo almacén de metadatos existe y hay que operarlo. Lo que lo hace aceptable es que es reemplazable: los eventos son de una spec abierta, así que el productor sobrevive al consumidor.

⭐⭐⭐ La regla que esto deja: se absorbe lo que DESCRIBE y se deriva lo que
DECIDE.
Y el criterio para saber en cuál estás: si apagar esa pieza deja pasar una
operación que antes se rechazaba, no era descriptiva.


5·quinquies · 🏁 L·0 y L·1 — MEDIDOS (2026-08-13)

① El terreno

GKE  1 nodo n2-standard-4 · CPU 88 % · memoria 87 % (requests)
     namespaces con carga:  spark 4  ·  catalog 1 (el IRC de Gravitino)  ·  stackable-operators 6

Y la VM de OpenMetadata NO EXISTE. No está caída: no está.

ssh 34.168.67.167     puerto 22 ⛔ · puerto 8585 ⛔
gcloud compute instances list   trino-k8s → sólo el nodo del GKE
                                primeval-rain-… → ninguna
                                compact-idiom-… → Compute API nunca habilitada

⚠️ Pero el stand-up está versionado: infra/openmetadata/ (compose, override, Caddy, env.example) + docs/runbooks/openmetadata-standup.md. ⇒ L·2 no es investigar, es reejecutar — con una corrección importante: el camino que OM tenía al Warehouse era por Trino, y Trino salió del cluster el 08-12. La ruta nueva es mejor y es la de este approach: el linaje se lo da OpenLineage y la identidad se la da Index, no un motor de perfilado.

② 🏁 L·1 · el motor EMITE linaje a nivel de columna, y con Spark 4.1.2

infra/gke/l1-openlineage-gate.yaml · Job desechable, --master local[1], sólo lectura, sin tocar el CR carbon-connect (cablearlo ahí reinicia Spark Connect y con él la sesión caliente del editor: primero se mide, después se cablea).

GATE 1  el listener CARGA con Spark 4.1.2-stackable26.7.0   ← NO estaba documentado
GATE 3  lectura de carbon.main.default.bronze_clientes OK · 3 filas   ← por LA CARA
GATE 4  eventos = 16 · con inputs Y outputs = 6
GATE 5  outputs con columnLineage = 9
GATE FINAL  VERDE

El coste de la absorción es una coordenada Maven y tres --conf — el mismo mecanismo que ya trajo Iceberg y el driver JDBC:

--packages io.openlineage:openlineage-spark_2.13:1.52.0
--conf spark.extraListeners=io.openlineage.spark.agent.OpenLineageSparkListener
--conf spark.openlineage.transport.type=…

Y un campo que no buscábamos: cada transformación de columna viene con "masking": false. El estándar ya modela si una transformación ENMASCARA ⇒ el día que exista la máscara de columna (concepto 7), el linaje sabrá decir por dónde viajó un dato enmascarado. No hay que pedirlo: viene puesto.

③ ⭐⭐⭐ La identidad viene RESUELTA — y es nuestro propio paradigma de nombres

Lo primero que emitió fue inquietante: nombra nuestra tabla por su ubicación.

s3://lakehouse :: warehouse/019f1a5f-3a4f-7972-8bea-77e92a58ddbf

Pero el facet symlinks lleva la otra mitad:

{"namespace": "https://app.paladio.io/api/iceberg",
 "name": "main.default.bronze_clientes", "type": "TABLE"}

El namespace es LA CARA y el nombre es la COORDENADA LÓGICA. No hay traductor que escribir: el motor emite el linaje ya atado a nuestro catálogo.

⭐⭐ Y fíjese en la forma: OpenLineage nombra por ubicación y pone la
coordenada en el symlink — es exactamente nombre ≠ ubicación, el paradigma que
N·1–N·5 nos costó cinco iteraciones. El estándar llegó a la misma separación por su
cuenta
, y eso es la mejor señal de que el modelo encaja: cuando dos sistemas que no
se conocen resuelven igual, no es una convención, es la estructura del problema.

⚠️ Con su reverso, que hay que decir: es la misma trampa de T·1·0. Quien consuma
estos eventos leyendo name en vez de symlinks resolverá por ubicación y perderá
el nombre — el bug que ya nos costó una tarde, esperando en un campo nuevo.

④ ⭐⭐ ¿OM y OL los dos en el cluster? — sí, y el motivo no es la elegancia

Pregunta del owner. La respuesta corta: OpenLineage ya está en el cluster (es un listener dentro del driver de Spark, no un servicio), así que la pregunta real es sólo sobre OpenMetadata. Y sí — pero por una razón que sale de lo medido hoy, no de que quede más limpio:

⭐⭐⭐ Lo que se perdió no fue una máquina: fue un despliegue NO DECLARADO. La VM se
levantó con scp + docker compose up siguiendo un runbook, y desapareció sin que
nadie se enterara. Un manifiesto en el repo aplicado al cluster se puede volver a
aplicar
; un procedimiento manual sólo se puede volver a ejecutar — y sólo si alguien
se acuerda. La reproducibilidad no es una virtud del k8s: es lo que hoy nos faltó.

El presupuesto, medido (no estimado):

CPUS_ALL_REGIONS   límite 12 · uso 4   ⇒ quedan 8
europe-west1       CPUS 32 · N2_CPUS 32 (holgado: el techo es el global)
nodo actual        n2-standard-4 · CPU 88 % · memoria 87 %   ⇒ ⛔ NO cabe aquí

Cabe un segundo nodo (n2-standard-4 → 8/12, quedan 4 de margen). Y debe ser un node pool aparte con taint: Spark está al 88 % y OM no puede provocarle desalojos. Es la misma disciplina de G·1 —«si no cabe, que lo diga el planificador y no lo pague Spark»— aplicada un nivel más arriba.

Dónde vive cada estado, y el criterio es la CRITICIDAD, no la comodidad:

PiezaDóndePor qué
OpenLineage🏁 ya en el clusterEs un jar en el driver. Ni pod ni cuota
OM servernode pool nuevo, con taintSin estado propio
OpenSearch de OMel cluster, con PVCÍndice reconstruible: perderlo es reindexar ⚠️ pide vm.max_map_count=262144 en el nodo — en GKE/COS eso es un DaemonSet privilegiado, y es fricción real
La BD de OMla base gestionada de europe-west1 (§8·quater·bis, opción A)Ya hay que crearla para el catálogo; un esquema más sale casi gratis y quita un PVC

⭐⭐ Y el matiz que hace esto distinto de la opción B del §8·quater·bis (que se descartó): allí el estado era la base del catálogo, donde vive la atomicidad del lakehouse; aquí es una proyección descriptiva. Perder OM es reingerir; perder el catálogo es perder el gobierno. El mismo argumento no se aplica a los dos: lo que decide dónde vive un estado es si su pérdida es reconstruible.


5·sexies · ⭐⭐⭐ ¿LA CAPA ONTOLÓGICA/AGÉNTICA SOBRE OM? — NO, y el motivo es estructural

Pregunta del owner: «¿construimos nuestra capa ontológica/agéntica sobre OM? Porque
entonces usamos SUS guardarraíles, y me sigue pareciendo una arquitectura inconexa
—sobre todo si queremos contexto a nivel enterprise»
.

El instinto es correcto y el motivo no es de gusto. Es éste:

⭐⭐⭐ El espacio de ACCIÓN no se puede proyectar

Nuestro norte lo dice ya: «un agente no consume un espacio de exploración: consume un ESPACIO DE ACCIÓN ACOTADO». Y un espacio de acción es, por definición, (lo que existe) ∩ (lo que ESTE principal puede hacer).

lo que EXISTE          lo sabe OM      (proyección descriptiva)
lo que PUEDES hacer    lo sabe INDEX   (la retícula, con herencia e implicaciones)
el ESPACIO DE ACCIÓN   sólo lo puede calcular quien tiene LAS DOS

⇒ Si el agente pregunta a OM, OM contesta lo que OM sabe, no lo que ese agente puede tocar. Para filtrarlo, OM necesitaría nuestra retícula dentro — y eso es la segunda retícula que llevamos toda la sesión evitando.

Un contexto sin filtrar no es contexto: es un catálogo de cosas que no puedes
usar.
Y para un agente es peor que no tener nada, porque planifica sobre él y
fracasa al ejecutar.

⛔ Y lo que perderíamos es exactamente la CUÑA del producto

La tesis de plataforma es que el cliente compra CONTEXTO y que la ventaja es tener el catálogo NATIVO. Si el contexto lo sirve OM:

  • el contexto sale de una proyección, que puede ir rancia, incompleta o desincronizada;
  • la ontología derivaría del modelo de OM, no del nuestro ⇒ regla 46: un tipo que no deriva del catálogo es una declaración;
  • y el catálogo deja de ser nativo, que es la cuña.

La incomodidad del owner no es arquitectónica: es de posicionamiento. Construir el producto sobre el modelo de otro es cómo se deja de tener producto.

✅ El reparto que sí funciona — UNA superficie agéntica, la nuestra

        ┌──────────── LA CAPA AGÉNTICA (nuestra) ────────────┐
agente ─▶│  compone: espacio de acción = catálogo ∩ retícula │
        └───────┬──────────────────────────────┬────────────┘
                │ pregunta (lectura)           │ ejecuta
                ▼                              ▼
        OpenMetadata                      JUNCTION — la puerta
        glosario · clasificación          decide, firma, deja asiento
        contrato · linaje · calidad
  • El agente NO habla con OM. Hablamos nosotros. OM es un servicio interno, no una plataforma sobre la que nos apoyamos.
  • Su MCP no se expone a nuestros agentes. No es un olvido: su MCP autoriza con «any role or policy defined» de OM ⇒ en el momento en que un agente actúe por ahí, apagar OM cambiaría lo que se puede hacer, y por la regla 66 eso demuestra que ya no era descriptivo.
  • ⭐ Y así no hay dos superficies agénticas, que era justo lo «inconexo» que el owner señalaba.

🧪 La prueba del algodón de esta absorción

⭐⭐⭐ Si apagar OM nos deja sin contexto, lo pusimos en el sitio equivocado.

Lo que OM produce —etiquetas, contratos, términos— aterriza en Index como propuestas (L·4) y la verdad vive en Index. Si OM desaparece mañana —como desapareció su VM— perdemos la superficie de curación y el perfilador, no el contexto. Eso es lo que hace la absorción reversible, y es la diferencia entre usar una herramienta y depender de ella.

⇒ Y por eso el orden importa: primero el modelo en Index (etiquetas, contrato, proceso como securable), después OM como el sitio donde se curan y se enriquecen. Al revés, el modelo lo acabaría dictando su esquema.


5·septies · ⭐⭐⭐ GOBERNAR ACTIVOS HETEROGÉNEOS — el panorama, y quién ya está en casa

Pregunta del owner: «¿qué alternativas más completas existen que podamos absorber,
teniendo en cuenta que el catálogo necesita gobernar tópicos, imágenes, datos,
modelos y volúmenes bajo un sistema gobernado enterprise e2e?»
— y con el añadido de
que los agentes consumen permisos, tipos, guardarraíles y entornos acotados.

Los candidatos, con lo que gobiernan de verdad

Qué objetos GOBIERNAAutoridadVeredicto
⭐⭐⭐ Apache GravitinoCATALOG · SCHEMA · TABLE · COLUMN · **FILESET** · **TOPIC** · ROLE · METALAKE · catálogo de MODELOS (0.9+) · 20+ tipos de catálogo: relacional, fileset, messaging, modelRBAC unificado entre catálogos (1.2.0, marzo 2026)🏁 YA ESTÁ EN EL CLUSTER (G·1, el IRC). Su lista es, literalmente, la del owner
⭐⭐ Unity Catalog OSStables · views · volumes · functions · models · services, en 3 niveles. Apache-2.0, LF AI & DataTodo activo es un securableEl MODELO de referencia —ya es el que copiamos— pero el servidor está en 0.1 / sandbox, y ⛔ el ABAC no es del OSS (§1·1)
Apache PolarisIceberg (+ generic tables)grants propios · Ranger externo (1.5.0)⛔ No cubre modelos, volúmenes ni tópicos
Apache Atlastipos abiertos (cualquier cosa)⛔ ninguna — aplica Ranger⛔ Descartado (§5·bis): no cabe y no aplica
Apache Egeriametadatos enterprise, muy amplioframeworks de gobernanza⛔ Mismo criterio que Atlas: peso enorme, y su punto de aplicación no es el nuestro

Y el dato que mira al futuro del owner: Unity Catalog está llevando sus ABAC Grant Policies a modelos, y anuncia extenderlas a componentes de IA — servicios MCP y AGENTES— además de tablas y volúmenes. ⇒ El agente como securable no es una idea nuestra: es hacia donde va el estándar. Nuestro vocabulario ya lo admite (PrincipalKind = 'user' | 'service' | 'agent' | 'job'), y sigue sin un solo sujeto de tipo agent.

⭐⭐⭐ Y aquí aparece la consecuencia que no estaba vista

La regla de §5·ter era: un kind nuevo sólo entra con su punto de aplicación. Ahora mírese dónde están los nuestros:

tabla Iceberg   ⇒ la cara /api/iceberg  +  la puerta          🏁 los tenemos
topic Kafka     ⇒ ¿?                                          ⛔ ninguno
fileset/volumen ⇒ ¿?                                          ⛔ ninguno
modelo          ⇒ ¿?                                          ⛔ ninguno

Nuestro punto de aplicación cubre exactamente UN tipo de activo. Gobernar tópicos, volúmenes y modelos no es añadir filas a securable_kind: es construir tres puertas nuevas… o usar las que Gravitino ya tiene.

⭐⭐ Por eso el primer activo NO-TABLA que se quiera gobernar FUERZA la decisión que
el §8·quinquies·② dejó abierta.
No es una decisión de catálogo: es la única forma de
que un topic gobernado no sea una fila decorativa.

RamaQué implica con activos heterogéneos
A · la cara delante, Gravitino detrás e inerteHabría que construir el punto de aplicación de cada tipo nuevo. Es reconstruir lo que ya existe
B · Gravitino aplica, Index proyectaSe usa su RBAC unificado como destino de proyección — el mismo patrón que él ya usa con Ranger, y que nosotros ya usamos con workspace_members → grants. Index sigue siendo el origen

Recomendación: seguir entrando por A para tablas —donde nuestro punto de aplicación ya existe y funciona— y diseñar B para todo lo demás. Es lo que el mapa ya recomendaba («entrar por A y diseñar para B»), sólo que ahora se sabe qué lo dispara: no un plan, sino el primer tópico o el primer modelo que alguien quiera gobernar.

⚠️ Con la condición de siempre, que aquí es más importante que nunca: la retícula de Gravitino es un destino, jamás una segunda fuente. El día que alguien conceda directamente allí, hay dos verdades — y ésa es la avería que este mapa lleva entero intentando evitar.


5·octies · ⭐⭐⭐ LA FRONTERA — Gravitino, Index y quién aplica de verdad

① Gravitino ↔ OpenLineage: es su CONSUMIDOR, y resuelve nuestra identidad

Gravitino trae un framework de linaje pluggable que recibe eventos OpenLineage, con sink a Marquez. Y —lo que más nos toca— un plugin de Spark propio que extrae el linaje y TRADUCE los identificadores de dataset a identificadores de Gravitino, con linaje de columna y a través de catálogos heterogéneos: fileset · Iceberg · Hudi · Paimon · Hive · Model.

⇒ Es exactamente el problema que L·1 midió: el motor nombra por ubicación (s3://lakehouse/warehouse/<uuid>) y deja la coordenada en symlinks. Gravitino hace esa traducción de serie.

⚠️ Y con eso aparece un solape que hay que decidir, no ignorar: dos sinks de
linaje.
Gravitino y OpenMetadata pueden consumir los mismos eventos. No es
redundancia gratuita
—OM aporta glosario, clasificación PII, contratos, calidad y
superficie de curación; Gravitino aporta el linaje unificado entre tipos de
activo
— pero mandar los eventos a los dos sin decidir cuál es autoritativo es cómo
se acaba con dos grafos que discrepan. [decidir]

② «RBAC unificado entre catálogos» — qué es exactamente

Privilege × SecurableObject × Role  →  User | Group
   securables: METALAKE · CATALOG · SCHEMA · TABLE · TOPIC · FILESET · ROLE (+ MODEL)
   privilegios: de gestión (CREATE, DELETE) y de operación (SELECT_TABLE, MODIFY_TABLE…)

«Unificado» significa un solo vocabulario para TODOS los tipos de catálogo — el mismo Role concede sobre una tabla Iceberg, un tópico Kafka y un fileset — en vez de un modelo de permisos por sistema (el de Hive, el de Kafka, el de MySQL…). Es literalmente nuestro modelo, con otros nombres, extendido a activos que nosotros no tenemos.

③ ⭐⭐⭐ Y AQUÍ ESTÁ LA RESPUESTA A «¿NOS OPACA?» — Gravitino NO APLICA

De su propia documentación de control de acceso:

«Gravitino doesn't support metadata authentication. It means that Gravitino won't
check the privileges when Gravitino receives the requests.»

Gravitino ADMINISTRA quién tiene qué; la aplicación la DELEGA («authorization push-down») a Ranger o al control de acceso del sistema subyacente.

Gravitino     define QUIÉN puede           ⇒ una DECLARACIÓN
Ranger/otro   comprueba SI pasa            ⇒ la APLICACIÓN

⭐⭐ No es que no queramos que Gravitino aplique: es que NO APLICA. El puesto de
«el que dice NO» sigue vacante, y hoy lo ocupamos nosotros (la cara + la puerta).
Eso responde la pregunta del owner de forma tajante: una implementación madura de
Gravitino no opaca a Index en lo que Index hace de verdad — sólo en el vocabulario.

⚠️ Reserva del instrumento, dicha: eso es la doc de 0.9.0-incubating; en 1.2.0 medimos que planTableScan lleva @AuthorizationExpression, así que su postura ha cambiado entre versiones. Lo que no cambia es el alcance: donde aplique, aplicará sobre operaciones de metadatos de su API, nunca sobre los bytes. [medir en 1.2+]

④ Qué de Index quedaría opacado, honestamente

Pilar de Index¿Lo cubre Gravitino?
Taxonomía (catalogs · schemas · datasets · tenant_warehouses)⚠️ — es su oficio. Aquí hay solape real
Retícula (privilege_grants + cadena)⚠️ Solapa el vocabulario, NO la aplicación ⇒ destino de proyección
Ledger (dataset_transactions · iceberg_sync_log · schema_versions)NO. Es nuestro control-plane de escritura
Uso (access_events)NO. El suyo audita su API; el nuestro registra decisiones de nuestra puerta
Política (policies · policy_attachments)NO. No tiene política de celda (medido en §8·1·f)
El lazo con el PRODUCTO (project_files, tiers, la coordenada que ve la UI)NO, y no puede. Index no es un catálogo: es un plano de control de producto

⑤ 🖼️ La vista idílica, con la frontera trazada

┌──────────────────────────────────────────────────────────────────────────────┐
│ ⑤  SUPERFICIE AGÉNTICA  (nuestra, única)                                     │
│    espacio de acción = lo que EXISTE ∩ lo que ESTE principal PUEDE           │
└───────────────┬─────────────────────────────────────────┬────────────────────┘
                │ ejecuta                                 │ pregunta (sólo lee)
┌───────────────▼─────────────────────────────┐   ┌───────▼────────────────────┐
│ ④  APLICACIÓN — el único que dice NO        │   │  DESCRIPTIVO (al lado)     │
│    la puerta (Junction) · la cara /api/iceberg│  │  OpenMetadata              │
│    ¿puede esta PERSONA? · ¿puede este MOTOR? │   │  glosario · clasificación  │
└───────────────┬─────────────────────────────┘   │  contrato · calidad        │
                │ pregunta a…                      └───────▲────────────────────┘
┌───────────────▼─────────────────────────────┐            │ eventos
│ ③  AUTORIDAD — INDEX                        │            │
│    retícula · políticas · etiquetas ·       │   ┌────────┴────────────────────┐
│    contratos · ledger · lazo con el producto│   │  OpenLineage (en el motor)  │
│    ⭐ ORIGEN de toda proyección              │   │  ya emite, a nivel COLUMNA  │
└───────────────┬─────────────────────────────┘   └─────────────────────────────┘
                │ proyecta ─────────────┐
┌───────────────▼─────────────────────┐ │
│ ②  CATÁLOGO — Gravitino / Lakekeeper│◀┘  su retícula = DESTINO, nunca fuente
│    qué existe · dónde · CAS del commit│
│    ⛔ define, NO aplica               │
└───────────────┬─────────────────────┘
┌───────────────▼─────────────────────┐
│ ①  BYTES — R2 · no saben de nadie   │
└─────────────────────────────────────┘

⑥ ⭐⭐⭐ «¿Gravitino es como Lakekeeper pero más completo?» — SON DOS ARTEFACTOS

⚠️ Y llevamos varias secciones mezclándolos. Lo desplegado (G·1) es el IRC server: Iceberg y nada más. Todo lo hablado en §5·septies —tópicos, filesets, modelos, RBAC unificado, linaje OpenLineage— es del servidor COMPLETO, que no está desplegado. Es la regla 8·ter aplicada a nosotros mismos: no contar como propia una capacidad que no está encendida.

El IRC contra Lakekeeper, medido hoy (GET /iceberg/v1/config desde dentro del cluster — endpoints[], que es lo autoritativo):

Lakekeeper (25)Gravitino IRC (24)
Registro + CAS del commit✅ el núcleo
…/tables/{table}/credentials (vending)MEDIDO — era el riesgo: sin él no podría sustituirlo, porque hoy Spark lee con vended-credentials
⭐⭐ POST …/tables/{table}/planla diferencia real
POST …/namespaces/{ns}/register (migrar)✅ ⇒ el re-registro es viable
Vistas (CRUD completo)
…/tables/{t}/metrics(la ruta que nuestra cara deja fuera y ensucia el log de todo motor)
POST /v1/{prefix}/transactions/commitdeclarado (sin estrenar)NO lo declara
Warehouses por inquilino (9)concepto propioson catálogos del metalake ⇒ hay que traducir
Soft-delete 7 días (P·3) · OIDC Keycloaksin comprobar

⭐ Y sus defaults ya vienen con R2 resuelto: s3.path-style-access=true · client.region=auto · el endpoint de R2 · S3FileIO.

Respuesta exacta: el IRC de Gravitino es un Lakekeeper con /plan y sin commit
multi-tabla.
Un cambio en cada dirección, no una mejora limpia. Lo «más completo»
vive en el metalake — que es otra decisión, otro despliegue y otro coste.

⚠️ Y el commit multi-tabla no es un detalle: sustrato-07 §7 lo tiene listado como
«la capacidad ya está comprada» para la deuda ⛔⛔ «una escritura toca UNA tabla».
Migrar hoy la desharía. [medir si 1.2+ lo declara]

Las tres reglas que sostienen la frontera:

  1. INDEX DEFINE · LA PUERTA APLICA · EL CATÁLOGO REGISTRA · OM DESCRIBE. Cuatro verbos, cuatro sitios, cero solapes de autoridad.
  2. Todo lo que baja del ③ es una PROYECCIÓN — la retícula de Gravitino, las entidades de OM. Nunca al revés, salvo lo que entra como propuesta (clasificación).
  3. El agente sólo tiene UNA puerta, y es la ⑤. Ni el MCP de OM ni el catálogo son superficie agéntica.

5·nonies · ⭐⭐⭐ VENDING vs POLÍTICA DE FILA — qué dice el estándar, y una corrección propia

Pregunta del owner: «¿quién firma según el estándar —el catálogo, el motor, la cara o
nadie? ¿Por qué tanta fricción entre el vending y la política de fila? ¿Cómo lo
resuelven los jugadores?»

La respuesta desmiente la salida D que este mismo documento proponía.

① Quién firma, según el estándar: NADIE. El catálogo VENDE

El mecanismo de primera clase de la spec Iceberg REST es la credencial vendida: …/tables/{t}/credentials y el bloque storage-credentials del loadTable. Temporal, acotada a la tabla, con refresco. Es lo que Lakekeeper hace hoy y lo que C·2·b midió (855 s).

⚠️ El remote signing NO es el camino estándar: es una extensión del cliente (S3V4RestSignerClient), no la recomendación de la spec. ⇒ Proponerlo era apartarse del estándar creyendo acercarse a él.

② Por qué hay fricción: no es de implementación, es estructural

Iceberg es un FORMATO DE FICHEROS.
Quien recibe acceso al fichero —por credencial o por firma— lee el fichero ENTERO.
⇒ filtro de fila y máscara de columna son INCOMPATIBLES con el acceso directo,
   salvo que confíes en el motor.

Firmar cada petición no arregla eso: da granularidad de fichero, no de fila.

③ Cómo lo resuelven los jugadores: cortándolo, no resolviéndolo

Unity Catalog, literal en su documentación:

«Tables with row filters or column masks … are not supported with credential
vending.»
· «You cannot use Iceberg REST catalog or Unity REST APIs to access tables
with row filters or column masks.»

Y Delta Sharing igual: no se comparten tablas con RLS/CM de nivel tabla.

JugadorQué hace con una tabla que tiene política
Unity CatalogLa saca del acceso directo. Ni vending ni Iceberg REST ⇒ sólo por su motor de confianza
SnowflakeIgual: su motor. Las Iceberg tables externas van sin políticas
PolarisManda el grano fino «a la capa del motor»
DremioCatálogo grueso + motor fino
Trino / StarburstMotor de confianza con OPA/Ranger

Nadie aplica grano fino en el catálogo. El patrón universal es: si hay política, el acceso directo se CORTA y sirve un motor de confianza.

La única excepción, y es de 2026: UC anunció Preview de control de acceso fino para motores externos«the first and only catalog to support cross-engine ABAC», con filtros y máscaras basados en etiquetas. Está en Preview, y es exactamente la frontera del sector.

④ ⇒ Qué deberíamos hacer nosotros — y la corrección

⛔⛔ La salida D (remote signing) queda RETIRADA como plan. Su premisa era «el
vending hay que abolirlo igual»
. Falso: el vending temporal es el estándar y
lo que hay que abolir es el acceso directo a las tablas CON política — que es otra
cosa y no cuesta latencia por fichero.

El patrón que sí toca copiar, y del que ya tenemos las dos mitades:

tabla SIN política  →  vending temporal por el catálogo     ← lo de hoy, y es el estándar
tabla CON política  →  NO se sirve por Iceberg REST
                       se sirve por LA PUERTA (Junction + Spark), que es
                       nuestro motor de confianza y ya aplica gobernanza

⭐⭐ Y ésta es la mejor noticia del análisis: no hay que construir el motor de confianza — lo tenemos. Junction + plan-reader + Spark es exactamente la pieza que UC llama su motor, y el SQL Editor ya pasa por ahí. Lo que falta no es infraestructura: es la política (concepto 7) y las etiquetas (concepto 6) que la hacen escalable.

⑤ Lo que esto le hace al cutover

⚠️ El bloqueo de C·2·b sigue, pero por el motivo CONTRARIO al que dije. No es que haya que abolir el vending: es que hay que conservarlo, porque es el estándar — y Gravitino no sabe vender temporales contra R2.

SalidaEstado
AParar el cutover. El IRC queda en pie, durable, con sus 9 catálogosla posición
⭐⭐ BQue Gravitino sepa vender temporales de R2 — contribuir el providersube a salida buena: es conservar el estándar, no inventar
CQue el vending lo sirva la caraPosible, pero es reimplementar lo que el catálogo ya hace
DRemote signingRETIRADA: no es el estándar, y no compra la política de fila

⭐⭐⭐ La regla que deja este episodio: «esto habría que quitarlo de todas formas»
es la coartada más cómoda para justificar un rodeo. Antes de apoyarse en ella, hay
que comprobar que el estándar también lo quita
— y aquí no lo quita: lo tiene como
mecanismo central, y lo que corta es otra cosa.


6 · ⭐⭐⭐ Lo que este mapa CAMBIA de decisiones ya tomadas

6·1 · La salida C de F·0 estaba mal evaluada

plano-gobernado-approach.md §7·bis dice, sobre cambiar de catálogo: «Ninguno lo tiene maduro todavía: UC lo tiene en beta».

Eso ya no es cierto. Gravitino 1.2.0 (marzo 2026) implementa scan planning offload en su servidor Iceberg REST, y motores como DuckDB y Spark ya delegan en él. Polaris lo usa para proyectar la gobernanza de Snowflake sobre motores externos.

No cambia la recomendación —sigue siendo A, aplicar en el motor—, pero cambia su MOTIVO. Ya no es «porque nadie lo tiene»; es «porque migrar el control-plane cuesta más que construir el aplicador si y sólo si el aplicador se construye para mudarse». Y hace que el diseño de F·4 tenga una obligación concreta:

El aplicador recibe (principal, objetos resueltos, política) y devuelve
(filtros, máscaras). Sin saber quién lo llama. El día que el catálogo
planifique —el nuestro o el que lo sustituya— se mueve la llamada, no la lógica.

⚠️ Y abre una pregunta que no estaba planteada: ¿Lakekeeper es el catálogo definitivo? No hay que contestarla hoy, pero sí dejar de asumir que sí. [medir]

6·2 · Las etiquetas suben de prioridad por encima de las políticas

UC lo confirma con su modelo GA: las etiquetas heredan hacia abajo (una etiqueta en el esquema alcanza a sus tablas; una en el catálogo, a todo) salvo en columnas, que no heredan de su tabla. Ese detalle es el que convierte «etiquetar» en barato.

Sin etiquetas, cada política es por objeto. Con 123 datasets y creciendo, eso no es un inconveniente: es el techo del sistema.

6·3 · El contrato ODCS no es «documentación»: es la fuente del tipo

ODCS v3.1.0 (validación JSON Schema estricta) hace que un contrato sea interpretable igual por todas las herramientas. Y en nuestro norte —donde la ontología es el espacio de acción— eso significa que el contrato que nace en la ingesta ES la definición del tipo, no un adorno alrededor.


7 · El orden que se deriva del mapa

No es el orden de lo importante: es el orden de lo que desbloquea a lo demás.

🏁 0 · SUJETOS                    ← hecho (F·2·0). Sin esto no había a quién aplicar
⭐ 1 · ENCENDER la aplicación      ← los grants existen y NO gobiernan (`shadow`)
⭐ 2 · ETIQUETAS en Index          ← el multiplicador: sin ellas, N tablas = N políticas
   3 · CONTRATO en la ingesta      ← quien PRODUCE las etiquetas, y define el tipo
   4 · POLÍTICA como objeto        ← con dueño, versión y `SHOW` privilegiado
   5 · CONMUTAR el vendado         ← «hay política ⇒ no vendo credencial». Sin esto, 4 es decorativo
   6 · EL APLICADOR en el motor    ← construido para mudarse al catálogo
   7 · UN SOLO plano de tipos      ← la Action escribiendo por la puerta
   8 · EL GRAFO como proyección    ← y sólo entonces

⚠️ El paso 5 es el que se salta todo el mundo, y es el que decide si los pasos 2-4 gobiernan o decoran. Mientras se venda credencial STS para leer el Parquet directo, cualquier política de fila se esquiva pidiendo la llave.


8 · 🏁 LAS SEIS PREGUNTAS, CONTESTADAS CONTRA EL ESTÁNDAR (2026-08-12)

Ninguna se contesta por criterio propio si la industria ya la tiene resuelta. Y
donde la respuesta sale de nuestro propio código, va con el fichero y la línea.

8·1 · ⭐⭐⭐ ¿Gravitino en vez de Lakekeeper?

CORRECCIÓN (misma fecha, tras medirlo). La primera versión de esta sección
concluía «hoy no» apoyándose en que perderíamos «OpenFGA por defecto, OPA bridge,
Cedar y permisos heredados hasta tabla y vista»
. Eso era falso: son
capacidades del producto Lakekeeper que nuestro despliegue no usa. Lo corrige
el owner y lo confirma la medición de abajo. Un argumento construido sobre
capacidades que no hemos encendido no compara dos sistemas: compara dos folletos.

8·1·a · ⭐ Lo que de verdad le pedimos a Lakekeeper, medido

Capacidad del folletoEn NUESTRO despliegueEvidencia
OpenFGA como autorizadorINERTEAUTHZ_BACKEND sigue en allowalldocs/runbooks/openfga-standup.md: «F4·0 despliega OpenFGA y NADA MÁS»
CedarNO EXISTE EN EL OSS — no es que no lo usemos: «el workspace sólo tiene crates/authz-openfga, no hay crate de Cedar», y el binario falla con cualquier otro backendinfra/lakekeeper/policies/README.md · docs/runbooks/index-catalog-rest-face.md
OPA bridgeEs de TRINO — las 14 políticas son trino_* y las consume f1-trino.yaml. Y Trino está fuera del norte (08-09)infra/gke/opa-bridge/
Remote signing / vending STSLos dos en falselib/storage/tenant-warehouse.ts:125-126
Herencia de permisos hasta tabla y vista⛔ La hace nuestro decidePrivilege, no Lakekeeperlib/governance/privilege.ts

Hoy Lakekeeper es, para nosotros, un almacén de metadatos Iceberg. Toda la gobernanza es nuestra.

Y el coste de salida, medido por los endpoints que llamamos:

/v1/config · /v1/{prefix}/namespaces · …/tables · …/tables/{table}   ← spec Iceberg REST
                                                                       PORTABLE a cualquier IRC
/management/v1/warehouse                                             ← LO ÚNICO propietario
                                                                       …y sólo lo llama la ruta de
                                                                       warehouses DEDICADOS, que
                                                                       hoy no usa ningún inquilino

⇒ ⭐⭐ No hay atadura de API. El coste real de migrar no es el código: es el registro — ~82-84 tablas, 9 warehouses, el OIDC de Keycloak y el perfil de storage.

8·1·b · Los dos argumentos de superficie, retirados

  • «Servidor JVM». Ya operamos una flota JVM en GKE (Spark Connect + ejecutores; Trino mientras dure). Un catálogo JVM no es una categoría nueva de coste.
  • «Topología de federación». No federamos: se copia al bucket. Es complejidad que no usaríamos, no un coste que pagaríamos.

8·1·c · Lo que Gravitino da, y encaja con el norte

⭐⭐⭐ /scan nativoLa PRIMERA implementación OSS de Iceberg REST que lo envía (1.2, marzo 2026): evalúa los filtros de partición en el servidor, devuelve la lista de ficheros ya hecha, y cachea el trabajo de metadata. DuckDB y Spark ya delegan
⭐⭐ Un solo RBAC sobre TODOPrivilege · SecurableObject · Role · User · Groupcasi nuestro modelo, uno a uno
⭐⭐ La MALLAIceberg + Hive + RDBMS + Kafka + ficheros (filesets) + activos de IA / modelos + UDFs, bajo una API
Mantenimiento de tablas (TMS)Compactación y expiración programadas — hoy lo hacemos a mano

Y lo que esto tumbaría de nuestro propio plan: con /scan nativo, el aplicador «en el motor» de F·4 deja de ser el destino y pasa a ser innecesario — y con él, el impuesto por motor que era su mayor pega.

8·1·d · ⛔ Los DOS riesgos que sí deciden, y ninguno es el que yo puse

RiesgoPor qué es serio
R1⛔⛔ ¿Soporta Cloudflare R2? Gravitino documenta S3, GCS, OSS y ADLS. Para almacenes S3-compatibles el path-style access es una mejora ABIERTA (issue #6504) — y R2 necesita exactamente eso, más region=auto y endpoint propioEs la misma razón por la que se descartó Polaris. Si no soporta R2, no hay conversación
R2⚠️ Su ABAC es futuro: «implementará un motor ABAC» para etiquetas dinámicas. Y su RBAC empuja los permisos hacia abajo, hoy a Apache RangerGravitino NO nos da etiquetas hoy — que es nuestra pieza nº 2 (§7·2). Y «empujar a Ranger» es el producto que decidimos no adoptar

8·1·e · 🏁 R1 — MEDIDO: SÍ, GRAVITINO HABLA R2 (2026-08-12)

Sonda real: apache/gravitino-iceberg-rest:1.2.0 en Docker, backend jdbc (SQLite, que la imagen ya trae), apuntando a nuestro bucket con un prefijo aislado.

gravitino.iceberg-rest.s3-endpoint          = <nuestro endpoint R2>
gravitino.iceberg-rest.s3-region            = auto
gravitino.iceberg-rest.s3-path-style-access = true
gravitino.iceberg-rest.io-impl              = org.apache.iceberg.aws.s3.S3FileIO

🏁 Y verificado LISTANDO EL BUCKET, no creyéndole al servidor:

_probe-gravitino/probe/r1/metadata/00000-6cf0bf0c….metadata.json   731 bytes
_probe-gravitino/probe/r2t/metadata/00000-a900401e….metadata.json  674 bytes

El riesgo que descartó a Polaris NO aplica a Gravitino. El path-style-access que R2 exige entró por el issue #6504 → PR #6541 (cerrado, 0.9.0) y está en el mapa de variables de la imagen (GRAVITINO_S3_PATH_STYLE_ACCESS).

Tres trampas del instrumento, anotadas porque volverán:

El backend memory NO toca el almacenamiento. Declara la ruta s3://… y sirve de memoria: la primera sonda dio «verde» con cero objetos en el bucket. Un catálogo en memoria no prueba nada de storage
:latest (1.3.x) está roto: NoSuchMethodError de Jackson (BufferRecycler.releaseToPool) en cuanto se crea nada. 1.2.0 funciona. Un fallo de dependencias se lee como fallo de R2 si no se mira la causa
⚠️El contrato de variables CAMBIÓ entre versiones: 1.2.0 usa GRAVITINO_S3_*, latest usa GRAVITINO_ICEBERG_REST_S3_*. Y el arranque reescribe su propio .conf, así que montarlo read-only revienta

8·1·f · 🏁 R2 — MEDIDO: el /plan está GOBERNADO, pero es frontera GRUESA

① El endpoint existe, y es la diferencia directa con Lakekeeper — mismo instrumento, misma disciplina que F·0 (endpoints[], que es lo autoritativo):

Gravitino 1.2.0  → 24 endpoints · POST /v1/{prefix}/tables/{table}/plan
Gravitino latest → 24 endpoints · POST /v1/{prefix}/namespaces/{ns}/tables/{table}/plan
Lakekeeper       → 25 endpoints · ⛔ NINGUNO

② Y SÍ autoriza — leído en la fuente (IcebergTableOperations.java, anotación del propio endpoint):

@Path("{table}/plan")
@AuthorizationExpression(
    expression = "ANY(OWNER, METALAKE, CATALOG) || SCHEMA_OWNER_WITH_USE_CATALOG || "
               + "ANY_USE_CATALOG && ANY_USE_SCHEMA && "
               + "(TABLE::OWNER || ANY_SELECT_TABLE || ANY_MODIFY_TABLE)",
    accessMetadataType = MetadataObject.Type.TABLE)
public Response planTableScan(, @HeaderParam(X_ICEBERG_ACCESS_DELEGATION) String accessDelegation)

Planificar es una operación autorizada por principal sobre la tabla, con la misma retícula que el resto (owner · SELECT/MODIFY · herencia por catálogo y esquema). Y acepta la delegación de credencial en el propio plan ⇒ credenciales con alcance de plan, que es estrictamente mejor que por tabla.

③ ⛔ Pero NO aplica política de celda. En ese camino no hay inyección de filtro de fila ni de máscara: los filters y projections los trae el cliente. El scan planning de Gravitino es poda de particiones en servidor + caché + frontera gruesa, no el «aplica la política una vez para N motores» de UC o Snowflake.

⭐⭐ Y aun así es exactamente lo que necesitábamos, por una razón distinta a la
que buscábamos: no nos da la política, nos da EL SITIO donde ponerla
— un punto
del catálogo, ya autorizado por principal, por el que pasa toda lectura de todo
motor. Es literalmente la «costura» que el §6·1 pedía construir «para mudarse»…
sólo que ya existiría, y sería del estándar en vez de nuestra.

⚠️ Reserva honesta del instrumento: mi POST al /plan devolvió 404 con la forma de ruta de 1.3 y 500 con las dos formas declaradas por 1.2.0 — la ruta existe (500 ≠ 404) y no conseguí un plan válido sobre una tabla vacía. El veredicto se apoya en endpoints[] y en el código, no en mi POST — que es la misma corrección que hubo que hacerle a la sonda de F·0.


8·1·g · ⭐⭐⭐ Y ENTONCES, ¿SE ABSORBE EN KUBERNETES?

Sí, pero no la bestia entera — y ése es el matiz que cambia el coste.

Son DOS artefactos distintos, y sólo uno hace falta:

Qué esChartQué nos da
Iceberg REST server (el que medí)Un servidor IRC soloiceberg-rest-catalog-chart/plan · R2 · vending · el catálogo Iceberg
Gravitino server completoEl metalake federado: Hive, RDBMS, Kafka, filesets, modelos, UDFs, TMS, UIchart (k8s 1.29+, Helm 3+, backend MySQL embebido o PostgreSQL externo)La MALLA

La adopción es por etapas, no un salto: entra el IRC server —que es lo que sustituye a Lakekeeper y trae /plan—, y el metalake completo entra el día que la malla Data+AI sea el producto, no antes. Empezar por el pequeño hace la decisión reversible.

Dónde ponerlo: en el cluster de Spark, y por una razón concreta. El scan planning está en el camino de lectura de toda query; un salto de red extra entre el planificador y el motor se paga en cada una. Hoy Lakekeeper vive en Railway y Spark en GKE — co-locarlos es una mejora, no sólo un traslado.

Y el techo que manda sobre todo esto: CPUS_ALL_REGIONS = 12, con subida denegada, sobre nodos n2-standard-2 donde el impuesto de sistema es el 77 %. ⇒ Sólo cabe si Trino sale — que ya está decidido (Spark = cómputo · StarRocks = serving). La salida de Trino no es un ahorro: es el presupuesto de esta operación.

⭐⭐ La decisión de diseño que hay que tomar ANTES de instalar nada

Gravitino trae su propia retícula (Privilege · SecurableObject · Role · User · Group) y empuja los permisos hacia abajo, hoy a Apache Ranger. Si entra sin decidir esto, tendríamos dos retículas — que es el error del plano ontológico otra vez, con mejor vocabulario.

   Index  ──DEFINE──▶  se PROYECTA a la retícula de Gravitino  ──APLICA──▶  /plan
   (una sola fuente)     (destino de proyección, NUNCA 2ª fuente)

Index sigue siendo el origen; la retícula de Gravitino es un destino de proyección — exactamente el patrón que Gravitino ya usa con Ranger, aplicado un nivel más arriba. Es lo mismo que hicimos con los grants de usuario: derivar, no declarar; sólo que ahora el destino es otro sistema.

⭐ Y la buena noticia sobre el coste de migrar

No hay que mover datos. La spec REST tiene POST …/namespaces/{ns}/register —que Lakekeeper ya declara— y las tablas son punteros a metadata en R2. Migrar las ~82-84 tablas es un bucle de re-registro, no una copia. Lo caro no son los bytes: son el OIDC, el perfil de storage y la cara.

El veredicto, ahora sí con las dos medidas hechas

🏁 R1 verde · R2 verde con matizla objeción que descartaba a Gravitino ha
caído, y la que queda —que no aplica celda— también la incumple Lakekeeper, que
además no tiene ni el sitio donde ponerla.

Gravitino pasa de «no compensa» a ser la opción por defecto para el catálogo,
con dos condiciones: entra sólo el IRC server, y entra con Index como única
fuente de la retícula
.

La salida B queda como respaldo, no como plan: Lakekeeper es Apache-2.0 y Rust, y /plan es un endpoint de spec estabilizada — contribuirlo sigue siendo posible si la migración se complica. Pero construir lo que otro ya tiene en producción, para un catálogo que no vamos a usar en su parte de gobernanza, es el peor de los dos caminos.

8·2 · ¿La etiqueta se ata al workspace o al catálogo? → TAXONOMÍA GLOBAL, aplicación por securable

Lo que hace el estándar, literal: en UC las governed tags son pares clave-valor definidos a nivel CUENTA y se aplican a catálogos, esquemas, tablas, columnas, modelos y volúmenes. En Lake Formation, las LF-Tags son igualmente del Data Catalog de la cuenta. Los dos separan lo mismo:

la TAXONOMÍA   (qué etiquetas existen)      →  global, de la plataforma
la APLICACIÓN  (qué objeto lleva cuál)      →  por securable, por inquilino

Para nosotros: la taxonomía es de plataforma, no por workspace. Si cada inquilino inventara su propio PII, ninguna política sería portable y la clasificación automática no tendría contra qué clasificar.

Y el matiz que lo hace viable en multiinquilino —que UC ya resuelve con permisos, no con jerarquía—: CREATE de etiquetas a nivel cuenta · ASSIGN sobre la etiqueta y APPLY TAG sobre el objeto para poder ponerla. ⇒ un cliente puede etiquetar sin poder inventar vocabulario; y si necesita el suyo, entra con el namespace de su workspace, no en la taxonomía común.

8·3 · ¿Las columnas heredan etiquetas de su tabla? → NO. Y hay que copiar el NO

El estándar es explícito y asimétrico: los securables heredan las etiquetas de su catálogo o esquema padre, y la herencia se puede sobreescribir en todos los niveles salvo en columnas; ⭐ las columnas NO heredan de su tabla — hay que aplicarlas directamente. Y las funciones de introspección lo refuerzan: has_tag() sobre una tabla mira etiquetas directas e heredadas, pero sobre una columna sólo las directas.

Por qué, y es la parte que se entiende mal: si una columna heredara PII de su tabla, etiquetar una tabla enmascararía TODAS sus columnas — y la etiqueta dejaría de decir qué es sensible para decir sólo dónde hay algo sensible. La herencia sirve para alcanzar objetos; el grano de columna sirve para identificarlos, y son cosas distintas.

Copiamos el modelo entero, asimetría incluida. Y con él viene su coste: la clasificación de columnas no sale gratis — la produce el contrato de la ingesta (§4·13) o un clasificador, nunca la jerarquía.

8·4 · ¿agent y job son principales de primera clase? → SÍ, y la industria va tarde

Lo que dice la industria en 2026: Microsoft, Okta y Google han enviado primitivas de identidad de agente y OWASP publicó su NHI Top 10 — y aun así sólo el 28 % de las organizaciones puede trazar las acciones de un agente hasta un humano patrocinador, y sólo el 18 % confía en que su IAM aguante agentes. La recomendación es unánime y operativa:

Identidad propia por agente, nunca una cuenta de servicio compartida — porque
«la atribución no se puede retrofitear».

Nuestra posición: el vocabulario ya los admite ('user' | 'service' | 'agent' | 'job', el mismo que access_events) y no hay ni un sujeto de esos dos tipos. Así que el cabo suelto no es el tipo: son dos cosas que faltan alrededor.

  1. ⭐⭐ El PATROCINADOR humano. Un agente que escribe tiene que poder atribuirse a la persona que lo lanzó. Es la métrica que la industria mide (28 %) y hoy no la tenemos. Es una relación, no una fila — ⇒ es el primer caso de uso real de OpenFGA (§8·5).
  2. El PROPÓSITO acotado. Principal.purpose existe en el código y no se usa (§3·6·4). Es literalmente lo que la literatura de 2026 llama delegation and scope: el agente no hereda los permisos de su patrocinador, hereda un subconjunto declarado.

⚠️ Y esto no es futuro lejano: nuestro norte es que los agentes consuman contexto gobernado. El día que uno escriba, la pregunta «¿quién fue?» tiene que tener respuesta, y la atribución no se añade después.

8·5 · OpenFGA: ¿cabida o sobre-iterar? → AMBAS, y depende de POR DÓNDE entre

⭐⭐⭐ El hallazgo que lo decide: Lakekeeper usa OpenFGA como su sistema de autorización por defecto, con permisos «heredados hasta tablas y vistas individuales» y modelos RBAC/ReBAC/ABAC. Es decir: en nuestro stack OpenFGA ya tiene dueño, y no somos nosotros.

Por dónde entraríaVeredicto
Como decisor de la RETÍCULA (nuestros grants)SOBRE-ITERAR. Tendríamos dos ReBAC sobre el mismo árbol —el de Lakekeeper y el nuestro— y el estado de autorización en dos sitios. Es el anti-patrón que la propia literatura nombra: «cada servicio haciendo su propia autorización» y «mantener el estado en varios sitios». Y es exactamente el error que estamos retirando del plano ontológico
⭐⭐ Como decisor del TRAVERSING del grafoSÍ, y es su caso canónico. «Puedo ver esta entidad porque estoy relacionado con aquélla» es lo que una retícula jerárquica no sabe expresar y ReBAC sí. Cuando la ontología se proyecte, gobernar el recorrido es un problema de relaciones, no de filas
🟡 Como registro del patrocinador de un agente (§8·4·1): «este agente actúa por cuenta de esta persona» es una relación

OpenFGA no entra por debajo: entra por arriba. Y hasta entonces, que siga inerte es la decisión correcta, no un pendiente. Lo que sí hay que corregir es la frase del approach que lo pinta como «el decisor de la retícula que ya existe»: ese puesto ya está ocupado por decidePrivilege y por el OpenFGA de Lakekeeper.

8·6 · ¿El grant de workspace debe alcanzar al dato? → SÍ. La divergencia era de NOMBRE, no de modelo

El estándar: en UC los grants a nivel metastore NO se heredan a los hijos — gobiernan operaciones de ámbito metastore (CREATE CATALOG), no el acceso al dato. Los grants de catálogo, en cambio, sí heredan hacia abajo.

La comparación correcta, que es donde estaba el error:

UC:        cuenta  →  metastore  →  CATÁLOGO  →  esquema  →  tabla
                      (no hereda)   (SÍ hereda al dato)
nosotros:            workspace   →  catalog   →  schema   →  dataset
                      ↑ juega el papel del CATÁLOGO de UC, no el del metastore

Nuestra herencia es correcta y coincide con el estándar. Lo que engaña es el nombre: workspace suena a cuenta y funciona como contenedor de catálogos.

⚠️ Pero deja un cabo REAL, y es concreto: en el mismo nivel conviven hoy WORKSPACE_MANAGE_CONTENT (que hereda al dato, correctamente) y WORKSPACE_MANAGE_ACCESS (que gobierna la retícula, y no debería tener nada que ver con el dato). Son los dos ejes de UC metidos en un solo nivel. Hoy no hace daño porque MANAGE_ACCESS no implica nada; el día que se le cuelgue una implicación, heredaría por donde no debe. ⇒ anotarlo como invariante con test, no como refactor.

8·7 · ⛔ Y DOS HALLAZGOS EN NUESTRO CÓDIGO QUE TOCAN A LO ANTERIOR

① 🏁 MEDIDA (2026-08-13): SÍ vendamos temporal, ~855 s

scripts/warehouse/c2b-que-vende-lakekeeper.ts — contra el warehouse compartido,
que es el que sirve:

s3.session-token (692 chars) · expiration-time · s3.session-token-expires-at-ms
client.refresh-credentials-endpoint          ⏱️ caduca en 855 s (~14 min)

La afirmación era CIERTA, y ahora tiene comando detrás. Con ella queda
confirmado el §5·3: mientras se venda credencial para leer el Parquet directo,
cualquier política de fila es decorativa
. Y bloquea el cutover de catálogo
(irc-cutover-approach.md §6·sexies): Gravitino sólo sabe entregar la llave larga
contra R2 ⇒ migrar sin resolverlo sería una regresión de seguridad.

⚠️ Lo de abajo se conserva porque el razonamiento del código sigue siendo válido y
explica por qué la duda era razonable
: la ruta que pone sts-enabled:false sólo
corre para warehouses dedicados, y el compartido se configuró fuera de ella.

①·bis El porqué de la duda — la ruta del código que decía lo contrario

lib/storage/tenant-warehouse.ts:125-126 construye el perfil de almacenamiento con:

'sts-enabled': false,
'remote-signing-enabled': false,

Ni vending temporal ni firma remota. Y hay un segundo matiz que lo complica: esa ruta sólo corre para warehouses dedicados, y hoy no tiene ninguno (pool, no silo) ⇒ el warehouse compartido —el que de verdad sirve— se configuró fuera de este código.

⇒ ⛔ sustrato-07 §5·3 y plano-gobernado §4 afirman «vendamos credencial STS (900 s)» y esa frase no tiene comando detrás. Puede ser cierta, falsa, o cierta por otra vía. Hay que medirla contra Lakekeeper antes de construir nada encima, porque el paso 5 entero cuelga de ella. [medir]

② La palanca del paso 5 ya existe, y es un flag

Lakekeeper soporta remote signing para S3 con role assumption — y el estándar lo describe como «control de acceso por fichero más fuerte, a cambio de latencia», frente al vending, que entrega la llave y se aparta. Con firma remota el catálogo sigue en el camino de lectura de cada fichero.

«Conmutar el vendado» (§7·5) puede no ser una obra: puede ser cambiar remote-signing-enabled por tabla con política. Eso rebaja radicalmente el coste del paso que ahora mismo bloquea toda la política de celda. [medir la latencia]


8·bis · Lo que queda ABIERTO de verdad tras contestar las seis

CaboQué lo cierra
🏁R1 · ¿Gravitino habla R2?, medido listando el bucket (§8·1·e)
🏁R2 · ¿su /plan aplica política?Autoriza por principal, NO aplica celda (§8·1·f). Nos da el sitio, no la política
La migración: re-registrar ~82-84 tablasPOST …/register — punteros, no datos. Medir el bucle
Sacar Trino del clusterEs el presupuesto de meter el IRC server bajo CPUS_ALL_REGIONS=12
¿Vendamos STS de verdad?Un GET al warehouse compartido de Lakekeeper. Bloquea el paso 5
¿Cuánto cuesta la firma remota?Medir latencia de una query real con remote-signing-enabled
🟡Contribuir /scan a LakekeeperDeja de ser plan B y pasa a ser el plan si R1 sale rojo
🟡El patrocinador humano de un agentePrimer caso de uso de OpenFGA, y la métrica que la industria falla (28 %)
🟡WORKSPACE_MANAGE_ACCESS vs _CONTENT en el mismo nivelUn test de invariante, no un refactor

8·ter · ⭐⭐⭐ La regla que este episodio deja

UN ARGUMENTO CONSTRUIDO SOBRE CAPACIDADES QUE NO HEMOS ENCENDIDO NO COMPARA DOS
SISTEMAS: COMPARA DOS FOLLETOS.

La primera versión de §8·1 defendió quedarse con Lakekeeper listando como pérdidas
OpenFGA (inerte), Cedar (no existe en el OSS) y el OPA bridge (es de Trino, que
sale)
. Las tres estaban en su README y ninguna en nuestro docker-compose.

El antídoto es el mismo del §2·4, aplicado a lo que YA tenemos y no sólo a lo que
vamos a adoptar: antes de contar una capacidad como propia, buscar el comando que
demuestra que está encendida.


8·quater · 🏁 G·1 — EL IRC, ABSORBIDO (2026-08-12)

infra/gke/g1-gravitino-irc.yaml · gate scripts/warehouse/g1-irc-gate.ts

namespace catalog · gravitino-irc 1/1 Running · ClusterIP, SIN ruta externa
nodo: el MISMO que Spark  (no hizo falta un segundo nodo)
✅ metadata.json escrito en R2 DESDE DENTRO DEL CLUSTER, verificado listando el bucket

El estado del cluster que lo hizo posible, medido antes: el namespace trino está vacío — Trino ya salió, y con él bajó de 3 nodos a 1 × n2-standard-4. El nodo estaba al 82 % de CPU y 79 % de memoria, y el IRC cupo con requests deliberadamente modestas (250m/1Gi): si no hubiera cabido, quería que lo dijera el planificador y no que lo pagara Spark con un desalojo.

⚠️ Nace inerte, y «inerte» aquí significa tres cosas concretas: nadie le apunta · ClusterIP sin Gateway ni LB · y el backend es SQLite sobre emptyDir, efímero a propósito — un catálogo que se pierde al reiniciar no puede confundirse con producción por accidente.

⛔⛔ La decisión que G·1 NO toma, y que es la más grande

El almacén de registro. Un catálogo Iceberg es, sobre todo, el sitio donde se serializa el commit (el CAS sobre el puntero de metadata). Dos medidas lo dejan abierto:

  1. El Postgres de control está en us-east-1 (Supabase) y el cómputo en europe-west1. El catálogo está en el camino de toda lectura ⇒ apuntarlo ahí es un salto transatlántico por query. (Y ya había una sospecha registrada de que ese cruce mató workers de Trino.)
  2. La imagen no trae driver de PostgreSQL — sólo sqlite-jdbc. Meterlo exige initContainer o imagen propia.

8·quater·bis · ⭐⭐⭐ DÓNDE VIVE LA BASE DEL CATÁLOGO — las opciones, medidas

Los números, tomados desde el cómputo (GKE europe-west1)

DestinoConexión TCPTotal
Lakekeeper (Railway)el catálogo de HOY46 ms222 ms
Supabase us-east-1donde vive Index119 ms
Cloudflare R2 (WEUR) — los bytes30 ms66 ms
Gravitino IRC, mismo nodo1,3 ms5,2 ms

⭐⭐ El catálogo en el cluster es ~40× más rápido que el de hoy. Y no es una
optimización: una query de N tablas hace ≥ N cargas de tabla, así que la
latencia del catálogo se multiplica por el ancho de la consulta. Con 5 tablas,
222 ms pasan a ser más de un segundo antes de leer un solo byte.

Y el número que descarta una opción entera: 119 ms a us-east-1. Una base de catálogo ahí haría varios viajes por carga de tabla. Sería peor que hoy.

⛔ La corrección: QUÉ ES INDEX CATALOG (y qué medí mal)

El primer censo midió pg_database_size() —541 MB— y lo llamó «Index». Es
falso.
Eso es el proyecto de Supabase entero: producto (brain_embeddings
293 MB, chat_sessions 105 MB), legacy (objects 71 MB, del plano ontológico
que se rehace) y los esquemas del propio proveedor.

Index Catalog no es una base de datos: es un conjunto concreto de tablas
el plano de control del Warehouse. Medir el continente y llamarlo contenido es
la misma clase de error que comparar folletos: el número sale, y no mide lo que
dice medir.

Index Catalog, medido de verdad (g2b-index-catalog-real.ts):

PilarTablasFilas
Taxonomía — qué existecatalogs · schemas · datasets · tenant_warehouses10 · 16 · 123 · 8
Retícula — quién puedeprincipal_roles · principal_role_members · catalog_roles · catalog_role_bindings · privilege_grants96 · 76 · 56 · 120 · 248
Ledger — qué se escribiódataset_transactions · iceberg_sync_log · dataset_schema_versions988 · 237 · 71
Uso — quién tocó quéaccess_events4.558
Política — bajo qué reglapolicies · policy_attachments0 · 0
INDEX CATALOG  ......  6,5 MB · 6.607 filas · 15 tablas
la BASE que lo aloja   541 MB
⇒ Index Catalog es el 1,21 % de la base donde vive.

⭐⭐⭐ Mover Index Catalog NO es mover Supabase. Son cosas de escala distinta, y
confundirlas hacía parecer una migración de plataforma lo que son 6,5 MB. Las
316 políticas RLS, las 308 funciones y los 6 esquemas del proveedor son de lo
otro
, no de esto.

⇒ Y eso pone sobre la mesa la opción que usa el estándar: en UC y en Snowflake el metastore es su propia base, no la base de la aplicación.

Las cinco opciones, sin maquillar

OpciónLatenciaCosteCierra §4·4Veredicto
ACloud SQL PostgreSQL en europe-west1 — sólo para el catálogo~1-3 ms~25-50 €/mes · backups y PITR incluidos · no consume CPUS_ALL_REGIONS⛔ No (Index sigue lejos)El paso inmediato
BPostgreSQL dentro del cluster (StatefulSet + PVC)~0,5 ms⛔ Consume del nodo (al 82 %) ⇒ 2º nodo ≈ +70 €/mes, y del techo de 12 CPU⛔ NoNo. Seríamos dueños de la HA, los backups y las actualizaciones de la base donde vive la atomicidad del lakehouse — y un PVC ya nos dejó un worker muerto para siempre
CSeguir en Supabase us-east-1⛔ 119 ms × N0⛔ NoDescartada por medición
DMover el proyecto de Supabase ENTERO~1-3 msAlto✅ SíNo, y ya no por el motivo que dije: arrastraría 534 MB que no son Index Catalog (producto y legacy), más 316 RLS, 308 funciones y 6 esquemas del proveedor. Es mover otra cosa
⭐⭐ EINDEX CATALOG y el catálogo, juntos en su propia base en europe-west1~1-3 msComo A⭐⭐ El destino, y son 6,5 MB. Es lo que hacen UC y Snowflake: el metastore es su propia base, no la de la aplicación

⇒ La recomendación — y con el número correcto, cambia

PASO 1  ·  A — una base en europe-west1 para el catálogo de Gravitino
              ganas los 40× de latencia, sin tocar nada más, reversible

PASO 2  ·  E — Index Catalog (6,5 MB · 15 tablas) se muda a ESA MISMA base
              ⇒ commit y asiento en UNA transacción ⇒ §4·4 cerrada

A es E sin su segunda mitad, así que el paso 1 no es trabajo tirado: es la misma base, ocupada primero por el catálogo y después por la gobernanza.

Y con 6,5 MB medidos, los dos pasos pueden ser uno solo. Lo que separaba a A de E no era el tamaño —eso era el error de medición—: es el acoplamiento. Lo único que hay que medir antes de fusionarlos es cuántas consultas cruzan hoy las 15 tablas de Index Catalog con las tablas de producto (hay una deuda conocida de «desacoplar dataset de project_files»). Ése es el precio real, y son consultas, no bytes. [medir]


8·quinquies · ⭐⭐⭐ LAS TRES PREGUNTAS DEL OWNER, Y RESULTAN SER UNA

① «¿IRC e Index son una sola unidad?» → Sí en el PLANO, no en el ESQUEMA

La intuición es correcta y conviene afilar en qué sentido:

¿Una unidad?Por qué
Como plano lógicoLos dos responden «qué existe y quién puede tocarlo». Partirlo es tener dos verdades sobre el mismo objeto
Como esquema de base de datosNOLas tablas del catálogo las versiona su migración. Fundirlas ata nuestras migraciones a las suyas — y es justo el acoplamiento que el §2·③ evita
⭐⭐ Como frontera de FALLO y de LATENCIASÍ, Y ES LO QUE HOY NO SE CUMPLEVer abajo

⭐⭐ Y ahí está el hallazgo: hoy Index vive en us-east-1 y el cómputo en europe-west1. La «unidad» ya está partida, y por un océano. La pregunta correcta no es si fusionarlos, es dónde vive el par — porque de esa distancia depende que la comprobación «¿esta tabla tiene política?» sea un JOIN local o una llamada de red en el camino de toda lectura (§7·q·3·④).

La unidad es la BASE DE DATOS y la REGIÓN, no el esquema. El catálogo y el Index en el mismo Postgres, en la misma región que el cómputo, en esquemas separados.

② «¿Es Index el que se proyecta a la retícula de Gravitino?» → DEPENDE DE UNA SOLA DECISIÓN

Y no son dos preguntas, son dos ramas de la misma:

RAMA A · LA CARA SIGUE DELANTE          (lo de hoy, con Lakekeeper detrás)
   motor ─▶ /api/iceberg (autoriza) ─▶ Gravitino IRC ─▶ R2
   · Index es la ÚNICA retícula · NO se proyecta NADA · la autorización de
     Gravitino se queda APAGADA, igual que la de Lakekeeper hoy
   · ⛔ y el Spark Connector NO se puede usar (③)

RAMA B · GRAVITINO APLICA
   motor ─▶ Gravitino (autoriza con SU retícula) ─▶ R2
   · hay que PROYECTAR Index ─▶ retícula de Gravitino (como él proyecta a Ranger)
   · ⭐ se gana que `/plan` autorice de verdad, y el Spark Connector
   · ⛔ se gana también un segundo sitio con estado de autorización

Recomendación: entrar por A y diseñar para B. A es un cambio de pieza detrás de
una puerta que no se mueve —el riesgo está acotado y la migración es contenida—.
B es el destino, porque es lo que hace que /plan valga para algo… y por eso el
día que se proyecte, la proyección tiene que ser DERIVADA de Index, no una segunda
declaración.
Exactamente el movimiento de F·2·0, con otro destino.

③ El Gravitino Spark Connector — y por qué hoy sería un RETROCESO

Medido en su documentación:

spark.plugins                = org.apache.gravitino.spark.connector.plugin.GravitinoSparkPlugin
spark.sql.gravitino.uri      = http://<servidor Gravitino>:8090     ← el servidor COMPLETO
spark.sql.gravitino.metalake = <metalake>                            ← concepto del servidor
spark.sql.gravitino.enableIcebergSupport = true

⇒ ⛔ NO funciona con el IRC standalone (puerto 9001): exige el servidor Gravitino completo y su API de metalake. G·1 no lo habilita, y eso está bien.

Y lo que de verdad importa, que es lo que el owner intuía: sí cambia el modelo de lectura y escritura, y hoy en la dirección equivocada.

Hoy (catálogo REST detrás de la cara)Con el Spark Connector
Quién configura el catálogo en SparkNosotros, apuntando a la caraGravitino, que le entrega hasta las propiedades de storage
Por dónde pasa la lecturaPor la cara, que autorizaDirecto a Gravitino
Quién aplicaIndexLa retícula de Gravitino

⛔⛔ El connector PUENTEA la cara. Y la cara es donde vive toda nuestra
gobernanza — su propio módulo lo dice: «no cierra la red; si los motores pueden
apuntar al catálogo directamente, esto no aplica nada»
.

El Spark Connector no es una mejora que se activa: es la RAMA B entera. Sólo
es adoptable cuando la retícula esté proyectada dentro de Gravitino. Antes de eso,
encenderlo sería quitar el punto de aplicación a cambio de comodidad.

Y lo que sí gana cuando llegue: federación real en Spark —Hive, JDBC, Paimon, Glue y Iceberg bajo un USE catalog—, que es el diagrama de ETL del owner y el sustrato de Carbon Flows. Sus operaciones cubren INSERT/OVERWRITE, MERGE INTO, DELETE, UPDATE, CALL y time travel. ⚠️ No cubre vistas, tablas de metadata Iceberg, ni CREATE OR REPLACE atómico — tres cosas que hoy sí usamos.


8·sexies · ⭐⭐ QUÉ HACE LAKEKEEPER HOY — el inventario de lo que se retira

Papel¿Lo usamos?Al migrar
⭐⭐⭐ El catálogo de registro (nombre → puntero de metadata)SÍ, es el núcleoLo asume Gravitino
⭐⭐⭐ El CAS del commitdonde vive la atomicidadSÍ. Invariante nº4: «Index nunca serializa escrituras; el CAS es suyo»Lo asume el backend JDBC de Gravitino
Los endpoints de la spec RESTIdénticos — es estándar
Warehouses (9, uno por inquilino)⚠️ Concepto propio: en Gravitino son catálogos del metalake. Hay que traducir
OIDC con KeycloakHay que rehacerlo
POST /transactions/commitDeclarado, sin estrenarSe pierde/gana según lo declare Gravitino [medir]
Protección de borradoSí, pasivaComprobar equivalente
OpenFGA / OPA / CedarNO — inerte, de Trino, e inexistente en el OSSNo se retira nada: no estaba encendido
/management/v1/warehouseSólo la ruta de warehouses dedicados, que no usa nadieÚnico acoplamiento de API

Y migrar no mueve datos: las tablas son punteros a metadata en R2 y la spec tiene POST …/namespaces/{ns}/register. Es un bucle de re-registro.


8·septies · ⭐⭐⭐ EL PLANO DE ESCRITURA, VISTO DESDE ARRIBA

escrituras-sustrato-debilidades.md se escribió antes de saber que íbamos a absorber Gravitino. Releído desde aquí, seis de sus siete debilidades no cambian — y la séptima se puede cerrar de raíz.

Debilidad¿La toca Gravitino?
4·1Una escritura toca una tablaNo. El commit multi-tabla es de la spec, no del producto
4·2Sin reintento ante conflictoNo. Iceberg es optimista: reintentar es del cliente
4·3Sin nivel de aislamiento declaradoNo
⭐⭐ 4·4Fallo parcial con reconciliación MANUAL (ControlPlaneUnrecordedError)🏁 SÍ — Y ES LA GRANDE
4·5Una sentencia por ejecuciónNo. Es nuestro ejecutor
4·6El verbo, por la primera palabra del textoNo. Es nuestro parser
4·7Guardianes con condición caducadaNo. Es disciplina nuestra

⭐⭐⭐ Por qué 4·4 es exactamente la intuición de «una sola unidad»

El agujero de 4·4 existe porque el commit y su asiento viven en dos sistemas: el catálogo commitea en su base, e Index anota en la suya. Entre las dos hay una ventana, y cuando falla ahí, alguien reconcilia a mano.

HOY      catálogo (su PG) ──✂── Index (nuestro PG)     ⇒ ventana ⇒ reconciliación manual
DESTINO  catálogo (esquema A) ── Index (esquema B) en el MISMO Postgres
         ⇒ el commit y su asiento pueden caber en UNA transacción

⭐⭐ «Una sola unidad» no era una preferencia de despliegue: era la única forma de
cerrar 4·4 sin inventar un protocolo de dos fases.

⚠️ Con dos condiciones que hay que decir enteras: ① el backend del catálogo tiene que ser nuestro Postgres (no SQLite ni una base ajena) y ② en la misma región que el cómputo — hoy no se cumple ninguna de las dos. Y ③ escribir en las tablas del catálogo dentro de nuestra transacción es acoplarse a su esquema interno, así que el asiento se hace alrededor, no dentro: misma conexión, misma transacción, tablas distintas.

El orden se reordena solo: el paso siguiente no es «pasar tablas a Gravitino», es decidir dónde vive su base. De esa decisión cuelgan la latencia de toda lectura y la única debilidad de escritura que la absorción puede cerrar.


9 · Fuentes