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