Published

Junction — mapa de ejecución (los puntos EXACTOS a tocar)

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

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 intent write, live-SQL por el nuevo intent query). El facade.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): runQuery como entrada PEER de loadItemForConsumption (no un método del DatasetHandle item-céntrico), porque una query live-SQL toca N tablas y no tiene un ItemRef primario. La puerta = { loadItemForConsumption (items) , runQuery (SQL) } — dos formas de una frontera con core compartido (resolución+gobernanza+motor). Intent NO gana un valor 'query': una query ES read o write, derivado del clasificador classify_statement (F4.2). Ver la discusión en el chat / §G.

lib/lakehouse/consumption-contract.ts (solo-tipos — todo deriva de aquí):

  • :44 type ContractVersion = 'v1.1'bump a 'v1.2' (el envelope runQuery es 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 } / reusar Page; el write devuelve CommitResult.
  • Nuevo type RunQuery = (q: QueryRequest, principal: Principal, opts?) => Promise<QueryResult | CommitResult> — la firma peer, junto a LoadItemForConsumption (: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).
  • :56 type Intentsin cambios (read/write/stream); runQuery deriva read↔write del clasificador y aplica el intent correspondiente.

lib/lakehouse/item-consumption.ts (el composer):

  • :92 DOOR_CONTRACT_VERSION: ContractVersion = 'v1.1''v1.2'.
  • Nueva función runQuery (peer de loadItemForConsumption), que comparte el core: clasificar (F4.2) → resolver el CatalogBinding de las N tablas (reusa duck-plan/fqn/resolveIdentity) → gobernanza POR-TABLA (read) o autorizar target fuera-de-banda (write, F4.2) → armar GovernanceContext → seleccionar motor → despachar por EngineAdapter con el backstop write_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 de provenance en 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 a Record<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 hoyFichero→ Bajo la puerta
sql-editor lee duck-directsql-editor/execute/route.ts:1092,1109intent query (cerrar el duck-direct en el MISMO PR)
dashboards traduce PG-JSONB propiodashboards/query/route.ts:102 jsonbCastintent query (motor DuckDB) → cierra la divergencia con sql-editor
sync/polling escribe directo al seampolling/dataset-writer.ts:915,1134intent write
cdc / ingest / manual / row.edit directocdc/sink.ts:99, datasets/ingest, manual-table, datasets/rowsintent write (retirar el throw del brazo PG item-consumption.ts:605 progresivamente)
pipeline output (mixto: facade + router directo)transformPaths.ts, native-output-write.tsverificar 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.mdel principal. Framing obsoleto: (a) "read-compute y write-path son ejes distintos" (:8,:210) → junction.md los UNIFICA; (b) la pieza = "nuevo lib/compute/gateway.ts" que envuelve la facade (:201) → junction.md: Junction ES la facade evolucionada, UNA puerta; (c) reader-flags.ts como fichero vivo (:69,125,201,224) → fusionado en port-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,129dialect.ts "el shim a retirar / a borrar en F3" → ya borrado; marcar RETIRADO.
  • warehouse-sql/02-query-development.md:47translateQueryexecuteReadOnlyQuery como la costura viva → ya borrado; corregir a DuckDB.
  • sql-editor-duckdb-migration.md:36,41,43 — retire-list de dialect.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 throw del brazo PG de write() (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:102 jsonbCast, sourceCte.ts, sql-source-hydration.ts (rama read-only) — convergen tras el intent query.
  • reader-flags.ts: verificar que no queda ninguna referencia viva (se fusionó a port-registry.ts).

G · Secuencia sin cabos sueltos + decisiones a cerrar ANTES de F1

Orden (cada punto mapeado a su fase de junction.md §8):

  1. F0 · Limpieza de docs (§F) — barrer el framing legacy ANTES de construir, para que el código nuevo no herede confusión. Cero riesgo.
  2. F1 · Contrato (§B) — intent query + GovernanceContext + bump v1.2. Inerte.
  3. Prereq · FQN único (§D) — antes del 2º motor tras la puerta.
  4. F2 · substrato ⊥ motor + EngineAdapter (§C).
  5. F3 · Equivalence harness (paridad query-semántica).
  6. F4 · Absorber sql-editor (§E) — cerrar duck-direct en el mismo PR.
  7. F5 · WRITE bajo la puerta (§E) — retirar el throw PG progresivamente.
  8. F6 · dashboards + preview al mismo gateway.

Decisiones (todas CERRADAS por el owner, 2026-07-10):

  • Forma del query: runQuery PEER (no handle.query()) — multi-tabla / gobernanza-por-tabla / EngineAdapter. Intent no gana 'query'; se deriva del clasificador. Su write-path = la capa Node de F4.2.
  • Home: lib/compute/contract.ts + junction.ts (reubicados desde lib/lakehouse/).
  • GovernanceContext: objeto nuevo (frontera más limpia).

Estado F1 (2026-07-10, HECHO — inerte): F1a reubicación (lib/lakehouse/consumption-contract.tslib/compute/contract.ts; item-consumption.tslib/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.