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.
Files changed (139) hide show
  1. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  2. package/.agents/skills/skill-creator/SKILL.md +485 -0
  3. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  4. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  5. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  6. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  7. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  8. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  9. package/.agents/skills/skill-creator/references/schemas.md +430 -0
  10. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  11. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  12. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  13. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  14. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  15. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  16. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  17. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  18. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  19. package/AGENTS.md +268 -6
  20. package/COMMAND_EVALUATION.md +15 -16
  21. package/DOMAIN_YAML_GUIDE.md +430 -14
  22. package/FUTURE_FEATURES.md +1627 -1168
  23. package/README.md +461 -13
  24. package/bin/eva4j.js +32 -14
  25. package/config/defaults.json +1 -0
  26. package/docs/commands/EVALUATE_SYSTEM.md +746 -261
  27. package/docs/commands/EXPORT_DIAGRAM.md +153 -0
  28. package/docs/commands/GENERATE_ENTITIES.md +599 -6
  29. package/docs/commands/INDEX.md +7 -0
  30. package/examples/domain-events.yaml +166 -20
  31. package/examples/domain-listeners.yaml +212 -0
  32. package/examples/domain-one-to-many.yaml +1 -0
  33. package/examples/domain-one-to-one.yaml +1 -0
  34. package/examples/domain-ports.yaml +414 -0
  35. package/examples/domain-soft-delete.yaml +47 -44
  36. package/examples/system/notification.yaml +147 -0
  37. package/examples/system/product.yaml +185 -0
  38. package/examples/system/system.yaml +112 -0
  39. package/examples/system-report.html +971 -0
  40. package/examples/system.yaml +46 -3
  41. package/package.json +2 -1
  42. package/src/agents/design-reviewer.agent.md +163 -0
  43. package/src/commands/build.js +714 -0
  44. package/src/commands/create.js +1 -0
  45. package/src/commands/detach.js +149 -108
  46. package/src/commands/evaluate-system.js +234 -8
  47. package/src/commands/export-diagram.js +522 -0
  48. package/src/commands/generate-entities.js +685 -66
  49. package/src/commands/generate-http-exchange.js +2 -0
  50. package/src/commands/generate-kafka-event.js +43 -10
  51. package/src/generators/base-generator.js +18 -6
  52. package/src/generators/postman-generator.js +188 -0
  53. package/src/generators/shared-generator.js +12 -2
  54. package/src/skills/build-system-yaml/SKILL.md +323 -0
  55. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
  56. package/src/skills/build-system-yaml/references/module-spec.md +367 -0
  57. package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
  58. package/src/utils/config-manager.js +54 -0
  59. package/src/utils/context-builder.js +1 -0
  60. package/src/utils/domain-diagram.js +192 -0
  61. package/src/utils/domain-validator.js +1020 -0
  62. package/src/utils/fake-data.js +376 -0
  63. package/src/utils/system-validator.js +319 -199
  64. package/src/utils/yaml-to-entity.js +272 -7
  65. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  66. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  67. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
  68. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  69. package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
  70. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  71. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  72. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  73. package/templates/base/docker/Dockerfile.ejs +21 -0
  74. package/templates/base/gradle/build.gradle.ejs +3 -2
  75. package/templates/base/root/AGENTS.md.ejs +306 -45
  76. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  77. package/templates/crud/Controller.java.ejs +4 -4
  78. package/templates/crud/CreateCommand.java.ejs +4 -0
  79. package/templates/crud/CreateCommandHandler.java.ejs +3 -6
  80. package/templates/crud/CreateItemDto.java.ejs +4 -0
  81. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  82. package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
  83. package/templates/crud/EndpointsController.java.ejs +6 -6
  84. package/templates/crud/FindByQuery.java.ejs +1 -1
  85. package/templates/crud/FindByQueryHandler.java.ejs +3 -9
  86. package/templates/crud/GetQueryHandler.java.ejs +2 -5
  87. package/templates/crud/ListQuery.java.ejs +1 -1
  88. package/templates/crud/ListQueryHandler.java.ejs +8 -14
  89. package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
  90. package/templates/crud/ScaffoldQuery.java.ejs +3 -2
  91. package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
  92. package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
  93. package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
  94. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
  95. package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
  96. package/templates/crud/UpdateCommand.java.ejs +4 -0
  97. package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
  98. package/templates/evaluate/report.html.ejs +394 -2
  99. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  100. package/templates/kafka-event/Event.java.ejs +9 -0
  101. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  102. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  103. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  104. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  105. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
  106. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  107. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  108. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  109. package/templates/mock/MockEvent.java.ejs +10 -0
  110. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  111. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  112. package/templates/mock/SpringEventListener.java.ejs +61 -0
  113. package/templates/ports/PortDomainModel.java.ejs +35 -0
  114. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  115. package/templates/ports/PortFeignClient.java.ejs +45 -0
  116. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  117. package/templates/ports/PortInterface.java.ejs +45 -0
  118. package/templates/ports/PortNestedType.java.ejs +28 -0
  119. package/templates/ports/PortRequestDto.java.ejs +30 -0
  120. package/templates/ports/PortResponseDto.java.ejs +28 -0
  121. package/templates/postman/Collection.json.ejs +1 -1
  122. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  123. package/templates/shared/annotations/LogAfter.java.ejs +2 -0
  124. package/templates/shared/annotations/LogBefore.java.ejs +2 -0
  125. package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
  126. package/templates/shared/annotations/LogLevel.java.ejs +8 -0
  127. package/templates/shared/annotations/LogTimer.java.ejs +1 -0
  128. package/templates/shared/annotations/Loggable.java.ejs +17 -0
  129. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
  130. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
  131. package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
  132. package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
  133. package/src/commands/generate-system.js +0 -243
  134. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
  135. package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
  136. package/templates/base/root/skill-build-system-yaml.ejs +0 -252
  137. package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
  138. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
  139. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
@@ -76,6 +76,7 @@ async function createCommand(projectName, options) {
76
76
  console.log(chalk.gray(` β”œβ”€β”€ src/main/java/${context.packagePath.replace(/\//g, '.')}`));
77
77
  console.log(chalk.gray(` β”‚ β”œβ”€β”€ ${context.applicationClassName}.java`));
78
78
  console.log(chalk.gray(` β”‚ └── common/`));
79
+ console.log(chalk.gray(` β”œβ”€β”€ system/`));
79
80
  console.log(chalk.gray(` β”œβ”€β”€ build.gradle`));
80
81
  console.log(chalk.gray(` └── README.md`));
81
82
 
@@ -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}`,
@@ -228,15 +229,20 @@ async function detachCommand(moduleName, options) {
228
229
  // Step 3: Generate base project using BaseGenerator
229
230
  await generateDetachedProject(newProjectDir, detachedContext);
230
231
 
232
+ spinner.text = 'Copying Gradle wrapper...';
233
+
234
+ // Step 4: Copy Gradle wrapper from parent project
235
+ await copyGradleWrapper(projectDir, newProjectDir);
236
+
231
237
  spinner.text = 'Copying module files...';
232
238
 
233
- // Step 4: Copy module directory
239
+ // Step 5: Copy module directory
234
240
  const newModuleDir = path.join(newProjectDir, 'src', 'main', 'java', packagePath, moduleName);
235
241
  await fs.copy(moduleDir, newModuleDir);
236
242
 
237
243
  spinner.text = 'Merging shared components...';
238
244
 
239
- // Step 5: Merge shared/domain into module/domain
245
+ // Step 6: Merge shared/domain into module/domain (filtering modulith-only components)
240
246
  await mergeSharedComponents(
241
247
  sharedDir,
242
248
  newModuleDir,
@@ -246,7 +252,7 @@ async function detachCommand(moduleName, options) {
246
252
 
247
253
  spinner.text = 'Updating package references...';
248
254
 
249
- // Step 6: Update all imports in module files
255
+ // Step 7: Update all imports in module files
250
256
  await updatePackageReferences(
251
257
  newModuleDir,
252
258
  packageName,
@@ -255,10 +261,10 @@ async function detachCommand(moduleName, options) {
255
261
 
256
262
  spinner.text = 'Cleaning up...';
257
263
 
258
- // Step 7: Remove package-info.java files
264
+ // Step 8: Remove package-info.java files
259
265
  await removePackageInfoFiles(newModuleDir);
260
266
 
261
- // Step 8: Copy test files if they exist
267
+ // Step 9: Copy test files if they exist
262
268
  const testModuleDir = path.join(projectDir, 'src', 'test', 'java', packagePath, moduleName);
263
269
  if (await fs.pathExists(testModuleDir)) {
264
270
  const newTestModuleDir = path.join(newProjectDir, 'src', 'test', 'java', packagePath, moduleName);
@@ -266,7 +272,7 @@ async function detachCommand(moduleName, options) {
266
272
  await updatePackageReferences(newTestModuleDir, packageName, moduleName);
267
273
  }
268
274
 
269
- // Step 9: Create detached project configuration
275
+ // Step 10: Create detached project configuration
270
276
  const detachedConfigManager = new ConfigManager(newProjectDir);
271
277
  await detachedConfigManager.saveProjectConfig({
272
278
  ...detachedContext,
@@ -280,8 +286,11 @@ async function detachCommand(moduleName, options) {
280
286
 
281
287
  spinner.text = 'Copying environment configurations...';
282
288
 
283
- // Step 10: Copy environment profile files from parent resources
284
- await copyEnvironmentProfiles(projectDir, newProjectDir, packageName, moduleName);
289
+ // Step 11: Copy parameter files from parent (kafka.yaml, urls.yaml, etc.)
290
+ await copyParameterFiles(projectDir, newProjectDir, packageName, moduleName);
291
+
292
+ // Step 12: Copy domain.yaml specification if it exists
293
+ await copyDomainYaml(projectDir, newProjectDir, moduleName);
285
294
 
286
295
  spinner.succeed(chalk.green('βœ… Module detached successfully! ✨'));
287
296
 
@@ -372,74 +381,43 @@ function transformSharedFileContent(content, packageName, moduleName) {
372
381
  }
373
382
 
374
383
  /**
375
- * Merge shared components into module structure
384
+ * Directories that are specific to Spring Modulith (modular monolith)
385
+ * and should NOT be copied when detaching to a standalone microservice.
386
+ */
387
+ const MODULITH_ONLY_DIRS = new Set([
388
+ 'eventPublicationConfig', // Spring Modulith event_publication table
389
+ 'mockEvent', // In-memory event surrogate for mock mode
390
+ ]);
391
+
392
+ /**
393
+ * Files specific to Spring Modulith that should be skipped during merge.
394
+ */
395
+ const MODULITH_ONLY_FILES = new Set([
396
+ 'package-info.java', // Contains @ApplicationModule(type = OPEN)
397
+ ]);
398
+
399
+ /**
400
+ * Merge shared components into module structure, filtering out modulith-only components.
376
401
  */
377
402
  async function mergeSharedComponents(sharedDir, moduleDir, packageName, moduleName) {
378
- // Merge shared/domain/* into module/domain/
379
- const sharedDomainDir = path.join(sharedDir, 'domain');
380
- if (await fs.pathExists(sharedDomainDir)) {
381
- const domainEntries = await fs.readdir(sharedDomainDir);
382
- for (const entry of domainEntries) {
383
- const sourcePath = path.join(sharedDomainDir, entry);
384
- const stat = await fs.stat(sourcePath);
385
-
386
- if (stat.isDirectory()) {
387
- // Copy and transform subdirectories (annotations, interfaces, customExceptions, etc.)
388
- const destPath = path.join(moduleDir, 'domain', entry);
389
- await copyAndTransformDirectory(sourcePath, destPath, packageName, moduleName);
390
- } else if (stat.isFile() && entry.endsWith('.java')) {
391
- // Copy and transform individual .java files (AuditableEntity.java, etc.)
392
- const destPath = path.join(moduleDir, 'domain', entry);
393
- if (!(await fs.pathExists(destPath))) {
394
- const content = await fs.readFile(sourcePath, 'utf-8');
395
- const transformed = transformSharedFileContent(content, packageName, moduleName);
396
- await fs.ensureDir(path.dirname(destPath));
397
- await fs.writeFile(destPath, transformed, 'utf-8');
398
- }
399
- }
400
- }
401
- }
402
-
403
- // Merge shared/infrastructure/* into module/infrastructure/
404
- const sharedInfraDir = path.join(sharedDir, 'infrastructure');
405
- if (await fs.pathExists(sharedInfraDir)) {
406
- const infraEntries = await fs.readdir(sharedInfraDir);
407
- for (const entry of infraEntries) {
408
- const sourcePath = path.join(sharedInfraDir, entry);
409
- const stat = await fs.stat(sourcePath);
410
-
411
- if (stat.isDirectory()) {
412
- // Copy and transform subdirectories
413
- const destPath = path.join(moduleDir, 'infrastructure', entry);
414
- await copyAndTransformDirectory(sourcePath, destPath, packageName, moduleName);
415
- } else if (stat.isFile() && entry.endsWith('.java')) {
416
- // Copy and transform individual .java files
417
- const destPath = path.join(moduleDir, 'infrastructure', entry);
418
- if (!(await fs.pathExists(destPath))) {
419
- const content = await fs.readFile(sourcePath, 'utf-8');
420
- const transformed = transformSharedFileContent(content, packageName, moduleName);
421
- await fs.ensureDir(path.dirname(destPath));
422
- await fs.writeFile(destPath, transformed, 'utf-8');
423
- }
424
- }
425
- }
426
- }
427
-
428
- // Merge shared/application/* into module/application/
429
- const sharedApplicationDir = path.join(sharedDir, 'application');
430
- if (await fs.pathExists(sharedApplicationDir)) {
431
- const applicationEntries = await fs.readdir(sharedApplicationDir);
432
- for (const entry of applicationEntries) {
433
- const sourcePath = path.join(sharedApplicationDir, entry);
403
+ const layers = ['domain', 'infrastructure', 'application'];
404
+
405
+ for (const layer of layers) {
406
+ const sharedLayerDir = path.join(sharedDir, layer);
407
+ if (!(await fs.pathExists(sharedLayerDir))) continue;
408
+
409
+ const entries = await fs.readdir(sharedLayerDir);
410
+ for (const entry of entries) {
411
+ const sourcePath = path.join(sharedLayerDir, entry);
434
412
  const stat = await fs.stat(sourcePath);
435
-
413
+
436
414
  if (stat.isDirectory()) {
437
- // Copy and transform subdirectories (dtos, events)
438
- const destPath = path.join(moduleDir, 'application', entry);
415
+ if (MODULITH_ONLY_DIRS.has(entry)) continue;
416
+ const destPath = path.join(moduleDir, layer, entry);
439
417
  await copyAndTransformDirectory(sourcePath, destPath, packageName, moduleName);
440
418
  } else if (stat.isFile() && entry.endsWith('.java')) {
441
- // Copy and transform individual .java files
442
- const destPath = path.join(moduleDir, 'application', entry);
419
+ if (MODULITH_ONLY_FILES.has(entry)) continue;
420
+ const destPath = path.join(moduleDir, layer, entry);
443
421
  if (!(await fs.pathExists(destPath))) {
444
422
  const content = await fs.readFile(sourcePath, 'utf-8');
445
423
  const transformed = transformSharedFileContent(content, packageName, moduleName);
@@ -605,55 +583,118 @@ async function findPackageInfoFiles(dir) {
605
583
  }
606
584
 
607
585
  /**
608
- * Copy environment profile files from parent to detached project
586
+ * Copy Gradle wrapper files from parent project to detached project.
587
+ * Includes gradlew, gradlew.bat, and gradle/wrapper/ directory.
609
588
  */
610
- async function copyEnvironmentProfiles(parentDir, newProjectDir, packageName, moduleName) {
611
- const parentResourcesDir = path.join(parentDir, 'src', 'main', 'resources');
612
- const newResourcesDir = path.join(newProjectDir, 'src', 'main', 'resources');
613
-
614
- // Copy environment profile files
615
- const profileFiles = [
616
- 'application-develop.yaml',
617
- 'application-local.yaml',
618
- 'application-production.yaml',
619
- 'application-test.yaml'
620
- ];
621
-
622
- for (const file of profileFiles) {
623
- const sourcePath = path.join(parentResourcesDir, file);
624
- if (await fs.pathExists(sourcePath)) {
625
- await fs.copy(sourcePath, path.join(newResourcesDir, file));
589
+ async function copyGradleWrapper(parentDir, newProjectDir) {
590
+ const wrapperFiles = ['gradlew', 'gradlew.bat'];
591
+ for (const file of wrapperFiles) {
592
+ const src = path.join(parentDir, file);
593
+ if (await fs.pathExists(src)) {
594
+ await fs.copy(src, path.join(newProjectDir, file));
626
595
  }
627
596
  }
628
-
629
- // Copy parameters folder if it exists
630
- const parametersDir = path.join(parentResourcesDir, 'parameters');
631
- if (await fs.pathExists(parametersDir)) {
632
- await fs.copy(parametersDir, path.join(newResourcesDir, 'parameters'));
633
-
634
- // Update package references in kafka.yaml files
635
- await updateKafkaConfigReferences(newResourcesDir, packageName, moduleName);
597
+
598
+ const wrapperDir = path.join(parentDir, 'gradle', 'wrapper');
599
+ if (await fs.pathExists(wrapperDir)) {
600
+ await fs.copy(wrapperDir, path.join(newProjectDir, 'gradle', 'wrapper'));
636
601
  }
637
602
  }
638
603
 
639
604
  /**
640
- * Update package references in kafka.yaml files
605
+ * Copy parameter files (kafka.yaml, urls.yaml, etc.) from parent to detached project.
606
+ * Updates package references from shared to module name.
607
+ * Adds missing imports to application-{env}.yaml files.
641
608
  */
642
- async function updateKafkaConfigReferences(resourcesDir, packageName, moduleName) {
609
+ async function copyParameterFiles(parentDir, newProjectDir, packageName, moduleName) {
610
+ const parentParametersDir = path.join(parentDir, 'src', 'main', 'resources', 'parameters');
611
+ const newParametersDir = path.join(newProjectDir, 'src', 'main', 'resources', 'parameters');
612
+
613
+ if (!(await fs.pathExists(parentParametersDir))) return;
614
+
615
+ await fs.copy(parentParametersDir, newParametersDir);
616
+
617
+ // Update package references in yaml config files (kafka.yaml, urls.yaml, etc.)
643
618
  const environments = ['local', 'develop', 'test', 'production'];
644
-
619
+ const sharedPattern = new RegExp(
620
+ `${packageName.replace(/\./g, '\\.')}\\.shared\\.infrastructure\\.`,
621
+ 'g'
622
+ );
623
+
624
+ // Base config files that BaseGenerator already imports (db.yaml, cors.yaml)
625
+ const baseImports = new Set(['db.yaml', 'cors.yaml']);
626
+
645
627
  for (const env of environments) {
646
- const kafkaYmlPath = path.join(resourcesDir, 'parameters', env, 'kafka.yaml');
647
-
648
- if (await fs.pathExists(kafkaYmlPath)) {
649
- let content = await fs.readFile(kafkaYmlPath, 'utf-8');
650
-
651
- // Replace .shared.infrastructure. with .{moduleName}.infrastructure.
652
- const pattern = new RegExp(`${packageName}\\.shared\\.infrastructure\\.`, 'g');
653
- content = content.replace(pattern, `${packageName}.${moduleName}.infrastructure.`);
654
-
655
- await fs.writeFile(kafkaYmlPath, content, 'utf-8');
628
+ const envDir = path.join(newParametersDir, env);
629
+ if (!(await fs.pathExists(envDir))) continue;
630
+
631
+ const files = await fs.readdir(envDir);
632
+ const extraImports = [];
633
+
634
+ for (const file of files) {
635
+ if (!file.endsWith('.yaml') && !file.endsWith('.yml')) continue;
636
+
637
+ // Update package references
638
+ const filePath = path.join(envDir, file);
639
+ let content = await fs.readFile(filePath, 'utf-8');
640
+ if (sharedPattern.test(content)) {
641
+ content = content.replace(sharedPattern, `${packageName}.${moduleName}.infrastructure.`);
642
+ await fs.writeFile(filePath, content, 'utf-8');
643
+ }
644
+ sharedPattern.lastIndex = 0;
645
+
646
+ // Collect extra parameter files that need imports
647
+ if (!baseImports.has(file)) {
648
+ extraImports.push(`classpath:parameters/${env}/${file}`);
649
+ }
656
650
  }
651
+
652
+ // Add missing imports to application-{env}.yaml
653
+ if (extraImports.length > 0) {
654
+ await addMissingImports(newProjectDir, env, extraImports);
655
+ }
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Add missing classpath imports to an application-{env}.yaml file.
661
+ */
662
+ async function addMissingImports(projectDir, env, imports) {
663
+ const appYmlPath = path.join(projectDir, 'src', 'main', 'resources', `application-${env}.yaml`);
664
+ if (!(await fs.pathExists(appYmlPath))) return;
665
+
666
+ let content = await fs.readFile(appYmlPath, 'utf-8');
667
+
668
+ const linesToAdd = imports.filter(imp => !content.includes(imp));
669
+ if (linesToAdd.length === 0) return;
670
+
671
+ // Append after existing import entries
672
+ const importPattern = /(spring:\s*\n\s*config:\s*\n\s*import:\s*\n(?:\s*-\s*"[^"]+"\s*\n)*)/;
673
+ if (importPattern.test(content)) {
674
+ const suffix = linesToAdd.map(imp => ` - "${imp}"`).join('\n') + '\n';
675
+ content = content.replace(importPattern, `$1${suffix}`);
676
+ }
677
+
678
+ await fs.writeFile(appYmlPath, content, 'utf-8');
679
+ }
680
+
681
+ /**
682
+ * Copy domain.yaml specification for the module if it exists.
683
+ * Looks in system/{moduleName}.yaml and domain.yaml at project root.
684
+ */
685
+ async function copyDomainYaml(parentDir, newProjectDir, moduleName) {
686
+ // Try system/{moduleName}.yaml first (multi-module system layout)
687
+ const systemYaml = path.join(parentDir, 'system', `${moduleName}.yaml`);
688
+ if (await fs.pathExists(systemYaml)) {
689
+ await fs.ensureDir(path.join(newProjectDir, 'system'));
690
+ await fs.copy(systemYaml, path.join(newProjectDir, 'system', `${moduleName}.yaml`));
691
+ return;
692
+ }
693
+
694
+ // Fallback: domain.yaml at project root (single-module layout)
695
+ const domainYaml = path.join(parentDir, 'domain.yaml');
696
+ if (await fs.pathExists(domainYaml)) {
697
+ await fs.copy(domainYaml, path.join(newProjectDir, 'domain.yaml'));
657
698
  }
658
699
  }
659
700
 
@@ -9,6 +9,7 @@ const ejs = require('ejs');
9
9
  const ora = require('ora');
10
10
 
11
11
  const { validateSystem } = require('../utils/system-validator');
12
+ const { validateDomain } = require('../utils/domain-validator');
12
13
 
13
14
  // ── Module icon heuristic ────────────────────────────────────────────────────
14
15
 
@@ -208,7 +209,7 @@ function buildEventFlows(systemConfig, modulesMap) {
208
209
 
209
210
  // ── Data extraction ──────────────────────────────────────────────────────────
210
211
 
211
- function extractReportData(systemConfig, validation) {
212
+ function extractReportData(systemConfig, validation, domainValidation) {
212
213
  const modulesConfig = systemConfig.modules || [];
213
214
  const asyncEvents = (systemConfig.integrations || {}).async || [];
214
215
  const syncIntegrations = (systemConfig.integrations || {}).sync || [];
@@ -259,6 +260,7 @@ function extractReportData(systemConfig, validation) {
259
260
  endpoints,
260
261
  flows,
261
262
  validation,
263
+ domainValidation,
262
264
  generatedAt: new Date().toISOString(),
263
265
  };
264
266
  }
@@ -277,10 +279,11 @@ async function evaluateSystemCommand(type, options = {}) {
277
279
  const outputPath = path.resolve(process.cwd(), options.output || './system-report.html');
278
280
 
279
281
  // ── 1. Read system.yaml ─────────────────────────────────────────────────
280
- const systemYamlPath = path.join(process.cwd(), 'system.yaml');
282
+ const systemYamlPath = path.join(process.cwd(), 'system', 'system.yaml');
281
283
  if (!(await fs.pathExists(systemYamlPath))) {
282
- console.error(chalk.red('❌ system.yaml not found in current directory'));
284
+ console.error(chalk.red('❌ system/system.yaml not found'));
283
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'));
284
287
  process.exit(1);
285
288
  }
286
289
 
@@ -289,17 +292,48 @@ async function evaluateSystemCommand(type, options = {}) {
289
292
  const content = await fs.readFile(systemYamlPath, 'utf-8');
290
293
  systemConfig = yaml.load(content);
291
294
  } catch (err) {
292
- console.error(chalk.red('❌ Failed to parse system.yaml:'), err.message);
295
+ console.error(chalk.red('❌ Failed to parse system/system.yaml:'), err.message);
293
296
  process.exit(1);
294
297
  }
295
298
 
296
- const spinner = ora('Analyzing system.yaml...').start();
299
+ const spinner = ora('Analyzing system/system.yaml...').start();
297
300
 
298
- // ── 2. Run validation ───────────────────────────────────────────────────
299
- const validation = validateSystem(systemConfig);
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
+ }
300
334
 
301
335
  // ── 3. Extract report data ──────────────────────────────────────────────
302
- const reportData = extractReportData(systemConfig, validation);
336
+ const reportData = extractReportData(systemConfig, validation, domainValidation);
303
337
 
304
338
  // ── 4. Render HTML ──────────────────────────────────────────────────────
305
339
  const templatePath = path.join(__dirname, '../../templates/evaluate/report.html.ejs');
@@ -317,6 +351,16 @@ async function evaluateSystemCommand(type, options = {}) {
317
351
  await fs.ensureDir(path.dirname(outputPath));
318
352
  await fs.writeFile(outputPath, htmlContent, 'utf-8');
319
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
+
320
364
  spinner.succeed(chalk.green('Analysis complete!'));
321
365
 
322
366
  // ── 6. Print validation summary ─────────────────────────────────────────
@@ -329,6 +373,9 @@ async function evaluateSystemCommand(type, options = {}) {
329
373
  console.log(
330
374
  ` ${chalk.yellow('🟑 Warnings:')} ${chalk.yellow.bold(validation.warnings.length)}`
331
375
  );
376
+ console.log(
377
+ ` ${chalk.cyan('πŸ”΅ Info:')} ${chalk.cyan.bold((validation.info || []).length)}`
378
+ );
332
379
  console.log(
333
380
  ` ${chalk.green('🟒 Passed:')} ${chalk.green.bold(validation.ok.length)}`
334
381
  );
@@ -349,6 +396,24 @@ async function evaluateSystemCommand(type, options = {}) {
349
396
  console.log();
350
397
  }
351
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
+
352
417
  // ── 7. Start HTTP server ─────────────────────────────────────────────────
353
418
  const server = http.createServer((req, res) => {
354
419
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -357,6 +422,7 @@ async function evaluateSystemCommand(type, options = {}) {
357
422
 
358
423
  server.listen(port, () => {
359
424
  console.log(chalk.gray(`Report written to: ${outputPath}`));
425
+ console.log(chalk.gray(`Evaluation written to: assets/system-evaluation.md`));
360
426
  console.log();
361
427
  console.log(chalk.bold.green(`🌐 Server running at: http://localhost:${port}`));
362
428
  console.log(chalk.gray('Open the URL in your browser to view the report'));
@@ -375,10 +441,170 @@ async function evaluateSystemCommand(type, options = {}) {
375
441
 
376
442
  // ── Helpers ──────────────────────────────────────────────────────────────────
377
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
+
378
488
  function toPascalCase(str) {
379
489
  return str
380
490
  .replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase())
381
491
  .replace(/^(.)/, (c) => c.toUpperCase());
382
492
  }
383
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
+
384
610
  module.exports = evaluateSystemCommand;