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/le ↔ min/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).