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>
15 KiB
name, description
| name | description |
|---|---|
| sf-backend-architecture | Aplicar de forma proactiva al diseñar, planificar, hacer brainstorming, revisar o auditar microservicios backend de Save Family / sf-sim. Cubre Hexagonal + DDD + EDA con RabbitMQ + CQRS en NodeJS/TypeScript siguiendo el estilo de la casa (carpetas domain/aplication/infrastructure/config, ports .port.ts, usecases .usecases.ts, tipo Result genérico, eventos sim.[compañia].[acción], exchange sim.exchange, DLX, retries con x-retry-count). Triggers de uso — planning, brainstorming, 'diseña un servicio', 'añade un comando/evento', 'revisa la arquitectura', 'audita este microservicio', 'esto sigue la arquitectura', bounded context, agregado, port/adapter, puerto/adaptador, outbox, eventual consistency, command/query, retry, dead letter. Úsala SIEMPRE que un usuario hable de añadir, dividir o revisar un microservicio del repo sim-eventos / sf-sim aunque no nombre la arquitectura explícitamente. |
sf-backend-architecture
Skill experta para diseñar y auditar microservicios del monorepo sim-eventos (sf-sim), siguiendo Hexagonal + DDD + EDA + CQRS con NodeJS/TypeScript y RabbitMQ.
Tiene dos modos de uso:
- Asesor en planning / brainstorming — proponer diseño inicial Y empujar las preguntas correctas para no saltarse decisiones críticas.
- Auditor para agentes revisores — emitir un informe con secciones fijas y veredicto, comparando un servicio existente contra el estilo de la casa.
Si la conversación es ambigua sobre qué modo aplicar, pregúntalo antes de actuar.
TL;DR del estilo de la casa
| Aspecto | Convención sf-sim |
|---|---|
| Carpetas por servicio | domain/ · aplication/ (con "p" simple, intencional) · infrastructure/ · config/ |
| Ports compartidos | packages/sim-shared/ports/ y packages/sim-shared/domain/*.port.ts |
| Naming ports | *.port.ts (interface o type) |
| Naming usecases | X.usecases.ts con clase XUsecases, constructor(args: { dep1, dep2, ... }) |
| Naming controllers | X.controller.ts |
| Naming routes | X.http.ts o XRoutes.http.ts |
| Naming repos | XRepository.ts en infrastructure/ |
| Errores | Result<E, D> (de sim-shared/domain/Result.ts) — NO excepciones para flujo de negocio |
| Inyección | Manual, por constructor con objeto de dependencias. Composition root en index.ts o config/*.config.ts |
| Bus | EventBus (port) → RabbitMQEventBus (adapter) |
| Routing keys | sim.[compañia].[acción] (typed con template literals en SimEvents) |
| Exchanges | Principal sim.exchange (topic) · sim.dlx · delayed por servicio |
| Retries | Header x-retry-count, tras maxRetry → DLX |
| Persistencia | pg puro, transacciones manuales, correlation_id (uuidv7) liga evento ↔ order |
| ESM | Imports con .js aunque el archivo sea .ts |
Si vas a crear un servicio nuevo, parte de packages/_template/.
Patrones obligatorios en código
Cuando propongas o revises código TypeScript del repo, estos dos patrones son convención dura — no los uses al gusto. Si los rompes en código que propones, te van a corregir; si los detectas rotos en código existente, repórtalo.
Inyección por constructor con objeto args
Todas las clases reciben sus dependencias en un objeto, NO posicionalmente. Esto evita errores al añadir deps y hace el cableado explícito.
// ✅ Correcto — convención del repo
export class SimMovistarUsecases {
private movistarRepository: MovistarRepositoryPort;
private orderRepository: OrderRepositoryPort;
constructor(args: {
movistarRepository: MovistarRepositoryPort,
orderRepository: OrderRepositoryPort,
}) {
this.movistarRepository = args.movistarRepository;
this.orderRepository = args.orderRepository;
}
}
// ❌ Incorrecto — positional, viola la convención
export class SimMovistarUsecases {
constructor(
private movistarRepository: MovistarRepositoryPort,
private orderRepository: OrderRepositoryPort,
) {}
}
Aplica a usecases, controllers, routers, repositorios y adapters. El cableado en index.ts/config/ pasa { key: value } al constructor.
Result<E, D> para errores esperables
Funciones de aplicación o I/O esperable devuelven Result. NO throw. El llamador chequea if (r.error != undefined).
// ✅ Correcto
async activate(args: { iccid: string }): Promise<Result<string, MovistarLine>> {
const r = await this.movistarRepository.activateSim(args.iccid);
if (r.error != undefined) return { error: r.error };
return { data: r.data };
}
// ❌ Incorrecto — throw para flujo de negocio
async activate(args: { iccid: string }): Promise<MovistarLine> {
const r = await this.movistarRepository.activateSim(args.iccid);
if (!r) throw new Error("activación falló");
return r;
}
throw queda reservado para invariantes rotas (bugs, configuración imposible). Detalle completo en references/HOUSE-STYLE.md.
La regla central: dependencias hacia dentro
infrastructure → aplication → domain
(adapters) (use cases) (núcleo)
domain/ no importa nada de infrastructure/ ni de librerías de I/O (pg, amqplib, axios, express). Si lo hace, es una violación.
Por qué importa esto en este repo: los workers consumen del bus, los gateways escriben en él, y un día nos tocará cambiar de RabbitMQ o de Postgres (o testear sin ellos). Si la lógica de negocio depende del adapter concreto, tenemos que reescribir el dominio. Cuando el dominio sólo conoce el EventBus.port y el XRepository.port, el adapter es intercambiable y el dominio es testeable sin infra.
Validación rápida: ¿podrías ejecutar el caso de uso desde un test sin Express, sin RabbitMQ y sin Postgres? Si no, los límites están mal.
Modo 1 — Asesor en planning / brainstorming
Cuando el usuario describa una nueva funcionalidad, servicio o bounded context, haz dos cosas en paralelo:
A. Lanza estas preguntas (no las omitas)
Lánzalas como bullets, en lenguaje natural, no como interrogatorio. Si ya tienes la respuesta del contexto, dilo y pasa a la siguiente.
- Bounded context y propiedad — ¿de qué agregado o subdominio es responsable este servicio? ¿Es un consumidor de eventos, un gateway, o ambos? ¿Comparte BDD con otro servicio (mala señal) o tiene la suya?
- Comandos vs eventos — ¿qué entra como comando síncrono (HTTP) y qué entra como evento? ¿Qué eventos publica y qué eventos consume?
- Routing keys — ¿cuál es el patrón? Por defecto
sim.[compañia].[acción]. Si introduces un nuevo nivel (sim.[compañia].[acción].[sub]) hay que justificarlo, porque rompe los bindings actuales. - Idempotencia y orden — ¿qué pasa si un evento llega dos veces? ¿Puede procesarse fuera de orden? ¿Necesitas
correlation_id,causation_id, o secuencias? - Reintentos y fallos — ¿qué errores son transitorios (delay + retry) y cuáles definitivos (DLX directo)? ¿Cuál es el
maxRetry? ¿Cómo se monitoriza la DLX? - CQRS — ¿hay separación entre los usecases que escriben (commands) y los que leen (queries)? ¿Ambos hablan con la misma BDD o hay un read model?
- Outbox / atomicidad — si el caso de uso debe escribir en BDD Y publicar evento, ¿cómo evitamos perder el evento si el publish falla tras el commit (o viceversa)?
- Webhook / respuesta externa — ¿hay un webhook host/endpoint asociado a la order? ¿Quién lo dispara y cuándo?
- Contratos compartidos — ¿qué tipos van en
sim-sharedy qué se queda local del servicio? Regla: si más de un servicio lo usa, va asim-shared. - Tests — ¿qué casos vas a probar a nivel de dominio (puro), de aplicación (con ports mockeados) y de infraestructura (con BDD/RMQ reales)?
B. Propón un esqueleto de diseño
Una vez tengas una idea suficiente, escribe un esqueleto concreto:
- Árbol de carpetas (
domain/,aplication/,infrastructure/,config/) - Lista de ports que el dominio necesita (con su firma TypeScript)
- Lista de adapters que vas a crear, con qué tecnología
- Lista de usecases (uno por comando/query)
- Lista de eventos publicados y consumidos, con su routing key tipada
- Lista de tablas / read models si aplica
- Notas sobre transacciones y consistencia (¿una sola tx? ¿outbox? ¿eventual?)
No completes detalles que no tengas. Marca con // TODO y vuelve a preguntar.
Cuándo simplificar: no todo necesita CQRS estricto, ni Event Sourcing, ni Saga. Si el servicio tiene un agregado pequeño y reglas simples, dilo y propón la versión mínima. La complejidad se mete cuando duele su ausencia, no antes.
Modo 2 — Auditor (informe con secciones fijas)
Cuando un agente revisor te invoque para auditar un servicio o un cambio, devuelve EXACTAMENTE esta plantilla. Las secciones son obligatorias en este orden, aunque alguna salga vacía (di explícitamente "Sin hallazgos").
# Auditoría de arquitectura — <nombre del servicio o PR>
## 1. Resumen ejecutivo
<2-4 líneas: qué se ha auditado, veredicto general (OK / OK con observaciones / Bloqueante), top-1 riesgo>
## 2. Capas y dependencias
- ¿`domain/` importa SÓLO domain? (sí/no, evidencias con archivo:línea)
- ¿`aplication/` depende de ports y no de adapters concretos? (sí/no, evidencias)
- ¿`infrastructure/` implementa ports definidos en `domain/` o `sim-shared`? (sí/no)
- Violaciones detectadas (lista con archivo:línea + por qué viola)
## 3. DDD táctico
- Agregados identificados y su raíz
- Entidades vs Value Objects (¿alguna inmutabilidad rota?)
- Eventos de dominio (nombre en pasado, payload, headers, ¿en `sim-shared/domain`?)
- Anti-patrones de dominio (anémico, lógica en services, primitive obsession, ...)
## 4. Hexagonal — ports & adapters
- Ports definidos (nombre, ubicación, ¿tienen sufijo `.port.ts`?)
- Adapters (nombre, ubicación, port que implementan)
- Composition root (¿dónde se cablean? ¿hay duplicación?)
- Acoplamientos sospechosos: import de adapter desde aplication o domain
## 5. EDA / RabbitMQ
- Eventos publicados (routing key, exchange, payload typed)
- Eventos consumidos (queue, binding, handler)
- Routing keys: ¿siguen `sim.[compañia].[acción]`? Excepciones documentadas
- Headers obligatorios: `message_id` (uuidv7), `x-retry-count` para reintentos
- Política de retry y DLX: ¿está configurada en este servicio o se asume del shared?
- Idempotencia del consumidor: ¿qué pasa con un mensaje duplicado?
## 6. CQRS
- Separación command/query: ¿está? ¿es necesaria aquí?
- Read models (si aplica): ubicación, sincronización, lag aceptable
- Mezclas problemáticas (un usecase que escribe Y devuelve datos derivados de otra fuente)
## 7. Persistencia y consistencia
- Repositorios: uno por agregado, no por tabla
- Transacciones: ¿BEGIN/COMMIT/ROLLBACK correctos? ¿alguna tx que toca varios agregados?
- Outbox / atomicidad publish+save: ¿está garantizada o hay ventana de pérdida?
- Manejo de `correlation_id` / `message_id`
## 8. Manejo de errores
- ¿Se usa `Result<E, D>` consistentemente?
- ¿Hay `throw` que escapan a controllers o handlers?
- ¿`tryCatch` se usa en los puntos de I/O?
## 9. Tests
- Cobertura por capa (domain unitario, application con mocks de ports, infra con BDD/RMQ reales)
- Tests que ejecutan sin infra (síntoma de buen aislamiento)
- Tests críticos ausentes
## 10. Estilo y convenciones
- Naming (`*.port.ts`, `*.usecases.ts`, `*.controller.ts`, `*.http.ts`, `*Repository.ts`)
- Ubicación de archivos (¿algo en la capa equivocada?)
- ESM imports con `.js` correctos
- Path aliases (`#config/*`, `#adapters/*`, `sim-shared/*`)
## 11. Riesgos priorizados
1. **[Bloqueante]** ...
2. **[Alto]** ...
3. **[Medio]** ...
4. **[Bajo / nice-to-have]** ...
## 12. Acciones recomendadas
Lista de cambios concretos, en orden de prioridad. Cada acción referencia archivo y línea.
Reglas del auditor:
- Cada hallazgo debe citar
archivo:línea. Si no puedes, marca "no verificable". - "Veredicto general" sólo es OK si no hay nada Bloqueante ni Alto.
- Algo es Bloqueante si introduce pérdida de datos, inconsistencia silenciosa, o rompe un contrato publicado (routing key, payload de evento).
- No inventes. Si no encuentras tests, di "no encontrados", no "pocos".
Decisiones rápidas
¿Dónde va este código?
¿Qué es?
├─ Lógica de negocio pura, sin I/O → domain/
├─ Orquesta dominio + tiene side effects → aplication/ (usecase)
├─ Habla con sistemas externos → infrastructure/ (adapter)
├─ Define una interfaz que el dominio usa → port (en domain/ o sim-shared)
├─ Implementa un port → adapter en infrastructure/
└─ Cableado, env, conexiones → config/
¿Comando o evento?
- Comando (síncrono, HTTP): el cliente espera una respuesta inmediata, fallar el comando es razonable de cara al cliente. Va por controller → usecase. Devuelve
Result. - Evento (asíncrono, RabbitMQ): notifica un hecho ocurrido. El emisor no espera. El consumidor se encarga de la idempotencia. Routing key en pasado o como verbo de acción según convención del repo (
sim.alai.activatedescribe la acción solicitada, no el resultado — es el patrón actual).
¿Es un agregado o lo divido?
- ¿Deben ser consistentes en una transacción? → mismo agregado.
- ¿Pueden ser eventualmente consistentes? → agregados distintos, eventos entre ellos.
- ¿Se referencian sólo por id? → agregados distintos.
- ¿Más de ~10 entidades dentro? → divídelo.
Regla operativa: una transacción toca un solo agregado. Cross-aggregate va por eventos.
Anti-patrones del repo (úsalos como ejemplos)
Estos están vivos en sf-sim y son material didáctico:
- Fuga de infra al usecase:
aplication/Sim.usecases.tsimportaOrderRepositoryconcreto en vez del port. El usecase debería depender de unOrderRepository.port. - Excepciones para flujo normal:
domain/companies.tscompanyFromIccidlanzaErroren vez de devolverResult. Esto fuerza try/catch en controllers. - Publish + save no atómico: en los usecases de Sim, primero se hace
eventBus.publishy luegosaveOrder. Si el publish va y el save falla, hay evento sin order; si el orden se invierte, hay order sin evento. Solución: outbox. - Controller con demasiadas responsabilidades:
controllerGeneratormezcla validación, mapping, ejecución y formateo de respuesta. Funciona, pero esconde el flujo y dificulta tests del controller.
Lista completa con ejemplos: ver references/ANTI-PATTERNS.md.
Referencias
| Archivo | Cuándo leerlo |
|---|---|
| references/HOUSE-STYLE.md | Necesitas detalle de carpetas, naming de ficheros, DI manual, Result, ESM |
| references/CODE-STYLE.md | Necesitas detalle de naming a nivel código, idioma de comentarios/identificadores, interface vs type, política de any, async, redacción de tests |
| references/EVENTS-RABBITMQ.md | Vas a tocar publish/consume, routing keys, retries, DLX, outbox, idempotencia |
| references/ANTI-PATTERNS.md | Vas a auditar o ya tienes un olor sospechoso |
| references/AUDIT-CHECKLIST.md | Vas a emitir el informe del Modo 2; lista exhaustiva de checks |
Lee SOLO la referencia que necesites. No las cargues todas por defecto.