Published

🧊 CONGELADO — sucedido por sustrato-06.md

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

🧊 CONGELADO — sucedido por sustrato-06.md

Este documento es una FOTO del 2026-08-11 (noche) y ya no se actualiza. Se
conserva porque la gracia de numerarlos es ver qué era verdad en qué momento.
Para el estado vigente, ir a sustrato-06.md.

⚠️ Y lleva al menos un error que el 06 corrige, dicho aquí para que nadie lo cite:
§10 afirmaba que el defecto del DELETE estaba «GUARDADO en producción». No lo
estaba
— el deploy vivo era 18 h anterior al código del guardián. La corrección está
dentro, pero se avisa arriba: un lector que sólo mire la tabla no llega a ella.


Sustrato · 05 — la arquitectura entera, de una pieza

Fecha de la medición: 2026-08-11 (noche). Sucede a
sustrato-04.md (08-10 noche), 03,
02 y 01, todos congelados: la gracia de
numerarlos es poder ver qué era verdad en qué momento.

Regla, la de siempre: cada hecho lleva el comando que lo demuestra. Lo que no lo
lleva va marcado como pendiente (§10) u opinión (§11).

Manda sobre cualquier approach o runbook donde discrepen.


⭐ Lo que cambió desde el 04, en cinco líneas:

  1. 🏁 Carbon SQL existe como CONTRATO — declarado, medido, publicado y con
    trinquete. Antes era un puntero a «lo que el motor haga hoy».
  2. 🏁 El plano de control dejó de ser el único que parsea: el motor devuelve su
    plan y la puerta lo lee. Todavía audita, no manda.
  3. Cuatro defectos VIVOS encontrados, dos ya arreglados, uno guardado en
    producción
    y uno abierto.
  4. ⭐ El namespace de resolución pasa a ser del inquilino.
  5. ⚠️⚠️ La lección de la jornada no es de código: es de MEDICIÓN. Nueve
    instrumentos mintieron antes que el sistema (§12·26).
  6. ⛔⛔ Y el décimo, por la tarde: el criterio de salida del auditor era
    INSATISFACIBLE
    — sólo arrancaba su ventana cuando algo fallaba, así que con la
    sombra funcionando perfecto no llegaba a verde nunca. Arreglado con el latido
    (§9, §12·34).

1 · Qué estamos construyendo, en un párrafo

El cliente compra CONTEXTO, no velocidad. Para velocidad están Databricks y
Snowflake. Nuestro producto es conocimiento consumible por agentes, y el
lakehouse es el sustrato que lo hace posible, no el producto.

  1. La cintura estrecha es el CATÁLOGO, no un motor. El estándar no es «rápido», es «gobernado y ontologicamente PROYECTABLE».
  2. El cómputo es PLURAL y desechable. Los motores entran y salen por un registro.
  3. La gobernanza emana del catálogo y el grafo la heredará por relación.
  4. Y la cuña se sirve por COPIA, no por federación (decisión del 08-11): el problema del cliente es de gobernanza, no de acceso — federar te deja mirar el dato ajeno, copiar te mete en su cadena de custodia. Ver federated-catalog-wedge.md.

2 · El mapa, de arriba abajo

   ┌──────────────────────── SUPERFICIES ────────────────────────┐
   │  SQL Editor · dashboards · pipelines · notebooks · ontología │
   └───────────────────────────┬─────────────────────────────────┘
                               │   ⚠️ sólo el SQL Editor pasa por la puerta (§4·3)
                    ⭐ JUNCTION — LA PUERTA (lib/compute)
                  loadItemForConsumption  ·  runQuery
              ┌────────────────┴────────────────┐
     REGISTRY DE MOTORES              LA CARA  /api/iceberg
     spark · duckdb · karma           (Iceberg REST gobernado)
     mlrunner · pg                              │
              └────────────────┬────────────────┘
        ┌──────────────────────┴───────────────────────┐
        │  CINTURA ESTRECHA:  Iceberg + Lakekeeper     │
        └──────────────────────┬───────────────────────┘
                     Cloudflare R2 (Parquet)

Y en paralelo, el plano de control: PostgreSQL (Index) + OpenFGA + Keycloak.


3 · La cintura estrecha — Iceberg + Lakekeeper

CatálogoLakekeeper, type=rest, en Railway
FormatoApache Iceberg
AlmacenamientoCloudflare R2, bucket lakehouse, WEUR
Warehouses9 (1 compartido + 8 por inquilino, éstos vacíos)

Por qué esto y no un motor: un motor que aplica política sólo la aplica a sí mismo. Con la política en el catálogo vale para todos.

⚠️ Lo que Iceberg NO da: seguridad de fila y columna (Lakekeeper+OpenFGA llega hasta tabla) — ni renombrar un namespace: el spec tiene renameTable/renameView y no renameNamespace (apache/iceberg #13023). Ni la referencia lo tiene entero: Unity Catalog renombra un esquema pero no un catálogo por SQL.


4 · Las puertas

4·1 · Junction — la puerta del PRODUCTO

lib/compute/junction.ts. Dos entradas (loadItemForConsumption, runQuery). Resuelve catálogo lógico → binding físico → valida la tenencia TABLA POR TABLA → construye el GovernanceContextelige motor → despacha.

⭐ Y desde hoy el contexto lleva defaultNamespace: el namespace de resolución del inquilino, resuelto por la puerta (que conoce Index) y transportado cerrado hasta el motor, igual que writeTarget. El adaptador sigue ciego.

elegirMotorDeLectura(esWrite)spark siempre, o lanza. Sin fallback silencioso.

4·2 · La cara — la puerta de los MOTORES

app/api/iceberg/v1/[...path]. Un Iceberg REST gobernado que hace siete cosas que un catálogo normal no: autoriza por inquilino · acuña credencial acotada · filtra listados · resuelve nombres lógicos en ambas direcciones · firma el commit · firma con el id del ledger · materializa tablas nuevas en Index.

El porqué que las une: son cosas que el motor no puede hacer y que, hechas aquí, valen para todos los motores a la vez.

4·3 · ⚠️⚠️ Y quién pasa por la puerta, medido hoy

grep runQuery( →  app/api/sql-editor/execute/route.ts
                  lib/warehouse/catalog-sql/handlers.ts   (crear vistas)

Sólo el SQL Editor. Pipelines, ingesta y notebooks escriben por iceberg-native-write / lakehouse-landing: no pasan por Junction. Es el hecho que más limita el alcance de todo lo construido hoy (§11·2).


5 · Gobernanza — quién puede qué

Qué gobiernaDónde
Index (PostgreSQL)privilegios · procedencia · access_events · el ledgerRailway
OpenFGAautorización de Lakekeeper (ReBAC)⚠️ inerte (allowall)
Keycloakidentidad de los serviciosRailway

⚠️ DOS caminos, DOS aplicadores

CaminoQuién aplica
por la cara (Spark)Index
directo a Lakekeeper (warehouse-writer, duck-server)OpenFGA ⛔ inerte

Los principals

lakekeeper-spark (lee, GATE) · ⭐ lakekeeper-spark-editor (lee · escribe · CREA; ⛔ NO TABLE_DROP) · -etl · -maintenance · -warehouse-writer · -operator.

TABLE_DROP sigue fuera, y hoy se midió que es útil que lo esté: es lo que obliga al corpus de escritura a deshacerse revirtiendo snapshots en vez de soltando tablas.

⚠️ El límite de la gobernanza de USUARIO

principal_grants: 360, todos de servicio · 0 de usuario (medido el 08-10). La autorización efectiva de un usuario es la pertenencia al workspace. Es el Horizonte 2.


6 · Almacenamiento y credenciales

Cloudflare R2, bucket lakehouse. Perímetro explícito: warehouse-writer RW · duck-server RO · Lakekeeper (el 4º sitio que se olvida) · ml-runner ninguno. Vending, no llaves estáticas: STS 900 s, acotado, con control negativo.


7 · Cómputo

MotorPapelEstado
sparkingesta, ETL, ML, notebooks y el SQL Editor entero🏁 lectura, DML, DDL y CTAS
duckdbvivo; Junction ya no lo elige
karmamotor propio (Rust)desplegado, inerte
mlrunner · pgops estructuradas / legacyvivos
trinodemolido 08-09fuera

El puente

Existe porque Spark Connect no tiene cliente Node. Es Python/pyspark, y su trabajo real es sostener sesiones calientes: primera 5.832 ms · p50 caliente 125 ms = 47×.

Su código vive en un ConfigMap, no en una imagen: desplegar es kubectl create configmap --dry-run | apply + rollout.

⭐ Desde hoy la sesión nace con el namespace de resolución del inquilino (viaja en el token; el puente no tiene BD, a propósito) y lo re-emite en caliente si cambia — sin recrear la sesión, que costaría el 47×.

⚠️ No escala en horizontal: sesiones en proceso, 1 réplica.


7·bis · ⭐⭐⭐ CARBON SQL — el contrato, y quién parsea

El dialecto es un CONTRATO, no un puntero

Carbon SQL v1 = Apache Spark SQL 4.1.2, con los valores medidos:

ClaveValor
🔴ansi.enabledtrue
🔴storeAssignmentPolicyANSI — gobierna el casting al insertar
🔴ansi.doubleQuotedIdentifiersfalse"x" es un literal, no un identificador
🔴session.timeZoneGMT
🔴caseSensitive · timeParserPolicy · partitionOverwriteModefalse · CORRECTED · STATIC

· Identificador de Carbon SQL: el backtick. · La superficie se GENERA en fríodocs/warehouse-sql/carbon-sql-contract-v1.md, trinquete npm run check:carbon-sql-contract. · Bundle 2026_08: una deriva entre lo declarado y lo medido es un cambio de comportamiento ⇒ c0-pinneo.ts es su detector, y un guarda impide tocar un valor 🔴 sin subir el bundle. Los dos probados en rojo.

El namespace por defecto — qué es y qué no

Una cadena de fallback para nombres sin cualificar, no una restricción (Databricks: sesión → cluster → workspace; Snowflake: propiedad del usuario). ⚠️ Y son dos defaults distintos: colocación (LAKEHOUSE_NAMESPACE, dónde nace un dataset sin coordenada) y resolución (qué significa un nombre pelado). Hoy: resolución por workspace (workspaces.settings, sin migración); colocación sigue en main.test ⚠️ y no apunta a donde está el dato (§10).

⭐⭐ Quién parsea: el motor

No escribimos un parser — la industria no los escribe a mano (ANTLR/JavaCC/bison; Vitess lo dice explícitamente), y el jugador que hace nuestro trabajo no parsea: Unity Catalog saca el linaje del plan de Spark.

EXPLAIN EXTENDED  →  Parsed Logical Plan  →  plan-reader.ts  →  HechosDelPlan
                                                                 (verbo · destino ·
                                                                  privilegio · refs · TVF)

· Se usa el plan PARSED, no el analyzed: no invierte el orden de la autorización, los hechos existen aunque el nombre no resuelva, y no depende del inquilino ⇒ la caché es global. · El plan regala lo que ninguna heurística tenía: el destino ya partido, __required_write_privileges__ (Spark declara el privilegio) y UnresolvedTableValuedFunction (el vector H5, nativo). · ⛔ Todavía AUDITA, no manda. Los 9 exports de sql-parse.ts siguen decidiendo (scanTableRefs 40 usos · extractDmlTarget 24 · …). La sustitución es P·4·bis, y su criterio ya es evaluable (p4bis-criterio-de-salida.ts).

⚠️ Y el problema tiene nombre: dos parsers decidiendo sobre la misma entrada es un parser differential — clase de vulnerabilidad con CVEs (WAFFLED 2026: 1.207 bypasses; CVE-2026-52747). Dejar el auditor como destino permanente sería institucionalizarlo.


8 · Dónde corre cada cosa

PlanoDónde
AplicaciónVercel · app.paladio.io · la cara /api/iceberg
CómputoGKE trino-compute-clone (europe-west1) — Spark 4.1.2 + el puente
ServiciosRailway — Lakekeeper · Keycloak · OpenFGA · Postgres · duck-server · ml-runner · warehouse-writer · karma · Jupyter
DatosCloudflare R2

Techo real: CPUS_ALL_REGIONS = 12, subida denegada (trial), igual que Spot. ⚠️ Los cuatro pods en el MISMO nodo — no es una malla.

Encender y apagar el cómputo es MODO DE OPERACIÓN NORMAL

kubectl -n spark get pods                 # vacío = apagado, no averiado
kubectl -n spark patch sparkconnectserver carbon-connect --type=merge \
  -p '{"spec":{"clusterOperation":{"stopped":false}}}'
kubectl -n spark scale deploy/spark-bridge --replicas=1

9 · Lo que está PROBADO — con su gate

Vigentes del 04: Spark arranca · lee por la cara · la autorización vive en el catálogo · vending acotado · 8/8 de aislamiento · W0-W5 (el camino de escritura completo, el ledger a 0, CREATE TABLE y CTAS).

Lo añadido el 08-11:

QuéEvidencia
🏁B9 · USE sustituye al prólogola sesión nacía con current_schema() vacío — la causa, en un número
🏁C0-C5 · Carbon SQL es un contratopinneo medido · superficie generada · detector de deriva + guarda del bundle, los dos en rojo
🏁C1 · D1 arregladoel citado se decidía en 5 sitios, uno solo sensible al dialecto ⇒ ident.ts
🏁C2 · corpus de LECTURA114 sondas · 18 ejes · 113 SOPORTADA
🏁C3 · corpus de ESCRITURA16 sondas · 6 ejes · doble oráculo · clase CORRUPTOR · reversión por snapshot
🏁P·0-P·3 · el plan del motorEXPLAIN cubre 6 verbos sin efectos · lector 37/37 en frío · diferencial con las 7 discrepancias estudiadas
🏁P·4 · el auditor en sombracriterio de salida en el código y evaluable
🏁P·5·a · la 3ª ruta del mismo falloqualifyWriteTarget callándose cuando el destino venía cualificado
🏁P·6 · el guardián del DELETE① rechaza con el arreglo exacto · ② el rodeo funciona · ③ el control demuestra que sin él se pierde la fila
🏁P·5·b · namespace por inquilinolo adopta en 706 ms ⇒ caliente, sin recrear la sesión
🏁⭐⭐⭐ P·4·bis·1 · el latido — el criterio de salida era INSATISFACIBLEel auditor sólo escribía al discrepar y la ventana se anclaba en su primera escritura ⇒ con la sombra funcionando perfecto, jamás se cumplía. Latido con deltas · ⓪ distingue off de shadow sano · ③ mide queries auditadas (los 500 declarados no los leía nadie). 8 tests, verificados en rojo con la reversión confirmada

541 tests · tsc sin errores nuevos · W3·b y W4 verdes tras todo.


10 · Lo que está abierto

Bloqueantes de producto

⛔⛔ DELETE borra las filas cuyo predicado es UNKNOWN⛔⛔ CORRECCIÓN 08-11 tarde: NO estaba guardado en producción. Aquí ponía «GUARDADO en el editor (DELETE_NULL_GUARD=reject en prod)» y era falso por partida doble — el último deploy de producción es del 10-ago 22:22 y el código del guardián se commiteó el 11-ago 16:09, 18 h después; y la variable, puesta 2 h antes de medirlo, es posterior al deploy, así que ese runtime no la lee. El defecto lleva todo el día vivo también en el editor. Se cierra con el despliegue, no estaba cerrado
⛔⛔ Un BIGINT pierde precisión al llegar al clienteno es dialecto: es transporte. El DECIMAL sí viaja como texto ⇒ el arreglo es hacer lo mismo con los enteros de 64 bits
⚠️ El contrato cubre las sesiones del PUENTEnotebooks, pipelines e ingesta corren Spark con su propia configuración: sin medir si tienen los mismos interruptores
⚠️ LAKEHOUSE_NAMESPACE no apunta a donde está el datomain.test (9 datasets) vs main.default (84). Y los 20 con namespace NULL podrían ser los 20 fantasmas del censo de deriva — por medir
rowsAffected siempre 0el arreglo honesto: el conteo del ledger, que ya lo sabe
B7 · mantenimiento Icebergtablas sin compactar ni expirar. Llega con el TIEMPO
B3 · resultados grandes · time travel · vistas materializadas · INSERT … SELECT⭐ este último ya sin obstáculo: el plan da fuente y destino por separado
⚠️ La sombra del auditor NO está encendida en producciónPLAN_AUDITOR existe en Vercel pero su valor no es legible (vercel env pull vacía las sensibles) y cero latidos en access_events ⇒ el reloj de los 14 días no ha arrancado. Es la acción que desbloquea P·4·bis
⚠️ Los 7 interruptores 🔴 NO están pinneados: casan por DEFECTOel sparkConf del CR carbon-connect no contiene ninguno (verificado 08-11). El contrato los detecta (c0-pinneo), nada los impone, y el gate sólo corre a mano ⇒ un bump del operador los mueve en silencio
⛔⛔ Un catálogo/esquema creado por el usuario NO EXISTE en el lakehouseapp/api/dataspace/schemas/route.ts hace un INSERT en Postgres y nada másCREATE TABLE test.test1.xNoSuchNamespaceException. Es el mismo defecto que produjo el fantasma main.my_first_project. Medido: scripts/governance/n3-namespace-nuevo.ts. Approach completo en namespace-paradigm-approach.md
⚠️ La coordenada catalog.schema se TIRA, no se validarewriteDottedFromRefs colapsa FROM a.b.cFROM c y se resuelve por nombre dentro del workspace ⇒ test.test1.item y otro.cosa.item resuelven al mismo dataset. El tercer nivel es decorativo en el camino de lectura (N·5)
⚠️ N·1 está hecho y APAGADO, y encenderlo tal cual no bastaopaqueNamespaceFor tiene un solo llamante (iceberg-native-write.ts:132): cubre ingesta y pipelines, no el CREATE TABLE del editor. Y LAKEHOUSE_ENV está en Railway (prod) pero no en Vercel, con el token divergiendo de lo provisionado (carbon-test-w-…)
⚠️ El writeTarget del editor lo decide el SEGUNDO PARSER, ANTES de la puertaapp/api/sql-editor/execute/route.ts:33,352 llama a extractDmlTarget y se lo pasa a runQuery. Aunque runQuery pasara al plan al 100 %, el destino de escritura seguiría saliendo de un escáner de texto. Y no es un find&replace: el plan se pide sobre planSql (vistas expandidas, P·4·0) pero el writeTarget hace falta antes de construirlo ⇒ hay que reordenar quién pregunta qué

Deuda de sustrato

⚠️ Deriva Index↔físico (67 huérfanas · 20 fantasmas, 08-10) · BRIDGE_JWT_SECRET simétrico · el puente no escala en horizontal · OpenFGA inerte · sin gobernanza por usuario · clientes de Keycloak a mano · el cluster se llama trino-compute-clone · la VM de OpenMetadata perfila vía un motor que ya no existe.


11 · ⭐ Las prioridades — por DAÑO

P0 · lo que YA cobra intereses
     1. el DELETE fuera del editor      ← el guardián sólo cubre la puerta
     2. el BIGINT del transporte        ← corrompe dato de usuario, en silencio
     3. mantenimiento Iceberg (B7)      ← llega con el TIEMPO

P1 · el cambio de punto de vista
     4. LA PUERTA COMO PASO ÚNICO       ← es lo que extiende TODO lo de hoy
     5. P·4·bis: retirar el 2º parser   ← ya medible
     6. medir el dialecto fuera del puente

P2 · producto que aún no existe
     7. INSERT…SELECT · EXTERNAL_LINKS · time travel · vistas materializadas

P3 · sustrato  ⛔ TODO bajo el techo de CPUS_ALL_REGIONS = 12

La 4 es la que multiplica el resto. Todo lo construido hoy —lector, auditor, guardián— vive en runQuery, y sólo el SQL Editor lo llama. El valor escala con cuántas superficies pasen por la puerta.

Los horizontes

  • H2 · la gobernanza efectiva — encender OpenFGA, fila/columna, y la capa de usuario.
  • H3 · la proyección ontológica — CQRS: el catálogo es el SoT, el grafo un read-model.
  • H4 · los agentesel agente NUNCA ve las tablas.

12 · Las reglas que la práctica dejó

Las 25 del 04 siguen vigentes. Lo que añadió el 08-11:

  1. ⚠️⚠️⚠️ EL MODO DE FALLO DOMINANTE DE UNA JORNADA PUEDE SER LA MEDICIÓN, NO EL CÓDIGO. Nueve instrumentos mintieron antes que el sistema: comparar contra 'default' cuando el valor era main.default · un gate no idempotente · una aserción que pedía lo imposible · normalizar la mitad de un fichero congelado · un comparador que mentía contra la implementación nueva · una reversión de prueba que no llegó a aplicarse (falso verde) · y tres seguidas creyendo que un ConfigMap había derivado cuando era idéntico. ⇒ Ante «el sistema ha cambiado», sospechar primero del extractor.
  2. ⭐⭐ Si un artefacto se congela para compararlo, hay que normalizar TODOS sus campos — no sólo el que uno tenía en la cabeza.
  3. ⭐⭐ Una reversión de prueba tiene que CONFIRMAR que ha revertido. «El test pasa sin el arreglo» sólo significa algo si el arreglo se quitó de verdad.
  4. ⭐⭐⭐ Un criterio de salida sin instrumento que lo evalúe no gobierna nada, y el modo sombra se vuelve permanente por omisión. Lo cometió el mismo documento que lo advertía.
  5. ⭐⭐ Cero discrepancias con cero tráfico no es evidencia: es silencio. Un gate tiene que saber distinguirlos, o aprueba vacíamente.
  6. ⭐⭐ Declarado, no derivado. Un default que se calcula solo («el namespace donde hay más tablas») cambia solo, y un valor que se mueve sin que nadie lo decida es peor que uno equivocado: no se puede razonar sobre él.
  7. Dos parsers decidiendo sobre la misma entrada tienen nombre: parser differential, con CVEs. No es deuda estética.
  8. La condición tiene que ser LOCAL. Saltarse el prólogo «cuando hay namespace de sesión» habría resuelto la tabla equivocada; «cuando el namespace de esa tabla coincide» es siempre correcto.
  9. ⭐⭐⭐ UN CRITERIO DE SALIDA HAY QUE PROBARLO CONTRA SU CASO DE ÉXITO, NO SÓLO CONTRA EL DE FALLO. El de P·4 sólo podía arrancar su ventana cuando algo fallaba ⇒ era insatisfacible justo cuando el sistema iba bien. Un criterio que no se puede cumplir gobierna igual de poco que uno que no se puede evaluar — y lo cometió el mismo fichero que arreglaba la versión anterior del mismo fallo. La pregunta que lo caza: «si todo sale perfecto, ¿esto llega alguna vez a verde?»
  10. ⭐⭐ Elige la ARITMÉTICA para que el lector no tenga que ser listo. Un latido con contadores acumulados obliga a quien suma a saber qué runtime escribió cada fila; con deltas, la suma de las filas ES el total. Cuando el dato se puede emitir de forma que el agregado trivial sea correcto, emitirlo de la otra es sembrar un error futuro.
  11. Un número declarado que ningún lector lee es decoración. queriesSinDiscrepancia: 500 estaba en CRITERIO_DE_SALIDA desde P·4 y el gate nunca lo miraba. Un criterio se audita entero: cada campo, ¿quién lo evalúa?
  12. ⛔⛔⛔ UNA VARIABLE PUESTA NO ES UNA VARIABLE VIGENTE, Y UN COMMIT NO ES UN DESPLIEGUE. Este documento afirmaba que el DELETE estaba guardado en producción con la única prueba de que la variable existía en Vercel. El deploy vivo era 18 h anterior al código del guardián y 2 h anterior a la variable ⇒ el guardián llevaba todo el día apagado mientras el doc decía lo contrario. Es la hermana de la lección de legacy-plane-removal (un health-check dice que el proceso vive, no que el camino exista): la prueba de que algo está activo es una medición contra el runtime desplegado, jamás la existencia de su configuración. ⇒ Corolario operativo: el deploy de producción sale del working tree local (no hay integración con git: vercel inspect no trae metadata de commit), así que desplegar con el árbol sucio embarcaría trabajo a medias. Se despliega desde un git worktree limpio del commit, nunca desde el directorio de trabajo.

13 · Los documentos que importan

DocPara qué
estela foto completa. Manda sobre todo lo demás
⭐⭐⭐ parser-plan-motor-approach.mdque parsee el motor · P·0-P·7 con sus mediciones
⭐⭐⭐ namespace-paradigm-approach.mdel paradigma de nombres: por qué es difuso, el norte (nombre ≠ ubicación), y N·3-N·6 con sus gates
⭐⭐⭐ carbon-sql-contract-approach.mdel contrato + los corpus (C0-C5)
⭐⭐ carbon-sql-contract-v1.mdla superficie publicada — se GENERA
⭐⭐ federated-catalog-wedge.mdla cuña, por COPIA
⭐⭐ mapa-computo.mdFurnace y Rubix · ⚠️ su §2·bis («no hay parseo») queda superado por §7·bis
w-write-path-approach.md · w3-ledger-idempotencia.md · w4-create-table.md · b-spark-bridge-approach.mdel camino de escritura y el puente
🧊 04 · 03 · 02 · 01congelados

Los gates que se corren

# en frío — sin red, sin motor
npm run check:carbon-sql-contract          # ¿el doc casa con el código?
npm run check:plan-diferencial             # lector vs escáneres viejos
npx vitest run lib/compute lib/warehouse   # 533

# contra el motor (port-forward al puente)
npx tsx scripts/bridge/c0-pinneo.ts                    # ¿el motor casa con el contrato?
npx tsx scripts/bridge/c2-corpus-lectura.ts            # 114 sondas
npx tsx scripts/bridge/c3-corpus-escritura.ts          # 16 sondas · clase CORRUPTOR
npx tsx scripts/bridge/p1-capturar-fixtures.ts --trinquete
npx tsx scripts/bridge/p5b2-gate-namespace-por-workspace.ts

# por la puerta (necesitan .env.local)
npx dotenv -e .env.local -- npx tsx scripts/governance/w3-b-gate-puerta-escribe.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/w4-gate-create-table.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/p6-gate-guardian-delete.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/p4bis-criterio-de-salida.ts