Published

🧊 Sustrato · 06 — la arquitectura entera, de una pieza

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

🧊 Sustrato · 06 — la arquitectura entera, de una pieza

🧊 CONGELADO el 2026-08-12 — lo sucede sustrato-07

No se actualiza más. Refleja lo que era verdad el 08-11, y se conserva para poder
ver qué cambió y cuándo. Donde discrepe con el 07, manda el 07.

⚠️ Lo que este documento afirma y ya no es cierto: N·5 LECTURA figura como
bloqueante (se cerró el 08-12) · los 9 nombres duplicados como ambiguos (ya
direccionan) · y todo el encuadre de la ontología, que dio un viraje entero (§5 del
07).

Fecha de la medición: 2026-08-11 (madrugada). Sucede a
sustrato-05.md (08-11 noche), 04,
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 05, en seis líneas:

  1. 🏁 El paradigma de nombres está CERRADO: nombre ≠ ubicación, como la
    referencia. El namespace físico ya no espeja la taxonomía, y hay trinquete.
  2. 🏁 El criterio de salida del auditor era INSATISFACIBLE — y dos de sus tres
    condiciones estaban puestas a ojo. Ahora mide cobertura y diversidad.
  3. ⛔⛔ El guardián del DELETE llevaba TODO EL DÍA APAGADO en producción mientras
    el sustrato-05 afirmaba lo contrario. La única prueba aportada era que la
    variable existía en Vercel.
  4. ⭐⭐ Los dos frentes no eran paralelos: son el mismo. Gobernar el nombre exige
    dejar de tratar el SQL como texto.
  5. 7 de 8 inquilinos ya se sirven de su propio warehouse.
  6. ⚠️⚠️ La lección: de ~15 defectos cerrados, la mayoría eran de los INSTRUMENTOS,
    no del sistema
    (§12·38-44). Los dos que sí eran de producto los encontró
    escribir SQL de verdad, no ninguno de los 30 gates.

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.
  2. El cómputo es PLURAL y desechable — aunque hoy, de hecho, sea uno solo (§7).
  3. La gobernanza emana del catálogo y el grafo la heredará por relación.
  4. La cuña se sirve por COPIA, no por federación (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 (el único elegible)        (Iceberg REST gobernado)
     duckdb · karma · 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, y ⭐ 7 de esos 8 ya se sirven

⚠️ Lo que Iceberg NO da: seguridad de fila y columna — ni renombrar un namespace: el spec tiene renameTable/renameView y no renameNamespace (apache/iceberg #13023). ⭐ Y desde el 08-11 eso dejó de importarnos: con el namespace opaco no renombramos namespaces nunca (§7·ter).


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.

elegirMotorDeLectura(esWrite)spark en SUS DOS RAMAS, o lanza. Sin fallback. Es un hecho con consecuencias: §7·ter·4 retiró un guardián entero por él.

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

app/api/iceberg/v1/[...path]. Un Iceberg REST gobernado que hace lo que el motor no puede: autoriza por inquilino · acuña credencial acotada · filtra listados · resuelve nombres lógicos · firma el commit · materializa tablas nuevas en Index · ⭐ y desde hoy traduce también el NAMESPACE, no sólo el nombre de tabla (§7·ter).

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

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

Sigue siendo sólo el SQL Editor. Pipelines, ingesta y notebooks escriben por iceberg-native-write / lakehouse-landing. ⭐ Pero ya no están fuera del paradigma de nombres: el canary alcanza los dos runtimes, así que acuñan namespace opaco igual.


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

Y el namespace opaco desbloquea lo que faltaba: OpenFGA hereda jerárquicamente sobre namespaces, así que con {env}.w_{workspace} la tenencia por fin es aplicable en el catálogo. Era el motivo estructural de los 360 grants, 0 de usuario.

⚠️ DOS caminos, DOS aplicadores — sin cambios

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

6 · Almacenamiento y credenciales

Cloudflare R2, bucket lakehouse. Perímetro explícito: warehouse-writer RW · duck-server RO · Lakekeeper · ml-runner ninguna. Vending STS 900 s.

Y el aislamiento por inquilino dejó de ser teórico: 7 de 8 organizaciones se sirven de su propio warehouse (carbon-prod-w-<hex>) con su propio key-prefix (prod/w_<hex>). Comando: n2-que-es-w-uuid.ts.


7 · Cómputo

MotorPapelEstado
sparkingesta, ETL, ML, notebooks y el SQL Editor entero🏁 el ÚNICO elegible
duckdbvivo; Junction no lo elige en ninguna rama
karma · mlrunner · pgvivos, no elegibles para live-SQL
trinodemolido 08-09fuera

El puente (Python/pyspark) existe porque Spark Connect no tiene cliente Node. Sostiene sesiones calientes: primera 5.832 ms · p50 caliente 125 ms = 47×. Su código vive en un ConfigMap. ⚠️ No escala en horizontal: 1 réplica.

N catálogos en una sesión cuestan ~1.062 ms cada uno, UNA vez, perezoso, sin impuesto por query y sin degradar a los ya registrados (scripts/bridge/n1-coste-n-catalogos.ts). ⇒ el coste no es el obstáculo de N·6.


7·bis · CARBON SQL — el contrato

Carbon SQL v1 = Apache Spark SQL 4.1.2, bundle 2026_08. Los 7 interruptores medidos y verdes (c0-pinneo.ts, corrido hoy: motor 4.1.2, los 6 semánticos casan, los 5 conductuales casan).

⚠️ Y los interruptores 🔴 NO están pinneados en el cluster: el sparkConf del CR carbon-connect no contiene ninguno. Casan porque son los defaults de Spark 4.1.2. El contrato los DETECTA; nada los IMPONE, y el gate sólo corre a mano.

⭐ Y una cláusula se retiró: ORDER BY ya no se bloquea

null-ordering rechazaba cualquier ORDER BY sin NULLS explícito — la cláusula más común de SQL después de WHERE; 4 de 15 queries reales caían por ella. Su propio comentario declaraba la condición de retirada —«mientras convivan dos motores»— y elegirMotorDeLectura devuelve spark en sus dos ramas. ⇒ soloConDosMotores: true.

La entrada NO se borra del catálogo: se sigue detectando, así que el conocimiento se conserva y el día que entre un 2º motor vuelve a bloquear sola.

⭐⭐ Quién parsea: el motor — pero todavía AUDITA, no manda

EXPLAIN EXTENDED → Parsed Logical Plan → plan-reader.ts → HechosDelPlan

Los 9 exports de sql-parse.ts siguen decidiendo. La sustitución es P·4·bis, y su criterio está por fin bien medido (§9).


7·ter · ⭐⭐⭐ EL PARADIGMA DE NOMBRES — cerrado

El principio, y por qué es el de la referencia

El namespace físico espeja lo INMUTABLE (el inquilino), no lo EDITABLE (la taxonomía).

lo que escribes    catalog . schema . tabla         (editable, en Index)
                            ↕  traduce LA CARA
donde viven bytes  {env}.w_{workspace} / ds_{uuid}  (opaco, inmutable)

Unity Catalog hace exactamente esto, y no lo contrario: guarda las tablas gestionadas en <managed-location>/tables/<table_id UUID> y los motores resuelven por nombre a través del catálogo, nunca por ruta. UC es limpio porque SEPARÓ el nombre de la ubicación; Carbon era el que los tenía pegados.

Lo hecho, con su gate

QuéComando
N·3·ael token de entorno decidido (prod) y reconciliado 8/8n3a-token-de-entorno.ts → 0
N·3·bel CREATE TABLE del editor entra al régimen opaco; la cara traduce el namespacen3-namespace-nuevo.ts
N·3·ccanary desplegadon3-namespace-nuevo.tsverde de punta a punta
N·3·dcontrol negativo: el canary está ACOTADOn3d-control-negativo.ts
N·3·esin regresión en el régimen del espejon3e-regresion-espejo.ts
N·4el espejo RETIRADO de los dos caminos de nacimiento + trinquetenpm run check:sin-espejo
N·5 escriturala coordenada gobierna el destinon5d-gate-escritura-gobernada.ts
N·6·a7 de 8 inquilinos en su propio warehousen6a-encender-warehouse-propio.ts

El canary está en * en Vercel Y Railway. Estado de las tablas del inquilino con datos: 84 main.default · 20 (null) · 9 main.test · **7 prod.w_7b500f4d…** · 2 karma.test · 1 main.my_first_project.

⚠️ Lo que N·6 resultó ser

Se planteó como «el catálogo del usuario ES un warehouse» — una pregunta de nombres. N·1/N·4 ya resolvieron los nombres, así que lo que queda es aislamiento de ALMACENAMIENTO, que el código ya modelaba como opt-in con motivo (dedicated_storage). No era una fase pendiente: era una capacidad diseñada y sin encender.

Y no hay versión incremental: la cara resuelve el warehouse por inquilino y por petición, no por tabla. Medido: 7 inquilinos → gratis · 1 → 123 tablas / 128.012 filas quedarían inalcanzables (n6-coste-de-la-mudanza.ts).


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
WorkersRailway «Carbon Jobs» — ingesta, pipelines, provisioning
ServiciosRailway — Lakekeeper · Keycloak · OpenFGA · Postgres · duck-server · ml-runner · warehouse-writer · karma · Jupyter
DatosCloudflare R2

Techo real: CPUS_ALL_REGIONS = 12, subida denegada. ⚠️ Los cuatro pods en el MISMO nodo.

⛔⛔ El despliegue de producción sale del WORKING TREE LOCAL

vercel inspect no devuelve metadata de commit: no hay integración con git. ⇒ vercel --prod embarca el árbol sucio. Se despliega desde un git worktree limpio. Y vercel env pull no devuelve valores: 160 de 169 vacías.

Y el 08-12 se vio el reverso de esa regla, que es peor: el arreglo que impide subir los 800 MB de .claude/ vivía sólo en el working tree desde el 08-09. Es decir: desplegar desde un worktree limpio habría dejado fuera precisamente el arreglo que hace que el deploy funcione. ⇒ un árbol sucio no sólo mete lo que no quieres: también esconde lo que sí necesitas. Commiteado en 360b2d8.


9 · Lo que está PROBADO — con su gate

Vigentes: Spark arranca · lee por la cara · vending acotado · 8/8 de aislamiento · W0-W5 · C0-C5 (Carbon SQL como contrato) · P·0-P·6 (el plan del motor).

Lo añadido el 08-11 (madrugada):

QuéEvidencia
🏁El paradigma de nombres, enteroN·3·a→e · N·4 · N·5 escritura · N·6·a, cada uno con su gate
🏁CREATE TABLE test.test1.item + UPDATE funcionann3-namespace-nuevo.ts verde de punta a punta
🏁El latido — el criterio era insatisfaciblep4bis-criterio-de-salida.ts
🏁El arnés del corpus — 17 formas por la puerta, 0 no-vistap4bis-arnes-corpus.ts
🏁La comparación del auditor, simétrica2 tests; lo destapó N·3
🏁ORDER BY desbloqueado17/17 queries reales corren
🏁Una tabla con datos, rescatada: animals era ilegible por CUALQUIER nombreSELECT nickname, weight_kg FROM animals → 5 filas

740 tests · tsc sin errores nuevos · 22 commits desde el 05.

⭐ Y el 08-12 (madrugada): desplegado

d12ff92 está en produccióndpl_HGmb1FbfBkChpVUN4kfcorgh6y9p, READY, target: production, y vercel inspect https://app.paladio.io confirma que es ese deployment el que sirve el alias. Es la primera vez que sale el arreglo de e965102 (el PLAN_AUDITOR que se apagaba en silencio), así que el auditor ya está midiendo en prod con el modo visible.

⚠️ Lo que el árbol escondía, encontrado al pasarle el check antes de commitear:

28.227 líneas de UI sin commitear, 118 ficheros untrackedEn ningún commit de ninguna rama (git log --all -S). Un git clean se las llevaba, y cualquier vercel --prod desde el root las embarcaba sin revisar. Commiteado en d12ff92
el .vercelignore con /.claude/Ver §8. Commiteado en 360b2d8
las 4 sondas de nombres + el filtro estricto de p4bisCommiteado en 516d5a1

⚠️ Y una trampa de medición que hay que anotar antes de que muerda otra vez: en producción, una ruta que no está en isPublicRoute de proxy.ts devuelve 404, no 401. /api/health y /api/files dan 404 igual que /dev-workspace-home-previewun 404 no es prueba de que la ruta no se haya desplegado. El control correcto es comparar contra una ruta pública del mismo build: /dev-sql-editor-carbon-preview y /dev-files-carbon-preview dan 200. (Next 16: el middleware.ts se llama proxy.ts.)

🏁 9·bis · N·5 CERRADO — la coordenada direcciona (2026-08-12)

Verificado en producción por el owner, que es la única prueba que cuenta: marcó karma.test.observatorios con un UPDATE y main.default.observatorios quedó intacto. Antes, con nueve nombres duplicados, ganaba el orden del Map.

No era un defecto: eran cuatro, encadenados, y cada uno tapaba al siguiente.

Qué
1El colapso. rewriteDottedFromRefs tiraba FROM a.b.cFROM c — no por descuido, sino porque el nombre tenía que quedar pelado para casar con la CTE homónima que el plan antepone
2El cuerpo de la CTE duplicaba la coordenada (main.default. + el token entero). Vinculaba bien y emitía mal — la peor de las dos
3⭐⭐ El régimen de coordenada era INERTE en la lectura: duck-plan hacía catalog.get(token) a pelo, sin pasar por resolveCatalogRef. NS_COORDENADA gobernaba la escritura y no la lectura
4El \b no casaba detrás de una comilla en qualifyWriteTarget: las dos ramas citadas no podían casar jamás. Invisible mientras el colapso dejaba el destino pelado

⭐ La regla se extrajo a query/coordenada.ts —un módulo sin dependencias, porque importar catalog.ts arrastraría Clerk y Supabase a un planificador que es código puro— para que los dos caminos usen la misma, no dos copias.

Y el instrumento estaba roto: n4-la-coordenada-desambigua.ts leía r.baseTables cuando el resultado es {ok, plan} ⇒ imprimía (sin bindings) en los ocho casos y parecía un hallazgo. Se arregló antes de rediseñar nada.

Gate: check:plan-diferencial pasó de 4 empates que pierden coordenada a 0.

El criterio de P·4·bis, hoy

⓪ el auditor ESTÁ midiendo ...... SÍ ✅
① sin no-vista sin explicar ..... SÍ ✅   (1 explicada, en `p4bis-discrepancias.ts`)
② 17 formas del corpus .......... SÍ ✅
③ 12 formas DISTINTAS de usuario  ⏳ (0)  ← lo único que falta

10 · Lo que está abierto

Bloqueantes de producto

③ de P·4·bis12 formas distintas de usuario por el SQL Editor. El registro se desplegó a las 22:57; las queries del owner anteriores a eso no contaron
🏁 N·5 LECTURACERRADO el 2026-08-12, desplegado y verificado en vivo por el owner con los dos observatorios: se marcó el de karma.test y el de main.default quedó intacto. Ver §9·bis
🏁 9 nombres duplicadosSiguen siendo nueve, pero ya no son ambiguos: la coordenada direcciona. Lo que queda es que un nombre PELADO sigue dependiendo del default de sesión — que es lo que significa un nombre corto en cualquier catálogo
⛔⛔ Un BIGINT pierde precisión al llegar al clientees transporte, no dialecto. Sin arreglar
⚠️ El DELETE fuera del editorlos pipelines no pasan por la puerta ⇒ el guardián no los cubre
⚠️ Los 7 interruptores 🔴 sin pinnearcasan por defecto de Spark 4.1.2, no por decisión
B7 · mantenimiento Icebergsin compactar ni expirar
INSERT en el editorno admitido (B3). Es lo que deja ② en 17 y no en 19

Deuda de sustrato

⚠️ 4 fallos PREEXISTENTES en lib/lakehouse (read-client, namespace-resolution) — comprobados con git stash: salen igual sin los cambios de hoy · la mudanza del inquilino con 123 tablas · BRIDGE_JWT_SECRET simétrico · el puente no escala · OpenFGA inerte · sin gobernanza por usuario · el cluster se llama trino-compute-clone.


11 · ⭐ Las prioridades — por DAÑO

P0 · lo que YA cobra intereses
     1. ③ y con él P·4·bis → enforce   ← cuesta 12 queries del owner
     2. N·5 LECTURA                    ← 9 nombres ambiguos, dato real
     3. el BIGINT del transporte       ← corrompe dato de usuario, en silencio
     4. el DELETE fuera del editor     ← el guardián sólo cubre la puerta

P1 · el cambio de punto de vista
     5. LA PUERTA COMO PASO ÚNICO      ← extiende TODO lo construido
     6. pinnear los interruptores 🔴
     7. mantenimiento Iceberg (B7)

P2 · producto que aún no existe
     8. INSERT · time travel · vistas materializadas · resultados grandes

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

Los horizontes

  • H2 · la gobernanza efectiva — encender OpenFGA. ⭐ El namespace opaco ya le puso el suelo: la tenencia es aplicable en el catálogo.
  • 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 37 anteriores siguen vigentes. Lo que añadió el 08-11 (madrugada):

  1. ⭐⭐⭐ UN NÚMERO EN UN CRITERIO TIENE QUE DERIVARSE DE ALGO. «500 queries», «300 queries», «14 días»: los tres estaban puestos a ojo, y el de 300 además medía repetición cuando quería medir diversidad (el auditor cachea por hash: N ejecuciones de la misma query son UNA medición repetida N veces). ⇒ un umbral sin derivación es una opinión disfrazada de umbral.
  2. ⭐⭐ UN INSTRUMENTO QUE MEZCLA DOS CATEGORÍAS DA UN NÚMERO ALARMANTE Y FALSO. «7 datasets romperían» eran en realidad «7 que ya no resolvían». Un número falso hacia arriba bloquea un cambio seguro, que es tan caro como aprobar uno peligroso.
  3. ⭐⭐ REIMPLEMENTAR UNA CONDICIÓN EN VEZ DE LEERLA. El censo comprobó state === 'active' cuando la condición real era isDedicated() (activo Y motivo Y id) y concluyó exactamente lo contrario de la verdad.
  4. ⭐⭐ UNA VENTANA DE TIEMPO MIDE EL RELOJ, NO LA CORRIDA. El arnés contó la discrepancia de la ejecución anterior y declaró que el arreglo no servía. Sí servía.
  5. ⭐⭐ SE MARCA LO ARTIFICIAL, Y LO MARCADO NUNCA CUENTA. Y el filtro tiene que ser estricto: «todo menos el arnés» dejaba colar cualquier otro origen como si fuera tráfico de usuario. Un gate que no separa su propio ruido se aprueba a sí mismo.
  6. ⭐⭐ UN GUARDIÁN CON CONDICIÓN DE RETIRADA ESCRITA HAY QUE RELEERLA CUANDO EL MUNDO CAMBIA. null-ordering decía «mientras convivan dos motores»; hacía tiempo que Spark era el único, y seguía bloqueando la cláusula más común de SQL. Retirar el bloqueo sin borrar la entrada conserva el conocimiento y hace que vuelva sola.
  7. ⭐⭐⭐ ESCRIBIR SQL DE VERDAD ENCUENTRA LO QUE 30 GATES NO MIRAN. Los dos únicos defectos de PRODUCTO del día —una tabla ilegible por cualquier nombre y el ORDER BY bloqueado— salieron de redactar 15 queries reales contra tablas reales. Ningún gate los veía porque todos medían mecanismos, y ninguno medía el uso.
  8. ⚠️ Y el balance del día, que es la regla que las engloba: de ~15 defectos cerrados, la mayoría eran de los instrumentos, no del sistema. El sustrato aguantó; lo que fallaba era cómo lo mirábamos.

13 · Los documentos que importan

DocPara qué
estela foto completa. Manda sobre todo lo demás
⭐⭐⭐ namespace-paradigm-approach.mdel paradigma de nombres · N·3-N·6 con sus mediciones
⭐⭐⭐ parser-plan-motor-approach.mdque parsee el motor · P·0-P·7
⭐⭐ carbon-sql-contract-approach.mdel contrato + los corpus
⭐⭐ carbon-sql-contract-v1.mdla superficie publicada — se GENERA
⭐⭐ federated-catalog-wedge.md · mapa-computo.mdla cuña · Furnace y Rubix
🧊 05 · 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
npm run check:sin-espejo                   # ⭐ ¿ha vuelto la fórmula del espejo?
npx vitest run lib/compute lib/warehouse lib/governance     # 740

# contra el motor (port-forward al puente)
npx tsx scripts/bridge/c0-pinneo.ts                    # ¿el motor casa con el contrato?
npx tsx scripts/bridge/n1-coste-n-catalogos.ts         # el coste de N catálogos

# por la puerta (necesitan .env.local)
npx dotenv -e .env.local -- npx tsx scripts/governance/n3-namespace-nuevo.ts       # ⭐ el paradigma
npx dotenv -e .env.local -- npx tsx scripts/governance/n3d-control-negativo.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/n3e-regresion-espejo.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/n5d-gate-escritura-gobernada.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/p4bis-arnes-corpus.ts       # ⭐ la cobertura
npx dotenv -e .env.local -- npx tsx scripts/governance/p4bis-criterio-de-salida.ts # ⭐ ¿enforce ya?
npx dotenv -e .env.local -- npx tsx scripts/warehouse/n2-que-es-w-uuid.ts
npx dotenv -e .env.local -- npx tsx scripts/warehouse/ns-fisicos.ts

14 · ⭐ Lo primero que hay que hacer en la próxima sesión

Doce queries distintas por el SQL Editor, y correr p4bis-criterio-de-salida.ts. Están escritas y validadas 17/17 en scripts/governance/p4bis-queries.ts.

Ya está desplegado (d12ff92, §9), así que no hay nada que esperar.

⚠️ Y esto no lo puede cerrar un script. El origen lo pone PLAN_AUDITOR_ORIGEN y el gate sólo cuenta forma:usuario —la que no lleva marca—, así que un arnés se marca solo y no suma, y correrlo sin marcar sería falsificar el gate (regla 42). Las 12 tienen que entrar tecleadas en el SQL Editor.

⚠️ Y si ③ sigue en 0 tras hacerlo, no es que falten queries: sería la hipótesis abierta desde el principio — que en serverless la escritura del auditor no sobrevive a la congelación del runtime. El arreglo está identificado (after() de Next 16) y ese caso también es información.