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
package/src/commands/create.js
CHANGED
|
@@ -55,7 +55,10 @@ async function createCommand(projectName, options) {
|
|
|
55
55
|
|
|
56
56
|
// Set all required dependencies
|
|
57
57
|
answers.dependencies = ['web', 'data-jpa', 'security', 'validation', 'actuator'];
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
// Preserve CLI name as project directory name (independent of artifactId)
|
|
60
|
+
answers.projectName = projectName || answers.artifactId;
|
|
61
|
+
|
|
59
62
|
// Build context
|
|
60
63
|
const context = buildBaseContext(answers);
|
|
61
64
|
|
|
@@ -69,15 +72,16 @@ async function createCommand(projectName, options) {
|
|
|
69
72
|
spinner.succeed(chalk.green('Project created successfully! β¨'));
|
|
70
73
|
|
|
71
74
|
console.log(chalk.blue('\nπ¦ Project structure:'));
|
|
72
|
-
console.log(chalk.gray(` ${context.
|
|
75
|
+
console.log(chalk.gray(` ${context.projectName}/`));
|
|
73
76
|
console.log(chalk.gray(` βββ src/main/java/${context.packagePath.replace(/\//g, '.')}`));
|
|
74
77
|
console.log(chalk.gray(` β βββ ${context.applicationClassName}.java`));
|
|
75
78
|
console.log(chalk.gray(` β βββ common/`));
|
|
79
|
+
console.log(chalk.gray(` βββ system/`));
|
|
76
80
|
console.log(chalk.gray(` βββ build.gradle`));
|
|
77
81
|
console.log(chalk.gray(` βββ README.md`));
|
|
78
82
|
|
|
79
83
|
console.log(chalk.blue('\nπ Next steps:'));
|
|
80
|
-
console.log(chalk.white(` cd ${context.
|
|
84
|
+
console.log(chalk.white(` cd ${context.projectName}`));
|
|
81
85
|
console.log(chalk.white(` eva4j add module user # Add your first module`));
|
|
82
86
|
console.log(chalk.white(` ./gradlew bootRun # Run the application`));
|
|
83
87
|
console.log();
|
package/src/commands/detach.js
CHANGED
|
@@ -191,6 +191,7 @@ async function detachCommand(moduleName, options) {
|
|
|
191
191
|
createdDate: new Date().toISOString().split('T')[0],
|
|
192
192
|
dependencyManagementVersion: defaults.dependencyManagementVersion,
|
|
193
193
|
springCloudVersion: projectConfig.springCloudVersion || defaults.springCloudVersion,
|
|
194
|
+
springdocVersion: projectConfig.springdocVersion || defaults.springdocVersion,
|
|
194
195
|
gradleVersion: defaults.gradleVersion,
|
|
195
196
|
license: 'MIT',
|
|
196
197
|
description: `Detached microservice: ${newProjectName}`,
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const ejs = require('ejs');
|
|
9
|
+
const ora = require('ora');
|
|
10
|
+
|
|
11
|
+
const { validateSystem } = require('../utils/system-validator');
|
|
12
|
+
const { validateDomain } = require('../utils/domain-validator');
|
|
13
|
+
|
|
14
|
+
// ββ Module icon heuristic ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
15
|
+
|
|
16
|
+
const ICON_RULES = [
|
|
17
|
+
[/payment|billing|invoice|charge/i, 'π³'],
|
|
18
|
+
[/notification|alert|email|sms|message|notify/i, 'π'],
|
|
19
|
+
[/customer|user|account|profile|member|client/i, 'π€'],
|
|
20
|
+
[/movie|film|cinema|content|catalog|media/i, 'π¬'],
|
|
21
|
+
[/theater|venue|seat|hall|screen/i, 'ποΈ'],
|
|
22
|
+
[/reservation|booking|ticket|order/i, 'ποΈ'],
|
|
23
|
+
[/product|item|inventory|catalog|stock/i, 'ποΈ'],
|
|
24
|
+
[/shipping|delivery|logistics|warehouse/i, 'π¦'],
|
|
25
|
+
[/auth|security|identity|session/i, 'π'],
|
|
26
|
+
[/report|analytics|metric|stat/i, 'π'],
|
|
27
|
+
[/search|index|discover/i, 'π'],
|
|
28
|
+
[/screening|schedule|program|event/i, 'π½οΈ'],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const COLOR_PALETTE = [
|
|
32
|
+
'#4a9eff', // blue
|
|
33
|
+
'#9b6dff', // purple
|
|
34
|
+
'#f5c842', // gold
|
|
35
|
+
'#2dcc8f', // green
|
|
36
|
+
'#ff8c42', // orange
|
|
37
|
+
'#e63950', // red/accent
|
|
38
|
+
'#40c4d0', // teal
|
|
39
|
+
'#ff6bac', // pink
|
|
40
|
+
'#a8e063', // lime
|
|
41
|
+
'#ffa07a', // salmon
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function assignIcon(name) {
|
|
45
|
+
for (const [pattern, icon] of ICON_RULES) {
|
|
46
|
+
if (pattern.test(name)) return icon;
|
|
47
|
+
}
|
|
48
|
+
return 'π';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ββ Flow auto-generation from async events βββββββββββββββββββββββββββββββββββ
|
|
52
|
+
|
|
53
|
+
// Maps trailing verb in an event name to the action verb used in useCases
|
|
54
|
+
const EVENT_VERB_MAP = {
|
|
55
|
+
created: 'Create',
|
|
56
|
+
confirmed: 'Confirm',
|
|
57
|
+
approved: 'Approve',
|
|
58
|
+
rejected: 'Reject',
|
|
59
|
+
cancelled: 'Cancel',
|
|
60
|
+
canceled: 'Cancel',
|
|
61
|
+
locked: 'Lock',
|
|
62
|
+
unlocked: 'Unlock',
|
|
63
|
+
expired: 'Expire',
|
|
64
|
+
scheduled: 'Schedule',
|
|
65
|
+
processed: 'Process',
|
|
66
|
+
published: 'Publish',
|
|
67
|
+
updated: 'Update',
|
|
68
|
+
deleted: 'Delete',
|
|
69
|
+
completed: 'Complete',
|
|
70
|
+
failed: 'Fail',
|
|
71
|
+
started: 'Start',
|
|
72
|
+
initiated: 'Initiate',
|
|
73
|
+
activated: 'Activate',
|
|
74
|
+
deactivated: 'Deactivate',
|
|
75
|
+
registered: 'Register',
|
|
76
|
+
requested: 'Request',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function extractEventVerb(eventName) {
|
|
80
|
+
// e.g. "ReservationCreatedEvent" β "created" β "Create"
|
|
81
|
+
const withoutSuffix = eventName.replace(/Event$/, '');
|
|
82
|
+
const parts = withoutSuffix.split(/(?=[A-Z])/); // split on uppercase
|
|
83
|
+
const lastWord = parts[parts.length - 1].toLowerCase();
|
|
84
|
+
return EVENT_VERB_MAP[lastWord] || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractEventSubject(eventName) {
|
|
88
|
+
// e.g. "ReservationCreatedEvent" β "Reservation"
|
|
89
|
+
const withoutSuffix = eventName.replace(/Event$/, '');
|
|
90
|
+
const verb = extractEventVerb(eventName);
|
|
91
|
+
if (!verb) return withoutSuffix;
|
|
92
|
+
const verbKey = Object.keys(EVENT_VERB_MAP).find(
|
|
93
|
+
(k) => EVENT_VERB_MAP[k] === verb
|
|
94
|
+
);
|
|
95
|
+
if (!verbKey) return withoutSuffix;
|
|
96
|
+
// Remove the trailing verb word from the event name
|
|
97
|
+
const verbCamel = verbKey.charAt(0).toUpperCase() + verbKey.slice(1);
|
|
98
|
+
return withoutSuffix.replace(new RegExp(verbCamel + '$', 'i'), '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findTriggerEndpoint(verb, producerName, modulesConfig) {
|
|
102
|
+
if (!verb) return null;
|
|
103
|
+
const mod = modulesConfig.find((m) => m.name === producerName);
|
|
104
|
+
if (!mod) return null;
|
|
105
|
+
return (mod.exposes || []).find((ep) => {
|
|
106
|
+
const uc = ep.useCase || '';
|
|
107
|
+
return uc.toLowerCase().startsWith(verb.toLowerCase()) || uc.includes(verb);
|
|
108
|
+
}) || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildEventFlows(systemConfig, modulesMap) {
|
|
112
|
+
const asyncEvents = (systemConfig.integrations || {}).async || [];
|
|
113
|
+
const syncIntegrations = (systemConfig.integrations || {}).sync || [];
|
|
114
|
+
const modulesConfig = systemConfig.modules || [];
|
|
115
|
+
|
|
116
|
+
const flows = [];
|
|
117
|
+
|
|
118
|
+
for (const ev of asyncEvents) {
|
|
119
|
+
const verb = extractEventVerb(ev.event);
|
|
120
|
+
const subject = extractEventSubject(ev.event);
|
|
121
|
+
const triggerEndpoint = findTriggerEndpoint(verb, ev.producer, modulesConfig);
|
|
122
|
+
|
|
123
|
+
const producerMod = modulesMap[ev.producer] || { color: '#888888', label: ev.producer, icon: 'π' };
|
|
124
|
+
const consumers = (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module));
|
|
125
|
+
|
|
126
|
+
// Find sync calls made by this producer module that might be part of this action
|
|
127
|
+
const producerSyncCalls = syncIntegrations.filter((s) => s.caller === ev.producer);
|
|
128
|
+
|
|
129
|
+
const steps = [];
|
|
130
|
+
|
|
131
|
+
// Step 1: HTTP trigger (from client to producer)
|
|
132
|
+
if (triggerEndpoint) {
|
|
133
|
+
const syncCallsForStep = producerSyncCalls.map((s) => ({
|
|
134
|
+
to: s.calls,
|
|
135
|
+
label: (s.using || [])[0] || `GET /${s.calls}`,
|
|
136
|
+
port: s.port,
|
|
137
|
+
}));
|
|
138
|
+
steps.push({
|
|
139
|
+
id: 1,
|
|
140
|
+
type: 'http',
|
|
141
|
+
from: 'client',
|
|
142
|
+
to: ev.producer,
|
|
143
|
+
label: `${triggerEndpoint.method} ${triggerEndpoint.path}`,
|
|
144
|
+
desc: triggerEndpoint.description || `${verb} ${subject}`,
|
|
145
|
+
syncCalls: syncCallsForStep.length > 0 ? syncCallsForStep : undefined,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
steps.push({
|
|
149
|
+
id: 1,
|
|
150
|
+
type: 'http',
|
|
151
|
+
from: 'client',
|
|
152
|
+
to: ev.producer,
|
|
153
|
+
label: `${verb || 'trigger'} /${subject.toLowerCase()}`,
|
|
154
|
+
desc: `AcciΓ³n que desencadena el evento`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Step 2: Kafka event
|
|
159
|
+
steps.push({
|
|
160
|
+
id: 2,
|
|
161
|
+
type: 'event',
|
|
162
|
+
from: ev.producer,
|
|
163
|
+
event: ev.event,
|
|
164
|
+
topic: ev.topic,
|
|
165
|
+
to: consumers,
|
|
166
|
+
desc: `${ev.event} publicado en Kafka (topic: ${ev.topic})`,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Step 3+: Consumer actions
|
|
170
|
+
for (let i = 0; i < consumers.length; i++) {
|
|
171
|
+
const consumer = consumers[i];
|
|
172
|
+
const consumerMod = modulesConfig.find((m) => m.name === consumer);
|
|
173
|
+
// Find a likely endpoint that the consumer would trigger on receiving this event
|
|
174
|
+
let actionLabel = `Procesa ${ev.event}`;
|
|
175
|
+
if (consumerMod) {
|
|
176
|
+
const verbLower = (verb || '').toLowerCase();
|
|
177
|
+
const match = (consumerMod.exposes || []).find((ep) => {
|
|
178
|
+
const uc = (ep.useCase || '').toLowerCase();
|
|
179
|
+
const method = (ep.method || '').toUpperCase();
|
|
180
|
+
return (uc.includes(verbLower) || uc.includes(subject.toLowerCase())) &&
|
|
181
|
+
(method === 'PUT' || method === 'PATCH' || method === 'POST');
|
|
182
|
+
});
|
|
183
|
+
if (match) {
|
|
184
|
+
actionLabel = `${match.useCase} (${match.method} ${match.path})`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
steps.push({
|
|
188
|
+
id: i + 3,
|
|
189
|
+
type: 'action',
|
|
190
|
+
from: consumer,
|
|
191
|
+
to: consumer,
|
|
192
|
+
label: actionLabel,
|
|
193
|
+
desc: `${consumer} reacciona al evento ${ev.event}`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
flows.push({
|
|
198
|
+
id: ev.event,
|
|
199
|
+
label: ev.event.replace(/Event$/, '').replace(/([A-Z])/g, ' $1').trim(),
|
|
200
|
+
icon: producerMod.icon || 'π¨',
|
|
201
|
+
description: `${ev.producer} β [${consumers.join(', ')}] vΓa topic ${ev.topic}`,
|
|
202
|
+
color: producerMod.color,
|
|
203
|
+
steps,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return flows;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ββ Data extraction ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
211
|
+
|
|
212
|
+
function extractReportData(systemConfig, validation, domainValidation) {
|
|
213
|
+
const modulesConfig = systemConfig.modules || [];
|
|
214
|
+
const asyncEvents = (systemConfig.integrations || {}).async || [];
|
|
215
|
+
const syncIntegrations = (systemConfig.integrations || {}).sync || [];
|
|
216
|
+
|
|
217
|
+
// Build modules map with color + icon
|
|
218
|
+
const modulesMap = {};
|
|
219
|
+
for (let i = 0; i < modulesConfig.length; i++) {
|
|
220
|
+
const mod = modulesConfig[i];
|
|
221
|
+
modulesMap[mod.name] = {
|
|
222
|
+
id: mod.name,
|
|
223
|
+
label: toPascalCase(mod.name),
|
|
224
|
+
icon: assignIcon(mod.name),
|
|
225
|
+
color: COLOR_PALETTE[i % COLOR_PALETTE.length],
|
|
226
|
+
desc: mod.description || mod.name,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Normalize events (consumers can be strings or objects with .module)
|
|
231
|
+
const events = asyncEvents.map((ev) => ({
|
|
232
|
+
event: ev.event,
|
|
233
|
+
producer: ev.producer,
|
|
234
|
+
topic: ev.topic,
|
|
235
|
+
consumers: (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module)),
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
// Normalize sync integrations
|
|
239
|
+
const syncList = syncIntegrations.map((s) => ({
|
|
240
|
+
caller: s.caller,
|
|
241
|
+
calls: s.calls,
|
|
242
|
+
port: s.port || `${toPascalCase(s.calls)}Service`,
|
|
243
|
+
endpoints: s.using || [],
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
// Build endpoints per module
|
|
247
|
+
const endpoints = {};
|
|
248
|
+
for (const mod of modulesConfig) {
|
|
249
|
+
endpoints[mod.name] = (mod.exposes || []).map((ep) => `${ep.method} ${ep.path}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Auto-generate flows
|
|
253
|
+
const flows = buildEventFlows(systemConfig, modulesMap);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
systemName: (systemConfig.system || {}).name || 'eva4j system',
|
|
257
|
+
modules: Object.values(modulesMap),
|
|
258
|
+
events,
|
|
259
|
+
syncIntegrations: syncList,
|
|
260
|
+
endpoints,
|
|
261
|
+
flows,
|
|
262
|
+
validation,
|
|
263
|
+
domainValidation,
|
|
264
|
+
generatedAt: new Date().toISOString(),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ββ Command ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
269
|
+
|
|
270
|
+
async function evaluateSystemCommand(type, options = {}) {
|
|
271
|
+
if (type !== 'system') {
|
|
272
|
+
console.error(chalk.red(`β Unknown evaluation type: '${type}'`));
|
|
273
|
+
console.log(chalk.gray("Usage: eva evaluate system"));
|
|
274
|
+
console.log(chalk.gray("Only 'system' is supported at this time."));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const port = parseInt(options.port || '3000', 10);
|
|
279
|
+
const outputPath = path.resolve(process.cwd(), options.output || './system-report.html');
|
|
280
|
+
|
|
281
|
+
// ββ 1. Read system.yaml βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
282
|
+
const systemYamlPath = path.join(process.cwd(), 'system', 'system.yaml');
|
|
283
|
+
if (!(await fs.pathExists(systemYamlPath))) {
|
|
284
|
+
console.error(chalk.red('β system/system.yaml not found'));
|
|
285
|
+
console.error(chalk.gray('Run this command from the root of an eva4j project'));
|
|
286
|
+
console.error(chalk.gray('Expected location: system/system.yaml'));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let systemConfig;
|
|
291
|
+
try {
|
|
292
|
+
const content = await fs.readFile(systemYamlPath, 'utf-8');
|
|
293
|
+
systemConfig = yaml.load(content);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.error(chalk.red('β Failed to parse system/system.yaml:'), err.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const spinner = ora('Analyzing system/system.yaml...').start();
|
|
300
|
+
|
|
301
|
+
// ββ 2a. Load domain YAMLs (needed by both system and domain validation) ββ
|
|
302
|
+
const domainConfigs = {};
|
|
303
|
+
let domainValidation = null;
|
|
304
|
+
const systemDir = path.join(process.cwd(), 'system');
|
|
305
|
+
let allFiles;
|
|
306
|
+
try {
|
|
307
|
+
allFiles = await fs.readdir(systemDir);
|
|
308
|
+
} catch {
|
|
309
|
+
allFiles = [];
|
|
310
|
+
}
|
|
311
|
+
const domainFiles = allFiles.filter((f) => f.endsWith('.yaml') && f !== 'system.yaml');
|
|
312
|
+
|
|
313
|
+
if (domainFiles.length === 0) {
|
|
314
|
+
console.warn(chalk.yellow('β No domain YAML files found in system/ (excluding system.yaml). Domain tab will be hidden.'));
|
|
315
|
+
} else {
|
|
316
|
+
for (const file of domainFiles) {
|
|
317
|
+
const moduleName = path.basename(file, '.yaml');
|
|
318
|
+
try {
|
|
319
|
+
const content = await fs.readFile(path.join(systemDir, file), 'utf-8');
|
|
320
|
+
domainConfigs[moduleName] = yaml.load(content) || {};
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.warn(chalk.yellow(`β Could not parse ${file}: ${err.message}`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ββ 2b. Run system validation (receives domainConfigs to cross-check) βββ
|
|
328
|
+
const validation = validateSystem(systemConfig, domainConfigs);
|
|
329
|
+
|
|
330
|
+
// ββ 2c. Run domain validation βββββββββββββββββββββββββββββββββββββββββββ
|
|
331
|
+
if (Object.keys(domainConfigs).length > 0) {
|
|
332
|
+
domainValidation = validateDomain(domainConfigs, systemConfig);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ββ 3. Extract report data ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
336
|
+
const reportData = extractReportData(systemConfig, validation, domainValidation);
|
|
337
|
+
|
|
338
|
+
// ββ 4. Render HTML ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
339
|
+
const templatePath = path.join(__dirname, '../../templates/evaluate/report.html.ejs');
|
|
340
|
+
let htmlContent;
|
|
341
|
+
try {
|
|
342
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
343
|
+
htmlContent = ejs.render(templateContent, { data: reportData });
|
|
344
|
+
} catch (err) {
|
|
345
|
+
spinner.fail('Failed to render HTML template');
|
|
346
|
+
console.error(chalk.red(err.message));
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ββ 5. Write HTML file ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
351
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
352
|
+
await fs.writeFile(outputPath, htmlContent, 'utf-8');
|
|
353
|
+
|
|
354
|
+
// ββ 5b. Write domain assets βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
355
|
+
if (domainValidation) {
|
|
356
|
+
await writeDomainAssets(domainValidation, process.cwd());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ββ 5c. Write system-evaluation.md ββββββββββββββββββββββββββββββββββββββ
|
|
360
|
+
const evalMdPath = path.resolve(process.cwd(), 'assets', 'system-evaluation.md');
|
|
361
|
+
await fs.ensureDir(path.dirname(evalMdPath));
|
|
362
|
+
await writeSystemEvaluation(validation, systemConfig, evalMdPath);
|
|
363
|
+
|
|
364
|
+
spinner.succeed(chalk.green('Analysis complete!'));
|
|
365
|
+
|
|
366
|
+
// ββ 6. Print validation summary βββββββββββββββββββββββββββββββββββββββββ
|
|
367
|
+
console.log();
|
|
368
|
+
console.log(chalk.bold('π Validation Summary'));
|
|
369
|
+
console.log(chalk.gray('β'.repeat(40)));
|
|
370
|
+
console.log(
|
|
371
|
+
` ${chalk.red('π΄ Errors:')} ${chalk.red.bold(validation.errors.length)}`
|
|
372
|
+
);
|
|
373
|
+
console.log(
|
|
374
|
+
` ${chalk.yellow('π‘ Warnings:')} ${chalk.yellow.bold(validation.warnings.length)}`
|
|
375
|
+
);
|
|
376
|
+
console.log(
|
|
377
|
+
` ${chalk.cyan('π΅ Info:')} ${chalk.cyan.bold((validation.info || []).length)}`
|
|
378
|
+
);
|
|
379
|
+
console.log(
|
|
380
|
+
` ${chalk.green('π’ Passed:')} ${chalk.green.bold(validation.ok.length)}`
|
|
381
|
+
);
|
|
382
|
+
console.log(
|
|
383
|
+
` ${chalk.blue('π Score:')} ${chalk.blue.bold(validation.score + '%')}`
|
|
384
|
+
);
|
|
385
|
+
console.log();
|
|
386
|
+
|
|
387
|
+
if (validation.errors.length > 0) {
|
|
388
|
+
console.log(chalk.red('Critical issues found:'));
|
|
389
|
+
validation.errors.forEach((e) => console.log(chalk.red(` β’ ${e}`)));
|
|
390
|
+
console.log();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (validation.warnings.length > 0) {
|
|
394
|
+
console.log(chalk.yellow('Warnings:'));
|
|
395
|
+
validation.warnings.forEach((w) => console.log(chalk.yellow(` β’ ${w}`)));
|
|
396
|
+
console.log();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if ((validation.info || []).length > 0) {
|
|
400
|
+
console.log(chalk.cyan('Info:'));
|
|
401
|
+
validation.info.forEach((i) => console.log(chalk.cyan(` β’ ${i}`)));
|
|
402
|
+
console.log();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ββ 6b. Print domain validation summary ββββββββββββββββββββββββββββββββ
|
|
406
|
+
if (domainValidation) {
|
|
407
|
+
const ds = domainValidation.summary;
|
|
408
|
+
console.log(chalk.bold('ποΈ Domain Validation Summary'));
|
|
409
|
+
console.log(chalk.gray('β'.repeat(40)));
|
|
410
|
+
console.log(` ${chalk.red('π΄ Errors:')} ${chalk.red.bold(ds.errors)}`);
|
|
411
|
+
console.log(` ${chalk.yellow('π‘ Warnings:')} ${chalk.yellow.bold(ds.warnings)}`);
|
|
412
|
+
console.log(` ${chalk.blue('π΅ Info:')} ${chalk.blue.bold(ds.info)}`);
|
|
413
|
+
console.log(` ${chalk.green('π’ OK:')} ${chalk.green.bold(ds.ok)}`);
|
|
414
|
+
console.log();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ββ 7. Start HTTP server βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
418
|
+
const server = http.createServer((req, res) => {
|
|
419
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
420
|
+
res.end(htmlContent);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
server.listen(port, () => {
|
|
424
|
+
console.log(chalk.gray(`Report written to: ${outputPath}`));
|
|
425
|
+
console.log(chalk.gray(`Evaluation written to: assets/system-evaluation.md`));
|
|
426
|
+
console.log();
|
|
427
|
+
console.log(chalk.bold.green(`π Server running at: http://localhost:${port}`));
|
|
428
|
+
console.log(chalk.gray('Open the URL in your browser to view the report'));
|
|
429
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
server.on('error', (err) => {
|
|
433
|
+
if (err.code === 'EADDRINUSE') {
|
|
434
|
+
console.error(chalk.red(`β Port ${port} is already in use. Try --port <other-port>`));
|
|
435
|
+
} else {
|
|
436
|
+
console.error(chalk.red('β Server error:'), err.message);
|
|
437
|
+
}
|
|
438
|
+
process.exit(1);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
443
|
+
|
|
444
|
+
async function writeSystemEvaluation(validation, systemConfig, filePath) {
|
|
445
|
+
const systemName = (systemConfig.system || {}).name || 'eva4j system';
|
|
446
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
447
|
+
const scoreLabel = validation.score > 80 ? 'π’ Bueno' : validation.score > 60 ? 'π‘ Aceptable' : 'π΄ CrΓtico';
|
|
448
|
+
|
|
449
|
+
const lines = [];
|
|
450
|
+
|
|
451
|
+
lines.push(`# EvaluaciΓ³n del sistema β ${systemName}`);
|
|
452
|
+
lines.push('');
|
|
453
|
+
lines.push(`> Generado: ${now} `);
|
|
454
|
+
lines.push(`> Score de calidad: **${validation.score}%** ${scoreLabel} `);
|
|
455
|
+
lines.push(`> π΄ Errores: ${validation.errors.length} | π‘ Advertencias: ${validation.warnings.length}`);
|
|
456
|
+
lines.push('');
|
|
457
|
+
lines.push('---');
|
|
458
|
+
lines.push('');
|
|
459
|
+
|
|
460
|
+
if (validation.errors.length > 0) {
|
|
461
|
+
lines.push('## π΄ Errores crΓticos');
|
|
462
|
+
lines.push('');
|
|
463
|
+
for (const e of validation.errors) {
|
|
464
|
+
lines.push(`- ${e}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push('');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (validation.warnings.length > 0) {
|
|
470
|
+
lines.push('## π‘ Advertencias');
|
|
471
|
+
lines.push('');
|
|
472
|
+
for (const w of validation.warnings) {
|
|
473
|
+
lines.push(`- ${w}`);
|
|
474
|
+
}
|
|
475
|
+
lines.push('');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (validation.errors.length === 0 && validation.warnings.length === 0) {
|
|
479
|
+
lines.push('## β
Sin errores ni advertencias');
|
|
480
|
+
lines.push('');
|
|
481
|
+
lines.push('El sistema supera todas las validaciones de errores y advertencias.');
|
|
482
|
+
lines.push('');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function toPascalCase(str) {
|
|
489
|
+
return str
|
|
490
|
+
.replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase())
|
|
491
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ββ writeDomainAssets βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
495
|
+
async function writeDomainAssets(domainValidation, cwd) {
|
|
496
|
+
const assetsDir = path.join(cwd, 'assets', 'evaluation');
|
|
497
|
+
await fs.ensureDir(assetsDir);
|
|
498
|
+
|
|
499
|
+
const { categories, diagrams, summary } = domainValidation;
|
|
500
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
501
|
+
|
|
502
|
+
// ββ 1. Write per-module .mmd files ββββββββββββββββββββββββββββββββββββββ
|
|
503
|
+
if (diagrams) {
|
|
504
|
+
for (const [moduleName, diagramText] of Object.entries(diagrams)) {
|
|
505
|
+
if (diagramText) {
|
|
506
|
+
await fs.writeFile(path.join(assetsDir, `${moduleName}.mmd`), diagramText, 'utf-8');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ββ 2. Build evaluation.md ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
512
|
+
|
|
513
|
+
// Collect all findings grouped by module
|
|
514
|
+
const byModule = {};
|
|
515
|
+
for (const cat of categories) {
|
|
516
|
+
for (const check of cat.checks) {
|
|
517
|
+
for (const finding of check.findings) {
|
|
518
|
+
const mod = finding.module || '(sin mΓ³dulo)';
|
|
519
|
+
if (!byModule[mod]) byModule[mod] = [];
|
|
520
|
+
byModule[mod].push({
|
|
521
|
+
category: `${cat.id} β ${cat.label}`,
|
|
522
|
+
checkId: check.id,
|
|
523
|
+
checkLabel: check.label,
|
|
524
|
+
severity: check.severity,
|
|
525
|
+
message: finding.message,
|
|
526
|
+
context: finding.context || '',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const SEV_EMOJI = { error: 'π΄', warning: 'π‘', info: 'π΅', ok: 'π’' };
|
|
533
|
+
|
|
534
|
+
const lines = [];
|
|
535
|
+
lines.push(`# Domain Evaluation Report`);
|
|
536
|
+
lines.push(`> Generated: ${now}`);
|
|
537
|
+
lines.push('');
|
|
538
|
+
|
|
539
|
+
// Summary table
|
|
540
|
+
lines.push(`## Summary`);
|
|
541
|
+
lines.push('');
|
|
542
|
+
lines.push(`| π΄ Errors | π‘ Warnings | π΅ Info | π’ OK |`);
|
|
543
|
+
lines.push(`|-----------|-------------|---------|-------|`);
|
|
544
|
+
lines.push(`| ${summary.errors} | ${summary.warnings} | ${summary.info} | ${summary.ok} |`);
|
|
545
|
+
lines.push('');
|
|
546
|
+
|
|
547
|
+
const moduleNames = Object.keys(byModule).sort();
|
|
548
|
+
|
|
549
|
+
if (moduleNames.length === 0) {
|
|
550
|
+
lines.push('_No findings detected across all modules._');
|
|
551
|
+
} else {
|
|
552
|
+
lines.push(`## Findings by Module`);
|
|
553
|
+
lines.push('');
|
|
554
|
+
|
|
555
|
+
for (const moduleName of moduleNames) {
|
|
556
|
+
const findings = byModule[moduleName];
|
|
557
|
+
const errorCount = findings.filter(f => f.severity === 'error').length;
|
|
558
|
+
const warningCount = findings.filter(f => f.severity === 'warning').length;
|
|
559
|
+
const infoCount = findings.filter(f => f.severity === 'info').length;
|
|
560
|
+
|
|
561
|
+
const badges = [
|
|
562
|
+
errorCount ? `π΄ ${errorCount} error${errorCount !== 1 ? 's' : ''}` : null,
|
|
563
|
+
warningCount ? `π‘ ${warningCount} warning${warningCount !== 1 ? 's' : ''}` : null,
|
|
564
|
+
infoCount ? `π΅ ${infoCount} info` : null,
|
|
565
|
+
].filter(Boolean).join(' Β· ');
|
|
566
|
+
|
|
567
|
+
lines.push(`### \`${moduleName}\`${badges ? ` <sub>${badges}</sub>` : ''}`);
|
|
568
|
+
lines.push('');
|
|
569
|
+
|
|
570
|
+
if (diagrams && diagrams[moduleName]) {
|
|
571
|
+
lines.push(`> π Diagram: [${moduleName}.mmd](./${moduleName}.mmd)`);
|
|
572
|
+
lines.push('');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
lines.push(`| Severity | Check | Message | Context |`);
|
|
576
|
+
lines.push(`|----------|-------|---------|---------|`);
|
|
577
|
+
|
|
578
|
+
for (const f of findings) {
|
|
579
|
+
const sev = `${SEV_EMOJI[f.severity] || ''} ${f.severity}`;
|
|
580
|
+
const checkCell = `**${f.checkId}** ${f.checkLabel}`;
|
|
581
|
+
const msg = f.message.replace(/\|/g, '\\|');
|
|
582
|
+
const ctx = f.context.replace(/\|/g, '\\|');
|
|
583
|
+
lines.push(`| ${sev} | ${checkCell} | ${msg} | ${ctx} |`);
|
|
584
|
+
}
|
|
585
|
+
lines.push('');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Modules with diagrams but no findings
|
|
590
|
+
if (diagrams) {
|
|
591
|
+
const cleanModules = Object.keys(diagrams)
|
|
592
|
+
.filter(m => diagrams[m] && !byModule[m])
|
|
593
|
+
.sort();
|
|
594
|
+
if (cleanModules.length > 0) {
|
|
595
|
+
lines.push(`## Clean Modules (no findings)`);
|
|
596
|
+
lines.push('');
|
|
597
|
+
for (const m of cleanModules) {
|
|
598
|
+
lines.push(`- \`${m}\` β π’ no findings Β· [${m}.mmd](./${m}.mmd)`);
|
|
599
|
+
}
|
|
600
|
+
lines.push('');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
await fs.writeFile(path.join(assetsDir, 'evaluation.md'), lines.join('\n'), 'utf-8');
|
|
605
|
+
|
|
606
|
+
const mmdFiles = diagrams ? Object.keys(diagrams).filter(m => diagrams[m]) : [];
|
|
607
|
+
console.log(chalk.gray(` Domain assets β assets/evaluation/ (evaluation.md + ${mmdFiles.length} .mmd files)`));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
module.exports = evaluateSystemCommand;
|