eva4j 1.0.16 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +218 -5
  2. package/DOMAIN_YAML_GUIDE.md +185 -2
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/docs/commands/EVALUATE_SYSTEM.md +18 -2
  5. package/examples/domain-events.yaml +26 -0
  6. package/examples/domain-read-models.yaml +113 -0
  7. package/package.json +1 -1
  8. package/read-model-spec.md +664 -0
  9. package/src/agents/design-reviewer.agent.md +3 -0
  10. package/src/commands/generate-entities.js +254 -10
  11. package/src/commands/generate-http-exchange.js +3 -0
  12. package/src/commands/generate-kafka-event.js +3 -0
  13. package/src/commands/generate-kafka-listener.js +3 -0
  14. package/src/commands/generate-record.js +2 -2
  15. package/src/commands/generate-resource.js +4 -1
  16. package/src/commands/generate-temporal-activity.js +4 -1
  17. package/src/commands/generate-temporal-flow.js +4 -1
  18. package/src/commands/generate-usecase.js +4 -1
  19. package/src/skills/build-system-yaml/SKILL.md +122 -1
  20. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +205 -24
  21. package/src/skills/build-system-yaml/references/module-spec.md +33 -1
  22. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  23. package/src/utils/config-manager.js +4 -2
  24. package/src/utils/domain-validator.js +230 -14
  25. package/src/utils/naming.js +10 -0
  26. package/src/utils/validator.js +3 -1
  27. package/src/utils/yaml-to-entity.js +272 -3
  28. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  29. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  30. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  31. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
  32. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  33. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  34. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  35. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  36. package/templates/read-model/ReadModelJpaRepository.java.ejs +11 -0
  37. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  38. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  39. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +81 -0
  40. package/templates/read-model/ReadModelSyncHandler.java.ejs +52 -0
  41. package/test-c2010.js +49 -0
  42. package/test-update-compat.js +109 -0
  43. package/test-update-lifecycle.js +121 -0
@@ -62,11 +62,32 @@ Si el usuario no proveyó todos los datos, **pregunta** antes de generar:
62
62
  3. **Endpoints REST** por módulo (método + path + caso de uso)
63
63
  4. **Flujos async**: evento → productor → consumidores + `useCase` de cada consumidor
64
64
  5. **Llamadas sync**: caller → destino → endpoints usados
65
+ 6. **Dependencias de datos cross-module**: ¿algún módulo necesita datos de otro para validar o enriquecer? → candidato a **Read Model** (proyección local mantenida por eventos) en vez de llamada sync
65
66
 
66
67
  > Si `system/system.yaml` ya existe, léelo y pregunta solo por cambios.
67
68
 
68
69
  Aplica el rol funcional: sugiere módulos necesarios no mencionados, propone flujos async coherentes, anticipa invariantes. Confirma antes de agregar elementos no solicitados.
69
70
 
71
+ ### Read Models — decisión de diseño
72
+
73
+ Cuando un módulo necesita datos de otro módulo, evalúa antes de decidir entre `ports:` (sync HTTP) y `readModels:` (async, proyección local):
74
+
75
+ | Pregunta | Si la respuesta es SÍ → |
76
+ |---|---|
77
+ | ¿El dato se consulta en cada request o en operaciones frecuentes? | `readModels:` |
78
+ | ¿Se tolera consistencia eventual (ms de delay)? | `readModels:` |
79
+ | ¿Se prepara el sistema para microservicios (`eva detach`)? | `readModels:` |
80
+ | ¿Se necesita consistencia fuerte (ej: saldo financiero)? | `ports:` |
81
+ | ¿Es una llamada infrecuente y simple (ej: lookup puntual)? | `ports:` |
82
+
83
+ **Patrón típico:** Si un módulo ya consume eventos de otro módulo (`listeners:`) y además llama sync para obtener datos del mismo módulo (`ports:`), es candidato fuerte a reemplazar el port por un readModel.
84
+
85
+ Si decides usar readModel:
86
+ 1. En `system.yaml`: declarar eventos con `consumers[].readModel:` en vez de `useCase:`
87
+ 2. En `{module}.yaml`: declarar `readModels:` con `source`, `tableName`, `fields`, `syncedBy`
88
+ 3. Eliminar la entrada `integrations.sync[]` que reemplaza
89
+ 4. Asegurar que el módulo fuente emita los eventos necesarios en sus `events:` — usar `lifecycle:` para eventos CRUD (`create`/`update`/`delete`/`softDelete`) en vez de `triggers:`
90
+
70
91
  ---
71
92
 
72
93
  ## Paso 2 — Estructura del system.yaml
@@ -108,6 +129,13 @@ integrations:
108
129
  consumers:
109
130
  - module: payments
110
131
  useCase: HandleOrderPlaced
132
+ # Read Model sync — usa readModel: en vez de useCase:
133
+ - event: ProductCreatedEvent
134
+ producer: products
135
+ topic: PRODUCT_CREATED
136
+ consumers:
137
+ - module: orders
138
+ readModel: ProductReadModel # proyección local de datos cross-module
111
139
  sync:
112
140
  - caller: orders
113
141
  calls: customers
@@ -134,6 +162,8 @@ integrations:
134
162
  - Sin port names genéricos compartidos entre módulos
135
163
  - `consumers[].useCase` siempre presente y en PascalCase
136
164
  - `calls.using:` solo referencia endpoints de `exposes:` del destino
165
+ - Cada consumer declara exactamente `useCase:` (lógica de negocio) o `readModel:` (proyección local), nunca ambos
166
+ - `readModel:` PascalCase + sufijo `ReadModel` — su módulo consumidor declara `readModels:` en domain.yaml
137
167
 
138
168
  ---
139
169
 
@@ -146,6 +176,8 @@ Antes de proponer el `system.yaml`, verifica:
146
176
  - [ ] Sin dependencias circulares síncronas
147
177
  - [ ] Todos los `consumers[].module` existen en `modules:`
148
178
  - [ ] Todos los `consumers[].useCase` presentes y en PascalCase
179
+ - [ ] `consumers[]` con `readModel:` en PascalCase + sufijo `ReadModel`
180
+ - [ ] Cada consumer tiene exactamente `useCase:` o `readModel:`, nunca ambos
149
181
  - [ ] Todos los `calls.using:` existen en `exposes:` del destino
150
182
  - [ ] Módulos pasivos no son `caller`
151
183
  - [ ] Todo en inglés
@@ -257,6 +289,7 @@ C4Container
257
289
  - `ContainerQueue()` para el broker — solo si `messaging.enabled: true`; usar el tipo de `messaging.broker`
258
290
  - `System_Ext()` fuera del boundary para servicios externos
259
291
  - **Flechas async siempre pasan por el broker**: `producer → broker` y `broker → consumer`, nunca directo
292
+ - **Read Model sync también pasa por el broker**: `Rel(source, broker, "ProductCreatedEvent", "Kafka")` + `Rel(broker, consumer, "Sync ProductReadModel", "Kafka")` — no usar flecha directa
260
293
  - **Flechas sync** directas entre containers: de caller a callee con label del port name
261
294
  - Los `Rel()` de eventos incluyen el nombre del evento en el campo `technology`/label
262
295
  - Derivar actores `Person()` de quienes consumen los `exposes[]`
@@ -302,7 +335,95 @@ Antes de guardar, verifica:
302
335
 
303
336
  Lee `references/domain-yaml-spec.md` para la especificación completa de estructura, reglas, restricciones y checklist del `system/{module}.yaml`.
304
337
 
305
- Para cada módulo en `modules:`, genera `system/{nombre-del-modulo}.yaml` con: aggregates, entities, valueObjects, enums (con transitions si aplica), events, endpoints, listeners y ports — todo inferido del `system.yaml`.
338
+ Para cada módulo en `modules:`, genera `system/{nombre-del-modulo}.yaml` con: aggregates, entities, valueObjects, enums (con transitions si aplica), events, endpoints, listeners, ports y **readModels** — todo inferido del `system.yaml`.
339
+
340
+ ### Inferencia de readModels desde system.yaml
341
+
342
+ Cuando `integrations.async[].consumers[]` tiene `readModel:` y el `module` es el módulo actual:
343
+ 1. Agrupar todos los eventos del mismo `readModel:` → una entrada `readModels:` con múltiples `syncedBy`
344
+ 2. `source.module` = el `producer` de esas integraciones async
345
+ 3. `source.aggregate` = derivar del nombre del readModel (ej: `ProductReadModel` → `Product`)
346
+ 4. `tableName` = `rm_` + snake_case del source module (ej: `rm_products`)
347
+ 5. `fields` = inferir del payload del evento fuente (incluir siempre `id`)
348
+ 6. `syncedBy[].action` = `UPSERT` para Created/Updated, `SOFT_DELETE` para Deactivated, `DELETE` para Deleted
349
+ 7. Si había una entrada `integrations.sync[]` al mismo módulo fuente → **no generar `ports:`** para esa llamada (el readModel la reemplaza)
350
+
351
+ ### Lifecycle events en módulos fuente
352
+
353
+ Cuando el módulo actual es `producer` en `integrations.async[]` y algún consumer tiene `readModel:`, los eventos de este módulo deben usar `lifecycle:` (operación CRUD) en vez de `triggers:` (transición de estado).
354
+
355
+ Para cada evento de este módulo consumido por un readModel:
356
+
357
+ 1. Agregar `lifecycle:` al evento — derivar el valor del nombre del evento:
358
+ | Patrón del nombre | `lifecycle:` |
359
+ |---|---|
360
+ | `*CreatedEvent`, `*RegisteredEvent` | `create` |
361
+ | `*UpdatedEvent` | `update` |
362
+ | `*DeletedEvent` | `delete` |
363
+ | `*DeactivatedEvent` | `softDelete` |
364
+
365
+ 2. **NO** agregar `triggers:` — estos eventos son CRUD, no transiciones de estado
366
+ 3. Si `lifecycle: softDelete` → la entidad raíz **debe** tener `hasSoftDelete: true`
367
+ 4. Si `lifecycle: delete` → la entidad raíz **NO debe** tener `hasSoftDelete: true`
368
+ 5. `fields:` del evento debe incluir **todos** los campos declarados en el `readModels[].fields` del módulo consumidor (el payload es la fuente de verdad de la proyección)
369
+ 6. Siempre incluir `{entityName}Id` como campo (se mapea a `aggregateId` del DomainEvent base)
370
+ 7. `fields:` del lifecycle event solo puede contener: (a) `{entityName}Id` (aggregateId), (b) campos que existen en la entidad raíz, (c) campos temporales `*At` + `LocalDateTime`. No incluir campos que no existan en la entidad — genera error `C2-010`
371
+ 8. Los campos de los readModels consumidores deben ser subconjunto de los campos de la entidad raíz del productor. Si el readModel necesita un campo, ese campo debe existir en la entidad fuente — de lo contrario los lifecycle events no podrán emitirlo (C2-010) y el campo siempre será null (C1-007)
372
+
373
+ **Ejemplo — módulo `products` como fuente de `ProductReadModel`:**
374
+
375
+ ```yaml
376
+ aggregates:
377
+ - name: Product
378
+ entities:
379
+ - name: product
380
+ isRoot: true
381
+ tableName: products
382
+ hasSoftDelete: true
383
+ audit:
384
+ enabled: true
385
+ fields:
386
+ - name: id
387
+ type: String
388
+ - name: name
389
+ type: String
390
+ - name: price
391
+ type: BigDecimal
392
+ - name: status
393
+ type: String
394
+ readOnly: true
395
+ defaultValue: "ACTIVE"
396
+ events:
397
+ - name: ProductCreatedEvent
398
+ lifecycle: create
399
+ fields:
400
+ - name: productId
401
+ type: String
402
+ - name: name
403
+ type: String
404
+ - name: price
405
+ type: BigDecimal
406
+ - name: status
407
+ type: String
408
+ - name: ProductUpdatedEvent
409
+ lifecycle: update
410
+ fields:
411
+ - name: productId
412
+ type: String
413
+ - name: name
414
+ type: String
415
+ - name: price
416
+ type: BigDecimal
417
+ - name: status
418
+ type: String
419
+ - name: ProductDeactivatedEvent
420
+ lifecycle: softDelete
421
+ fields:
422
+ - name: productId
423
+ type: String
424
+ - name: deactivatedAt
425
+ type: LocalDateTime
426
+ ```
306
427
 
307
428
  ---
308
429
 
@@ -32,6 +32,14 @@ Propón campos necesarios no mencionados, Value Objects expresivos, invariantes
32
32
  11. ❌ **Todo en inglés**
33
33
  12. ❌ **No transiciones sin evidencia de activación** — toda transición activada por `ports:` o scheduler necesita un domain event con `triggers`
34
34
  13. ❌ **No reutilizar `service:` en `ports[]` entre módulos** — nombres propios del bounded context
35
+ 14. ❌ **No `readModels[].source.module` igual al módulo actual** — readModels son cross-module exclusivamente
36
+ 15. ❌ **No auditoría, endpoints REST ni lógica de negocio en `readModels:`** — son proyecciones inmutables
37
+ 16. ❌ **No `readModels[].name` sin sufijo `ReadModel`** — siempre `PascalCase + ReadModel`
38
+ 17. ❌ **No `readModels[].tableName` sin prefijo `rm_`** — identificación visual obligatoria
39
+ 18. ❌ **No `lifecycle:` y `triggers:` en el mismo evento** — son mutuamente excluyentes
40
+ 19. ❌ **No `lifecycle: softDelete` sin `hasSoftDelete: true`** en la entidad raíz
41
+ 20. ❌ **No `lifecycle: delete` con `hasSoftDelete: true`** — delete es hard delete
42
+ 21. ❌ **No declarar campos en lifecycle events que no existan en la entidad raíz** — solo `{entityName}Id`, campos de la entidad y campos temporales auto-resolubles (`*At` + `LocalDateTime`). Genera error `C2-010`
35
43
 
36
44
  ---
37
45
 
@@ -41,7 +49,8 @@ Propón campos necesarios no mencionados, Value Objects expresivos, invariantes
41
49
  |---|---|
42
50
  | `modules[x].exposes[]` | `endpoints:` con `basePath` + `versions[].operations[]` |
43
51
  | `integrations.async[]` donde `producer = módulo` | `events:` |
44
- | `integrations.async[].consumers[]` donde `module = módulo` | `listeners:` (con `useCase`) |
52
+ | `integrations.async[].consumers[]` donde `module = módulo` y tiene `useCase` | `listeners:` (con `useCase`) |
53
+ | `integrations.async[].consumers[]` donde `module = módulo` y tiene `readModel` | `readModels:` (con `syncedBy`) |
45
54
  | `integrations.sync[]` donde `caller = módulo` | `ports:` (con `service`, `http`) |
46
55
 
47
56
  ### Formato correcto de endpoints
@@ -176,6 +185,35 @@ aggregates:
176
185
  - name: reason
177
186
  type: String
178
187
 
188
+ # ── Lifecycle events (CRUD-based, no transitions) ──
189
+ # Use lifecycle: instead of triggers: when the event is emitted
190
+ # at a CRUD operation (create/update/delete/softDelete).
191
+ # Typical use: source modules whose events feed readModels.
192
+ #
193
+ # - name: ProductCreatedEvent
194
+ # lifecycle: create # raise() in creation constructor
195
+ # fields:
196
+ # - name: productId
197
+ # type: String
198
+ # - name: name
199
+ # type: String
200
+ #
201
+ # - name: ProductUpdatedEvent
202
+ # lifecycle: update # raise() in UpdateCommandHandler
203
+ # fields: [...]
204
+ #
205
+ # - name: ProductDeletedEvent
206
+ # lifecycle: delete # raise() in DeleteCommandHandler
207
+ # fields:
208
+ # - name: productId
209
+ # type: String
210
+ #
211
+ # - name: ProductDeactivatedEvent
212
+ # lifecycle: softDelete # raise() in softDelete() method
213
+ # fields:
214
+ # - name: productId
215
+ # type: String
216
+
179
217
  endpoints:
180
218
  basePath: /orders
181
219
  versions:
@@ -289,34 +327,164 @@ Anotaciones: `NotNull`, `NotBlank`, `NotEmpty`, `Email`, `Size`, `Min`, `Max`, `
289
327
 
290
328
  ---
291
329
 
292
- ## Proyecciones locales (read models)
330
+ ## Proyecciones locales (`readModels:`)
331
+
332
+ Cuando un módulo necesita datos de otro bounded context para validar precondiciones o enriquecer entidades, usar `readModels:` en vez de `ports:` (sync HTTP). Esto elimina dependencias síncronas y mejora autonomía, resiliencia y rendimiento.
293
333
 
294
- Agregados sincronizados por listeners Kafka. Señales:
295
- - Campos coinciden con `listeners[].fields`
296
- - useCase: `Register*InLocalCatalog`, `Sync*`, `Update*FromEvent`
297
- - Sin endpoints de escritura
298
- - Existe para evitar llamadas síncronas en tiempo real
334
+ ### Cuándo usar readModels vs ports
335
+
336
+ | Criterio | `readModels:` (async) | `ports:` (sync) |
337
+ |---|---|---|
338
+ | Consistencia eventual aceptable | | |
339
+ | Se necesita consistencia fuerte | — | ✅ |
340
+ | Datos se consultan frecuentemente | ✅ | — |
341
+ | Llamada infrecuente y simple | — | ✅ |
342
+ | Preparación para microservicios | ✅ | — |
299
343
 
300
- **Auditoría:** `audit.enabled: true`, `trackUser: false` (cambios vienen de eventos, no de usuarios).
344
+ ### Estructura
301
345
 
302
346
  ```yaml
303
- - name: BikeCatalog
304
- entities:
305
- - name: bikeCatalogEntry
306
- isRoot: true
307
- tableName: bike_catalog_entries
308
- audit:
309
- enabled: true
310
- trackUser: false
311
- fields:
312
- - name: id
313
- type: String
314
- - name: isAvailable
315
- type: Boolean
316
- readOnly: true
317
- defaultValue: true
347
+ # Nivel raíz, sibling de aggregates:, listeners:, ports:
348
+ readModels:
349
+ - name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
350
+ source: # Trazabilidad al módulo fuente (OBLIGATORIO)
351
+ module: products # Módulo fuente (kebab-case)
352
+ aggregate: Product # Agregado fuente (PascalCase)
353
+ tableName: rm_products # Tabla en BD (OBLIGATORIO, prefijo rm_)
354
+ fields: # Campos proyectados — subconjunto del fuente
355
+ - name: id
356
+ type: String
357
+ - name: name
358
+ type: String
359
+ - name: price
360
+ type: BigDecimal
361
+ - name: status
362
+ type: String
363
+ syncedBy: # Eventos que mantienen esta tabla (min 1)
364
+ - event: ProductCreatedEvent # Nombre del evento (PascalCase + sufijo Event)
365
+ action: UPSERT # Acción: UPSERT | DELETE | SOFT_DELETE
366
+ - event: ProductUpdatedEvent
367
+ action: UPSERT
368
+ - event: ProductDeactivatedEvent
369
+ action: SOFT_DELETE
318
370
  ```
319
371
 
372
+ ### Reglas
373
+
374
+ - **`name:`** — PascalCase, **DEBE** terminar con `ReadModel`
375
+ - **`tableName:`** — **DEBE** empezar con `rm_` (identificación visual en BD)
376
+ - **`fields:`** — **DEBE** incluir un campo `id`
377
+ - **`syncedBy:`** — **DEBE** tener al menos una entrada
378
+ - **`source.module:`** — **NO PUEDE** ser el mismo módulo actual (cross-module exclusivamente)
379
+ - **Sin auditoría** — Los readModels **nunca** tienen campos de auditoría
380
+ - **Sin endpoints REST** — Los readModels **nunca** exponen endpoints
381
+ - **Sin lógica de negocio** — La clase de dominio es inmutable (solo getters)
382
+
383
+ ### Acciones de sincronización
384
+
385
+ | Acción | Significado | Uso |
386
+ |---|---|---|
387
+ | `UPSERT` | Insertar si es nuevo, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
388
+ | `DELETE` | Eliminar el registro permanentemente | Hard deletes en el módulo fuente |
389
+ | `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
390
+
391
+ ### Inferencia desde system.yaml
392
+
393
+ | Fuente en system.yaml | Destino en domain.yaml |
394
+ |---|---|
395
+ | `consumers[].readModel: ProductReadModel` | `readModels:` entry con ese nombre |
396
+ | `integrations.async[].producer` | `readModels[].source.module` |
397
+ | `integrations.async[].topic` | Topic derivado automáticamente del nombre del evento |
398
+
399
+ ### Impacto en el módulo fuente
400
+
401
+ El módulo fuente **debe** emitir los eventos referenciados en `syncedBy`. Asegurar que los `events:` del fuente incluyan todos los campos declarados en `readModels[].fields` (el payload es la fuente de verdad para la proyección).
402
+
403
+ **Restricción de cobertura:** Los campos del readModel (excepto `id`) deben estar cubiertos por al menos un evento UPSERT en `syncedBy[]`. Si un campo no aparece en ningún evento UPSERT, siempre será null — genera warning `C1-007`. Además, los campos del readModel deben ser subconjunto de los campos de la entidad raíz del módulo fuente (por C2-010, los lifecycle events no pueden emitir campos ajenos a la entidad).
404
+
405
+ ### Propiedad `lifecycle:` en eventos del módulo fuente
406
+
407
+ Cuando un evento existe para alimentar un readModel (operación CRUD pura, sin transición de estado), usar `lifecycle:` en vez de `triggers:`.
408
+
409
+ | Valor | Punto de emisión | Descripción |
410
+ |---|---|---|
411
+ | `create` | Constructor de creación de la entidad | UUID auto-generado como id antes de raise() |
412
+ | `update` | UpdateCommandHandler, antes de `repository.save()` | raise() sobre la entidad reconstruida |
413
+ | `delete` | DeleteCommandHandler, antes de `repository.delete()` | Hard delete — requiere `hasSoftDelete` ausente o false |
414
+ | `softDelete` | Método `softDelete()` de la entidad | Requiere `hasSoftDelete: true` en la entidad raíz |
415
+
416
+ **Derivación desde el nombre del evento:**
417
+
418
+ | Patrón del nombre del evento | `lifecycle:` |
419
+ |---|---|
420
+ | `*CreatedEvent`, `*RegisteredEvent` | `create` |
421
+ | `*UpdatedEvent` | `update` |
422
+ | `*DeletedEvent` | `delete` |
423
+ | `*DeactivatedEvent` | `softDelete` |
424
+
425
+ **Ejemplo — módulo fuente `products`:**
426
+
427
+ ```yaml
428
+ aggregates:
429
+ - name: Product
430
+ entities:
431
+ - name: product
432
+ isRoot: true
433
+ tableName: products
434
+ hasSoftDelete: true # requerido por lifecycle: softDelete
435
+ audit:
436
+ enabled: true
437
+ fields:
438
+ - name: id
439
+ type: String
440
+ - name: name
441
+ type: String
442
+ - name: price
443
+ type: BigDecimal
444
+ - name: status
445
+ type: String
446
+ readOnly: true
447
+ defaultValue: "ACTIVE"
448
+ events:
449
+ - name: ProductCreatedEvent
450
+ lifecycle: create
451
+ fields:
452
+ - name: productId
453
+ type: String
454
+ - name: name
455
+ type: String
456
+ - name: price
457
+ type: BigDecimal
458
+ - name: status
459
+ type: String
460
+ - name: ProductUpdatedEvent
461
+ lifecycle: update
462
+ fields:
463
+ - name: productId
464
+ type: String
465
+ - name: name
466
+ type: String
467
+ - name: price
468
+ type: BigDecimal
469
+ - name: status
470
+ type: String
471
+ - name: ProductDeactivatedEvent
472
+ lifecycle: softDelete
473
+ fields:
474
+ - name: productId
475
+ type: String
476
+ - name: deactivatedAt
477
+ type: LocalDateTime
478
+ ```
479
+
480
+ **Reglas:**
481
+ - `lifecycle:` y `triggers:` son mutuamente excluyentes — un evento usa uno u otro, nunca ambos
482
+ - `lifecycle: softDelete` requiere `hasSoftDelete: true` en la entidad raíz
483
+ - `lifecycle: delete` requiere que `hasSoftDelete` sea `false` o esté ausente
484
+ - `fields:` del evento debe incluir **todos** los campos del readModel que lo consume (el payload es la fuente de verdad para la proyección)
485
+ - Siempre incluir `{entityName}Id` como campo (se mapea a `aggregateId` del DomainEvent base)
486
+ - `fields:` del lifecycle event solo puede contener: (a) `{entityName}Id`, (b) campos que existen en la entidad raíz del agregado, (c) campos temporales auto-resolubles (nombre termina en `At` + tipo `LocalDateTime`). Cualquier otro campo genera error `C2-010`
487
+
320
488
  ---
321
489
 
322
490
  ## Clasificación de campos readOnly
@@ -406,5 +574,18 @@ Si un valor no es trazable → falta un endpoint o listener en el diseño.
406
574
  - [ ] `nestedTypes[]` para campos de tipo objeto en listeners/ports
407
575
  - [ ] Transiciones por `ports:` tienen domain event con `triggers`
408
576
  - [ ] Cada enum `*Type` valor trazable a listener/endpoint/event
409
- - [ ] Proyecciones: `audit.enabled: true`, `trackUser: false`
577
+ - [ ] Proyecciones cross-module: usar `readModels:` con `source`, `tableName` (prefijo `rm_`), `fields` (incluye `id`), `syncedBy`
578
+ - [ ] `readModels[].name` termina con `ReadModel` (PascalCase)
579
+ - [ ] `readModels[].source.module` ≠ módulo actual
580
+ - [ ] `readModels[].syncedBy` → al menos una entrada con `action` válida (UPSERT, DELETE, SOFT_DELETE)
581
+ - [ ] Eventos en `syncedBy` deben existir como `events:` en el módulo fuente
582
+ - [ ] Eventos del módulo fuente consumidos por readModels → deben tener `lifecycle:` (no `triggers:`)
583
+ - [ ] `lifecycle:` derivado del nombre del evento: `*CreatedEvent`→`create`, `*UpdatedEvent`→`update`, `*DeletedEvent`→`delete`, `*DeactivatedEvent`→`softDelete`
584
+ - [ ] `lifecycle: softDelete` → entidad raíz tiene `hasSoftDelete: true`
585
+ - [ ] `lifecycle: delete` → entidad raíz NO tiene `hasSoftDelete: true`
586
+ - [ ] `lifecycle:` y `triggers:` nunca en el mismo evento
587
+ - [ ] Lifecycle event fields son campos de la entidad raíz (excluyendo `{entityName}Id` y `*At` temporal) — `C2-010`
588
+ - [ ] ReadModel fields cubiertos por eventos UPSERT del productor — `C1-007`
589
+ - [ ] ReadModel fields son subconjunto de los campos de la entidad raíz fuente
590
+ - [ ] Si `readModels:` reemplaza un `ports:`, eliminar la entrada sync correspondiente
410
591
  - [ ] Todo en inglés
@@ -61,6 +61,17 @@ Especificación técnica narrativa del sistema completo. Una sección `##` por c
61
61
  **When called:** [in which use case and under what condition]
62
62
  **Endpoints used:** [METHOD /path list]
63
63
  **Data obtained and how it's used:** [detailed description]
64
+
65
+ ### Read Models (local projections)
66
+ [Only if module has readModels: in its domain.yaml]
67
+
68
+ #### {ReadModelName} ← {source-module}
69
+ **Purpose:** [why this projection exists — what data it provides and why sync HTTP was replaced]
70
+ **Source aggregate:** [{Aggregate} in {source-module}]
71
+ **Table:** [{tableName}]
72
+ **Projected fields:** [field list with types]
73
+ **Synced by:** [event names and actions (UPSERT/DELETE/SOFT_DELETE)]
74
+ **Consistency model:** Eventual — acceptable delay: [specify, e.g. "milliseconds"]
64
75
  ```
65
76
 
66
77
  ### Reglas del system.md
@@ -245,6 +256,27 @@ flowchart TD
245
256
  **When called:** [in which use case and condition]
246
257
  **Endpoints used:** [METHOD /path list]
247
258
  **Data obtained and how it's used:** [detailed description]
259
+
260
+ ## Read Models (local projections)
261
+ [Only if module has readModels: in its domain.yaml]
262
+
263
+ ### {ReadModelName} ← {source-module}
264
+ **Purpose:** [why this projection exists — what data it provides and why sync HTTP was not used]
265
+ **Source aggregate:** [{Aggregate} in {source-module}]
266
+ **Table:** [{tableName}]
267
+ **Projected fields:** [field list with types and business meaning]
268
+ **Synced by:** [event name → action, for each syncedBy entry]
269
+ **Consistency model:** Eventual — acceptable delay: [specify]
270
+ **Replaces:** [sync port name, if applicable — e.g., "Replaces OrderProductService sync call"]
271
+
272
+ ### Architectural Decision — {ReadModelName}
273
+
274
+ **Context:** [{module} needs {data description} from {source-module} to {business reason}]
275
+ **Decision:** Use event-driven Local Read Model instead of sync HTTP call.
276
+ **Consequences:**
277
+ - (+) Module is fully autonomous — no runtime dependency on {source-module}
278
+ - (+) Lower latency — local DB query vs HTTP roundtrip
279
+ - (-) Eventual consistency — milliseconds delay on updates
248
280
  ```
249
281
 
250
282
  ---
@@ -253,7 +285,7 @@ flowchart TD
253
285
 
254
286
  - **Todo en inglés**: títulos, secciones, descripciones, invariantes, use cases.
255
287
  - **INVARIANTES obligatorias**: al menos 2–3 por módulo. Analizar unicidad, estados válidos, rangos, precondiciones de transición.
256
- - **Diagrama de interacciones obligatorio**: todos los endpoints y eventos entrantes. Sin entradas → nodo `[[passive module]]`.
288
+ - **Diagrama de interacciones obligatorio**: todos los endpoints, eventos entrantes y read models. Sin entradas → nodo `[[passive module]]`. Read models se muestran como subgraphs separados con label `Read Models` y nodos `📦 {ReadModelName}` conectados desde eventos de sincronización.
257
289
  - **Diagrama de secuencia obligatorio**: al menos un `sequenceDiagram` cubriendo el happy path. Diagramas adicionales para bifurcaciones (error, compensación). Modelar todos los actores reales.
258
290
  - **Diagrama de flujo por caso de uso**: `flowchart TD` dentro de cada `### {UseCase}` con trigger, invariantes, lógica y eventos.
259
291
  - **Máquina de estados condicional**: solo si hay entidades con ciclo de vida. Restricciones de transición son invariantes implícitas.
@@ -58,6 +58,14 @@ integrations:
58
58
  - module: notifications
59
59
  useCase: NotifyOrderPlaced # acción que notifications ejecuta
60
60
 
61
+ # Read Model sync — consumer usa readModel: en vez de useCase:
62
+ - event: ProductCreatedEvent
63
+ producer: products
64
+ topic: PRODUCT_CREATED
65
+ consumers:
66
+ - module: orders
67
+ readModel: ProductReadModel # ← indica sync de read model, no lógica de negocio
68
+
61
69
  sync:
62
70
  - caller: orders # módulo que hace la llamada
63
71
  calls: customers # módulo destino
@@ -78,6 +86,7 @@ integrations:
78
86
  | Port names | PascalCase + sufijo `Service` — **único por módulo** | `OrderCustomerService` | `CustomerService` (compartido) |
79
87
  | useCases | PascalCase, verbo + sustantivo | `CreateOrder`, `FindAllOrders` | `createOrder`, `orders` |
80
88
  | `consumers[].useCase` | PascalCase, verbo + sustantivo | `HandleOrderPlaced` | `orderPlaced` |
89
+ | `consumers[].readModel` | PascalCase + `ReadModel` | `ProductReadModel` | `productReadModel` |
81
90
 
82
91
  ---
83
92
 
@@ -91,6 +100,31 @@ integrations:
91
100
  - ✅ `consumers[].module` debe existir en `modules:`
92
101
  - ✅ Módulos pasivos (notificaciones, auditoría) son **consumidores**, nunca `caller`
93
102
  - ℹ️ Varios módulos pueden consumir el mismo evento sin riesgo de colisión de beans
103
+ - ℹ️ `consumers[]` puede usar `readModel:` en vez de `useCase:` para indicar sync de Read Model local (proyección de datos cross-module mantenida por eventos)
104
+
105
+ ---
106
+
107
+ ## Consumers: useCase vs readModel
108
+
109
+ Cada entrada en `consumers[]` debe declarar **exactamente uno** de:
110
+
111
+ | Campo | Cuándo usar | Qué genera en domain.yaml |
112
+ |---|---|---|
113
+ | `useCase:` | Lógica de negocio (handler + command) | `listeners:` entry |
114
+ | `readModel:` | Proyección local de datos (sync handler) | `readModels:` entry con `syncedBy` |
115
+
116
+ ```yaml
117
+ consumers:
118
+ - module: payments
119
+ useCase: HandleOrderPlaced # → listeners: en payments.yaml
120
+ - module: orders
121
+ readModel: ProductReadModel # → readModels: en orders.yaml
122
+ ```
123
+
124
+ **Reglas de `readModel:`:**
125
+ - PascalCase, sufijo `ReadModel`
126
+ - El module consumidor declara `readModels:` en su `domain.yaml` con `source.module` apuntando al producer
127
+ - Al reemplazar un port sync por un readModel, eliminar la entrada de `integrations.sync[]`
94
128
 
95
129
  ---
96
130
 
@@ -170,6 +204,8 @@ consumers:
170
204
  - [ ] Sin dependencias circulares síncronas
171
205
  - [ ] Todos los `consumers[].module` existen en `modules:`
172
206
  - [ ] Todos los `consumers[].useCase` presentes y en PascalCase
207
+ - [ ] `consumers[]` con `readModel:` usan PascalCase + sufijo `ReadModel`
208
+ - [ ] Cada consumer tiene exactamente uno de `useCase:` o `readModel:` (nunca ambos)
173
209
  - [ ] Todos los `calls.using:` existen en `exposes:` del destino
174
210
  - [ ] Módulos pasivos no son `caller`
175
211
  - [ ] `useCases` en PascalCase
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
+ const { toCamelCase } = require('./naming');
3
4
 
4
5
  class ConfigManager {
5
6
  constructor(projectPath = process.cwd()) {
@@ -88,8 +89,9 @@ class ConfigManager {
88
89
  async moduleExists(moduleName) {
89
90
  const config = await this.loadProjectConfig();
90
91
  if (!config) return false;
91
-
92
- return config.modules.some(module => module.name === moduleName);
92
+
93
+ const normalized = toCamelCase(moduleName);
94
+ return config.modules.some(module => module.name === normalized);
93
95
  }
94
96
 
95
97
  /**