eva4j 1.0.16 → 1.0.18

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 (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. package/COMMAND_EVALUATION.md +0 -911
@@ -25,13 +25,21 @@ Propón campos necesarios no mencionados, Value Objects expresivos, invariantes
25
25
  4. ❌ **No `transitions` sin `initialValue`** en el enum
26
26
  5. ❌ **No inventar módulos en `reference.module`** — solo los de `system/system.yaml`
27
27
  6. ❌ **No duplicar en `endpoints:`** lo de `system.yaml → exposes:`
28
- 7. ❌ **`endpoints:` NUNCA es lista plana** — siempre `{ basePath, versions: [{ version, operations }] }`
28
+ 7. ❌ **`endpoints:` NUNCA es lista plana** — siempre `{ basePath, versions: [{ version, operations }] }`. Si el módulo tiene **2+ agregados**, usar `basePath: ""` y paths absolutos por operación
29
29
  8. ❌ **No inventar eventos** — deben coincidir con `integrations.async[]` donde `producer` es este módulo
30
30
  9. ❌ **No inventar listeners** — deben coincidir con `integrations.async[].consumers[]` donde `module` es este módulo
31
31
  10. ❌ **No inventar ports** — deben coincidir con `integrations.sync[]` donde `caller` es este módulo
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
@@ -67,6 +76,50 @@ endpoints:
67
76
  path: /{id}
68
77
  ```
69
78
 
79
+ ### Módulos con múltiples agregados
80
+
81
+ Cuando un módulo contiene **2 o más agregados** (ej: Product + Category), NO es posible usar un solo `basePath` porque cada agregado tiene su propio recurso REST. En este caso:
82
+
83
+ - Usar `basePath: ""` (string vacío — **NO** `basePath: /` que genera slash trailing)
84
+ - Declarar paths **absolutos** en cada operación (ej: `/products`, `/categories/{id}`)
85
+ - El controlador generado tendrá `@RequestMapping("/api/v1")` (limpio, sin slash extra)
86
+
87
+ ```yaml
88
+ # ✅ Módulo con múltiples agregados — basePath vacío + paths absolutos
89
+ endpoints:
90
+ basePath: ""
91
+ versions:
92
+ - version: v1
93
+ operations:
94
+ # ── Product operations ──
95
+ - useCase: CreateProduct
96
+ method: POST
97
+ path: /products
98
+ - useCase: GetProduct
99
+ method: GET
100
+ path: /products/{id}
101
+ - useCase: FindAllProducts
102
+ method: GET
103
+ path: /products
104
+ # ── Category operations ──
105
+ - useCase: CreateCategory
106
+ method: POST
107
+ path: /categories
108
+ - useCase: GetCategory
109
+ method: GET
110
+ path: /categories/{id}
111
+ - useCase: FindProductsByCategory
112
+ method: GET
113
+ path: /categories/{id}/products
114
+ ```
115
+
116
+ **Regla de decisión:**
117
+
118
+ | Agregados en el módulo | basePath | Paths en operations |
119
+ |---|---|---|
120
+ | 1 agregado | `/recurso` (ej: `/orders`) | Relativos: `/`, `/{id}`, `/{id}/confirm` |
121
+ | 2+ agregados | `""` (vacío) | Absolutos: `/products`, `/categories/{id}` |
122
+
70
123
  ---
71
124
 
72
125
  ## Estructura completa del module.yaml
@@ -176,6 +229,35 @@ aggregates:
176
229
  - name: reason
177
230
  type: String
178
231
 
232
+ # ── Lifecycle events (CRUD-based, no transitions) ──
233
+ # Use lifecycle: instead of triggers: when the event is emitted
234
+ # at a CRUD operation (create/update/delete/softDelete).
235
+ # Typical use: source modules whose events feed readModels.
236
+ #
237
+ # - name: ProductCreatedEvent
238
+ # lifecycle: create # raise() in creation constructor
239
+ # fields:
240
+ # - name: productId
241
+ # type: String
242
+ # - name: name
243
+ # type: String
244
+ #
245
+ # - name: ProductUpdatedEvent
246
+ # lifecycle: update # raise() in UpdateCommandHandler
247
+ # fields: [...]
248
+ #
249
+ # - name: ProductDeletedEvent
250
+ # lifecycle: delete # raise() in DeleteCommandHandler
251
+ # fields:
252
+ # - name: productId
253
+ # type: String
254
+ #
255
+ # - name: ProductDeactivatedEvent
256
+ # lifecycle: softDelete # raise() in softDelete() method
257
+ # fields:
258
+ # - name: productId
259
+ # type: String
260
+
179
261
  endpoints:
180
262
  basePath: /orders
181
263
  versions:
@@ -289,34 +371,164 @@ Anotaciones: `NotNull`, `NotBlank`, `NotEmpty`, `Email`, `Size`, `Min`, `Max`, `
289
371
 
290
372
  ---
291
373
 
292
- ## Proyecciones locales (read models)
374
+ ## Proyecciones locales (`readModels:`)
375
+
376
+ 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.
377
+
378
+ ### Cuándo usar readModels vs ports
379
+
380
+ | Criterio | `readModels:` (async) | `ports:` (sync) |
381
+ |---|---|---|
382
+ | Consistencia eventual aceptable | ✅ | — |
383
+ | Se necesita consistencia fuerte | — | ✅ |
384
+ | Datos se consultan frecuentemente | ✅ | — |
385
+ | Llamada infrecuente y simple | — | ✅ |
386
+ | Preparación para microservicios | ✅ | — |
387
+
388
+ ### Estructura
389
+
390
+ ```yaml
391
+ # Nivel raíz, sibling de aggregates:, listeners:, ports:
392
+ readModels:
393
+ - name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
394
+ source: # Trazabilidad al módulo fuente (OBLIGATORIO)
395
+ module: products # Módulo fuente (kebab-case)
396
+ aggregate: Product # Agregado fuente (PascalCase)
397
+ tableName: rm_orders_products # Tabla en BD (OBLIGATORIO, prefijo rm_{consumer}_{source})
398
+ fields: # Campos proyectados — subconjunto del fuente
399
+ - name: id
400
+ type: String
401
+ - name: name
402
+ type: String
403
+ - name: price
404
+ type: BigDecimal
405
+ - name: status
406
+ type: String
407
+ syncedBy: # Eventos que mantienen esta tabla (min 1)
408
+ - event: ProductCreatedEvent # Nombre del evento (PascalCase + sufijo Event)
409
+ action: UPSERT # Acción: UPSERT | DELETE | SOFT_DELETE
410
+ - event: ProductUpdatedEvent
411
+ action: UPSERT
412
+ - event: ProductDeactivatedEvent
413
+ action: SOFT_DELETE
414
+ ```
415
+
416
+ ### Reglas
417
+
418
+ - **`name:`** — PascalCase, **DEBE** terminar con `ReadModel`
419
+ - **`tableName:`** — **DEBE** seguir el patrón `rm_{consumerModule}_{sourceModule}` (ej: `rm_orders_products`) para evitar colisiones en monolitos
420
+ - **`fields:`** — **DEBE** incluir un campo `id`
421
+ - **`syncedBy:`** — **DEBE** tener al menos una entrada
422
+ - **`source.module:`** — **NO PUEDE** ser el mismo módulo actual (cross-module exclusivamente)
423
+ - **Sin auditoría** — Los readModels **nunca** tienen campos de auditoría
424
+ - **Sin endpoints REST** — Los readModels **nunca** exponen endpoints
425
+ - **Sin lógica de negocio** — La clase de dominio es inmutable (solo getters)
426
+
427
+ ### Acciones de sincronización
428
+
429
+ | Acción | Significado | Uso |
430
+ |---|---|---|
431
+ | `UPSERT` | Insertar si es nuevo, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
432
+ | `DELETE` | Eliminar el registro permanentemente | Hard deletes en el módulo fuente |
433
+ | `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
434
+
435
+ ### Inferencia desde system.yaml
436
+
437
+ | Fuente en system.yaml | Destino en domain.yaml |
438
+ |---|---|
439
+ | `consumers[].readModel: ProductReadModel` | `readModels:` entry con ese nombre |
440
+ | `integrations.async[].producer` | `readModels[].source.module` |
441
+ | `integrations.async[].topic` | Topic derivado automáticamente del nombre del evento |
442
+
443
+ ### Impacto en el módulo fuente
444
+
445
+ 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).
446
+
447
+ **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).
293
448
 
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
449
+ ### Propiedad `lifecycle:` en eventos del módulo fuente
450
+
451
+ Cuando un evento existe para alimentar un readModel (operación CRUD pura, sin transición de estado), usar `lifecycle:` en vez de `triggers:`.
452
+
453
+ | Valor | Punto de emisión | Descripción |
454
+ |---|---|---|
455
+ | `create` | Constructor de creación de la entidad | UUID auto-generado como id antes de raise() |
456
+ | `update` | UpdateCommandHandler, antes de `repository.save()` | raise() sobre la entidad reconstruida |
457
+ | `delete` | DeleteCommandHandler, antes de `repository.delete()` | Hard delete — requiere `hasSoftDelete` ausente o false |
458
+ | `softDelete` | Método `softDelete()` de la entidad | Requiere `hasSoftDelete: true` en la entidad raíz |
299
459
 
300
- **Auditoría:** `audit.enabled: true`, `trackUser: false` (cambios vienen de eventos, no de usuarios).
460
+ **Derivación desde el nombre del evento:**
461
+
462
+ | Patrón del nombre del evento | `lifecycle:` |
463
+ |---|---|
464
+ | `*CreatedEvent`, `*RegisteredEvent` | `create` |
465
+ | `*UpdatedEvent` | `update` |
466
+ | `*DeletedEvent` | `delete` |
467
+ | `*DeactivatedEvent` | `softDelete` |
468
+
469
+ **Ejemplo — módulo fuente `products`:**
301
470
 
302
471
  ```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
472
+ aggregates:
473
+ - name: Product
474
+ entities:
475
+ - name: product
476
+ isRoot: true
477
+ tableName: products
478
+ hasSoftDelete: true # requerido por lifecycle: softDelete
479
+ audit:
480
+ enabled: true
481
+ fields:
482
+ - name: id
483
+ type: String
484
+ - name: name
485
+ type: String
486
+ - name: price
487
+ type: BigDecimal
488
+ - name: status
489
+ type: String
490
+ readOnly: true
491
+ defaultValue: "ACTIVE"
492
+ events:
493
+ - name: ProductCreatedEvent
494
+ lifecycle: create
495
+ fields:
496
+ - name: productId
497
+ type: String
498
+ - name: name
499
+ type: String
500
+ - name: price
501
+ type: BigDecimal
502
+ - name: status
503
+ type: String
504
+ - name: ProductUpdatedEvent
505
+ lifecycle: update
506
+ fields:
507
+ - name: productId
508
+ type: String
509
+ - name: name
510
+ type: String
511
+ - name: price
512
+ type: BigDecimal
513
+ - name: status
514
+ type: String
515
+ - name: ProductDeactivatedEvent
516
+ lifecycle: softDelete
517
+ fields:
518
+ - name: productId
519
+ type: String
520
+ - name: deactivatedAt
521
+ type: LocalDateTime
318
522
  ```
319
523
 
524
+ **Reglas:**
525
+ - `lifecycle:` y `triggers:` son mutuamente excluyentes — un evento usa uno u otro, nunca ambos
526
+ - `lifecycle: softDelete` requiere `hasSoftDelete: true` en la entidad raíz
527
+ - `lifecycle: delete` requiere que `hasSoftDelete` sea `false` o esté ausente
528
+ - `fields:` del evento debe incluir **todos** los campos del readModel que lo consume (el payload es la fuente de verdad para la proyección)
529
+ - Siempre incluir `{entityName}Id` como campo (se mapea a `aggregateId` del DomainEvent base)
530
+ - `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`
531
+
320
532
  ---
321
533
 
322
534
  ## Clasificación de campos readOnly
@@ -397,7 +609,9 @@ Si un valor no es trazable → falta un endpoint o listener en el diseño.
397
609
  - [ ] `events[].triggers[]` referencia métodos existentes en `transitions[].method`
398
610
  - [ ] `{entityName}Id` en `events[].fields` cuando cruza módulos via Kafka
399
611
  - [ ] `endpoints:` con estructura `{ basePath, versions }` — no lista plana
400
- - [ ] `endpoints[].path` relativos al basePath
612
+ - [ ] Módulo con 1 agregado → `basePath: /recurso` y paths relativos
613
+ - [ ] Módulo con 2+ agregados → `basePath: ""` y paths absolutos por operación
614
+ - [ ] `endpoints[].path` coherentes con el basePath elegido
401
615
  - [ ] `listeners[]` para todos los eventos donde este módulo es consumidor
402
616
  - [ ] `listeners[].useCase` coincide con `consumers[].useCase`
403
617
  - [ ] `listeners[].topic` bare SCREAMING_SNAKE_CASE (sin topicPrefix)
@@ -406,5 +620,18 @@ Si un valor no es trazable → falta un endpoint o listener en el diseño.
406
620
  - [ ] `nestedTypes[]` para campos de tipo objeto en listeners/ports
407
621
  - [ ] Transiciones por `ports:` tienen domain event con `triggers`
408
622
  - [ ] Cada enum `*Type` valor trazable a listener/endpoint/event
409
- - [ ] Proyecciones: `audit.enabled: true`, `trackUser: false`
623
+ - [ ] Proyecciones cross-module: usar `readModels:` con `source`, `tableName` (prefijo `rm_`), `fields` (incluye `id`), `syncedBy`
624
+ - [ ] `readModels[].name` termina con `ReadModel` (PascalCase)
625
+ - [ ] `readModels[].source.module` ≠ módulo actual
626
+ - [ ] `readModels[].syncedBy` → al menos una entrada con `action` válida (UPSERT, DELETE, SOFT_DELETE)
627
+ - [ ] Eventos en `syncedBy` deben existir como `events:` en el módulo fuente
628
+ - [ ] Eventos del módulo fuente consumidos por readModels → deben tener `lifecycle:` (no `triggers:`)
629
+ - [ ] `lifecycle:` derivado del nombre del evento: `*CreatedEvent`→`create`, `*UpdatedEvent`→`update`, `*DeletedEvent`→`delete`, `*DeactivatedEvent`→`softDelete`
630
+ - [ ] `lifecycle: softDelete` → entidad raíz tiene `hasSoftDelete: true`
631
+ - [ ] `lifecycle: delete` → entidad raíz NO tiene `hasSoftDelete: true`
632
+ - [ ] `lifecycle:` y `triggers:` nunca en el mismo evento
633
+ - [ ] Lifecycle event fields son campos de la entidad raíz (excluyendo `{entityName}Id` y `*At` temporal) — `C2-010`
634
+ - [ ] ReadModel fields cubiertos por eventos UPSERT del productor — `C1-007`
635
+ - [ ] ReadModel fields son subconjunto de los campos de la entidad raíz fuente
636
+ - [ ] Si `readModels:` reemplaza un `ports:`, eliminar la entrada sync correspondiente
410
637
  - [ ] Todo en inglés
@@ -34,16 +34,23 @@ Especificación técnica narrativa del sistema completo. Una sección `##` por c
34
34
  **Postconditions:** [system state after successful execution]
35
35
  **Validations and errors:** [exception conditions and error types]
36
36
  **Events emitted:** [DomainEvent name and trigger condition, or "none"]
37
+ **Operations:**
38
+ 1. [Load entity / validate precondition — throw NotFoundException or 400 if invalid]
39
+ 2. [External port call: {PortName}.{method}() — if applicable]
40
+ 3. [Invoke domain method: entity.{method}()]
41
+ 4. [Persist via repository]
42
+ 5. [Emit event — if applicable]
37
43
 
38
44
  ### Exposed Endpoints
39
45
  [One `####` per endpoint in exposes:]
40
46
 
41
47
  #### {METHOD} {/path}
42
48
  **Purpose:** [endpoint description and usage context]
43
- **Path params / Query params:** [each parameter described]
44
- **Request body:** [expected fields, types, validation constraints]
45
- **Response:** [returned fields and their business meaning]
46
- **Errors:** [HTTP status codes and when they occur]
49
+ **Path params:** [param Type Description; omit if none]
50
+ **Query params:** [GET/DELETE only — param: Type, required/optional, default, description; omit if none]
51
+ **Request body:** [POST/PUT/PATCH only field: Type — constraint; omit for GET/DELETE]
52
+ **Response:** [GET only field: Type business meaning; list endpoints include {content:[...], totalElements, page, size}]
53
+ **Errors:** [HTTP status — condition]
47
54
 
48
55
  ### Emitted Events
49
56
  [Only if module is producer in integrations.async]
@@ -61,6 +68,17 @@ Especificación técnica narrativa del sistema completo. Una sección `##` por c
61
68
  **When called:** [in which use case and under what condition]
62
69
  **Endpoints used:** [METHOD /path list]
63
70
  **Data obtained and how it's used:** [detailed description]
71
+
72
+ ### Read Models (local projections)
73
+ [Only if module has readModels: in its domain.yaml]
74
+
75
+ #### {ReadModelName} ← {source-module}
76
+ **Purpose:** [why this projection exists — what data it provides and why sync HTTP was replaced]
77
+ **Source aggregate:** [{Aggregate} in {source-module}]
78
+ **Table:** [{tableName}]
79
+ **Projected fields:** [field list with types]
80
+ **Synced by:** [event names and actions (UPSERT/DELETE/SOFT_DELETE)]
81
+ **Consistency model:** Eventual — acceptable delay: [specify, e.g. "milliseconds"]
64
82
  ```
65
83
 
66
84
  ### Reglas del system.md
@@ -70,6 +88,8 @@ Especificación técnica narrativa del sistema completo. Una sección `##` por c
70
88
  - **Incluir useCases de consumers** como casos de uso del módulo consumidor.
71
89
  - **Referenciar módulos por nombre**.
72
90
  - **Máquinas de estado** cuando hay ciclos de vida.
91
+ - **Endpoints detallados**: separar path params de query params. Para GETs indicar los campos clave de la respuesta con sus tipos. Para POST/PUT/PATCH listar los campos del body con tipo y constraint. Nunca "ver request" como descripción de response.
92
+ - **Operations en casos de uso**: el campo `**Operations:**` es obligatorio en cada `#### {UseCaseName}`. Cada ítem describe un paso concreto del handler referenciando nombres reales de repositorios, ports, métodos de dominio y eventos. No usar items genéricos como "execute business logic".
73
93
  - Omitir secciones no aplicables.
74
94
 
75
95
  ---
@@ -204,6 +224,12 @@ sequenceDiagram
204
224
  **Invariants verified:** [ID list — e.g., INV-01, INV-03]
205
225
  **Validations and errors:** [exception conditions, error type, HTTP status]
206
226
  **Events emitted:** [DomainEvent name and condition, or "none"]
227
+ **Operations:**
228
+ 1. [Load entity by id via {Repository}.findById() — throw NotFoundException if absent]
229
+ 2. [Call {PortName}.{method}() to obtain cross-module data — if applicable]
230
+ 3. [Invoke entity.{domainMethod}() — domain enforces invariants]
231
+ 4. [Persist via {Repository}.save(entity)]
232
+ 5. [DomainEventHandler publishes event after transaction commit — if applicable]
207
233
 
208
234
  **Flow diagram:**
209
235
  ```mermaid
@@ -224,10 +250,42 @@ flowchart TD
224
250
  ### {METHOD} {/path}
225
251
  **Use case:** `{UseCase}`
226
252
  **Purpose:** [description]
227
- **Path params / Query params:** [each parameter]
228
- **Request body:** [fields, types, validations]
229
- **Response:** [fields and business meaning]
230
- **Errors:** [HTTP status codes and conditions]
253
+ **Path params:** _(omit table if none)_
254
+
255
+ | Param | Type | Description |
256
+ |-------|------|-------------|
257
+ | {param} | `String` | [description] |
258
+
259
+ **Query params:** _(GET / DELETE only — omit table if none)_
260
+
261
+ | Param | Type | Required | Default | Description |
262
+ |-------|------|----------|---------|-------------|
263
+ | {param} | `String` | No | — | [description] |
264
+
265
+ **Request body:** _(POST / PUT / PATCH only — omit for GET / DELETE)_
266
+ ```json
267
+ {
268
+ "field": "String", // required — [constraint]
269
+ "otherField": "Integer" // optional — [constraint]
270
+ }
271
+ ```
272
+
273
+ **Response schema:** _(GET single only — omit for mutations)_
274
+ ```json
275
+ {
276
+ "id": "String",
277
+ "field": "Type" // [business meaning]
278
+ }
279
+ ```
280
+ _(For GET list: `{ "content": [...], "totalElements": "Long", "page": "Integer", "size": "Integer" }`)_
281
+
282
+ **Errors:**
283
+
284
+ | Status | Condition |
285
+ |--------|-----------|
286
+ | 404 | [entity] not found |
287
+ | 400 | [validation failure] |
288
+ | 409 | [invariant violated] |
231
289
 
232
290
  ## Emitted Events
233
291
  [Only if module is producer in integrations.async]
@@ -245,6 +303,27 @@ flowchart TD
245
303
  **When called:** [in which use case and condition]
246
304
  **Endpoints used:** [METHOD /path list]
247
305
  **Data obtained and how it's used:** [detailed description]
306
+
307
+ ## Read Models (local projections)
308
+ [Only if module has readModels: in its domain.yaml]
309
+
310
+ ### {ReadModelName} ← {source-module}
311
+ **Purpose:** [why this projection exists — what data it provides and why sync HTTP was not used]
312
+ **Source aggregate:** [{Aggregate} in {source-module}]
313
+ **Table:** [{tableName}]
314
+ **Projected fields:** [field list with types and business meaning]
315
+ **Synced by:** [event name → action, for each syncedBy entry]
316
+ **Consistency model:** Eventual — acceptable delay: [specify]
317
+ **Replaces:** [sync port name, if applicable — e.g., "Replaces OrderProductService sync call"]
318
+
319
+ ### Architectural Decision — {ReadModelName}
320
+
321
+ **Context:** [{module} needs {data description} from {source-module} to {business reason}]
322
+ **Decision:** Use event-driven Local Read Model instead of sync HTTP call.
323
+ **Consequences:**
324
+ - (+) Module is fully autonomous — no runtime dependency on {source-module}
325
+ - (+) Lower latency — local DB query vs HTTP roundtrip
326
+ - (-) Eventual consistency — milliseconds delay on updates
248
327
  ```
249
328
 
250
329
  ---
@@ -253,11 +332,13 @@ flowchart TD
253
332
 
254
333
  - **Todo en inglés**: títulos, secciones, descripciones, invariantes, use cases.
255
334
  - **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]]`.
335
+ - **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
336
  - **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
337
  - **Diagrama de flujo por caso de uso**: `flowchart TD` dentro de cada `### {UseCase}` con trigger, invariantes, lógica y eventos.
259
338
  - **Máquina de estados condicional**: solo si hay entidades con ciclo de vida. Restricciones de transición son invariantes implícitas.
260
339
  - **Referenciar invariantes** en cada caso de uso (INV-01, INV-02...).
340
+ - **Endpoints con contratos ricos**: para cada endpoint separar path params, query params, body y response en bloques distintos con tipos y constraints reales del dominio. Para GETs incluir el JSON schema de respuesta; para POST/PUT/PATCH el JSON del body. Listas incluyen wrapper de paginación `{ content, totalElements, page, size }`. Nunca usar placeholders genéricos.
341
+ - **Operations en casos de uso**: el campo `**Operations:**` es **obligatorio** en cada `### {UseCase}`. Lista los pasos del handler con nombres concretos: repositorio, port, método de dominio, evento. Sirve como contrato de implementación evaluable por revisores humanos antes de generar código.
261
342
  - **No duplicar** system.md — el `.md` del módulo es la especificación completa.
262
343
  - Archivos en `system/{module-name}.md`.
263
344
 
@@ -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