@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,242 @@
|
|
|
1
|
+
# Architecture Decision Records (ADR)
|
|
2
|
+
|
|
3
|
+
Las decisiones arquitectónicas son el activo más valioso de un proyecto a largo plazo. Sin registro, cada nueva incorporación al equipo redescubre lo mismo. Los ADRs capturan el qué, el por qué y las consecuencias de cada decisión.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Formato ADR Estándar
|
|
8
|
+
|
|
9
|
+
Cada ADR es un archivo en `docs/adr/` con el formato `NNNN-title-with-dashes.md`.
|
|
10
|
+
|
|
11
|
+
```md
|
|
12
|
+
# NNNN — Título corto pero descriptivo
|
|
13
|
+
|
|
14
|
+
**Estado:** [Proposed | Accepted | Deprecated | Superseded | Amended]
|
|
15
|
+
**Fecha:** YYYY-MM-DD
|
|
16
|
+
**Decisores:** [lista de personas que tomaron la decisión]
|
|
17
|
+
|
|
18
|
+
## Contexto
|
|
19
|
+
|
|
20
|
+
Describe el problema o situación que motiva la decisión. Incluye:
|
|
21
|
+
- Restricciones técnicas o de negocio
|
|
22
|
+
- Alternativas consideradas brevemente
|
|
23
|
+
- Por qué el status quo no es aceptable
|
|
24
|
+
- Enlaces a ADRs relacionados
|
|
25
|
+
|
|
26
|
+
## Decisión
|
|
27
|
+
|
|
28
|
+
La decisión que se tomó. Debe ser específica, no genérica.
|
|
29
|
+
|
|
30
|
+
> Adoptamos Prisma como ORM para la capa de infra, con esquemas separados
|
|
31
|
+
> por schema de base de datos Postgres, mapeando cada feature a un schema.
|
|
32
|
+
|
|
33
|
+
## Consecuencias
|
|
34
|
+
|
|
35
|
+
Lo que cambia a partir de esta decisión:
|
|
36
|
+
|
|
37
|
+
- **Positivas:**
|
|
38
|
+
- Type-safe queries sin runtime validation
|
|
39
|
+
- Migraciones automáticas con `prisma migrate`
|
|
40
|
+
- Schemas por feature como primer paso hacia microservicios
|
|
41
|
+
- **Negativas:**
|
|
42
|
+
- Vendor lock-in con Prisma (cambiar de ORM requiere reescribir repos)
|
|
43
|
+
- Migraciones lentas en bases de datos con millones de registros
|
|
44
|
+
- Dependencia de `prisma generate` como paso de build
|
|
45
|
+
- **Neutrales:**
|
|
46
|
+
- El equipo necesita aprender Prisma (curva de 1-2 semanas)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Variantes
|
|
52
|
+
|
|
53
|
+
### ADR Completo (recomendado para decisiones fundacionales)
|
|
54
|
+
|
|
55
|
+
Incluye Contexto + Decisión + Consecuencias + Alternativas evaluadas.
|
|
56
|
+
|
|
57
|
+
```md
|
|
58
|
+
## Alternativas Consideradas
|
|
59
|
+
|
|
60
|
+
| Alternativa | Pros | Contras | Veredicto |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| TypeORM | Maduro, Decorators | Performance pobre en joins complejos | ❌ |
|
|
63
|
+
| Drizzle | SQL-like, sin decorators | Ecosistema más pequeño | ❌ |
|
|
64
|
+
| Prisma | Type-safe, Migraciones, Schemas | Vendor lock-in, generate step | ✅ |
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### ADR Ligero (para decisiones tácticas)
|
|
68
|
+
|
|
69
|
+
```md
|
|
70
|
+
# 0012 — Usar tRPC para endpoints internos
|
|
71
|
+
|
|
72
|
+
**Estado:** Accepted
|
|
73
|
+
**Fecha:** 2026-05-10
|
|
74
|
+
|
|
75
|
+
**Contexto:** Los endpoints entre features dentro del monolith
|
|
76
|
+
necesitan type-safety sin overhead de serialización REST.
|
|
77
|
+
|
|
78
|
+
**Decisión:** Los endpoints internos en platform/http usarán tRPC.
|
|
79
|
+
Los endpoints públicos siguen siendo REST con OpenAPI.
|
|
80
|
+
|
|
81
|
+
**Consecuencias:** Type-safety extremo entre features pero
|
|
82
|
+
acoplamiento a tRPC en la capa de plataforma.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### ADR de Excepción (para decisiones que violan una regla de Forge)
|
|
86
|
+
|
|
87
|
+
```md
|
|
88
|
+
# 0023 — Ignorar R1 temporalmente en ReportEngine
|
|
89
|
+
|
|
90
|
+
**Estado:** Accepted
|
|
91
|
+
**Fecha:** 2026-06-15
|
|
92
|
+
**Expira:** 2026-09-15
|
|
93
|
+
|
|
94
|
+
**Contexto:** ReportEngine necesita acceso directo a datos de infra
|
|
95
|
+
para generar reportes en tiempo real. Extraer a servicio separado
|
|
96
|
+
requiere 3 sprints.
|
|
97
|
+
|
|
98
|
+
**Decisión:** Se permite `feature/reports → infra/prisma` como
|
|
99
|
+
excepción temporal, documentada en ADR y con `forge-ignore: R1`
|
|
100
|
+
en los imports afectados.
|
|
101
|
+
|
|
102
|
+
**Consecuencias:** Degradación arquitectónica controlada.
|
|
103
|
+
Se revertirá antes de la expiración.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Estados de un ADR
|
|
109
|
+
|
|
110
|
+
```mermaid
|
|
111
|
+
stateDiagram-v2
|
|
112
|
+
[*] --> Proposed
|
|
113
|
+
Proposed --> Accepted
|
|
114
|
+
Proposed --> Rejected
|
|
115
|
+
Accepted --> Amended
|
|
116
|
+
Accepted --> Superseded
|
|
117
|
+
Accepted --> Deprecated
|
|
118
|
+
Deprecated --> [*]
|
|
119
|
+
Superseded --> [*]
|
|
120
|
+
Amended --> Accepted
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Estado | Significado |
|
|
124
|
+
|---|---|
|
|
125
|
+
| **Proposed** | Propuesto, en discusión |
|
|
126
|
+
| **Accepted** | Aprobado e implementado |
|
|
127
|
+
| **Rejected** | Descartado, se preserva para no repetir |
|
|
128
|
+
| **Deprecated** | Ya no se aplica, pero sigue vigente para sistemas existentes |
|
|
129
|
+
| **Superseded** | Reemplazado por otro ADR |
|
|
130
|
+
| **Amended** | Modificado parcialmente, el ADR original + amendment |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Integración con Forge
|
|
135
|
+
|
|
136
|
+
### ADRs y ARCHITECTURE.md
|
|
137
|
+
|
|
138
|
+
`forge inscribe` debe incluir en ARCHITECTURE.md:
|
|
139
|
+
|
|
140
|
+
```md
|
|
141
|
+
## Architecture Decision Records
|
|
142
|
+
|
|
143
|
+
| ADR | Título | Estado | Fecha |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| 0001 | Adoptar Prisma como ORM | Accepted | 2025-12-01 |
|
|
146
|
+
| 0002 | Schemas separados por feature | Accepted | 2025-12-10 |
|
|
147
|
+
| 0003 | Event Bus asíncrono con RabbitMQ | Proposed | 2026-01-15 |
|
|
148
|
+
| 0004 | Feature Splits de Catalog a Search | Superseded por 0007 | 2026-02-01 |
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### ADRs y `forge assay`
|
|
152
|
+
|
|
153
|
+
El ensayo multi-persona (`assay`) debe considerar ADRs como fuente de información:
|
|
154
|
+
|
|
155
|
+
- **Bezos**: evalúa si la decisión es reversible o irreversible
|
|
156
|
+
- **Fowler**: evalúa la evolución de la decisión en el tiempo
|
|
157
|
+
- **Arquitecta Senior**: evalúa consistencia con el modelo arquitectónico
|
|
158
|
+
|
|
159
|
+
### ADRs y reglas inline ignore
|
|
160
|
+
|
|
161
|
+
Cuando se usa `forge-ignore` para excepcionar una regla, debe referenciar el ADR que la autoriza:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// forge-ignore: R1 — ver ADR-0023
|
|
165
|
+
import { PrismaClient } from "../../infra/prisma/client";
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### ADRs y `forge quench`
|
|
169
|
+
|
|
170
|
+
`forge quench` debe verificar que:
|
|
171
|
+
- Los ADRs aceptados tienen su decisión implementada
|
|
172
|
+
- Los ADRs con expiración no han vencido sin renovación
|
|
173
|
+
- No hay `forge-ignore` sin ADR asociado
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Cuándo escribir un ADR
|
|
178
|
+
|
|
179
|
+
| Situación | Ejemplo | ADR necesario |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| Decisión fundacional | Framework, ORM, BD, message broker | ✅ Obligatorio |
|
|
182
|
+
| Patrón arquitectónico | CQRS, Event Sourcing, Sagas | ✅ Recomendado |
|
|
183
|
+
| Cambio de regla de Forge | Ignorar R8 entre dos features | ✅ Obligatorio |
|
|
184
|
+
| Tecnología nueva | Adoptar Redis, Elasticsearch | ✅ Recomendado |
|
|
185
|
+
| Estándar de equipo | Formato de commits, naming conventions | ✅ Ligero |
|
|
186
|
+
| Excepción temporal | Ignorar R1 por 3 sprints | ✅ Obligatorio |
|
|
187
|
+
| Cambio de provider | Migrar de AWS a GCP | ✅ Obligatorio |
|
|
188
|
+
| Refactor mayor | Extraer Catalog como microservicio | ✅ Obligatorio |
|
|
189
|
+
| Dependencia externa | Adoptar librería X | ⚠️ Si tiene impacto arquitectónico |
|
|
190
|
+
| Bugfix complejo | Cambio en algoritmo de pricing | ❌ |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Estructura de directorios
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
docs/
|
|
198
|
+
adr/
|
|
199
|
+
0001-adopt-prisma-orm.md
|
|
200
|
+
0002-separate-schemas-per-feature.md
|
|
201
|
+
0003-event-bus-rabbitmq.md
|
|
202
|
+
README.md ← index de ADRs activos
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
El `README.md` se genera automáticamente listando los ADRs activos:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# scripts/forge-adr.mjs (futuro)
|
|
209
|
+
node scripts/forge-adr.mjs list # lista todos los ADRs
|
|
210
|
+
node scripts/forge-adr.mjs new # crea nuevo ADR desde template
|
|
211
|
+
node scripts/forge-adr.mjs status # cambia estado de un ADR
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Anti-patrones
|
|
217
|
+
|
|
218
|
+
| Anti-patrón | Problema | Solución |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| **ADR sin contexto** | "Usamos Prisma". Sin por qué ni alternativas. | Incluir motivación y alternativas consideradas siempre. |
|
|
221
|
+
| **ADR sin consecuencia** | "Adoptamos Kafka" sin decir el costo operativo. | Documentar consecuencias positivas, negativas y neutrales. |
|
|
222
|
+
| **ADRs que nadie lee** | Se escriben y se archivan. Nadie los consulta. | Integrar en `forge inscribe`. Mencionar en code review cuando aplica. |
|
|
223
|
+
| **ADR sobre tecnología obvia** | "Usamos TypeScript" como ADR. | No todo es ADR. Si no hay trade-off significativo, no es ADR. |
|
|
224
|
+
| **ADR sin fecha** | No se sabe cuándo se tomó ni si sigue vigente. | Fecha obligatoria. Estados para ciclo de vida. |
|
|
225
|
+
| **Demasiados ADRs** | Cada PR tiene un ADR. Se deja de leer. | Solo decisiones con impacto arquitectónico. No para implementación cotidiana. |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Conexión con Forge
|
|
230
|
+
|
|
231
|
+
| Comando | Acción |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `forge inscribe` | Incluye ADRs activos en ARCHITECTURE.md |
|
|
234
|
+
| `forge assay` | Usa ADRs como insumo para el ensayo multi-persona |
|
|
235
|
+
| `forge quench` | Verifica que ADRs aceptados están implementados |
|
|
236
|
+
| `forge inspect` | Reporta ADRs vencidos o sin implementar |
|
|
237
|
+
| `forge reforge` | Sugiere crear ADR cuando se detecta un cambio arquitectónico |
|
|
238
|
+
|
|
239
|
+
## Ver también
|
|
240
|
+
|
|
241
|
+
- `reference/evolutionary-architecture.md` — fitness functions que los ADRs registran
|
|
242
|
+
- `reference/principles.md` — principios que los ADRs documentan como decisiones
|
|
@@ -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
|