@ronaldjdevfs/forge 1.2.0 → 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 (74) hide show
  1. package/README.md +36 -21
  2. package/package.json +7 -2
  3. package/skills/forge/SKILL.md +56 -122
  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 +86 -0
  45. package/skills/forge/scripts/forgeSentinel.mjs +184 -0
  46. package/skills/forge/scripts/forgeSmith-admin.mjs +104 -0
  47. package/skills/forge/scripts/forgeSmith.mjs +164 -0
  48. package/skills/forge/scripts/graph.mjs +65 -9
  49. package/skills/forge/scripts/hook.mjs +2 -2
  50. package/skills/forge/scripts/inspect.mjs +56 -48
  51. package/skills/forge/scripts/parse-imports.mjs +0 -2
  52. package/skills/forge/scripts/pin.mjs +10 -3
  53. package/skills/forge/scripts/posttool.mjs +2 -2
  54. package/skills/forge/scripts/recommendation-engine.mjs +125 -0
  55. package/skills/forge/scripts/rollback.mjs +5 -3
  56. package/skills/forge/templates/agents/SKILL.md.template +283 -0
  57. package/skills/forge/templates/agents/agents/hooks.json +18 -0
  58. package/skills/forge/templates/agents/claude/CLAUDE.md +17 -16
  59. package/skills/forge/templates/agents/claude/settings.local.json +18 -0
  60. package/skills/forge/templates/agents/codex/hooks.json +18 -0
  61. package/skills/forge/templates/agents/cursor/.cursorrules +22 -5
  62. package/skills/forge/templates/agents/cursor/hooks.json +11 -0
  63. package/skills/forge/templates/agents/gemini/SKILL.md +13 -0
  64. package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
  65. package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
  66. package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
  67. package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
  68. package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
  69. package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
  70. package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
  71. package/skills/forge/tests/core.test.mjs +284 -0
  72. package/src/agents.mjs +35 -2
  73. package/src/cli.js +112 -39
  74. package/src/wizard.mjs +142 -90
@@ -0,0 +1,283 @@
1
+ # Idempotency — Operaciones Seguras para Retry
2
+
3
+ En sistemas distribuidos, las operaciones fallan. Los clientes reintentan. Sin idempotencia, un reintento puede crear facturas duplicadas, cargar dos veces la misma tarjeta o enviar un email dos veces. La idempotencia garantiza que N intentos producen el mismo resultado que uno.
4
+
5
+ ---
6
+
7
+ ## Definición
8
+
9
+ Una operación es **idempotente** si ejecutarla una o varias veces produce el mismo efecto secundario.
10
+
11
+ ```ts
12
+ GET /orders/123 → idempotente (natural, no cambia estado)
13
+ PUT /orders/123 → idempotente (el mismo body produce el mismo estado final)
14
+ DELETE /orders/123 → idempotente (borrar algo ya borrado no cambia nada)
15
+ POST /orders → NO es idempotente (crea un recurso nuevo cada vez)
16
+ PATCH /orders/123 → NO es idempotente por defecto (depende del delta)
17
+ ```
18
+
19
+ Para hacer POST y PATCH idempotentes se necesita un **idempotency key**.
20
+
21
+ ---
22
+
23
+ ## Idempotency Keys en APIs
24
+
25
+ El cliente envía una clave única en el header `Idempotency-Key`. El servidor:
26
+ 1. Si la clave es nueva: ejecuta la operación, guarda la respuesta, la retorna
27
+ 2. Si la clave ya existe: retorna la respuesta guardada sin ejecutar la operación
28
+
29
+ ### Implementación
30
+
31
+ ```ts
32
+ // Platform middleware
33
+ // platform/http/middleware/IdempotencyMiddleware.ts
34
+ export class IdempotencyMiddleware {
35
+ constructor(
36
+ private readonly idempotencyRepo: IIdempotencyRepository,
37
+ private readonly clock: IClock,
38
+ ) {}
39
+
40
+ async handle(req: Request, res: Response, next: NextFunction): Promise<void> {
41
+ // Solo para métodos mutantes
42
+ if (!["POST", "PATCH", "PUT"].includes(req.method)) return next();
43
+
44
+ const key = req.headers["idempotency-key"] as string;
45
+ if (!key) {
46
+ // POST sin key es válido pero no tiene protección de idempotencia
47
+ // Para operaciones críticas (pagos), debería ser obligatorio
48
+ return next();
49
+ }
50
+
51
+ // Validar formato UUID
52
+ if (!this.isValidUUID(key)) {
53
+ return res.status(400).json({
54
+ error: "Invalid idempotency key format. Must be a UUID v4.",
55
+ });
56
+ }
57
+
58
+ const existing = await this.idempotencyRepo.find(key);
59
+ if (existing) {
60
+ // Clave ya usada → devolver respuesta en caché
61
+ // Si la clave expiró, rechazar con 422
62
+ if (existing.isExpired(this.clock)) {
63
+ return res.status(422).json({
64
+ error: "Idempotency key expired. Use a new key.",
65
+ });
66
+ }
67
+ return res.status(existing.statusCode).json(existing.responseBody);
68
+ }
69
+
70
+ // Registrar clave antes de ejecutar (protege contra duplicados simultáneos)
71
+ await this.idempotencyRepo.save(IdempotencyEntry.pending(key, req));
72
+
73
+ // Ejecutar y guardar respuesta
74
+ res.on("finish", async () => {
75
+ await this.idempotencyRepo.save(
76
+ IdempotencyEntry.completed(key, res.statusCode, res.body),
77
+ );
78
+ });
79
+
80
+ next();
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Repositorio de idempotencia
86
+
87
+ ```ts
88
+ // infra/redis/IdempotencyRepository.ts
89
+ export class RedisIdempotencyRepository implements IIdempotencyRepository {
90
+ private readonly TTL_SECONDS = 60 * 60 * 24; // 24 horas
91
+
92
+ constructor(private readonly redis: Redis) {}
93
+
94
+ async find(key: string): Promise<IdempotencyEntry | null> {
95
+ const data = await this.redis.get(`idempotency:${key}`);
96
+ if (!data) return null;
97
+ return IdempotencyEntry.fromJSON(data);
98
+ }
99
+
100
+ async save(entry: IdempotencyEntry): Promise<void> {
101
+ await this.redis.set(
102
+ `idempotency:${entry.key}`,
103
+ entry.toJSON(),
104
+ "EX",
105
+ this.TTL_SECONDS,
106
+ );
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### Contract en shared/
112
+
113
+ ```ts
114
+ // shared/contracts/http/IIdempotencyRepository.ts
115
+ export interface IIdempotencyRepository {
116
+ find(key: string): Promise<IdempotencyEntry | null>;
117
+ save(entry: IdempotencyEntry): Promise<void>;
118
+ }
119
+ ```
120
+
121
+ ### Reglas del Idempotency Key
122
+
123
+ 1. **Generado por el cliente** (nunca por el servidor): el cliente necesita poder reintentar sin haber recibido respuesta
124
+ 2. **Formato UUID v4**: único, no secuencial, sin colisiones
125
+ 3. **TTL**: 24 horas típicamente. Si el cliente reintenta después del TTL, la clave expiró y debe generar una nueva
126
+ 4. **Único por recurso/operación**: un mismo key no debe reusarse para operaciones distintas
127
+ 5. **Protección de concurrencia**: dos requests simultáneos con el mismo key deben producir exactamente una ejecución (lock optimista en Redis)
128
+
129
+ ---
130
+
131
+ ## Idempotencia en Eventos
132
+
133
+ Los consumidores de eventos deben deduplicar por event ID para garantizar exactly-once processing.
134
+
135
+ ```ts
136
+ // base consumer pattern
137
+ export abstract class IdempotentEventConsumer<T extends DomainEvent> {
138
+ constructor(
139
+ protected readonly processedEvents: IProcessedEventRepository,
140
+ ) {}
141
+
142
+ async handle(event: T): Promise<void> {
143
+ // 1. Deduplicación
144
+ const alreadyProcessed = await this.processedEvents.exists(event.eventId);
145
+ if (alreadyProcessed) return;
146
+
147
+ // 2. Ejecutar lógica de negocio
148
+ await this.processEvent(event);
149
+
150
+ // 3. Marcar como procesado (idealmente en la misma transacción)
151
+ await this.processedEvents.markProcessed(event.eventId);
152
+ }
153
+
154
+ protected abstract processEvent(event: T): Promise<void>;
155
+ }
156
+ ```
157
+
158
+ ```ts
159
+ // Implementación concreta
160
+ export class OrderPlacedHandler extends IdempotentEventConsumer<OrderPlacedEvent> {
161
+ constructor(
162
+ processedEvents: IProcessedEventRepository,
163
+ private readonly inventoryService: IInventoryService,
164
+ ) {
165
+ super(processedEvents);
166
+ }
167
+
168
+ protected async processEvent(event: OrderPlacedEvent): Promise<void> {
169
+ await this.inventoryService.reserveStock(event.orderId, event.items);
170
+ }
171
+ }
172
+ ```
173
+
174
+ ### Tabla de eventos procesados
175
+
176
+ ```sql
177
+ CREATE TABLE IF NOT EXISTS public.processed_events (
178
+ event_id VARCHAR(255) PRIMARY KEY,
179
+ consumer_name VARCHAR(255) NOT NULL,
180
+ processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
181
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
182
+ );
183
+
184
+ -- Cleanup periódico: eventos procesados > 7 días
185
+ CREATE INDEX idx_processed_events_cleanup ON public.processed_events (processed_at);
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Idempotencia Natural por Operación
191
+
192
+ | Operación HTTP | Idempotencia natural | Cómo hacerla idempotente |
193
+ |---|---|---|
194
+ | **GET** | ✅ Sí | Nada. Es idempotente por definición. |
195
+ | **PUT** | ✅ Sí | Mismo body → mismo estado. |
196
+ | **DELETE** | ✅ Sí | Borrar algo ya borrado no cambia estado. |
197
+ | **POST** | ❌ No | Idempotency-Key header. |
198
+ | **PATCH** | ❌ No | Idempotency-Key header + operaciones basadas en estado (ej. "set status = X" es idempotente, "increment count" no lo es). |
199
+
200
+ ### Operaciones condicionalmente idempotentes
201
+
202
+ ```ts
203
+ // ❌ NO idempotente: el resultado cambia cada vez
204
+ PATCH /orders/123 { "action": "addItem", "productId": "abc" }
205
+
206
+ // ✅ Idempotente: el resultado es el mismo estado final
207
+ PUT /orders/123 { "items": ["abc", "def"] }
208
+
209
+ // ✅ Condicionalmente idempotente con idempotency key
210
+ POST /orders/123/items
211
+ Idempotency-Key: uuid-abc-123
212
+ Body: { "productId": "abc", "quantity": 1 }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Implementación en el Modelo de Forge
218
+
219
+ ### Estructura
220
+
221
+ ```
222
+ src/platform/
223
+ http/
224
+ middleware/
225
+ IdempotencyMiddleware.ts ← middleware que intercepta requests
226
+ contracts/
227
+ IIdempotencyRepository.ts ← interfaz en shared/
228
+
229
+ src/infra/
230
+ redis/
231
+ IdempotencyRepository.ts ← implementación con Redis
232
+
233
+ src/features/<name>/
234
+ adapters/
235
+ in/
236
+ events/
237
+ <Name>Handler.ts ← consumer con deduplicación
238
+ out/
239
+ persistence/
240
+ ProcessedEventRepository.ts ← tabla processed_events
241
+ ```
242
+
243
+ ### Reglas de ubicación
244
+
245
+ | Componente | Capa | Razón |
246
+ |---|---|---|
247
+ | Middleware HTTP | platform/ | Es infraestructura transversal |
248
+ | Interfaz IIdempotencyRepository | shared/contracts/ | Contrato puro sin implementación |
249
+ | Implementación Redis | infra/ | Implementación concreta |
250
+ | IdempotencyKey generación | Cliente (frontend/CLI) | El cliente necesita la clave antes del request |
251
+ | Deduplicación en consumidores | feature/adapters/in/events/ | Específico del feature |
252
+
253
+ ---
254
+
255
+ ## Anti-patrones
256
+
257
+ | Anti-patrón | Problema | Solución |
258
+ |---|---|---|
259
+ | **Idempotency sin expiración** | Las claves se acumulan infinitamente. | TTL obligatorio (24h típico). Configurable por operación. |
260
+ | **Claves generadas por el servidor** | El servidor genera la key y la devuelve. Si la respuesta se pierde, el cliente no puede reintentar. | El cliente genera la key (UUID v4). El servidor solo la valida. |
261
+ | **Idempotencia en GET** | GET debe ser idempotente por definición. Si no lo es, el problema es otro (efectos secundarios en GET). | No añadir idempotency keys a GET. Mover efectos secundarios a POST/PUT. |
262
+ | **Mutex como idempotencia** | Bloquear el recurso en vez de deduplicar la operación. | Idempotencia no es locking. La clave permite deduplicar sin bloquear. |
263
+ | **Idempotency key corta** | Claves predecibles (incrementales, timestamp). Un cliente puede adivinar claves de otros. | UUID v4 obligatorio. No secuencial. No predecible. |
264
+ | **No verificar método** | El middleware aplica a GET también, añadiendo latencia innecesaria. | Solo POST, PATCH, PUT. GET pasa sin verificar. |
265
+
266
+ ---
267
+
268
+ ## Conexión con Forge
269
+
270
+ | Comando | Acción |
271
+ |---|---|
272
+ | `forge cast` | Incluir idempotency middleware + repo Redis en features críticas |
273
+ | `forge quench` | Regla detect: feature con POST sin idempotency key |
274
+ | `forge inspect` | Reporta endpoints sin protección de idempotencia |
275
+ | `forge api` | Verifica que los contratos OpenAPI incluyen idempotency-key header |
276
+ | `forge temper` | Endurece DI del middleware de idempotencia |
277
+
278
+ ## Ver también
279
+
280
+ - `reference/transactional-outbox.md` — deduplicación en relayer
281
+ - `reference/api-design.md` — idempotency-key header en APIs
282
+ - `reference/events.md` — handlers idempotentes
283
+ - `reference/sagas.md` — retry seguro en sagas
@@ -127,3 +127,8 @@ Genera y mantiene el archivo `ARCHITECTURE.md` del proyecto.
127
127
  - Se actualiza automáticamente después de cada comando
128
128
  - No editar manualmente los campos auto-detectados
129
129
  - Forge preserva cualquier sección adicional que el usuario agregue
130
+
131
+ ## Ver también
132
+
133
+ - `reference/adr.md` — Architecture Decision Records como insumo para ARCHITECTURE.md
134
+ - `reference/principles.md` — principios arquitectónicos documentados en ARCHITECTURE.md
@@ -55,4 +55,10 @@ Inspecciona la conformidad arquitectónica del proyecto.
55
55
  node .opencode/skills/forge/scripts/inspect.mjs
56
56
  node .opencode/skills/forge/scripts/inspect.mjs --json
57
57
  node .opencode/skills/forge/scripts/detect.mjs --severity ERROR
58
+
59
+ ## Ver también
60
+
61
+ - `reference/evolutionary-architecture.md` — fitness functions como complemento al inspect
62
+ - `reference/assay.md` — interpretación cualitativa multi-persona del reporte de inspect
63
+ - `reference/principles.md` — principios contra los que inspect evalúa el proyecto
58
64
  ```
@@ -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