eva4j 1.0.13 → 1.0.15
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 +314 -10
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +576 -10
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +318 -13
- package/bin/eva4j.js +34 -0
- package/config/defaults.json +1 -0
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +994 -0
- package/docs/commands/GENERATE_ENTITIES.md +795 -6
- 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/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +332 -0
- package/package.json +2 -1
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +7 -3
- package/src/commands/detach.js +1 -0
- package/src/commands/evaluate-system.js +610 -0
- package/src/commands/generate-entities.js +1331 -49
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +98 -11
- package/src/generators/base-generator.js +8 -1
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +10 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +970 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +434 -0
- package/src/utils/yaml-to-entity.js +302 -8
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +8 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
- package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
- 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/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -8
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +13 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +21 -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/crud/UpdateCommand.java.ejs +4 -0
- package/templates/evaluate/report.html.ejs +1363 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +16 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
# Command `evaluate system`
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Description and purpose](#1-description-and-purpose)
|
|
8
|
+
2. [Syntax and options](#2-syntax-and-options)
|
|
9
|
+
3. [system.yaml structure required](#3-systemyaml-structure-required)
|
|
10
|
+
4. [system.yaml evaluation criteria (S1–S5)](#4-systemyaml-evaluation-criteria-s1s5)
|
|
11
|
+
- [S1 — Module integrity](#s1--module-integrity)
|
|
12
|
+
- [S2 — Async event graph integrity](#s2--async-event-graph-integrity)
|
|
13
|
+
- [S3 — Sync call integrity](#s3--sync-call-integrity)
|
|
14
|
+
- [S4 — Endpoint coherence](#s4--endpoint-coherence)
|
|
15
|
+
- [S5 — Global system coherence](#s5--global-system-coherence)
|
|
16
|
+
5. [Domain evaluation criteria (C1–C4) — `--domain`](#5-domain-evaluation-criteria-c1c4----domain)
|
|
17
|
+
- [C1 — Kafka event contracts](#c1--kafka-event-contracts)
|
|
18
|
+
- [C2 — Behavior gaps](#c2--behavior-gaps)
|
|
19
|
+
- [C3 — Cross-reference integrity](#c3--cross-reference-integrity)
|
|
20
|
+
- [C4 — Audit & traceability](#c4--audit--traceability)
|
|
21
|
+
6. [Score calculation](#6-score-calculation)
|
|
22
|
+
7. [Report output](#7-report-output)
|
|
23
|
+
8. [Practical examples with real findings](#8-practical-examples-with-real-findings)
|
|
24
|
+
9. [Common errors and how to fix them](#9-common-errors-and-how-to-fix-them)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Description and purpose
|
|
29
|
+
|
|
30
|
+
`evaluate system` statically analyzes a `system.yaml` file to detect architectural problems in a microservices design **before writing a single line of Java code**.
|
|
31
|
+
|
|
32
|
+
The command is domain-agnostic: it works for any system described in `system.yaml`, whether it's a cinema booking platform, an e-commerce system, a fintech application, or any other domain.
|
|
33
|
+
|
|
34
|
+
**What it produces:**
|
|
35
|
+
|
|
36
|
+
- A **quality score** (0–100%) based on checks passed vs. total
|
|
37
|
+
- A list of **critical errors** (broken references, self-loops, duplicate routes)
|
|
38
|
+
- A list of **warnings** (potential design problems that aren't necessarily wrong)
|
|
39
|
+
- A list of **info notes** (observations that do not affect the score)
|
|
40
|
+
- A list of **passed validations** (proof that good practices are in place)
|
|
41
|
+
- An **interactive HTML report** served on a local HTTP server
|
|
42
|
+
- A **`assets/system-evaluation.md`** file with only errors and warnings for quick review
|
|
43
|
+
|
|
44
|
+
The HTML report contains four interactive tabs:
|
|
45
|
+
|
|
46
|
+
| Tab | Contents |
|
|
47
|
+
|-----|----------|
|
|
48
|
+
| **Validación** | Score cards, collapsible sections for errors / warnings / info / passed |
|
|
49
|
+
| **Simulador de flujos** | Step-by-step playback of each async event flow |
|
|
50
|
+
| **Arquitectura** | Module dependency explorer, sync dependency cards, Kafka topic map, interactive network diagram |
|
|
51
|
+
| **Dominio** | Per-module Mermaid diagrams (only with `--domain` flag) |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2. Syntax and options
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
eva evaluate system
|
|
59
|
+
eva evaluate system --port 8080 # serve the report on a custom port (default: 3000)
|
|
60
|
+
eva evaluate system --output ./report.html # write HTML to a custom path
|
|
61
|
+
eva evaluate system --domain # also validate domain.yaml files in system/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Options
|
|
65
|
+
|
|
66
|
+
| Option | Default | Description |
|
|
67
|
+
|--------|---------|-------------|
|
|
68
|
+
| `--port <n>` | `3000` | Port for the local HTTP preview server |
|
|
69
|
+
| `--output <path>` | `./system-report.html` | Where to write the generated HTML file |
|
|
70
|
+
| `--domain` | off | Also load and cross-validate domain YAML files from `system/` |
|
|
71
|
+
|
|
72
|
+
### Requirements
|
|
73
|
+
|
|
74
|
+
- Must be run from a directory containing a `system/system.yaml` file
|
|
75
|
+
- No eva4j project scaffold is required — `system.yaml` can be a standalone design file
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. system.yaml structure required
|
|
80
|
+
|
|
81
|
+
The minimal structure the evaluator expects:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
system:
|
|
85
|
+
name: my-system # used as report title
|
|
86
|
+
|
|
87
|
+
messaging:
|
|
88
|
+
enabled: true
|
|
89
|
+
broker: kafka
|
|
90
|
+
kafka:
|
|
91
|
+
topicPrefix: myapp # used by S2-007 topic prefix check
|
|
92
|
+
|
|
93
|
+
modules:
|
|
94
|
+
- name: orders # unique module identifier
|
|
95
|
+
description: "..." # required by S1-003
|
|
96
|
+
exposes: # REST endpoints this module offers
|
|
97
|
+
- method: POST
|
|
98
|
+
path: /orders
|
|
99
|
+
useCase: CreateOrder
|
|
100
|
+
description: "..." # required by S4-004
|
|
101
|
+
- method: GET
|
|
102
|
+
path: /orders/{id}
|
|
103
|
+
useCase: GetOrder
|
|
104
|
+
description: "..."
|
|
105
|
+
|
|
106
|
+
integrations:
|
|
107
|
+
async:
|
|
108
|
+
- event: OrderCreatedEvent # must follow PascalCase + Event suffix (S2-006)
|
|
109
|
+
producer: orders
|
|
110
|
+
topic: ORDER_CREATED
|
|
111
|
+
consumers:
|
|
112
|
+
- module: payments
|
|
113
|
+
useCase: HandleOrderCreated
|
|
114
|
+
- module: notifications
|
|
115
|
+
useCase: NotifyOrderCreated
|
|
116
|
+
|
|
117
|
+
sync:
|
|
118
|
+
- caller: payments
|
|
119
|
+
calls: orders
|
|
120
|
+
port: OrderService
|
|
121
|
+
using:
|
|
122
|
+
- GET /orders/{id} # must exist in orders.exposes[] (S3-002)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
All fields are optional except `modules[].name`. The evaluator gracefully handles missing sections.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 4. system.yaml evaluation criteria (S1–S5)
|
|
130
|
+
|
|
131
|
+
These rules evaluate **only** `system.yaml`. For the domain-level criteria applied when using `--domain`, see [section 5](#5-domain-evaluation-criteria-c1c4----domain).
|
|
132
|
+
|
|
133
|
+
The evaluator runs **5 rule groups (S1–S5)** with a total of **20 rules** across three severity levels:
|
|
134
|
+
|
|
135
|
+
| Severity | Symbol | Affects score | Description |
|
|
136
|
+
|----------|--------|--------------|-------------|
|
|
137
|
+
| **error** | 🔴 | Yes (counts as 1) | Must be fixed |
|
|
138
|
+
| **warning** | 🟡 | Yes (counts as 0.5) | Should be reviewed |
|
|
139
|
+
| **info** | 🔵 | No | Observation only |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### S1 — Module integrity
|
|
144
|
+
|
|
145
|
+
Verifies that all modules declared in `modules[]` have defined responsibilities and all modules referenced in `integrations` are declared.
|
|
146
|
+
|
|
147
|
+
| Rule | Severity | Description |
|
|
148
|
+
|------|----------|-------------|
|
|
149
|
+
| S1-001 | 🔴 error | Module referenced in `integrations` but not declared in `modules[]` |
|
|
150
|
+
| S1-002 | 🔴 error | Module with no responsibilities — no exposes, no events produced or consumed |
|
|
151
|
+
| S1-003 | 🟡 warning | Module without a `description` field |
|
|
152
|
+
| S1-004 | 🟡 warning | Purely reactive module (only consumes events) not documented explicitly in its description |
|
|
153
|
+
|
|
154
|
+
#### S1-001 — Undeclared module referenced in integrations
|
|
155
|
+
|
|
156
|
+
Covers producers, consumers, sync callers, and sync callees.
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
# ❌ ERROR — 'billing' is not declared in modules[]
|
|
160
|
+
- event: InvoiceCreatedEvent
|
|
161
|
+
producer: billing
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Message:** `[S1-001] Módulo 'billing' referenciado en integrations pero no declarado en modules[]`
|
|
165
|
+
|
|
166
|
+
**Fix:** Add the missing module to `modules[]` or correct the name typo.
|
|
167
|
+
|
|
168
|
+
#### S1-002 — Module with no responsibilities
|
|
169
|
+
|
|
170
|
+
A module that doesn't expose endpoints, doesn't produce events, and doesn't consume events serves no purpose in the design.
|
|
171
|
+
|
|
172
|
+
**Message:** `[S1-002] Módulo 'reporting' no tiene ninguna responsabilidad — no expone endpoints, no produce ni consume eventos`
|
|
173
|
+
|
|
174
|
+
**Fix:** Add endpoints to `exposes[]`, connect it to an async event, or remove the module.
|
|
175
|
+
|
|
176
|
+
#### S1-003 — Module without description
|
|
177
|
+
|
|
178
|
+
**Message:** `[S1-003] Módulo 'payments' no tiene campo description declarado`
|
|
179
|
+
|
|
180
|
+
**Fix:** Add a `description` field summarizing the module's responsibility.
|
|
181
|
+
|
|
182
|
+
#### S1-004 — Purely reactive module not documented
|
|
183
|
+
|
|
184
|
+
A module that only consumes events (no REST endpoints, no events produced) should say so explicitly in its description.
|
|
185
|
+
|
|
186
|
+
**Message:** `[S1-004] Módulo 'notifications' es puramente reactivo (solo consume eventos) pero su description no lo documenta explícitamente`
|
|
187
|
+
|
|
188
|
+
**Fix:** Add words like "consumes", "reacts to", or "event-driven" to the description.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### S2 — Async event graph integrity
|
|
193
|
+
|
|
194
|
+
Verifies that the producer → consumer graph declared in `integrations.async` is coherent: no orphan events, no topic collisions, no self-loops.
|
|
195
|
+
|
|
196
|
+
| Rule | Severity | Description |
|
|
197
|
+
|------|----------|-------------|
|
|
198
|
+
| S2-001 | 🔴 error | Event declared in `integrations.async` with no consumers |
|
|
199
|
+
| S2-002 | 🔴 error | Same `topic` value declared for two different events |
|
|
200
|
+
| S2-003 | 🔴 error | Module listed as consumer of its own event (self-loop) |
|
|
201
|
+
| S2-004 | 🟡 warning | Module that produces events but consumes none |
|
|
202
|
+
| S2-005 | 🟡 warning | Module that consumes events but produces none |
|
|
203
|
+
| S2-006 | 🟡 warning | Event name not following PascalCase + `Event` suffix convention |
|
|
204
|
+
| S2-007 | 🔵 info | Topic name does not include the prefix declared in `messaging.kafka.topicPrefix` |
|
|
205
|
+
|
|
206
|
+
#### S2-001 — Event with no consumers
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
# ❌ ERROR — no consumers declared
|
|
210
|
+
- event: OrderShippedEvent
|
|
211
|
+
producer: shipping
|
|
212
|
+
topic: ORDER_SHIPPED
|
|
213
|
+
consumers: []
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Message:** `[S2-001] Evento 'OrderShippedEvent' declarado en integrations.async sin consumidores`
|
|
217
|
+
|
|
218
|
+
#### S2-002 — Duplicate topic value
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
# ❌ ERROR — both events share the same topic
|
|
222
|
+
- event: OrderCreatedEvent
|
|
223
|
+
topic: ORDER_EVENTS
|
|
224
|
+
- event: OrderCancelledEvent
|
|
225
|
+
topic: ORDER_EVENTS # ← collision
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Message:** `[S2-002] Topic 'ORDER_EVENTS' está declarado para dos eventos distintos: 'OrderCreatedEvent' y 'OrderCancelledEvent'`
|
|
229
|
+
|
|
230
|
+
#### S2-003 — Self-loop (module consuming its own event)
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
# ❌ ERROR — orders producing AND consuming its own event
|
|
234
|
+
- event: OrderCreatedEvent
|
|
235
|
+
producer: orders
|
|
236
|
+
consumers:
|
|
237
|
+
- module: orders # ← self-loop
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Message:** `[S2-003] Módulo 'orders' está listado como consumidor de su propio evento 'OrderCreatedEvent' (self-loop)`
|
|
241
|
+
|
|
242
|
+
#### S2-004/S2-005 — Unbalanced producer/consumer roles
|
|
243
|
+
|
|
244
|
+
- `[S2-004]` — Module produces events but never consumes any (may be intentional)
|
|
245
|
+
- `[S2-005]` — Module consumes events but never produces any (may be intentional for sinks like notifications)
|
|
246
|
+
|
|
247
|
+
#### S2-006 — Event name convention
|
|
248
|
+
|
|
249
|
+
Event names must follow `PascalCase` with an `Event` suffix.
|
|
250
|
+
|
|
251
|
+
**Examples of violations:** `orderCreated`, `ORDER_CREATED`, `OrderCreated` (missing `Event`), `Order_Created_Event`
|
|
252
|
+
|
|
253
|
+
**Message:** `[S2-006] Nombre de evento 'orderCreated' no sigue la convención PascalCase con sufijo 'Event'`
|
|
254
|
+
|
|
255
|
+
#### S2-007 — Topic without configured prefix (info)
|
|
256
|
+
|
|
257
|
+
When `messaging.kafka.topicPrefix` is declared, every topic name should include it for consistency.
|
|
258
|
+
|
|
259
|
+
**Message:** `[S2-007] Topic 'ORDER_CREATED' (evento 'OrderCreatedEvent') no incluye el prefijo configurado 'myapp'`
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### S3 — Sync call integrity
|
|
264
|
+
|
|
265
|
+
Verifies that all synchronous dependencies declared in `integrations.sync` reference existing modules and endpoints, and do not generate circular or excessive coupling.
|
|
266
|
+
|
|
267
|
+
| Rule | Severity | Description |
|
|
268
|
+
|------|----------|-------------|
|
|
269
|
+
| S3-001 | 🔴 error | Sync call to a module that declares no `exposes[]` |
|
|
270
|
+
| S3-002 | 🔴 error | Path in `sync[].using[]` does not exist in target module's `exposes[]` |
|
|
271
|
+
| S3-003 | 🟡 warning | Bidirectional sync coupling — A calls B and B calls A |
|
|
272
|
+
| S3-004 | 🟡 warning | Module with more than 3 distinct outgoing sync dependencies |
|
|
273
|
+
| S3-005 | 🔵 info | Module consulted synchronously but emits no events when its state changes |
|
|
274
|
+
|
|
275
|
+
#### S3-001 — Sync call to module without endpoints
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
# ❌ ERROR — 'notifications' has no exposes[]
|
|
279
|
+
sync:
|
|
280
|
+
- caller: orders
|
|
281
|
+
calls: notifications
|
|
282
|
+
using:
|
|
283
|
+
- POST /notifications
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Message:** `[S3-001] 'orders' llama síncronamente a 'notifications' pero este módulo no declara exposes[]`
|
|
287
|
+
|
|
288
|
+
#### S3-002 — Endpoint not declared in target module
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
sync:
|
|
292
|
+
- caller: payments
|
|
293
|
+
calls: orders
|
|
294
|
+
using:
|
|
295
|
+
- GET /orders/{id}/items # ❌ not in orders.exposes[]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Message:** `[S3-002] Endpoint 'GET /orders/{id}/items' usado por 'payments' no está declarado en exposes[] de 'orders'`
|
|
299
|
+
|
|
300
|
+
**Fix:** Add the endpoint to `orders.exposes[]` or remove it from `using[]`.
|
|
301
|
+
|
|
302
|
+
#### S3-003 — Bidirectional sync coupling
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
# ❌ WARNING — A↔B mutual sync dependency
|
|
306
|
+
sync:
|
|
307
|
+
- caller: orders
|
|
308
|
+
calls: inventory
|
|
309
|
+
- caller: inventory
|
|
310
|
+
calls: orders
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Message:** `[S3-003] Acoplamiento síncrono bidireccional: 'orders' llama a 'inventory' y viceversa`
|
|
314
|
+
|
|
315
|
+
**Fix:** Replace one direction with an async event, or extract the shared data into a third read-model module.
|
|
316
|
+
|
|
317
|
+
#### S3-004 — Too many outgoing sync dependencies
|
|
318
|
+
|
|
319
|
+
A module calling more than 3 distinct modules synchronously is tightly coupled and fragile under partial failures.
|
|
320
|
+
|
|
321
|
+
**Message:** `[S3-004] Módulo 'reservations' tiene 4 dependencias síncronas salientes distintas (>3): screenings, customers, payments, inventory`
|
|
322
|
+
|
|
323
|
+
#### S3-005 — Module consulted synchronously but emits no events (info)
|
|
324
|
+
|
|
325
|
+
When other modules depend synchronously on a module but that module never publishes events, downstream consumers have no way to react to its state changes.
|
|
326
|
+
|
|
327
|
+
**Message:** `[S3-005] Módulo 'movies' es consultado síncronamente pero no emite ningún evento cuando su estado cambia`
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### S4 — Endpoint coherence
|
|
332
|
+
|
|
333
|
+
Verifies that endpoints declared in `modules[].exposes[]` are internally coherent: no route collisions, complete operation pairs, minimal documentation.
|
|
334
|
+
|
|
335
|
+
| Rule | Severity | Description |
|
|
336
|
+
|------|----------|-------------|
|
|
337
|
+
| S4-001 | 🔴 error | Two endpoints with the same HTTP method and path in the same module |
|
|
338
|
+
| S4-002 | 🟡 warning | Module with `PUT /{id}` but no `GET /{id}` for the same resource |
|
|
339
|
+
| S4-003 | 🟡 warning | `DELETE` endpoint exposed without a description indicating physical vs. logical deletion |
|
|
340
|
+
| S4-004 | 🔵 info | Endpoint without a `description` field |
|
|
341
|
+
| S4-005 | 🔵 info | Module with a `POST` creation endpoint but no `GET /{id}` to retrieve the created resource |
|
|
342
|
+
|
|
343
|
+
#### S4-001 — Duplicate route
|
|
344
|
+
|
|
345
|
+
```yaml
|
|
346
|
+
exposes:
|
|
347
|
+
- method: GET
|
|
348
|
+
path: /orders/{id}
|
|
349
|
+
useCase: GetOrder
|
|
350
|
+
- method: GET
|
|
351
|
+
path: /orders/{id} # ❌ duplicate
|
|
352
|
+
useCase: GetOrderDetail
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Message:** `[S4-001] Módulo 'orders' tiene dos endpoints con el mismo método y path: GET /orders/{id}`
|
|
356
|
+
|
|
357
|
+
#### S4-002 — PUT without GET for same resource
|
|
358
|
+
|
|
359
|
+
```yaml
|
|
360
|
+
exposes:
|
|
361
|
+
- method: PUT
|
|
362
|
+
path: /orders/{id}
|
|
363
|
+
useCase: UpdateOrder
|
|
364
|
+
# ❌ no GET /orders/{id} declared
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Message:** `[S4-002] Módulo 'orders' tiene PUT /orders/{id} sin el correspondiente GET /orders/{id}`
|
|
368
|
+
|
|
369
|
+
#### S4-003 — DELETE without description
|
|
370
|
+
|
|
371
|
+
```yaml
|
|
372
|
+
exposes:
|
|
373
|
+
- method: DELETE
|
|
374
|
+
path: /products/{id}
|
|
375
|
+
useCase: DeleteProduct
|
|
376
|
+
# ❌ no description — is this physical or soft delete?
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Message:** `[S4-003] Endpoint DELETE /products/{id} en 'products' no tiene description que indique si el borrado es físico o lógico`
|
|
380
|
+
|
|
381
|
+
**Fix:** Add a description: `"Eliminación lógica: marca el producto como inactivo (soft delete)"` or `"Eliminación física del registro de base de datos"`.
|
|
382
|
+
|
|
383
|
+
#### S4-004 — Endpoint without description (info)
|
|
384
|
+
|
|
385
|
+
**Message:** `[S4-004] Endpoint POST /orders en 'orders' no tiene campo description`
|
|
386
|
+
|
|
387
|
+
#### S4-005 — POST without GET /{id} (info)
|
|
388
|
+
|
|
389
|
+
**Message:** `[S4-005] Módulo 'shipments' tiene POST de creación pero no declara GET /{id} para recuperar el recurso creado`
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
### S5 — Global system coherence
|
|
394
|
+
|
|
395
|
+
Verifies properties that can only be evaluated by observing the entire system: contradictions between configuration and declarations, flows without failure coverage, and disconnected modules.
|
|
396
|
+
|
|
397
|
+
| Rule | Severity | Description |
|
|
398
|
+
|------|----------|-------------|
|
|
399
|
+
| S5-001 | 🟡 warning | `messaging.enabled: false` with async events declared in `integrations.async` |
|
|
400
|
+
| S5-002 | 🟡 warning | Critical business flow with a success event but no corresponding failure event for compensation |
|
|
401
|
+
| S5-003 | 🔵 info | Module handling authentication with no declared integration with any other module |
|
|
402
|
+
| S5-004 | 🔵 info | Module with no connection to the system graph — neither async nor sync |
|
|
403
|
+
|
|
404
|
+
#### S5-001 — Messaging disabled but events declared
|
|
405
|
+
|
|
406
|
+
```yaml
|
|
407
|
+
messaging:
|
|
408
|
+
enabled: false # ❌ contradicts async events below
|
|
409
|
+
|
|
410
|
+
integrations:
|
|
411
|
+
async:
|
|
412
|
+
- event: OrderCreatedEvent
|
|
413
|
+
…
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Message:** `[S5-001] messaging.enabled está en false pero hay 3 eventos declarados en integrations.async`
|
|
417
|
+
|
|
418
|
+
#### S5-002 — Success event without matching failure event
|
|
419
|
+
|
|
420
|
+
When a flow has a success event (`*ConfirmedEvent`, `*ApprovedEvent`, `*PlacedEvent`, `*CompletedEvent`), there should be a failure/compensation event for the same subject so consumers can react to the unhappy path.
|
|
421
|
+
|
|
422
|
+
```yaml
|
|
423
|
+
# ⚠️ WARNING — success exists but no failure counterpart
|
|
424
|
+
- event: PaymentApprovedEvent # ✅ success
|
|
425
|
+
producer: payments
|
|
426
|
+
…
|
|
427
|
+
# If PaymentRejectedEvent or PaymentFailedEvent were missing → S5-002 fires
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Message:** `[S5-002] Evento de éxito 'PaymentApprovedEvent' existe pero no hay un evento de fallo correspondiente para el sujeto 'payment' que permita compensación`
|
|
431
|
+
|
|
432
|
+
#### S5-003 — Auth module without integrations (info)
|
|
433
|
+
|
|
434
|
+
Modules whose names match `auth`, `security`, `identity`, or `session` that have no declared integrations may be siloed or forgotten.
|
|
435
|
+
|
|
436
|
+
**Message:** `[S5-003] Módulo 'auth' parece manejar autenticación/seguridad pero no tiene ninguna integración declarada con otros módulos`
|
|
437
|
+
|
|
438
|
+
#### S5-004 — Isolated module (info)
|
|
439
|
+
|
|
440
|
+
A module with no async events (produced or consumed) and no sync calls (as caller or callee) is completely disconnected from the system graph.
|
|
441
|
+
|
|
442
|
+
**Message:** `[S5-004] Módulo 'reporting' no tiene ninguna conexión al grafo del sistema — ni async ni sync`
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 5. Domain evaluation criteria (--domain)
|
|
447
|
+
|
|
448
|
+
When `--domain` is passed, the evaluator additionally loads a `domain.yaml` file from each module subdirectory under `system/` and cross-validates them against each other and against `system.yaml`.
|
|
449
|
+
|
|
450
|
+
**Requirements:**
|
|
451
|
+
- Each module subdirectory under `system/` must contain a `domain.yaml` file to be included
|
|
452
|
+
- Modules without a `domain.yaml` are silently skipped — their rules will not fire
|
|
453
|
+
- Domain findings appear in the **Dominio** tab of the HTML report
|
|
454
|
+
|
|
455
|
+
Domain rules are organized in **4 rule groups (C1–C4)** with a total of **19 rules**:
|
|
456
|
+
|
|
457
|
+
| Severity | Symbol | Affects score | Description |
|
|
458
|
+
|----------|--------|--------------|-------------|
|
|
459
|
+
| **error** | 🔴 | Yes (counts as 1) | Must be fixed |
|
|
460
|
+
| **warning** | 🟡 | Yes (counts as 0.5) | Should be reviewed |
|
|
461
|
+
| **info** | 🔵 | No | Observation only |
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### C1 — Kafka event contracts
|
|
466
|
+
|
|
467
|
+
Verifies that the producer → consumer graph is coherent at the code level: events declared in `domain.yaml` match what `system.yaml` declares, and field-level contracts are consistent between producers and consumers.
|
|
468
|
+
|
|
469
|
+
| Rule | Severity | Description |
|
|
470
|
+
|------|----------|-------------|
|
|
471
|
+
| C1-001 | 🟡 warning | Domain event produced by a module but has no consumers registered in `system.yaml` |
|
|
472
|
+
| C1-002 | 🔴 error | `listeners[]` references an event that no domain module produces |
|
|
473
|
+
| C1-003 | 🔴 error | Field in `listener.fields` does not exist in the producer event |
|
|
474
|
+
| C1-004 | 🔴 error | Field exists in both producer and consumer but with incompatible types |
|
|
475
|
+
| C1-005 | 🔴 error | `system.yaml` registers a module as consumer but that module has no matching `listener` |
|
|
476
|
+
| C1-006 | 🔴 error | `listener.producer` references the wrong producer module |
|
|
477
|
+
|
|
478
|
+
#### C1-001 — Produced event with no consumers in system.yaml
|
|
479
|
+
|
|
480
|
+
```yaml
|
|
481
|
+
# orders/domain.yaml
|
|
482
|
+
aggregates:
|
|
483
|
+
- name: Order
|
|
484
|
+
events:
|
|
485
|
+
- name: OrderConfirmed # ⚠️ WARNING — not registered in system.yaml
|
|
486
|
+
fields: [...]
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Message:** `[C1-001] El evento 'OrderConfirmed' no tiene consumidores registrados en system.yaml`
|
|
490
|
+
|
|
491
|
+
**Fix:** Add the event to `integrations.async` in `system.yaml` with at least one consumer, or remove it from `domain.yaml` if unused.
|
|
492
|
+
|
|
493
|
+
#### C1-002 — Listener references event no module produces
|
|
494
|
+
|
|
495
|
+
```yaml
|
|
496
|
+
# notifications/domain.yaml
|
|
497
|
+
listeners:
|
|
498
|
+
- event: StockDepletedEvent # ❌ ERROR — no domain.yaml in the system produces this
|
|
499
|
+
producer: inventory
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Message:** `[C1-002] Listener de 'StockDepletedEvent' pero ningún módulo en los domain.yaml lo produce`
|
|
503
|
+
|
|
504
|
+
**Fix:** Declare `StockDepletedEvent` in the `inventory` module's `domain.yaml`, or correct the event name.
|
|
505
|
+
|
|
506
|
+
#### C1-003 — Field in listener missing in producer event
|
|
507
|
+
|
|
508
|
+
```yaml
|
|
509
|
+
# Producer (payments/domain.yaml):
|
|
510
|
+
events:
|
|
511
|
+
- name: PaymentApprovedEvent
|
|
512
|
+
fields:
|
|
513
|
+
- { name: paymentId, type: String }
|
|
514
|
+
|
|
515
|
+
# Consumer (reservations/domain.yaml):
|
|
516
|
+
listeners:
|
|
517
|
+
- event: PaymentApprovedEvent
|
|
518
|
+
fields:
|
|
519
|
+
- { name: approvedAt, type: LocalDateTime } # ❌ ERROR — not in producer
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Message:** `[C1-003] Campo 'approvedAt' en listener de 'PaymentApprovedEvent' no existe en los campos del evento del productor (payments)`
|
|
523
|
+
|
|
524
|
+
**Fix:** Remove the field from the listener, or add it to the producer event declaration.
|
|
525
|
+
|
|
526
|
+
#### C1-004 — Incompatible field types between producer and consumer
|
|
527
|
+
|
|
528
|
+
```yaml
|
|
529
|
+
# Producer declares: amount: BigDecimal
|
|
530
|
+
# Consumer declares: amount: String # ❌ ERROR
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Message:** `[C1-004] Campo 'amount' en listener de 'PaymentApprovedEvent': tipo incompatible — Productor declara 'BigDecimal', listener declara 'String'`
|
|
534
|
+
|
|
535
|
+
**Fix:** Align the field type in the listener with the type declared in the producer event.
|
|
536
|
+
|
|
537
|
+
#### C1-005 — Registered consumer has no listener in domain.yaml
|
|
538
|
+
|
|
539
|
+
```yaml
|
|
540
|
+
# system.yaml registers notifications as consumer:
|
|
541
|
+
consumers:
|
|
542
|
+
- module: notifications
|
|
543
|
+
useCase: NotifyOrderCreated
|
|
544
|
+
# notifications/domain.yaml has no listeners[] for OrderCreatedEvent ❌ ERROR
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Message:** `[C1-005] system.yaml registra 'notifications' como consumidor de 'OrderCreatedEvent' pero el módulo no tiene listener declarado`
|
|
548
|
+
|
|
549
|
+
**Fix:** Add a `listeners[]` entry for `OrderCreatedEvent` in `notifications/domain.yaml`.
|
|
550
|
+
|
|
551
|
+
#### C1-006 — listener.producer references wrong module
|
|
552
|
+
|
|
553
|
+
```yaml
|
|
554
|
+
listeners:
|
|
555
|
+
- event: PaymentApprovedEvent
|
|
556
|
+
producer: orders # ❌ ERROR — this event is actually produced by 'payments'
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Message:** `[C1-006] Listener declara producer: 'orders' pero 'PaymentApprovedEvent' es producido por 'payments'`
|
|
560
|
+
|
|
561
|
+
**Fix:** Update `producer:` to match the actual producing module.
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
### C2 — Behavior gaps
|
|
566
|
+
|
|
567
|
+
Verifies that every transition method and every use case has a traceable activation mechanism — either an HTTP endpoint or an async listener — ensuring no business behavior is accidentally unreachable.
|
|
568
|
+
|
|
569
|
+
| Rule | Severity | Description |
|
|
570
|
+
|------|----------|-------------|
|
|
571
|
+
| C2-001 | 🟡 warning | Transition method with no matching HTTP endpoint or listener |
|
|
572
|
+
| C2-002 | 🔵 info | Listener `useCase` has no equivalent REST endpoint in a module that exposes REST |
|
|
573
|
+
| C2-003 | 🟡 warning | Value in a `*Type` enum with no traceable Kafka event as origin |
|
|
574
|
+
| C2-004 | 🔴 error | Event `triggers[]` references a transition method that does not exist |
|
|
575
|
+
| C2-005 | 🔵 info | Transition method without any associated Domain Event (no `triggers`) |
|
|
576
|
+
|
|
577
|
+
> **Note on C2-001:** Silenced automatically when the transition method already has an event `trigger` declared — the trigger itself is sufficient design evidence.
|
|
578
|
+
|
|
579
|
+
#### C2-001 — Transition with no endpoint or listener
|
|
580
|
+
|
|
581
|
+
```yaml
|
|
582
|
+
enums:
|
|
583
|
+
- name: OrderStatus
|
|
584
|
+
transitions:
|
|
585
|
+
- from: CONFIRMED
|
|
586
|
+
to: CANCELLED
|
|
587
|
+
method: cancel # ⚠️ WARNING — nothing calls 'cancel'
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Message:** `[C2-001] Transición 'cancel' de OrderStatus (CONFIRMED → CANCELLED) no tiene endpoint HTTP ni listener asociado`
|
|
591
|
+
|
|
592
|
+
**Fix:** Add a `POST /orders/{id}/cancel` endpoint or connect it to a listener with `useCase: CancelOrder`.
|
|
593
|
+
|
|
594
|
+
#### C2-004 — Trigger references non-existent transition method
|
|
595
|
+
|
|
596
|
+
```yaml
|
|
597
|
+
events:
|
|
598
|
+
- name: OrderShipped
|
|
599
|
+
triggers:
|
|
600
|
+
- ship # ❌ ERROR — no transition method named 'ship' exists
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Message:** `[C2-004] Evento 'OrderShipped' tiene trigger 'ship' que no corresponde a ningún método de transición`
|
|
604
|
+
|
|
605
|
+
**Fix:** Add a transition with `method: ship`, or update the trigger to reference an existing method.
|
|
606
|
+
|
|
607
|
+
#### C2-005 — Transition without a Domain Event (info)
|
|
608
|
+
|
|
609
|
+
**Message:** `[C2-005] Transición 'confirm' (OrderStatus: PENDING → CONFIRMED) no tiene ningún Domain Event asociado`
|
|
610
|
+
|
|
611
|
+
**Recommendation:** Declare a domain event with `triggers: [confirm]` so downstream modules can react to this state change.
|
|
612
|
+
|
|
613
|
+
#### C2-002 / C2-003 — Completeness checks (info / warning)
|
|
614
|
+
|
|
615
|
+
- `[C2-002]` — Listener `useCase` has no REST equivalent in the same module (info; acceptable when the module is purely event-driven)
|
|
616
|
+
- `[C2-003]` — Value in a `*Type` enum (e.g., `PaymentType.BANK_TRANSFER`) has no Kafka event whose name or fields overlap — suggests the value originates from an external trigger not yet documented
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
### C3 — Cross-reference integrity
|
|
621
|
+
|
|
622
|
+
Verifies that all declared dependencies between modules are consistent: cross-aggregate field references have a corresponding port or listener, port calls target declared endpoints, and there is no hidden circular coupling.
|
|
623
|
+
|
|
624
|
+
| Rule | Severity | Description |
|
|
625
|
+
|------|----------|-------------|
|
|
626
|
+
| C3-001 | 🟡 warning | Field with `reference.module=X` but no port or listener connecting to X |
|
|
627
|
+
| C3-002 | 🔴 error | `port.target` refers to an internal module with no `domain.yaml` loaded |
|
|
628
|
+
| C3-003 | 🟡 warning | Port calls an endpoint not declared in the target module |
|
|
629
|
+
| C3-004 | 🟡 warning | Sync dependency on a module that emits no Kafka events |
|
|
630
|
+
| C3-005 | 🔴 error | Bidirectional sync coupling between two modules (A calls B **and** B calls A) |
|
|
631
|
+
| C3-006 | 🟡 warning | `system.yaml` declares a sync call but the caller module has no matching port |
|
|
632
|
+
|
|
633
|
+
> **Note:** C3-002, C3-003, and C3-004 are skipped for external services — targets ending in `-external` or whose `baseUrl` points to a non-localhost host.
|
|
634
|
+
|
|
635
|
+
#### C3-001 — Cross-aggregate reference without a port or listener
|
|
636
|
+
|
|
637
|
+
```yaml
|
|
638
|
+
# reservations/domain.yaml
|
|
639
|
+
fields:
|
|
640
|
+
- name: customerId
|
|
641
|
+
type: String
|
|
642
|
+
reference:
|
|
643
|
+
aggregate: Customer
|
|
644
|
+
module: customers # ⚠️ WARNING — no port or listener to 'customers'
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Message:** `[C3-001] Campo 'customerId' referencia módulo 'customers' pero no hay port ni listener que conecte con ese módulo`
|
|
648
|
+
|
|
649
|
+
**Fix:** Add a `ports[]` entry targeting `customers` in `reservations/domain.yaml`.
|
|
650
|
+
|
|
651
|
+
#### C3-002 — Port target missing domain.yaml
|
|
652
|
+
|
|
653
|
+
```yaml
|
|
654
|
+
ports:
|
|
655
|
+
- name: findScreeningById
|
|
656
|
+
service: ScreeningService
|
|
657
|
+
target: screenings # ❌ ERROR — 'screenings' in system.yaml but no domain.yaml loaded
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Message:** `[C3-002] Port 'ScreeningService' apunta a 'screenings' que está en system.yaml pero no tiene domain.yaml cargado`
|
|
661
|
+
|
|
662
|
+
**Fix:** Ensure `system/screenings/domain.yaml` exists and is reachable when running `--domain`.
|
|
663
|
+
|
|
664
|
+
#### C3-003 — Port calls undeclared endpoint
|
|
665
|
+
|
|
666
|
+
```yaml
|
|
667
|
+
ports:
|
|
668
|
+
- http: GET /orders/{id}/items # ⚠️ WARNING — not listed in orders.exposes[]
|
|
669
|
+
target: orders
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Message:** `[C3-003] Port 'OrderService' llama 'GET /orders/{id}/items' en 'orders' pero ese endpoint no está declarado en el módulo destino`
|
|
673
|
+
|
|
674
|
+
**Fix:** Add `GET /orders/{id}/items` to `orders.exposes[]` in `system.yaml`, or correct the port path.
|
|
675
|
+
|
|
676
|
+
#### C3-005 — Bidirectional sync coupling
|
|
677
|
+
|
|
678
|
+
**Message:** `[C3-005] Acoplamiento síncrono bidireccional entre 'reservations' y 'payments'`
|
|
679
|
+
|
|
680
|
+
**Fix:** Same as [S3-003](#s3-003--bidirectional-sync-coupling) — break the cycle with an async event or a shared read-model.
|
|
681
|
+
|
|
682
|
+
#### C3-006 — system.yaml sync call without matching port
|
|
683
|
+
|
|
684
|
+
```yaml
|
|
685
|
+
# system.yaml: reservations calls screenings
|
|
686
|
+
# reservations/domain.yaml: no ports[] for 'screenings' ⚠️ WARNING
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
**Message:** `[C3-006] system.yaml declara que 'reservations' llama síncronamente a 'screenings' pero el módulo no tiene port declarado hacia 'screenings'`
|
|
690
|
+
|
|
691
|
+
**Fix:** Add a `ports[]` section in `reservations/domain.yaml` with a method targeting `screenings`.
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
### C4 — Audit & traceability
|
|
696
|
+
|
|
697
|
+
Verifies that critical business entities have change-tracking mechanisms and that data from external bounded contexts is stored in a structured format.
|
|
698
|
+
|
|
699
|
+
> **Critical modules heuristic:** A module is considered critical when its name contains any of: `payment`, `billing`, `order`, `reservation`, `customer`, `user`, or `inventory`.
|
|
700
|
+
|
|
701
|
+
| Rule | Severity | Description |
|
|
702
|
+
|------|----------|-------------|
|
|
703
|
+
| C4-001 | 🟡 warning | Child entity with `cascade: REMOVE` has no audit and no soft delete, while its root entity has audit enabled |
|
|
704
|
+
| C4-002 | 🟡 warning | Root entity in a critical module without `audit.enabled: true` |
|
|
705
|
+
| C4-003 | 🟡 warning | Field with an external-data name typed as plain `String` in a module that declares ports |
|
|
706
|
+
| C4-004 | 🟡 warning | `readOnly` field in a critical module that does not appear in any event of that module |
|
|
707
|
+
|
|
708
|
+
#### C4-001 — Child entity deletable without audit trail
|
|
709
|
+
|
|
710
|
+
```yaml
|
|
711
|
+
entities:
|
|
712
|
+
- name: Order
|
|
713
|
+
isRoot: true
|
|
714
|
+
audit:
|
|
715
|
+
enabled: true
|
|
716
|
+
relationships:
|
|
717
|
+
- type: OneToMany
|
|
718
|
+
target: OrderItem
|
|
719
|
+
cascade: [REMOVE] # root is audited, child is not → ⚠️ WARNING
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
**Message:** `[C4-001] Entidad hija 'OrderItem' tiene cascade REMOVE pero sin audit ni soft delete`
|
|
723
|
+
|
|
724
|
+
**Fix:** Add `audit.enabled: true` or `hasSoftDelete: true` to the `OrderItem` entity.
|
|
725
|
+
|
|
726
|
+
#### C4-002 — Critical root entity without audit
|
|
727
|
+
|
|
728
|
+
**Message:** `[C4-002] Entidad raíz 'Order' en módulo crítico 'orders' no tiene audit.enabled:true`
|
|
729
|
+
|
|
730
|
+
**Fix:**
|
|
731
|
+
```yaml
|
|
732
|
+
entities:
|
|
733
|
+
- name: Order
|
|
734
|
+
isRoot: true
|
|
735
|
+
audit:
|
|
736
|
+
enabled: true
|
|
737
|
+
trackUser: true
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
#### C4-003 — External data stored as unstructured String
|
|
741
|
+
|
|
742
|
+
```yaml
|
|
743
|
+
fields:
|
|
744
|
+
- name: rawData # ⚠️ WARNING — external payload stored as plain String
|
|
745
|
+
type: String
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
**Message:** `[C4-003] Campo 'rawData' almacena datos externos como String no estructurado`
|
|
749
|
+
|
|
750
|
+
**Fix:** Replace with a `nestedType` or a structured Value Object mapping the relevant fields explicitly.
|
|
751
|
+
|
|
752
|
+
#### C4-004 — readOnly field not surfaced in any event
|
|
753
|
+
|
|
754
|
+
```yaml
|
|
755
|
+
fields:
|
|
756
|
+
- name: totalAmount
|
|
757
|
+
type: BigDecimal
|
|
758
|
+
readOnly: true # ⚠️ WARNING — not included in any domain event of this module
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Message:** `[C4-004] Campo readOnly 'totalAmount' en módulo crítico 'reservations' no aparece en ningún evento del módulo`
|
|
762
|
+
|
|
763
|
+
**Fix:** Include `totalAmount` in a relevant domain event, or document why it is intentionally private.
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## 6. Score calculation
|
|
768
|
+
|
|
769
|
+
The score **only** counts errors, warnings, and passing validations. **Info items do not affect the score.**
|
|
770
|
+
|
|
771
|
+
```
|
|
772
|
+
score = round(passed / (passed + errors + warnings × 0.5) × 100)
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
| Score | Color | Interpretation |
|
|
776
|
+
|-------|-------|----------------|
|
|
777
|
+
| > 80% | 🟢 Green | Good architecture — minor issues only |
|
|
778
|
+
| 60–80% | 🟡 Yellow | Moderate issues — review warnings before coding |
|
|
779
|
+
| < 60% | 🔴 Red | Significant problems — resolve errors before proceeding |
|
|
780
|
+
|
|
781
|
+
A score of 100% means zero errors, zero warnings, and at least one passing validation.
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## 7. Report output
|
|
786
|
+
|
|
787
|
+
The command produces three output artifacts:
|
|
788
|
+
|
|
789
|
+
### 1. Console summary
|
|
790
|
+
|
|
791
|
+
```
|
|
792
|
+
✔ Analysis complete!
|
|
793
|
+
|
|
794
|
+
📊 Validation Summary
|
|
795
|
+
────────────────────────────────────────
|
|
796
|
+
🔴 Errors: 0
|
|
797
|
+
🟡 Warnings: 3
|
|
798
|
+
🔵 Info: 12
|
|
799
|
+
🟢 Passed: 11
|
|
800
|
+
📈 Score: 88%
|
|
801
|
+
|
|
802
|
+
Report written to: ./system-report.html
|
|
803
|
+
Evaluation written to: assets/system-evaluation.md
|
|
804
|
+
|
|
805
|
+
🌐 Server running at: http://localhost:3000
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### 2. HTML report (`system-report.html`)
|
|
809
|
+
|
|
810
|
+
Self-contained HTML file with four interactive tabs. Can be shared without a server.
|
|
811
|
+
|
|
812
|
+
### 3. Markdown evaluation (`assets/system-evaluation.md`)
|
|
813
|
+
|
|
814
|
+
A concise file containing only **errors and warnings** — suitable for committing alongside `system.yaml` as a living architecture review document.
|
|
815
|
+
|
|
816
|
+
```markdown
|
|
817
|
+
# Evaluación del sistema — my-system
|
|
818
|
+
|
|
819
|
+
> Generado: 2026-03-13 10:45:00
|
|
820
|
+
> Score de calidad: **88%** 🟢 Bueno
|
|
821
|
+
> 🔴 Errores: 0 | 🟡 Advertencias: 3
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## 🟡 Advertencias
|
|
826
|
+
|
|
827
|
+
- [S1-004] Módulo 'notifications' es puramente reactivo …
|
|
828
|
+
- [S2-005] Módulo 'customers' consume eventos pero no produce ninguno
|
|
829
|
+
- [S2-005] Módulo 'notifications' consume eventos pero no produce ninguno
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
## 8. Practical examples with real findings
|
|
835
|
+
|
|
836
|
+
### Example: cinema booking system
|
|
837
|
+
|
|
838
|
+
Running `eva evaluate system` on a cinema booking `system.yaml` with 7 modules and 9 async events produced:
|
|
839
|
+
|
|
840
|
+
**Score: 88% (0 errors, 3 warnings, 11 passed, 12 info)**
|
|
841
|
+
|
|
842
|
+
| Rule | Severity | Finding | Recommendation |
|
|
843
|
+
|------|----------|---------|----------------|
|
|
844
|
+
| S1-004 | 🟡 | `notifications` is purely reactive but description doesn't say so | Add "consumes events" to its description |
|
|
845
|
+
| S2-005 | 🟡 | `customers` consumes events but produces none | Intentional — accumulates loyalty points. Acceptable. |
|
|
846
|
+
| S2-005 | 🟡 | `notifications` consumes events but produces none | Intentional — pure notification sink. Acceptable. |
|
|
847
|
+
| S2-007 | 🔵 | 9 topics don't include the `cinema` prefix | Rename topics to `cinema.RESERVATION_CREATED`, etc. |
|
|
848
|
+
| S3-005 | 🔵 | `movies`, `theaters`, `customers` consulted sync but emit no events | Acceptable for catalog/reference data modules |
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## 9. Common errors and how to fix them
|
|
853
|
+
|
|
854
|
+
### Error S1-001 — Module referenced but not declared
|
|
855
|
+
|
|
856
|
+
```
|
|
857
|
+
[S1-001] Módulo 'billing' referenciado en integrations pero no declarado en modules[]
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
**Fix:** Add the module to `modules[]` or correct the name typo in `integrations`.
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
### Error S1-002 — Module with no responsibilities
|
|
865
|
+
|
|
866
|
+
```
|
|
867
|
+
[S1-002] Módulo 'reporting' no tiene ninguna responsabilidad — no expone endpoints,
|
|
868
|
+
no produce ni consume eventos
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Fix:** Add `exposes[]` endpoints, connect the module to an async event, or remove it.
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
### Error S2-001 — Event with no consumers
|
|
876
|
+
|
|
877
|
+
```
|
|
878
|
+
[S2-001] Evento 'OrderShippedEvent' declarado en integrations.async sin consumidores
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
**Fix:** Add at least one consumer, or remove the event if it is not yet implemented.
|
|
882
|
+
|
|
883
|
+
---
|
|
884
|
+
|
|
885
|
+
### Error S2-002 — Topic collision
|
|
886
|
+
|
|
887
|
+
```
|
|
888
|
+
[S2-002] Topic 'ORDER_EVENTS' está declarado para dos eventos distintos:
|
|
889
|
+
'OrderCreatedEvent' y 'OrderCancelledEvent'
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Fix:** Give each event a unique topic name: `ORDER_CREATED`, `ORDER_CANCELLED`.
|
|
893
|
+
|
|
894
|
+
---
|
|
895
|
+
|
|
896
|
+
### Error S2-003 — Self-loop
|
|
897
|
+
|
|
898
|
+
```
|
|
899
|
+
[S2-003] Módulo 'orders' está listado como consumidor de su propio evento
|
|
900
|
+
'OrderCreatedEvent' (self-loop)
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
**Fix:** Remove the self-reference from `consumers`, or redesign the flow so a different module consumes the event.
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
### Error S3-001 — Sync call to module without endpoints
|
|
908
|
+
|
|
909
|
+
```
|
|
910
|
+
[S3-001] 'orders' llama síncronamente a 'notifications' pero este módulo
|
|
911
|
+
no declara exposes[]
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
**Fix:** Add `exposes[]` to the target module, or replace the sync call with an async event.
|
|
915
|
+
|
|
916
|
+
---
|
|
917
|
+
|
|
918
|
+
### Error S3-002 — Endpoint not found in target module
|
|
919
|
+
|
|
920
|
+
```
|
|
921
|
+
[S3-002] Endpoint 'GET /orders/{id}/items' usado por 'shipping' no está
|
|
922
|
+
declarado en exposes[] de 'orders'
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Fix:** Add the missing endpoint to `orders.exposes[]`:
|
|
926
|
+
|
|
927
|
+
```yaml
|
|
928
|
+
- method: GET
|
|
929
|
+
path: /orders/{id}/items
|
|
930
|
+
useCase: GetOrderItems
|
|
931
|
+
description: "..."
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
### Error S4-001 — Duplicate route
|
|
937
|
+
|
|
938
|
+
```
|
|
939
|
+
[S4-001] Módulo 'orders' tiene dos endpoints con el mismo método y path:
|
|
940
|
+
GET /orders/{id}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
**Fix:** Remove the duplicate or rename the path of the second endpoint.
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
### Warning S3-003 — Bidirectional sync coupling
|
|
948
|
+
|
|
949
|
+
```
|
|
950
|
+
[S3-003] Acoplamiento síncrono bidireccional: 'orders' llama a 'inventory'
|
|
951
|
+
y viceversa
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Fix options:**
|
|
955
|
+
1. Replace one direction with an async event
|
|
956
|
+
2. Extract the shared data into a third read-model module that both query
|
|
957
|
+
3. Pass the needed data in the initial request payload, avoiding the reverse call
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
### Warning S5-001 — Messaging disabled with events declared
|
|
962
|
+
|
|
963
|
+
```
|
|
964
|
+
[S5-001] messaging.enabled está en false pero hay 5 eventos declarados
|
|
965
|
+
en integrations.async
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
**Fix:** Set `messaging.enabled: true`, or remove the async events from `integrations` if messaging is truly not used.
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
### Warning S5-002 — Success event without failure counterpart
|
|
973
|
+
|
|
974
|
+
```
|
|
975
|
+
[S5-002] Evento de éxito 'PaymentApprovedEvent' existe pero no hay un evento
|
|
976
|
+
de fallo correspondiente para el sujeto 'payment' que permita compensación
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Fix:** Add a corresponding failure event so consumers can react to unhappy paths:
|
|
980
|
+
|
|
981
|
+
```yaml
|
|
982
|
+
- event: PaymentRejectedEvent
|
|
983
|
+
producer: payments
|
|
984
|
+
topic: PAYMENT_REJECTED
|
|
985
|
+
consumers:
|
|
986
|
+
- module: reservations
|
|
987
|
+
useCase: ExpireReservation
|
|
988
|
+
- module: notifications
|
|
989
|
+
useCase: NotifyPaymentRejected
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|