Published

Approach técnico · I·2 + I·3b — la puerta de escritura de Index y el carrier de procedencia

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

Approach técnico · I·2 + I·3b — la puerta de escritura de Index y el carrier de procedencia

Documento de ejecución (2026-07-30). El QUÉ está en warehouse-index.md §5.3 y §7; el porqué del ORDEN, en index-handoff.md §7. Esto es el cómo: los call-sites exactos, las fases, la guarda de cada una y lo que mide.

Por qué I·2 e I·3b van juntos y no seguidos. «Quién escribe la gobernanza» y «dónde la escribe» son la misma pregunta. Mover el carrier sin una puerta obliga a tocar cada productor dos veces; poner la puerta sin mover el carrier la deja escribiendo en la columna que se va a retirar.

La ventana, dicha por adelantado. El censo mide 0 filas con procedencia en produccióneste movimiento no necesita backfill. Es la única vez que va a ser gratis. Cada derivación que nazca a partir de hoy lo encarece.


0 · Lo que el reconocimiento corrigió

0.1 · «194 puntos de acceso a datasets» es cierto y es el número equivocado

Medido hoy: 202 from('datasets') en app/+lib/+scripts/. Pero de ésos:

~132LECTURASno le tocan a la puerta de escritura
70escrituras (.insert/.update/.upsert)la mayoría son UPDATE de estadística — y ésos los retira W·1, no I·2
21NACIMIENTOSéste es el número de I·2

La puerta no tiene que interceptar 194 sitios. Tiene que interceptar el NACIMIENTO, y son 21 — de los que 7 ya pasan por el chokepoint. El trabajo real son 14.

Encuadrar I·2 como «migrar 194 accesos» la hace parecer inabordable y por eso lleva pendiente. Encuadrarla como «14 nacimientos» la hace un entregable.

0.2 · El chokepoint ya existe, ya está duplicado, y las copias YA han driftado

createDatasetArtifact (dataset-output-materialiser.ts:709) hace las dos inserciones —satélite + fila de datasets— y estampa schema_id y tier. Cinco sitios copian su cuerpo en vez de llamarla, y al copiarlo perdieron campos:

Nacimiento fuera del chokepointfile_idprocedenciatierschema_id
transformPaths.ts:555 (transform)
transformPaths.ts:983 (join)
transformPaths.ts:1637 (split)
runtime.ts:380 (transform legacy)
checkpoint-ephemeral.ts:159 (checkpoint)NULL

🔴 Esto no es deuda estética: es un defecto vivo. El materialiser propaga el tier con inheritedTier([...]) porque «derivar limpia o transforma, no cambia la capa». Las tres derivaciones de transformPaths no lo hacen: un transform/join/split sobre una tabla gold_ produce un output sin tier. La regla del medallón se cumple por un camino y se incumple por el otro, y la causa es exactamente que hay dos cuerpos donde debería haber uno.

(No verificado: si algún paso posterior re-estampa el tier de esos outputs. El grep de tier en transformPaths.ts da cero.)

Y los otros nueve nacimientos entran por supabase.from('datasets').insert() directo: datasets/ingest:137 · datasets/parse:361 · datasets/route:386 y :435 · notebooks/…/upload:197 · sql-editor/execute:406 · fileBackedDatasetUpload:175 y :434 · dataset-writer:1513.

0.3 · Los nacimientos SIN satélite no son un accidente: son el paradigma nuevo

I·3a cerró el lector para los objetos con file_id NULL, y el censo contó 5 hoy. Pero mirando quién los produce, la cifra va a subir por diseño — el árbol de Files deja de ser el sitio donde nacen las cosas:

CaminoNace con file_id
sql-editor/execute:406CREATE VIEWfile_id: null explícito
datasets/route:390 — creación Warehouse-nativefile_id: null explícito
notebooks/…/upload:201file_id: null explícito
checkpoint-ephemeral:159file_id NULL en el SQL
fileBackedDatasetUpload:182,439fileRecord?.id ?? null

Ése es el argumento definitivo de I·3b, y no es «el contrato D1 lo declara canónico». Es que el carrier de hoy vive en una tabla que los objetos nuevos ya no tienen. project_files.metadata no es un carrier discutible: es un carrier que se está quedando sin filas.

Y hay un caso que lo enseña entero: una VIEW tiene procedencia trivialmente derivable —las tablas de su propio view_definition— y hoy nace con metadata vacía. gold_order_risk_episodes (2026-07-11) es esa fila.


1 · El estado de partida

I·3a · el lectorHECHO (f3e243b) — resolveCarrier(canonical, satellite), canónico gana. Inerte para los 101 satelitados
El censoscripts/dataspaces/i3-carrier-census.ts0/101 con procedencia, 5 sin carrier alcanzable
La puerta de LECTURAloadItemForConsumption es única y valida tenencia
La puerta de ESCRITURA⛔ no existe. 21 nacimientos, 7 por el chokepoint

2 · Las fases

Ordenadas por «lo que reduce superficie sin depender de ninguna decisión» primero. Ninguna requiere la siguiente.

G·0 · El ratchet, ANTES que nada (coste: muy bajo · el que sostiene a los demás)

scripts/check-dataset-births.ts: falla si aparece un INSERT INTO datasets o un from('datasets').insert( fuera de una allowlist explícita, con conteo por fichero para que sólo pueda bajar — exactamente la forma de check-dataset-rows-seam y check-namespace-resolution, y cableado al job guards que ya es bloqueante.

Por qué va primero: sin él, cada nacimiento que se migre abajo puede reaparecer en el PR siguiente y nadie se entera. Es la lección de N·-1, ya pagada una vez.

⚠️ Y la lección de N·0 que hay que no repetir: el ratchet no debe contar prosa. La primera versión del de namespace contaba menciones en JSDoc y daba un falso positivo. Quitar comentarios y descontar la definición. Y verificar que caza, inyectando una regresión real — una guarda que no se ha visto fallar no es una guarda.

I·2a · Unificar los cinco cuerpos duplicados (coste: bajo · riesgo: bajo · reversible)

Los cinco nacimientos de §0.2 pasan a llamar a createDatasetArtifact. No es un refactor cosmético: arregla el tier perdido de las tres derivaciones de transformPaths.

Lo que hay que resolver al hacerlo, y es la única fricción real: checkpoint-ephemeral nace con file_id NULL y createDatasetArtifact siempre crea satélite. Hace falta admitir el nacimiento sin satélite en la firma (parentId: null ya existe; falta satellite: false), que es justo lo que I·3b necesita después. Se hace aquí, no se difiere.

Cómo se sabe que está bien: un output de transform/join/split sobre un source gold_ nace con tier='gold'. Hoy nace sin tier. Es una aserción, no una impresión.

I·2b · La puerta (coste: medio)

createDatasetArtifact se promueve a createGovernedObject y se le da lo que hoy no tiene y la puerta de lectura sí:

  1. Validación de tenencia — el simétrico de la que runQuery ya hace por tabla (junction.ts, el rechazo 'cross-workspace').
  2. La coordenada obligatoriaschema_id explícito o el trigger; nunca «lo que caiga».
  3. El carrier en un solo sitio — es lo que hace posible I·3b sin tocar 14 productores.

Los nueve nacimientos por supabase.insert() se migran de uno en uno, cada uno bajando el conteo del ratchet. dataset-writer:1513 (el polling) es el más caliente: va el último.

⭐ I·3b · Mover la procedencia a datasets.metadata (coste: bajo · la ventana)

Con la puerta puesta, es un cambio en createGovernedObject: la procedencia se escribe en datasets.metadata en vez de en el satélite. El lector ya la prefiere (I·3a).

Sin backfill, y eso está medido, no supuesto: 0 de 101 filas llevan procedencia.

⚠️ Sólo se mueve la PROCEDENCIA. project_files.metadata lleva config viva en las 101 filas —transaction_policy, formatos primario/adicionales, media_sync, schema_label, item_count— y su lector es otro (fileBackedDatasetUpload.ts:548-557). Mover eso es otro entregable y no es éste. Confundirlos convierte una fase barata en una migración.

I·3c · Sembrar la procedencia que hoy se tira (coste: bajo · el que hace visible el pilar)

Con el carrier en su sitio, tres productores pueden empezar a declarar lo que ya saben y hoy descartan:

  • CREATE VIEWsourceDatasetIds = las tablas de su propio view_definition. El planificador del SQL Editor ya las extrae (scanTableRefs, de H5). Cero trabajo de análisis nuevo.
  • checkpoint-ephemeral → deriva de un dataset que lee en la línea de arriba (SELECT project_id, name, schema … FROM datasets WHERE id = $1) y no lo apunta.
  • transformPaths:1637 (split) y runtime.ts:380 → ya calculan su source; sólo les falta el mergeUpstreamProvenance que sus hermanos sí hacen.

Es lo que convierte el 0/101 en un número distinto de cero por primera vez — y el censo lo mide sin tocar nada.


3 · Qué MIDE cada fase (no qué hace)

FaseReversibleQué mide
G·0 ratchetcuántos nacimientos hay fuera de la puerta (hoy: 14)
I·2a unificarsi un derivado de un gold_ hereda el tier ← hoy NO por tres caminos
I·2b la puertasí, por sitiocuántos nacimientos quedan sin validar tenencia
I·3b el carriersí (el lector acepta las dos)nada cambia: es el movimiento inerte que abre I·3c
I·3c sembrari3-carrier-census deja de decir 0

El criterio de parada, dicho por adelantado: si al unificar los cinco cuerpos (I·2a) aparece que alguno diverge por un motivo deliberado y documentado —y no por copia envejecida—, la unificación se para ahí y ese sitio se queda fuera con su porqué escrito. Una puerta con una excepción explicada es mejor que una puerta que rompe un caso.


4 · Lo que NO se toca en este tramo

  • Las ~50 escrituras de estadística (row_count, size_bytes, status). Son W·1, y su autoridad ya la reclama el reconciliador de N·2c. Meterlas aquí mezcla dos planes.
  • project_files como árbol de autoría. datasets.project_id/file_id no son legacy (20261249 revocó esa etiqueta y la BD viva lo confirma: project_id es NOT NULL). I·3b mueve un bloque de un JSONB, no retira el satélite.
  • La config file-backed. §I·3b, aviso.
  • El CAS y el catálogo. Nada de este tramo los roza.

5 · Decisiones antes de empezar

  1. ¿createGovernedObject valida tenencia, o sólo registra? Recomendación: valida. Una puerta que no puede negar no es una puerta — es la misma frase que warehouse-index.md §5.2 usa contra las siete piezas declaradas-y-no-aplicadas, y sería irónico repetirla aquí.
  2. ¿Se admite el nacimiento sin satélite en la firma? Recomendación: sí, en I·2a. Ya hay cinco caminos que lo hacen (§0.3) y forzarles satélite sería mover el producto hacia atrás.
  3. ¿I·3c siembra también los datasets ya existentes? Recomendación: no. Sembrar hacia atrás es inventar linaje que no se observó. Los 28 derivados de hoy nacieron sin procedencia y deben seguir diciendo que no la tienen: un null honesto vale más que un grafo verosímil.

Cruza con: warehouse-index.md (qué es Index · §5.3 la asimetría) · index-handoff.md (el estado y el orden) · index-frontier.md (el diseño de la frontera) · warehouse-scaffolding-retirement.md (el plan W, con el que I·2 NO se solapa).