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