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>
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
- Estructura de carpetas
- Naming
- Inyección de dependencias
- Result<E,D>
- ESM y path aliases
- Composition root
- Tipos compartidos vs locales
- Health, puertos y boot
- 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.ts → class 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/(osim-shared/domainsi compartido), nunca eninfrastructure/. - Adapters SIEMPRE en
infrastructure/. - Usecases en
aplication/. - DTOs de comando/query en
aplication/. Tipos de dominio endomain/.
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>oResult<Error, T>. - Las funciones de dominio puras pueden devolver
Tdirecto si no fallan; si fallan por reglas de negocio,Result.
Cuándo NO usarlo:
- Bugs / invariantes rotas →
assertothrow. 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
.jsaunque el archivo sea.ts. TypeScript lo permite. - Yarn workspaces:
sim-sharedse importa comosim-shared/...desde cualquier servicio. - Path aliases por servicio (en
tsconfig.jsonypackage.jsonimports):#config/*→./config/*#adapters/*→./infrastructure/*- (revisar el
tsconfigde 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/Ximporta../infrastructure/Ysaltándose el port).
Composition root
El cableado vive en:
config/eventBusConfig.ts— instancia el bus con la conexión RMQconfig/postgreConfig.ts— instancia elPgClientindex.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/ |
✅ Sí | Tipos, ports y adapters compartidos |
sim-entrada-eventos/ |
✅ Sí (con olores conocidos, ver ANTI-PATTERNS) | Gateway HTTP → bus |
sim-consumidor-nos/ |
✅ Sí | Worker NOS, ejemplo de consumer típico |
sim-consumidor-objenious/ |
✅ Sí | 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 /healthque 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.ts↔X.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.
supertestpara 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.