Golden SkyGolden SkyArquitectura

Arquitectura · A4 · Extensibilidad

Extender el sistema

Agregar una función nueva cruza todo el stack de la misma forma, siempre: nueve pasos, del dominio puro a la pantalla, con el tipado viajando solo por el medio. El ejemplo: un medidor de estrés de mercado que clasifica el entorno en LOW / MEDIUM / HIGH / CRITICAL.

§1El recorrido

Backend primero (el contrato), un comando que sincroniza los tipos, y frontend después. El orden no es negociable: el dominio nace antes que la UI.

1domain/Cálculo puro sobre DataFrames — sin I/O.
2schemas/Contrato Pydantic: Config de entrada, Result de salida.
3services/Orquestación: lee artefactos, invoca el dominio, persiste.
4api/routes/Endpoint HTTP como sub-recurso por intención.
5npm run genEl puente: regenera los tipos TypeScript desde el OpenAPI.
6lib/schemas/Zod espejo para validar el form (UX).
7lib/api/Endpoint tipado + hook de React Query.
8messages/Catálogo i18n: es.json y en.json, mismas keys.
9app/…/page.tsxLa pantalla: form + panel de resultados con hooks.

§2Backend — el dominio primero

Pasos 1–4

La lógica matemática vive aislada; encima, el contrato; encima, la orquestación; y al borde, la ruta.

1 · Dominio puro — solo calcula

domain/market_stress/calculator.pydef compute_market_stress(df, lookback_periods) -> str: # retorna un código inmutable: LOW/MEDIUM/HIGH/CRITICAL recent = df.tail(lookback_periods) range_pct = (recent['high'] - recent['low']) / recent['open'] vol = range_pct.mean() if vol > 0.05: return "CRITICAL" if vol > 0.03: return "HIGH" if vol > 0.015: return "MEDIUM" return "LOW"

2 · Contrato — restricciones duras

schemas/market_stress.pyclass MarketStressConfig(BaseModel): lookback_periods: int = Field(14, ge=5, le=100) target_asset: str = Field(..., min_length=1) class MarketStressResult(BaseModel): stress_level: str = Field(..., regex="^(LOW|MEDIUM|HIGH|CRITICAL)$") recommendation_code: str

3 · Servicio — orquesta el I/O

services/market_stress_service.pydef execute_market_stress(run_id, config) -> MarketStressResult: df = read(storage.get_artifact(run_id, "df_full.pkl")) # leer level = compute_market_stress(df, config.lookback_periods) # dominio rec = "HALT_TRADING" if level == "CRITICAL" else "PROCEED" result = MarketStressResult(stress_level=level, recommendation_code=rec) storage.save_artifact(run_id, "market_stress_result.json", result...) # persistir return result

4 · Ruta — sub-recurso por intención

api/routes/runs.py@router.post("/{id}/market-stress", response_model=MarketStressResult) def run_market_stress(id, config): # el filelock por (run_id, 'market-stress') corre adentro del service return execute_market_stress(id, config)

§3El puente automático

Paso 5

Modificada la API, nunca se escriben las interfaces del cliente a mano. Un comando lo hace, exacto y determinista.

npm run gen  →  exporta el OpenAPI JSON e invoca openapi-typescript para reescribir generated/schema.ts

Desde acá, los tipos del backend están disponibles en el frontend. Si el Config o el Result cambian, TypeScript marca cada lugar que hay que ajustar — el drift es imposible por construcción (ver A2).

§4Frontend — sobre el tipo generado

Pasos 6–9

Todo el frontend se apoya en el tipo que ya vino del backend: validación, llamada, traducción y pantalla.

6 · Zod — espejo del Pydantic (UX)

lib/schemas/marketStress.tsexport const marketStressSchema = z.object({ lookback_periods: z.number().min(5).max(100), target_asset: z.string().min(1, "El activo es obligatorio"), });

7 · Endpoint + hook — tipados desde el schema generado

lib/api/endpoints.tstype Config = components["schemas"]["MarketStressConfig"]; export const executeMarketStress = (runId, config: Config) => client.post(`/api/v1/runs/${runId}/market-stress`, config);

8 · i18n — los códigos se vuelven etiquetas

messages/es.json · en.json (mismas keys)"marketStress": { "states": { "LOW": "Estrés Bajo — Entorno Seguro", "CRITICAL": "Estrés Crítico — Bloqueo de Operaciones" } }

Paso 9: la page.tsx arma el formulario y el panel de resultados con esos hooks y traducciones. Ningún texto visible se hardcodea en el JSX.

§5Checklist antes del PR

Las cuatro reglas duras que un revisor verifica antes de aprobar un módulo nuevo.

El dominio es una función pura — sin print ni accesos directos al filesystem.
En el mismo commit: el Pydantic del backend, el openapi.json intermedio y el schema.ts generado.
Los límites de Zod coinciden con los del Pydantic (ge/lemin/max).
Traducciones en es.json y en.json bajo la misma estructura jerárquica.

Viene de A3 (dominio y servicio). Con esto cierra la guía de arquitectura tecnológica.

Fuente: webapp/docs/ — Guía de Extensibilidad (ejemplo Medidor de Estrés de Mercado); ARCHITECTURE.md §8 (workflow feature nueva).