Junction — mapa de ejecución (los puntos EXACTOS a tocar)
Companion quirúrgico de junction.md (2026-07-10). Antes de tocar una línea: la imagen completa de touch-points + el inventario de referencias al contrato legacy a limpiar (código y docs), para no dejar cabos sueltos. junction.md = el qué/por qué; esto = el exactamente qué tocar y en qué orden.
A · El hecho central: HOY hay DOS entradas
La puerta NO es la única entrada. Un consumidor accede a las filas de un dataset por una de dos vías, y ese es el desorden que Junction consolida:
(1) Por la puerta loadItemForConsumption(ref, intent, principal) — el contrato gobernado. Consumidores REALES en código (~15):
app/api/datasets/route.ts (browse) · dataspace/item/route.ts + sample · model/objects/route.ts + sync.ts · model/schema/route.ts · graphs/explorer/{values,search,data} · datasets/[id]/export · sdk/v1/datasets/[…]/rows · dashboards/query/route.ts (vía hydration) · table-export/run-export.ts:368 · graphs/kuzu-engine.ts · pipelines/source-resolver.ts:57 (pipeline.source) · pipelines/native-output-write.ts (write) · media-set/item.
(2) Por los routers a pelo resolveDatasetRowSink/Source(...) — saltándose el contrato de la puerta. Casi todo el WRITE va por aquí: polling/dataset-writer.ts:915,1134 (sync.full/incremental/append) · pipelines/transformPaths.ts:363,797,1383,1384 (pipeline.output) · pipelines/runtime.ts:424 · pipelines/model-node-materialiser.ts · pipelines/dataset-output-materialiser.ts:372 · cdc/sink.ts:99 · datasets/rows/route.ts (row.edit) · datasets/parse + datasets/ingest (ingest.api/stream) · pipelines/[id]/manual-table · notebooks/[…]/datasets/upload + sdk/[…]/files/upload + fileBackedDatasetUpload.ts (dataset.manifest) · lecturas: studio/{training-pair,digest}-worker.ts.
(Nota: los __src/__lookupSrc con prefijo __ son MARKERS de conformidad del seam —scripts/check-dataset-rows-seam.ts—, no el read real; no confundir con un consumidor.)
(3) Fuera de ambos (bypass total): sql-editor lee duck-direct (route.ts:1092 buildDuckReadPlan→:1109 duckQuery) sin pasar por la puerta ni los routers; dashboards traduce con su propio jsonbCast (route.ts:102).
Objetivo Junction: UNA entrada. Los directos-a-router y el duck-direct se consolidan bajo la puerta (read por intent
read/stream/aggregate, write por intentwrite, live-SQL por el nuevo intentquery). Elfacade.write()deja de tener el brazo PG que lanza a medida que los writers rezagados enrutan por la puerta.
B · Touch-points del CONTRATO (F1 · intent query + GovernanceContext)
Forma decidida (recomendación, 2026-07-10):
runQuerycomo entrada PEER deloadItemForConsumption(no un método delDatasetHandleitem-céntrico), porque una query live-SQL toca N tablas y no tiene unItemRefprimario. La puerta ={ loadItemForConsumption (items) , runQuery (SQL) }— dos formas de una frontera con core compartido (resolución+gobernanza+motor).IntentNO gana un valor'query': una query ES read o write, derivado del clasificadorclassify_statement(F4.2). Ver la discusión en el chat / §G.
lib/lakehouse/consumption-contract.ts (solo-tipos — todo deriva de aquí):
:44type ContractVersion = 'v1.1'→ bump a'v1.2'(el enveloperunQueryes un cambio de FORMA del contrato).- Nuevo
interface QueryRequest { sql; catalog: CatalogBinding; session: GovernanceContext }+interface CatalogBinding(las N tablas referenciadas → FQNs físicas + descriptor + veredicto de frescura POR TABLA) — el envelope tipado (§2 junction.md), NO Substrait. +interface QueryResult { columns; rows | arrow }/ reusarPage; el write devuelveCommitResult. - Nuevo
type RunQuery = (q: QueryRequest, principal: Principal, opts?) => Promise<QueryResult | CommitResult>— la firma peer, junto aLoadItemForConsumption (:309-314). - Nuevo
interface GovernanceContext { principal; credential; freshness; provenance; tier; writeTarget? }— unifica lo hoy disperso (Principal:86-97+FreshnessVerdict:100-114+ProvenanceBlock:120-131+CredentialSlot:151-156+tier). :56type Intent— sin cambios (read/write/stream);runQueryderiva read↔write del clasificador y aplica el intent correspondiente.
lib/lakehouse/item-consumption.ts (el composer):
:92DOOR_CONTRACT_VERSION: ContractVersion = 'v1.1'→'v1.2'.- Nueva función
runQuery(peer deloadItemForConsumption), que comparte el core: clasificar (F4.2) → resolver elCatalogBindingde las N tablas (reusaduck-plan/fqn/resolveIdentity) → gobernanza POR-TABLA (read) o autorizar target fuera-de-banda (write, F4.2) → armarGovernanceContext→ seleccionar motor → despachar porEngineAdaptercon el backstopwrite_target. Su write-path ES la capa Node de F4.2 (el ejecutor DML gobernado). Reusa frescura/identidad, NO reimplementa. - Activar los 3 slots inertes al ensamblar el contexto:
purpose(Principal.purpose),CredentialSlot, captura deprovenanceen el path DML.
C · Touch-points substrato ⊥ motor (F2 · separar los dos ejes) — ✅ HECHO 2026-07-28
El enum 'postgres'|'iceberg' mezclaba dónde viven los bytes (substrato) con quién computa (motor). Ya separados: substrato 'pg'|'iceberg' ⊥ motor 'mlrunner'|'duckdb'|'karma'|'pg'.
El barrido resultó ser de 14 ficheros, no ~6 — y 2 de ellos tsc NO los detectaba (el valor se degradaba a string en la frontera). Lista definitiva + el porqué: junction-f2-engine-adapter.md §3·bis. Los dos silenciosos:
components/workspace/tabs/warehouse/WarehouseTab.tsx:107— habría degradado la UI a'—'sin error. Re-tipado aRecord<Substrate, string>exhaustivo.app/api/dataspace/item/sample/route.ts:45— el substrato cruza el wire (passthrough).
Lo implementado: run<T>({postgres, iceberg}) se dejó INTACTO (inert-first: los ~30 caller-closures no se tocan); lo que se añadió es el campo engine en el resuelto de ambos routers + engine?: Engine en ReaderPort/WriterPort (que nunca declaraban motor — el gap de capa-3), y el contrato EngineAdapter/SqlEngineAdapter + registry en lib/compute/engines/. runQuery NO es un método del contrato base: mlrunner no tiene endpoint SQL y pg no es un servicio — ver §2·bis del entregable.
D · La cintura: unificar la fórmula FQN (prerequisito de la pluralidad)
lib/warehouse/query/fqn.ts:32 datasetFqn (TS) ≡ services/ml-runner/app/lakehouse/writer.py:416-419 native_table_identifier (Python) — duplicada bit-a-bit, mantenida a mano. → promover a un artefacto único (generado/serializado) o un test de conformidad cross-lenguaje en CI que falle si TS y Python divergen. Sin esto, cada motor nuevo (DuckDB ATTACH, Karma) multiplica las copias que deben coincidir o el cross-engine se corrompe en silencio.
E · Bypasses a consolidar (F4/F5 · una entrada)
| Bypass hoy | Fichero | → Bajo la puerta |
|---|---|---|
| sql-editor lee duck-direct | sql-editor/execute/route.ts:1092,1109 | intent query (cerrar el duck-direct en el MISMO PR) |
| dashboards traduce PG-JSONB propio | dashboards/query/route.ts:102 jsonbCast | intent query (motor DuckDB) → cierra la divergencia con sql-editor |
| sync/polling escribe directo al seam | polling/dataset-writer.ts:915,1134 | intent write |
| cdc / ingest / manual / row.edit directo | cdc/sink.ts:99, datasets/ingest, manual-table, datasets/rows | intent write (retirar el throw del brazo PG item-consumption.ts:605 progresivamente) |
| pipeline output (mixto: facade + router directo) | transformPaths.ts, native-output-write.ts | verificar path exacto; unificar por la puerta |
F · Inventario de LEGACY a limpiar (código + docs)
Docs con framing SUPERSEDED por junction.md (actualizar cabecera → apuntar a junction.md como sucesor, corregir el framing):
compute-integration.md— el principal. Framing obsoleto: (a) "read-compute y write-path son ejes distintos" (:8,:210) → junction.md los UNIFICA; (b) la pieza = "nuevolib/compute/gateway.ts" que envuelve la facade (:201) → junction.md: Junction ES la facade evolucionada, UNA puerta; (c)reader-flags.tscomo fichero vivo (:69,125,201,224) → fusionado enport-registry.ts; (d)dialect.ts+translateQuery(:94-95,208,226) → ya BORRADOS (F3-RETIRO); (e) DML como no-goal (:234) → ahora eje unificado (F4). Ya tiene header "PARCIALMENTE OBSOLETO"; añadir puntero a junction.md.duckdbengine.md:39,79,93,129—dialect.ts"el shim a retirar / a borrar en F3" → ya borrado; marcar RETIRADO.warehouse-sql/02-query-development.md:47—translateQuery→executeReadOnlyQuerycomo la costura viva → ya borrado; corregir a DuckDB.sql-editor-duckdb-migration.md:36,41,43— retire-list dedialect.ts/translateQuery/jsonbCast→ ya ejecutado; es histórico (marcar "hecho").warehouse-unification-program.md,warehouse-open-catalog.md— ya con header obsoleto; ok como registro.
Código legacy a saldar (no urgente, pero es "el contrato legacy" que el owner quiere evitar):
- El enum binario
'postgres'|'iceberg'(§C) — la deuda de tipos que confunde substrato/motor. - El
throwdel brazo PG dewrite()(item-consumption.ts:605-611) — se retira a medida que los writers enrutan por la puerta (§E). - Los traductores per-superficie a la deriva:
dashboards/query/route.ts:102jsonbCast,sourceCte.ts,sql-source-hydration.ts(rama read-only) — convergen tras el intentquery. reader-flags.ts: verificar que no queda ninguna referencia viva (se fusionó aport-registry.ts).
G · Secuencia sin cabos sueltos + decisiones a cerrar ANTES de F1
Orden (cada punto mapeado a su fase de junction.md §8):
- F0 · Limpieza de docs (§F) — barrer el framing legacy ANTES de construir, para que el código nuevo no herede confusión. Cero riesgo.
- F1 · Contrato (§B) — intent
query+GovernanceContext+ bump v1.2. Inerte. - Prereq · FQN único (§D) — antes del 2º motor tras la puerta.
- F2 · substrato ⊥ motor + EngineAdapter (§C).
- F3 · Equivalence harness (paridad query-semántica).
- F4 · Absorber sql-editor (§E) — cerrar duck-direct en el mismo PR.
- F5 · WRITE bajo la puerta (§E) — retirar el throw PG progresivamente.
- F6 · dashboards + preview al mismo gateway.
Decisiones (todas CERRADAS por el owner, 2026-07-10):
- ✅ Forma del
query:runQueryPEER (nohandle.query()) — multi-tabla / gobernanza-por-tabla / EngineAdapter.Intentno gana'query'; se deriva del clasificador. Su write-path = la capa Node de F4.2. - ✅ Home:
lib/compute/—contract.ts+junction.ts(reubicados desdelib/lakehouse/). - ✅
GovernanceContext: objeto nuevo (frontera más limpia).
Estado F1 (2026-07-10, HECHO — inerte): F1a reubicación (lib/lakehouse/consumption-contract.ts→lib/compute/contract.ts; item-consumption.ts→lib/compute/junction.ts; 23 imports; tsc 0). F1b contrato v1.2 (GovernanceContext, CatalogBinding, QueryRequest, QueryResult, Substrate, Engine, RunQuery + runQuery stub inerte JunctionNotWiredError; bump v1.1→v1.2). tsc 0 + 72/72 tests.
Estado F2 (2026-07-28, HECHO — inerte): §C completo. Substrate cableado (14 ficheros); engine estampado en el resuelto de ambos routers + declarable en ReaderPort/WriterPort; lib/compute/engine-adapter.ts + lib/compute/engines/{duckdb,mlrunner,karma,pg,index}.ts. tsc 0 + 1030/1030 (seam) + 12/12 (registry). SIGUIENTE = F3 (equivalence harness query-semántica — es lo que puebla capabilities().features, hoy vacío a propósito).
Doc vivo. Deriva de junction.md + un sweep determinista de touch-points (loadItemForConsumption, resolveDatasetRowSink/Source, el enum substrato/motor, ContractVersion) y de referencias legacy en docs. Antes de F1: cerrar las 3 decisiones de §G.