Published

Semantic Context Graph — el Pod Layer

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

Semantic Context Graph — el Pod Layer

Estado: approach reescrito 2026-08-01 bajo el norte de
agentic-data-cloud.md. Supersede por completo la primera
versión de este documento (espejo 1:1 de OpenMetadata autorado desde el Index).
Qué es: el grafo de Pods — objetos empresariales gobernados e hidratados — y la
superficie donde se autoran. Es lo único que un agente llega a ver.


0 · Qué cambió y por qué

La primera versión de este documento partía de una premisa razonable y equivocada: que Carbon debía autorar un glosario en el Index y proyectarlo a OpenMetadata.

Está superada por dos razones:

  1. OpenMetadata es más maduro que el Index para construir el grafo, y está diseñado para hacerlo desde la ingesta. Autorar a mano lo que sus conectores extraen solos era trabajo duplicado con peor resultado.
  2. El primitivo estaba mal elegido. Un GlossaryTerm es una definición. Lo que un agente necesita pedir no es una definición: es un objeto de negocio hidratado. El espejo 1:1 nos habría dejado con un diccionario muy bien sincronizado y sin producto.
Decisión viejaEstadoDecisión nueva
D1 · Espejo 1:1 de primitivas OM❌ superadaN1 · Nuestro primitivo es el Pod, que OM no tiene
D2 · Index autora, OM proyectainvertidaN2 · OM es SoT de metadata desde la ingesta; Carbon la lee
D3 · OM self-hosted en Railway✅ sigueN3 · igual, pero con ingesta (Airflow deja de ser opcional)

1 · Decisiones

N1 — El Pod es el primitivo. Un objeto empresarial con cuatro mitades: semántica, hidratación, acción y política (norte §3). Vive en el Index porque es nuestro: OpenMetadata no tiene nada equivalente y forzarlo dentro de un GlossaryTerm con customProperties sería exactamente el error que acabamos de corregir, con otro disfraz.

N2 — OpenMetadata es la fuente de verdad de la metadata, desde la ingesta. Sus conectores construyen el grafo técnico del ecosistema entero. El Index deja de aspirar a ser el catálogo.

N2-bis (corrección, 2026-08-01). «Carbon nunca escribe en OM» era demasiado
literal y habría hecho imposible el producto. La frontera real corre entre dos planos
distintos
, y así no hay dos verdades peleándose por lo mismo:

PlanoQuién autoraDirección
Metadata — esquemas, linaje, calidad, glosario, tagsOpenMetadata, desde la ingestaOM → Carbon (lectura)
Configuración — qué fuentes hay, con qué credenciales, cada cuánto se ingiereCarbon, desde su UICarbon → OM (escritura)

Lo intocable sigue siendo: Carbon no autora metadata en OM. No escribimos allí
descripciones, términos ni clasificaciones — eso lo produce la ingesta. Lo que sí
escribimos son servicios y pipelines de ingesta, porque el usuario los crea desde
Carbon.

Consecuencia sobre el token de §6 del runbook: el bot de Carbon ya no puede ser de
sólo lectura pura
. Necesita permiso sobre servicios e ingestas, y ninguno sobre las
entidades de metadata. El canario de escritura del runbook cambia de objetivo: debe
seguir devolviendo 403 al crear un glosario, y 201 al crear un servicio.

N3 — OM self-hosted, con ingesta. El cambio de N2 tiene una consecuencia de infra directa: en el plan viejo Airflow era prescindible (autorábamos nosotros); ahora la ingesta es el mecanismo, así que el orquestador entra en el alcance del stand-up.

N3-bis — El flujo se monta sobre OpenMetadata, y su UI es un activo. No sólo el catálogo: los conectores a 130+ servicios, el modelo de entidades dirigido por esquema (JSON Schema + RDF/JSON-LD), el motor de linaje, la calidad y la gestión de ingestas. Reescribir eso no es producto. Alcance y límites en §2.4.

N5 — La superficie de conexión es el asistente de nueva fuente que ya existe. El usuario conecta una fuente en el Data Gateway de Carbon —credencial, destino en el Warehouse— y Carbon registra el servicio y la ingesta en OM por su API. El usuario nunca ve Airflow ni configura nada en la UI de OpenMetadata. Un solo sitio para conectar una fuente, o acabaría conectando la misma cosa dos veces.

N4 — El anclaje es por FQN. Un Pod y sus atributos apuntan a entidades de OM por su fullyQualifiedName, no por su UUID interno. El FQN sobrevive a una reingesta; el UUID no siempre. Es la diferencia entre un anclaje y un puntero colgante.


2 · El modelo

2.1 · Lo que NO modelamos (porque es de OM)

Glosario, términos, sinónimos, clasificaciones, tags, lineage, calidad, perfilado, esquemas de tabla y columna. Todo eso se lee de OM. Ninguna tabla nuestra lo duplica.

2.2 · Lo que SÍ modelamos (el Pod, en el Index)

EntidadQué es
podsEl objeto empresarial. Nombre, descripción, entorno (sandbox/prod — parte de su identidad, no un flag), estado de publicación, y su anclaje semántico: el FQN del término de OM que lo define.
pod_attributesLos campos del objeto. Cada uno con su nombre de negocio, su tipo, su anclaje (FQN de la columna en OM) y su hidratación (de dónde sale el valor).
pod_relationsArista Pod→Pod con cardinalidad (Customer →* Order) y composición (Gestión de Churn compone Customer). Es lo que permite a un agente navegar sin conocer una sola clave foránea — y lo que hace que la granularidad pueda evolucionar (norte §8). La composición entra desde P2, no como extensión futura: retrofitearla en un modelo plano es caro.
pod_policiesQuién puede pedir el Pod, con qué recorte de datos, en qué entorno.
pod_capabilitiesQué se puede hacer con el objeto, con sus precondiciones y sus efectos. Se construyen limpias: la maquinaria de action_types de la ontología legacy queda fuera (decisión cerrada del norte §9).
pod_feedbackQué se consultó, qué acción se ejecutó y con qué resultado, y qué no se supo responder. Es lo que convierte la cobertura del catálogo en una medición en vez de una estimación.

2.3 · La hidratación

Es la mitad que ningún catálogo puede darte, y la razón de que el Pod no sea un documento. Cada atributo declara cómo se resuelve su valor, y la resolución pasa por Junction — la puerta única del dato (junction.md) — nunca por una conexión propia:

  pod_attributes.hydration
      ├─ dataset_id  → el asset gobernado del Warehouse
      ├─ column      → la columna dentro de él
      └─ (futuro) expresión / agregado / join declarado

Cerrar la hidratación en dataset_id + column para la primera fase es deliberado: es lo que Junction ya sabe servir hoy sin inventar nada. Las expresiones y los joins declarados son la extensión natural, y no se diseñan hasta tener un Pod real funcionando — es justamente donde se aprende qué hace falta de verdad.


2.4 · Cuánto de OpenMetadata adoptamos

La decisión N3-bis lleva una pregunta detrás que conviene dejar contestada: si el flujo se monta sobre OM y su UI se puede pulir, ¿qué queda de Carbon?

Lo que se adopta tal cual: conectores, modelo de entidades, linaje, calidad, glosario, descubrimiento, y su interfaz como superficie de administración de datos (stewardship).

Lo que no puede venir de OM, por capacidad y no por preferencia:

OpenMetadata sabe sobre el dato. Nunca sirve el dato.

De ahí salen los límites, y son duros:

  1. La hidratación. Poblar un Pod exige leer datos reales por Junction. OM no lo hace ni pretende hacerlo. Es la mitad que define el producto.
  2. Las capacidades. OM cataloga; no ejecuta acciones de negocio con precondiciones y efectos.
  3. El feedback. OM registra su propio uso, no lo que un agente supo o no supo responder.
  4. La invariante del agente. La UI de OM enseña tablas y columnas — es su trabajo. Por eso puede ser superficie de operador o steward, pero nunca la puerta por la que un agente o un usuario final mira el dato, o la invariante del norte §3 se cae.

Dicho de otro modo: de OM se compra la flota de ingesta y el modelo; el Pod Layer sigue siendo íntegramente nuestro. No hay solape, y por eso adoptar mucho de OM no diluye el producto.

Un coste que se subestima siempre: «pulir su UI a nuestro gusto» tiene dos versiones muy distintas. Si OM soporta personalización de marca y de páginas de forma nativa, es barato y se mantiene solo. Si hay que bifurcar su frontal React, se paga el coste de reconciliar cada versión nueva, para siempre — y OM publica seguido. Antes de asumir que se puede pulir, hay que verificar qué permite de fábrica. Es un [confirmar] de P1, no un detalle de acabado.


3 · La superficie (esta pantalla)

El Semantic Context Graph es el editor y el visor del grafo de Pods.

  • Árbol (izq) — los Pods del workspace y sus atributos.
  • Canvas (centro) — el grafo: Pods, sus relaciones, y las aristas de hidratación hacia los assets reales del Warehouse. Ver las dos clases de nodo juntas es lo que hace visible de un vistazo qué parte del objeto está realmente respaldada por datos y qué parte es todavía una promesa.
  • Panel (der) — el contrato del Pod: descripción, atributos, anclaje en OM, hidratación, política y estado de publicación.

Lo construido en la sesión anterior (árbol con guías, canvas NVL, panel de detalle, picker del Warehouse para atar assets) se conserva: el esqueleto es el correcto. Lo que cambia es de qué se alimenta — GlossaryTerm autorado en PG pasa a ser Pod anclado en OM.

3.1 · El enclave, y cómo se absorbe OpenMetadata

El objetivo: que Carbon acabe ofreciendo las features de OM sin que el usuario sepa que está debajo. El camino que NO es: fusionar las dos interfaces.

Cuatro estrategias posibles, tres son trampas:

EstrategiaVeredicto
Bifurcar el frontal React de OM❌ publica cada pocas semanas; reconcilias el fork para siempre
Sólo re-marcar OM con su branding nativo❌ te deja un OM repintado, no absorbido: sigue siendo otra app en otra URL
Iframe como destino final❌ el usuario nota el cambio de chrome a los tres segundos
Reconstruir cada superficie en Carbon contra su API REST

Por qué la cuarta funciona, y no es fe: OM es API-first — todo lo que hace su UI lo hace por /api/v1. Por construcción, lo que su interfaz enseña, la API lo sirve. Y lo que lo hace seguro: el contrato estable es la API (dirigida por JSON Schema), no su frontal. Construir contra la API es lo que permite subir de versión sin romperse.

El enclave es el andamio, no el destino. components/workspace/tabs/semantic/OpenMetadataEnclave.tsx embebe OM entero en un iframe, con su botón en el rail. Da el 100% de las features el día uno mientras las superficies nativas se construyen. Los atajos de sección que lleva dentro (SECTIONS) son, literalmente, el marcador de lo que queda por migrar: cada pieza absorbida se borra de esa lista, y cuando quede vacía el enclave se elimina.

Requisito de servidor: el proxy manda Content-Security-Policy: frame-ancestors con los orígenes de Carbon. Sin esa cabecera OM no manda ninguna política de framing — o sea que cualquiera podría embeberla (clickjacking sobre una UI de administración). La misma línea habilita el enclave y cierra el agujero.

Orden de absorción (de más a menos valor de producto):

#SuperficieDónde acabaNota
1Grafo de PodsCarbon nativoes el producto; no existe en OM
2Descubrimiento y búsquedaCarbon nativo/api/v1/search/query
3LinajeCarbon nativoya hay LineageTab y canvas NVL
4Glosario y clasificaciónCarbon nativolectura primero; la autoría sigue siendo de la ingesta (N2-bis)
5Calidad y contratosCarbon nativofase posterior
Bots, políticas, roles, versiones de conectorse queda en el enclaveadministración pura; el usuario final no la ve

Aclaración que evita un malentendido caro: absorber el descubrimiento significa que Carbon enseñará tablas y columnas. Eso no rompe el norte. La invariante «el agente nunca ve las tablas» es sobre agentes, no sobre personas: el steward humano necesita el detalle técnico para gobernar; el agente sólo ve Pods. Dos audiencias, dos superficies — y si se mezclan, la invariante se cae por la puerta de atrás.

El habilitador técnico es uno: un cliente único de OM en lib/semantic/openmetadata/, por el que pasa toda pantalla nativa. Es la cintura estrecha de OM, misma disciplina que Junction para el dato. Eso es P1.

3.1.1 · El port: qué bloquea de verdad (medido, no supuesto)

Se probó copiando un widget real verbatim (DataAssetsCoveragePieChartWidget, 140 líneas) en vez de razonar sobre ello. Resultados:

Hipótesis previaRealidad medida
«antd v4 no soporta React 19»Falso. peerDependencies: react >=16.9.0, rango abierto. Instala y renderiza bajo React 19.2.1 sin tocar findDOMNode
«no se puede portar sin datos»Falso. La estructura se porta leyendo el código; los ceros que salen son ceros correctos
«hay que reescribir con nuestro design system»⚠️ Innecesario. La lógica copia literal

Lo que sí arrastra — y son sustituciones de una línea, porque su app es un SPA de Vite y Carbon es Next: lodash → nativo · react-i18next → literales · react-router useNavigateuseRouter · SVGR → icono inline · .less → inline. recharts ya estaba en Carbon (^2.15.4), así que sus gráficos copian casi enteros.

El coste real es el CSS, no React. antd/dist/antd.css son 545 KB con un reset global que toca html, body, p, h1-h6, hr, textarea — importarlo repintaría Carbon entero. Solución: scripts/semantic/scope-antd-css.mjs filtra la hoja dejando sólo reglas .ant-* (4.255 conservadas, 74 descartadas, cero selectores globales). Se importa desde el componente, no del layout raíz, para que viaje en el chunk de su pestaña.

Lección de método: cada vez que se afirmó sin medir (permisos de certs, las dos capas de CSP, «no se puede portar sin datos», 3 subsecciones en vez de 4, una aggregationQuery inventada) la afirmación era falsa. Cada vez que se ejecutó, apareció un detalle que no se habría adivinado — como que el recuento de cobertura viaja en un campo llamado originEntityFQN. Portar leyendo el código; verificar ejecutándolo.

3.1.2 · Estado del port de «Observabilidad del dato»

El marcador vive en el código (om-ui/sections.ts), no aquí; esta tabla es la foto y el registro de lo que costó cada pieza. Las cuatro secciones son nativas: no queda ningún iframe, y con la última se retira también el EmbeddedSection que las servía. El andamio del §3.1 hizo su trabajo y se desmonta — dejarlo «por si acaso» sería dejar un camino que nadie recorre y nadie mantiene.

Sección · pestañaEstadoQué falta
Tests de calidad › Resumennativalas 4 tarjetas de su chartCards
Tests de calidad › Casos de Pruebaparcialde sus 10 filtros funcionan los 4 de enumerado; los otros 6 pueblan su desplegable con una búsqueda ES aparte
Tests de calidad › Suites de Pruebasparcialfiltro por Propietario (su selector de usuarios y equipos)
Gestor de Incidentesparcialsólo lectura (ver abajo) + la pantalla de detalle
Alertasparcialalta/edición: formulario propio de OM, y escritura que el bot no tiene
Biblioteca de Pruebasparcialautoría (crear/editar/habilitar una regla)

Lo que la frontera N2-bis se lleva por delante, y está bien que se lo lleve. En OM el estado, la severidad y el asignado de un incidente se editan en la propia tabla. Aquí no: el bot de Carbon lee toda la metadata y escribe sólo configuración, así que un PATCH sobre un incidente es 403. No es deuda del port — es la política funcionando. Se dice en la pantalla en vez de pintar controles que fallarían al pulsarlos.

Cuatro detalles que sólo aparecieron ejecutando. El patrón se repite: la respuesta de una agregación no se indexa por el bucketName de la spec, sino por el campo agregado, y su propio bloque metadata lo confirma — es el sitio donde verificarlo, no la suposición.

DóndeLo que hay que saber
serie por estadoel recuento viaja en testCase.fullyQualifiedName; el índice es testCaseResult (ejecuciones, no pruebas)
serie de incidentesviaja en stateId, y el filtro por métrica de los tiempos tiene que ser nested
lista de incidenteslatest=true es obligatorio: sin él cada transición del ciclo (New→Ack→Assigned→Resolved) es una fila y el mismo incidente sale cuatro veces
lista de alertasalertType=Observability y provider !== system
biblioteca de pruebasel filtro es testPlatform en singular (con el plural devuelve las 25 filas, sin quejarse) y entityType distingue mayúsculas (TABLE→9, table→0). Además es la única lista sin search/list: pagina por cursor

Ese último cambió lo que la pantalla dice. Las «2 alertas» que se veían son ActivityFeedAlert y WorkflowEventConsumer: fontanería interna de OM, ninguna de observabilidad. Con los filtros buenos la lista sale vacía, y ésa es la respuesta correcta — la anterior enseñaba las tripas del catálogo como si fueran alertas del usuario.

Y una del port previo: «Dimensiones de Datos» listaba 4 dimensiones de las 8 que define DataQualityDimensions. Las pruebas de Unicidad, Validez, SQL o «Sin dimensión» desaparecían de la tarjeta sin decir nada — el cero silencioso de siempre. Corregido.

El sidebar lista secciones, no pestañas. Las tres de Tests de calidad estaban también en el árbol de la izquierda: dos mandos para lo mismo y dos sitios donde se ve cuál está activa. Viven donde viven en OM, dentro de la vista.

3.2 · Jerarquía de la observabilidad (cerrada 2026-08-02)

Un nombre, un dueño. «Observabilidad» en Carbon significa la salud del DATO, y vive en la app ObservabilityTab sobre OpenMetadata. No se comparte el nombre con nada más.

Lo que había antes bajo app/api/observability/* no era observabilidad de plataforma —como se creyó en un primer análisis— sino telemetría del bus de la ontología legacy: sus feature flags (chains_enabled, dispatcher_enabled, triggers_enabled, executions_enabled) gatean lib/chains/* y el wrapper de action_types, o sea exactamente la maquinaria que el norte §9 deja fuera. Y no tenía ninguna pantalla que la consumiera.

AntesAhoraPor qué
api/observability/health (606 líneas)borradotelemetría del bus legacy, cero llamadores, cero UI
api/observability/kill-switchmovido a api/ontology-bus/kill-switchno es observabilidad: es un control operativo. Se retira con el bus
ObservabilityTab (OM)la observabilidad

Por qué el kill-switch se movió en vez de borrarse: es el freno de emergencia de una maquinaria que puede seguir corriendo en producción. Quitar el endpoint no apaga el bus — sólo quita la forma de apagarlo. Se borra cuando el bus esté apagado, no antes.

Fuera de esta jerarquía, y no por descuido: la observabilidad de modelos (/api/ml/models/…/observability) es de inferencia, otro dominio, y no se toca.

El principio, para las próximas colisiones de nombre: cuando dos cosas se llaman igual, una de las dos está mal nombrada. Aquí lo estaba la vieja — «observabilidad» describía dónde vivía el código, no qué observaba.


4 · Fases

FaseQuéEstado
P0Stand-up de OpenMetadata con ingesta — server + BD + buscador + orquestador; conectores contra el Warehouse (Iceberg/Lakekeeper) y contra las fuentes del ecosistema. Runbook: ../runbooks/openmetadata-standup.md.operador
P1Read-through a OM. Cliente de sólo lectura (lib/semantic/openmetadata/) + /api/semantic/catalog que sirve el grafo de OM a la UI. Cero escrituras hacia OM.pendiente
P2El Pod en el Index. Migración de pods / pod_attributes / pod_relations, API de autoría, y la pantalla apuntando a ellos.pendiente
P3La hidratación. GET /pods/{name} resuelve de verdad, por Junction, y devuelve el objeto poblado. Es el primer momento en que existe producto.pendiente
P4Servir a agentes. MCP de Pods + política + separación sandbox/prod. Aquí nace pod_feedback: el primer agente que pide un Pod ya debe dejar rastro, o el bucle del norte §7 se queda en intención.pendiente
P5Capacidades. La tercera cosa que consume un agente. Construcción limpia — nada de action_types.pendiente

El gate que importa es P3. P0–P2 son andamiaje: hasta que un agente no pueda pedir Customer y recibirlo hidratado, no hemos entregado nada que un catálogo enriquecido no diera ya.


5 · Qué hacer con lo construido en la sesión anterior

Nada de esto está aplicado ni desplegado, así que el giro se paga en horas.

ArtefactoQué hacer
supabase/migrations/20261265_semantic_context_graph.sqlNo aplicar. Reemplazar por la migración de Pods (P2). Sus tablas de glosario/clasificación/tag duplican a OM. Lo que se rescata: la disciplina de FQN materializado por trigger y el patrón de RLS por workspace.
lib/semantic/types.tsRescatar ProjectionState/SyncState (siguen valiendo para el anclaje a OM) y validateEntityName. Sustituir Glossary/GlossaryTerm por los tipos leídos de OM y añadir Pod.
lib/semantic/rows.ts, lib/semantic/validate.tsReutilizables casi tal cual sobre las tablas nuevas.
app/api/semantic/graph, glossaries, termsRetirar. Su función la cubren el read-through de P1 y la API de Pods de P2.
app/api/semantic/bindingsEvoluciona a la hidratación: es literalmente la arista atributo → dataset+columna con otro nombre.
components/workspace/tabs/semantic/*Se conserva. Cambia el modelo que consume, no la forma.
Registro en WorkspaceShell (TabType, app del launcher, color)Se conserva tal cual.

6 · Riesgos

  • R1 · Cobertura del catálogo de Pods. La invariante «el agente nunca ve las tablas» significa que lo que no esté en un Pod, no existe para el agente. Es la métrica de producto y no hay atajo: se mide, no se estima.
  • R2 · Multi-tenancy a través de OM. Carbon es multi-workspace; OM modela Domains y Teams, no workspaces. Antes de proyectar más de un workspace a una instancia hay que verificar el aislamiento — y con N2 el riesgo es mayor que antes, porque ahora OM contiene la metadata de todo el ecosistema ingerido, no un glosario curado a mano.
  • R3 · Peso operativo de la ingesta. Con N3 entra un orquestador con estado en la topología. Los conectores fallan, se atascan y hay que vigilarlos: es una superficie de operación nueva, no una casilla de configuración.
  • R4 · Deriva del anclaje. Una reingesta puede renombrar o mover una entidad de OM y dejar el FQN de un atributo apuntando a la nada. Hace falta detección de anclajes rotos desde P2 — no como pulido posterior: un Pod con un atributo colgante hidrata mal y el agente no tiene forma de saberlo.
  • R5 · Dos fuentes de verdad conviviendo. OM manda en metadata, el Index manda en Pods, y el anclaje cruza la frontera. Es la clase de costura donde históricamente se han metido los bugs de este programa. La regla que la mantiene sana es simple y hay que defenderla: Carbon nunca escribe en OM.