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:
- 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.
- El primitivo estaba mal elegido. Un
GlossaryTermes 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 vieja | Estado | Decisión nueva |
|---|---|---|
| D1 · Espejo 1:1 de primitivas OM | ❌ superada | N1 · Nuestro primitivo es el Pod, que OM no tiene |
| D2 · Index autora, OM proyecta | ❌ invertida | N2 · OM es SoT de metadata desde la ingesta; Carbon la lee |
| D3 · OM self-hosted en Railway | ✅ sigue | N3 · 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:
Plano Quién autora Dirección Metadata — esquemas, linaje, calidad, glosario, tags OpenMetadata, desde la ingesta OM → Carbon (lectura) Configuración — qué fuentes hay, con qué credenciales, cada cuánto se ingiere Carbon, desde su UI Carbon → 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)
| Entidad | Qué es |
|---|---|
pods | El 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_attributes | Los 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_relations | Arista 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_policies | Quién puede pedir el Pod, con qué recorte de datos, en qué entorno. |
pod_capabilities | Qué 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_feedback | Qué 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:
- 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.
- Las capacidades. OM cataloga; no ejecuta acciones de negocio con precondiciones y efectos.
- El feedback. OM registra su propio uso, no lo que un agente supo o no supo responder.
- 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:
| Estrategia | Veredicto |
|---|---|
| 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):
| # | Superficie | Dónde acaba | Nota |
|---|---|---|---|
| 1 | Grafo de Pods | Carbon nativo | es el producto; no existe en OM |
| 2 | Descubrimiento y búsqueda | Carbon nativo | /api/v1/search/query |
| 3 | Linaje | Carbon nativo | ya hay LineageTab y canvas NVL |
| 4 | Glosario y clasificación | Carbon nativo | lectura primero; la autoría sigue siendo de la ingesta (N2-bis) |
| 5 | Calidad y contratos | Carbon nativo | fase posterior |
| — | Bots, políticas, roles, versiones de conector | se queda en el enclave | administració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 previa | Realidad 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 useNavigate → useRouter · 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ña | Estado | Qué falta |
|---|---|---|
| Tests de calidad › Resumen | nativa | las 4 tarjetas de su chartCards |
| Tests de calidad › Casos de Prueba | parcial | de 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 Pruebas | parcial | filtro por Propietario (su selector de usuarios y equipos) |
| Gestor de Incidentes | parcial | sólo lectura (ver abajo) + la pantalla de detalle |
| Alertas | parcial | alta/edición: formulario propio de OM, y escritura que el bot no tiene |
| Biblioteca de Pruebas | parcial | autorí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ónde | Lo que hay que saber |
|---|---|
| serie por estado | el recuento viaja en testCase.fullyQualifiedName; el índice es testCaseResult (ejecuciones, no pruebas) |
| serie de incidentes | viaja en stateId, y el filtro por métrica de los tiempos tiene que ser nested |
| lista de incidentes | latest=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 alertas | alertType=Observability y provider !== system |
| biblioteca de pruebas | el 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.
| Antes | Ahora | Por qué |
|---|---|---|
api/observability/health (606 líneas) | borrado | telemetría del bus legacy, cero llamadores, cero UI |
api/observability/kill-switch | movido a api/ontology-bus/kill-switch | no 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
| Fase | Qué | Estado |
|---|---|---|
| P0 | Stand-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 |
| P1 | Read-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 |
| P2 | El Pod en el Index. Migración de pods / pod_attributes / pod_relations, API de autoría, y la pantalla apuntando a ellos. | pendiente |
| P3 | La hidratación. GET /pods/{name} resuelve de verdad, por Junction, y devuelve el objeto poblado. Es el primer momento en que existe producto. | pendiente |
| P4 | Servir 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 |
| P5 | Capacidades. 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.
| Artefacto | Qué hacer |
|---|---|
supabase/migrations/20261265_semantic_context_graph.sql | No aplicar. Reemplazar por la migración de Pods (P2). Sus tablas de glosario/clasificación/tag duplican a OM. Lo que sí se rescata: la disciplina de FQN materializado por trigger y el patrón de RLS por workspace. |
lib/semantic/types.ts | Rescatar 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.ts | Reutilizables casi tal cual sobre las tablas nuevas. |
app/api/semantic/graph, glossaries, terms | Retirar. Su función la cubren el read-through de P1 y la API de Pods de P2. |
app/api/semantic/bindings | Evoluciona 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.