@ronaldjdevfs/forge 1.3.0-beta → 1.3.2
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.
- package/README.md +11 -4
- package/package.json +1 -1
- package/skills/forge/SKILL.md +57 -128
- package/skills/forge/command/forge.md +59 -14
- package/skills/forge/reference/adr.md +242 -0
- package/skills/forge/reference/anti-corruption-layer.md +340 -0
- package/skills/forge/reference/api-design.md +7 -0
- package/skills/forge/reference/api-versioning.md +354 -0
- package/skills/forge/reference/architectural-depth-checklist.md +311 -0
- package/skills/forge/reference/architecture-template.md +41 -0
- package/skills/forge/reference/assay.md +6 -0
- package/skills/forge/reference/bounded-contexts.md +311 -0
- package/skills/forge/reference/chain.md +6 -0
- package/skills/forge/reference/cohesion-checklist.md +256 -0
- package/skills/forge/reference/cqrs.md +286 -0
- package/skills/forge/reference/data-patterns.md +6 -0
- package/skills/forge/reference/di-strategies.md +6 -0
- package/skills/forge/reference/errors.md +5 -0
- package/skills/forge/reference/events.md +8 -0
- package/skills/forge/reference/evolutionary-architecture.md +300 -0
- package/skills/forge/reference/forge.md +7 -0
- package/skills/forge/reference/hooks.md +6 -0
- package/skills/forge/reference/idempotency.md +283 -0
- package/skills/forge/reference/inscribe.md +5 -0
- package/skills/forge/reference/inspect.md +6 -0
- package/skills/forge/reference/modular-monolith.md +252 -0
- package/skills/forge/reference/observability.md +5 -0
- package/skills/forge/reference/quench.md +5 -0
- package/skills/forge/reference/relocate.md +6 -0
- package/skills/forge/reference/sagas.md +359 -0
- package/skills/forge/reference/security-patterns.md +6 -0
- package/skills/forge/reference/smelt.md +6 -0
- package/skills/forge/reference/temper.md +6 -0
- package/skills/forge/reference/testing-patterns.md +6 -0
- package/skills/forge/reference/transactional-outbox.md +311 -0
- package/skills/forge/scripts/architecture.mjs +10 -5
- package/skills/forge/scripts/assay.mjs +2 -2
- package/skills/forge/scripts/chain.mjs +31 -5
- package/skills/forge/scripts/context.mjs +24 -4
- package/skills/forge/scripts/detect.mjs +57 -36
- package/skills/forge/scripts/forge-boot.mjs +108 -0
- package/skills/forge/scripts/forge-config.mjs +182 -3
- package/skills/forge/scripts/forge-state.mjs +1 -1
- package/skills/forge/scripts/forgeSentinel-lib.mjs +2 -2
- package/skills/forge/scripts/forgeSentinel.mjs +2 -2
- package/skills/forge/scripts/forgeSmith.mjs +2 -2
- package/skills/forge/scripts/graph.mjs +65 -9
- package/skills/forge/scripts/hook.mjs +2 -2
- package/skills/forge/scripts/inspect.mjs +56 -48
- package/skills/forge/scripts/parse-imports.mjs +0 -2
- package/skills/forge/scripts/posttool.mjs +211 -17
- package/skills/forge/scripts/recommendation-engine.mjs +125 -0
- package/skills/forge/scripts/rename.mjs +62 -14
- package/skills/forge/scripts/rollback.mjs +5 -3
- package/skills/forge/templates/feature/acl-gateway.ts.md +52 -0
- package/skills/forge/templates/feature/acl-repository.ts.md +36 -0
- package/skills/forge/templates/feature/acl-translator.ts.md +24 -0
- package/skills/forge/templates/feature/cqrs-query.ts.md +40 -0
- package/skills/forge/templates/feature/entity.ts.md +1 -1
- package/skills/forge/templates/feature/mapper.ts.md +1 -1
- package/skills/forge/templates/feature/outbox-repository.ts.md +36 -0
- package/skills/forge/templates/feature/repository-impl.ts.md +2 -2
- package/skills/forge/templates/feature/repository-interface.ts.md +2 -2
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
- package/skills/forge/templates/feature/schema.ts.md +1 -1
- package/skills/forge/templates/feature/use-case.ts.md +2 -2
- package/skills/forge/templates/platform/outbox-relayer.ts.md +80 -0
- package/skills/forge/tests/core.test.mjs +288 -4
- package/src/cli.js +26 -13
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# API Versioning — Evolución de Contratos
|
|
2
|
+
|
|
3
|
+
Las APIs evolucionan. Los clientes no siempre pueden actualizarse al mismo ritmo. El versionado de APIs permite cambiar contratos sin romper clientes existentes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Estrategias de Versionado
|
|
8
|
+
|
|
9
|
+
### URL Path (más común)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
GET /api/v1/orders
|
|
13
|
+
GET /api/v2/orders
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// features/orders/adapters/in/http/v1/OrderController.ts
|
|
18
|
+
@Router("/api/v1/orders")
|
|
19
|
+
export class OrderV1Controller {
|
|
20
|
+
@Get("/:id")
|
|
21
|
+
async getById(@Param("id") id: string) {
|
|
22
|
+
return this.useCase.execute(new GetOrderQuery(id));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// features/orders/adapters/in/http/v2/OrderController.ts
|
|
27
|
+
@Router("/api/v2/orders")
|
|
28
|
+
export class OrderV2Controller {
|
|
29
|
+
@Get("/:id")
|
|
30
|
+
async getById(@Param("id") id: string) {
|
|
31
|
+
const result = await this.useCase.execute(new GetOrderQuery(id));
|
|
32
|
+
// v2 devuelve datos adicionales que v1 no tenía
|
|
33
|
+
return { ...result, estimatedDelivery: result.shippingEstimate };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Pros:** explícito, fácil de rutear, fácil de cachear
|
|
39
|
+
**Contras:** URLs feas, versionado de toda la API (no granular)
|
|
40
|
+
|
|
41
|
+
### Header (Accept / Content-Type)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
GET /api/orders
|
|
45
|
+
Accept: application/vnd.company.v1+json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Middleware que selecciona versión según Accept header
|
|
50
|
+
export class VersionResolver {
|
|
51
|
+
resolve(req: Request): string {
|
|
52
|
+
const accept = req.headers["accept"] || "";
|
|
53
|
+
const match = accept.match(/application\/vnd\.company\.v(\d+)\+json/);
|
|
54
|
+
return match ? `v${match[1]}` : "v1"; // default v1
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Pros:** URLs limpias, versionado granular por endpoint
|
|
60
|
+
**Contras:** complejidad en el cliente, difícil de debuggear, caché HTTP limitada
|
|
61
|
+
|
|
62
|
+
### Query Parameter
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
GET /api/orders?version=2
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Pros:** simple, fácil de testear
|
|
69
|
+
**Contras:** contamina la semántica del query param, fácil de olvidar
|
|
70
|
+
|
|
71
|
+
### Content Negotiation (recurso vs representación)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
GET /api/orders/123
|
|
75
|
+
Accept: application/json;version=2
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Pros:** semánticamente correcto (negociación de contenido)
|
|
79
|
+
**Contras:** complejidad, poco soporte tooling
|
|
80
|
+
|
|
81
|
+
### Recomendación de Forge
|
|
82
|
+
|
|
83
|
+
| Contexto | Estrategia |
|
|
84
|
+
|---|---|
|
|
85
|
+
| API pública (terceros) | URL path (`/api/v1/`) — la más explícita |
|
|
86
|
+
| API interna (entre features) | Header Accept — URLs limpias para el monolith |
|
|
87
|
+
| API móvil | URL path — los clientes móviles cachean URLs |
|
|
88
|
+
| BFF (Backend for Frontend) | Sin versionado (se despliega junto al frontend) |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Compatibilidad
|
|
93
|
+
|
|
94
|
+
### Cambios backward-compatible
|
|
95
|
+
|
|
96
|
+
| Cambio | Ejemplo |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Añadir campo opcional en response | `{ name, email }` → `{ name, email, phone? }` |
|
|
99
|
+
| Añadir endpoint nuevo | `GET /v1/orders/:id/items` |
|
|
100
|
+
| Añadir campo en request con default | `{ name }` → `{ name, locale?: "en" }` |
|
|
101
|
+
| Extender enum | `Status.Active` → `Status.Active \| Status.Pending` |
|
|
102
|
+
| Relajar validación | Campo required → opcional |
|
|
103
|
+
|
|
104
|
+
### Cambios breaking (requieren nueva versión)
|
|
105
|
+
|
|
106
|
+
| Cambio | Ejemplo |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Eliminar campo del response | `{ name, email }` → `{ name }` |
|
|
109
|
+
| Renombrar campo | `{ email }` → `{ emailAddress }` |
|
|
110
|
+
| Cambiar tipo de campo | `{ price: string }` → `{ price: number }` |
|
|
111
|
+
| Hacer campo requerido | `{ locale? }` → `{ locale }` |
|
|
112
|
+
| Eliminar endpoint | `DELETE /v1/users` |
|
|
113
|
+
| Cambiar estructura | `{ address: string }` → `{ address: { street, city } }` |
|
|
114
|
+
| Cambiar error codes | `404` → `400` para mismo error |
|
|
115
|
+
|
|
116
|
+
### Compatibilidad en TypeScript
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// shared/contracts/orders/v1/OrderDTO.ts
|
|
120
|
+
export interface OrderV1DTO {
|
|
121
|
+
id: string;
|
|
122
|
+
total: number;
|
|
123
|
+
status: string;
|
|
124
|
+
items: { sku: string; quantity: number }[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// shared/contracts/orders/v2/OrderDTO.ts
|
|
128
|
+
export interface OrderV2DTO extends Omit<OrderV1DTO, "items"> {
|
|
129
|
+
total: number;
|
|
130
|
+
status: string;
|
|
131
|
+
items: { sku: string; quantity: number; name: string; imageUrl: string }[];
|
|
132
|
+
estimatedDelivery: string; // nuevo campo
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Versionado en el Modelo de Forge
|
|
139
|
+
|
|
140
|
+
### Estructura de directorios
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
src/features/orders/
|
|
144
|
+
adapters/
|
|
145
|
+
in/http/
|
|
146
|
+
v1/
|
|
147
|
+
OrderController.ts
|
|
148
|
+
OrderRoutes.ts
|
|
149
|
+
OrderValidator.ts ← validación de input v1
|
|
150
|
+
OrderPresenter.ts ← formateo de output v1
|
|
151
|
+
v2/
|
|
152
|
+
OrderController.ts
|
|
153
|
+
OrderRoutes.ts
|
|
154
|
+
OrderValidator.ts
|
|
155
|
+
OrderPresenter.ts
|
|
156
|
+
application/
|
|
157
|
+
use-cases/ ← los use cases NO se versionan
|
|
158
|
+
GetOrderUseCase.ts ← compartido entre v1 y v2
|
|
159
|
+
PlaceOrderUseCase.ts
|
|
160
|
+
mappers/ ← los mappers pueden tener versiones
|
|
161
|
+
v1/
|
|
162
|
+
OrderMapper.ts ← entidad de dominio → DTO v1
|
|
163
|
+
v2/
|
|
164
|
+
OrderMapper.ts ← entidad de dominio → DTO v2
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Principio: la lógica de negocio no se versiona
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// ✅ Los use cases son compartidos entre versiones
|
|
171
|
+
// v1 y v2 llaman al mismo use case
|
|
172
|
+
// Solo cambia el mapper (cómo se presenta el resultado)
|
|
173
|
+
|
|
174
|
+
// v1 mapper: devuelve el DTO original
|
|
175
|
+
export class OrderV1Mapper {
|
|
176
|
+
toDTO(order: OrderEntity): OrderV1DTO {
|
|
177
|
+
return {
|
|
178
|
+
id: order.id,
|
|
179
|
+
total: order.total,
|
|
180
|
+
status: order.status,
|
|
181
|
+
items: order.items.map((i) => ({ sku: i.sku, quantity: i.quantity })),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// v2 mapper: devuelve más información
|
|
187
|
+
export class OrderV2Mapper {
|
|
188
|
+
toDTO(order: OrderEntity): OrderV2DTO {
|
|
189
|
+
return {
|
|
190
|
+
id: order.id,
|
|
191
|
+
total: order.total,
|
|
192
|
+
status: order.status,
|
|
193
|
+
items: order.items.map((i) => ({
|
|
194
|
+
sku: i.sku,
|
|
195
|
+
quantity: i.quantity,
|
|
196
|
+
name: i.productName,
|
|
197
|
+
imageUrl: i.productImage,
|
|
198
|
+
})),
|
|
199
|
+
estimatedDelivery: this.calculateDelivery(order),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Routing multi-versión
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// platform/http/Router.ts
|
|
209
|
+
export class ApiRouter {
|
|
210
|
+
constructor() {
|
|
211
|
+
this.registerV1Routes();
|
|
212
|
+
this.registerV2Routes();
|
|
213
|
+
this.registerVersionRedirect();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private registerVersionRedirect(): void {
|
|
217
|
+
// Si el cliente no especifica versión, redirigir a la más reciente estable
|
|
218
|
+
this.router.get("/api/orders", (req, res) => {
|
|
219
|
+
res.redirect("/api/v2/orders");
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Deprecación
|
|
228
|
+
|
|
229
|
+
### Política de deprecación
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
v1: lanzada 2025-01
|
|
233
|
+
v2: lanzada 2026-01 (v1 deprecated)
|
|
234
|
+
v3: lanzada 2027-01 (v1 sunset, v2 deprecated)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Headers de deprecación
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// Middleware que añade headers de deprecación a versiones antiguas
|
|
241
|
+
export class DeprecationMiddleware {
|
|
242
|
+
private readonly sunsetVersions: Record<string, string> = {
|
|
243
|
+
v1: "2027-01-01", // fecha de retiro
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
handle(req: Request, res: Response, next: NextFunction): void {
|
|
247
|
+
const version = this.extractVersion(req.path);
|
|
248
|
+
const sunsetDate = this.sunsetVersions[version];
|
|
249
|
+
if (sunsetDate) {
|
|
250
|
+
res.setHeader("Sunset", sunsetDate);
|
|
251
|
+
res.setHeader("Deprecation", `true`);
|
|
252
|
+
res.setHeader(
|
|
253
|
+
"Link",
|
|
254
|
+
`</api/v2${req.path.replace(/\/api\/v[0-9]/, "")}>; rel="successor-version"`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
next();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Política de retiro
|
|
263
|
+
|
|
264
|
+
1. **Anunciar**: deprecación con 6 meses de anticipación (header `Deprecation: true` + `Sunset: fecha`)
|
|
265
|
+
2. **Migrar**: soporte simultáneo durante el período de transición
|
|
266
|
+
3. **Monitorear**: tracking de uso de versiones antiguas
|
|
267
|
+
4. **Retirar**: cuando el tráfico de la versión antigua es < 1%, retirar con PR visible
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Comando para monitorear uso de versiones
|
|
271
|
+
forge api --usage
|
|
272
|
+
# v1: 12% de requests
|
|
273
|
+
# v2: 88% de requests
|
|
274
|
+
# Sugerencia: v1 está lista para retiro (< 15%)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Testing Multi-Versión
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// tests/integration/orders/OrderApi.test.ts
|
|
283
|
+
describe("Orders API", () => {
|
|
284
|
+
const versions = ["v1", "v2"];
|
|
285
|
+
|
|
286
|
+
versions.forEach((version) => {
|
|
287
|
+
describe(`${version} — getOrder`, () => {
|
|
288
|
+
it("returns 200 for existing order", async () => {
|
|
289
|
+
const response = await request(app)
|
|
290
|
+
.get(`/api/${version}/orders/test-id`)
|
|
291
|
+
.expect(200);
|
|
292
|
+
|
|
293
|
+
if (version === "v1") {
|
|
294
|
+
expect(response.body).not.toHaveProperty("estimatedDelivery");
|
|
295
|
+
}
|
|
296
|
+
if (version === "v2") {
|
|
297
|
+
expect(response.body).toHaveProperty("estimatedDelivery");
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns same core fields across versions", async () => {
|
|
302
|
+
const [v1Res, v2Res] = await Promise.all([
|
|
303
|
+
request(app).get("/api/v1/orders/test-id"),
|
|
304
|
+
request(app).get("/api/v2/orders/test-id"),
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
expect(v1Res.body.id).toBe(v2Res.body.id);
|
|
308
|
+
expect(v1Res.body.total).toBe(v2Res.body.total);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Compatibilidad backward en CI
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# Verificar que v2 no rompe cambios backward
|
|
319
|
+
forge api --check-compatibility
|
|
320
|
+
# Checking orders v1 → v2... ✔ No breaking changes
|
|
321
|
+
# Checking users v1 → v2... ✖ Breaking: 'email' renamed to 'emailAddress'
|
|
322
|
+
# → Crear ADR y plan de deprecación
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Anti-patrones
|
|
328
|
+
|
|
329
|
+
| Anti-patrón | Problema | Solución |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| **Versionado global** | Versionar `v1/`, `v2/` para toda la API. Un cambio mínimo en un endpoint fuerza nueva versión de toda la API. | Versionado por recurso/feature. No toda la API tiene que estar en la misma versión. |
|
|
332
|
+
| **Mantener versiones para siempre** | "No podemos romper al cliente X". La deuda de mantener N versiones crece. | Política de retiro explícita (Sunset header + fecha). Máximo 2 versiones activas simultáneas. |
|
|
333
|
+
| **Versionado por "fecha"** | `/api/2025-01/orders`. Sin semántica de estabilidad. | Usar `v1`, `v2` (semver simplificado: major version only). |
|
|
334
|
+
| **Lógica de negocio versionada** | El use case v2 es distinto del v1. La lógica se duplica. | Los use cases son compartidos. Solo versionar la presentación (mappers + controllers). |
|
|
335
|
+
| **Versionado sin deprecación** | Se lanza v2 pero v1 no se depreca oficialmente. Los clientes nunca migran. | Header Deprecation + Sunset. Comunicación proactiva a clientes. |
|
|
336
|
+
| **API versionada pero eventos no** | La API versiona contratos HTTP, pero los eventos internos cambian sin versión. | Versionar schemas de eventos también (ej. `OrderPlaced.v2`). |
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Conexión con Forge
|
|
341
|
+
|
|
342
|
+
| Comando | Acción |
|
|
343
|
+
|---|---|
|
|
344
|
+
| `forge cast payments` | Crea feature con estructura v1/ en adapters/in/http/ |
|
|
345
|
+
| `forge api --check-compatibility` | Verifica que los cambios no rompen versiones anteriores |
|
|
346
|
+
| `forge api --usage` | Muestra distribución de tráfico entre versiones |
|
|
347
|
+
| `forge reforge` | Migra controllers de v1 a v2, depreca la anterior |
|
|
348
|
+
| `forge inspect` | Reporta versiones sin Deprecation header cuando hay versión superior |
|
|
349
|
+
|
|
350
|
+
## Ver también
|
|
351
|
+
|
|
352
|
+
- `reference/api-design.md` — diseño de APIs REST/GraphQL
|
|
353
|
+
- `reference/adr.md` — ADRs para decisiones de versionado
|
|
354
|
+
- `reference/evolutionary-architecture.md` — evolución guiada de APIs
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Architectural Depth — Checklist de Referencias
|
|
2
|
+
|
|
3
|
+
Checklist de las 10 nuevas referencias estratégicas para Forge. Cada entrada define el alcance exacto, secciones obligatorias, relaciones con referencias existentes y criterio de completitud.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Estado actual
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
[x] 01 — bounded-contexts.md — DDD Estratégico
|
|
11
|
+
[x] 02 — modular-monolith.md — Estrategia de despliegue
|
|
12
|
+
[x] 03 — adr.md — Registro de decisiones
|
|
13
|
+
[x] 04 — anti-corruption-layer.md — Integración legacy
|
|
14
|
+
[x] 05 — evolutionary-architecture.md — Evolución arquitectónica
|
|
15
|
+
[x] 06 — cqrs.md — CQRS
|
|
16
|
+
[x] 07 — sagas.md — Transacciones distribuidas
|
|
17
|
+
[x] 08 — transactional-outbox.md — Fiabilidad de eventos
|
|
18
|
+
[x] 09 — idempotency.md — Idempotencia
|
|
19
|
+
[x] 10 — api-versioning.md — Evolución de contratos
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 01 — `bounded-contexts.md`
|
|
25
|
+
|
|
26
|
+
**Propósito:** Fundamentar la capa `features/` en DDD Estratégico. Sin bounded contexts, las features son sólo directorios.
|
|
27
|
+
|
|
28
|
+
**Depende de:** nada (fundacional)
|
|
29
|
+
**Es usado por:** `modular-monolith.md`, `anti-corruption-layer.md`, `cqrs.md`, `sagas.md`
|
|
30
|
+
|
|
31
|
+
### Secciones obligatorias
|
|
32
|
+
|
|
33
|
+
- [ ] **Fundamentos de DDD Estratégico**: dominio, subdominio (core/supporting/generic), bounded context
|
|
34
|
+
- [ ] **Ubiquitous Language**: por contexto, glosario compartido, conflictos de lenguaje
|
|
35
|
+
- [ ] **Context Mapping**: Partnership, Shared Kernel, Customer-Supplier, Conformist, Anti-Corruption Layer, Open-Host Service, Published Language, Separate Ways
|
|
36
|
+
- [ ] **Identificación de bounded contexts**: heurísticas (equipo, lenguaje, modelo, base de datos)
|
|
37
|
+
- [ ] **Mapeo visual**: diagrama de contexts con relaciones y flujos
|
|
38
|
+
- [ ] **Relación con el modelo de Forge**: cómo cada feature se corresponde con (parte de) un bounded context
|
|
39
|
+
- [ ] **Anti-patrones**: contextos gigantes (orphan core), contextos fantasma, contextos sin lenguaje
|
|
40
|
+
- [ ] **Ejemplo completo**: sistema de e-commerce con 4-5 bounded contexts mapeados
|
|
41
|
+
- [ ] **Conexión con reglas R8 y R9**: cómo los contextos prohíben acoplamiento directo y ciclos
|
|
42
|
+
|
|
43
|
+
### Criterio de completitud
|
|
44
|
+
|
|
45
|
+
Un lector puede tomar cualquier feature existente y determinar su bounded context, su relación con otros contexts, y si el mapeo actual viola algún patrón de integridad.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 02 — `modular-monolith.md`
|
|
50
|
+
|
|
51
|
+
**Propósito:** Proveer un marco de decisión para elegir entre monolith modular y microservicios, alineado con el modelo de 4 capas.
|
|
52
|
+
|
|
53
|
+
**Depende de:** `bounded-contexts.md`
|
|
54
|
+
**Es usado por:** `relocate.md`, `reforge.md`, `cast.md`
|
|
55
|
+
|
|
56
|
+
### Secciones obligatorias
|
|
57
|
+
|
|
58
|
+
- [ ] **Definición de modular monolith**: módulos con boundaries fuertes, mismo proceso, despliegue único
|
|
59
|
+
- [ ] **Espectro Monolith → Modular → Microservices**: continuo, no binario
|
|
60
|
+
- [ ] **Marco de decisión**: cohesión, acoplamiento, topología de equipo (Conway's Law), escalabilidad, deployment autonomy
|
|
61
|
+
- [ ] **Cuándo mantener monolith**: equipo pequeño, dominio cohesionado, latencia crítica, inicio incierto
|
|
62
|
+
- [ ] **Señales para partir**: equipo scaling, deployment bottleneck, boundaries maduros, different failure characteristics
|
|
63
|
+
- [ ] **Criterio de corte por capa**: qué puede vivir como servicio vs qué debe compartirse (shared, platform)
|
|
64
|
+
- [ ] **Integración con Forge**: cómo modelar módulos usando features existentes, qué reglas (R8) se relajan dentro del monolith
|
|
65
|
+
- [ ] **Transición controlada**: split de features sin reescritura (strangler fig aplicado a features)
|
|
66
|
+
- [ ] **Anti-patrones**: distributed monolith, premature splitting, shared database entre servicios, nano-services
|
|
67
|
+
- [ ] **Ejemplo**: monolith modular de SaaS facturación que eventualmente parte billing en servicio separado
|
|
68
|
+
|
|
69
|
+
### Criterio de completitud
|
|
70
|
+
|
|
71
|
+
Un equipo puede evaluar su arquitectura actual contra el marco de decisión y obtener una recomendación accionable sobre si partir o consolidar y por dónde empezar.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 03 — `adr.md`
|
|
76
|
+
|
|
77
|
+
**Propósito:** Capturar y preservar decisiones arquitectónicas como parte del workflow de Forge.
|
|
78
|
+
|
|
79
|
+
**Depende de:** nada
|
|
80
|
+
**Es usado por:** `reforge.md`, `cast.md`, `inscribe.md`, todos
|
|
81
|
+
|
|
82
|
+
### Secciones obligatorias
|
|
83
|
+
|
|
84
|
+
- [ ] **Formato ADR estándar**: Title, Status, Context, Decision, Consequences (plantilla)
|
|
85
|
+
- [ ] **Variantes**: ADR simple (5 secciones), ADR extendido (con alternatives, compliance), ADR ligero (1 párrafo)
|
|
86
|
+
- [ ] **Estados**: Proposed → Accepted / Deprecated / Superseded / Amended
|
|
87
|
+
- [ ] **Integración con `inscribe`**: anexar ADRs activos en ARCHITECTURE.md
|
|
88
|
+
- [ ] **Integración con `assay`**: las decisiones como insumo para el ensayo multi-persona
|
|
89
|
+
- [ ] **Cuándo escribir un ADR**: scoping, tecnología, patrón, estándar, cambio de regla, excepción
|
|
90
|
+
- [ ] **ADRs como fuente de verdad**: enlazar ADRs desde reglas de detect.mjs, desde inline ignores
|
|
91
|
+
- [ ] **Ejemplos**: ADRs reales del modelo Forge (ej. "usar capa Shared en vez de cross-feather imports", "adoptar Prisma como ORM")
|
|
92
|
+
- [ ] **Tooling**: script para crear, listar, cambiar estado de ADRs
|
|
93
|
+
- [ ] **Anti-patrones**: ADRs que nunca se leen, ADRs sin contexto, ADRs sin consecuencia, ADRs de frameworks
|
|
94
|
+
|
|
95
|
+
### Criterio de completitud
|
|
96
|
+
|
|
97
|
+
Un `forge inscribe` genera ARCHITECTURE.md con enlaces a ADRs activos. Un nuevo miembro del equipo puede entender las decisiones clave en 10 minutos.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 04 — `anti-corruption-layer.md`
|
|
102
|
+
|
|
103
|
+
**Propósito:** Aislar sistemas legacy o externos sin contaminar el modelo de dominio de Forge.
|
|
104
|
+
|
|
105
|
+
**Depende de:** `bounded-contexts.md`
|
|
106
|
+
**Es usado por:** `relocate.md`, `reforge.md`, `cast.md`
|
|
107
|
+
|
|
108
|
+
### Secciones obligatorias
|
|
109
|
+
|
|
110
|
+
- [ ] **Definición y propósito ACL**: traducir entre modelos, evitar corrupción del modelo de dominio
|
|
111
|
+
- [ ] **Estructura de una ACL**: adapters de entrada (traducen del externo al dominio), adapters de salida (traducen del dominio al externo)
|
|
112
|
+
- [ ] **Strangler Fig pattern**: migración incremental con ACL como fachada
|
|
113
|
+
- [ ] **Implementación en el modelo de Forge**: la ACL vive en `adapter/out/` de una feature y traduce entre `infra/` y el modelo de dominio
|
|
114
|
+
- [ ] **Mapeo de integraciones legacy**: cómo modelar sistemas externos como bounded contexts con ACL
|
|
115
|
+
- [ ] **Estrategias de traducción**: event-based (publicar/suscribir), service-based (llamadas sincrónicas), repository-based (datos compartidos)
|
|
116
|
+
- [ ] **Conexión con reglas R1 y R7**: cómo ACL permite feature → infra sin violar la regla (porque el adapter traduce)
|
|
117
|
+
- [ ] **Detección automática**: qué patrones en detect.mjs indican necesidad de ACL
|
|
118
|
+
- [ ] **Ejemplo**: feature de "Orders" migrando de legacy SQL a nuevo schema con ACL + Strangler Fig
|
|
119
|
+
- [ ] **Anti-patrones**: ACL que filtra sin traducir (leaky abstraction), ACL que muta el origen, ACL como pasamanos
|
|
120
|
+
|
|
121
|
+
### Criterio de completitud
|
|
122
|
+
|
|
123
|
+
Un desarrollador puede identificar cuándo necesita una ACL, modelarla dentro del feature correspondiente, y ejecutar la migración con `relocate` sin romper el sistema existente.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 05 — `evolutionary-architecture.md`
|
|
128
|
+
|
|
129
|
+
**Propósito:** Guiar la evolución continua de la arquitectura sin reescrituras, usando fitness functions.
|
|
130
|
+
|
|
131
|
+
**Depende de:** nada (fundacional)
|
|
132
|
+
**Es usado por:** `inspect.md`, `reforge.md`, `graph.md`, `quench.md`
|
|
133
|
+
|
|
134
|
+
### Secciones obligatorias
|
|
135
|
+
|
|
136
|
+
- [ ] **Definición**: arquitectura que evoluciona incrementalmente guiada por fitness functions
|
|
137
|
+
- [ ] **Fitness functions**: tests automatizados que validan características arquitectónicas (acoplamiento, modularidad, performance, seguridad)
|
|
138
|
+
- [ ] **Tipos de fitness functions**: estáticas (lint-level), dinámicas (runtime), periódicas (benchmark), trigger-based (CI)
|
|
139
|
+
- [ ] **Implementación en Forge**: las reglas R1-R9 como fitness functions gobernadas por detect.mjs
|
|
140
|
+
- [ ] **Fitness functions custom**: cómo el usuario define sus propias funciones y se registran en registry/rules.mjs
|
|
141
|
+
- [ ] **Guía de cambio incremental**: smallest viable change, refactor patterns, scaffolding antes de feature completo
|
|
142
|
+
- [ ] **Evolución de boundaries**: cómo partir, fusionar o mover features sin reescribir
|
|
143
|
+
- [ ] **Integración con CI/CD**: fitness functions en pipeline, gate de deployment, alertas de regresión
|
|
144
|
+
- [ ] **Ejemplo**: fitness function "no feature importa infra" evolucionando a "ningún módulo importa otro módulo sin interfaz"
|
|
145
|
+
- [ ] **Anti-patrones**: big-bang rewrite, frozen architecture, analysis paralysis, chasing tech debt sin métrica
|
|
146
|
+
|
|
147
|
+
### Criterio de completitud
|
|
148
|
+
|
|
149
|
+
Un equipo puede añadir fitness functions personalizadas, integrarlas en su pipeline, y medir la salud arquitectónica en cada PR sin intervención manual.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 06 — `cqrs.md`
|
|
154
|
+
|
|
155
|
+
**Propósito:** Modelar separación de commands y queries dentro de features con demandas asimétricas.
|
|
156
|
+
|
|
157
|
+
**Depende de:** `bounded-contexts.md`
|
|
158
|
+
**Es usado por:** `data-patterns.md`, `events.md`, `sagas.md`, `cast.md`
|
|
159
|
+
|
|
160
|
+
### Secciones obligatorias
|
|
161
|
+
|
|
162
|
+
- [ ] **Fundamentos**: Command (escritura, efecto secundario, validación) vs Query (lectura, proyección, sin efectos)
|
|
163
|
+
- [ ] **Cuándo aplicar CQRS**: modelos de lectura/escritura divergentes, escalabilidad asimétrica, equipos separados, event sourcing
|
|
164
|
+
- [ ] **Implementación en el modelo de Forge**: command → use-case, query → repository o query service separado
|
|
165
|
+
- [ ] **Read models**: proyecciones desnormalizadas, tablas de lectura, caché de queries
|
|
166
|
+
- [ ] **CQRS parcial (sin event sourcing)**: commands y queries separados en la aplicación, misma BD
|
|
167
|
+
- [ ] **CQRS completo**: read model separado (tabla, BD, cache), eventual consistency
|
|
168
|
+
- [ ] **Materialized views / Projections**: cómo mantener read models actualizados
|
|
169
|
+
- [ ] **Separación de interfaces**: ICommandBus, IQueryBus como contratos en shared/
|
|
170
|
+
- [ ] **Conexión con reglas**: CQRS no viola ninguna regla de Forge porque commands y queries residen dentro del mismo feature
|
|
171
|
+
- [ ] **Anti-patrones**: CQRS everywhere (para CRUD simple), query leak (lógica de negocio en read model), eventual consistency ignorada
|
|
172
|
+
- [ ] **Ejemplo**: feature "Analytics" con CQRS parcial + read model en Redis via infra/redis
|
|
173
|
+
|
|
174
|
+
### Criterio de completitud
|
|
175
|
+
|
|
176
|
+
Un feature complejo con demands asimétricas puede modelarse con CQRS dentro de la estructura de Forge sin violar reglas y sin over-engineering.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 07 — `sagas.md`
|
|
181
|
+
|
|
182
|
+
**Propósito:** Coordinar transacciones multi-feature sin violar la regla R8 (no acoplamiento directo entre features).
|
|
183
|
+
|
|
184
|
+
**Depende de:** `bounded-contexts.md`, `events.md`
|
|
185
|
+
**Es usado por:** `cast.md`, `reforge.md`, `events.md`
|
|
186
|
+
|
|
187
|
+
### Secciones obligatorias
|
|
188
|
+
|
|
189
|
+
- [ ] **Problema**: transacciones que cruzan bounded contexts sin ACID distribuido
|
|
190
|
+
- [ ] **Definición de Saga**: secuencia de transacciones locales con compensación
|
|
191
|
+
- [ ] **Coreografía (Choreography)**: cada participante publica/escucha eventos, decisión descentralizada
|
|
192
|
+
- [ ] **Orquestación (Orchestration)**: coordinador central que instruye a los participantes
|
|
193
|
+
- [ ] **Compensación**: transacciones reversibles, acciones compensatorias, consistencia eventual
|
|
194
|
+
- [ ] **Manejo de fallos**: retry con backoff, dead letter queue, fallback humano, saga log
|
|
195
|
+
- [ ] **Implementación en el modelo de Forge**: saga orchestrator como feature independiente (ej. "checkout-saga"), saga participantes usan eventos de dominio + adapters
|
|
196
|
+
- [ ] **Conexión con reglas R8 y R5**: cómo las sagas coordinan features sin importarlas directamente, eventos como contrato en shared/
|
|
197
|
+
- [ ] **Testing de sagas**: unit (participante individual), integration (flujo completo con mock), resilience (fallos y compensaciones)
|
|
198
|
+
- [ ] **Anti-patrones**: saga sin compensación (irreversible), orchestrator con lógica de negocio, coreografía sin trazabilidad, timeout único
|
|
199
|
+
- [ ] **Ejemplo**: saga de checkout (Inventory → Payment → Shipping) con orquestación y compensación por fallo de pago
|
|
200
|
+
|
|
201
|
+
### Criterio de completitud
|
|
202
|
+
|
|
203
|
+
Un desarrollador puede modelar un flujo multi-feature usando sagas sin violar R8, con compensaciones claras, testing cubierto y trazabilidad.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 08 — `transactional-outbox.md`
|
|
208
|
+
|
|
209
|
+
**Propósito:** Garantizar entrega confiable de eventos sin exponer inconsistencias transaccionales.
|
|
210
|
+
|
|
211
|
+
**Depende de:** `events.md`
|
|
212
|
+
**Es usado por:** `sagas.md`, `cqrs.md`, `events.md`
|
|
213
|
+
|
|
214
|
+
### Secciones obligatorias
|
|
215
|
+
|
|
216
|
+
- [ ] **Problema**: dual-write (escribir en BD + publicar evento) atómico sin 2PC
|
|
217
|
+
- [ ] **Patrón Outbox**: escribir evento en tabla outbox dentro de la misma transacción que la operación de negocio
|
|
218
|
+
- [ ] **Outbox processor (relayer)**: proceso que lee la tabla outbox y publica eventos al message broker
|
|
219
|
+
- [ ] **Garantías**: at-least-once delivery, exactly-once processing con idempotencia
|
|
220
|
+
- [ ] **Implementaciones**: poll-based (relayer periódico), log-based (CDC con Debezium), hybrid
|
|
221
|
+
- [ ] **Integración con el modelo de Forge**: outbox en infra/prisma o infra/mongodb, processor como script/scheduler en platform/scheduler
|
|
222
|
+
- [ ] **Manejo de fallos**: retry con exponential backoff, poison messages, dead letter queue, alertas de outbox stuck
|
|
223
|
+
- [ ] **Idempotencia en consumidores**: deduplicación por event ID, idempotency key
|
|
224
|
+
- [ ] **Conexión con reglas**: outbox es infra que feature escribe mediante su adapter, sin violar R1
|
|
225
|
+
- [ ] **Anti-patrones**: outbox sin cleanup (tabla infinita), processor sin límite de reintentos, eventos sin idempotencia
|
|
226
|
+
- [ ] **Ejemplo**: feature "Orders" escribe pedido + outbox event en misma transacción Prisma → processor publica a RabbitMQ → "Payments" consume
|
|
227
|
+
|
|
228
|
+
### Criterio de completitud
|
|
229
|
+
|
|
230
|
+
Un feature que publica eventos puede implementar outbox pattern siguiendo el template de Forge, garantizando at-least-once delivery sin riesgo de inconsistencias.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 09 — `idempotency.md`
|
|
235
|
+
|
|
236
|
+
**Propósito:** Garantizar operaciones seguras para retry en APIs, eventos y procesos asíncronos.
|
|
237
|
+
|
|
238
|
+
**Depende de:** `api-design.md`
|
|
239
|
+
**Es usado por:** `api-design.md`, `events.md`, `sagas.md`, `cast.md`
|
|
240
|
+
|
|
241
|
+
### Secciones obligatorias
|
|
242
|
+
|
|
243
|
+
- [ ] **Definición**: propiedad de una operación que puede aplicarse múltiples veces sin efecto secundario adicional
|
|
244
|
+
- [ ] **Tipos de idempotencia**: natural (GET), por clave (idempotency key), por semántica (last-write-wins)
|
|
245
|
+
- [ ] **Idempotency keys en APIs**: cabecera Idempotency-Key, almacenamiento en BD/cache, deduplicación, respuesta en caché
|
|
246
|
+
- [ ] **Idempotencia en eventos**: event ID como clave, deduplicación en consumidor, exactly-once semantics
|
|
247
|
+
- [ ] **Implementación en el modelo de Forge**: middleware en platform/http, repository en infra/redis, contract en shared/contracts
|
|
248
|
+
- [ ] **Idempotencia en sagas**: cómo cada paso de saga debe ser idempotente para retry seguro
|
|
249
|
+
- [ ] **Manejo de expiración**: TTL de claves de idempotencia, cleanup, conflictos de clave
|
|
250
|
+
- [ ] **Testing de idempotencia**: enviar misma request N veces, verificar resultado único
|
|
251
|
+
- [ ] **Conexión con reglas**: idempotency middleware es platform, repositorio de claves es infra, contracts son shared
|
|
252
|
+
- [ ] **Anti-patrones**: idempotency sin expiración, claves generadas por el servidor, idempotencia en GET, mutex como idempotencia
|
|
253
|
+
- [ ] **Ejemplo**: POST /payments con Idempotency-Key, key almacenada en Redis, respuesta en caché, consumidor de eventos con deduplicación
|
|
254
|
+
|
|
255
|
+
### Criterio de completitud
|
|
256
|
+
|
|
257
|
+
Cada API de mutación en el feature expone idempotency keys. Cada consumidor de eventos maneja deduplicación. Retry es seguro en toda la cadena.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 10 — `api-versioning.md`
|
|
262
|
+
|
|
263
|
+
**Propósito:** Evolucionar contratos de API sin romper clientes existentes.
|
|
264
|
+
|
|
265
|
+
**Depende de:** `api-design.md`
|
|
266
|
+
**Es usado por:** `api-design.md`, `reforge.md`, `cast.md`
|
|
267
|
+
|
|
268
|
+
### Secciones obligatorias
|
|
269
|
+
|
|
270
|
+
- [ ] **Estrategias de versionado**: URL path (/v1/), header (Accept: application/vnd.api+json;version=1), content negotiation, query param
|
|
271
|
+
- [ ] **Compatibilidad**: backward compatible (additive changes), breaking changes (removal, rename, type change, required→optional)
|
|
272
|
+
- [ ] **Evolución de OpenAPI**: spec versionada, changelog automático, diff entre versiones
|
|
273
|
+
- [ ] **Versionado en el modelo de Forge**: controllers versionados por feature (features/users/adapters/in/http/v1/, v2/), routes con prefijo
|
|
274
|
+
- [ ] **Deprecación**: cabeceras Sunset, Deprecation, Retirement policy, migración de clientes
|
|
275
|
+
- [ ] **Internal vs Public API**: versionado estricto para pública, semver para interna
|
|
276
|
+
- [ ] **Integration testing multi-versión**: tests que corren contra v1 y v2 simultáneamente
|
|
277
|
+
- [ ] **Conexión con reglas**: ningún controller versionado debe violar R0 (cero lógica de negocio). La lógica vive en use cases
|
|
278
|
+
- [ ] **Anti-patrones**: versionado por "fecha" sin estabilidad, mantener N versiones sin política de muerte, versionado de toda la API en vez de por endpoint
|
|
279
|
+
- [ ] **Ejemplo**: feature "Users" migrando de v1 a v2 con cambio de modelo, controllers separados, deprecación gradual
|
|
280
|
+
|
|
281
|
+
### Criterio de completitud
|
|
282
|
+
|
|
283
|
+
Un feature puede evolucionar su API de forma segura, con versionado explícito, deprecación controlada, tests multi-versión, y sin tocar la lógica de negocio.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Integración en SKILL.md
|
|
288
|
+
|
|
289
|
+
- [x] Agregar las 10 referencias a la tabla **Module Index** en SKILL.md
|
|
290
|
+
- [x] Agregar entradas de **Command Routing** para los nuevos temas (lenguaje natural → referencia)
|
|
291
|
+
- [ ] Verificar que las referencias existentes que mencionan temas ahora cubiertos (events.md → sagas + outbox, data-patterns.md → CQRS) hagan `Ver: reference/<nueva>.md`
|
|
292
|
+
|
|
293
|
+
## Templates
|
|
294
|
+
|
|
295
|
+
- [x] Evaluar si `templates/feature/` necesita nuevos templates (saga orchestrator, outbox processor, ACL adapter, CQRS query service)
|
|
296
|
+
- [x] saga-orchestrator.ts.md — orchestrator con compensaciones
|
|
297
|
+
- [x] cqrs-query.ts.md — query service para lecturas separadas
|
|
298
|
+
- [x] acl-repository.ts.md — ACL que implementa repositorio de dominio
|
|
299
|
+
- [x] acl-translator.ts.md — traducción entre DTO externo y entidad de dominio
|
|
300
|
+
- [x] acl-gateway.ts.md — comunicación con sistema externo (HTTP)
|
|
301
|
+
- [x] outbox-repository.ts.md — repositorio de outbox integrado en UnitOfWork
|
|
302
|
+
- [x] outbox-relayer.ts.md — relayer en platform/scheduler
|
|
303
|
+
- [x] domain-event.ts.md — clase base DomainEvent con eventId para deduplicación
|
|
304
|
+
- [ ] Evaluar si `templates/shared/` necesita contratos de idempotencia o de eventos de integración
|
|
305
|
+
|
|
306
|
+
## Testing
|
|
307
|
+
|
|
308
|
+
- [x] transactional-outbox pattern — 5 tests (entry lifecycle, retry policy, DLQ, required fields, pending detection)
|
|
309
|
+
- [x] idempotency pattern — 5 tests (UUID validation, cached response, separate keys, TTL expiry, method filtering)
|
|
310
|
+
- [x] anti-corruption-layer pattern — 5 tests (external→domain mapping, domain→external mapping, null handling, 404 handling, delegation order)
|
|
311
|
+
- [ ] Prioridad futura: ADR workflow, evolutionary architecture fitness functions
|