eva4j 1.0.13 → 1.0.14
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 +51 -9
- package/DOMAIN_YAML_GUIDE.md +150 -0
- package/bin/eva4j.js +31 -1
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +542 -0
- package/docs/commands/GENERATE_ENTITIES.md +196 -0
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/system.yaml +289 -0
- package/package.json +1 -1
- package/src/commands/create.js +6 -3
- package/src/commands/evaluate-system.js +384 -0
- package/src/commands/generate-entities.js +677 -14
- package/src/commands/generate-kafka-event.js +59 -5
- package/src/commands/generate-system.js +243 -0
- package/src/generators/base-generator.js +9 -1
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +314 -0
- package/src/utils/yaml-to-entity.js +31 -2
- package/templates/aggregate/AggregateRepository.java.ejs +5 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
- package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
- package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
- package/templates/base/root/skill-build-system-yaml.ejs +252 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +12 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/evaluate/report.html.ejs +971 -0
- package/templates/kafka-event/Event.java.ejs +7 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-domain-yaml
|
|
3
|
+
description: 'Construir o actualizar el domain.yaml de un módulo eva4j. Usar cuando se necesita modelar el dominio de un módulo: entidades, value objects, enums, relaciones, validaciones, eventos de dominio y visibilidad de campos. Lee los archivos del proyecto antes de generar.'
|
|
4
|
+
argument-hint: 'Nombre del módulo y descripción del negocio (ej: módulo orders — gestión del ciclo de vida de pedidos)'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Eres un arquitecto de software experto en DDD y arquitectura hexagonal trabajando en el proyecto **<%= projectName %>**.
|
|
8
|
+
|
|
9
|
+
Tu tarea es construir o actualizar el `domain.yaml` de un módulo específico.
|
|
10
|
+
|
|
11
|
+
Documentos de referencia del proyecto:
|
|
12
|
+
- [AGENTS.md](/AGENTS.md) — patrones, convenciones y checklist de este proyecto eva4j
|
|
13
|
+
- [system.yaml](/system.yaml) — arquitectura del sistema (módulos, endpoints, eventos)
|
|
14
|
+
- [references/GENERATE_ENTITIES.md](references/GENERATE_ENTITIES.md) — referencia técnica completa del generador: tipos soportados, propiedades de campo, relaciones, auditoría, validaciones, enums con transiciones, eventos de dominio y lista de archivos generados
|
|
15
|
+
|
|
16
|
+
> **Lee siempre los tres archivos antes de proponer cualquier cambio.**
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Restricciones absolutas
|
|
21
|
+
|
|
22
|
+
Antes de escribir cualquier YAML, verifica que NO estás haciendo ninguno de estos errores:
|
|
23
|
+
|
|
24
|
+
1. ❌ **No agregar `@ManyToOne` / `@OneToMany` entre agregados distintos** — las referencias cross-aggregate son IDs simples con `reference:`
|
|
25
|
+
2. ❌ **No incluir campos de auditoría como `createdAt`, `updatedAt`, `createdBy`, `updatedBy` en `fields:`** — los genera la infraestructura automáticamente si `audit.enabled: true`
|
|
26
|
+
3. ❌ **No usar `defaultValue` en campos que no son `readOnly: true`** — solo aplica a campos readOnly
|
|
27
|
+
4. ❌ **No declarar `transitions` sin `initialValue` en el enum** — si hay lifecycle declarar ambos
|
|
28
|
+
5. ❌ **No inventar módulos en `reference.module`** — solo los que existen en `system.yaml → modules:`
|
|
29
|
+
6. ❌ **No poner validaciones JSR-303 en entidades de dominio** — van solo en Commands y DTOs (el generador lo hace; tú solo declara `validations:` en el campo)
|
|
30
|
+
7. ❌ **No incluir endpoints REST en `domain.yaml`** si ya están declarados en `system.yaml → exposes:` — usa `endpoints:` solo para detalles adicionales no cubiertos
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Paso 1 — Recopilar información
|
|
35
|
+
|
|
36
|
+
Lee `system.yaml` para obtener:
|
|
37
|
+
- Módulos existentes (para referencias cross-aggregate)
|
|
38
|
+
- Endpoints expuestos por el módulo (alimentan `endpoints:` → `useCase`)
|
|
39
|
+
- Eventos que produce el módulo (alimentan `events:`)
|
|
40
|
+
- Puertos síncronos que consume (alimentan `ports:`)
|
|
41
|
+
|
|
42
|
+
Luego pregunta al usuario lo que no puedas inferir:
|
|
43
|
+
|
|
44
|
+
1. **¿Cuál es el módulo?** (nombre en kebab-case)
|
|
45
|
+
2. **¿Cuáles son las entidades principales?** ¿Cuál es la raíz del agregado?
|
|
46
|
+
3. **¿Qué campos tiene cada entidad?** Para cada campo: nombre, tipo, ¿es requerido en creación?, ¿debe ocultarse en la respuesta?, ¿tiene validaciones?
|
|
47
|
+
4. **¿Algún campo es calculado/derivado?** (→ `readOnly: true`) ¿Con qué valor inicial? (→ `defaultValue`)
|
|
48
|
+
5. **¿Algún campo es sensible?** (passwords, tokens) (→ `hidden: true`)
|
|
49
|
+
6. **¿Hay Value Objects?** (dirección, dinero, coordenadas) ¿Necesitan métodos de negocio?
|
|
50
|
+
7. **¿La entidad tiene un ciclo de vida/estados?** ¿Cuáles son y qué transiciones son válidas?
|
|
51
|
+
8. **¿Hay relaciones dentro del mismo agregado?** (OneToOne, OneToMany)
|
|
52
|
+
9. **¿Necesita soft delete?** (borrado lógico con `deletedAt`)
|
|
53
|
+
10. **¿Necesita auditoría?** ¿Solo timestamps (`audit.enabled`) o también usuario (`audit.trackUser`)?
|
|
54
|
+
|
|
55
|
+
> Si el `domain.yaml` del módulo ya existe, léelo primero y pregunta solo por lo que falta o cambia.
|
|
56
|
+
|
|
57
|
+
> Consulta [references/GENERATE_ENTITIES.md](references/GENERATE_ENTITIES.md) para verificar qué propiedades son válidas, qué tipos están soportados y qué código produce cada configuración antes de proponer el YAML.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Paso 2 — Estructura completa del domain.yaml
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
aggregates:
|
|
65
|
+
- name: Order # PascalCase — nombre del agregado
|
|
66
|
+
entities:
|
|
67
|
+
- name: order # camelCase — nombre de la entidad
|
|
68
|
+
isRoot: true # true para la raíz del agregado
|
|
69
|
+
tableName: orders # snake_case — nombre de tabla SQL
|
|
70
|
+
hasSoftDelete: false # true para borrado lógico (deletedAt)
|
|
71
|
+
audit:
|
|
72
|
+
enabled: true # agrega createdAt, updatedAt
|
|
73
|
+
trackUser: false # agrega createdBy, updatedBy
|
|
74
|
+
fields:
|
|
75
|
+
- name: id
|
|
76
|
+
type: String
|
|
77
|
+
- name: orderNumber
|
|
78
|
+
type: String
|
|
79
|
+
validations:
|
|
80
|
+
- type: NotBlank
|
|
81
|
+
message: "Número de pedido requerido"
|
|
82
|
+
- name: totalAmount
|
|
83
|
+
type: BigDecimal
|
|
84
|
+
readOnly: true # calculado — no en CreateDto ni constructor de negocio
|
|
85
|
+
defaultValue: "0.00" # valor inicial en constructor de creación
|
|
86
|
+
- name: status
|
|
87
|
+
type: OrderStatus
|
|
88
|
+
readOnly: true # gestionado por transitions
|
|
89
|
+
- name: processingToken
|
|
90
|
+
type: String
|
|
91
|
+
hidden: true # sensible — no en ResponseDto
|
|
92
|
+
- name: customerId
|
|
93
|
+
type: String
|
|
94
|
+
reference:
|
|
95
|
+
aggregate: Customer # PascalCase
|
|
96
|
+
module: customers # módulo donde vive
|
|
97
|
+
relationships:
|
|
98
|
+
- type: OneToMany # OneToOne | OneToMany | ManyToOne
|
|
99
|
+
target: OrderItem
|
|
100
|
+
mappedBy: order
|
|
101
|
+
cascade: [PERSIST, MERGE, REMOVE]
|
|
102
|
+
fetch: LAZY
|
|
103
|
+
|
|
104
|
+
- name: orderItem # entidad secundaria — isRoot omitido (false por defecto)
|
|
105
|
+
tableName: order_items
|
|
106
|
+
fields:
|
|
107
|
+
- name: id
|
|
108
|
+
type: String
|
|
109
|
+
- name: productId
|
|
110
|
+
type: String
|
|
111
|
+
- name: quantity
|
|
112
|
+
type: Integer
|
|
113
|
+
validations:
|
|
114
|
+
- type: Min
|
|
115
|
+
value: 1
|
|
116
|
+
- name: unitPrice
|
|
117
|
+
type: BigDecimal
|
|
118
|
+
# No declarar ManyToOne hacia Order — el generador lo infiere automáticamente
|
|
119
|
+
# desde la relación OneToMany declarada en la entidad raíz
|
|
120
|
+
|
|
121
|
+
valueObjects:
|
|
122
|
+
- name: ShippingAddress # PascalCase — inmutable, sin ID
|
|
123
|
+
fields:
|
|
124
|
+
- name: street
|
|
125
|
+
type: String
|
|
126
|
+
- name: city
|
|
127
|
+
type: String
|
|
128
|
+
- name: zipCode
|
|
129
|
+
type: String
|
|
130
|
+
methods: # opcional — lógica de negocio del VO
|
|
131
|
+
- name: format
|
|
132
|
+
returnType: String
|
|
133
|
+
parameters: []
|
|
134
|
+
body: "return street + \", \" + city + \" \" + zipCode;"
|
|
135
|
+
|
|
136
|
+
enums:
|
|
137
|
+
- name: OrderStatus
|
|
138
|
+
initialValue: PENDING # estado inicial — excluido del CreateDto
|
|
139
|
+
transitions:
|
|
140
|
+
- from: PENDING
|
|
141
|
+
to: CONFIRMED
|
|
142
|
+
method: confirm
|
|
143
|
+
- from: CONFIRMED
|
|
144
|
+
to: SHIPPED
|
|
145
|
+
method: ship
|
|
146
|
+
- from: [PENDING, CONFIRMED] # múltiples orígenes
|
|
147
|
+
to: CANCELLED
|
|
148
|
+
method: cancel
|
|
149
|
+
guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
|
|
150
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
151
|
+
|
|
152
|
+
events:
|
|
153
|
+
- name: OrderConfirmedEvent # PascalCase, tiempo pasado, sufijo Event
|
|
154
|
+
fields:
|
|
155
|
+
- name: orderId
|
|
156
|
+
type: String
|
|
157
|
+
- name: confirmedAt
|
|
158
|
+
type: LocalDateTime
|
|
159
|
+
# kafka: true ← agregar solo si el módulo usa mensajería (system.yaml → messaging.enabled)
|
|
160
|
+
|
|
161
|
+
endpoints: # Opcional — solo si system.yaml ya declara exposes: para este módulo
|
|
162
|
+
basePath: /orders
|
|
163
|
+
versions:
|
|
164
|
+
- version: v1
|
|
165
|
+
operations:
|
|
166
|
+
- useCase: GetOrder # debe coincidir con system.yaml → exposes[].useCase
|
|
167
|
+
method: GET
|
|
168
|
+
path: /orders/{id}
|
|
169
|
+
- useCase: FindAllOrders
|
|
170
|
+
method: GET
|
|
171
|
+
path: /orders
|
|
172
|
+
- useCase: CreateOrder
|
|
173
|
+
method: POST
|
|
174
|
+
path: /orders
|
|
175
|
+
- useCase: ConfirmOrder # nombre de negocio → genera scaffold
|
|
176
|
+
method: PUT
|
|
177
|
+
path: /orders/{id}/confirm
|
|
178
|
+
|
|
179
|
+
ports: # Opcional — solo si el módulo consume servicios síncronos
|
|
180
|
+
- name: CustomerService # debe coincidir con system.yaml → integrations.sync[].port
|
|
181
|
+
module: customers
|
|
182
|
+
operations:
|
|
183
|
+
- method: GET
|
|
184
|
+
path: /customers/{id}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Paso 3 — Reglas de campos
|
|
190
|
+
|
|
191
|
+
### Tabla de visibilidad
|
|
192
|
+
|
|
193
|
+
| Configuración | Constructor negocio | CreateDto | ResponseDto |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| Campo normal | ✅ | ✅ | ✅ |
|
|
196
|
+
| `readOnly: true` | ❌ | ❌ | ✅ |
|
|
197
|
+
| `readOnly: true` + `defaultValue` | ⚡ asignado con default | ❌ | ✅ |
|
|
198
|
+
| `hidden: true` | ✅ | ✅ | ❌ |
|
|
199
|
+
| Ambos flags | ❌ | ❌ | ❌ |
|
|
200
|
+
|
|
201
|
+
### Reglas de `defaultValue`
|
|
202
|
+
|
|
203
|
+
- Solo para campos `readOnly: true`
|
|
204
|
+
- Tipos numéricos: `0`, `0.00`
|
|
205
|
+
- Tipos String: `"valor"`
|
|
206
|
+
- Enums: nombre del valor sin comillas (ej: `PENDING`)
|
|
207
|
+
- Boolean: `true` o `false`
|
|
208
|
+
|
|
209
|
+
### Tipos soportados
|
|
210
|
+
|
|
211
|
+
| Tipo YAML | Tipo Java |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `String` | String |
|
|
214
|
+
| `Integer` | Integer |
|
|
215
|
+
| `Long` | Long |
|
|
216
|
+
| `BigDecimal` | BigDecimal |
|
|
217
|
+
| `Boolean` | Boolean |
|
|
218
|
+
| `LocalDate` | LocalDate |
|
|
219
|
+
| `LocalDateTime` | LocalDateTime |
|
|
220
|
+
| `UUID` | UUID |
|
|
221
|
+
|
|
222
|
+
### Validaciones JSR-303 disponibles
|
|
223
|
+
|
|
224
|
+
`NotNull`, `NotBlank`, `NotEmpty`, `Email`, `Size` (min/max), `Min` (value), `Max` (value), `Pattern` (regexp), `Digits` (integer/fraction), `Positive`, `PositiveOrZero`, `Negative`, `Past`, `Future`, `AssertTrue`, `AssertFalse`
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Paso 4 — Reglas de relaciones
|
|
229
|
+
|
|
230
|
+
- ✅ `OneToMany` / `OneToOne` entre entidades del **mismo agregado** → declara la relación **solo en la entidad raíz**
|
|
231
|
+
- ✅ El generador infiere automáticamente el lado inverso (`ManyToOne`) en la entidad secundaria — **no declararlo**
|
|
232
|
+
- ✅ Referencia a entidad de **otro agregado** → usa `reference:` en el campo ID (tipo String/Long), nunca `relationships:`
|
|
233
|
+
- ❌ No declarar `ManyToOne` en la entidad secundaria si ya existe `OneToMany` en la raíz con `mappedBy`
|
|
234
|
+
- El `mappedBy` debe coincidir con el nombre del campo en la entidad secundaria que apunta de vuelta a la raíz
|
|
235
|
+
- Cascadas recomendadas: `[PERSIST, MERGE, REMOVE]` para composición, `[PERSIST, MERGE]` para asociación
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Paso 5 — Consistencia con system.yaml
|
|
240
|
+
|
|
241
|
+
Verifica antes de entregar:
|
|
242
|
+
|
|
243
|
+
- **`events[].name`** debe coincidir con los eventos en `system.yaml → integrations.async[].event` del módulo
|
|
244
|
+
- **`endpoints[].operations[].useCase`** debe coincidir con `system.yaml → modules[].exposes[].useCase`
|
|
245
|
+
- **`ports[].name`** debe coincidir con `system.yaml → integrations.sync[].port` donde `caller` es este módulo
|
|
246
|
+
- **`reference.module`** debe ser un módulo declarado en `system.yaml → modules[].name`
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Paso 6 — Checklist de validación interna
|
|
251
|
+
|
|
252
|
+
Antes de proponer el `domain.yaml`, verifica:
|
|
253
|
+
|
|
254
|
+
- [ ] Campo `id` presente en todas las entidades
|
|
255
|
+
- [ ] Solo una entidad tiene `isRoot: true` por agregado — las demás no necesitan declararlo
|
|
256
|
+
- [ ] Relaciones intra-agregado declaradas solo en la entidad raíz; lado inverso NO declarado en la secundaria
|
|
257
|
+
- [ ] Campos de auditoría NO en `fields:` (los genera el framework)
|
|
258
|
+
- [ ] Campos `readOnly` con `defaultValue` si tienen valor inicial conocido
|
|
259
|
+
- [ ] Enums con ciclo de vida tienen `initialValue` y `transitions`
|
|
260
|
+
- [ ] `transitions.guard` apunta a condición que lanza excepción (no la condición feliz)
|
|
261
|
+
- [ ] Value Objects sin campo `id`
|
|
262
|
+
- [ ] Relaciones cross-aggregate usando `reference:` en el campo ID, no `relationships:`
|
|
263
|
+
- [ ] `events[].name` consistente con `system.yaml`
|
|
264
|
+
- [ ] `endpoints[].useCase` consistente con `system.yaml`
|
|
265
|
+
- [ ] `ports[].name` consistente con `system.yaml`
|
|
266
|
+
- [ ] Validaciones solo en `fields.validations:`, no en otro lado
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Paso 7 — Entregar el resultado
|
|
271
|
+
|
|
272
|
+
1. Muestra el `domain.yaml` completo en un bloque de código YAML
|
|
273
|
+
2. Explica brevemente las decisiones no obvias:
|
|
274
|
+
- Por qué ciertos campos son `readOnly` o `hidden`
|
|
275
|
+
- Por qué un campo es un Value Object y no un campo simple
|
|
276
|
+
- Qué transiciones de estado se modelaron y por qué
|
|
277
|
+
3. Menciona cualquier inconsistencia detectada con `system.yaml` que el usuario deba resolver
|
|
278
|
+
4. Indica el comando siguiente:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
eva g entities <nombre-del-módulo>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Ciclo de refinamiento
|
|
287
|
+
|
|
288
|
+
Después de entregar el `domain.yaml` v1, el usuario puede pedir ajustes:
|
|
289
|
+
- Aplica el **cambio mínimo** necesario
|
|
290
|
+
- Vuelve a verificar el checklist del Paso 6
|
|
291
|
+
- Entrega solo el diff explicado, no el archivo completo de nuevo (salvo que se pida)
|
|
292
|
+
- Si el cambio afecta consistencia con `system.yaml`, señálalo explícitamente
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-system-yaml
|
|
3
|
+
description: 'Construir o actualizar el system.yaml de un proyecto eva4j. Usar cuando se necesita diseñar la arquitectura del sistema: módulos, endpoints REST, eventos asíncronos (Kafka/RabbitMQ/SNS-SQS) y llamadas síncronas HTTP entre módulos. Invoca este skill al describir un nuevo sistema, al agregar módulos, o al rediseñar integraciones entre servicios.'
|
|
4
|
+
argument-hint: 'Describe el sistema o los módulos que necesitas (ej: tienda online con pedidos, pagos y notificaciones)'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Eres un arquitecto de software experto en DDD y arquitectura hexagonal trabajando en el proyecto **<%= projectName %>**.
|
|
8
|
+
|
|
9
|
+
Tu tarea es construir o actualizar el `system.yaml` del proyecto.
|
|
10
|
+
|
|
11
|
+
Documentos de referencia del proyecto:
|
|
12
|
+
- [AGENTS.md](/AGENTS.md) — patrones y convenciones de este proyecto eva4j
|
|
13
|
+
- [system.yaml](/system.yaml) — archivo actual (léelo antes de proponer cambios)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Cuando usar este skill
|
|
18
|
+
|
|
19
|
+
- Diseñar la arquitectura inicial de un sistema nuevo
|
|
20
|
+
- Agregar módulos a un proyecto existente
|
|
21
|
+
- Definir integraciones asíncronas (Kafka/RabbitMQ) entre módulos
|
|
22
|
+
- Definir llamadas síncronas HTTP entre módulos
|
|
23
|
+
- Revisar o refactorizar la estructura de módulos
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Rol y objetivo
|
|
28
|
+
|
|
29
|
+
Producir un `system.yaml` completo, correcto y listo para ejecutar `eva generate system`.
|
|
30
|
+
|
|
31
|
+
El archivo describe **qué módulos existen y cómo se comunican** — NO contiene entidades, campos ni lógica de negocio. Eso es territorio de `domain.yaml`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Paso 1 — Recopilar información
|
|
36
|
+
|
|
37
|
+
Si el usuario no proveyó todos los datos, **pregunta** antes de generar:
|
|
38
|
+
|
|
39
|
+
1. **¿Usa mensajería asíncrona?** Si sí: `kafka` | `rabbitmq` | `sns-sqs`
|
|
40
|
+
2. **Lista de módulos** con su responsabilidad (nombre en plural, kebab-case)
|
|
41
|
+
3. **Endpoints REST** que expone cada módulo (método + path + caso de uso)
|
|
42
|
+
4. **Flujos async**: qué módulo publica qué evento, quiénes lo consumen
|
|
43
|
+
5. **Llamadas sync**: qué módulo llama a quién y usando qué endpoint
|
|
44
|
+
|
|
45
|
+
> Si el `system.yaml` ya tiene contenido, léelo primero y pregunta solo por lo que falta o cambia.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Paso 2 — Estructura del system.yaml
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
system:
|
|
53
|
+
name: <%= projectName %>
|
|
54
|
+
groupId: <%= groupId %>
|
|
55
|
+
javaVersion: <%= javaVersion %>
|
|
56
|
+
springBootVersion: <%= springBootVersion %>
|
|
57
|
+
database: <%= databaseType %>
|
|
58
|
+
|
|
59
|
+
messaging: # Omitir sección completa si no hay mensajería
|
|
60
|
+
enabled: true
|
|
61
|
+
broker: kafka # kafka | rabbitmq | sns-sqs (solo kafka soportado hoy)
|
|
62
|
+
kafka:
|
|
63
|
+
bootstrapServers: localhost:9092
|
|
64
|
+
defaultGroupId: <%= projectName %>
|
|
65
|
+
topicPrefix: <%= projectName %> # opcional — prefixa todos los topics
|
|
66
|
+
|
|
67
|
+
modules:
|
|
68
|
+
- name: orders # plural, kebab-case
|
|
69
|
+
description: "Gestión del ciclo de vida de pedidos"
|
|
70
|
+
exposes:
|
|
71
|
+
- method: GET # GET | POST | PUT | PATCH | DELETE
|
|
72
|
+
path: /orders/{id}
|
|
73
|
+
useCase: GetOrder # PascalCase — alimenta endpoints: en domain.yaml
|
|
74
|
+
description: "Obtener pedido por ID"
|
|
75
|
+
- method: GET
|
|
76
|
+
path: /orders
|
|
77
|
+
useCase: FindAllOrders
|
|
78
|
+
description: "Listar pedidos con filtros y paginación"
|
|
79
|
+
- method: POST
|
|
80
|
+
path: /orders
|
|
81
|
+
useCase: CreateOrder
|
|
82
|
+
description: "Crear nuevo pedido"
|
|
83
|
+
- method: PUT
|
|
84
|
+
path: /orders/{id}/confirm
|
|
85
|
+
useCase: ConfirmOrder
|
|
86
|
+
description: "Confirmar pedido pendiente"
|
|
87
|
+
|
|
88
|
+
- name: notifications
|
|
89
|
+
description: "Envío de notificaciones"
|
|
90
|
+
# Sin endpoints REST — solo consume eventos
|
|
91
|
+
|
|
92
|
+
integrations:
|
|
93
|
+
async:
|
|
94
|
+
- event: OrderPlacedEvent # PascalCase, tiempo pasado, sufijo Event
|
|
95
|
+
producer: orders
|
|
96
|
+
topic: ORDER_PLACED # SCREAMING_SNAKE_CASE
|
|
97
|
+
consumers:
|
|
98
|
+
- module: payments
|
|
99
|
+
- module: notifications
|
|
100
|
+
|
|
101
|
+
sync:
|
|
102
|
+
- caller: orders # módulo que hace la llamada
|
|
103
|
+
calls: customers # módulo destino
|
|
104
|
+
port: CustomerService # PascalCase + sufijo Service
|
|
105
|
+
using:
|
|
106
|
+
- GET /customers/{id} # debe existir en exposes: de 'customers'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Paso 3 — Reglas obligatorias
|
|
112
|
+
|
|
113
|
+
### Convenciones de nombres
|
|
114
|
+
|
|
115
|
+
| Elemento | Convención | Ejemplo |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| Módulos | plural, kebab-case | `orders`, `order-items`, `product-catalog` |
|
|
118
|
+
| Eventos | PascalCase + tiempo pasado + sufijo `Event` | `OrderPlacedEvent` ✅ `PlaceOrderEvent` ❌ |
|
|
119
|
+
| Topics Kafka | SCREAMING_SNAKE_CASE | `ORDER_PLACED` |
|
|
120
|
+
| Port names | PascalCase + sufijo `Service` | `CustomerService` |
|
|
121
|
+
| useCases | PascalCase, verbo + sustantivo | `CreateOrder`, `GetCustomer`, `FindAllOrders` |
|
|
122
|
+
|
|
123
|
+
### Restricciones estructurales
|
|
124
|
+
|
|
125
|
+
- ❌ **Sin dependencias circulares síncronas** — si `A` llama a `B`, `B` no puede llamar a `A`
|
|
126
|
+
- ❌ **Sin campos de dominio** — entidades, campos, enums → van en `domain.yaml`
|
|
127
|
+
- ✅ Cada módulo tiene **una sola responsabilidad**
|
|
128
|
+
- ✅ `calls.using:` solo referencia endpoints declarados en `exposes:` del módulo destino
|
|
129
|
+
- ✅ `consumers[].module` debe existir en `modules:`
|
|
130
|
+
- ✅ Módulos pasivos (notificaciones, auditoría, reportes) son **consumidores**, nunca `caller`
|
|
131
|
+
|
|
132
|
+
### useCases — patrones de nombres
|
|
133
|
+
|
|
134
|
+
Los nombres de `useCase` siguen el patrón **`Verbo + Sustantivo`** en PascalCase.
|
|
135
|
+
|
|
136
|
+
#### Verbos comunes por tipo de operación
|
|
137
|
+
|
|
138
|
+
| Tipo de operación | Verbos recomendados | Ejemplo |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| Crear un recurso | `Create` | `CreateOrder`, `CreateCustomer` |
|
|
141
|
+
| Actualizar datos generales | `Update` | `UpdateOrder`, `UpdateCustomerAddress` |
|
|
142
|
+
| Eliminar un recurso | `Delete` | `DeleteOrder`, `DeleteProduct` |
|
|
143
|
+
| Obtener uno por ID | `Get` | `GetOrder`, `GetCustomerById` |
|
|
144
|
+
| Listar con filtros/paginación | `FindAll` | `FindAllOrders`, `FindAllPendingPayments` |
|
|
145
|
+
| Transición de estado de negocio | `Confirm`, `Cancel`, `Approve`, `Reject`, `Activate`, `Deactivate`, `Suspend`, `Close`, `Complete`, `Submit`, `Publish`, `Archive`, `Restore` | `ConfirmOrder`, `CancelPayment`, `ApproveRefund` |
|
|
146
|
+
| Acción de negocio puntual | `Send`, `Process`, `Calculate`, `Generate`, `Assign`, `Transfer`, `Notify`, `Validate` | `SendNotification`, `ProcessPayment`, `AssignCourier` |
|
|
147
|
+
| Búsqueda especializada | `Search`, `Find`, `Lookup` | `SearchProductsByCategory`, `FindOrdersByCustomer` |
|
|
148
|
+
|
|
149
|
+
#### Regla de generación de código
|
|
150
|
+
|
|
151
|
+
eva4j distingue dos categorías al leer el `useCase`:
|
|
152
|
+
|
|
153
|
+
**Casos de uso CRUD estándar** — el generador produce la implementación completa del handler:
|
|
154
|
+
|
|
155
|
+
| useCase (patrón) | HTTP | Implementación generada |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `Create{Aggregate}` | POST `/resource` | Handler completo con repositorio |
|
|
158
|
+
| `Update{Aggregate}` | PUT `/resource/{id}` | Handler completo con repositorio |
|
|
159
|
+
| `Delete{Aggregate}` | DELETE `/resource/{id}` | Handler completo con repositorio |
|
|
160
|
+
| `Get{Aggregate}` | GET `/resource/{id}` | Handler completo con repositorio |
|
|
161
|
+
| `FindAll{Aggregate}s` | GET `/resource` | Handler completo con repositorio |
|
|
162
|
+
|
|
163
|
+
**Casos de uso de negocio** — el generador produce un **scaffold** con `throw new UnsupportedOperationException(...)` que el desarrollador implementa:
|
|
164
|
+
|
|
165
|
+
```java
|
|
166
|
+
// Ejemplo de scaffold generado para ConfirmOrder
|
|
167
|
+
public class ConfirmOrderCommandHandler implements CommandHandler<ConfirmOrderCommand, Void> {
|
|
168
|
+
@Override
|
|
169
|
+
public Void handle(ConfirmOrderCommand command) {
|
|
170
|
+
throw new UnsupportedOperationException("ConfirmOrderCommandHandler not implemented yet");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Regla práctica:** usa los nombres CRUD para operaciones simples de persistencia. Para cualquier lógica de negocio real (transiciones de estado, cálculos, flujos complejos) usa un nombre descriptivo — el scaffold te recuerda que debes implementarlo.
|
|
176
|
+
|
|
177
|
+
#### Ejemplos completos por módulo
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
# orders — mezcla de CRUD y casos de uso de negocio
|
|
181
|
+
exposes:
|
|
182
|
+
- method: GET
|
|
183
|
+
path: /orders/{id}
|
|
184
|
+
useCase: GetOrder # ← CRUD: implementación completa
|
|
185
|
+
- method: GET
|
|
186
|
+
path: /orders
|
|
187
|
+
useCase: FindAllOrders # ← CRUD: implementación completa
|
|
188
|
+
- method: POST
|
|
189
|
+
path: /orders
|
|
190
|
+
useCase: CreateOrder # ← CRUD: implementación completa
|
|
191
|
+
- method: PUT
|
|
192
|
+
path: /orders/{id}/confirm
|
|
193
|
+
useCase: ConfirmOrder # ← Negocio: scaffold
|
|
194
|
+
- method: PUT
|
|
195
|
+
path: /orders/{id}/cancel
|
|
196
|
+
useCase: CancelOrder # ← Negocio: scaffold
|
|
197
|
+
- method: PUT
|
|
198
|
+
path: /orders/{id}/assign-courier
|
|
199
|
+
useCase: AssignCourier # ← Negocio: scaffold
|
|
200
|
+
|
|
201
|
+
# payments — transiciones y acciones
|
|
202
|
+
exposes:
|
|
203
|
+
- method: POST
|
|
204
|
+
path: /payments
|
|
205
|
+
useCase: CreatePayment # ← CRUD: implementación completa
|
|
206
|
+
- method: GET
|
|
207
|
+
path: /payments/{id}
|
|
208
|
+
useCase: GetPayment # ← CRUD: implementación completa
|
|
209
|
+
- method: POST
|
|
210
|
+
path: /payments/{id}/refund
|
|
211
|
+
useCase: RefundPayment # ← Negocio: scaffold
|
|
212
|
+
- method: POST
|
|
213
|
+
path: /payments/{id}/process
|
|
214
|
+
useCase: ProcessPayment # ← Negocio: scaffold
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Mensajería
|
|
218
|
+
|
|
219
|
+
- Solo `kafka` está implementado actualmente; `rabbitmq` y `sns-sqs` generan warning
|
|
220
|
+
- Los **campos** de los eventos NO van en `system.yaml` → se declaran en `domain.yaml → events[].fields` del productor
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Paso 4 — Checklist de validación interna
|
|
225
|
+
|
|
226
|
+
Antes de proponer el `system.yaml`, verifica:
|
|
227
|
+
|
|
228
|
+
- [ ] Módulos en plural kebab-case
|
|
229
|
+
- [ ] Eventos en tiempo pasado con sufijo `Event`
|
|
230
|
+
- [ ] Sin dependencias circulares síncronas
|
|
231
|
+
- [ ] Todos los `consumers[].module` existen en `modules:`
|
|
232
|
+
- [ ] Todos los `calls.using:` existen en `exposes:` del módulo destino
|
|
233
|
+
- [ ] Módulos pasivos no son `caller`
|
|
234
|
+
- [ ] `useCases` en PascalCase
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Paso 5 — Presentar el resultado
|
|
239
|
+
|
|
240
|
+
1. Muestra el `system.yaml` completo en un bloque de código YAML
|
|
241
|
+
2. Explica brevemente las decisiones no obvias (ej. por qué un flujo es async y no sync)
|
|
242
|
+
3. Menciona advertencias si detectas módulos muy acoplados, responsabilidades difusas o ciclos potenciales
|
|
243
|
+
4. Indica el comando siguiente: `eva generate system`
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Ciclo de refinamiento
|
|
248
|
+
|
|
249
|
+
Después de entregar el `system.yaml` v1, el usuario puede pedir ajustes:
|
|
250
|
+
- Aplica el **cambio mínimo** necesario (no rehaces todo el archivo)
|
|
251
|
+
- Vuelve a validar el checklist del Paso 4
|
|
252
|
+
- Entrega solo el diff explicado, no el archivo completo de nuevo (salvo que se pida)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# system.yaml
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Archivo de arquitectura del sistema — System-First Development con eva4j
|
|
4
|
+
#
|
|
5
|
+
# Describe qué módulos existen, qué exponen y cómo se comunican entre sí.
|
|
6
|
+
# Los campos de dominio (aggregates, events, ports) se declaran en domain.yaml.
|
|
7
|
+
#
|
|
8
|
+
# Comandos disponibles:
|
|
9
|
+
# eva system validate → valida coherencia del grafo (referencias, ciclos)
|
|
10
|
+
# eva generate system → genera módulos + domain.yaml con endpoints: pre-generado
|
|
11
|
+
# eva system diagram → genera diagrama Mermaid del sistema
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
system:
|
|
15
|
+
name: <%= projectName %>
|
|
16
|
+
groupId: <%= groupId %>
|
|
17
|
+
javaVersion: <%= javaVersion %>
|
|
18
|
+
springBootVersion: <%= springBootVersion %>
|
|
19
|
+
database: <%= databaseType %>
|
|
20
|
+
|
|
21
|
+
# messaging: # Descomentar si el sistema usa mensajería asíncrona
|
|
22
|
+
# enabled: true
|
|
23
|
+
# broker: kafka # kafka | rabbitmq | sns-sqs
|
|
24
|
+
# kafka:
|
|
25
|
+
# bootstrapServers: localhost:9092
|
|
26
|
+
# defaultGroupId: <%= projectName %>
|
|
27
|
+
# topicPrefix: <%= projectName %> # prefija todos los topics: <%= projectName %>.ORDER_PLACED
|
|
28
|
+
|
|
29
|
+
modules:
|
|
30
|
+
# Declara cada módulo del sistema (nombre en plural, kebab-case).
|
|
31
|
+
# eva generate system genera por cada módulo: carpeta + domain.yaml con endpoints: pre-generado.
|
|
32
|
+
#
|
|
33
|
+
# - name: orders
|
|
34
|
+
# description: "Gestión del ciclo de vida de pedidos"
|
|
35
|
+
# exposes:
|
|
36
|
+
# - method: GET
|
|
37
|
+
# path: /orders/{id}
|
|
38
|
+
# useCase: GetOrder # PascalCase — alimenta endpoints: en domain.yaml
|
|
39
|
+
# description: "Obtener pedido por ID"
|
|
40
|
+
# - method: GET
|
|
41
|
+
# path: /orders
|
|
42
|
+
# useCase: FindAllOrders
|
|
43
|
+
# description: "Listar pedidos con filtros y paginación"
|
|
44
|
+
# - method: POST
|
|
45
|
+
# path: /orders
|
|
46
|
+
# useCase: CreateOrder
|
|
47
|
+
# description: "Crear nuevo pedido"
|
|
48
|
+
# - method: PUT
|
|
49
|
+
# path: /orders/{id}/confirm
|
|
50
|
+
# useCase: ConfirmOrder
|
|
51
|
+
# description: "Confirmar pedido pendiente"
|
|
52
|
+
# - method: PUT
|
|
53
|
+
# path: /orders/{id}/cancel
|
|
54
|
+
# useCase: CancelOrder
|
|
55
|
+
# description: "Cancelar pedido (PENDING o CONFIRMED)"
|
|
56
|
+
#
|
|
57
|
+
# - name: customers
|
|
58
|
+
# description: "Registro y gestión de clientes"
|
|
59
|
+
# exposes:
|
|
60
|
+
# - method: GET
|
|
61
|
+
# path: /customers/{id}
|
|
62
|
+
# useCase: GetCustomer
|
|
63
|
+
# description: "Obtener cliente por ID"
|
|
64
|
+
# - method: POST
|
|
65
|
+
# path: /customers
|
|
66
|
+
# useCase: CreateCustomer
|
|
67
|
+
# description: "Registrar nuevo cliente"
|
|
68
|
+
#
|
|
69
|
+
# - name: notifications
|
|
70
|
+
# description: "Envío de notificaciones"
|
|
71
|
+
# # Sin endpoints REST — solo consume eventos
|
|
72
|
+
|
|
73
|
+
# integrations:
|
|
74
|
+
# async:
|
|
75
|
+
# # Eventos asíncronos entre módulos.
|
|
76
|
+
# # Los campos del evento se declaran en domain.yaml → events[].fields del productor.
|
|
77
|
+
# - event: OrderPlacedEvent
|
|
78
|
+
# producer: orders
|
|
79
|
+
# topic: ORDER_PLACED
|
|
80
|
+
# consumers:
|
|
81
|
+
# - module: payments
|
|
82
|
+
# - module: notifications
|
|
83
|
+
#
|
|
84
|
+
# - event: PaymentProcessedEvent
|
|
85
|
+
# producer: payments
|
|
86
|
+
# topic: PAYMENT_PROCESSED
|
|
87
|
+
# consumers:
|
|
88
|
+
# - module: orders
|
|
89
|
+
#
|
|
90
|
+
# sync:
|
|
91
|
+
# # Llamadas HTTP síncronas entre módulos.
|
|
92
|
+
# # El shape del DTO de respuesta se declara en domain.yaml → ports[] del caller.
|
|
93
|
+
# - caller: orders
|
|
94
|
+
# calls: customers
|
|
95
|
+
# port: CustomerService
|
|
96
|
+
# using:
|
|
97
|
+
# - GET /customers/{id}
|