eva4j 1.0.16 → 1.0.18

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 (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. package/COMMAND_EVALUATION.md +0 -911
@@ -5,15 +5,19 @@ const fs = require('fs-extra');
5
5
  const inquirer = require('inquirer');
6
6
  const ConfigManager = require('../utils/config-manager');
7
7
  const { isEva4jProject } = require('../utils/validator');
8
- const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord } = require('../utils/naming');
8
+ const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord, singularizeWord } = require('../utils/naming');
9
9
  const { renderAndWrite, renderTemplate } = require('../utils/template-engine');
10
- const { parseDomainYaml, generateEntityImports, generateValidationImports } = require('../utils/yaml-to-entity');
10
+ const { parseDomainYaml, generateEntityImports, generateValidationImports, resolveLifecycleEventArgs, resolveEventArgs } = require('../utils/yaml-to-entity');
11
11
  const { createOrUpdateUrlsConfig, ensureUrlsImport } = require('./generate-http-exchange');
12
12
  const SharedGenerator = require('../generators/shared-generator');
13
13
  const ChecksumManager = require('../utils/checksum-manager');
14
14
  const { generateFakeValue, initSeed } = require('../utils/fake-data');
15
15
  const { getInstalledBroker, generateSingleKafkaEvent, buildKafkaEventContext, updateKafkaYml,
16
16
  generateEventRecord, createOrUpdateMessageBroker, updateDomainEventHandler } = require('./generate-kafka-event');
17
+ const { generateSingleRabbitEvent, buildRabbitEventContext, updateRabbitMQYml, updateRabbitMQYmlQueue,
18
+ createOrUpdateRabbitMessageBroker, updateRabbitMQConfigForConsumer, updateRabbitMQYmlForConsumer } = require('./generate-rabbitmq-event');
19
+ const { parseSystemYaml } = require('../utils/system-yaml-parser');
20
+ const { computeWorkflowInputFields } = require('./generate-temporal-system');
17
21
 
18
22
  // Maximum depth for recursive relationship traversal
19
23
  const MAX_DEPTH = 5;
@@ -216,6 +220,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
216
220
  const { packageName, artifactId } = projectConfig;
217
221
  const packagePath = toPackagePath(packageName);
218
222
 
223
+ // Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
224
+ moduleName = toCamelCase(moduleName);
225
+
219
226
  // Validate module exists
220
227
  if (!(await configManager.moduleExists(moduleName))) {
221
228
  console.error(chalk.red(`❌ Module '${moduleName}' not found`));
@@ -244,7 +251,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
244
251
 
245
252
  try {
246
253
  // Parse domain.yaml
247
- const { aggregates, allEnums, endpoints, listeners, ports } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
254
+ const { aggregates, allEnums, endpoints, listeners, ports, readModels } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
248
255
 
249
256
  spinner.succeed(chalk.green(`Found ${aggregates.length} aggregate(s) and ${allEnums.length} enum(s)`));
250
257
 
@@ -273,7 +280,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
273
280
  }
274
281
 
275
282
  // Detect installed message broker for auto-wiring integration events
276
- const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0))
283
+ const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0) || (readModels && readModels.length > 0))
277
284
  ? await getInstalledBroker(configManager)
278
285
  : null;
279
286
  // When brokerMode:'mock' is requested AND a broker is installed, use mock Spring-Events adapter
@@ -281,6 +288,33 @@ async function generateEntitiesCommand(moduleName, options = {}) {
281
288
  ? 'mock'
282
289
  : installedBroker;
283
290
 
291
+ // Detect Temporal for auto-wiring DomainEvent → Workflow bridge
292
+ const hasNotifiesInModule = aggregates.some(agg =>
293
+ (agg.domainEvents || []).some(e => (e.notifies || []).length > 0)
294
+ );
295
+ const temporalInstalled = hasNotifiesInModule
296
+ ? await configManager.featureExists('temporal')
297
+ : false;
298
+
299
+ // Build workflow → inputFields map for typed DomainEvent → Workflow bridge
300
+ const workflowInputMap = new Map();
301
+ if (temporalInstalled) {
302
+ try {
303
+ const systemDir = path.join(projectDir, 'system');
304
+ if (await fs.pathExists(path.join(systemDir, 'system.yaml'))) {
305
+ const { workflows } = await parseSystemYaml(systemDir);
306
+ for (const wf of workflows) {
307
+ const inputFields = computeWorkflowInputFields(wf.steps);
308
+ const flowPascal = wf.namePascal.replace(/Workflow$/, '');
309
+ workflowInputMap.set(wf.namePascal, { inputFields, flowPascal });
310
+ workflowInputMap.set(wf.name, { inputFields, flowPascal });
311
+ }
312
+ }
313
+ } catch (_) {
314
+ // system.yaml not required — standalone modules work without it
315
+ }
316
+ }
317
+
284
318
  // Generate audit-related shared components if needed
285
319
  if (hasAuditableEntities || hasTrackUserEntities) {
286
320
 
@@ -369,6 +403,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
369
403
  const { name: aggregateName, rootEntity, secondaryEntities, valueObjects } = aggregate;
370
404
 
371
405
  // 1. Generate Domain Aggregate Root
406
+ const resolvedLifecycle = resolveLifecycleEventArgs(
407
+ aggregate.lifecycleEventsMap || {}, rootEntity.name, rootEntity.fields, valueObjects
408
+ );
372
409
  const rootDomainContext = {
373
410
  packageName,
374
411
  moduleName,
@@ -381,7 +418,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
381
418
  auditable: rootEntity.auditable,
382
419
  hasSoftDelete: rootEntity.hasSoftDelete || false,
383
420
  domainEvents: aggregate.domainEvents || [],
384
- triggeredEventsMap: aggregate.triggeredEventsMap || {}
421
+ triggeredEventsMap: aggregate.triggeredEventsMap || {},
422
+ lifecycleEventsMap: resolvedLifecycle
385
423
  };
386
424
 
387
425
  await renderAndWrite(
@@ -393,6 +431,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
393
431
  generatedFiles.push({ type: 'Domain Entity', name: rootEntity.name, path: `${moduleName}/domain/models/entities/${rootEntity.name}.java` });
394
432
 
395
433
  // 2. Generate JPA Aggregate Root
434
+ const hasCreateLifecycle = !!(aggregate.lifecycleEventsMap && aggregate.lifecycleEventsMap.create && aggregate.lifecycleEventsMap.create.length > 0);
396
435
  const rootJpaContext = {
397
436
  packageName,
398
437
  moduleName,
@@ -405,7 +444,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
405
444
  enums: allEnums,
406
445
  auditable: rootEntity.auditable,
407
446
  audit: rootEntity.audit,
408
- hasSoftDelete: rootEntity.hasSoftDelete || false
447
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
448
+ hasCreateLifecycle
409
449
  };
410
450
 
411
451
  await renderAndWrite(
@@ -524,6 +564,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
524
564
  moduleName,
525
565
  rootEntity,
526
566
  hasSoftDelete: rootEntity.hasSoftDelete || false,
567
+ hasDomainEvents: (aggregate.domainEvents || []).length > 0,
568
+ hasDeleteLifecycle: !!(aggregate.lifecycleEventsMap || {}).delete,
527
569
  findByOps: []
528
570
  };
529
571
 
@@ -551,6 +593,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
551
593
  aggregateName,
552
594
  rootEntity,
553
595
  hasDomainEvents: (aggregate.domainEvents || []).length > 0,
596
+ hasDeleteLifecycle: !!(aggregate.lifecycleEventsMap || {}).delete,
554
597
  hasSoftDelete: rootEntity.hasSoftDelete || false,
555
598
  findByOps: []
556
599
  };
@@ -592,9 +635,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
592
635
  aggregateName,
593
636
  domainEvents: aggregateDomainEvents.map(e => ({
594
637
  ...e,
595
- integrationEventClassName: `${e.name}IntegrationEvent`
638
+ integrationEventClassName: `${e.name}IntegrationEvent`,
639
+ notifies: (e.notifies || []).map(n => {
640
+ const wfKey = toPascalCase(n.workflow || '');
641
+ const meta = workflowInputMap.get(wfKey);
642
+ return {
643
+ ...n,
644
+ inputFields: meta ? meta.inputFields : [],
645
+ flowPascal: meta ? meta.flowPascal : wfKey.replace(/Workflow$/, ''),
646
+ };
647
+ }),
596
648
  })),
597
- broker
649
+ broker,
650
+ temporal: temporalInstalled
598
651
  };
599
652
  await renderAndWrite(
600
653
  path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventHandler.java.ejs'),
@@ -687,6 +740,52 @@ async function generateEntitiesCommand(moduleName, options = {}) {
687
740
  name: 'MessageBroker (updated)',
688
741
  path: `${moduleName}/application/ports/MessageBroker.java`
689
742
  });
743
+ } else if (broker === 'rabbitmq') {
744
+ // ── RabbitMQ broker: RabbitTemplate adapter ──────────────────────────
745
+ const STANDARD_EVENT_TYPES = new Set([
746
+ 'String','Integer','Long','Double','Float','Boolean',
747
+ 'BigDecimal','LocalDate','LocalDateTime','LocalTime','Instant','UUID'
748
+ ]);
749
+ for (const event of aggregateDomainEvents) {
750
+ const rabbitCtx = buildRabbitEventContext(packageName, moduleName, event);
751
+ await generateSingleRabbitEvent(projectDir, packagePath, rabbitCtx);
752
+ generatedFiles.push({
753
+ type: 'Integration Event',
754
+ name: rabbitCtx.eventClassName,
755
+ path: `${moduleName}/application/events/${rabbitCtx.eventClassName}.java`
756
+ });
757
+
758
+ // Generate stub records for custom collection element types
759
+ const customElementTypes = [...new Set(
760
+ (event.fields || [])
761
+ .filter(f => f.isCollection && f.collectionElementType && !STANDARD_EVENT_TYPES.has(f.collectionElementType))
762
+ .map(f => f.collectionElementType)
763
+ )];
764
+ for (const typeName of customElementTypes) {
765
+ const stubPath = path.join(moduleBasePath, 'domain', 'models', 'events', `${typeName}.java`);
766
+ await renderAndWrite(
767
+ path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventSnapshot.java.ejs'),
768
+ stubPath,
769
+ { packageName, moduleName, name: typeName, fields: [] },
770
+ { ...writeOptions, overwrite: false }
771
+ );
772
+ generatedFiles.push({
773
+ type: 'Event Snapshot Type',
774
+ name: typeName,
775
+ path: `${moduleName}/domain/models/events/${typeName}.java`
776
+ });
777
+ }
778
+ }
779
+ generatedFiles.push({
780
+ type: 'Integration Event',
781
+ name: `${toPascalCase(moduleName)}RabbitMessageBroker (updated)`,
782
+ path: `${moduleName}/infrastructure/adapters/rabbitmqMessageBroker/${toPascalCase(moduleName)}RabbitMessageBroker.java`
783
+ });
784
+ generatedFiles.push({
785
+ type: 'Integration Event',
786
+ name: 'MessageBroker (updated)',
787
+ path: `${moduleName}/application/ports/MessageBroker.java`
788
+ });
690
789
  }
691
790
  }
692
791
  }
@@ -708,10 +807,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
708
807
  const topicRaw = listener.topic;
709
808
  const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
710
809
  const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
810
+ const kafkaListenerClassName = `${listener.baseName}KafkaListener`;
711
811
  const listenerContext = {
712
812
  packageName,
713
813
  moduleName,
714
814
  ...listener,
815
+ listenerClassName: kafkaListenerClassName,
715
816
  topicConstant: topicRaw,
716
817
  topicSpringProperty: `\${topics.${topicKey}}`,
717
818
  topicVariableName: toCamelCase(topicSuffix.toLowerCase())
@@ -757,7 +858,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
757
858
  // If the file is currently a mock Spring listener, force overwrite with the real Kafka impl
758
859
  const kafkaListenerPath = path.join(
759
860
  moduleBasePath, 'infrastructure', 'kafkaListener',
760
- `${listener.listenerClassName}.java`
861
+ `${kafkaListenerClassName}.java`
761
862
  );
762
863
  let kafkaListenerWriteOpts = writeOptions;
763
864
  if (await fs.pathExists(kafkaListenerPath)) {
@@ -774,8 +875,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
774
875
  );
775
876
  generatedFiles.push({
776
877
  type: 'Kafka Listener',
777
- name: listener.listenerClassName,
778
- path: `${moduleName}/infrastructure/kafkaListener/${listener.listenerClassName}.java`
878
+ name: kafkaListenerClassName,
879
+ path: `${moduleName}/infrastructure/kafkaListener/${kafkaListenerClassName}.java`
779
880
  });
780
881
 
781
882
  // 3. Register topic in kafka.yaml (all environments)
@@ -832,10 +933,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
832
933
  const topicRaw = listener.topic;
833
934
  const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
834
935
  const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
936
+ const mockListenerClassName = `${listener.baseName}KafkaListener`;
835
937
  const listenerContext = {
836
938
  packageName,
837
939
  moduleName,
838
940
  ...listener,
941
+ listenerClassName: mockListenerClassName,
839
942
  topicConstant: topicRaw,
840
943
  topicSpringProperty: `\${topics.${topicKey}}`,
841
944
  topicVariableName: toCamelCase(topicSuffix.toLowerCase())
@@ -864,14 +967,14 @@ async function generateEntitiesCommand(moduleName, options = {}) {
864
967
  generatedFiles.push({ type: 'Listener Integration Event', name: listener.integrationEventClassName, path: `${moduleName}/application/events/${listener.integrationEventClassName}.java` });
865
968
 
866
969
  // 2. Spring @EventListener class (mock — same file path as Kafka listener)
867
- const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${listener.listenerClassName}.java`);
970
+ const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${mockListenerClassName}.java`);
868
971
  await renderAndWrite(
869
972
  path.join(__dirname, '..', '..', 'templates', 'mock', 'SpringEventListener.java.ejs'),
870
973
  listenerPath,
871
974
  listenerContext,
872
975
  { ...writeOptions, force: true }
873
976
  );
874
- generatedFiles.push({ type: 'Spring Listener (mock)', name: listener.listenerClassName, path: `${moduleName}/infrastructure/kafkaListener/${listener.listenerClassName}.java` });
977
+ generatedFiles.push({ type: 'Spring Listener (mock)', name: mockListenerClassName, path: `${moduleName}/infrastructure/kafkaListener/${mockListenerClassName}.java` });
875
978
 
876
979
  // 3. NO kafka.yaml update in mock mode
877
980
 
@@ -896,8 +999,146 @@ async function generateEntitiesCommand(moduleName, options = {}) {
896
999
  generatedFiles.push({ type: 'Handler', name: `${listener.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java` });
897
1000
  }
898
1001
  spinner.succeed(chalk.green(`Spring Event listeners generated (mock mode)! ✨`));
1002
+ } else if (broker === 'rabbitmq') {
1003
+ // ── RabbitMQ listeners: @RabbitListener ─────────────────────────────
1004
+ spinner.start(`Generating ${listeners.length} RabbitMQ listener(s)...`);
1005
+ for (const listener of listeners) {
1006
+ if (!listener.topic) {
1007
+ spinner.warn(chalk.yellow(`⚠ listener '${listener.event}': topic is required when there is no system.yaml. Skipping.`));
1008
+ continue;
1009
+ }
1010
+
1011
+ const topicRaw = listener.topic;
1012
+ const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
1013
+ const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
1014
+ const rabbitListenerClassName = `${listener.baseName}RabbitListener`;
1015
+ const consumerTopicKey = `${moduleName}-${topicKey}`;
1016
+ const listenerContext = {
1017
+ packageName,
1018
+ moduleName,
1019
+ ...listener,
1020
+ listenerClassName: rabbitListenerClassName,
1021
+ topicConstant: topicRaw,
1022
+ topicSpringProperty: `\${queues.${consumerTopicKey}}`,
1023
+ topicVariableName: toCamelCase(topicSuffix.toLowerCase())
1024
+ };
1025
+
1026
+ // 0. Nested type records (auxiliary value objects for object-typed fields)
1027
+ for (const nt of (listener.nestedTypes || [])) {
1028
+ const ntPath = path.join(
1029
+ moduleBasePath, 'application', 'events',
1030
+ `${nt.name}.java`
1031
+ );
1032
+ await renderAndWrite(
1033
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerNestedType.java.ejs'),
1034
+ ntPath,
1035
+ { packageName, moduleName, name: nt.name, fields: nt.fields },
1036
+ writeOptions
1037
+ );
1038
+ generatedFiles.push({
1039
+ type: 'Listener Nested Type',
1040
+ name: nt.name,
1041
+ path: `${moduleName}/application/events/${nt.name}.java`
1042
+ });
1043
+ }
1044
+
1045
+ // 1. Integration Event record (same template — broker-agnostic)
1046
+ const integrationEventPath = path.join(
1047
+ moduleBasePath, 'application', 'events',
1048
+ `${listener.integrationEventClassName}.java`
1049
+ );
1050
+ await renderAndWrite(
1051
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
1052
+ integrationEventPath,
1053
+ listenerContext,
1054
+ writeOptions
1055
+ );
1056
+ generatedFiles.push({
1057
+ type: 'Listener Integration Event',
1058
+ name: listener.integrationEventClassName,
1059
+ path: `${moduleName}/application/events/${listener.integrationEventClassName}.java`
1060
+ });
1061
+
1062
+ // 2. RabbitMQ listener class
1063
+ const rabbitListenerPath = path.join(
1064
+ moduleBasePath, 'infrastructure', 'rabbitListener',
1065
+ `${rabbitListenerClassName}.java`
1066
+ );
1067
+ let rabbitListenerWriteOpts = writeOptions;
1068
+ if (await fs.pathExists(rabbitListenerPath)) {
1069
+ const existing = await fs.readFile(rabbitListenerPath, 'utf-8');
1070
+ if (existing.includes('@EventListener') && !existing.includes('@RabbitListener')) {
1071
+ rabbitListenerWriteOpts = { ...writeOptions, force: true };
1072
+ }
1073
+ }
1074
+ await renderAndWrite(
1075
+ path.join(__dirname, '..', '..', 'templates', 'rabbitmq-listener', 'RabbitListenerClass.java.ejs'),
1076
+ rabbitListenerPath,
1077
+ listenerContext,
1078
+ rabbitListenerWriteOpts
1079
+ );
1080
+ generatedFiles.push({
1081
+ type: 'RabbitMQ Listener',
1082
+ name: rabbitListenerClassName,
1083
+ path: `${moduleName}/infrastructure/rabbitListener/${rabbitListenerClassName}.java`
1084
+ });
1085
+
1086
+ // 3. Register consumer infrastructure in rabbitmq.yaml + RabbitMQConfig.java
1087
+ // Use module-prefixed keys to avoid collision with producer-side in monolith mode
1088
+ const producerModule = toCamelCase(listener.producer || moduleName);
1089
+ const consumerExchangeName = `${producerModule}.events`;
1090
+ const consumerRoutingKey = topicKey.replace(/-/g, '.');
1091
+ const consumerQueueName = `${moduleName}.${topicKey}`;
1092
+ const consumerBeanMethodName = `${toCamelCase(consumerTopicKey)}Topic`;
1093
+
1094
+ await updateRabbitMQYmlForConsumer(
1095
+ projectDir, consumerTopicKey, consumerQueueName,
1096
+ producerModule, consumerExchangeName, consumerRoutingKey
1097
+ );
1098
+ await updateRabbitMQConfigForConsumer(projectDir, packagePath, {
1099
+ producerModule,
1100
+ topicKey: consumerTopicKey,
1101
+ beanMethodName: consumerBeanMethodName,
1102
+ valueFieldName: consumerBeanMethodName
1103
+ });
1104
+
1105
+ // 4. Typed Command dispatched from the listener
1106
+ const commandPath = path.join(
1107
+ moduleBasePath, 'application', 'commands',
1108
+ `${listener.commandClassName}.java`
1109
+ );
1110
+ await renderAndWrite(
1111
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommand.java.ejs'),
1112
+ commandPath,
1113
+ listenerContext,
1114
+ writeOptions
1115
+ );
1116
+ generatedFiles.push({
1117
+ type: 'Listener Command',
1118
+ name: listener.commandClassName,
1119
+ path: `${moduleName}/application/commands/${listener.commandClassName}.java`
1120
+ });
1121
+
1122
+ // 5. Use case handler that processes the command
1123
+ const handlerPath = path.join(
1124
+ moduleBasePath, 'application', 'usecases',
1125
+ `${listener.useCase}CommandHandler.java`
1126
+ );
1127
+ await renderAndWrite(
1128
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommandHandler.java.ejs'),
1129
+ handlerPath,
1130
+ listenerContext,
1131
+ writeOptions
1132
+ );
1133
+ generatedFiles.push({
1134
+ type: 'Handler',
1135
+ name: `${listener.useCase}CommandHandler`,
1136
+ path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java`
1137
+ });
1138
+ }
1139
+ spinner.succeed(chalk.green(`RabbitMQ listeners generated! ✨`));
899
1140
  } else if (listeners.length > 0) {
900
- console.log(chalk.yellow(`⚠ listeners: section found but no broker is installed. Run 'eva add kafka-client' to generate listener classes.`));
1141
+ console.log(chalk.yellow(`⚠ listeners: section found but no broker is installed. Run 'eva add kafka-client' or 'eva add rabbitmq-client' to generate listener classes.`));
901
1142
  }
902
1143
  }
903
1144
 
@@ -924,6 +1165,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
924
1165
 
925
1166
  const adapterDir = path.join(moduleBasePath, 'infrastructure', 'adapters', adapterPackage);
926
1167
 
1168
+ const aclMapperClassName = `${serviceName}AclMapper`;
1169
+
927
1170
  const portContext = {
928
1171
  packageName,
929
1172
  moduleName,
@@ -936,6 +1179,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
936
1179
  feignClientClassName,
937
1180
  feignAdapterClassName,
938
1181
  feignConfigClassName,
1182
+ aclMapperClassName,
939
1183
  adapterPackage,
940
1184
  methods,
941
1185
  nestedTypes,
@@ -976,9 +1220,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
976
1220
  });
977
1221
  }
978
1222
 
979
- // 1b. Infra DTOs (one per method that has fields:) — live in infrastructure/adapters/{service}/
1223
+ // 1b. Infra DTOs (one per method that has fields:) — live in infrastructure/adapters/{service}/dtos/
980
1224
  for (const method of methods.filter(m => m.hasResponse)) {
981
- const infraDtoPath = path.join(adapterDir, `${method.infraDtoName}.java`);
1225
+ const infraDtoPath = path.join(adapterDir, 'dtos', `${method.infraDtoName}.java`);
982
1226
  await renderAndWrite(
983
1227
  path.join(__dirname, '..', '..', 'templates', 'ports', 'PortResponseDto.java.ejs'),
984
1228
  infraDtoPath,
@@ -988,7 +1232,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
988
1232
  generatedFiles.push({
989
1233
  type: 'Port Infra DTO',
990
1234
  name: method.infraDtoName,
991
- path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${method.infraDtoName}.java`
1235
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/dtos/${method.infraDtoName}.java`
992
1236
  });
993
1237
  }
994
1238
 
@@ -1049,6 +1293,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1049
1293
  path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignAdapterClassName}.java`
1050
1294
  });
1051
1295
 
1296
+ // 5b. ACL Mapper (maps infra DTOs → domain models)
1297
+ await renderAndWrite(
1298
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortAclMapper.java.ejs'),
1299
+ path.join(adapterDir, `${aclMapperClassName}.java`),
1300
+ portContext,
1301
+ writeOptions
1302
+ );
1303
+ generatedFiles.push({
1304
+ type: 'HTTP Port',
1305
+ name: aclMapperClassName,
1306
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${aclMapperClassName}.java`
1307
+ });
1308
+
1052
1309
  // 6. Feign Config
1053
1310
  await renderAndWrite(
1054
1311
  path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignConfig.java.ejs'),
@@ -1072,6 +1329,230 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1072
1329
  spinner.succeed(chalk.green(`HTTP ports generated! ✨`));
1073
1330
  }
1074
1331
 
1332
+ // ── Generate Read Models (local projections of external data) ────────────
1333
+ if (readModels && readModels.length > 0) {
1334
+ if (broker === 'kafka' || broker === 'rabbitmq' || broker === 'mock') {
1335
+ spinner.start(`Generating ${readModels.length} read model(s)...`);
1336
+
1337
+ const readModelTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'read-model');
1338
+
1339
+ for (const rm of readModels) {
1340
+ const rmContext = {
1341
+ packageName,
1342
+ moduleName,
1343
+ ...rm
1344
+ };
1345
+
1346
+ // 1. Domain read model class
1347
+ const domainPath = path.join(
1348
+ moduleBasePath, 'domain', 'models', 'readmodels',
1349
+ `${rm.domainClassName}.java`
1350
+ );
1351
+ await renderAndWrite(
1352
+ path.join(readModelTemplatesDir, 'ReadModelDomain.java.ejs'),
1353
+ domainPath,
1354
+ rmContext,
1355
+ writeOptions
1356
+ );
1357
+ generatedFiles.push({
1358
+ type: 'Read Model',
1359
+ name: rm.domainClassName,
1360
+ path: `${moduleName}/domain/models/readmodels/${rm.domainClassName}.java`
1361
+ });
1362
+
1363
+ // 2. JPA entity
1364
+ const jpaPath = path.join(
1365
+ moduleBasePath, 'infrastructure', 'database', 'entities',
1366
+ `${rm.jpaEntityName}.java`
1367
+ );
1368
+ await renderAndWrite(
1369
+ path.join(readModelTemplatesDir, 'ReadModelJpa.java.ejs'),
1370
+ jpaPath,
1371
+ rmContext,
1372
+ writeOptions
1373
+ );
1374
+ generatedFiles.push({
1375
+ type: 'Read Model',
1376
+ name: rm.jpaEntityName,
1377
+ path: `${moduleName}/infrastructure/database/entities/${rm.jpaEntityName}.java`
1378
+ });
1379
+
1380
+ // 3. JPA repository
1381
+ const jpaRepoPath = path.join(
1382
+ moduleBasePath, 'infrastructure', 'database', 'repositories',
1383
+ `${rm.jpaRepositoryName}.java`
1384
+ );
1385
+ await renderAndWrite(
1386
+ path.join(readModelTemplatesDir, 'ReadModelJpaRepository.java.ejs'),
1387
+ jpaRepoPath,
1388
+ rmContext,
1389
+ writeOptions
1390
+ );
1391
+ generatedFiles.push({
1392
+ type: 'Read Model',
1393
+ name: rm.jpaRepositoryName,
1394
+ path: `${moduleName}/infrastructure/database/repositories/${rm.jpaRepositoryName}.java`
1395
+ });
1396
+
1397
+ // 4. Domain repository interface
1398
+ const repoPath = path.join(
1399
+ moduleBasePath, 'domain', 'repositories',
1400
+ `${rm.repositoryName}.java`
1401
+ );
1402
+ await renderAndWrite(
1403
+ path.join(readModelTemplatesDir, 'ReadModelRepository.java.ejs'),
1404
+ repoPath,
1405
+ rmContext,
1406
+ writeOptions
1407
+ );
1408
+ generatedFiles.push({
1409
+ type: 'Read Model',
1410
+ name: rm.repositoryName,
1411
+ path: `${moduleName}/domain/repositories/${rm.repositoryName}.java`
1412
+ });
1413
+
1414
+ // 5. Repository implementation
1415
+ const repoImplPath = path.join(
1416
+ moduleBasePath, 'infrastructure', 'database', 'repositories',
1417
+ `${rm.repositoryImplName}.java`
1418
+ );
1419
+ await renderAndWrite(
1420
+ path.join(readModelTemplatesDir, 'ReadModelRepositoryImpl.java.ejs'),
1421
+ repoImplPath,
1422
+ rmContext,
1423
+ writeOptions
1424
+ );
1425
+ generatedFiles.push({
1426
+ type: 'Read Model',
1427
+ name: rm.repositoryImplName,
1428
+ path: `${moduleName}/infrastructure/database/repositories/${rm.repositoryImplName}.java`
1429
+ });
1430
+
1431
+ // 6. Sync handler
1432
+ const handlerPath = path.join(
1433
+ moduleBasePath, 'application', 'usecases',
1434
+ `${rm.syncHandlerName}.java`
1435
+ );
1436
+ await renderAndWrite(
1437
+ path.join(readModelTemplatesDir, 'ReadModelSyncHandler.java.ejs'),
1438
+ handlerPath,
1439
+ rmContext,
1440
+ writeOptions
1441
+ );
1442
+ generatedFiles.push({
1443
+ type: 'Read Model',
1444
+ name: rm.syncHandlerName,
1445
+ path: `${moduleName}/application/usecases/${rm.syncHandlerName}.java`
1446
+ });
1447
+
1448
+ // 7. Per syncedBy event: integration event + Kafka listener + topic registration
1449
+ for (const sync of rm.syncedBy) {
1450
+ const listenerContext = {
1451
+ packageName,
1452
+ moduleName,
1453
+ ...sync,
1454
+ syncHandlerName: rm.syncHandlerName,
1455
+ domainClassName: rm.domainClassName,
1456
+ repositoryName: rm.repositoryName
1457
+ };
1458
+
1459
+ // Integration event (reuse if already exists from listeners: section)
1460
+ // Only generate for UPSERT — DELETE/SOFT_DELETE bypass IntegrationEvent entirely
1461
+ const integrationEventPath = path.join(
1462
+ moduleBasePath, 'application', 'events',
1463
+ `${sync.integrationEventClassName}.java`
1464
+ );
1465
+ if (sync.action === 'UPSERT' && !(await fs.pathExists(integrationEventPath))) {
1466
+ await renderAndWrite(
1467
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
1468
+ integrationEventPath,
1469
+ {
1470
+ packageName,
1471
+ moduleName,
1472
+ integrationEventClassName: sync.integrationEventClassName,
1473
+ topicConstant: sync.topicConstant,
1474
+ producer: rm.sourceModule,
1475
+ fields: sync.fields
1476
+ },
1477
+ writeOptions
1478
+ );
1479
+ generatedFiles.push({
1480
+ type: 'Read Model Integration Event',
1481
+ name: sync.integrationEventClassName,
1482
+ path: `${moduleName}/application/events/${sync.integrationEventClassName}.java`
1483
+ });
1484
+ }
1485
+
1486
+ // Kafka listener
1487
+ if (broker === 'kafka') {
1488
+ const kafkaListenerPath = path.join(
1489
+ moduleBasePath, 'infrastructure', 'kafkaListener',
1490
+ `${sync.listenerClassName}.java`
1491
+ );
1492
+ await renderAndWrite(
1493
+ path.join(readModelTemplatesDir, 'ReadModelKafkaListener.java.ejs'),
1494
+ kafkaListenerPath,
1495
+ listenerContext,
1496
+ writeOptions
1497
+ );
1498
+ generatedFiles.push({
1499
+ type: 'Read Model Kafka Listener',
1500
+ name: sync.listenerClassName,
1501
+ path: `${moduleName}/infrastructure/kafkaListener/${sync.listenerClassName}.java`
1502
+ });
1503
+
1504
+ // Register topic in kafka.yaml
1505
+ await updateKafkaYml(projectDir, sync.topicKey, sync.topicConstant);
1506
+ } else if (broker === 'rabbitmq') {
1507
+ const rabbitListenerPath = path.join(
1508
+ moduleBasePath, 'infrastructure', 'rabbitListener',
1509
+ `${sync.listenerClassName}.java`
1510
+ );
1511
+ const rmConsumerTopicKey = `${moduleName}-${sync.topicKey}`;
1512
+ const rabbitListenerContext = {
1513
+ ...listenerContext,
1514
+ topicSpringProperty: `\${queues.${rmConsumerTopicKey}}`
1515
+ };
1516
+ await renderAndWrite(
1517
+ path.join(readModelTemplatesDir, 'ReadModelRabbitListener.java.ejs'),
1518
+ rabbitListenerPath,
1519
+ rabbitListenerContext,
1520
+ writeOptions
1521
+ );
1522
+ generatedFiles.push({
1523
+ type: 'Read Model RabbitMQ Listener',
1524
+ name: sync.listenerClassName,
1525
+ path: `${moduleName}/infrastructure/rabbitListener/${sync.listenerClassName}.java`
1526
+ });
1527
+
1528
+ // Register consumer infrastructure in rabbitmq.yaml + RabbitMQConfig.java
1529
+ // Use module-prefixed keys to avoid collision with producer-side in monolith mode
1530
+ const rmProducerModule = toCamelCase(rm.sourceModule || moduleName);
1531
+ const rmExchangeName = `${rmProducerModule}.events`;
1532
+ const rmRoutingKey = sync.topicKey.replace(/-/g, '.');
1533
+ const rmQueueName = `${moduleName}.${sync.topicKey}`;
1534
+ const rmBeanMethodName = `${toCamelCase(rmConsumerTopicKey)}Topic`;
1535
+
1536
+ await updateRabbitMQYmlForConsumer(
1537
+ projectDir, rmConsumerTopicKey, rmQueueName,
1538
+ rmProducerModule, rmExchangeName, rmRoutingKey
1539
+ );
1540
+ await updateRabbitMQConfigForConsumer(projectDir, packagePath, {
1541
+ producerModule: rmProducerModule,
1542
+ topicKey: rmConsumerTopicKey,
1543
+ beanMethodName: rmBeanMethodName,
1544
+ valueFieldName: rmBeanMethodName
1545
+ });
1546
+ }
1547
+ }
1548
+ }
1549
+
1550
+ spinner.succeed(chalk.green(`Read models generated! ✨`));
1551
+ } else if (readModels.length > 0) {
1552
+ console.log(chalk.yellow(`⚠ readModels: section found but no broker is installed. Run 'eva add kafka-client' or 'eva add rabbitmq-client' to generate listener classes.`));
1553
+ }
1554
+ }
1555
+
1075
1556
  console.log(chalk.blue('\n📦 Generated files:'));
1076
1557
  const groupedFiles = generatedFiles.reduce((acc, file) => {
1077
1558
  if (!acc[file.type]) acc[file.type] = [];
@@ -1113,8 +1594,18 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1113
1594
  }
1114
1595
  }
1115
1596
  if (!op._ownerAggregate) {
1116
- // True scaffold: heuristic — find aggregate whose name appears inside the use case name
1117
- const matched = aggregates.find(agg =>
1597
+ // True scaffold: heuristic — strip CRUD verb prefix and singularize to find
1598
+ // the best aggregate match via prefix comparison.
1599
+ // e.g. "FindAllGuarantees" → suffix "Guarantees" → singular "Guarantee"
1600
+ // → "GuaranteeCatalog".startsWith("guarantee") ✓
1601
+ const ucSuffix = op.useCase.replace(/^(FindAll|GetAll|Get|Create|Update|Delete|Add|Remove)/, '');
1602
+ const singularSuffix = ucSuffix ? singularizeWord(ucSuffix).toLowerCase() : '';
1603
+ const matched = (singularSuffix &&
1604
+ aggregates.find(agg => {
1605
+ const aggLower = agg.name.toLowerCase();
1606
+ return aggLower.startsWith(singularSuffix) || singularSuffix.startsWith(aggLower);
1607
+ })
1608
+ ) || aggregates.find(agg =>
1118
1609
  op.useCase.toLowerCase().includes(agg.name.toLowerCase())
1119
1610
  );
1120
1611
  const owner = matched || aggregates[0];
@@ -1410,11 +1901,14 @@ function classifyUseCase(op, aggregateName, aggregate) {
1410
1901
  };
1411
1902
  }
1412
1903
  if (op.useCase === `Remove${rel.target}`) {
1904
+ const removeTargetEntity = (aggregate.secondaryEntities || []).find(e => e.name === rel.target);
1905
+ const removeIdField = removeTargetEntity ? removeTargetEntity.fields.find(f => f.name === 'id') : null;
1413
1906
  return {
1414
1907
  category: 'subEntityRemove',
1415
1908
  entityName: rel.target,
1416
1909
  fieldName: rel.fieldName,
1417
- removeMethodName: `remove${rel.target}ById`
1910
+ removeMethodName: `remove${rel.target}ById`,
1911
+ itemIdType: removeIdField ? removeIdField.javaType : 'String'
1418
1912
  };
1419
1913
  }
1420
1914
  }
@@ -1434,6 +1928,34 @@ function classifyUseCase(op, aggregateName, aggregate) {
1434
1928
  }
1435
1929
  }
1436
1930
 
1931
+ // 5. Fuzzy FindAll — useCase starts with "FindAll" and the singular of the
1932
+ // suffix is a prefix of the aggregate name (or vice-versa).
1933
+ // e.g. FindAllGuarantees → singular "Guarantee" → "GuaranteeCatalog" starts with it ✓
1934
+ if (op.useCase.startsWith('FindAll')) {
1935
+ const suffix = op.useCase.slice(7);
1936
+ if (suffix) {
1937
+ const singularSuffix = singularizeWord(suffix).toLowerCase();
1938
+ const aggLower = aggregateName.toLowerCase();
1939
+ if (aggLower.startsWith(singularSuffix) || singularSuffix.startsWith(aggLower)) {
1940
+ return { category: 'standard', variant: 'findAll' };
1941
+ }
1942
+ }
1943
+ }
1944
+
1945
+ // 6. Fuzzy Get — useCase starts with "Get" and the suffix matches the
1946
+ // aggregate name as a prefix (or vice-versa).
1947
+ // e.g. GetCatTariff → "CatTariff" matches aggregate "CatTariff" ✓
1948
+ if (op.useCase.startsWith('Get') && !op.useCase.startsWith('GetAll')) {
1949
+ const suffix = op.useCase.slice(3);
1950
+ if (suffix) {
1951
+ const suffixLower = suffix.toLowerCase();
1952
+ const aggLower = aggregateName.toLowerCase();
1953
+ if (aggLower.startsWith(suffixLower) || suffixLower.startsWith(aggLower)) {
1954
+ return { category: 'standard', variant: 'getById' };
1955
+ }
1956
+ }
1957
+ }
1958
+
1437
1959
  return { category: 'scaffold' };
1438
1960
  }
1439
1961
 
@@ -1552,13 +2074,18 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1552
2074
  const oneToManyRelationshipsApp = enrichRelsWithSchemaExamples(
1553
2075
  transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
1554
2076
 
2077
+ const resolvedLifecycleEndpoints = resolveLifecycleEventArgs(
2078
+ aggregate.lifecycleEventsMap || {}, aggregateName, rootEntity.fields, valueObjects
2079
+ );
1555
2080
  const baseContext = {
1556
2081
  packageName, moduleName, aggregateName, aggregateNamePlural, rootEntity, secondaryEntities,
1557
2082
  responseFields, responseSecondaryEntities, idType,
1558
2083
  commandFields: commandFieldsApp, oneToManyRelationships, oneToOneRelationships,
1559
2084
  hasValueObjects, hasEnums, imports: rootEntity.imports,
1560
2085
  resourceNameCamel, resourceNameKebab,
1561
- hasSoftDelete: rootEntity.hasSoftDelete || false
2086
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
2087
+ domainEvents: aggregate.domainEvents || [],
2088
+ lifecycleEventsMap: resolvedLifecycleEndpoints
1562
2089
  };
1563
2090
 
1564
2091
  // ── Step 1: Validated VO Dtos ────────────────────────────────────────
@@ -1819,7 +2346,8 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1819
2346
  useCaseName: op.useCase,
1820
2347
  idType,
1821
2348
  entityName: cl.entityName,
1822
- removeMethodName: cl.removeMethodName
2349
+ removeMethodName: cl.removeMethodName,
2350
+ itemIdType: cl.itemIdType || 'String'
1823
2351
  };
1824
2352
  await renderAndWrite(
1825
2353
  path.join(templatesDir, 'SubEntityRemoveCommand.java.ejs'),
@@ -1863,7 +2391,10 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1863
2391
 
1864
2392
  } else {
1865
2393
  // Scaffold: no semantic pattern matched → generate stub with TODO
1866
- const scaffoldContext = { packageName, moduleName, aggregateName, useCaseName: op.useCase };
2394
+ const hasPathVar = Boolean(op.path && op.path.includes('{'));
2395
+ const pathVarMatch = hasPathVar ? op.path.match(/\{([^}]+)\}/) : null;
2396
+ const pathVarName = pathVarMatch ? pathVarMatch[1] : 'id';
2397
+ const scaffoldContext = { packageName, moduleName, aggregateName, useCaseName: op.useCase, hasPathVar, pathVarName, idType };
1867
2398
  const scaffoldType = op.type || (op.method === 'GET' ? 'query' : 'command');
1868
2399
  if (scaffoldType === 'command') {
1869
2400
  await renderAndWrite(
@@ -2065,6 +2596,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
2065
2596
  transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
2066
2597
 
2067
2598
  // Base context for all templates
2599
+ const resolvedLifecycleCrud = resolveLifecycleEventArgs(
2600
+ aggregate.lifecycleEventsMap || {}, aggregateName, rootEntity.fields, valueObjects
2601
+ );
2068
2602
  const baseContext = {
2069
2603
  packageName,
2070
2604
  moduleName,
@@ -2085,7 +2619,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
2085
2619
  resourceNameCamel,
2086
2620
  resourceNameKebab,
2087
2621
  hasSoftDelete: rootEntity.hasSoftDelete || false,
2088
- hasCreateOperation: true // In interactive CRUD flow, Create is always generated
2622
+ hasCreateOperation: true, // In interactive CRUD flow, Create is always generated
2623
+ domainEvents: aggregate.domainEvents || [],
2624
+ lifecycleEventsMap: resolvedLifecycleCrud
2089
2625
  };
2090
2626
 
2091
2627
  // 0. Generate Create<VoName>Dto for validated Value Objects