Files
sf-sim/.agents/skills/sf-backend-architecture/SKILL.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

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:

  1. Asesor en planning / brainstorming — proponer diseño inicial Y empujar las preguntas correctas para no saltarse decisiones críticas.
  2. 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.

  1. 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?
  2. Comandos vs eventos — ¿qué entra como comando síncrono (HTTP) y qué entra como evento? ¿Qué eventos publica y qué eventos consume?
  3. 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.
  4. Idempotencia y orden — ¿qué pasa si un evento llega dos veces? ¿Puede procesarse fuera de orden? ¿Necesitas correlation_id, causation_id, o secuencias?
  5. 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?
  6. 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?
  7. 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)?
  8. Webhook / respuesta externa — ¿hay un webhook host/endpoint asociado a la order? ¿Quién lo dispara y cuándo?
  9. Contratos compartidos — ¿qué tipos van en sim-shared y qué se queda local del servicio? Regla: si más de un servicio lo usa, va a sim-shared.
  10. 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.activate describe 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.ts importa OrderRepository concreto en vez del port. El usecase debería depender de un OrderRepository.port.
  • Excepciones para flujo normal: domain/companies.ts companyFromIccid lanza Error en vez de devolver Result. Esto fuerza try/catch en controllers.
  • Publish + save no atómico: en los usecases de Sim, primero se hace eventBus.publish y luego saveOrder. 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: controllerGenerator mezcla 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.