Vista generada de dec-0062: r52 — Eden Treaty App type: runtime-first (bypass de contract-first)
docs/decisions/dec-0062-eden-app-type-runtime-first.mdVista generada desde
docs/decisions/dec-0062-eden-app-type-runtime-first.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED (migrado de un rNN por gate-3: el fichero no declara estado) |
| Referencia legada | r52-eden-app-type-runtime-first-2026-07-15 |
| Fichero | docs/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.
Leído de docs/decisions/dec-0062-eden-app-type-runtime-first.md, el fichero canónico.
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:
packages/clients/src/app-shape.ts (0 callers verificados — WF-2 K3 grep)../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.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).
r20 §2.3 (ADR canónico, locked 2026-06-20) establece:
"NO se derivan tipos de apps Elysia.
@styx/api-contractses 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.
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.
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>;
}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.@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).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:
| Path | TS drift detection | Costo real |
|---|---|---|
| Path 1 (replica) | TS falla si replica vs createHttpApp divergen | 2 sitios a mantener |
| Path 2 (runtime) | TS falla si consumer vs App diverge | 1 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:
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.
| ID | Riesgo | Probabilidad | Impacto | Mitigación |
|---|---|---|---|---|
| R-D2.1 | Elysia rompe la type inference (upgrade @elysiajs/eden) | baja | alto | Pin version de @elysiajs/eden en @styx/clients/package.json. Plan L1 ya pineaba ^1.4.9. Si upgrade rompe, conftest detecta. |
| R-D2.2 | catalog-svc renombra createHttpApp (signature cambia) | muy baja | medio | Search-and-replace afecta 2 archivos: app.ts (definición) + barrel. Renombrar App consumers (clients + tests) globalmente. |
| R-D2.3 | Drift entre @styx/api-contracts schema Arktype y createHttpApp Elysia body parsing | baja | medio | Layer 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.4 | El barrel @styx/catalog-svc/transport/http/app exporta runtime + type en una sola surface | media | bajo | El 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). |
packages/clients/src/app-shape.ts NO existe (verificado ls).packages/clients/package.json NO contiene "./app-shape" en exports.packages/clients/src/index.ts NO exporta ./app-shape.apps/web/src/lib/api/eden-client.ts (NEW) importa App de
@styx/catalog-svc/transport/http/app.bun run typecheck pasa en apps/web, catalog-svc, packages/clients.bun run lint pasa en los 3 packages.app-shape returns 0 hits post-DELETE).waxin, este ADR se revisará mañana. Mientras tanto, las preguntas que pueden surfacer son:
if version 1.x then AppView<App>)?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.
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.
docs/decisions/r20-f0a-architecture-correction-pass-2026-06-20.md
(matizado por r52, interpretación B adoptada).docs/plans/f-ui-sprint2-no-throw-integration-2026-07-15.md §3.3.docs/handoffs/sprint2-plans/plan-layer1-eden-clients.md.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).