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
@@ -0,0 +1,708 @@
1
+ /**
2
+ * generate-temporal-system.js
3
+ *
4
+ * New command: `eva g temporal-system`
5
+ *
6
+ * Reads system.yaml → workflows[], cross-references each step with its
7
+ * target module's domain.yaml activities, and generates:
8
+ * 1. Shared activity contracts (Interface + Input + Output) in shared/
9
+ * 2. WorkFlowInterface + WorkFlowImpl + WorkFlowService + WorkFlowInput in host module
10
+ * 3. Updates ModuleTemporalWorkerConfig
11
+ * 4. Updates temporal.yaml with module queue sections
12
+ */
13
+
14
+ const ora = require('ora');
15
+ const chalk = require('chalk');
16
+ const path = require('path');
17
+ const fs = require('fs-extra');
18
+ const ConfigManager = require('../utils/config-manager');
19
+ const { isEva4jProject } = require('../utils/validator');
20
+ const { toPackagePath, toPascalCase, toCamelCase, toScreamingSnakeCase } = require('../utils/naming');
21
+ const { renderAndWrite } = require('../utils/template-engine');
22
+ const ChecksumManager = require('../utils/checksum-manager');
23
+ const { parseSystemYaml, resolveFieldImports, parseTimeout } = require('../utils/system-yaml-parser');
24
+ const {
25
+ generateSharedContracts,
26
+ buildActivityContext,
27
+ scanExistingSharedTypes,
28
+ } = require('./generate-temporal-activity');
29
+
30
+ async function generateTemporalSystemCommand(options = {}) {
31
+ const projectDir = process.cwd();
32
+
33
+ // ── Validations ─────────────────────────────────────────────────────────
34
+ if (!(await isEva4jProject(projectDir))) {
35
+ console.error(chalk.red('❌ Not in an eva4j project directory'));
36
+ process.exit(1);
37
+ }
38
+
39
+ const configManager = new ConfigManager(projectDir);
40
+
41
+ if (!(await configManager.featureExists('temporal'))) {
42
+ console.error(chalk.red('❌ Temporal client is not installed'));
43
+ console.error(chalk.gray('Install Temporal first: eva add temporal-client'));
44
+ process.exit(1);
45
+ }
46
+
47
+ const projectConfig = await configManager.loadProjectConfig();
48
+ if (!projectConfig) {
49
+ console.error(chalk.red('❌ Could not load project configuration'));
50
+ process.exit(1);
51
+ }
52
+
53
+ const { packageName } = projectConfig;
54
+ const packagePath = toPackagePath(packageName);
55
+ const javaRoot = path.join(projectDir, 'src', 'main', 'java', packagePath);
56
+
57
+ // ── Locate system.yaml ─────────────────────────────────────────────────
58
+ const systemDir = path.join(projectDir, 'system');
59
+ const systemYamlPath = path.join(systemDir, 'system.yaml');
60
+ if (!(await fs.pathExists(systemYamlPath))) {
61
+ console.error(chalk.red('❌ system/system.yaml not found'));
62
+ console.error(chalk.gray('Create a system.yaml in the system/ directory'));
63
+ process.exit(1);
64
+ }
65
+
66
+ const spinner = ora('Parsing system.yaml...').start();
67
+
68
+ try {
69
+ // ── 1. Parse system.yaml ─────────────────────────────────────────────
70
+ const parsed = await parseSystemYaml(systemDir);
71
+ const { workflows, activityRegistry, warnings } = parsed;
72
+
73
+ if (workflows.length === 0) {
74
+ spinner.warn(chalk.yellow('No workflows found in system.yaml'));
75
+ return;
76
+ }
77
+
78
+ spinner.succeed(chalk.green(
79
+ `Found ${workflows.length} workflow(s) with ${activityRegistry.size} registered activities`
80
+ ));
81
+
82
+ // Print warnings
83
+ for (const w of warnings) {
84
+ console.log(chalk.yellow(` ⚠ ${w}`));
85
+ }
86
+
87
+ // ── 2. Generate shared activity contracts ────────────────────────────
88
+ const sharedSpinner = ora('Generating shared activity contracts...').start();
89
+ const sharedBasePath = path.join(javaRoot, 'shared');
90
+ const sharedChecksumManager = new ChecksumManager(sharedBasePath);
91
+ await sharedChecksumManager.load();
92
+ const sharedWriteOptions = { force: options.force, checksumManager: sharedChecksumManager };
93
+
94
+ const generatedShared = [];
95
+ const processedActivities = new Set();
96
+
97
+ // Scan shared/domain/contracts/ once before the loop so that later activities
98
+ // in the same run do not write duplicate nestedTypes for types already written
99
+ // by an earlier module (e.g. CartItemDetail in carts.yaml AND products.yaml).
100
+ const existingSharedTypes = await scanExistingSharedTypes(sharedBasePath);
101
+
102
+ // Collect all cross-module activity names from workflows
103
+ for (const wf of workflows) {
104
+ for (const step of wf.steps) {
105
+ // Only generate shared contracts for cross-module activities
106
+ if (!step.isLocal) {
107
+ await processActivity(step.activityName, activityRegistry, packageName, processedActivities, generatedShared, sharedBasePath, sharedWriteOptions, existingSharedTypes);
108
+ }
109
+
110
+ // Also handle compensation activities (only cross-module)
111
+ if (step.compensation && step.compensation.module !== wf.hostModule) {
112
+ await processActivity(step.compensation.name, activityRegistry, packageName, processedActivities, generatedShared, sharedBasePath, sharedWriteOptions, existingSharedTypes);
113
+ }
114
+ }
115
+ }
116
+
117
+ await sharedChecksumManager.save();
118
+ sharedSpinner.succeed(chalk.green(`${generatedShared.length} shared contract file(s) generated`));
119
+
120
+ // Re-scan shared types so that computeStubActivities can detect activities
121
+ // whose contracts were just generated (e.g. GetOrderDetails used both
122
+ // cross-module and locally — the local workflow must import from shared/).
123
+ const refreshedSharedTypes = await scanExistingSharedTypes(sharedBasePath);
124
+ for (const [k, v] of refreshedSharedTypes) existingSharedTypes.set(k, v);
125
+
126
+ // ── 3. Generate workflows in host modules ────────────────────────────
127
+ const flowSpinner = ora('Generating workflow implementations...').start();
128
+ const templatesDir = path.join(__dirname, '..', '..', 'templates', 'temporal-flow');
129
+ const generatedFlows = [];
130
+
131
+ for (const wf of workflows) {
132
+ if (!wf.hostModule) {
133
+ warnings.push(`Workflow '${wf.name}' has no trigger.module — skipping`);
134
+ continue;
135
+ }
136
+
137
+ const flowPascal = wf.namePascal.replace(/Workflow$/, '');
138
+ flowSpinner.text = `Generating ${flowPascal}WorkFlow...`;
139
+
140
+ const moduleBasePath = path.join(javaRoot, wf.hostModule);
141
+ if (!(await fs.pathExists(moduleBasePath))) {
142
+ warnings.push(`Host module directory '${wf.hostModule}' not found — skipping ${wf.name}`);
143
+ continue;
144
+ }
145
+
146
+ const moduleChecksumManager = new ChecksumManager(moduleBasePath);
147
+ await moduleChecksumManager.load();
148
+ const moduleWriteOptions = { force: options.force, checksumManager: moduleChecksumManager };
149
+
150
+ // ── Enrich steps with input sources ──────────────────────────────
151
+ enrichStepsWithInputSources(wf.steps);
152
+
153
+ // Enrich compensation inputSources
154
+ for (const step of wf.steps) {
155
+ if (step.compensation) {
156
+ enrichCompensationInputSources(step, wf.steps);
157
+ }
158
+ }
159
+
160
+ // ── Compute stub activities ──────────────────────────────────────
161
+ const stubActivities = computeStubActivities(wf.steps, activityRegistry, wf.hostModule, existingSharedTypes);
162
+
163
+ // ── Compute workflow input fields ────────────────────────────────
164
+ const inputFields = computeWorkflowInputFields(wf.steps);
165
+ const inputImports = resolveFieldImports(inputFields);
166
+
167
+ // ── Compute render blocks ────────────────────────────────────────
168
+ const renderBlocks = computeRenderBlocks(wf.steps, wf.parallelGroups);
169
+
170
+ const flowContext = {
171
+ packageName,
172
+ moduleName: wf.hostModule,
173
+ modulePascalCase: wf.hostModulePascal,
174
+ moduleCamelCase: wf.hostModule,
175
+ moduleScreamingSnake: wf.hostModuleScreamingSnake,
176
+ flowPascalCase: wf.namePascal.replace(/Workflow$/, ''),
177
+ flowName: wf.name,
178
+ taskQueue: wf.taskQueue,
179
+ isSaga: wf.isSaga,
180
+ steps: wf.steps,
181
+ parallelGroups: wf.parallelGroups,
182
+ targetModules: wf.targetModules,
183
+ hasParallelSteps: wf.hasParallelSteps,
184
+ hasCompensations: wf.hasCompensations,
185
+ hasAsyncSteps: wf.hasAsyncSteps,
186
+ hasOptionalSteps: wf.hasOptionalSteps,
187
+ trigger: wf.trigger,
188
+ // Fase 5 enrichments
189
+ stubActivities,
190
+ inputFields,
191
+ inputImports,
192
+ renderBlocks,
193
+ };
194
+
195
+ const usecasesDir = path.join(moduleBasePath, 'application', 'usecases');
196
+
197
+ // WorkFlow input record
198
+ if (inputFields.length > 0) {
199
+ await renderAndWrite(
200
+ path.join(templatesDir, 'WorkFlowInput.java.ejs'),
201
+ path.join(usecasesDir, `${flowPascal}Input.java`),
202
+ flowContext,
203
+ moduleWriteOptions
204
+ );
205
+ }
206
+
207
+ // WorkFlow interface
208
+ await renderAndWrite(
209
+ path.join(templatesDir, 'WorkFlowInterface.java.ejs'),
210
+ path.join(usecasesDir, `${flowPascal}WorkFlow.java`),
211
+ flowContext,
212
+ moduleWriteOptions
213
+ );
214
+
215
+ // WorkFlow implementation
216
+ await renderAndWrite(
217
+ path.join(templatesDir, 'WorkFlowImpl.java.ejs'),
218
+ path.join(usecasesDir, `${flowPascal}WorkFlowImpl.java`),
219
+ flowContext,
220
+ moduleWriteOptions
221
+ );
222
+
223
+ // WorkFlow service facade
224
+ await renderAndWrite(
225
+ path.join(templatesDir, 'WorkFlowService.java.ejs'),
226
+ path.join(usecasesDir, `${flowPascal}WorkFlowService.java`),
227
+ flowContext,
228
+ moduleWriteOptions
229
+ );
230
+
231
+ // Register workflow in ModuleTemporalWorkerConfig
232
+ const configPath = path.join(
233
+ moduleBasePath, 'infrastructure', 'configurations',
234
+ `${wf.hostModulePascal}TemporalWorkerConfig.java`
235
+ );
236
+ if (await fs.pathExists(configPath)) {
237
+ await registerWorkflowInConfig(
238
+ configPath, packageName, wf.hostModule, flowPascal, wf.hostModulePascal
239
+ );
240
+ }
241
+
242
+ // Append module queues to temporal.yaml
243
+ await appendModuleQueues(projectDir, wf.hostModule, wf.hostModuleScreamingSnake);
244
+
245
+ await moduleChecksumManager.save();
246
+ generatedFlows.push(wf);
247
+ }
248
+
249
+ flowSpinner.succeed(chalk.green(`${generatedFlows.length} workflow(s) generated`));
250
+
251
+ // ── 4. Summary ───────────────────────────────────────────────────────
252
+ console.log(chalk.blue('\n📁 Shared contracts:'));
253
+ for (const f of generatedShared) {
254
+ console.log(chalk.gray(` ${f}`));
255
+ }
256
+
257
+ console.log(chalk.blue('\n📁 Workflows:'));
258
+ for (const wf of generatedFlows) {
259
+ const fp = wf.namePascal.replace(/Workflow$/, '');
260
+ console.log(chalk.gray(` ${wf.hostModule}/application/usecases/${fp}Input.java`));
261
+ console.log(chalk.gray(` ${wf.hostModule}/application/usecases/${fp}WorkFlow.java`));
262
+ console.log(chalk.gray(` ${wf.hostModule}/application/usecases/${fp}WorkFlowImpl.java`));
263
+ console.log(chalk.gray(` ${wf.hostModule}/application/usecases/${fp}WorkFlowService.java`));
264
+ }
265
+
266
+ if (warnings.length > 0) {
267
+ console.log(chalk.yellow('\n⚠ Warnings:'));
268
+ for (const w of warnings) {
269
+ console.log(chalk.yellow(` ${w}`));
270
+ }
271
+ }
272
+
273
+ console.log(chalk.green('\n✅ Temporal system generation complete'));
274
+
275
+ } catch (error) {
276
+ spinner.fail(chalk.red('Failed to generate temporal system'));
277
+ console.error(chalk.red(error.message));
278
+ if (options.verbose) console.error(error.stack);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
284
+
285
+ /**
286
+ * Process a single activity for shared contract generation.
287
+ */
288
+ async function processActivity(actName, activityRegistry, packageName, processedSet, generatedList, sharedBasePath, writeOptions, existingSharedTypes = new Map()) {
289
+ if (processedSet.has(actName)) return;
290
+ processedSet.add(actName);
291
+
292
+ const registered = activityRegistry.get(actName);
293
+ if (!registered) return;
294
+
295
+ const actCtx = buildActivityContext(registered.definition, packageName, registered.module, true, null, new Set(), existingSharedTypes);
296
+
297
+ const files = await generateSharedContracts(actCtx, sharedBasePath, writeOptions, existingSharedTypes);
298
+ generatedList.push(...files);
299
+ }
300
+
301
+ /**
302
+ * Enrich each step with `inputSources` — where each input field value comes from.
303
+ * Uses system.yaml `rawInputNames` to determine wiring: if a raw input name
304
+ * matches a prior step's `rawOutputNames`, it comes from that step's result.
305
+ * Otherwise it comes from the workflow input.
306
+ */
307
+ function enrichStepsWithInputSources(steps) {
308
+ for (const step of steps) {
309
+ const rawNames = step.rawInputNames;
310
+ const sources = [];
311
+
312
+ for (let j = 0; j < rawNames.length; j++) {
313
+ const rawName = rawNames[j];
314
+
315
+ // Find the most-recent prior step that produced this field (last-wins).
316
+ // When two steps output the same name (e.g. `items` produced by GetCartDetails
317
+ // and then transformed by ValidateAndGetProducts), the later step's version
318
+ // is the one in scope — identical semantics to overwriting a local variable.
319
+ // Note: Array.findLast() requires Node ≥18; [...].reverse().find() is Node 14+ safe.
320
+ const sourceStep = [...steps].reverse().find(
321
+ (s) => s.index < step.index && s.rawOutputNames.includes(rawName)
322
+ );
323
+
324
+ if (sourceStep) {
325
+ sources.push({
326
+ source: 'step',
327
+ stepIndex: sourceStep.index,
328
+ varName: sourceStep.activityCamel + 'Result',
329
+ fieldName: rawName,
330
+ });
331
+ } else {
332
+ sources.push({ source: 'input', fieldName: rawName });
333
+ }
334
+ }
335
+
336
+ // If no rawInputNames (system.yaml didn't declare input:), fall back to
337
+ // activity formal input fields, all wired from workflow input
338
+ if (rawNames.length === 0 && step.inputFields.length > 0) {
339
+ for (const field of step.inputFields) {
340
+ sources.push({ source: 'input', fieldName: field.name });
341
+ }
342
+ }
343
+
344
+ step.inputSources = sources;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Enrich compensation with inputSources.
350
+ *
351
+ * Compensation undoes what the parent step did, so it receives the same inputs.
352
+ * When the parent step and the compensation activity have the same number of
353
+ * input fields, we reuse the parent step's inputSources directly — this avoids
354
+ * name-based resolution that can match the wrong variable when field names
355
+ * coincide across different types (e.g., `items` in two distinct contexts).
356
+ *
357
+ * Falls back to name-based matching only when the input counts diverge.
358
+ */
359
+ function enrichCompensationInputSources(step, allSteps) {
360
+ if (!step.compensation) return;
361
+
362
+ const compInputFields = step.compensation.inputFields || [];
363
+
364
+ // Primary strategy: reuse parent step's already-resolved inputSources.
365
+ if (step.inputSources && step.inputSources.length === compInputFields.length) {
366
+ step.compensation.inputSources = step.inputSources.map((src) => ({ ...src }));
367
+ return;
368
+ }
369
+
370
+ // Fallback: match compensation fields against prior step outputs or workflow input
371
+ const sources = [];
372
+
373
+ for (const field of compInputFields) {
374
+ // Same last-wins resolution as enrichStepsWithInputSources.
375
+ const sourceStep = [...allSteps].reverse().find(
376
+ (s) => s.index <= step.index && s.rawOutputNames.includes(field.name)
377
+ );
378
+
379
+ if (sourceStep) {
380
+ sources.push({
381
+ source: 'step',
382
+ stepIndex: sourceStep.index,
383
+ varName: sourceStep.activityCamel + 'Result',
384
+ fieldName: field.name,
385
+ });
386
+ } else {
387
+ sources.push({ source: 'input', fieldName: field.name });
388
+ }
389
+ }
390
+
391
+ step.compensation.inputSources = sources;
392
+ }
393
+
394
+ /**
395
+ * Compute the list of unique activity stubs needed by the workflow.
396
+ * Includes both main step activities and compensation activities.
397
+ */
398
+ function computeStubActivities(steps, activityRegistry, hostModule, existingSharedTypes = new Map()) {
399
+ // Pre-scan: collect all externalType references within this workflow's stubs
400
+ // to detect local nestedTypes that are shared across modules
401
+ const externalRefs = new Set(); // "sourceModule:typeName"
402
+ for (const step of steps) {
403
+ const reg = activityRegistry.get(step.activityName);
404
+ if (reg && reg.definition && Array.isArray(reg.definition.externalTypes)) {
405
+ for (const et of reg.definition.externalTypes) {
406
+ externalRefs.add(`${toCamelCase(et.module)}:${toPascalCase(et.name)}`);
407
+ }
408
+ }
409
+ if (step.compensation) {
410
+ const compReg = activityRegistry.get(step.compensation.name);
411
+ if (compReg && compReg.definition && Array.isArray(compReg.definition.externalTypes)) {
412
+ for (const et of compReg.definition.externalTypes) {
413
+ externalRefs.add(`${toCamelCase(et.module)}:${toPascalCase(et.name)}`);
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ // Pre-scan 2: collect simple names contributed by external (non-local) stubs —
420
+ // nestedTypes and externalTypes of external activities live in shared/domain/contracts/{module}/.
421
+ // Used to detect simple-name collisions with local stubs' nestedTypes so the template
422
+ // can emit the correct shared import instead of a local one.
423
+ const externalNestedNames = new Map(); // PascalCase simpleName → camelCase sourceModule
424
+ for (const step of steps) {
425
+ if (!step.isLocal) {
426
+ const reg = activityRegistry.get(step.activityName);
427
+ if (reg && reg.definition) {
428
+ for (const nt of (reg.definition.nestedTypes || [])) {
429
+ const name = toPascalCase(nt.name);
430
+ if (!externalNestedNames.has(name)) externalNestedNames.set(name, step.targetModule);
431
+ }
432
+ for (const et of (reg.definition.externalTypes || [])) {
433
+ const name = toPascalCase(et.name);
434
+ if (!externalNestedNames.has(name)) externalNestedNames.set(name, toCamelCase(et.module));
435
+ }
436
+ }
437
+ }
438
+ if (step.compensation) {
439
+ const compModule = toCamelCase(step.compensation.module || step.targetModule);
440
+ if (compModule !== hostModule) {
441
+ const compReg = activityRegistry.get(step.compensation.name);
442
+ if (compReg && compReg.definition) {
443
+ for (const nt of (compReg.definition.nestedTypes || [])) {
444
+ const name = toPascalCase(nt.name);
445
+ if (!externalNestedNames.has(name)) externalNestedNames.set(name, compModule);
446
+ }
447
+ for (const et of (compReg.definition.externalTypes || [])) {
448
+ const name = toPascalCase(et.name);
449
+ if (!externalNestedNames.has(name)) externalNestedNames.set(name, toCamelCase(et.module));
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+ const stubs = new Map();
457
+
458
+ for (const step of steps) {
459
+ if (!stubs.has(step.activityName)) {
460
+ const registered = activityRegistry.get(step.activityName);
461
+ const rawNestedTypes = registered && registered.definition && Array.isArray(registered.definition.nestedTypes)
462
+ ? registered.definition.nestedTypes.map((nt) => {
463
+ const name = toPascalCase(nt.name);
464
+ // Mark as external if referenced via externalTypes in another stub,
465
+ // OR if an external stub contributes the same simple name (collision → shared wins).
466
+ // existingSharedTypes is the authoritative source: it reflects the actual filesystem
467
+ // path where generateSharedContracts wrote the file (first-writer-wins).
468
+ const isExternallyReferenced = externalRefs.has(`${step.targetModule}:${name}`);
469
+ const collidingExternalModule = externalNestedNames.get(name);
470
+ const actualOwner = existingSharedTypes.get(name);
471
+ const isExternal = isExternallyReferenced || !!collidingExternalModule || !!actualOwner;
472
+ const sourceModule = actualOwner || ((isExternal && collidingExternalModule) ? collidingExternalModule : step.targetModule);
473
+ return { name, sourceModule, isExternal };
474
+ })
475
+ : [];
476
+ const rawExternalTypes = registered && registered.definition && Array.isArray(registered.definition.externalTypes)
477
+ ? registered.definition.externalTypes.map((et) => ({ name: toPascalCase(et.name), sourceModule: toCamelCase(et.module), isExternal: true }))
478
+ : [];
479
+ const nestedTypeImports = [...rawNestedTypes, ...rawExternalTypes];
480
+ // If the step targets the same module as the workflow host (isLocal), but
481
+ // shared contracts already exist for this activity (generated by another
482
+ // workflow that uses it cross-module), use the shared import path.
483
+ const hasSharedContract = existingSharedTypes.has(step.activityName + 'Activity');
484
+ const effectiveIsLocal = step.isLocal && !hasSharedContract;
485
+ stubs.set(step.activityName, {
486
+ activityName: step.activityName,
487
+ activityCamel: step.activityCamel,
488
+ interfaceName: step.activityName + 'Activity',
489
+ stubVarName: step.activityCamel + 'Activity',
490
+ isLocal: effectiveIsLocal,
491
+ queue: step.stepQueue,
492
+ actType: step.actType,
493
+ timeout: parseTimeout(step.timeout),
494
+ targetModule: step.targetModule,
495
+ targetModulePascal: step.targetModulePascal,
496
+ isCompensation: false,
497
+ hasInput: step.hasInput,
498
+ hasOutput: step.hasOutput,
499
+ nestedTypeImports,
500
+ });
501
+ }
502
+
503
+ if (step.compensation) {
504
+ const compName = step.compensation.name;
505
+ if (!stubs.has(compName)) {
506
+ const compRegistered = activityRegistry.get(compName);
507
+ const compModule = step.compensation.module;
508
+ const compType = compRegistered ? compRegistered.type : 'light';
509
+ const compQueue = compType === 'heavy'
510
+ ? `${toScreamingSnakeCase(compModule)}_HEAVY_TASK_QUEUE`
511
+ : `${toScreamingSnakeCase(compModule)}_LIGHT_TASK_QUEUE`;
512
+ const compTimeout = compRegistered ? compRegistered.timeout : null;
513
+ const compNestedTypes = compRegistered && compRegistered.definition && Array.isArray(compRegistered.definition.nestedTypes)
514
+ ? compRegistered.definition.nestedTypes.map((nt) => {
515
+ const name = toPascalCase(nt.name);
516
+ const isExternallyReferenced = externalRefs.has(`${compModule}:${name}`);
517
+ const collidingExternalModule = externalNestedNames.get(name);
518
+ const actualOwner = existingSharedTypes.get(name);
519
+ const isExternal = isExternallyReferenced || !!collidingExternalModule || !!actualOwner;
520
+ const sourceModule = actualOwner || ((isExternal && collidingExternalModule) ? collidingExternalModule : compModule);
521
+ return { name, sourceModule, isExternal };
522
+ })
523
+ : [];
524
+ const compExternalTypes = compRegistered && compRegistered.definition && Array.isArray(compRegistered.definition.externalTypes)
525
+ ? compRegistered.definition.externalTypes.map((et) => ({ name: toPascalCase(et.name), sourceModule: toCamelCase(et.module), isExternal: true }))
526
+ : [];
527
+ const compNestedTypeImports = [...compNestedTypes, ...compExternalTypes];
528
+
529
+ const compHasSharedContract = existingSharedTypes.has(compName + 'Activity');
530
+ const compEffectiveIsLocal = (compModule === hostModule) && !compHasSharedContract;
531
+ stubs.set(compName, {
532
+ activityName: compName,
533
+ activityCamel: compName.charAt(0).toLowerCase() + compName.slice(1),
534
+ interfaceName: compName + 'Activity',
535
+ stubVarName: (compName.charAt(0).toLowerCase() + compName.slice(1)) + 'Activity',
536
+ isLocal: compEffectiveIsLocal,
537
+ queue: compQueue,
538
+ actType: compType,
539
+ timeout: parseTimeout(compTimeout),
540
+ targetModule: compModule,
541
+ targetModulePascal: toPascalCase(compModule),
542
+ isCompensation: true,
543
+ hasInput: (step.compensation.inputFields || []).length > 0,
544
+ hasOutput: false,
545
+ nestedTypeImports: compNestedTypeImports,
546
+ });
547
+ }
548
+ }
549
+ }
550
+
551
+ return Array.from(stubs.values());
552
+ }
553
+
554
+ /**
555
+ * Compute workflow input fields: all unique field names from step rawInputNames
556
+ * that don't derive from a prior step's rawOutputNames.
557
+ * Types are inferred from the activity's formal input fields (positional).
558
+ */
559
+ function computeWorkflowInputFields(steps) {
560
+ const fields = [];
561
+ const seenNames = new Set();
562
+ const priorOutputNames = new Set();
563
+
564
+ for (const step of steps) {
565
+ const rawNames = step.rawInputNames.length > 0
566
+ ? step.rawInputNames
567
+ : step.inputFields.map((f) => f.name);
568
+
569
+ for (let j = 0; j < rawNames.length; j++) {
570
+ const rawName = rawNames[j];
571
+ if (seenNames.has(rawName)) continue;
572
+ seenNames.add(rawName);
573
+
574
+ // Skip fields that come from a prior step's output
575
+ if (priorOutputNames.has(rawName)) continue;
576
+
577
+ // Type from activity's formal input field (positional match)
578
+ const formalField = step.inputFields[j];
579
+ const javaType = formalField ? formalField.javaType : 'String';
580
+
581
+ fields.push({ name: rawName, javaType });
582
+ }
583
+
584
+ // Add this step's outputs to the prior set
585
+ for (const name of step.rawOutputNames) {
586
+ priorOutputNames.add(name);
587
+ }
588
+ }
589
+
590
+ return fields;
591
+ }
592
+
593
+ /**
594
+ * Compute render blocks for the WorkFlowImpl template.
595
+ * Groups steps into sequential, parallel, or async blocks.
596
+ */
597
+ function computeRenderBlocks(steps, parallelGroups) {
598
+ const blocks = [];
599
+ const inParallelGroup = new Set();
600
+
601
+ for (const pg of parallelGroups) {
602
+ for (const s of pg.steps) {
603
+ inParallelGroup.add(s.index);
604
+ }
605
+ }
606
+
607
+ for (const step of steps) {
608
+ if (inParallelGroup.has(step.index)) {
609
+ // Only emit a block for the first step in the group
610
+ const group = parallelGroups.find((g) => g.steps[0].index === step.index);
611
+ if (group) {
612
+ blocks.push({ type: 'parallel', steps: group.steps });
613
+ }
614
+ } else if (step.isAsync) {
615
+ blocks.push({ type: 'async', step });
616
+ } else {
617
+ blocks.push({ type: 'sequential', step });
618
+ }
619
+ }
620
+
621
+ return blocks;
622
+ }
623
+
624
+ /**
625
+ * Register a workflow class in the module's TemporalWorkerConfig.
626
+ */
627
+ async function registerWorkflowInConfig(configPath, packageName, moduleName, flowPascalCase, modulePascalCase) {
628
+ if (!(await fs.pathExists(configPath))) return;
629
+
630
+ let content = await fs.readFile(configPath, 'utf-8');
631
+
632
+ const implClass = `${flowPascalCase}WorkFlowImpl`;
633
+ const importLine = `import ${packageName}.${moduleName}.application.usecases.${implClass};`;
634
+
635
+ // Skip if already registered
636
+ if (content.includes(implClass)) return;
637
+
638
+ // Add import
639
+ if (!content.includes(importLine)) {
640
+ const allImports = [...content.matchAll(/^import .+;/gm)];
641
+ if (allImports.length > 0) {
642
+ const lastImport = allImports[allImports.length - 1];
643
+ const insertPos = lastImport.index + lastImport[0].length;
644
+ content = content.slice(0, insertPos) + '\n' + importLine + content.slice(insertPos);
645
+ }
646
+ }
647
+
648
+ // Check if there is already an active registerWorkflowImplementationTypes call
649
+ const activeRegisterRegex = /workflowWorker\.registerWorkflowImplementationTypes\(([^)]+)\);/;
650
+ if (activeRegisterRegex.test(content)) {
651
+ content = content.replace(activeRegisterRegex, (match, classes) => {
652
+ const classList = classes
653
+ .split(',')
654
+ .map((c) => c.trim())
655
+ .filter((c) => c.length > 0);
656
+ if (!classList.includes(`${implClass}.class`)) {
657
+ classList.push(`${implClass}.class`);
658
+ }
659
+ return `workflowWorker.registerWorkflowImplementationTypes(${classList.join(', ')});`;
660
+ });
661
+ } else {
662
+ // Insert active registration after the comment marker
663
+ content = content.replace(
664
+ /(\/\/ registered by eva g temporal-flow\r?\n)/,
665
+ `$1 workflowWorker.registerWorkflowImplementationTypes(${implClass}.class);\n`
666
+ );
667
+ }
668
+
669
+ await fs.writeFile(configPath, content, 'utf-8');
670
+ }
671
+
672
+ /**
673
+ * Append module queue section to temporal.yaml across all environments.
674
+ */
675
+ async function appendModuleQueues(projectDir, moduleName, moduleScreamingSnake) {
676
+ const resourcesDir = path.join(projectDir, 'src', 'main', 'resources', 'parameters');
677
+ if (!(await fs.pathExists(resourcesDir))) return;
678
+
679
+ const envDirs = await fs.readdir(resourcesDir);
680
+ const queueSection = [
681
+ ` ${moduleName}:`,
682
+ ` flow-queue: ${moduleScreamingSnake}_WORKFLOW_QUEUE`,
683
+ ` light-queue: ${moduleScreamingSnake}_LIGHT_TASK_QUEUE`,
684
+ ` heavy-queue: ${moduleScreamingSnake}_HEAVY_TASK_QUEUE`,
685
+ ].join('\n');
686
+
687
+ for (const env of envDirs) {
688
+ const temporalYaml = path.join(resourcesDir, env, 'temporal.yaml');
689
+ if (!(await fs.pathExists(temporalYaml))) continue;
690
+
691
+ let content = await fs.readFile(temporalYaml, 'utf-8');
692
+
693
+ // Skip if module queues already exist
694
+ if (content.includes(`${moduleName}:`)) continue;
695
+
696
+ // Append under temporal.modules:
697
+ if (content.includes('modules:')) {
698
+ content = content.trimEnd() + '\n' + queueSection + '\n';
699
+ } else {
700
+ content = content.trimEnd() + '\n modules:\n' + queueSection + '\n';
701
+ }
702
+
703
+ await fs.writeFile(temporalYaml, content, 'utf-8');
704
+ }
705
+ }
706
+
707
+ module.exports = generateTemporalSystemCommand;
708
+ module.exports.computeWorkflowInputFields = computeWorkflowInputFields;