@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,300 @@
1
+ # Evolutionary Architecture
2
+
3
+ La arquitectura no es un diseño inicial que se congela. Es una estructura que evoluciona con el conocimiento del dominio, el tamaño del equipo y las restricciones del negocio. Forge está diseñado para guiar esa evolución sin reescrituras.
4
+
5
+ ---
6
+
7
+ ## Definición
8
+
9
+ Una **arquitectura evolutiva** es aquella en la que los cambios significativos pueden realizarse de forma incremental, guiados por **fitness functions** que verifican que las propiedades arquitectónicas se mantienen.
10
+
11
+ **Propiedades de una arquitectura evolutiva:**
12
+ - **Incremental**: los cambios grandes se dividen en pasos pequeños y reversibles
13
+ - **Guiada por métricas**: se sabe si un cambio mejora o degrada la arquitectura
14
+ - **Fitness functions automatizadas**: las propiedades se verifican en CI
15
+ - **Sin big-bang rewrites**: nunca se reescribe desde cero
16
+
17
+ ---
18
+
19
+ ## Fitness Functions
20
+
21
+ Una **fitness function** es un test automatizado que valida una característica arquitectónica. Forge ya implementa varias:
22
+
23
+ ### Built-in (R1-R9)
24
+
25
+ Las 9 reglas de Forge son fitness functions:
26
+
27
+ ```ts
28
+ // scripts/registry/rules.mjs (conceptual)
29
+ const rules = {
30
+ R1: {
31
+ name: "feature → infra prohibited",
32
+ severity: "CRITICAL",
33
+ check: (edge) =>
34
+ edge.from.type === "feature" && edge.to.type === "infra",
35
+ },
36
+ R8: {
37
+ name: "cross-feature direct import prohibited",
38
+ severity: "ERROR",
39
+ check: (edge) =>
40
+ edge.from.type === "feature" && edge.to.type === "feature",
41
+ },
42
+ // R2-R7, R9 con estructura similar
43
+ };
44
+ ```
45
+
46
+ Estas fitness functions se ejecutan en:
47
+ - `node scripts/detect.mjs` — detección local
48
+ - `forge quench` — validación completa
49
+ - PostToolUse hook — después de cada escritura del agente
50
+
51
+ ### Custom Fitness Functions
52
+
53
+ Los usuarios pueden registrar sus propias funciones:
54
+
55
+ ```ts
56
+ // scripts/registry/rules.mjs
57
+ import { registerRule } from "./registry/rules.mjs";
58
+
59
+ registerRule({
60
+ id: "CUSTOM_01",
61
+ name: "no console.log in use-cases",
62
+ severity: "WARNING",
63
+ check: ({ filePath, content }) =>
64
+ filePath.includes("application/use-cases") && content.includes("console.log"),
65
+ description: "Los casos de uso no deben tener console.log. Usar logger inyectado.",
66
+ });
67
+ ```
68
+
69
+ ### Tipos de Fitness Functions
70
+
71
+ | Tipo | Ejecución | Ejemplo |
72
+ |---|---|---|
73
+ | **Estática** | Lint/build | `detect.mjs` analiza imports |
74
+ | **Dinámica** | Runtime | Verificar que event bus no pierde eventos |
75
+ | **Benchmark** | CI periódico | Tiempo de respuesta de queries < 200ms |
76
+ | **Trigger-based** | Evento (PR, deploy) | No hay imports directos entre features |
77
+ | **Contrato** | CI multi-servicio | Las APIs son compatibles con versiones anteriores |
78
+
79
+ ### Integración en CI
80
+
81
+ ```yaml
82
+ # .github/workflows/forge-fitness.yml
83
+ jobs:
84
+ forge-quench:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - run: node .opencode/skills/forge/scripts/detect.mjs
89
+ env:
90
+ FORGE_STRICT: "true"
91
+ - run: node .opencode/skills/forge/scripts/chain.mjs --json
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Guided Change
97
+
98
+ El flujo de cambio guiado de Forge asegura que cada modificación arquitectónica es segura:
99
+
100
+ ### 1. Estado actual (before)
101
+
102
+ ```bash
103
+ node scripts/inspect.mjs --json
104
+ # Score: 85
105
+ # Violaciones: 2 WARNING (R9 borderline, naming)
106
+ ```
107
+
108
+ ### 2. Proponer cambio
109
+
110
+ ```bash
111
+ forge reforge --move features/old-payments to features/payments/v2
112
+ # Generate plan: split feature, add ACL, migrate use-cases
113
+ ```
114
+
115
+ ### 3. Verificar antes de aplicar
116
+
117
+ ```bash
118
+ forge quench --diff
119
+ # Simula el cambio y reporta impacto en score
120
+ ```
121
+
122
+ ### 4. Ejecutar con rollback
123
+
124
+ ```bash
125
+ forge reforge --apply
126
+ # Guarda backup en .forge/backup/
127
+ # Si falla: forge rollback --last
128
+ ```
129
+
130
+ ### 5. Estado actual (after)
131
+
132
+ ```bash
133
+ node scripts/inspect.mjs --json
134
+ # Score: 92 (+7)
135
+ # Violaciones: 0
136
+ ```
137
+
138
+ Si el score baja, el cambio se rechaza automáticamente (configurable con `FORGE_ALLOW_DEGRADE=true`).
139
+
140
+ ---
141
+
142
+ ## Evolución Típica de un Proyecto
143
+
144
+ ### Fase 1: Prototipo / MVP
145
+
146
+ ```
147
+ 10 features, 1 equipo, monolith
148
+ Reglas: todas activas
149
+ Foco: velocidad de entrega
150
+ Tolerancia: alta para violaciones temporales
151
+ ```
152
+
153
+ ```bash
154
+ forge quench --allow-warnings
155
+ # Reporta violaciones pero no bloquea
156
+ ```
157
+
158
+ ### Fase 2: Crecimiento
159
+
160
+ ```
161
+ 20 features, 2 equipos, monolith modular
162
+ Reglas: estrictas (R1-R9 bloquean)
163
+ Foco: boundaries
164
+ ```
165
+
166
+ ```bash
167
+ forge quench --strict
168
+ # Las violaciones ERROR bloquean el PR
169
+ ```
170
+
171
+ ### Fase 3: Escalamiento
172
+
173
+ ```
174
+ 40 features, 3+ equipos, modular → microservicios
175
+ Reglas: R1-R9 + custom (CQRS, outbox, SLA)
176
+ Foco: autonomía de equipos
177
+ ```
178
+
179
+ ```bash
180
+ forge inspect --extended
181
+ # Incluye fitness functions custom
182
+ ```
183
+
184
+ ### Fase 4: Madurez
185
+
186
+ ```
187
+ N features, equipos autónomos, servicios independientes
188
+ Reglas: fitness functions distribuidas
189
+ Foco: evolución continua sin regresión
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Smallest Viable Change
195
+
196
+ Cada cambio arquitectónico debe ser el **cambio más pequeño que mejora la arquitectura sin romper funcionalidad**.
197
+
198
+ | Cambio grande (evitar) | Cambio pequeño (preferir) |
199
+ |---|---|
200
+ | Extraer 3 features como microservicios a la vez | Extraer 1 feature, verificar, repetir |
201
+ | Reescribir todo el ORM | Migrar un repositorio por PR |
202
+ | Refactorizar "toda la capa de aplicación" | Refactorizar un caso de uso, probar, continuar |
203
+ | Cambiar de framework | Aislar framework tras interfaces primero, luego reemplazar |
204
+
205
+ ### Patrón: Scaffolding antes de Feature
206
+
207
+ Antes de implementar una feature completa, crear la estructura del feature y verificar que no viola reglas:
208
+
209
+ ```bash
210
+ # Fase 1: scaffold
211
+ mkdir -p src/features/payments/{domain,application/use-cases,adapters/in/http,adapters/out/persistence}
212
+ touch src/features/payments/domain/PaymentEntity.ts
213
+ touch src/features/payments/domain/IPaymentRepository.ts
214
+
215
+ # Fase 2: verify
216
+ forge quench
217
+ # Score: 100 (aún sin implementar, pero los boundaries son correctos)
218
+
219
+ # Fase 3: implementar use-case
220
+ touch src/features/payments/application/use-cases/ProcessPaymentUseCase.ts
221
+
222
+ # Fase 4: verify again
223
+ forge quench
224
+ # Score: 100 (el use-case solo importa de domain y shared)
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Evolución de Boundaries
230
+
231
+ ### Partir un feature
232
+
233
+ ```bash
234
+ # Antes: features/catalog (15k líneas, toca todo)
235
+ # Después: features/catalog (core) + features/search (búsqueda)
236
+
237
+ forge cast search --from catalog
238
+ # 1. Crea features/search con estructura completa
239
+ # 2. Mueve SearchService y SearchIndexer de catalog a search
240
+ # 3. Crea contratos en shared/contracts/catalog/ para que search consuma datos
241
+ # 4. Verifica que no quedan imports de catalog → search ni viceversa
242
+ ```
243
+
244
+ ### Fusionar features
245
+
246
+ ```bash
247
+ # Antes: features/standard-checkout + features/express-checkout
248
+ # (80% del código duplicado entre ambos)
249
+
250
+ forge reforge --merge features/express-checkout into features/checkout
251
+ # 1. Mueve el código único de express a checkout
252
+ # 2. Parametriza el checkout con "mode: standard | express"
253
+ # 3. Elimina features/express-checkout
254
+ # 4. Verifica que nada importaba de express-checkout
255
+ ```
256
+
257
+ ### Mover un shared kernel a package
258
+
259
+ ```bash
260
+ # Antes: src/shared/contracts/ (referencia local)
261
+ # Después: @company/contracts (npm package)
262
+
263
+ forge reforge --publish shared/contracts as @company/contracts
264
+ # 1. Extrae contracts/ a packages/contracts/
265
+ # 2. Configura build con tsc
266
+ # 3. Actualiza imports en todas las features
267
+ # 4. Verifica con detect.mjs que los nuevos imports son válidos
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Anti-patrones
273
+
274
+ | Anti-patrón | Problema | Solución |
275
+ |---|---|---|
276
+ | **Big-Bang Rewrite** | Se reescribe todo. El sistema legacy se congela. El rewrite nunca alcanza el feature parity. | Strangler Fig + ACL. Migrar feature por feature. |
277
+ | **Frozen Architecture** | "No podemos cambiar la estructura ahora". La arquitectura se vuelve un impedimento. | Fitness functions + guided change. El cambio seguro está soportado por diseño. |
278
+ | **Analysis Paralysis** | Demasiado tiempo diseñando, poco tiempo implementando. | Smallest viable change. El diseño emerge, no se predice. |
279
+ | **Tech Debt sin métrica** | Se acumula deuda sin saber cuánta ni dónde. | `forge inspect` da score numérico. La deuda se mide, no se estima. |
280
+ | **Rewrite por moda** | "Pasamos a microservicios porque es moderno". | `reference/modular-monolith.md` — evaluar antes de partir. |
281
+ | **Golden Hammer** | Forzar CQRS, Event Sourcing o Hexagonal en features que no lo necesitan. | Cada referencia tiene "cuándo usarlo". Si no aplica, no lo fuerces. |
282
+
283
+ ---
284
+
285
+ ## Conexión con Forge
286
+
287
+ | Comando | Acción |
288
+ |---|---|
289
+ | `forge inspect` | Score + violaciones + fitness functions |
290
+ | `forge quench` | Ejecuta fitness functions en el código actual |
291
+ | `forge reforge` | Cambio arquitectónico guiado con verificación |
292
+ | `forge relocate` | Migración incremental con rollback |
293
+ | `forge chain` | Verifica que el grafo de dependencias evoluciona saludablemente |
294
+ | `forge graph` | Visualiza la evolución del grafo arquitectónico |
295
+
296
+ ## Ver también
297
+
298
+ - `reference/adr.md` — ADRs como registro de cambios evolutivos
299
+ - `reference/principles.md` — principios que las fitness functions protegen
300
+ - `reference/modular-monolith.md` — evolución de monolith a microservicios
@@ -42,3 +42,10 @@ Inicializa un proyecto para trabajar con Forge como Backend Architecture Operati
42
42
  | Platform layer ausente | SUGGESTION |
43
43
  | Dependencias faltantes | WARNING |
44
44
  | Ownership con huérfanos | WARNING |
45
+
46
+ ## Ver también
47
+
48
+ - `reference/bounded-contexts.md` — identificación de contexts al inicializar
49
+ - `reference/modular-monolith.md` — decisión de estructura al iniciar proyecto
50
+ - `reference/principles.md` — principios que guían la inicialización
51
+ - `reference/evolutionary-architecture.md` — bootstrap como primer paso evolutivo
@@ -60,3 +60,9 @@ Las reglas ignoradas se almacenan en `.forge/hooks-ignore.json`:
60
60
 
61
61
  Cuando una regla está ignorada, el hook no bloquea el commit por
62
62
  violaciones de esa regla, aunque el detector sigue reportándolas.
63
+
64
+ ## Ver también
65
+
66
+ - `reference/quench.md` — validación que el hook ejecuta en pre-commit
67
+ - `reference/evolutionary-architecture.md` — fitness functions como hook
68
+ - `reference/adr.md` — ADRs como insumo para validación en hook
@@ -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
  ```