@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,340 @@
1
+ # Anti-Corruption Layer (ACL)
2
+
3
+ Cuando un bounded context debe integrarse con otro que tiene un modelo distinto (especialmente legacy o externo), la ACL es la capa que traduce sin contaminar.
4
+
5
+ ---
6
+
7
+ ## Definición y Propósito
8
+
9
+ Una **Anti-Corruption Layer** es un mecanismo de traducción entre dos bounded contexts. Su función es evitar que el modelo de un sistema externo (o legacy) "corrompa" el modelo de dominio del sistema nuevo.
10
+
11
+ ```mermaid
12
+ flowchart LR
13
+ subgraph "Sistema Legacy"
14
+ LegacyModel[(BD Legacy)]
15
+ Customer[CustomerLegacy]
16
+ end
17
+ subgraph "ACL"
18
+ Translator[Traductor]
19
+ Gateway[Gateway]
20
+ end
21
+ subgraph "Sistema Nuevo (Forge)"
22
+ DomainModel[(BD Nueva)]
23
+ Service[Orders Service]
24
+ end
25
+ Customer --> Gateway
26
+ Gateway --> Translator
27
+ Translator --> Service
28
+ Service --> DomainModel
29
+ ```
30
+
31
+ **Cuándo usar ACL:**
32
+ - Integración con un sistema legacy que no puede modificarse (SAP, Salesforce, mainframe)
33
+ - Integración con un servicio externo cuyo modelo no controlas (API de terceros)
34
+ - Migración incremental donde ambos sistemas coexisten (Strangler Fig)
35
+ - Dos bounded contexts que evolucionan con ritmos distintos pero necesitan comunicarse
36
+
37
+ **Cuándo NO usar ACL:**
38
+ - El sistema externo expone un Published Language estable (usa el contrato directamente)
39
+ - El modelo externo es idéntico o trivialmente mapeable (un mapper simple basta)
40
+ - La integración es temporal y el sistema externo será reemplazado pronto
41
+
42
+ ---
43
+
44
+ ## Estructura de una ACL
45
+
46
+ La ACL vive en `adapter/out/` de la feature que necesita protegerse:
47
+
48
+ ```
49
+ src/features/orders/
50
+ domain/
51
+ entities/
52
+ OrderEntity.ts
53
+ CustomerEntity.ts ← modelo limpio
54
+ repositories/
55
+ ICustomerRepository.ts ← puerto que la ACL implementa
56
+ adapters/
57
+ out/
58
+ legacy-crm/
59
+ CustomerGateway.ts ← llama al sistema legacy
60
+ CustomerTranslator.ts ← traduce del modelo legacy al dominio
61
+ CustomerACL.ts ← facade que combina gateway + translator
62
+ persistence/
63
+ PostgresCustomerRepository.ts ← implementación normal
64
+ ```
65
+
66
+ ### Componentes de la ACL
67
+
68
+ | Componente | Responsabilidad |
69
+ |---|---|
70
+ | **Gateway** | Comunicación con el sistema externo (HTTP, SOAP, archivos, BD) |
71
+ | **Translator** | Mapeo del modelo externo al modelo de dominio y viceversa |
72
+ | **Facade** | Orquesta gateway + translator. Implementa la interfaz del puerto. |
73
+ | **Contract** | DTOs del modelo externo (viven en la ACL, no contaminan el dominio) |
74
+
75
+ ---
76
+
77
+ ## Implementación
78
+
79
+ ### 1. Definir el puerto (dominio)
80
+
81
+ ```ts
82
+ // src/features/orders/domain/repositories/ICustomerRepository.ts
83
+ export interface ICustomerRepository {
84
+ findById(id: string): Promise<CustomerEntity | null>;
85
+ findByEmail(email: string): Promise<CustomerEntity | null>;
86
+ }
87
+ ```
88
+
89
+ ### 2. Gateway (comunicación con externo)
90
+
91
+ ```ts
92
+ // src/features/orders/adapters/out/legacy-crm/CustomerGateway.ts
93
+ // Este archivo es infra y puede depender de HTTP clients, SOAP, etc.
94
+
95
+ import { LegacyCustomerDTO } from "./LegacyCustomerDTO";
96
+
97
+ export class LegacyCRMGateway {
98
+ private readonly baseUrl: string;
99
+
100
+ constructor(baseUrl: string) {
101
+ this.baseUrl = baseUrl;
102
+ }
103
+
104
+ async fetchCustomer(crmId: string): Promise<LegacyCustomerDTO> {
105
+ const response = await fetch(`${this.baseUrl}/api/customers/${crmId}`, {
106
+ headers: { Authorization: `Bearer ${this.apiKey}` },
107
+ });
108
+ if (!response.ok) throw new Error(`CRM error: ${response.statusText}`);
109
+ return response.json();
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### 3. Translator (traducción)
115
+
116
+ ```ts
117
+ // src/features/orders/adapters/out/legacy-crm/CustomerTranslator.ts
118
+ // Este archivo es PURO. No depende de infraestructura. Solo transforma datos.
119
+
120
+ import { LegacyCustomerDTO } from "./LegacyCustomerDTO";
121
+ import { CustomerEntity } from "../../../domain/entities/CustomerEntity";
122
+
123
+ export class CustomerTranslator {
124
+ toDomain(dto: LegacyCustomerDTO): CustomerEntity {
125
+ return new CustomerEntity({
126
+ id: dto.customerId,
127
+ name: `${dto.firstName} ${dto.lastName}`,
128
+ email: dto.emailAddress,
129
+ status: this.mapStatus(dto.active),
130
+ createdAt: new Date(dto.registrationDate),
131
+ });
132
+ }
133
+
134
+ private mapStatus(legacyActive: boolean): "active" | "inactive" {
135
+ return legacyActive ? "active" : "inactive";
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### 4. Facade ACL (implementación del puerto)
141
+
142
+ ```ts
143
+ // src/features/orders/adapters/out/legacy-crm/CustomerACL.ts
144
+ // Implementa ICustomerRepository traduciendo desde el CRM legacy.
145
+
146
+ import { ICustomerRepository } from "../../../domain/repositories/ICustomerRepository";
147
+ import { CustomerEntity } from "../../../domain/entities/CustomerEntity";
148
+ import { LegacyCRMGateway } from "./CustomerGateway";
149
+ import { CustomerTranslator } from "./CustomerTranslator";
150
+
151
+ export class LegacyCRMRepository implements ICustomerRepository {
152
+ constructor(
153
+ private readonly gateway: LegacyCRMGateway,
154
+ private readonly translator: CustomerTranslator,
155
+ ) {}
156
+
157
+ async findById(id: string): Promise<CustomerEntity | null> {
158
+ try {
159
+ const dto = await this.gateway.fetchCustomer(id);
160
+ return this.translator.toDomain(dto);
161
+ } catch (error) {
162
+ // El gateway lanza errores técnicos. La ACL traduce a errores de dominio.
163
+ if (error.message.includes("404")) return null;
164
+ throw new Error("CustomerRepository.fetchFailed");
165
+ }
166
+ }
167
+
168
+ async findByEmail(email: string): Promise<CustomerEntity | null> {
169
+ // Si el CRM legacy no soporta búsqueda por email, la ACL puede:
170
+ // 1. Obtener todos los clientes y filtrar (ineficiente)
171
+ // 2. Cachear localmente y buscar en caché
172
+ // 3. Fallar con error explícito
173
+ throw new Error("CustomerRepository.methodNotSupported");
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### 5. Registro en DI
179
+
180
+ ```ts
181
+ // En la composición root (platform/di/container.ts)
182
+ container.registerSingleton<ICustomerRepository>("ICustomerRepository", {
183
+ useClass: process.env.USE_LEGACY_CRM === "true"
184
+ ? LegacyCRMRepository // ACL
185
+ : PostgresCustomerRepository, // implementación nativa
186
+ });
187
+ ```
188
+
189
+ La DI permite switchear entre legacy y nativo sin cambiar el caso de uso.
190
+
191
+ ---
192
+
193
+ ## Strangler Fig Pattern
194
+
195
+ El Strangler Fig permite migrar de un sistema legacy a uno nuevo incrementalmente, usando ACL como fachada.
196
+
197
+ ### Fase 1: ACL intercepta todo
198
+
199
+ ```
200
+ [API Gateway] → [ACL] → [Legacy CRM]
201
+ ```
202
+
203
+ Todas las llamadas pasan por la ACL que habla con el legacy.
204
+
205
+ ### Fase 2: Rutas individuales migradas
206
+
207
+ ```ts
208
+ // La ACL decide por ruta/característica si va al legacy o al nuevo
209
+ export class CustomerRouterACL implements ICustomerRepository {
210
+ constructor(
211
+ private readonly legacy: LegacyCRMRepository,
212
+ private readonly newRepo: PostgresCustomerRepository,
213
+ private readonly migrationFlag: FeatureFlagService,
214
+ ) {}
215
+
216
+ async findById(id: string): Promise<CustomerEntity | null> {
217
+ if (this.migrationFlag.isEnabled("customer-read")) {
218
+ return await this.newRepo.findById(id);
219
+ }
220
+ return await this.legacy.findById(id);
221
+ }
222
+ }
223
+ ```
224
+
225
+ ### Fase 3: Legacy retirado
226
+
227
+ ```
228
+ [API Gateway] → [PostgresCustomerRepository]
229
+ ```
230
+
231
+ La ACL ya no es necesaria. Se elimina el gateway, el translator y la facade.
232
+
233
+ ### Reglas para Strangler Fig exitoso
234
+
235
+ 1. **Un feature completo se migra a la vez**, no funciones sueltas
236
+ 2. **La ACL nunca modifica el legacy**. Solo lee y traduce
237
+ 3. **Feature flags** para activar/desactivar rutas migradas
238
+ 4. **Métricas de comparación**: response time, error rate, data consistency entre legacy y nuevo
239
+ 5. **Rollback plan**: desactivar el feature flag vuelve al legacy instantáneamente
240
+
241
+ ---
242
+
243
+ ## Conexión con el Modelo de Forge
244
+
245
+ ### Reglas de dependencia y ACL
246
+
247
+ | Regla | Relación con ACL |
248
+ |---|---|
249
+ | **R1** (feature → infra) | La ACL está en `adapter/out/` y accede a infra (el gateway). Está permitido porque el adapter es el puerto de salida. El dominio nunca ve infra. |
250
+ | **R7** (infra → feature) | El gateway externo llama a la ACL (infra → adapter). No viola R7 porque el adapter recibe la llamada y la traduce al dominio. |
251
+ | **R8** (cross-feature) | Si la ACL se comunica con otra feature de Forge, debe hacerlo mediante eventos o contratos en shared/, no con imports directos. |
252
+
253
+ ### La ACL en la arquitectura de 4 capas
254
+
255
+ ```
256
+ ┌─────────────────────────────────────────────────┐
257
+ │ features/orders/ │
258
+ │ domain/ ← modelo protegido │
259
+ │ CustomerEntity │
260
+ │ ICustomerRepository ← puerto │
261
+ │ adapters/out/ │
262
+ │ legacy-crm/ ← ACL completa │
263
+ │ CustomerGateway ← infra (llama externo) │
264
+ │ CustomerTranslator ← pure translation │
265
+ │ CustomerACL ← facade │
266
+ │ persistence/ │
267
+ │ PostgresCustomerRepository │
268
+ └─────────────────────────────────────────────────┘
269
+
270
+ ▼ (gateway)
271
+ ┌──────────────────────┐
272
+ │ CRM Legacy (externo) │
273
+ │ /api/customers/:id │
274
+ └──────────────────────┘
275
+ ```
276
+
277
+ ### Dónde NO poner la ACL
278
+
279
+ | Ubicación incorrecta | Problema |
280
+ |---|---|
281
+ | `src/shared/acl/` | La ACL es específica de un feature, no compartida. Cada feature tiene su propia ACL. |
282
+ | `src/infra/` | La ACL no es infraestructura genérica. Pertenece al adapter del feature. |
283
+ | `src/platform/` | Platform no debe saber qué sistemas externos existen. |
284
+
285
+ ---
286
+
287
+ ## Anti-patrones
288
+
289
+ | Anti-patrón | Problema | Solución |
290
+ |---|---|---|
291
+ | **Leaky Abstraction** | La ACL pasa DTOs del sistema externo en vez de traducir. El dominio termina dependiendo del modelo legacy. | El Translator debe mapear TODOS los campos. Si falta uno, el dominio no sabe que existe. |
292
+ | **Pasamanos (passthrough)** | La ACL no traduce nada. Solo delega. Es una capa que añade complejidad sin valor. | Si no hay traducción que hacer, no uses ACL. Usa el contrato directamente. |
293
+ | **ACL que escribe en el legacy** | La ACL muta el sistema externo para "facilitar" la integración. | La ACL solo traduce. Si necesitas escribir, el legacy debe exponer API de escritura. |
294
+ | **ACL compartida** | Una sola ACL para todos los features. | Cada feature tiene su propia ACL adaptada a su modelo de dominio. |
295
+ | **Gateway sin timeout** | Una llamada al legacy lenta bloquea todo el feature. | Timeouts, circuit breakers y fallbacks obligatorios en el gateway. |
296
+ | **ACL sin test** | No se puede verificar que la traducción es correcta. | Tests unitarios del Translator + tests de integración del Gateway. |
297
+
298
+ ### Testing de ACL
299
+
300
+ ```ts
301
+ // Test unitario del Translator (puro, sin infraestructura)
302
+ describe("CustomerTranslator", () => {
303
+ it("traduce LegacyCustomerDTO a CustomerEntity", () => {
304
+ const dto: LegacyCustomerDTO = {
305
+ customerId: "123",
306
+ firstName: "John",
307
+ lastName: "Doe",
308
+ emailAddress: "john@example.com",
309
+ active: true,
310
+ registrationDate: "2024-01-15T10:00:00Z",
311
+ };
312
+ const result = new CustomerTranslator().toDomain(dto);
313
+ expect(result.id).toBe("123");
314
+ expect(result.status).toBe("active");
315
+ });
316
+
317
+ it("mapea active=false a status=inactive", () => {
318
+ const dto = { ...baseDTO, active: false };
319
+ const result = new CustomerTranslator().toDomain(dto);
320
+ expect(result.status).toBe("inactive");
321
+ });
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Conexión con Forge
328
+
329
+ | Comando | Acción |
330
+ |---|---|
331
+ | `forge cast` | Durante el shape de la feature, detectar si necesita ACL |
332
+ | `forge inspect` | Detecta patrones que indican necesidad de ACL (imports directos a infra externa) |
333
+ | `forge relocate` | Puede generar ACL automática al migrar un feature desde legacy |
334
+ | `forge reforge` | Sugiere introducir ACL cuando detecta leaky abstractions |
335
+
336
+ ## Ver también
337
+
338
+ - `reference/bounded-contexts.md` — contexts que la ACL aísla
339
+ - `reference/modular-monolith.md` — migración con ACL entre módulos
340
+ - `reference/relocate.md` — uso de ACL durante migración legacy
@@ -60,3 +60,10 @@ export interface IPaginatedResponse<T> {
60
60
  - Errores normalizados: `{ error: { code, message, details? } }`
61
61
  - Idempotencia en POST con `Idempotency-Key` header
62
62
  - HATEOAS opcional, solo para APIs hipermedia
63
+
64
+ ## Ver también
65
+
66
+ - `reference/api-versioning.md` — versionado de APIs y deprecación
67
+ - `reference/idempotency.md` — idempotency keys en POST
68
+ - `reference/security-patterns.md` — AuthN/AuthZ, rate limiting, validación
69
+ - `reference/testing-patterns.md` — tests de controllers y endpoints
@@ -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