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.
Files changed (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
@@ -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.artifactId}/`));
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.artifactId}`));
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();
@@ -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;