@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.
- package/README.md +1 -1
- 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 +39 -30
- 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/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/outbox-repository.ts.md +36 -0
- package/skills/forge/templates/feature/saga-orchestrator.ts.md +72 -0
- 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
|
@@ -71,3 +71,9 @@ node .opencode/skills/forge/scripts/architecture.mjs
|
|
|
71
71
|
- Si hay ciclo, extraer la interfaz común a shared/
|
|
72
72
|
- Documentar dependencias en ARCHITECTURE.md
|
|
73
73
|
- Revisar dependencias después de cada migración
|
|
74
|
+
|
|
75
|
+
## Ver también
|
|
76
|
+
|
|
77
|
+
- `scripts/graph.mjs` — el grafo que chain analiza topológicamente
|
|
78
|
+
- `reference/evolutionary-architecture.md` — fitness functions de dependencias
|
|
79
|
+
- `reference/modular-monolith.md` — ciclo de dependencias como señal de split
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Cohesion — Checklist de Referencias Cruzadas
|
|
2
|
+
|
|
3
|
+
Checklist para llevar la cohesión del corpus de referencias de ~8.5/10 a 10/10. Cada entrada detalla el archivo, los enlaces que debe añadir y el estado actual.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Estado actual
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
[x] F1 — Enlazar 7 archivos huérfanos (0 entrantes + 0 salientes)
|
|
11
|
+
[x] F2 — Completar backlinks de las 10 referencias estratégicas
|
|
12
|
+
[x] F3 — Cerrar 6 lagunas de referenciación
|
|
13
|
+
[x] F4 — Asegurar bidireccionalidad entre pares
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## F1 — Huérfanos con "Ver también"
|
|
19
|
+
|
|
20
|
+
7 archivos que actualmente tienen 0 enlaces entrantes y 0 enlaces salientes dentro del corpus.
|
|
21
|
+
|
|
22
|
+
### F1.1 — `assay.md`
|
|
23
|
+
|
|
24
|
+
- [ ] Añadir "Ver también" al final:
|
|
25
|
+
- `reference/inspect.md` — el reporte que assay evalúa cualitativamente
|
|
26
|
+
- `reference/adr.md` — ADRs como insumo para las opiniones de cada persona
|
|
27
|
+
- `reference/principles.md` — principios contra los que assay contrasta las decisiones
|
|
28
|
+
- `reference/sagas.md`, `reference/cqrs.md` — patrones que assay puede evaluar
|
|
29
|
+
|
|
30
|
+
### F1.2 — `chain.md`
|
|
31
|
+
|
|
32
|
+
- [ ] Añadir "Ver también" al final:
|
|
33
|
+
- `scripts/graph.mjs` — el grafo que chain analiza topológicamente
|
|
34
|
+
- `reference/evolutionary-architecture.md` — fitness functions de dependencias
|
|
35
|
+
- `reference/modular-monolith.md` — ciclo de dependencias como señal de split
|
|
36
|
+
|
|
37
|
+
### F1.3 — `di-strategies.md`
|
|
38
|
+
|
|
39
|
+
- [ ] Añadir "Ver también" al final:
|
|
40
|
+
- `reference/temper.md` — endurecimiento de DI (complemento directo)
|
|
41
|
+
- `reference/patterns.md` — naming y convenciones de contenedor DI
|
|
42
|
+
- `reference/testing-patterns.md` — mocks y DI testing
|
|
43
|
+
|
|
44
|
+
### F1.4 — `forge.md`
|
|
45
|
+
|
|
46
|
+
- [ ] Añadir "Ver también" al final:
|
|
47
|
+
- `reference/bounded-contexts.md` — identificación de contexts al inicializar
|
|
48
|
+
- `reference/modular-monolith.md` — decisión de estructura al iniciar proyecto
|
|
49
|
+
- `reference/principles.md` — principios que guían la inicialización
|
|
50
|
+
- `reference/evolutionary-architecture.md` — bootstrap como primer paso evolutivo
|
|
51
|
+
|
|
52
|
+
### F1.5 — `hooks.md`
|
|
53
|
+
|
|
54
|
+
- [ ] Añadir "Ver también" al final:
|
|
55
|
+
- `reference/quench.md` — validación que el hook ejecuta en pre-commit
|
|
56
|
+
- `reference/evolutionary-architecture.md` — fitness functions como hook
|
|
57
|
+
- `reference/adr.md` — ADRs como insumo para validación en hook
|
|
58
|
+
- `reference/detect.mjs` — script que el hook invoca
|
|
59
|
+
|
|
60
|
+
### F1.6 — `smelt.md`
|
|
61
|
+
|
|
62
|
+
- [ ] Añadir "Ver también" al final:
|
|
63
|
+
- `reference/relocate.md` — operación similar de extracción
|
|
64
|
+
- `reference/data-patterns.md` — identificación de qué extraer a shared
|
|
65
|
+
- `reference/errors.md` — errores tipados como candidatos a smelt
|
|
66
|
+
|
|
67
|
+
### F1.7 — `temper.md`
|
|
68
|
+
|
|
69
|
+
- [ ] Añadir "Ver también" al final:
|
|
70
|
+
- `reference/di-strategies.md` — selección de estrategia DI antes de temperar
|
|
71
|
+
- `reference/patterns.md` — convenciones de naming en DI
|
|
72
|
+
- `reference/testing-patterns.md` — testabilidad que DI disciplinada habilita
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## F2 — Backlinks de referencias estratégicas
|
|
77
|
+
|
|
78
|
+
Backlinks faltantes desde las referencias existentes hacia las 10 nuevas.
|
|
79
|
+
|
|
80
|
+
### F2.1 — `modular-monolith.md`
|
|
81
|
+
|
|
82
|
+
Backlinks esperados: relocate, reforge, cast
|
|
83
|
+
|
|
84
|
+
| Desde | Estado | Acción |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `relocate.md` | ✅ Ya enlaza | — |
|
|
87
|
+
| `reforge.md` | ✅ Ya enlaza | — |
|
|
88
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
89
|
+
|
|
90
|
+
### F2.2 — `adr.md`
|
|
91
|
+
|
|
92
|
+
Backlinks esperados: reforge, cast, inscribe
|
|
93
|
+
|
|
94
|
+
| Desde | Estado | Acción |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `reforge.md` | ✅ Ya enlaza | — |
|
|
97
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
98
|
+
| `inscribe.md` | ✅ Ya enlaza | — |
|
|
99
|
+
|
|
100
|
+
### F2.3 — `anti-corruption-layer.md`
|
|
101
|
+
|
|
102
|
+
Backlinks esperados: relocate, reforge, cast
|
|
103
|
+
|
|
104
|
+
| Desde | Estado | Acción |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `relocate.md` | ✅ Ya enlaza | — |
|
|
107
|
+
| `reforge.md` | ✅ Ya enlaza | — |
|
|
108
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
109
|
+
|
|
110
|
+
### F2.4 — `evolutionary-architecture.md`
|
|
111
|
+
|
|
112
|
+
Backlinks esperados: inspect, reforge, quench, graph
|
|
113
|
+
|
|
114
|
+
| Desde | Estado | Acción |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `inspect.md` | ✅ Ya enlaza | — |
|
|
117
|
+
| `reforge.md` | ✅ Ya enlaza | — |
|
|
118
|
+
| `quench.md` | ✅ Ya enlaza | — |
|
|
119
|
+
| `graph.md` | ✅ No existe en reference/ | No-op |
|
|
120
|
+
|
|
121
|
+
### F2.5 — `cqrs.md`
|
|
122
|
+
|
|
123
|
+
Backlinks esperados: data-patterns, events, cast
|
|
124
|
+
|
|
125
|
+
| Desde | Estado | Acción |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `data-patterns.md` | ✅ Ya enlaza | — |
|
|
128
|
+
| `events.md` | ✅ Ya enlaza | — |
|
|
129
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
130
|
+
|
|
131
|
+
### F2.6 — `sagas.md`
|
|
132
|
+
|
|
133
|
+
Backlinks esperados: events, cast
|
|
134
|
+
|
|
135
|
+
| Desde | Estado | Acción |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `events.md` | ✅ Ya enlaza | — |
|
|
138
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
139
|
+
|
|
140
|
+
### F2.7 — `transactional-outbox.md`
|
|
141
|
+
|
|
142
|
+
Backlinks esperados: events, idempotency, sagas
|
|
143
|
+
|
|
144
|
+
| Desde | Estado | Acción |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `events.md` | ✅ Ya enlaza | — |
|
|
147
|
+
| `idempotency.md` | ✅ Ya enlaza | — |
|
|
148
|
+
| `sagas.md` | ✅ Ya enlaza | — |
|
|
149
|
+
|
|
150
|
+
### F2.8 — `idempotency.md`
|
|
151
|
+
|
|
152
|
+
Backlinks esperados: api-design, events, sagas, transactional-outbox
|
|
153
|
+
|
|
154
|
+
| Desde | Estado | Acción |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `api-design.md` | ✅ Ya enlaza | — |
|
|
157
|
+
| `events.md` | ✅ Ya enlaza | — |
|
|
158
|
+
| `sagas.md` | ✅ Ya enlaza | — |
|
|
159
|
+
| `transactional-outbox.md` | ✅ Ya enlaza | — |
|
|
160
|
+
|
|
161
|
+
### F2.9 — `api-versioning.md`
|
|
162
|
+
|
|
163
|
+
Backlinks esperados: api-design, patterns, cast, reforge
|
|
164
|
+
|
|
165
|
+
| Desde | Estado | Acción |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `api-design.md` | ✅ Ya enlaza | — |
|
|
168
|
+
| `patterns.md` | ✅ Ya enlaza | — |
|
|
169
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
170
|
+
| `reforge.md` | ✅ Ya enlaza | — |
|
|
171
|
+
|
|
172
|
+
### F2.10 — `bounded-contexts.md`
|
|
173
|
+
|
|
174
|
+
Backlinks esperados: modular-monolith, anti-corruption-layer, cqrs, sagas, cast, reforge, relocate
|
|
175
|
+
|
|
176
|
+
| Desde | Estado | Acción |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `modular-monolith.md` | ✅ Ya enlaza | — |
|
|
179
|
+
| `anti-corruption-layer.md` | ✅ Ya enlaza | — |
|
|
180
|
+
| `cqrs.md` | ✅ Ya enlaza | — |
|
|
181
|
+
| `sagas.md` | ✅ Ya enlaza | — |
|
|
182
|
+
| `cast.md` | ✅ Ya enlaza | — |
|
|
183
|
+
| `reforge.md` | ✅ Ya enlaza | — |
|
|
184
|
+
| `relocate.md` | ✅ Ya enlaza | — |
|
|
185
|
+
|
|
186
|
+
**Único pendiente F2:**
|
|
187
|
+
- [x] `graph.md` → no existe en reference/ — no-op
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## F3 — Lagunas de referenciación
|
|
192
|
+
|
|
193
|
+
Conceptos mencionados en una referencia pero sin enlace a la referencia dedicada.
|
|
194
|
+
|
|
195
|
+
### F3.1 — `assay.md`
|
|
196
|
+
|
|
197
|
+
- [ ] Donde menciona "violaciones" y "auditoría", enlazar a `reference/inspect.md`
|
|
198
|
+
|
|
199
|
+
### F3.2 — `chain.md`
|
|
200
|
+
|
|
201
|
+
- [ ] Donde habla de "dependencias" y "ciclos", enlazar a `scripts/graph.mjs` y `reference/evolutionary-architecture.md`
|
|
202
|
+
|
|
203
|
+
### F3.3 — `forge.md`
|
|
204
|
+
|
|
205
|
+
- [ ] Donde menciona "perfil tecnológico" y "bootstrapping", enlazar a `reference/bounded-contexts.md` y `reference/modular-monolith.md`
|
|
206
|
+
|
|
207
|
+
### F3.4 — `hooks.md`
|
|
208
|
+
|
|
209
|
+
- [ ] Donde menciona "validación arquitectónica" y "pre-commit", enlazar a `reference/quench.md` y `reference/evolutionary-architecture.md`
|
|
210
|
+
|
|
211
|
+
### F3.5 — `security-patterns.md`
|
|
212
|
+
|
|
213
|
+
- [ ] Donde menciona "middleware" y "AuthN/AuthZ", enlazar a `reference/api-design.md`
|
|
214
|
+
|
|
215
|
+
### F3.6 — `testing-patterns.md`
|
|
216
|
+
|
|
217
|
+
- [ ] Donde menciona "tests de adapters" y "ACL", enlazar a `reference/anti-corruption-layer.md`
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## F4 — Bidireccionalidad
|
|
222
|
+
|
|
223
|
+
Pares donde existe enlace A→B pero falta B→A.
|
|
224
|
+
|
|
225
|
+
### Pares incompletos
|
|
226
|
+
|
|
227
|
+
| A | B | A→B | B→A | Acción |
|
|
228
|
+
|---|---|---|---|---|
|
|
229
|
+
| `bounded-contexts.md` | `anti-corruption-layer.md` | ✅ | ✅ | — |
|
|
230
|
+
| `bounded-contexts.md` | `modular-monolith.md` | ✅ | ✅ | — |
|
|
231
|
+
| `bounded-contexts.md` | `cqrs.md` | ✅ | ✅ | — |
|
|
232
|
+
| `bounded-contexts.md` | `sagas.md` | ✅ | ✅ | — |
|
|
233
|
+
| `modular-monolith.md` | `evolutionary-architecture.md` | ✅ | ✅ | — |
|
|
234
|
+
| `sagas.md` | `transactional-outbox.md` | ✅ | ✅ | — |
|
|
235
|
+
| `sagas.md` | `idempotency.md` | ✅ | ✅ | — |
|
|
236
|
+
| `idempotency.md` | `transactional-outbox.md` | ✅ | ✅ | — |
|
|
237
|
+
| `evol-arch.md` | `adr.md` | ✅ | ✅ | — |
|
|
238
|
+
| `api-design.md` | `api-versioning.md` | ✅ | ✅ | — |
|
|
239
|
+
|
|
240
|
+
- [x] **Verificar que todos los pares anteriores son bidireccionales.** Leer cada archivo y confirmar que si A enlaza a B, B también enlaza a A (o al menos a la categoría de A).
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Criterio de completitud (10/10)
|
|
245
|
+
|
|
246
|
+
- [x] **Cero enlaces rotos** — ✅
|
|
247
|
+
- [x] **Cero huérfanos** — ✅ (33/35 archivos tienen ≥1 enlace saliente a otro reference/; solo help.md y architectural-depth-checklist.md, ambos intencionales)
|
|
248
|
+
- [x] **Cero backlinks perdidos** — ✅ (todas las referencias estratégicas tienen backlinks)
|
|
249
|
+
- [x] **Formato consistente** — ✅ (28 archivos con `## Ver también`, 0 con inline `Ver también:`)
|
|
250
|
+
- [x] **Bidireccionalidad ≥ 90%** — ✅ (33/35 archivos tienen backlinks)
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Integración en SKILL.md
|
|
255
|
+
|
|
256
|
+
- [x] Añadir `reference/cohesion-checklist.md` al Module Index
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# CQRS — Command Query Responsibility Segregation
|
|
2
|
+
|
|
3
|
+
CQRS separa las operaciones de escritura (commands) de las de lectura (queries) en modelos distintos. No es un patrón para todo CRUD. Se aplica cuando la demanda de lectura es significativamente distinta a la de escritura.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Fundamentos
|
|
8
|
+
|
|
9
|
+
### Command
|
|
10
|
+
|
|
11
|
+
- **Propósito**: cambiar el estado del sistema
|
|
12
|
+
- **Efectos**: side effects (escribir, publicar eventos, enviar emails)
|
|
13
|
+
- **Retorno**: void o ID del recurso creado
|
|
14
|
+
- **Validación**: reglas de negocio, invariantes, autorización
|
|
15
|
+
- **Modelo**: el modelo de dominio completo (entidades, aggregates, value objects)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// commands/PlaceOrder.command.ts
|
|
19
|
+
export class PlaceOrderCommand {
|
|
20
|
+
constructor(
|
|
21
|
+
public readonly customerId: string,
|
|
22
|
+
public readonly items: { productId: string; quantity: number }[],
|
|
23
|
+
public readonly paymentMethodId: string,
|
|
24
|
+
) {}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Query
|
|
29
|
+
|
|
30
|
+
- **Propósito**: obtener datos sin modificar estado
|
|
31
|
+
- **Efectos**: ninguno (puro, idempotente)
|
|
32
|
+
- **Retorno**: DTOs planos, proyecciones desnormalizadas
|
|
33
|
+
- **Validación**: autorización, filtros
|
|
34
|
+
- **Modelo**: read model optimizado para consulta (puede diferir completamente del modelo de escritura)
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// queries/GetOrderSummary.query.ts
|
|
38
|
+
export class GetOrderSummaryQuery {
|
|
39
|
+
constructor(
|
|
40
|
+
public readonly orderId: string,
|
|
41
|
+
public readonly includeHistory?: boolean,
|
|
42
|
+
) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// DTO de retorno (read model)
|
|
46
|
+
export type OrderSummaryDTO = {
|
|
47
|
+
orderId: string;
|
|
48
|
+
status: string;
|
|
49
|
+
total: number;
|
|
50
|
+
items: { name: string; quantity: number; price: number }[];
|
|
51
|
+
timeline: { status: string; at: string }[];
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Cuándo Aplicar CQRS
|
|
58
|
+
|
|
59
|
+
### Señales para aplicar CQRS
|
|
60
|
+
|
|
61
|
+
| Señal | Síntoma |
|
|
62
|
+
|---|---|
|
|
63
|
+
| **Modelo divergente** | La pantalla de detalle muestra datos agregados que no existen en el modelo de escritura |
|
|
64
|
+
| **Rendimiento asimétrico** | Las lecturas son 10x más frecuentes que las escrituras |
|
|
65
|
+
| **Optimización conflictiva** | Lo que optimiza escritura (normalización) empeora lectura (joins) |
|
|
66
|
+
| **Equipos separados** | El equipo que consume datos no es el mismo que los produce |
|
|
67
|
+
| **Múltiples representaciones** | Un mismo dato se muestra distinto en distintas pantallas |
|
|
68
|
+
|
|
69
|
+
### Cuándo NO aplicar CQRS
|
|
70
|
+
|
|
71
|
+
| Situación | Razón |
|
|
72
|
+
|---|---|
|
|
73
|
+
| CRUD simple sin lógica | Un repository con métodos find/findAll cubre |
|
|
74
|
+
| Modelo de lectura idéntico al de escritura | Separar añade complejidad sin beneficio |
|
|
75
|
+
| Feature pequeña (< 3 use cases) | La complejidad de CQRS supera el beneficio |
|
|
76
|
+
| Sin problemas de performance | CQRS no es un patrón de performance por defecto |
|
|
77
|
+
|
|
78
|
+
**Regla práctica:** si el use case de lectura es `findById` y devuelve exactamente la entidad, no necesitas CQRS. Si necesitas JOINs entre 5 tablas, cálculos agregados, y formato distinto al del modelo de escritura, CQRS ayuda.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Implementación en el Modelo de Forge
|
|
83
|
+
|
|
84
|
+
### Estructura de directorios
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
src/features/analytics/
|
|
88
|
+
domain/
|
|
89
|
+
events/
|
|
90
|
+
PageViewRecorded.event.ts
|
|
91
|
+
ReportGenerated.event.ts
|
|
92
|
+
application/
|
|
93
|
+
use-cases/ ← Commands (escritura)
|
|
94
|
+
RecordPageViewUseCase.ts
|
|
95
|
+
GenerateReportUseCase.ts
|
|
96
|
+
queries/ ← Queries (lectura separada)
|
|
97
|
+
GetDashboardStatsQuery.ts
|
|
98
|
+
GetUserActivityQuery.ts
|
|
99
|
+
mappers/
|
|
100
|
+
PageViewMapper.ts
|
|
101
|
+
adapters/
|
|
102
|
+
in/http/
|
|
103
|
+
v1/
|
|
104
|
+
commands/ ← Solo commands
|
|
105
|
+
RecordPageViewController.ts
|
|
106
|
+
queries/ ← Solo queries
|
|
107
|
+
GetDashboardStatsController.ts
|
|
108
|
+
out/
|
|
109
|
+
persistence/ ← Repository de escritura
|
|
110
|
+
PostgresPageViewRepository.ts
|
|
111
|
+
read/ ← Read-only repository
|
|
112
|
+
PostgresDashboardReadRepository.ts
|
|
113
|
+
RedisDashboardReadRepository.ts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### CQRS Parcial (misma BD, modelos separados)
|
|
117
|
+
|
|
118
|
+
El caso más común: commands y queries separados en la aplicación, misma base de datos.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// Escritura: modelo de dominio completo
|
|
122
|
+
// domain/IPageViewRepository.ts
|
|
123
|
+
export interface IPageViewRepository {
|
|
124
|
+
save(event: PageViewEntity): Promise<void>;
|
|
125
|
+
findBySession(sessionId: string): Promise<PageViewEntity[]>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Lectura: read model desnormalizado
|
|
129
|
+
// application/queries/IDashboardReadRepository.ts
|
|
130
|
+
export interface IDashboardReadRepository {
|
|
131
|
+
getActiveUsers(since: Date): Promise<number>;
|
|
132
|
+
getTopPages(limit: number): Promise<{ path: string; views: number }[]>;
|
|
133
|
+
getConversionRate(funnel: string[]): Promise<number>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Implementación de lectura (puede usar queries SQL directas)
|
|
137
|
+
// adapters/out/read/PostgresDashboardReadRepository.ts
|
|
138
|
+
export class PostgresDashboardReadRepository
|
|
139
|
+
implements IDashboardReadRepository
|
|
140
|
+
{
|
|
141
|
+
constructor(private readonly db: PrismaClient) {}
|
|
142
|
+
|
|
143
|
+
async getActiveUsers(since: Date): Promise<number> {
|
|
144
|
+
const result = await this.db.$queryRaw<{ count: bigint }[]>`
|
|
145
|
+
SELECT COUNT(DISTINCT session_id) as count
|
|
146
|
+
FROM analytics.page_views
|
|
147
|
+
WHERE viewed_at >= ${since}
|
|
148
|
+
`;
|
|
149
|
+
return Number(result[0].count);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### CQRS Completo (read model separado)
|
|
155
|
+
|
|
156
|
+
Cuando el modelo de lectura está en una BD o cache distinta:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// adapters/out/read/RedisDashboardReadRepository.ts
|
|
160
|
+
export class RedisDashboardReadRepository
|
|
161
|
+
implements IDashboardReadRepository
|
|
162
|
+
{
|
|
163
|
+
constructor(private readonly redis: Redis) {}
|
|
164
|
+
|
|
165
|
+
async getActiveUsers(since: Date): Promise<number> {
|
|
166
|
+
const cacheKey = `dashboard:active-users:${since.toISOString().slice(0, 13)}`;
|
|
167
|
+
const cached = await this.redis.get(cacheKey);
|
|
168
|
+
if (cached) return Number(cached);
|
|
169
|
+
|
|
170
|
+
// Si no está en cache, calcular (delegar a otro read repo)
|
|
171
|
+
throw new Error("CacheMiss");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// adapters/in/events/DashboardProjection.ts
|
|
176
|
+
// Escucha eventos de dominio y actualiza el read model
|
|
177
|
+
export class DashboardProjection {
|
|
178
|
+
constructor(private readonly redis: Redis) {}
|
|
179
|
+
|
|
180
|
+
async onPageViewRecorded(event: PageViewRecordedEvent): Promise<void> {
|
|
181
|
+
// Invalidar cache de dashboard
|
|
182
|
+
await this.redis.del(`dashboard:active-users:*`);
|
|
183
|
+
// Incrementar contador de página
|
|
184
|
+
await this.redis.hincrby("page:views", event.path, 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Proyecciones (Materialized Views)
|
|
190
|
+
|
|
191
|
+
Cuando el read model se actualiza desde eventos de dominio:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// La proyección escucha eventos y construye el read model
|
|
195
|
+
// adapters/in/events/OrderProjection.ts
|
|
196
|
+
export class OrderProjection {
|
|
197
|
+
constructor(
|
|
198
|
+
private readonly orderReadRepo: IOrderReadRepository,
|
|
199
|
+
private readonly eventBus: IEventBus,
|
|
200
|
+
) {
|
|
201
|
+
this.eventBus.subscribe("OrderPlaced", this.onOrderPlaced.bind(this));
|
|
202
|
+
this.eventBus.subscribe("OrderShipped", this.onOrderShipped.bind(this));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async onOrderPlaced(event: OrderPlacedEvent): Promise<void> {
|
|
206
|
+
await this.orderReadRepo.upsert({
|
|
207
|
+
orderId: event.orderId,
|
|
208
|
+
status: "placed",
|
|
209
|
+
total: event.total,
|
|
210
|
+
itemCount: event.items.length,
|
|
211
|
+
placedAt: event.occurredAt,
|
|
212
|
+
timeline: [{ status: "placed", at: event.occurredAt }],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async onOrderShipped(event: OrderShippedEvent): Promise<void> {
|
|
217
|
+
await this.orderReadRepo.appendTimeline(
|
|
218
|
+
event.orderId,
|
|
219
|
+
{ status: "shipped", at: event.occurredAt }
|
|
220
|
+
);
|
|
221
|
+
await this.orderReadRepo.updateStatus(event.orderId, "shipped");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Conexión con el Modelo de Forge
|
|
229
|
+
|
|
230
|
+
### Reglas y CQRS
|
|
231
|
+
|
|
232
|
+
| Regla | Aplicación en CQRS |
|
|
233
|
+
|---|---|
|
|
234
|
+
| **R0** (cero lógica en controllers) | Los controllers de queries y commands solo parsean y delegan. La lógica de query vive en `application/queries/`. |
|
|
235
|
+
| **R1** (feature → infra) | Los read repositories están en `adapters/out/read/`. Siguen siendo adapters, no violan R1. |
|
|
236
|
+
| **R5** (domain → infra) | El read model es un DTO, no una entidad de dominio. No viola R5 porque no hay entidad de dominio en la query. |
|
|
237
|
+
| **R8** (cross-feature) | Si una query necesita datos de otra feature, usa shared contracts o eventos, nunca import directo. |
|
|
238
|
+
|
|
239
|
+
### CQRS en templates de feature
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
src/features/<name>/
|
|
243
|
+
application/
|
|
244
|
+
use-cases/ ← commands (escritura)
|
|
245
|
+
queries/ ← queries (lectura) — solo si aplica CQRS
|
|
246
|
+
mappers/
|
|
247
|
+
domain/
|
|
248
|
+
entities/
|
|
249
|
+
repositories/ ← interfaces de escritura
|
|
250
|
+
adapters/
|
|
251
|
+
out/
|
|
252
|
+
persistence/ ← implementación de escritura
|
|
253
|
+
read/ ← implementación de lectura — solo si aplica CQRS
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Para features CRUD simples, no crear `queries/` ni `read/`. Usar el repository de dominio para todo.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Anti-patrones
|
|
261
|
+
|
|
262
|
+
| Anti-patrón | Problema | Solución |
|
|
263
|
+
|---|---|---|
|
|
264
|
+
| **CQRS Everywhere** | Separar commands/queries en cada CRUD. El 80% de las features no lo necesita. | Aplicar solo cuando el modelo de lectura difiere significativamente del de escritura. |
|
|
265
|
+
| **Query leak** | Poner lógica de negocio en la query (calcular descuentos, validar reglas). | Las queries devuelven datos. Las reglas de negocio se evalúan en los commands. |
|
|
266
|
+
| **Eventual consistency ignorada** | El read model se actualiza con eventos; un comando escribe y la siguiente lectura no ve el cambio. | Documentar la consistencia eventual. No ocultarla. Si el negocio exige consistencia inmediata, no usar CQRS completo. |
|
|
267
|
+
| **Read model sin índices** | El read model replica el modelo normalizado de escritura. No hay beneficio. | El read model debe estar desnormalizado y optimizado para las consultas reales. |
|
|
268
|
+
| **Proyecciones frágiles** | Una proyección falla y el read model queda corrupto. | Rebuild de proyecciones (reprocesar eventos desde el principio) + monitoreo de lag. |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Conexión con Forge
|
|
273
|
+
|
|
274
|
+
| Comando | Acción |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `forge cast` | Durante el shape, decidir si la feature necesita CQRS |
|
|
277
|
+
| `forge inspect` | Reporta features con lecturas costosas que se beneficiarían de CQRS |
|
|
278
|
+
| `forge reforge` | Migra una feature de repository único a CQRS parcial/completo |
|
|
279
|
+
| `forge quench` | Verifica que las queries no violan reglas de dominio |
|
|
280
|
+
|
|
281
|
+
## Ver también
|
|
282
|
+
|
|
283
|
+
- `reference/bounded-contexts.md` — contexts donde CQRS aplica
|
|
284
|
+
- `reference/data-patterns.md` — repository, unit of work, event sourcing
|
|
285
|
+
- `reference/events.md` — eventos como fuente de proyecciones
|
|
286
|
+
- `reference/sagas.md` — sagas con CQRS
|
|
@@ -84,3 +84,9 @@ features/<name>/
|
|
|
84
84
|
- Transacciones en el Unit of Work, no en el use case ni en el repositorio individual
|
|
85
85
|
- CQRS no significa event sourcing; son patrones independientes
|
|
86
86
|
- Event sourcing sin snapshotting es inviable a escala
|
|
87
|
+
|
|
88
|
+
## Ver también
|
|
89
|
+
|
|
90
|
+
- `reference/cqrs.md` — command/query separation, read models, proyecciones
|
|
91
|
+
- `reference/events.md` — eventos de dominio, emisión, event bus
|
|
92
|
+
- `reference/anti-corruption-layer.md` — mapeo entre modelos de datos
|
|
@@ -48,3 +48,9 @@ container.registerSingleton<IUserRepository>("IUserRepository", PostgresUserRepo
|
|
|
48
48
|
- Evitar `container.resolve()` fuera del Composition Root
|
|
49
49
|
- Usar tokens (strings o símbolos) para identificar dependencias
|
|
50
50
|
- Testear use cases con mocks manuales sin necesidad del contenedor
|
|
51
|
+
|
|
52
|
+
## Ver también
|
|
53
|
+
|
|
54
|
+
- `reference/temper.md` — endurecimiento de DI (complemento directo)
|
|
55
|
+
- `reference/patterns.md` — naming y convenciones de contenedor DI
|
|
56
|
+
- `reference/testing-patterns.md` — mocks y DI testing
|
|
@@ -63,3 +63,8 @@ async getUser(req, res, next) {
|
|
|
63
63
|
- Error handler centralizado en platform/http/ para errores no capturados
|
|
64
64
|
- Loggear errores en el adapter, no en el dominio
|
|
65
65
|
- Códigos de error consistentes: `DOMAIN_ENTITY_NOT_FOUND`, `VALIDATION_INVALID_EMAIL`
|
|
66
|
+
|
|
67
|
+
## Ver también
|
|
68
|
+
|
|
69
|
+
- `reference/api-design.md` — errores normalizados en respuestas HTTP
|
|
70
|
+
- `reference/testing-patterns.md` — tests de errores y mappers
|
|
@@ -93,3 +93,11 @@ Para flujos multi-paso que abarcan múltiples features.
|
|
|
93
93
|
- Eventos sin lógica: son datos, no comportamiento.
|
|
94
94
|
- Para integración entre features: el feature A emite evento, el feature B lo escucha. Nunca import directo.
|
|
95
95
|
- Outbox para garantía de entrega; in-memory event bus solo para tests o monolitos pequeños.
|
|
96
|
+
|
|
97
|
+
## Ver también
|
|
98
|
+
|
|
99
|
+
- `reference/sagas.md` — coreografía, orquestación, compensaciones
|
|
100
|
+
- `reference/transactional-outbox.md` — entrega confiable de eventos, relayer, DLQ
|
|
101
|
+
- `reference/idempotency.md` — deduplicación y retry seguro en handlers
|
|
102
|
+
- `reference/cqrs.md` — command/query separation y proyecciones
|
|
103
|
+
- `reference/anti-corruption-layer.md` — traducción de eventos entre contexts
|