Files
sf-sim/.agents/skills/sf-backend-architecture/references/HOUSE-STYLE.md
Jorge 5e77619d37 feat(commands): añadir /md-lint y aplicarlo a docs existentes
Crea el slash command `/md-lint` para barrer cualquier `.md` del repo
contra un set mínimo de reglas (MD004, MD030, MD031, MD032, MD036,
MD040, MD026, MD047, MD034) sin añadir markdownlint-cli2 como devDep.

Aplica el primer pase: 7 fences sin lenguaje declarado pasan a `text`
en check.md, md-lint.md, SKILL.md, EVENTS-RABBITMQ.md y HOUSE-STYLE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 12:42:58 +02:00

9.2 KiB

House Style — sf-sim / sim-eventos

Convenciones de arquitectura del repo (capas, ports, composition root, ESM, tests). Para convenciones de código a bajo nivel (naming, idioma, interface/type, política de any, async, redacción de tests) ver CODE-STYLE.md.

Si algo no aparece en ninguno de los dos, asume las defaults razonables de Hexagonal/DDD.

Tabla de contenidos

  1. Estructura de carpetas
  2. Naming
  3. Inyección de dependencias
  4. Result<E,D>
  5. ESM y path aliases
  6. Composition root
  7. Tipos compartidos vs locales
  8. Health, puertos y boot
  9. Tests

Estructura de carpetas

packages/
├── _template/                  # Plantilla para crear servicios nuevos
├── sim-shared/                 # Tipos, ports, adapters compartidos
│   ├── domain/                 # DomainEvent, EventBus.port, Result, Order, SimEvents...
│   ├── aplication/             # Helpers transversales (BodyValidator, JWT.service)
│   ├── infrastructure/         # RabbitMQEventBus, OrderRepository, PgClient, HTTPClient
│   ├── ports/                  # Ports compartidos adicionales (queues/AMQPclient.port)
│   └── config/                 # jwtService.config, ...
├── sim-entrada-eventos/        # Gateway HTTP → publish a RMQ
│   ├── domain/                 # Reglas y tipos LOCALES del gateway (companies.ts)
│   ├── aplication/             # Sim.usecases, Sim.controller, Order.usecases, validators
│   ├── infrastructure/         # *Routes.http.ts — Express routers
│   └── config/                 # eventBusConfig, postgreConfig, env/
├── sim-consumidor-nos/         # Worker NOS
├── sim-consumidor-objenious/   # Worker Objenious
└── sim-objenious-cron/         # Cron de seguimiento Objenious

Nota intencional: aplication con "p" simple. Es la convención del repo. Mantenla. Si la cambias, cambia todos los path aliases del monorepo.

Una carpeta por capa, no por feature. Dentro de cada capa los archivos se nombran por concepto (ver Naming).

Naming

Tipo Patrón Ejemplos
Port (interface o type) *.port.ts EventBus.port.ts, operationsRepository.port.ts, AMQPclient.port.ts
Use case (clase orquestadora) *.usecases.ts con clase XUsecases Sim.usecases.tsclass SimUsecases
Controller HTTP *.controller.ts Sim.controller.ts
Router Express *.http.ts o *Routes.http.ts simRoutes.http.ts, franceRoutes.http.ts
Repositorio (adapter) *Repository.ts OrderRepository.ts, NosRepository.ts
Cliente HTTP externo *HttpClient.ts o *Client.ts NosHttpClient.ts
Servicio externo *Service.ts NosJwtService.ts
Validators httpValidators.ts (uno por servicio)
Eventos de dominio *Events.ts con namespace SimEvents.activation, SimEvents.cancel
Tests mismo nombre + .test.ts OrderRepository.test.ts
Config X.config.ts o XConfig.ts (mezclado) eventBusConfig.ts, jwtService.config.ts
Env config/env/index.ts o config/env/env.ts acceso central a process.env

Reglas:

  • Ports SIEMPRE en domain/ (o sim-shared/domain si compartido), nunca en infrastructure/.
  • Adapters SIEMPRE en infrastructure/.
  • Usecases en aplication/.
  • DTOs de comando/query en aplication/. Tipos de dominio en domain/.

Inyección de dependencias

Manual, sin framework. Constructor con objeto args: { dep1, dep2, ... }. Ver el ejemplo concreto y el contraste positional vs args en SKILL.md → "Patrones obligatorios en código".

Lo crítico: los tipos del constructor apuntan al port, no al adapter concreto. Si SimUsecases recibe OrderRepository (clase concreta), es violación. Pasa por OrderRepositoryPort.

Result<E,D>

packages/sim-shared/domain/Result.ts:

export type Success<D> = { error?: undefined | null, data: D }
export type Failure<E = Error> = { data?: undefined | null, error: E }
export type Result<E, D> = Failure<E> | Success<D>

Cuándo usarlo:

  • Toda función de aplicación o repositorio que pueda fallar por motivos de negocio o I/O esperable → Result<string, T> o Result<Error, T>.
  • Las funciones de dominio puras pueden devolver T directo si no fallan; si fallan por reglas de negocio, Result.

Cuándo NO usarlo:

  • Bugs / invariantes rotas → assert o throw. Esto es para errores que no deberían poder ocurrir.

Patrón de chequeo:

const r = await usecase.activation(args);
if (r.error != undefined) {
  // ramificación de error
  return { error: r.error };
}
// aquí r.data está garantizado

Helper: tryCatch(promise) envuelve cualquier promesa y devuelve Result<Error, T>.

Por qué Result y no excepciones: las excepciones se propagan silenciosamente, hacen que los tipos mientan ("devuelve T" pero realmente puede explotar). Result te obliga a manejar el caso de error en el sitio donde ocurre. Es coste cognitivo al escribir, pero ahorra horas de debug.

ESM y path aliases

  • Proyecto en ESM puro. Imports con extensión .js aunque el archivo sea .ts. TypeScript lo permite.
  • Yarn workspaces: sim-shared se importa como sim-shared/... desde cualquier servicio.
  • Path aliases por servicio (en tsconfig.json y package.json imports):
    • #config/*./config/*
    • #adapters/*./infrastructure/*
    • (revisar el tsconfig de cada servicio porque varían)

Ejemplo correcto:

import { rabbitmqEventBus } from '#config/eventBusConfig.js';
import { simRoutes } from "./infrastructure/simRoutes.http.js"
import { EventBus } from "sim-shared/domain/EventBus.port.js";

Errores típicos:

  • Olvidar el .js → fallo en runtime de Node.
  • Importar de sim-shared/... con ruta relativa larga → usa el alias de workspace.
  • Importar archivo con .. cruzando capas (e.g. aplication/X importa ../infrastructure/Y saltándose el port).

Composition root

El cableado vive en:

  1. config/eventBusConfig.ts — instancia el bus con la conexión RMQ
  2. config/postgreConfig.ts — instancia el PgClient
  3. index.ts — instancia repositorios → usecases → controllers → routers → arranca Express y conecta el bus

Regla: todo new X(...) que cablee dependencias debe vivir en config/ o index.ts. NUNCA dentro de un usecase, controller, ni adapter. Si encuentras un new fuera de ahí, es un olor.

Tipos compartidos vs locales

Lo usa Va a
Solo este servicio <servicio>/domain/ o <servicio>/aplication/ según corresponda
Más de un servicio sim-shared/domain/ o sim-shared/aplication/
Es un puerto compartido (cola, JWT, ...) sim-shared/ports/ o sim-shared/domain/*.port.ts

No promuevas a sim-shared "por si acaso". Si solo lo usa un servicio, mantenlo local. Promover prematuramente acopla servicios.

Servicios y excepciones documentadas

Lista de servicios del monorepo y si pueden usarse como referencia del estilo:

Servicio ¿Referencia válida? Notas
_template/ Sí — punto de partida oficial Plantilla para nuevos servicios
sim-shared/ Tipos, ports y adapters compartidos
sim-entrada-eventos/ Sí (con olores conocidos, ver ANTI-PATTERNS) Gateway HTTP → bus
sim-consumidor-nos/ Worker NOS, ejemplo de consumer típico
sim-consumidor-objenious/ Worker Objenious
sim-objenious-cron/ NO usar como referencia Excepción documentada por el equipo: no cumple la arquitectura por características específicas de la API de Objenious que consume. Si auditas este servicio, los desvíos del estilo son intencionales y no deben reportarse como bloqueantes salvo que indiques explícitamente que estás auditando esta excepción.

Si un servicio se aleja del estilo, debe quedar documentado aquí y en su README local con la razón.

Health, puertos y boot

  • Cada servicio expone GET /health que devuelve { status: "ok" } con 200.
  • Puertos asignados en README.md (3000 gateway, 3001 NOS, 3002 Objenious...). Al añadir uno nuevo, actualiza el README.
  • Boot: conecta RMQ ANTES de empezar a aceptar HTTP, si el servicio depende del bus para servir. Si no depende del bus para HTTP (p.ej. solo health), puedes conectarlo en background.

Tests

  • vitest. Tests al lado del archivo: X.tsX.test.ts.
  • Tres niveles, cada uno con criterio distinto:
    • Domain (puro): sin mocks, sin async, sin I/O. Si necesitas mockear algo, está mal ubicado.
    • Application (usecase): mocks de ports. Verifica orquestación, no implementación de adapters.
    • Infrastructure (adapter): contra BDD/RMQ reales o testcontainers. Es lento — sólo tests críticos.
  • supertest para tests HTTP de extremo a extremo.

Señal de buena arquitectura: los tests de aplicación se escriben sin tocar pg, amqplib ni express. Si necesitas levantar Postgres para testear un usecase, tienes una fuga.