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
@@ -307,3 +307,5 @@ async function ensureUrlsImport(projectDir) {
307
307
  }
308
308
 
309
309
  module.exports = generateHttpExchangeCommand;
310
+ module.exports.createOrUpdateUrlsConfig = createOrUpdateUrlsConfig;
311
+ module.exports.ensureUrlsImport = ensureUrlsImport;
@@ -166,9 +166,9 @@ async function generateKafkaEventCommand(moduleName, eventName) {
166
166
  }
167
167
  }
168
168
 
169
- const topicNameKebab = toKebabCase(name);
170
- const topicNameCamel = toCamelCase(name);
171
- const topicNameSnake = toSnakeCase(name).toUpperCase();
169
+ const topicNameKebab = toKebabCase(stripEventSuffix(name));
170
+ const topicNameCamel = toCamelCase(stripEventSuffix(name));
171
+ const topicNameSnake = toSnakeCase(stripEventSuffix(name)).toUpperCase();
172
172
  const topicSpringProperty = `\${topics.${topicNameKebab}}`;
173
173
  const selectedDomainEvent = domainEventMap[normalizedName] || null;
174
174
 
@@ -218,8 +218,8 @@ async function generateKafkaEventCommand(moduleName, eventName) {
218
218
 
219
219
  if (!isBatch && generated.length === 1) {
220
220
  const r = generated[0];
221
- const topicSnake = toSnakeCase(eventNames[0]).toUpperCase();
222
- const topicKebab = toKebabCase(eventNames[0]);
221
+ const topicSnake = toSnakeCase(stripEventSuffix(eventNames[0])).toUpperCase();
222
+ const topicKebab = toKebabCase(stripEventSuffix(eventNames[0]));
223
223
  console.log(chalk.blue('\n✅ Kafka event configured successfully!'));
224
224
  console.log(chalk.white(`\n Event: ${r.name}`));
225
225
  console.log(chalk.white(` Topic: ${topicSnake} (${topicKebab})`));
@@ -393,6 +393,15 @@ async function createOrUpdateKafkaMessageBroker(projectDir, packagePath, context
393
393
  // Update existing implementation
394
394
  let content = await fs.readFile(adapterPath, 'utf-8');
395
395
 
396
+ // If this is a mock impl (generated by build --mock), replace it wholesale
397
+ // with the real Kafka implementation before proceeding.
398
+ const isMockImpl = content.includes('ApplicationEventPublisher') && !content.includes('KafkaTemplate');
399
+ if (isMockImpl) {
400
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'kafka-event', 'KafkaMessageBroker.java.ejs');
401
+ await renderAndWrite(templatePath, adapterPath, context);
402
+ return;
403
+ }
404
+
396
405
  // Check if method already exists
397
406
  if (content.includes(methodName)) {
398
407
  return; // Method already exists
@@ -606,8 +615,10 @@ async function updateDomainEventHandler(projectDir, packagePath, context) {
606
615
  }
607
616
 
608
617
  // 4. Render the mapping call and replace the TODO block
618
+ // Derive aggregateName from the handler file name (e.g. BikeDomainEventHandler.java → Bike)
619
+ const aggregateName = handlerFile.replace('DomainEventHandler.java', '');
609
620
  const templatePath = path.join(__dirname, '..', '..', 'templates', 'kafka-event', 'DomainEventHandlerMethod.ejs');
610
- const mappingLine = await renderTemplate(templatePath, { ...context, domainEventFields: context.eventFields });
621
+ const mappingLine = await renderTemplate(templatePath, { ...context, domainEventFields: context.eventFields, aggregateName });
611
622
 
612
623
  const todoRegex = new RegExp(
613
624
  `([ \\t]*\/\/ TODO: handle ${domainEventName}[^\\n]*\\n)(?:[ \\t]*\/\/[^\\n]*\\n)*`
@@ -651,6 +662,18 @@ async function getInstalledBroker(configManager) {
651
662
  return null;
652
663
  }
653
664
 
665
+ /**
666
+ * Strip the conventional Java 'Event' suffix from an event class name
667
+ * before deriving the Kafka topic name.
668
+ * ProductPublishedEvent → ProductPublished → PRODUCT_PUBLISHED
669
+ *
670
+ * If the event has an explicit `topic:` property in domain.yaml, that takes
671
+ * precedence and this function is not called.
672
+ */
673
+ function stripEventSuffix(name) {
674
+ return name.endsWith('Event') ? name.slice(0, -'Event'.length) : name;
675
+ }
676
+
654
677
  /**
655
678
  * Build a Kafka event generation context for a given domain event.
656
679
  * Intended for use by `generate-entities.js` to auto-wire broker integration events
@@ -658,7 +681,7 @@ async function getInstalledBroker(configManager) {
658
681
  *
659
682
  * @param {string} packageName
660
683
  * @param {string} moduleName
661
- * @param {{ name: string, fields: Array }} domainEvent - Domain event from parsed domain.yaml
684
+ * @param {{ name: string, topic?: string, fields: Array }} domainEvent - Domain event from parsed domain.yaml
662
685
  * @param {{ partitions?: number, replicas?: number }} [options]
663
686
  * @returns {Object} context ready for generateSingleKafkaEvent()
664
687
  */
@@ -667,9 +690,15 @@ function buildKafkaEventContext(packageName, moduleName, domainEvent, { partitio
667
690
  const integrationEventClassName = normalizedName.endsWith('IntegrationEvent')
668
691
  ? normalizedName
669
692
  : `${normalizedName}IntegrationEvent`;
670
- const topicNameKebab = toKebabCase(domainEvent.name);
671
- const topicNameCamel = toCamelCase(domainEvent.name);
672
- const topicNameSnake = toSnakeCase(domainEvent.name).toUpperCase();
693
+ // If an explicit topic is declared in domain.yaml, use it as source of truth.
694
+ // Otherwise strip the 'Event' suffix before deriving snake/kebab names so that
695
+ // ProductPublishedEvent PRODUCT_PUBLISHED (not PRODUCT_PUBLISHED_EVENT).
696
+ const topicBase = domainEvent.topic
697
+ ? domainEvent.topic.trim().toUpperCase().replace(/-/g, '_')
698
+ : toSnakeCase(stripEventSuffix(domainEvent.name)).toUpperCase();
699
+ const topicNameSnake = topicBase;
700
+ const topicNameKebab = topicBase.toLowerCase().replace(/_/g, '-');
701
+ const topicNameCamel = toCamelCase(topicNameKebab);
673
702
  const topicSpringProperty = `\${topics.${topicNameKebab}}`;
674
703
  return {
675
704
  packageName,
@@ -694,3 +723,7 @@ module.exports = generateKafkaEventCommand;
694
723
  module.exports.generateSingleKafkaEvent = generateSingleKafkaEvent;
695
724
  module.exports.buildKafkaEventContext = buildKafkaEventContext;
696
725
  module.exports.getInstalledBroker = getInstalledBroker;
726
+ module.exports.updateKafkaYml = updateKafkaYml;
727
+ module.exports.generateEventRecord = generateEventRecord;
728
+ module.exports.createOrUpdateMessageBroker = createOrUpdateMessageBroker;
729
+ module.exports.updateDomainEventHandler = updateDomainEventHandler;
@@ -63,6 +63,8 @@ class BaseGenerator {
63
63
  await fs.ensureDir(resources);
64
64
  await fs.ensureDir(path.join(resources, 'static'));
65
65
  await fs.ensureDir(path.join(resources, 'templates'));
66
+ await fs.ensureDir(path.join(this.projectDir, 'system'));
67
+ await fs.ensureDir(path.join(this.projectDir, '.agents'));
66
68
  }
67
69
 
68
70
  async generateApplication() {
@@ -132,16 +134,14 @@ class BaseGenerator {
132
134
  path.join(this.projectDir, 'AGENTS.md'));
133
135
  await this.generateFile('root/system.yaml.ejs',
134
136
  path.join(this.projectDir, 'system.yaml'));
135
- await this.generateFile('root/skill-build-system-yaml.ejs',
136
- path.join(this.projectDir, '.agents', 'skills', 'build-system-yaml', 'SKILL.md'));
137
- await this.generateFile('root/skill-build-domain-yaml.ejs',
138
- path.join(this.projectDir, '.agents', 'skills', 'build-domain-yaml', 'SKILL.md'));
139
- await this.generateFile('root/skill-build-domain-yaml-references-generate-entities.md.ejs',
140
- path.join(this.projectDir, '.agents', 'skills', 'build-domain-yaml', 'references', 'GENERATE_ENTITIES.md'));
137
+ await this.copySkills();
138
+ await this.copyAgents();
141
139
 
142
140
  if (this.context.features.includeDocker) {
143
141
  await this.generateFile('docker/docker-compose.yaml.ejs',
144
142
  path.join(this.projectDir, 'docker-compose.yaml'));
143
+ await this.generateFile('docker/Dockerfile.ejs',
144
+ path.join(this.projectDir, 'Dockerfile'));
145
145
  }
146
146
  }
147
147
 
@@ -152,6 +152,18 @@ class BaseGenerator {
152
152
  `${this.context.applicationClassName}Tests.java`));
153
153
  }
154
154
 
155
+ async copySkills() {
156
+ const srcSkillsDir = path.join(__dirname, '../skills');
157
+ const destSkillsDir = path.join(this.projectDir, '.agents', 'skills');
158
+ await fs.copy(srcSkillsDir, destSkillsDir);
159
+ }
160
+
161
+ async copyAgents() {
162
+ const srcAgentsDir = path.join(__dirname, '../agents');
163
+ const destAgentsDir = path.join(this.projectDir, '.github', 'agents');
164
+ await fs.copy(srcAgentsDir, destAgentsDir);
165
+ }
166
+
155
167
  async generateFile(templateRelPath, destPath) {
156
168
  const templatePath = path.join(this.templatesDir, templateRelPath);
157
169
  await renderAndWrite(templatePath, destPath, this.context);
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+ const crypto = require('crypto');
7
+
8
+ const { parseDomainYaml } = require('../utils/yaml-to-entity');
9
+ const { renderTemplate } = require('../utils/template-engine');
10
+ const { toKebabCase, toCamelCase, toPackagePath } = require('../utils/naming');
11
+ const { initSeed, generateFakeValue, generateFakeBody, generateFakeId } = require('../utils/fake-data');
12
+
13
+ const TEMPLATES_DIR = path.join(__dirname, '../../templates/postman');
14
+
15
+ // Audit / internal fields excluded from command bodies
16
+ const EXCLUDED_FIELDS = new Set([
17
+ 'id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'deletedAt',
18
+ ]);
19
+
20
+ /**
21
+ * Determine which aggregate an operation belongs to based on use case naming.
22
+ * E.g. "CreateProduct" → "Product", "FindAllCategorys" → "Category".
23
+ *
24
+ * @param {string} useCase - e.g. "CreateProduct"
25
+ * @param {Array} aggregates - parsed aggregates with `.name`
26
+ * @returns {string|null} - aggregate name or null
27
+ */
28
+ function resolveOwnerAggregate(useCase, aggregates) {
29
+ const ucLower = useCase.toLowerCase();
30
+ // Sort longest name first so "UserProfile" matches before "User"
31
+ const sorted = [...aggregates].sort((a, b) => b.name.length - a.name.length);
32
+ for (const agg of sorted) {
33
+ if (ucLower.includes(agg.name.toLowerCase())) return agg.name;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Generate a unified Postman collection covering every module in the system.
40
+ *
41
+ * @param {Object} opts
42
+ * @param {string} opts.projectDir - Absolute path to the project root
43
+ * @param {string} opts.systemDir - Absolute path to the system/ directory
44
+ * @param {string} opts.packageName - Full Java package name (e.g. "com.example.myapp")
45
+ * @param {Object} opts.systemConfig - Parsed system.yaml object
46
+ * @param {Object} opts.projectConfig - Parsed .eva4j.json project config
47
+ * @returns {Promise<string|null>} - Path to the generated file, or null on error
48
+ */
49
+ async function generateUnifiedPostmanCollection({
50
+ projectDir,
51
+ systemDir,
52
+ packageName,
53
+ systemConfig,
54
+ projectConfig,
55
+ }) {
56
+ const systemName = systemConfig.system?.name || projectConfig.projectName || projectConfig.artifactId || 'eva4j-app';
57
+ const port = projectConfig.server?.port || 8040;
58
+ const modules = systemConfig.modules || [];
59
+
60
+ if (!modules.length) return null;
61
+
62
+ // Seed faker for deterministic output
63
+ initSeed(42);
64
+
65
+ // ── Collect module contexts ───────────────────────────────────────────────
66
+ const moduleContexts = [];
67
+
68
+ for (const mod of modules) {
69
+ const yamlPath = path.join(systemDir, `${mod.name}.yaml`);
70
+ if (!(await fs.pathExists(yamlPath))) continue;
71
+
72
+ let parsed;
73
+ try {
74
+ parsed = await parseDomainYaml(yamlPath, packageName, mod.name);
75
+ } catch (err) {
76
+ console.log(chalk.yellow(` ⚠️ Could not parse ${mod.name}.yaml for Postman: ${err.message}`));
77
+ continue;
78
+ }
79
+
80
+ const { aggregates, allEnums, endpoints } = parsed;
81
+ const aggregateContexts = [];
82
+
83
+ for (const agg of aggregates) {
84
+ const rootEntity = agg.rootEntity;
85
+ const idField = rootEntity.fields.find(f => f.name === 'id');
86
+ const idType = idField ? idField.javaType : 'String';
87
+ const exampleId = generateFakeId(idType);
88
+ const trackUser = rootEntity.audit?.trackUser === true;
89
+ const valueObjects = agg.valueObjects || [];
90
+
91
+ // Command fields: exclude id, audit, readOnly, deletedAt
92
+ const commandFields = rootEntity.fields.filter(
93
+ f => !EXCLUDED_FIELDS.has(f.name) && !f.readOnly
94
+ );
95
+
96
+ // Build fake body for create/update
97
+ const defaultBody = generateFakeBody(commandFields, [], allEnums, valueObjects);
98
+
99
+ if (endpoints && endpoints.versions && endpoints.versions.length > 0) {
100
+ // ── Endpoint-driven ───────────────────────────────────────────────
101
+ const operations = [];
102
+ const bodies = {};
103
+
104
+ for (const version of endpoints.versions) {
105
+ for (const op of version.operations) {
106
+ // Classify operation → which aggregate it belongs to
107
+ const owner = resolveOwnerAggregate(op.useCase, aggregates);
108
+ if (owner && owner !== agg.name) continue;
109
+ // If no aggregate could be resolved, assign to the first aggregate
110
+ if (!owner && agg !== aggregates[0]) continue;
111
+
112
+ const basePath = endpoints.basePath || '/';
113
+ operations.push({
114
+ useCase: op.useCase,
115
+ method: op.method,
116
+ path: op.path || '/',
117
+ basePath,
118
+ version: version.version,
119
+ });
120
+
121
+ // Generate body for write operations
122
+ if (op.method === 'POST' || op.method === 'PUT' || op.method === 'PATCH') {
123
+ // Re-seed per operation so bodies vary
124
+ bodies[op.useCase] = generateFakeBody(commandFields, [], allEnums, valueObjects);
125
+ }
126
+ }
127
+ }
128
+
129
+ aggregateContexts.push({
130
+ name: agg.name,
131
+ trackUser,
132
+ idType,
133
+ exampleId,
134
+ resourceNameKebab: toKebabCase(agg.name),
135
+ operations,
136
+ defaultCrud: false,
137
+ bodies,
138
+ });
139
+ } else {
140
+ // ── Default CRUD (no endpoints section) ──────────────────────────
141
+ aggregateContexts.push({
142
+ name: agg.name,
143
+ trackUser,
144
+ idType,
145
+ exampleId,
146
+ resourceNameKebab: toKebabCase(agg.name),
147
+ operations: null,
148
+ defaultCrud: true,
149
+ bodies: { default: defaultBody },
150
+ });
151
+ }
152
+ }
153
+
154
+ if (aggregateContexts.length > 0) {
155
+ moduleContexts.push({
156
+ name: mod.name,
157
+ aggregates: aggregateContexts,
158
+ });
159
+ }
160
+ }
161
+
162
+ if (!moduleContexts.length) return null;
163
+
164
+ // ── Render template ─────────────────────────────────────────────────────
165
+ const templatePath = path.join(TEMPLATES_DIR, 'UnifiedCollection.json.ejs');
166
+ const collectionId = crypto.randomUUID();
167
+
168
+ const context = {
169
+ systemName,
170
+ collectionId,
171
+ port,
172
+ modules: moduleContexts,
173
+ };
174
+
175
+ const content = await renderTemplate(templatePath, context);
176
+
177
+ // ── Write output ────────────────────────────────────────────────────────
178
+ const outputDir = path.join(projectDir, 'postman');
179
+ await fs.ensureDir(outputDir);
180
+
181
+ const outputFileName = `${toKebabCase(systemName)}-Postman-Collection.json`;
182
+ const outputPath = path.join(outputDir, outputFileName);
183
+ await fs.writeFile(outputPath, content, 'utf-8');
184
+
185
+ return outputPath;
186
+ }
187
+
188
+ module.exports = { generateUnifiedPostmanCollection };
@@ -136,7 +136,7 @@ class SharedGenerator {
136
136
 
137
137
  async generateAnnotations(basePath) {
138
138
  const annotationsPath = path.join(basePath, 'domain', 'annotations');
139
- const files = ['ApplicationComponent', 'DomainComponent', 'LogAfter', 'LogBefore', 'LogExceptions', 'LogTimer'];
139
+ const files = ['ApplicationComponent', 'DomainComponent', 'Loggable', 'LogAfter', 'LogBefore', 'LogExceptions', 'LogLevel', 'LogTimer'];
140
140
 
141
141
  for (const file of files) {
142
142
  await this.generateFile(`annotations/${file}.java.ejs`,
@@ -170,7 +170,7 @@ class SharedGenerator {
170
170
 
171
171
  async generateErrorMessages(basePath) {
172
172
  const errorMessagePath = path.join(basePath, 'domain', 'errorMessage');
173
- const files = ['ErrorMessage', 'FullErrorMessage', 'ShortErrorMessage'];
173
+ const files = ['ErrorResponse'];
174
174
 
175
175
  for (const file of files) {
176
176
  await this.generateFile(`errorMessage/${file}.java.ejs`,
@@ -194,6 +194,12 @@ class SharedGenerator {
194
194
  path.join(handlerExceptionPath, 'HandlerExceptions.java'));
195
195
  }
196
196
 
197
+ async generateMockEvent(basePath) {
198
+ const destPath = path.join(basePath, 'infrastructure', 'mockEvent', 'MockEvent.java');
199
+ const templatePath = path.join(__dirname, '../../templates/mock/MockEvent.java.ejs');
200
+ await renderAndWrite(templatePath, destPath, this.context, { overwrite: false });
201
+ }
202
+
197
203
  async generateConfigurations(basePath) {
198
204
  const configurationsPath = path.join(basePath, 'infrastructure', 'configurations');
199
205
 
@@ -209,6 +215,10 @@ class SharedGenerator {
209
215
  await this.generateFile('configurations/swaggerConfig/SwaggerConfig.java.ejs',
210
216
  path.join(configurationsPath, 'swaggerConfig', 'SwaggerConfig.java'));
211
217
 
218
+ // Event publication schema fix (Spring Modulith varchar(255) → TEXT)
219
+ await this.generateFile('configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs',
220
+ path.join(configurationsPath, 'eventPublicationConfig', 'EventPublicationSchemaConfig.java'));
221
+
212
222
  // UseCase config
213
223
  await this.generateFile('configurations/useCaseConfig/UseCaseAutoRegister.java.ejs',
214
224
  path.join(configurationsPath, 'useCaseConfig', 'UseCaseAutoRegister.java'));