eva4j 1.0.14 → 1.0.16
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/skills/skill-creator/LICENSE.txt +202 -0
- package/.agents/skills/skill-creator/SKILL.md +485 -0
- package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
- package/.agents/skills/skill-creator/agents/comparator.md +202 -0
- package/.agents/skills/skill-creator/agents/grader.md +223 -0
- package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.agents/skills/skill-creator/references/schemas.md +430 -0
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
- package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
- package/.agents/skills/skill-creator/scripts/utils.py +47 -0
- package/AGENTS.md +268 -6
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +430 -14
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +461 -13
- package/bin/eva4j.js +32 -14
- package/config/defaults.json +1 -0
- package/docs/commands/EVALUATE_SYSTEM.md +746 -261
- package/docs/commands/EXPORT_DIAGRAM.md +153 -0
- package/docs/commands/GENERATE_ENTITIES.md +599 -6
- package/docs/commands/INDEX.md +7 -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 +46 -3
- package/package.json +2 -1
- package/src/agents/design-reviewer.agent.md +163 -0
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +1 -0
- package/src/commands/detach.js +149 -108
- package/src/commands/evaluate-system.js +234 -8
- package/src/commands/export-diagram.js +522 -0
- package/src/commands/generate-entities.js +685 -66
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +43 -10
- package/src/generators/base-generator.js +18 -6
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +12 -2
- package/src/skills/build-system-yaml/SKILL.md +323 -0
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
- package/src/skills/build-system-yaml/references/module-spec.md +367 -0
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -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 +1020 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/system-validator.js +319 -199
- package/src/utils/yaml-to-entity.js +272 -7
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
- 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/base/docker/Dockerfile.ejs +21 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- 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/CreateCommandHandler.java.ejs +3 -6
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
- package/templates/crud/EndpointsController.java.ejs +6 -6
- package/templates/crud/FindByQuery.java.ejs +1 -1
- package/templates/crud/FindByQueryHandler.java.ejs +3 -9
- package/templates/crud/GetQueryHandler.java.ejs +2 -5
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -14
- package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
- package/templates/crud/ScaffoldQuery.java.ejs +3 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
- package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
- package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
- package/templates/evaluate/report.html.ejs +394 -2
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +9 -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 +25 -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/annotations/LogAfter.java.ejs +2 -0
- package/templates/shared/annotations/LogBefore.java.ejs +2 -0
- package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
- package/templates/shared/annotations/LogLevel.java.ejs +8 -0
- package/templates/shared/annotations/LogTimer.java.ejs +1 -0
- package/templates/shared/annotations/Loggable.java.ejs +17 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
- package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
- package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
- package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
- package/src/commands/generate-system.js +0 -243
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
- package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
- package/templates/base/root/skill-build-system-yaml.ejs +0 -252
- package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
- package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
- package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Validates a parsed system.yaml object against
|
|
4
|
+
* Validates a parsed system.yaml object against the S1–S5 static evaluation rules.
|
|
5
5
|
*
|
|
6
6
|
* @param {object} systemConfig - Parsed system.yaml content
|
|
7
|
-
* @
|
|
7
|
+
* @param {Record<string, object>} [domainConfigs={}] - moduleName → parsed domain YAML (used to check domain-level events)
|
|
8
|
+
* @returns {{ errors: string[], warnings: string[], info: string[], ok: string[], score: number }}
|
|
8
9
|
*/
|
|
9
|
-
function validateSystem(systemConfig) {
|
|
10
|
+
function validateSystem(systemConfig, domainConfigs = {}) {
|
|
10
11
|
const errors = [];
|
|
11
12
|
const warnings = [];
|
|
13
|
+
const info = [];
|
|
12
14
|
const ok = [];
|
|
13
15
|
|
|
14
16
|
const modules = systemConfig.modules || [];
|
|
@@ -16,93 +18,206 @@ function validateSystem(systemConfig) {
|
|
|
16
18
|
const integrations = systemConfig.integrations || {};
|
|
17
19
|
const asyncEvents = integrations.async || [];
|
|
18
20
|
const syncIntegrations = integrations.sync || [];
|
|
21
|
+
const messaging = systemConfig.messaging || {};
|
|
22
|
+
const topicPrefix = (messaging.kafka || {}).topicPrefix || null;
|
|
19
23
|
|
|
20
|
-
//
|
|
21
|
-
const
|
|
24
|
+
// Helper: normalize a consumer entry to its module name string
|
|
25
|
+
const consumerModule = (c) => (typeof c === 'string' ? c : c.module);
|
|
26
|
+
|
|
27
|
+
// ── S1 — Integridad de módulos ────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
// Collect all module names referenced in integrations
|
|
30
|
+
const referencedInIntegrations = new Set();
|
|
31
|
+
for (const ev of asyncEvents) {
|
|
32
|
+
if (ev.producer) referencedInIntegrations.add(ev.producer);
|
|
33
|
+
for (const c of ev.consumers || []) referencedInIntegrations.add(consumerModule(c));
|
|
34
|
+
}
|
|
35
|
+
for (const sync of syncIntegrations) {
|
|
36
|
+
if (sync.caller) referencedInIntegrations.add(sync.caller);
|
|
37
|
+
if (sync.calls) referencedInIntegrations.add(sync.calls);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// S1-001: module referenced but not declared
|
|
41
|
+
let s1_001_found = false;
|
|
42
|
+
for (const ref of referencedInIntegrations) {
|
|
43
|
+
if (!moduleNames.has(ref)) {
|
|
44
|
+
errors.push(`[S1-001] Módulo '${ref}' referenciado en integrations pero no declarado en modules[]`);
|
|
45
|
+
s1_001_found = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!s1_001_found) {
|
|
49
|
+
ok.push('[S1-001] Todos los módulos referenciados en integrations están declarados en modules[] ✓');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// S1-002: module with no responsibilities
|
|
53
|
+
let s1_002_found = false;
|
|
22
54
|
for (const mod of modules) {
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
const hasExposes = (mod.exposes || []).length > 0;
|
|
56
|
+
const producesEvents = asyncEvents.some((e) => e.producer === mod.name);
|
|
57
|
+
const consumesEvents = asyncEvents.some((e) =>
|
|
58
|
+
(e.consumers || []).some((c) => consumerModule(c) === mod.name)
|
|
25
59
|
);
|
|
60
|
+
if (!hasExposes && !producesEvents && !consumesEvents) {
|
|
61
|
+
errors.push(`[S1-002] Módulo '${mod.name}' no tiene ninguna responsabilidad — no expone endpoints, no produce ni consume eventos`);
|
|
62
|
+
s1_002_found = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!s1_002_found) {
|
|
66
|
+
ok.push('[S1-002] Todos los módulos tienen al menos una responsabilidad declarada ✓');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// S1-003: module without description
|
|
70
|
+
let s1_003_found = false;
|
|
71
|
+
for (const mod of modules) {
|
|
72
|
+
if (!mod.description || mod.description.trim() === '') {
|
|
73
|
+
warnings.push(`[S1-003] Módulo '${mod.name}' no tiene campo description declarado`);
|
|
74
|
+
s1_003_found = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!s1_003_found) {
|
|
78
|
+
ok.push('[S1-003] Todos los módulos tienen description declarado ✓');
|
|
26
79
|
}
|
|
27
80
|
|
|
28
|
-
//
|
|
81
|
+
// S1-004: purely reactive module not documented
|
|
82
|
+
for (const mod of modules) {
|
|
83
|
+
const producesEvents = asyncEvents.some((e) => e.producer === mod.name);
|
|
84
|
+
const consumesEvents = asyncEvents.some((e) =>
|
|
85
|
+
(e.consumers || []).some((c) => consumerModule(c) === mod.name)
|
|
86
|
+
);
|
|
87
|
+
const makesSyncCalls = syncIntegrations.some((s) => s.caller === mod.name);
|
|
88
|
+
const hasExposes = (mod.exposes || []).length > 0;
|
|
29
89
|
|
|
30
|
-
|
|
90
|
+
const isPurelyReactive = consumesEvents && !producesEvents && !makesSyncCalls && !hasExposes;
|
|
91
|
+
if (isPurelyReactive) {
|
|
92
|
+
const desc = (mod.description || '').toLowerCase();
|
|
93
|
+
const documentedAsReactive = desc.includes('consume') || desc.includes('reactiv') || desc.includes('event') || desc.includes('suscri') || desc.includes('listen');
|
|
94
|
+
if (!documentedAsReactive) {
|
|
95
|
+
info.push(`[S1-004] Módulo '${mod.name}' es puramente reactivo (solo consume eventos) pero su description no lo documenta explícitamente`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── S2 — Integridad del grafo de eventos async ────────────────────────────
|
|
101
|
+
|
|
102
|
+
// S2-001: event with no consumers
|
|
103
|
+
let s2_001_found = false;
|
|
31
104
|
for (const ev of asyncEvents) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
105
|
+
const consumers = ev.consumers || [];
|
|
106
|
+
if (consumers.length === 0) {
|
|
107
|
+
errors.push(`[S2-001] Evento '${ev.event}' declarado en integrations.async sin consumidores`);
|
|
108
|
+
s2_001_found = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!s2_001_found && asyncEvents.length > 0) {
|
|
112
|
+
ok.push('[S2-001] Todos los eventos async tienen al menos un consumidor declarado ✓');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// S2-002: duplicate topic values
|
|
116
|
+
const topicToEvent = {};
|
|
117
|
+
let s2_002_found = false;
|
|
118
|
+
for (const ev of asyncEvents) {
|
|
119
|
+
if (!ev.topic) continue;
|
|
120
|
+
if (topicToEvent[ev.topic]) {
|
|
121
|
+
errors.push(`[S2-002] Topic '${ev.topic}' está declarado para dos eventos distintos: '${topicToEvent[ev.topic]}' y '${ev.event}'`);
|
|
122
|
+
s2_002_found = true;
|
|
36
123
|
} else {
|
|
37
|
-
|
|
124
|
+
topicToEvent[ev.topic] = ev.event;
|
|
38
125
|
}
|
|
39
126
|
}
|
|
127
|
+
if (!s2_002_found && asyncEvents.length > 0) {
|
|
128
|
+
ok.push('[S2-002] No hay colisiones de topics en integrations.async ✓');
|
|
129
|
+
}
|
|
40
130
|
|
|
41
|
-
//
|
|
131
|
+
// S2-003: self-loop (module consuming its own event)
|
|
132
|
+
let s2_003_found = false;
|
|
42
133
|
for (const ev of asyncEvents) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
errors.push(
|
|
48
|
-
`Integridad referencial: el consumidor '${moduleName}' del evento '${ev.event}' no está declarado en modules[]`
|
|
49
|
-
);
|
|
134
|
+
for (const c of ev.consumers || []) {
|
|
135
|
+
if (consumerModule(c) === ev.producer) {
|
|
136
|
+
errors.push(`[S2-003] Módulo '${ev.producer}' está listado como consumidor de su propio evento '${ev.event}' (self-loop)`);
|
|
137
|
+
s2_003_found = true;
|
|
50
138
|
}
|
|
51
139
|
}
|
|
52
140
|
}
|
|
53
|
-
if (
|
|
54
|
-
ok.push('
|
|
141
|
+
if (!s2_003_found && asyncEvents.length > 0) {
|
|
142
|
+
ok.push('[S2-003] No se detectaron self-loops en el grafo de eventos ✓');
|
|
55
143
|
}
|
|
56
144
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
145
|
+
// S2-004: module produces but never consumes
|
|
146
|
+
const producerSet = new Set(asyncEvents.map((e) => e.producer).filter(Boolean));
|
|
147
|
+
const consumerSet = new Set(
|
|
148
|
+
asyncEvents.flatMap((e) => (e.consumers || []).map(consumerModule))
|
|
149
|
+
);
|
|
150
|
+
for (const mod of modules) {
|
|
151
|
+
if (producerSet.has(mod.name) && !consumerSet.has(mod.name)) {
|
|
152
|
+
warnings.push(`[S2-004] Módulo '${mod.name}' produce eventos pero no consume ninguno`);
|
|
63
153
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// S2-005: module consumes but never produces
|
|
157
|
+
// Also check domain-level events[] — a module may produce Domain Events not yet
|
|
158
|
+
// wired in system.yaml integrations.async[], which is a valid design-in-progress.
|
|
159
|
+
for (const mod of modules) {
|
|
160
|
+
if (consumerSet.has(mod.name) && !producerSet.has(mod.name)) {
|
|
161
|
+
const domainCfg = domainConfigs[mod.name];
|
|
162
|
+
const producesDomainEvents = (domainCfg?.aggregates || []).some(
|
|
163
|
+
(agg) => (agg.events || []).length > 0
|
|
67
164
|
);
|
|
165
|
+
if (!producesDomainEvents) {
|
|
166
|
+
warnings.push(`[S2-005] Módulo '${mod.name}' consume eventos pero no produce ninguno`);
|
|
167
|
+
}
|
|
68
168
|
}
|
|
69
169
|
}
|
|
70
170
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const found = targetExposes.some((ep) => {
|
|
79
|
-
const normalizedEp = ep.trim().toUpperCase().replace(/\/+$/, '');
|
|
80
|
-
return normalizedEp === normalized || endpointMatches(ep, endpoint);
|
|
81
|
-
});
|
|
82
|
-
if (!found) {
|
|
83
|
-
errors.push(
|
|
84
|
-
`Integridad referencial: el endpoint '${endpoint}' usado por '${sync.caller}' no está declarado en el exposes[] de '${sync.calls}'`
|
|
85
|
-
);
|
|
86
|
-
allSyncEndpointsFound = false;
|
|
87
|
-
}
|
|
171
|
+
// S2-006: event name not following PascalCase + Event suffix
|
|
172
|
+
const eventNameRegex = /^[A-Z][a-zA-Z0-9]*Event$/;
|
|
173
|
+
let s2_006_found = false;
|
|
174
|
+
for (const ev of asyncEvents) {
|
|
175
|
+
if (ev.event && !eventNameRegex.test(ev.event)) {
|
|
176
|
+
warnings.push(`[S2-006] Nombre de evento '${ev.event}' no sigue la convención PascalCase con sufijo 'Event'`);
|
|
177
|
+
s2_006_found = true;
|
|
88
178
|
}
|
|
89
179
|
}
|
|
90
|
-
if (
|
|
91
|
-
ok.push('Todos los
|
|
180
|
+
if (!s2_006_found && asyncEvents.length > 0) {
|
|
181
|
+
ok.push('[S2-006] Todos los nombres de eventos siguen la convención PascalCase + sufijo Event ✓');
|
|
92
182
|
}
|
|
93
183
|
|
|
94
|
-
// ──
|
|
184
|
+
// ── S3 — Integridad de llamadas síncronas ────────────────────────────────
|
|
95
185
|
|
|
96
|
-
//
|
|
97
|
-
|
|
186
|
+
// S3-001: sync call to module with no exposes
|
|
187
|
+
let s3_001_found = false;
|
|
98
188
|
for (const sync of syncIntegrations) {
|
|
99
|
-
if (!
|
|
100
|
-
|
|
189
|
+
if (!moduleNames.has(sync.calls)) continue; // already caught by S1-001
|
|
190
|
+
const targetMod = modules.find((m) => m.name === sync.calls);
|
|
191
|
+
if (targetMod && (!targetMod.exposes || targetMod.exposes.length === 0)) {
|
|
192
|
+
errors.push(`[S3-001] '${sync.caller}' llama síncronamente a '${sync.calls}' pero este módulo no declara exposes[]`);
|
|
193
|
+
s3_001_found = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!s3_001_found && syncIntegrations.length > 0) {
|
|
197
|
+
ok.push('[S3-001] Todos los módulos destino de llamadas síncronas tienen endpoints expuestos ✓');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// S3-002: endpoint in using[] not found in target exposes[]
|
|
201
|
+
let s3_002_found = false;
|
|
202
|
+
for (const sync of syncIntegrations) {
|
|
203
|
+
if (!moduleNames.has(sync.calls)) continue;
|
|
204
|
+
const targetMod = modules.find((m) => m.name === sync.calls);
|
|
205
|
+
const targetExposes = (targetMod?.exposes || []).map((ep) => `${ep.method} ${ep.path}`);
|
|
206
|
+
for (const endpoint of sync.using || []) {
|
|
207
|
+
const found = targetExposes.some((ep) => endpointMatches(ep, endpoint));
|
|
208
|
+
if (!found) {
|
|
209
|
+
errors.push(`[S3-002] Endpoint '${endpoint}' usado por '${sync.caller}' no está declarado en exposes[] de '${sync.calls}'`);
|
|
210
|
+
s3_002_found = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!s3_002_found && syncIntegrations.length > 0) {
|
|
215
|
+
ok.push('[S3-002] Todos los endpoints referenciados en llamadas síncronas existen en el módulo destino ✓');
|
|
101
216
|
}
|
|
102
217
|
|
|
103
|
-
//
|
|
218
|
+
// S3-003: bidirectional sync coupling (WARNING, not error)
|
|
104
219
|
const biDirChecked = new Set();
|
|
105
|
-
let
|
|
220
|
+
let s3_003_found = false;
|
|
106
221
|
for (const sync of syncIntegrations) {
|
|
107
222
|
const key = [sync.caller, sync.calls].sort().join('↔');
|
|
108
223
|
if (biDirChecked.has(key)) continue;
|
|
@@ -111,189 +226,194 @@ function validateSystem(systemConfig) {
|
|
|
111
226
|
(s) => s.caller === sync.calls && s.calls === sync.caller
|
|
112
227
|
);
|
|
113
228
|
if (reverse) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
);
|
|
117
|
-
biDirFound = true;
|
|
229
|
+
warnings.push(`[S3-003] Acoplamiento síncrono bidireccional: '${sync.caller}' llama a '${sync.calls}' y viceversa`);
|
|
230
|
+
s3_003_found = true;
|
|
118
231
|
}
|
|
119
232
|
}
|
|
233
|
+
if (!s3_003_found && syncIntegrations.length > 0) {
|
|
234
|
+
ok.push('[S3-003] No se detectó acoplamiento síncrono bidireccional ✓');
|
|
235
|
+
}
|
|
120
236
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (result) return result;
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
237
|
+
// S3-004: module with more than 3 distinct outgoing sync dependencies
|
|
238
|
+
const outgoingSyncDeps = {};
|
|
239
|
+
for (const sync of syncIntegrations) {
|
|
240
|
+
if (!outgoingSyncDeps[sync.caller]) outgoingSyncDeps[sync.caller] = new Set();
|
|
241
|
+
outgoingSyncDeps[sync.caller].add(sync.calls);
|
|
242
|
+
}
|
|
243
|
+
for (const [caller, deps] of Object.entries(outgoingSyncDeps)) {
|
|
244
|
+
if (deps.size > 3) {
|
|
245
|
+
warnings.push(`[S3-004] Módulo '${caller}' tiene ${deps.size} dependencias síncronas salientes distintas (>${3}): ${[...deps].join(', ')}`);
|
|
133
246
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
if (cycle && cycle.length > 2) {
|
|
143
|
-
const cycleStr = cycle.join(' → ');
|
|
144
|
-
errors.push(`Ciclo síncrono detectado: ${cycleStr}`);
|
|
145
|
-
cycle.forEach((n) => cycleChecked.add(n));
|
|
146
|
-
cycleFound = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// S3-005: module consulted synchronously but emits no events
|
|
250
|
+
const syncCallees = new Set(syncIntegrations.map((s) => s.calls).filter(Boolean));
|
|
251
|
+
for (const callee of syncCallees) {
|
|
252
|
+
const producesAny = asyncEvents.some((e) => e.producer === callee);
|
|
253
|
+
if (!producesAny) {
|
|
254
|
+
info.push(`[S3-005] Módulo '${callee}' es consultado síncronamente pero no emite ningún evento cuando su estado cambia`);
|
|
147
255
|
}
|
|
148
256
|
}
|
|
149
257
|
|
|
150
|
-
|
|
151
|
-
|
|
258
|
+
// S3-006: duplicate port name across different caller modules → ConflictingBeanDefinitionException
|
|
259
|
+
let s3_006_found = false;
|
|
260
|
+
const portsByName = {};
|
|
261
|
+
for (const sync of syncIntegrations) {
|
|
262
|
+
const portName = sync.port;
|
|
263
|
+
if (!portName) continue;
|
|
264
|
+
if (!portsByName[portName]) portsByName[portName] = [];
|
|
265
|
+
portsByName[portName].push(sync.caller);
|
|
266
|
+
}
|
|
267
|
+
for (const [portName, callers] of Object.entries(portsByName)) {
|
|
268
|
+
const uniqueCallers = [...new Set(callers)];
|
|
269
|
+
if (uniqueCallers.length > 1) {
|
|
270
|
+
const suggestions = uniqueCallers.map((c) => {
|
|
271
|
+
const prefix = c.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
272
|
+
return `${prefix[0].toUpperCase() + prefix.slice(1)}${portName}`;
|
|
273
|
+
}).join(', ');
|
|
274
|
+
errors.push(
|
|
275
|
+
`[S3-006] Port '${portName}' es usado por módulos distintos (${uniqueCallers.join(', ')}). ` +
|
|
276
|
+
`Esto causa ConflictingBeanDefinitionException en Spring. ` +
|
|
277
|
+
`Cada módulo debe usar un nombre propio: ej. ${suggestions}`
|
|
278
|
+
);
|
|
279
|
+
s3_006_found = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!s3_006_found && syncIntegrations.length > 0) {
|
|
283
|
+
ok.push('[S3-006] No hay nombres de port duplicados entre módulos distintos ✓');
|
|
152
284
|
}
|
|
153
285
|
|
|
154
|
-
// ──
|
|
286
|
+
// ── S4 — Coherencia de endpoints ─────────────────────────────────────────
|
|
155
287
|
|
|
156
288
|
for (const mod of modules) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
289
|
+
const exposes = mod.exposes || [];
|
|
290
|
+
|
|
291
|
+
// S4-001: duplicate METHOD + path within same module
|
|
292
|
+
const endpointKeys = new Set();
|
|
293
|
+
for (const ep of exposes) {
|
|
294
|
+
const key = `${(ep.method || '').toUpperCase()} ${ep.path || ''}`;
|
|
295
|
+
if (endpointKeys.has(key)) {
|
|
296
|
+
errors.push(`[S4-001] Módulo '${mod.name}' tiene dos endpoints con el mismo método y path: ${key}`);
|
|
297
|
+
} else {
|
|
298
|
+
endpointKeys.add(key);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
166
301
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
302
|
+
// S4-002: PUT /{id} without GET /{id} for same resource base
|
|
303
|
+
for (const ep of exposes) {
|
|
304
|
+
if ((ep.method || '').toUpperCase() !== 'PUT') continue;
|
|
305
|
+
// Normalize path param to detect /{id} pattern
|
|
306
|
+
const normalizedPut = (ep.path || '').replace(/\{[^}]+\}$/, '{id}');
|
|
307
|
+
if (!normalizedPut.match(/\{id\}$/)) continue; // only check PUT /{id} style paths
|
|
308
|
+
const resourceBase = normalizedPut.replace(/\{id\}$/, '{id}');
|
|
309
|
+
const hasGet = exposes.some(
|
|
310
|
+
(g) => (g.method || '').toUpperCase() === 'GET' &&
|
|
311
|
+
(g.path || '').replace(/\{[^}]+\}$/, '{id}') === resourceBase
|
|
174
312
|
);
|
|
175
|
-
|
|
176
|
-
|
|
313
|
+
if (!hasGet) {
|
|
314
|
+
warnings.push(`[S4-002] Módulo '${mod.name}' tiene PUT ${ep.path} sin el correspondiente GET ${ep.path}`);
|
|
315
|
+
}
|
|
177
316
|
}
|
|
178
317
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
318
|
+
// S4-003: DELETE without description documenting physical vs logical
|
|
319
|
+
for (const ep of exposes) {
|
|
320
|
+
if ((ep.method || '').toUpperCase() !== 'DELETE') continue;
|
|
321
|
+
if (!ep.description || ep.description.trim() === '') {
|
|
322
|
+
warnings.push(`[S4-003] Endpoint DELETE ${ep.path} en '${mod.name}' no tiene description que indique si el borrado es físico o lógico`);
|
|
323
|
+
}
|
|
182
324
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ── Check 4: Behavior Gaps ─────────────────────────────────────────────────
|
|
186
325
|
|
|
187
|
-
|
|
188
|
-
|
|
326
|
+
// S4-004: endpoint without description (info)
|
|
327
|
+
for (const ep of exposes) {
|
|
328
|
+
if (!ep.description || ep.description.trim() === '') {
|
|
329
|
+
info.push(`[S4-004] Endpoint ${ep.method} ${ep.path} en '${mod.name}' no tiene campo description`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
189
332
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const matchedVerb = schedulerVerbs.find((v) => useCaseLower.startsWith(v) || useCaseLower.includes(v));
|
|
198
|
-
if (!matchedVerb) continue;
|
|
199
|
-
|
|
200
|
-
// Check: is this endpoint reachable via an async event
|
|
201
|
-
const triggeredByEvent = asyncEvents.some((ev) =>
|
|
202
|
-
(ev.consumers || []).some((c) => {
|
|
203
|
-
const consumer = typeof c === 'string' ? c : c.module;
|
|
204
|
-
if (consumer !== mod.name) return false;
|
|
205
|
-
// The event name often matches the use case verb
|
|
206
|
-
const eventLower = ev.event.toLowerCase();
|
|
207
|
-
return schedulerVerbs.some((v) => eventLower.includes(v));
|
|
208
|
-
})
|
|
333
|
+
// S4-005: module with POST but no GET /{id} (info)
|
|
334
|
+
const hasPost = exposes.some((ep) => (ep.method || '').toUpperCase() === 'POST');
|
|
335
|
+
if (hasPost) {
|
|
336
|
+
const hasGetById = exposes.some(
|
|
337
|
+
(ep) => (ep.method || '').toUpperCase() === 'GET' &&
|
|
338
|
+
/\{[^}]+\}$/.test(ep.path || '')
|
|
209
339
|
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const triggeredBySync = syncIntegrations.some((s) => s.calls === mod.name && (s.using || []).some((u) => u.includes(ep.path)));
|
|
213
|
-
|
|
214
|
-
if (!triggeredByEvent && !triggeredBySync) {
|
|
215
|
-
warnings.push(
|
|
216
|
-
`Gap de comportamiento: '${ep.useCase}' (${ep.method} ${ep.path}) en '${mod.name}' no tiene ningún evento ni llamada síncrona que lo active. Puede necesitar un scheduler o job periódico.`
|
|
217
|
-
);
|
|
340
|
+
if (!hasGetById) {
|
|
341
|
+
info.push(`[S4-005] Módulo '${mod.name}' tiene POST de creación pero no declara GET /{id} para recuperar el recurso creado`);
|
|
218
342
|
}
|
|
219
343
|
}
|
|
220
344
|
}
|
|
221
345
|
|
|
222
|
-
//
|
|
223
|
-
for (const mod of modules) {
|
|
224
|
-
if (!mod.exposes || mod.exposes.length === 0) {
|
|
225
|
-
ok.push(`'${mod.name}' no expone endpoints REST directamente (módulo de integración)`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
346
|
+
// ── S5 — Coherencia del sistema global ───────────────────────────────────
|
|
228
347
|
|
|
229
|
-
//
|
|
348
|
+
// S5-001: messaging.enabled: false with async events declared
|
|
349
|
+
if (messaging.enabled === false && asyncEvents.length > 0) {
|
|
350
|
+
warnings.push(`[S5-001] messaging.enabled está en false pero hay ${asyncEvents.length} eventos declarados en integrations.async`);
|
|
351
|
+
} else if (messaging.enabled !== false && asyncEvents.length > 0) {
|
|
352
|
+
ok.push('[S5-001] Configuración de messaging es coherente con los eventos declarados ✓');
|
|
353
|
+
}
|
|
230
354
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
355
|
+
// S5-002: success event without matching failure event for same subject
|
|
356
|
+
// Suffixes that represent external operations with side-effects → warning if no failure counterpart
|
|
357
|
+
const successSuffixesWarning = ['confirmedevent', 'approvedevent', 'placedevent', 'activatedevent'];
|
|
358
|
+
// Suffixes that represent physical/irreversible facts → info only (compensation less expected)
|
|
359
|
+
const successSuffixesInfo = ['completedevent'];
|
|
360
|
+
const failureSuffixes = ['failedevent', 'rejectedevent', 'cancelledevent', 'canceledevent', 'expiredevent'];
|
|
234
361
|
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
362
|
+
for (const ev of asyncEvents) {
|
|
363
|
+
const evLower = (ev.event || '').toLowerCase();
|
|
364
|
+
const matchedWarning = successSuffixesWarning.find((s) => evLower.endsWith(s));
|
|
365
|
+
const matchedInfo = !matchedWarning && successSuffixesInfo.find((s) => evLower.endsWith(s));
|
|
366
|
+
const matched = matchedWarning || matchedInfo;
|
|
367
|
+
if (!matched) continue;
|
|
368
|
+
|
|
369
|
+
// Derive subject: strip the matched suffix
|
|
370
|
+
const subjectLength = evLower.length - matched.length;
|
|
371
|
+
const subject = evLower.slice(0, subjectLength);
|
|
372
|
+
|
|
373
|
+
// Check if there's any failure event with the same subject prefix
|
|
374
|
+
const hasFailure = asyncEvents.some((other) => {
|
|
375
|
+
const otherLower = (other.event || '').toLowerCase();
|
|
376
|
+
return failureSuffixes.some((f) => otherLower.endsWith(f) && otherLower.startsWith(subject));
|
|
239
377
|
});
|
|
240
378
|
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
379
|
+
if (!hasFailure) {
|
|
380
|
+
const msg = `[S5-002] Evento de éxito '${ev.event}' existe pero no hay un evento de fallo correspondiente para el sujeto '${subject}' que permita compensación`;
|
|
381
|
+
if (matchedWarning) {
|
|
382
|
+
warnings.push(msg);
|
|
383
|
+
} else {
|
|
384
|
+
info.push(msg);
|
|
385
|
+
}
|
|
246
386
|
}
|
|
247
387
|
}
|
|
248
388
|
|
|
249
|
-
//
|
|
389
|
+
// S5-003: auth/security module with no integrations (info)
|
|
390
|
+
const authPattern = /auth|security|identity|session/i;
|
|
250
391
|
for (const mod of modules) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
(ev) =>
|
|
258
|
-
(ev.consumers || []).some((c) => (typeof c === 'string' ? c : c.module) === mod.name) &&
|
|
259
|
-
asyncEvents.some(() => false) // placeholder; real dual-trigger needs business knowledge
|
|
260
|
-
);
|
|
261
|
-
// Flag endpoints used in sync AND the module also consumes events (approximate heuristic)
|
|
262
|
-
if (inSync) {
|
|
263
|
-
const modConsumesEvents = asyncEvents.some((ev) =>
|
|
264
|
-
(ev.consumers || []).some((c) => (typeof c === 'string' ? c : c.module) === mod.name)
|
|
265
|
-
);
|
|
266
|
-
if (modConsumesEvents) {
|
|
267
|
-
ok.push(
|
|
268
|
-
`'${mod.name}' tiene endpoints accesibles tanto síncronamente como vía eventos (diseño dual — intencional)`
|
|
269
|
-
);
|
|
270
|
-
break; // one ok per module is enough
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
void inEvents; // suppress unused warning
|
|
392
|
+
if (!authPattern.test(mod.name)) continue;
|
|
393
|
+
const hasAnyIntegration =
|
|
394
|
+
asyncEvents.some((e) => e.producer === mod.name || (e.consumers || []).some((c) => consumerModule(c) === mod.name)) ||
|
|
395
|
+
syncIntegrations.some((s) => s.caller === mod.name || s.calls === mod.name);
|
|
396
|
+
if (!hasAnyIntegration) {
|
|
397
|
+
info.push(`[S5-003] Módulo '${mod.name}' parece manejar autenticación/seguridad pero no tiene ninguna integración declarada con otros módulos`);
|
|
274
398
|
}
|
|
275
399
|
}
|
|
276
400
|
|
|
277
|
-
//
|
|
401
|
+
// S5-004: module with no connection to system graph (info)
|
|
278
402
|
for (const mod of modules) {
|
|
279
|
-
const
|
|
280
|
-
asyncEvents.some((e) => e.producer === mod.name)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
!syncIntegrations.some((s) => s.calls === mod.name);
|
|
285
|
-
|
|
286
|
-
if (onlyProduces) {
|
|
287
|
-
ok.push(`'${mod.name}' es productor puro de eventos sin dependencias entrantes (bajo acoplamiento) ✓`);
|
|
403
|
+
const hasAnyConnection =
|
|
404
|
+
asyncEvents.some((e) => e.producer === mod.name || (e.consumers || []).some((c) => consumerModule(c) === mod.name)) ||
|
|
405
|
+
syncIntegrations.some((s) => s.caller === mod.name || s.calls === mod.name);
|
|
406
|
+
if (!hasAnyConnection) {
|
|
407
|
+
info.push(`[S5-004] Módulo '${mod.name}' no tiene ninguna conexión al grafo del sistema — ni async ni sync`);
|
|
288
408
|
}
|
|
289
409
|
}
|
|
290
410
|
|
|
291
|
-
// ── Score
|
|
411
|
+
// ── Score (info items do not affect score) ────────────────────────────────
|
|
292
412
|
|
|
293
413
|
const total = ok.length + errors.length + warnings.length * 0.5;
|
|
294
414
|
const score = total > 0 ? Math.round((ok.length / total) * 100) : 100;
|
|
295
415
|
|
|
296
|
-
return { errors, warnings, ok, score };
|
|
416
|
+
return { errors, warnings, info, ok, score };
|
|
297
417
|
}
|
|
298
418
|
|
|
299
419
|
/**
|