eva4j 1.0.17 → 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 (134) hide show
  1. package/AGENTS.md +2 -0
  2. package/DOMAIN_YAML_GUIDE.md +3 -1
  3. package/QUICK_REFERENCE.md +8 -4
  4. package/bin/eva4j.js +70 -2
  5. package/config/defaults.json +1 -0
  6. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  7. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  8. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  9. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  10. package/docs/commands/EVALUATE_SYSTEM.md +272 -8
  11. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  12. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  13. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  14. package/docs/commands/INDEX.md +27 -3
  15. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  16. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  17. package/docs/prototype/system/RISKS.md +277 -0
  18. package/docs/prototype/system/customers.yaml +133 -0
  19. package/docs/prototype/system/inventory.yaml +109 -0
  20. package/docs/prototype/system/notifications.yaml +131 -0
  21. package/docs/prototype/system/orders.yaml +241 -0
  22. package/docs/prototype/system/payments.yaml +256 -0
  23. package/docs/prototype/system/products.yaml +168 -0
  24. package/docs/prototype/system/system.yaml +269 -0
  25. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  26. package/examples/domain-read-models.yaml +2 -2
  27. package/examples/system/customer.yaml +89 -0
  28. package/examples/system/orders.yaml +119 -0
  29. package/examples/system/product.yaml +27 -0
  30. package/examples/system/system.yaml +80 -0
  31. package/package.json +1 -1
  32. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  33. package/src/agents/design-gap-analyst.agent.md +383 -0
  34. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  35. package/src/agents/design-reviewer.agent.md +31 -5
  36. package/src/agents/implement-use-cases.prompt.md +179 -0
  37. package/src/agents/ux-gap-analyst.agent.md +412 -0
  38. package/src/commands/add-rabbitmq-client.js +261 -0
  39. package/src/commands/add-temporal-client.js +22 -2
  40. package/src/commands/build.js +267 -11
  41. package/src/commands/evaluate-system.js +700 -13
  42. package/src/commands/generate-entities.js +308 -16
  43. package/src/commands/generate-rabbitmq-event.js +665 -0
  44. package/src/commands/generate-rabbitmq-listener.js +205 -0
  45. package/src/commands/generate-temporal-activity.js +968 -34
  46. package/src/commands/generate-temporal-flow.js +95 -38
  47. package/src/commands/generate-temporal-system.js +708 -0
  48. package/src/skills/build-system-yaml/SKILL.md +222 -2
  49. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
  50. package/src/skills/build-system-yaml/references/module-spec.md +57 -8
  51. package/src/skills/build-temporal-system/SKILL.md +752 -0
  52. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  53. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  54. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  55. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  56. package/src/skills/implement-use-case/SKILL.md +350 -0
  57. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  58. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  59. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  60. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  61. package/src/utils/bounded-context-diagram.js +844 -0
  62. package/src/utils/domain-validator.js +266 -4
  63. package/src/utils/naming.js +10 -0
  64. package/src/utils/system-validator.js +169 -11
  65. package/src/utils/system-yaml-parser.js +318 -0
  66. package/src/utils/temporal-validator.js +497 -0
  67. package/src/utils/yaml-to-entity.js +10 -7
  68. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  69. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
  70. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  71. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  72. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  73. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  74. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  75. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  76. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  77. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  78. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  79. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  80. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  81. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  82. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  83. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  84. package/templates/base/root/AGENTS.md.ejs +1 -1
  85. package/templates/crud/EndpointsController.java.ejs +1 -1
  86. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  87. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  88. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  89. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  90. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  91. package/templates/evaluate/report.html.ejs +1447 -90
  92. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  93. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  94. package/templates/ports/PortAclMapper.java.ejs +35 -0
  95. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  96. package/templates/ports/PortFeignClient.java.ejs +4 -0
  97. package/templates/ports/PortResponseDto.java.ejs +1 -1
  98. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  99. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  100. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  101. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  102. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  103. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  104. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  105. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  106. package/templates/read-model/ReadModelJpa.java.ejs +1 -1
  107. package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
  108. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  109. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
  110. package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
  111. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  112. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  113. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  114. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  115. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  116. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  117. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  118. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  119. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  120. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  121. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  122. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  123. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  124. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  125. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  126. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  127. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  128. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  129. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  130. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  131. package/COMMAND_EVALUATION.md +0 -911
  132. package/test-c2010.js +0 -49
  133. package/test-update-compat.js +0 -109
  134. package/test-update-lifecycle.js +0 -121
@@ -14,6 +14,10 @@ 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;
@@ -284,6 +288,33 @@ async function generateEntitiesCommand(moduleName, options = {}) {
284
288
  ? 'mock'
285
289
  : installedBroker;
286
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
+
287
318
  // Generate audit-related shared components if needed
288
319
  if (hasAuditableEntities || hasTrackUserEntities) {
289
320
 
@@ -604,9 +635,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
604
635
  aggregateName,
605
636
  domainEvents: aggregateDomainEvents.map(e => ({
606
637
  ...e,
607
- 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
+ }),
608
648
  })),
609
- broker
649
+ broker,
650
+ temporal: temporalInstalled
610
651
  };
611
652
  await renderAndWrite(
612
653
  path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventHandler.java.ejs'),
@@ -699,6 +740,52 @@ async function generateEntitiesCommand(moduleName, options = {}) {
699
740
  name: 'MessageBroker (updated)',
700
741
  path: `${moduleName}/application/ports/MessageBroker.java`
701
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
+ });
702
789
  }
703
790
  }
704
791
  }
@@ -720,10 +807,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
720
807
  const topicRaw = listener.topic;
721
808
  const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
722
809
  const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
810
+ const kafkaListenerClassName = `${listener.baseName}KafkaListener`;
723
811
  const listenerContext = {
724
812
  packageName,
725
813
  moduleName,
726
814
  ...listener,
815
+ listenerClassName: kafkaListenerClassName,
727
816
  topicConstant: topicRaw,
728
817
  topicSpringProperty: `\${topics.${topicKey}}`,
729
818
  topicVariableName: toCamelCase(topicSuffix.toLowerCase())
@@ -769,7 +858,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
769
858
  // If the file is currently a mock Spring listener, force overwrite with the real Kafka impl
770
859
  const kafkaListenerPath = path.join(
771
860
  moduleBasePath, 'infrastructure', 'kafkaListener',
772
- `${listener.listenerClassName}.java`
861
+ `${kafkaListenerClassName}.java`
773
862
  );
774
863
  let kafkaListenerWriteOpts = writeOptions;
775
864
  if (await fs.pathExists(kafkaListenerPath)) {
@@ -786,8 +875,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
786
875
  );
787
876
  generatedFiles.push({
788
877
  type: 'Kafka Listener',
789
- name: listener.listenerClassName,
790
- path: `${moduleName}/infrastructure/kafkaListener/${listener.listenerClassName}.java`
878
+ name: kafkaListenerClassName,
879
+ path: `${moduleName}/infrastructure/kafkaListener/${kafkaListenerClassName}.java`
791
880
  });
792
881
 
793
882
  // 3. Register topic in kafka.yaml (all environments)
@@ -844,10 +933,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
844
933
  const topicRaw = listener.topic;
845
934
  const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
846
935
  const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
936
+ const mockListenerClassName = `${listener.baseName}KafkaListener`;
847
937
  const listenerContext = {
848
938
  packageName,
849
939
  moduleName,
850
940
  ...listener,
941
+ listenerClassName: mockListenerClassName,
851
942
  topicConstant: topicRaw,
852
943
  topicSpringProperty: `\${topics.${topicKey}}`,
853
944
  topicVariableName: toCamelCase(topicSuffix.toLowerCase())
@@ -876,14 +967,14 @@ async function generateEntitiesCommand(moduleName, options = {}) {
876
967
  generatedFiles.push({ type: 'Listener Integration Event', name: listener.integrationEventClassName, path: `${moduleName}/application/events/${listener.integrationEventClassName}.java` });
877
968
 
878
969
  // 2. Spring @EventListener class (mock — same file path as Kafka listener)
879
- const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${listener.listenerClassName}.java`);
970
+ const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${mockListenerClassName}.java`);
880
971
  await renderAndWrite(
881
972
  path.join(__dirname, '..', '..', 'templates', 'mock', 'SpringEventListener.java.ejs'),
882
973
  listenerPath,
883
974
  listenerContext,
884
975
  { ...writeOptions, force: true }
885
976
  );
886
- 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` });
887
978
 
888
979
  // 3. NO kafka.yaml update in mock mode
889
980
 
@@ -908,8 +999,146 @@ async function generateEntitiesCommand(moduleName, options = {}) {
908
999
  generatedFiles.push({ type: 'Handler', name: `${listener.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java` });
909
1000
  }
910
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! ✨`));
911
1140
  } else if (listeners.length > 0) {
912
- 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.`));
913
1142
  }
914
1143
  }
915
1144
 
@@ -936,6 +1165,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
936
1165
 
937
1166
  const adapterDir = path.join(moduleBasePath, 'infrastructure', 'adapters', adapterPackage);
938
1167
 
1168
+ const aclMapperClassName = `${serviceName}AclMapper`;
1169
+
939
1170
  const portContext = {
940
1171
  packageName,
941
1172
  moduleName,
@@ -948,6 +1179,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
948
1179
  feignClientClassName,
949
1180
  feignAdapterClassName,
950
1181
  feignConfigClassName,
1182
+ aclMapperClassName,
951
1183
  adapterPackage,
952
1184
  methods,
953
1185
  nestedTypes,
@@ -988,9 +1220,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
988
1220
  });
989
1221
  }
990
1222
 
991
- // 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/
992
1224
  for (const method of methods.filter(m => m.hasResponse)) {
993
- const infraDtoPath = path.join(adapterDir, `${method.infraDtoName}.java`);
1225
+ const infraDtoPath = path.join(adapterDir, 'dtos', `${method.infraDtoName}.java`);
994
1226
  await renderAndWrite(
995
1227
  path.join(__dirname, '..', '..', 'templates', 'ports', 'PortResponseDto.java.ejs'),
996
1228
  infraDtoPath,
@@ -1000,7 +1232,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1000
1232
  generatedFiles.push({
1001
1233
  type: 'Port Infra DTO',
1002
1234
  name: method.infraDtoName,
1003
- path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${method.infraDtoName}.java`
1235
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/dtos/${method.infraDtoName}.java`
1004
1236
  });
1005
1237
  }
1006
1238
 
@@ -1061,6 +1293,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1061
1293
  path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignAdapterClassName}.java`
1062
1294
  });
1063
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
+
1064
1309
  // 6. Feign Config
1065
1310
  await renderAndWrite(
1066
1311
  path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignConfig.java.ejs'),
@@ -1086,7 +1331,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1086
1331
 
1087
1332
  // ── Generate Read Models (local projections of external data) ────────────
1088
1333
  if (readModels && readModels.length > 0) {
1089
- if (broker === 'kafka' || broker === 'mock') {
1334
+ if (broker === 'kafka' || broker === 'rabbitmq' || broker === 'mock') {
1090
1335
  spinner.start(`Generating ${readModels.length} read model(s)...`);
1091
1336
 
1092
1337
  const readModelTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'read-model');
@@ -1258,13 +1503,53 @@ async function generateEntitiesCommand(moduleName, options = {}) {
1258
1503
 
1259
1504
  // Register topic in kafka.yaml
1260
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
+ });
1261
1546
  }
1262
1547
  }
1263
1548
  }
1264
1549
 
1265
1550
  spinner.succeed(chalk.green(`Read models generated! ✨`));
1266
1551
  } else if (readModels.length > 0) {
1267
- console.log(chalk.yellow(`⚠ readModels: section found but no broker is installed. Run 'eva add kafka-client' to generate listener classes.`));
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.`));
1268
1553
  }
1269
1554
  }
1270
1555
 
@@ -1616,11 +1901,14 @@ function classifyUseCase(op, aggregateName, aggregate) {
1616
1901
  };
1617
1902
  }
1618
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;
1619
1906
  return {
1620
1907
  category: 'subEntityRemove',
1621
1908
  entityName: rel.target,
1622
1909
  fieldName: rel.fieldName,
1623
- removeMethodName: `remove${rel.target}ById`
1910
+ removeMethodName: `remove${rel.target}ById`,
1911
+ itemIdType: removeIdField ? removeIdField.javaType : 'String'
1624
1912
  };
1625
1913
  }
1626
1914
  }
@@ -2058,7 +2346,8 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
2058
2346
  useCaseName: op.useCase,
2059
2347
  idType,
2060
2348
  entityName: cl.entityName,
2061
- removeMethodName: cl.removeMethodName
2349
+ removeMethodName: cl.removeMethodName,
2350
+ itemIdType: cl.itemIdType || 'String'
2062
2351
  };
2063
2352
  await renderAndWrite(
2064
2353
  path.join(templatesDir, 'SubEntityRemoveCommand.java.ejs'),
@@ -2102,7 +2391,10 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
2102
2391
 
2103
2392
  } else {
2104
2393
  // Scaffold: no semantic pattern matched → generate stub with TODO
2105
- 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 };
2106
2398
  const scaffoldType = op.type || (op.method === 'GET' ? 'query' : 'command');
2107
2399
  if (scaffoldType === 'command') {
2108
2400
  await renderAndWrite(