@ronaldjdevfs/forge 1.3.0-beta → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/forge/SKILL.md +57 -128
  4. package/skills/forge/command/forge.md +59 -14
  5. package/skills/forge/reference/adr.md +242 -0
  6. package/skills/forge/reference/anti-corruption-layer.md +340 -0
  7. package/skills/forge/reference/api-design.md +7 -0
  8. package/skills/forge/reference/api-versioning.md +354 -0
  9. package/skills/forge/reference/architectural-depth-checklist.md +311 -0
  10. package/skills/forge/reference/architecture-template.md +41 -0
  11. package/skills/forge/reference/assay.md +6 -0
  12. package/skills/forge/reference/bounded-contexts.md +311 -0
  13. package/skills/forge/reference/chain.md +6 -0
  14. package/skills/forge/reference/cohesion-checklist.md +256 -0
  15. package/skills/forge/reference/cqrs.md +286 -0
  16. package/skills/forge/reference/data-patterns.md +6 -0
  17. package/skills/forge/reference/di-strategies.md +6 -0
  18. package/skills/forge/reference/errors.md +5 -0
  19. package/skills/forge/reference/events.md +8 -0
  20. package/skills/forge/reference/evolutionary-architecture.md +300 -0
  21. package/skills/forge/reference/forge.md +7 -0
  22. package/skills/forge/reference/hooks.md +6 -0
  23. package/skills/forge/reference/idempotency.md +283 -0
  24. package/skills/forge/reference/inscribe.md +5 -0
  25. package/skills/forge/reference/inspect.md +6 -0
  26. package/skills/forge/reference/modular-monolith.md +252 -0
  27. package/skills/forge/reference/observability.md +5 -0
  28. package/skills/forge/reference/quench.md +5 -0
  29. package/skills/forge/reference/relocate.md +6 -0
  30. package/skills/forge/reference/sagas.md +359 -0
  31. package/skills/forge/reference/security-patterns.md +6 -0
  32. package/skills/forge/reference/smelt.md +6 -0
  33. package/skills/forge/reference/temper.md +6 -0
  34. package/skills/forge/reference/testing-patterns.md +6 -0
  35. package/skills/forge/reference/transactional-outbox.md +311 -0
  36. package/skills/forge/scripts/architecture.mjs +10 -5
  37. package/skills/forge/scripts/assay.mjs +2 -2
  38. package/skills/forge/scripts/chain.mjs +31 -5
  39. package/skills/forge/scripts/context.mjs +24 -4
  40. package/skills/forge/scripts/detect.mjs +39 -30
  41. package/skills/forge/scripts/forge-boot.mjs +108 -0
  42. package/skills/forge/scripts/forge-config.mjs +182 -3
  43. package/skills/forge/scripts/forge-state.mjs +1 -1
  44. package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
  45. package/skills/forge/scripts/forgeSentinel.mjs +2 -2
  46. package/skills/forge/scripts/forgeSmith.mjs +2 -2
  47. package/skills/forge/scripts/graph.mjs +65 -9
  48. package/skills/forge/scripts/hook.mjs +2 -2
  49. package/skills/forge/scripts/inspect.mjs +56 -48
  50. package/skills/forge/scripts/parse-imports.mjs +0 -2
  51. package/skills/forge/scripts/posttool.mjs +211 -17
  52. package/skills/forge/scripts/recommendation-engine.mjs +125 -0
  53. package/skills/forge/scripts/rollback.mjs +5 -3
  54. package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
  55. package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
  56. package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
  57. package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
  58. package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
  59. package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
  60. package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
  61. package/skills/forge/tests/core.test.mjs +288 -4
  62. package/src/cli.js +26 -13
@@ -0,0 +1,252 @@
1
+ # Modular Monolith
2
+
3
+ No todo proyecto necesita microservicios. El modular monolith es el punto óptimo para la mayoría de los equipos: la modularidad de features de Forge con la simplicidad operativa de un solo deploy.
4
+
5
+ ---
6
+
7
+ ## Espectro Monolith → Modular → Microservices
8
+
9
+ ```
10
+ Monolith clásico Modular Monolith Microservicios
11
+ ┌──────────────────┐ ┌──────────────────┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
12
+ │ Todo en una capa │ │ ┌─┐ ┌─┐ ┌─┐ ┌─┐│ │F1│ │F2│ │F3│ │F4│
13
+ │ Sin boundaries │ │ │F1│ │F2│ │F3│ │F4││ └──┘ └──┘ └──┘ └──┘
14
+ │ BD compartida │ │ └─┘ └─┘ └─┘ └─┘│ BD separadas
15
+ │ Sin features │ │ Platform/Shared/Infra │ APIs síncronas
16
+ └──────────────────┘ └──────────────────┘ │ Eventos asíncronos
17
+ │ Deploy independiente
18
+ └───────────────────────────
19
+ ```
20
+
21
+ | Aspecto | Monolith clásico | Modular Monolith | Microservicios |
22
+ |---|---|---|---|
23
+ | **Boundaries** | No existen | Features con R8-R9 | Servicios independientes |
24
+ | **Comunicación** | Llamadas directas | Inyección de interfaces + eventos | HTTP/gRPC + eventos |
25
+ | **Deploy** | Todo junto | Todo junto | Independiente |
26
+ | **BD** | Compartida | Schemas separados por feature | BD independiente |
27
+ | **Equipo** | 1 equipo | 1-2 equipos | 3+ equipos |
28
+ | **Forge** | No aplica | Modelo nativo | Modelo nativo + split |
29
+ | **Escala** | < 20 features | < 40 features | Sin límite |
30
+
31
+ El modular monolith NO es un monolith clásico. La diferencia son los **boundaries explícitos**: cada feature respeta R8 (no imports directos) y R9 (no ciclos), igual que en microservicios, pero todo corre en el mismo proceso.
32
+
33
+ ---
34
+
35
+ ## Marco de Decisión
36
+
37
+ ### Cuándo mantener Modular Monolith
38
+
39
+ | Condición | Señal |
40
+ |---|---|
41
+ | Equipo pequeño (< 8 personas) | Un solo equipo puede mantener todas las features |
42
+ | Dominio cohesionado | Los bounded contexts están estrechamente relacionados |
43
+ | Latencia crítica | El overhead de red de microservicios es inaceptable |
44
+ | Fase inicial | El dominio no está lo suficientemente entendido para partir |
45
+ | Baja carga operativa | Sin equipo de infraestructura dedicado |
46
+ | Deploy semanal/mensual | La velocidad de deploy no es el cuello de botella |
47
+
48
+ ### Cuándo considerar partir a microservicios
49
+
50
+ | Señal | Síntoma |
51
+ |---|---|
52
+ | **Team scaling** | Dos equipos distintos modifican la misma feature frecuentemente |
53
+ | **Deployment bottleneck** | Un cambio en una feature requiere validar todo el monolith |
54
+ | **Boundaries maduros** | Los bounded contexts están estables y bien definidos |
55
+ | **Failure isolation** | Una feature con alta criticidad (ej. pagos) arrastra a las demás en fallos |
56
+ | **Diferentes características** | Una feature necesita escalar distinto (ej. Search vs Catalog) |
57
+ | **Stack divergence** | Una feature se beneficiaría de un stack tecnológico distinto |
58
+
59
+ ### Contra-señales (no partir)
60
+
61
+ | Falsa señal | Realidad |
62
+ |---|---|
63
+ | "Es lo moderno" | Microservicios son una decisión de negocio, no técnica |
64
+ | "Escalabilidad" | El cuello de botella suele ser BD, no el monolith |
65
+ | "Equipos autónomos" | Sin boundaries bien definidos, los microservicios serán un distributed monolith |
66
+ | "Rendimiento" | El overhead de red puede empeorar la latencia |
67
+ | "Está de moda" | Moda tecnológica no justifica la complejidad operativa |
68
+
69
+ ---
70
+
71
+ ## Arquitectura Interna del Modular Monolith
72
+
73
+ ### Comunicación entre features
74
+
75
+ En el modular monolith, la comunicación entre features sigue exactamente las mismas reglas que en microservicios:
76
+
77
+ ```ts
78
+ // ✅ Permitido: inyección de interfaz desde shared
79
+ // src/features/orders/application/use-cases/PlaceOrderUseCase.ts
80
+ import { IInventoryService } from "src/shared/contracts/catalog";
81
+ import { IUnitOfWork } from "src/shared/contracts/data";
82
+
83
+ // ❌ Prohibido: import directo a otra feature (R8)
84
+ import { StockEntity } from "src/features/inventory/domain/StockEntity";
85
+
86
+ // ❌ Prohibido: import directo a infra de otra feature (R1 + R8)
87
+ import { prisma } from "src/features/inventory/adapters/out/persistence/prisma";
88
+ ```
89
+
90
+ ### Shared Kernel
91
+
92
+ ```ts
93
+ // src/shared/contracts/catalog/IInventoryService.ts
94
+ export interface IInventoryService {
95
+ reserveStock(orderId: string, items: LineItem[]): Promise<ReservationResult>;
96
+ releaseStock(reservationId: string): Promise<void>;
97
+ }
98
+
99
+ // Los DTOs de los contratos viven junto a la interfaz
100
+ export type ReservationResult = {
101
+ success: boolean;
102
+ reservationId?: string;
103
+ insufficientItems: { sku: string; available: number }[];
104
+ };
105
+ ```
106
+
107
+ La implementación se inyecta en tiempo de construcción:
108
+
109
+ ```ts
110
+ // src/features/inventory/adapters/out/shared/InventoryService.ts
111
+ export class InventoryServiceImpl implements IInventoryService {
112
+ constructor(private readonly stockRepo: IStockRepository) {}
113
+ // implementación real que la feature Inventory expone al Shared Kernel
114
+ }
115
+ ```
116
+
117
+ ### Base de datos
118
+
119
+ En el modular monolith, cada feature tiene su propio schema dentro de la misma BD:
120
+
121
+ ```sql
122
+ -- Schema por feature, misma base de datos
123
+ CREATE SCHEMA IF NOT EXISTS orders;
124
+ CREATE SCHEMA IF NOT EXISTS catalog;
125
+ CREATE SCHEMA IF NOT EXISTS inventory;
126
+
127
+ CREATE TABLE orders.orders ( … );
128
+ CREATE TABLE catalog.products ( … );
129
+ CREATE TABLE inventory.stock ( … );
130
+
131
+ -- No existen foreign keys entre schemas de distintas features
132
+ -- La integridad referencial se maneja en la aplicación, no en la BD
133
+ ```
134
+
135
+ En Prisma esto se configura con `schema` por modelo:
136
+
137
+ ```prisma
138
+ generator client {
139
+ provider = "prisma-client-js"
140
+ }
141
+
142
+ datasource db {
143
+ provider = "postgresql"
144
+ url = env("DATABASE_URL")
145
+ schemas = ["orders", "catalog", "inventory"]
146
+ }
147
+
148
+ model Order {
149
+ id String @id @default(uuid())
150
+ // ...
151
+ @@schema("orders")
152
+ }
153
+
154
+ model Product {
155
+ id String @id @default(uuid())
156
+ // ...
157
+ @@schema("catalog")
158
+ }
159
+ ```
160
+
161
+ ### Eventos dentro del monolith
162
+
163
+ Dentro del modular monolith, los eventos pueden ser sincrónicos (in-process event bus) o asíncronos (message broker):
164
+
165
+ ```ts
166
+ // Sincrónico: EventBus in-process, sin serialización
167
+ // platform/events/EventBus.ts
168
+ export interface IEventBus {
169
+ publish(event: DomainEvent): Promise<void>;
170
+ subscribe<T extends DomainEvent>(eventType: string, handler: (event: T) => Promise<void>): void;
171
+ }
172
+
173
+ // Uso en feature
174
+ // orders/application/use-cases/PlaceOrderUseCase.ts
175
+ await this.eventBus.publish(new OrderPlacedEvent(order));
176
+ // inventory/adapters/in/events/OrderPlacedHandler.ts
177
+ eventBus.subscribe("OrderPlaced", async (event) => {
178
+ await this.stockService.reserveStock(event.orderId, event.items);
179
+ });
180
+ ```
181
+
182
+ **Regla: dentro del monolith, puedes elegir síncrono o asíncrono.** Pero si planeas partir a microservicios en el futuro, usa asíncrono desde el día 1 (message broker). La migración será solo cambiar la URL del broker.
183
+
184
+ ---
185
+
186
+ ## Integración con Forge
187
+
188
+ | Elemento | Modular Monolith | Microservicios |
189
+ |---|---|---|
190
+ | **Reglas R1-R9** | Se aplican igual | Se aplican igual |
191
+ | **Contratos en shared/** | Interfaces + DTOs | Mismos contratos (packages publicados) |
192
+ | **Eventos** | In-process o broker | Broker siempre |
193
+ | **Platform** | Monolítica, compartida | Por servicio o shared library |
194
+ | **Infra** | Schemas separados en misma BD | BD independientes |
195
+ | **Deploy** | Un solo artefacto | N artefactos |
196
+ | **forge cast** | Crea feature en el monolith | Crea feature + esqueleto de servicio |
197
+ | **forge inspect** | Auditoría normal | Auditoría + health check distribuido |
198
+ | **forge relocate** | Extraer feature del monolith | Dividir servicio o fusionar |
199
+
200
+ ### Transición guiada: Modular Monolith → Microservicios
201
+
202
+ ```
203
+ Fase 1: Asegurar boundaries
204
+ - Verificar que R8 y R9 se cumplen
205
+ - shared/contracts/ debe contener todas las interfaces entre features
206
+ - Schemas separados por feature
207
+
208
+ Fase 2: Extraer eventos
209
+ - Migrar de event bus in-process a broker (RabbitMQ, Kafka)
210
+ - Verificar que los handlers de eventos son idempotentes
211
+
212
+ Fase 3: Extraer feature como servicio
213
+ 1. forge relocate extrae la feature a un nuevo repo
214
+ 2. La feature origen ahora inyecta un cliente HTTP/gRPC en vez de la impl directa
215
+ 3. El contrato en shared/ se convierte en API contract
216
+ 4. Los eventos viajan por el broker existente
217
+
218
+ Fase 4: Iterar
219
+ - Repetir Fase 3 para cada feature que se beneficie de partir
220
+ - Dejar las demás en el monolith
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Anti-patrones
226
+
227
+ | Anti-patrón | Problema | Solución |
228
+ |---|---|---|
229
+ | **Distributed Monolith** | Microservicios que se llaman sincrónicamente en cadena. Si un servicio falla, todos fallan. | Usar eventos asíncronos entre servicios. Si la latencia lo exige sincrónico, reconsiderar si deberían ser un solo servicio. |
230
+ | **Shared Database (microservices)** | Microservicios que comparten BD. Los boundaries son ficticios. | Migrar a schemas separados o BD independientes. Empezar con schemas si el split es futuro. |
231
+ | **Nano-services** | Microservicio por entidad. Cada servicio es un CRUD sin lógica. | Fusionar en features por dominio de negocio. El overhead de operar N servicios pequeños es mayor que el beneficio. |
232
+ | **Premature splitting** | Partir antes de entender los bounded contexts. | Esperar a que los contexts estén estables. El monolith modular permite partir sin reescribir. |
233
+ | **Monolith con carpetas** | Solo hay separación por carpetas, no por boundaries. Cualquier código importa cualquier otro. | Implementar R8 y R9. Si duele, significa que los boundaries están mal diseñados o no existen. |
234
+
235
+ ---
236
+
237
+ ## Conexión con Forge
238
+
239
+ | Comando | Acción |
240
+ |---|---|
241
+ | `forge cast` | Crea feature en el monolith modular con boundaries correctos |
242
+ | `forge inspect` | Verifica que los boundaries entre features son saludables |
243
+ | `forge relocate` | Extrae una feature del monolith a un servicio independiente |
244
+ | `forge reforge` | Rediseña boundaries mal definidos dentro del monolith |
245
+ | `forge graph` | Visualiza dependencias entre features; identifica candidatos a partir |
246
+
247
+ ## Ver también
248
+
249
+ - `reference/bounded-contexts.md` — contexts como módulos del monolith
250
+ - `reference/anti-corruption-layer.md` — ACL entre módulos
251
+ - `reference/sagas.md` — coordinación entre módulos
252
+ - `reference/events.md` — comunicación asíncrona entre módulos
@@ -64,3 +64,8 @@ export interface IHealthCheck {
64
64
  - Tracing distribuido con baggage contextual
65
65
  - Health checks sin autenticación (solo para orquestadores)
66
66
  - Alertas basadas en métricas, no en logs
67
+
68
+ ## Ver también
69
+
70
+ - `reference/security-patterns.md` — audit logging como práctica de seguridad
71
+ - `reference/testing-patterns.md` — tests de instrumentación y health checks
@@ -71,4 +71,9 @@ node .opencode/skills/forge/scripts/detect.mjs --severity ERROR
71
71
 
72
72
  # Solo un tipo específico
73
73
  node .opencode/skills/forge/scripts/detect.mjs --type layers
74
+
75
+ ## Ver también
76
+
77
+ - `reference/evolutionary-architecture.md` — fitness functions como validación continua
78
+ - `reference/hooks.md` — integración de quench en pre-commit hook
74
79
  ```
@@ -49,3 +49,9 @@ El backup se almacena en `.forge/backups/<target>--<timestamp>/` y preserva la e
49
49
  | Feature | `src/application/use-cases/<name>/` | `src/features/<name>/` |
50
50
  | Shared | `src/utils/`, `src/helpers/`, `src/lib/` | `src/shared/<name>/` |
51
51
  | Infra | `src/database/`, `src/providers/` | `src/infra/<name>/` |
52
+
53
+ ## Ver también
54
+
55
+ - `reference/anti-corruption-layer.md` — aislamiento de legacy durante migración
56
+ - `reference/modular-monolith.md` — decisión de estructura al migrar features
57
+ - `reference/reforge.md` — refactor post-migración
@@ -0,0 +1,359 @@
1
+ # Sagas — Transacciones Multi-Feature
2
+
3
+ Los procesos de negocio rara vez caben en una sola feature. Una saga coordina múltiples pasos a través de bounded contexts, garantizando consistencia eventual sin ACID distribuido.
4
+
5
+ ---
6
+
7
+ ## El Problema
8
+
9
+ Una transacción como "realizar un pedido" cruza múltiples features:
10
+
11
+ ```
12
+ Orders: crear pedido → Inventory: reservar stock → Payments: cobrar → Shipping: preparar envío
13
+ ```
14
+
15
+ En un monolith con una sola BD, esto sería una transacción ACID. Cuando cada feature tiene su propio schema o BD, no hay transacción distribuida. Las sagas resuelven este problema.
16
+
17
+ ### Una saga no es:
18
+ - Una transacción ACID (no hay rollback, hay compensación)
19
+ - Un workflow BPMN con motor externo (aunque puede parecerse)
20
+ - Un event pipeline unidireccional (tiene lógica de decisión)
21
+
22
+ ### Una saga es:
23
+ - Una secuencia de transacciones locales
24
+ - Cada paso tiene una acción compensatoria si falla
25
+ - La saga completa tiene consistencia eventual garantizada
26
+
27
+ ---
28
+
29
+ ## Coreografía (Choreography)
30
+
31
+ Cada participante publica eventos y reacciona a eventos de otros. No hay coordinador central.
32
+
33
+ ```mermaid
34
+ sequenceDiagram
35
+ participant Orders
36
+ participant Inventory
37
+ participant Payments
38
+ participant Shipping
39
+
40
+ Orders->>Orders: crear pedido
41
+ Orders->>Inventory: OrderPlaced (event)
42
+ Inventory->>Inventory: reservar stock
43
+ alt stock suficiente
44
+ Inventory->>Orders: StockReserved (event)
45
+ Orders->>Payments: cobrar (event)
46
+ Payments->>Payments: procesar pago
47
+ else stock insuficiente
48
+ Inventory->>Orders: StockReservationFailed (event)
49
+ Orders->>Orders: cancelar pedido
50
+ end
51
+ ```
52
+
53
+ ### Implementación coreografía
54
+
55
+ ```ts
56
+ // Cada feature publica y escucha eventos
57
+ // features/orders/adapters/in/events/OrderSaga.ts
58
+ export class OrderSaga {
59
+ constructor(
60
+ private readonly orderRepo: IOrderRepository,
61
+ private readonly eventBus: IEventBus,
62
+ ) {
63
+ // Escucha respuestas de otros features
64
+ this.eventBus.subscribe("StockReserved", this.onStockReserved.bind(this));
65
+ this.eventBus.subscribe("StockReservationFailed", this.onStockFailed.bind(this));
66
+ this.eventBus.subscribe("PaymentProcessed", this.onPaymentProcessed.bind(this));
67
+ this.eventBus.subscribe("PaymentFailed", this.onPaymentFailed.bind(this));
68
+ }
69
+
70
+ async onStockReserved(event: StockReservedEvent): Promise<void> {
71
+ await this.orderRepo.updateStatus(event.orderId, "stock_reserved");
72
+ this.eventBus.publish(new RequestPaymentCommand(event.orderId, event.amount));
73
+ }
74
+
75
+ async onStockFailed(event: StockReservationFailedEvent): Promise<void> {
76
+ await this.orderRepo.updateStatus(event.orderId, "cancelled_no_stock");
77
+ // No hay compensación adicional: el pedido nunca se confirmó
78
+ }
79
+
80
+ async onPaymentProcessed(event: PaymentProcessedEvent): Promise<void> {
81
+ await this.orderRepo.updateStatus(event.orderId, "paid");
82
+ this.eventBus.publish(new RequestShippingCommand(event.orderId));
83
+ }
84
+
85
+ async onPaymentFailed(event: PaymentFailedEvent): Promise<void> {
86
+ await this.orderRepo.updateStatus(event.orderId, "payment_failed");
87
+ // Compensación: liberar stock
88
+ this.eventBus.publish(new ReleaseStockCommand(event.orderId));
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### Pros y contras
94
+
95
+ | Pro | Contra |
96
+ |---|---|
97
+ | Sin punto central de fallo | Lógica de saga distribuida: difícil de entender el flujo completo |
98
+ | Cada feature es autónomo | Seguimiento complejo (¿en qué paso está cada pedido?) |
99
+ | Fácil de añadir nuevos participantes | Riesgo de ciclos de eventos |
100
+ | Escala horizontalmente | Testing de integración más complejo |
101
+
102
+ ---
103
+
104
+ ## Orquestación (Orchestration)
105
+
106
+ Un coordinador central (orchestrator) dirige cada paso y decide el flujo.
107
+
108
+ ```mermaid
109
+ sequenceDiagram
110
+ participant CheckoutSaga as Orchestrator
111
+ participant Orders
112
+ participant Inventory
113
+ participant Payments
114
+ participant Shipping
115
+
116
+ CheckoutSaga->>Orders: crearPedido()
117
+ Orders-->>CheckoutSaga: ok
118
+ CheckoutSaga->>Inventory: reservarStock()
119
+ Inventory-->>CheckoutSaga: ok
120
+ CheckoutSaga->>Payments: procesarPago()
121
+ alt pago exitoso
122
+ Payments-->>CheckoutSaga: ok
123
+ CheckoutSaga->>Shipping: prepararEnvio()
124
+ else pago falla
125
+ Payments-->>CheckoutSaga: failed
126
+ CheckoutSaga->>Inventory: liberarStock() ← compensación
127
+ CheckoutSaga->>Orders: cancelarPedido() ← compensación
128
+ end
129
+ ```
130
+
131
+ ### Implementación orquestación
132
+
133
+ El orchestrator es un feature independiente:
134
+
135
+ ```
136
+ src/features/checkout-saga/
137
+ domain/
138
+ SagaInstance.entity.ts ← estado de cada saga activa
139
+ ISagaStepExecutor.ts ← interfaz para ejecutar pasos
140
+ application/
141
+ use-cases/
142
+ ExecuteCheckoutSaga.ts ← orquesta la saga
143
+ adapters/
144
+ out/
145
+ saga-steps/
146
+ CreateOrderStep.ts ← llama a Orders
147
+ ReserveStockStep.ts ← llama a Inventory
148
+ ProcessPaymentStep.ts ← llama a Payments
149
+ PrepareShippingStep.ts ← llama a Shipping
150
+ CancelOrderStep.ts ← compensación de Orders
151
+ ReleaseStockStep.ts ← compensación de Inventory
152
+ ```
153
+
154
+ ```ts
155
+ // features/checkout-saga/application/use-cases/ExecuteCheckoutSaga.ts
156
+ export class ExecuteCheckoutSaga {
157
+ constructor(
158
+ private readonly sagaRepo: ISagaInstanceRepository,
159
+ private readonly stepFactory: SagaStepFactory,
160
+ ) {}
161
+
162
+ async execute(command: StartCheckoutCommand): Promise<void> {
163
+ const saga = SagaInstance.start("CHECKOUT", {
164
+ orderId: command.orderId,
165
+ customerId: command.customerId,
166
+ items: command.items,
167
+ });
168
+
169
+ const steps = [
170
+ this.stepFactory.createOrderStep(),
171
+ this.stepFactory.reserveStockStep(),
172
+ this.stepFactory.processPaymentStep(),
173
+ this.stepFactory.prepareShippingStep(),
174
+ ];
175
+
176
+ for (const step of steps) {
177
+ try {
178
+ await step.execute(saga.context);
179
+ saga.advance(step.name);
180
+ await this.sagaRepo.save(saga);
181
+ } catch (error) {
182
+ saga.fail(step.name, error.message);
183
+ await this.compensate(saga, steps);
184
+ return;
185
+ }
186
+ }
187
+
188
+ saga.complete();
189
+ await this.sagaRepo.save(saga);
190
+ }
191
+
192
+ private async compensate(saga: SagaInstance, steps: SagaStep[]): Promise<void> {
193
+ // Ejecuta compensaciones en orden inverso
194
+ const executedSteps = steps.slice(0, saga.currentStepIndex);
195
+ for (const step of executedSteps.reverse()) {
196
+ try {
197
+ await step.compensate(saga.context);
198
+ } catch (err) {
199
+ // La compensación falló — requiere intervención manual
200
+ saga.requireManualIntervention();
201
+ }
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ ### Pros y contras
208
+
209
+ | Pro | Contra |
210
+ |---|---|
211
+ | Flujo completo visible en un lugar | Punto central de fallo |
212
+ | Fácil de testear el orchestrator | El orchestrator conoce todos los participantes |
213
+ | Compensaciones explícitas y centralizadas | Riesgo de orchestrator con lógica de negocio |
214
+ | Trazabilidad: cada saga tiene estado | Más boilerplate que coreografía |
215
+
216
+ ---
217
+
218
+ ## Cuándo Elegir Cada Una
219
+
220
+ | Situación | Recomendación |
221
+ |---|---|
222
+ | 2-3 features, flujo simple | Coreografía |
223
+ | 4+ features, flujo con ramas | Orquestación |
224
+ | Equipos autónomos que no quieren coordinar | Coreografía |
225
+ | Requisito de trazabilidad y monitoreo | Orquestación |
226
+ | Flujo con muchas compensaciones | Orquestación |
227
+ | Feature nuevo que se suma al flujo existente | Coreografía (solo escucha/publica) |
228
+
229
+ ---
230
+
231
+ ## Manejo de Fallos
232
+
233
+ ### Retry con Backoff
234
+
235
+ ```ts
236
+ export class RetryPolicy {
237
+ constructor(
238
+ private readonly maxRetries: number = 3,
239
+ private readonly baseDelay: number = 100,
240
+ ) {}
241
+
242
+ async execute<T>(fn: () => Promise<T>): Promise<T> {
243
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
244
+ try {
245
+ return await fn();
246
+ } catch (error) {
247
+ if (attempt === this.maxRetries) throw error;
248
+ const delay = this.baseDelay * Math.pow(2, attempt - 1);
249
+ await new Promise((resolve) => setTimeout(resolve, delay));
250
+ }
251
+ }
252
+ }
253
+ }
254
+ ```
255
+
256
+ ### Dead Letter Queue (DLQ)
257
+
258
+ Cuando una saga no puede completarse ni compensarse después de N reintentos:
259
+
260
+ ```ts
261
+ if (saga.retryCount >= 5) {
262
+ saga.sendToDLQ({
263
+ reason: "Payment service unavailable after 5 retries",
264
+ context: saga.context,
265
+ lastError: error.message,
266
+ });
267
+ // Requiere intervención humana
268
+ await this.sagaRepo.markFailed(saga);
269
+ await this.notificationService.alertOperations(
270
+ `Saga ${saga.id} requiere intervención manual`
271
+ );
272
+ }
273
+ ```
274
+
275
+ ### Fallback Humano
276
+
277
+ Para casos donde la compensación automática no es posible (ej. un pago ya procesado que no puede revertirse automáticamente):
278
+
279
+ ```ts
280
+ if (error.name === "IrreversibleError") {
281
+ saga.requireManualIntervention({
282
+ reason: "Payment already captured, manual refund required",
283
+ instructions: `Login to payment dashboard, refund transaction ${event.paymentId}, then resume saga ${saga.id}`,
284
+ severity: "HIGH",
285
+ });
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Conexión con el Modelo de Forge
292
+
293
+ ### Reglas y Sagas
294
+
295
+ | Regla | Aplicación |
296
+ |---|---|
297
+ | **R8** (no cross-feature imports) | La saga se comunica con features mediante eventos o interfaces. Nunca importa directamente. |
298
+ | **R5** (domain → infra) | La saga opera en application/use-cases/. No hay infra en dominio de saga. |
299
+ | **R9** (no ciclos) | Las sagas coreografiadas deben auditarse para evitar ciclos de eventos (A→B→C→A). |
300
+
301
+ ### Ubicación de la saga en la arquitectura
302
+
303
+ ```
304
+ src/features/
305
+ checkout-saga/ ← orchestrator como feature
306
+ domain/
307
+ SagaInstance.entity.ts
308
+ ISagaStepExecutor.ts
309
+ application/
310
+ use-cases/
311
+ ExecuteCheckoutSaga.ts
312
+ adapters/
313
+ out/
314
+ saga-steps/
315
+ CreateOrderStep.ts ← llama a contracts de Orders
316
+ ReserveStockStep.ts ← llama a contracts de Inventory
317
+ ProcessPaymentStep.ts ← llama a contracts de Payments
318
+ saga-persistence/
319
+ PostgresSagaRepository.ts
320
+ orders/
321
+ adapters/
322
+ in/
323
+ events/ ← escucha eventos de la saga
324
+ http/
325
+ commands/ ← la saga llama aquí via contract
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Anti-patrones
331
+
332
+ | Anti-patrón | Problema | Solución |
333
+ |---|---|---|
334
+ | **Saga sin compensación** | Un paso falla y el sistema queda inconsistente. | Cada paso debe tener compensación definida explícitamente. |
335
+ | **Orchestrator con lógica de negocio** | El orchestrator decide descuentos, valida reglas, calcula montos. | El orchestrator solo coordina. La lógica de negocio vive en los use cases de cada feature. |
336
+ | **Coreografía sin trazabilidad** | Múltiples eventos volando, nadie sabe el estado global de un proceso. | Usar saga log + event store. Monitorear con `forge chain`. |
337
+ | **Timeout único** | Todas las operaciones tienen el mismo timeout. Operaciones lentas (pagos) fallan donde rápidas (stock) no. | Timeout configurable por paso de saga. |
338
+ | **Compensación que falla sin alerta** | La compensación falla silenciosamente. El sistema cree que compensó pero no lo hizo. | Alertas de operaciones para compensaciones fallidas. Siempre. |
339
+ | **Saga como transacción ACID** | Se intenta revertir un paso que ya tuvo efectos visibles para el usuario. | Las sagas ofrecen consistencia eventual. Los efectos visibles deben diseñarse para ser reversibles (o aceptar que no lo son). |
340
+
341
+ ---
342
+
343
+ ## Conexión con Forge
344
+
345
+ | Comando | Acción |
346
+ |---|---|
347
+ | `forge cast checkout-saga` | Crea feature orchestrator con estructura de saga |
348
+ | `forge inspect` | Detecta features que se comunican sincrónicamente (candidatos a saga) |
349
+ | `forge graph` | Visualiza el flujo de la saga como grafo de dependencias entre features |
350
+ | `forge chain` | Verifica que la saga no introduce ciclos (R9) |
351
+ | `forge assay` | Evalúa si la estrategia de coordinación (coreografía vs orquestación) es la adecuada |
352
+
353
+ ## Ver también
354
+
355
+ - `reference/bounded-contexts.md` — contexts que la saga coordina
356
+ - `reference/events.md` — eventos como unidad de la saga
357
+ - `reference/cqrs.md` — commands como pasos de la saga
358
+ - `reference/transactional-outbox.md` — entrega confiable de eventos de saga
359
+ - `reference/idempotency.md` — retry seguro de pasos de saga
@@ -85,3 +85,9 @@ export const createUserSchema = z.object({
85
85
  - Helmet para headers de seguridad (CSP, X-Frame-Options, etc.)
86
86
  - Secretos en variables de entorno, nunca en código
87
87
  - Auditoría de acciones sensibles: login, delete, role change, export
88
+
89
+ ## Ver también
90
+
91
+ - `reference/api-design.md` — middleware de seguridad, validación de entrada
92
+ - `reference/observability.md` — audit logging como práctica de observabilidad
93
+ - `reference/testing-patterns.md` — tests de middleware y seguridad
@@ -29,3 +29,9 @@ Extrae código reutilizable desde features hacia `shared/`.
29
29
  - El código extraído NO debe importar de infraestructura
30
30
  - El código extraído debe ser puro o depender solo de otros componentes shared
31
31
  - Si el código depende de platform, considerar moverlo a platform en lugar de shared
32
+
33
+ ## Ver también
34
+
35
+ - `reference/relocate.md` — operación similar de extracción
36
+ - `reference/data-patterns.md` — identificación de qué extraer a shared
37
+ - `reference/errors.md` — errores tipados como candidatos a smelt
@@ -47,3 +47,9 @@ Templa la arquitectura aplicando reglas de inyección de dependencias, seguridad
47
47
  - ❌ Importar tsyringe en archivos de dominio
48
48
  - ❌ Mezclar DI manual y contenedor en el mismo feature
49
49
  - ❌ Proxies automáticos o decoradores ocultos que dificulten el rastreo
50
+
51
+ ## Ver también
52
+
53
+ - `reference/di-strategies.md` — selección de estrategia DI antes de temperar
54
+ - `reference/patterns.md` — convenciones de naming en DI
55
+ - `reference/testing-patterns.md` — testabilidad que DI disciplinada habilita
@@ -67,3 +67,9 @@ it("POST /users returns 201", async () => {
67
67
  - Nombrar tests como unidades de comportamiento, no métodos
68
68
  - Coverage mínimo sugerido: 85% use cases, 75% adapters
69
69
  - Los mappers se testean con fixtures: entrada conocida → salida esperada
70
+
71
+ ## Ver también
72
+
73
+ - `reference/anti-corruption-layer.md` — tests de ACL y traducción entre contexts
74
+ - `reference/di-strategies.md` — DI testing con mocks manuales
75
+ - `reference/temper.md` — DI disciplinada que habilita testabilidad