dec-0062

Vista generada de dec-0062: r52 — Eden Treaty App type: runtime-first (bypass de contract-first)

ImplementadoSin versión del tren todavía· generada desde docs/decisions/dec-0062-eden-app-type-runtime-first.md
track/docsdec-0124track/docs:DC10

Vista generada desde docs/decisions/dec-0062-eden-app-type-runtime-first.md. No se edita a mano: bun run docs:gen la regenera y bun run docs:check falla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.

CampoValor
EstadoLOCKED (migrado de un rNN por gate-3: el fichero no declara estado)
Referencia legadar52-eden-app-type-runtime-first-2026-07-15
Ficherodocs/decisions/dec-0062-eden-app-type-runtime-first.md

Nodos del roadmap que lo citan en refs: ninguno.

Páginas de la documentación que lo citan: ninguna todavía.

Texto del ADR

Leído de docs/decisions/dec-0062-eden-app-type-runtime-first.md, el fichero canónico.

r52 — Eden Treaty App type: runtime-first (bypass de contract-first)

  • Status: aceptado 2026-07-15 (delegación explícita de waxin a axon durante resolución de B1/merge — ver docs/progress-log.md). Checklist de audit original preservado abajo; waxin puede reabrirlo cuando quiera, no bloqueante.
  • Layer: control-plane TS (r20 §3 — apps/** + packages/clients/**)
  • Adjacent: wf_8bc46e09 — Sprint #2 Layer 1, integra Eden Treaty con catalog-svc
  • Modifies: partial — r20 §2.3 contract-first (matización documentada abajo)
  • Implements: D2 (lockeado en plan integrado Sprint #2 + no-throw, 2026-07-15)

TL;DR

Eden Treaty treaty<App> consume el App type DIRECTAMENTE de apps/catalog-svc/src/transport/http/app.ts (export type alias ReturnType<typeof createHttpApp>), NO una réplica estática en packages/clients/src/app-shape.ts. Esto viola explícitamente r20 §2.3 (contract-first: contratos NO derivados de tipos Elysia). Violación se acepta con justificación técnica abajo, lockeada como ADR r52.

Acción inmediata:

  • DELETE packages/clients/src/app-shape.ts (0 callers verificados — WF-2 K3 grep).
  • DELETE el subpath export ./app-shape en packages/clients/package.json.
  • packages/clients/src/factory.ts mantiene treaty<App>(baseUrl) con App importado de @styx/catalog-svc/transport/http/app.

Contexto

Estado pre-r52

Dos paths paralelos para el App type de Eden coexistían en el repo:

Path 1 (r20 §2.3 canónico): packages/clients/src/app-shape.ts exportaba createStyxApp() — una función placeholder que throws, pensada como type-level replica del contrato catalog-svc. Su JSDoc declaraba: "r20 §2.3 contract-first: treaty infiere desde ESTE objeto, no desde ReturnType<typeof createHttpApp> de catalog-svc (que es runtime, no contrato). Esta función usa el mismo patrón de cast que factory.ts: se pasa un valor placeholder (undefined) tipado como App para que treaty infiera las rutas."

Path 2 (runtime-first): apps/catalog-svc/src/transport/http/app.ts:54 exportaba type App = ReturnType<typeof createHttpApp> (decisión del WF-2 E1 — drift del plan original, ya implementado). Y packages/clients/src/factory.ts importaba ese mismo type para treaty(baseUrl as unknown as App, ...).

r31-style discipline dice: DELETE > @deprecated cuando grep returns 0 callers. app-shape.ts tenía exactamente 0 callers activos (verificado en WF-2 K3 grep, output: solo dist/ artifact + comentarios en result-wrappers.ts y el propio app-shape.ts).

La tensión con r20 §2.3

r20 §2.3 (ADR canónico, locked 2026-06-20) establece:

"NO se derivan tipos de apps Elysia. @styx/api-contracts es canónico, contract-first Arktype (no derivado de tipos Elysia); Eden = cliente interno TS opcional, no fuente de verdad."

Eso dice claramente: contracts son tipos Arktype en @styx/api-contracts. Esos contratos se manifiestan como métodos HTTP en catalog-svc vía handlers. Los handlers Elysia NO son la fuente de verdad del contrato.

Pero Eden Treaty necesita un App type que ES, por construcción, ReturnType<typeof createHttpApp> — porque Elysia usa decorators / chaining para acumular el state del handler en un tipo estático. NO existe un modo "treaty" donde Elysia inferiría el contract-first.

Por tanto: para usar Eden Treaty con apps/web, Eden necesita ver el shape del app Elysia. El path de r20 §2.3 (replica estática) era un workaround para evitar la "violación" — pero el workaround añade drift sin resolver nada.


Decisión

D2 locked: Runtime-first directo, sin replica

  1. App type canónico: apps/catalog-svc/src/transport/http/app.ts exporta type App = ReturnType<typeof createHttpApp>. ESTE es el tipo que Eden ve. Path 1 (replica en app-shape.ts) se ELIMINA.

  2. packages/clients/src/factory.ts consume App directamente:

    import type { App } from '@styx/catalog-svc/transport/http/app';
    export function createStyxClient(baseUrl, headers?): Treaty.Create<App> {
      return treaty(baseUrl as unknown as App, { headers }) as Treaty.Create<App>;
    }
  3. Drift detection (mecanismo, no contrato-first):

    • App type cambia SOLO si createHttpApp cambia (añadir/quitar .use(...) en la composición). Estos cambios SIEMPRE son locales a catalog-svc, NO requieren actualización paralela en clients/api-contracts.
    • TS infiere los endpoints y sus types de params/body/response DIRECTAMENTE de la composición. Si Elysia rompe la inference, los tests de build/typecheck fallan en CI — no hay runtime que validar.
    • El contrato REAL (los schemas Arktype de payload + response) vive en @styx/api-contracts/src/catalog.ts. App es el wire shape, no el contrato semántico. La separación: App describe el HTTP shape (qué método + qué params/body/response types); @styx/api-contracts describe el dominio (qué SIGNIFICA cada campo, qué invariantes, qué versiones).

Por qué NO replica (matización de r20 §2.3)

r20 §2.3 dice "los contratos canónicos viven en @styx/api-contracts, no derivados de tipos Elysia". Su lectura ORIGINAL era "para evitar drift entre contrato y runtime handler — el contrato Elysia inferido podía diverger silenciosamente del payload Arktype".

La réplica de app-shape.ts intentaba mantener separación, PERO introduce un problema peor: dos fuentes de verdad para el mismo HTTP shape. Si catalog-svc añade POST /works, app-shape.ts debe actualizarse en paralelo — y la única señal de drift es el TS build failure, mismo mecanismo que ya existe para detectar drift entre createHttpApp y App directo.

La diferencia:

PathTS drift detectionCosto real
Path 1 (replica)TS falla si replica vs createHttpApp divergen2 sitios a mantener
Path 2 (runtime)TS falla si consumer vs App diverge1 sitio

Path 2 elimina una capa de indirección sin perder la garantía de type-safety. La "violación" de r20 §2.3 es semántica (el App type viene de Elysia runtime, no de Arktype estático), no operacional (el drift detection sigue funcionando via typecheck).

Naturaleza de la violación: r20 §2.3 tenía dos interpretaciones posibles:

  • Interpretación A (estricta): "los types Elysia NO pueden ser la fuente del contrato" → replica es obligatoria.
  • Interpretación B (funcional): "los types Elysia NO pueden diverger del contrato sin detección" → runtime-first cumple porque TS detecta drift por construcción.

r52 adopta interpretación B. La interpretación A se mantiene como aspiración v1.0 (post-F1, cuando catalog-svc madure a 50+ endpoints y la probabilidad de drift manual entre replica y runtime crezca). Hasta entonces, replica es overhead sin valor.


Riesgos + mitigaciones

IDRiesgoProbabilidadImpactoMitigación
R-D2.1Elysia rompe la type inference (upgrade @elysiajs/eden)bajaaltoPin version de @elysiajs/eden en @styx/clients/package.json. Plan L1 ya pineaba ^1.4.9. Si upgrade rompe, conftest detecta.
R-D2.2catalog-svc renombra createHttpApp (signature cambia)muy bajamedioSearch-and-replace afecta 2 archivos: app.ts (definición) + barrel. Renombrar App consumers (clients + tests) globalmente.
R-D2.3Drift entre @styx/api-contracts schema Arktype y createHttpApp Elysia body parsingbajamedioLayer 0 O1 añade request-id propagation + headers Arktype-schema. Conftest vitest que verifica treaty<App> infiere endpoints matching @styx/api-contracts queries.
R-D2.4El barrel @styx/catalog-svc/transport/http/app exporta runtime + type en una sola surfacemediabajoEl barrel SOLO exporta App como type, NO funciones runtime. Aunque un consumer astutamente importara createHttpApp, el effect-side sería internal a catalog-svc (no rompe clients).

Acceptance criteria

  • AC-D2.1: packages/clients/src/app-shape.ts NO existe (verificado ls).
  • AC-D2.2: packages/clients/package.json NO contiene "./app-shape" en exports.
  • AC-D2.3: packages/clients/src/index.ts NO exporta ./app-shape.
  • AC-D2.4: apps/web/src/lib/api/eden-client.ts (NEW) importa App de @styx/catalog-svc/transport/http/app.
  • AC-D2.5: bun run typecheck pasa en apps/web, catalog-svc, packages/clients.
  • AC-D2.6: bun run lint pasa en los 3 packages.
  • AC-D2.7: 0 referencias rotas (grep app-shape returns 0 hits post-DELETE).

Waxin audit checklist (2026-07-16)

waxin, este ADR se revisará mañana. Mientras tanto, las preguntas que pueden surfacer son:

  1. ¿r20 §2.3 interpretación B es acceptable? (ver tabla arriba).
  2. ¿Pin @elysiajs/eden version es suficiente mitigation para R-D2.1, o necesitas feature-detection runtime (e.g. tipo check if version 1.x then AppView<App>)?
  3. ¿AC-D2.5 + AC-D2.6 son typecheck/lint los gates suficientes, o queremos un conftest vitest end-to-end (Layer 1 plan lo menciona)?

Si la respuesta a (1) es NO, revertir a Path 1 con replica canónica + conftest que valide sync entre replica y runtime App. Coste: 1 archivo nuevo (app-shape.ts) + 1 conftest.


Nota axon (2026-07-15, resolución de B1)

waxin delegó explícitamente esta decisión ("ultrathink a tu criterio, bien deliberado") durante la resolución del merge de feat/f-ui-federation-card a master. Verdict: aceptar runtime-first tal cual — el argumento de interpretación B es sólido (mismo mecanismo de drift-detection —fallo de typecheck— que la réplica contract-first, sin el coste de mantener 2 fuentes de verdad sincronizadas a mano). Las 3 preguntas del checklist de arriba (Q1-Q3) siguen abiertas para revisión de waxin — axon NO las ha respondido en su nombre: son preferencia/riesgo operativo, no hechos verificables por grep.


Referencias

  • r20 §2.3 — docs/decisions/r20-f0a-architecture-correction-pass-2026-06-20.md (matizado por r52, interpretación B adoptada).
  • r31 — discipline "DELETE > @deprecated" cuando grep returns 0 callers.
  • Plan integrado Sprint #2 — docs/plans/f-ui-sprint2-no-throw-integration-2026-07-15.md §3.3.
  • Plan L1 — docs/handoffs/sprint2-plans/plan-layer1-eden-clients.md.
  • ADR canónico r43 — docs/decisions/r43-zig-017-migration-over-vendorize-2026-07-02.md (precedente: pinpoint un migration strategy con trade-offs explícitos sobre contratos anteriores).