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.
- package/AGENTS.md +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- 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 (
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
- [ ] `
|
|
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: `
|
|
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
|
|
44
|
-
**
|
|
45
|
-
**
|
|
46
|
-
**
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|