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
@@ -0,0 +1,844 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generates Mermaid flowchart (Bounded Context Blueprint) per module from parsed domain.yaml objects.
5
+ *
6
+ * The blueprint shows the full behavioral picture of a bounded context:
7
+ * API surface, incoming events, use cases, aggregate structure, state machine,
8
+ * outgoing events, sync ports, and read models — all in one diagram.
9
+ *
10
+ * @param {Object} domainConfigs Plain object { [moduleName]: parsedDomainYaml }
11
+ * @param {Object} [systemConfig] Parsed system.yaml (optional). When present and
12
+ * orchestration.engine === 'temporal', the blueprint includes Temporal-specific
13
+ * subgraphs (Activities, Local Workflows, Workflow Participation) instead of
14
+ * broker-specific ones (Incoming Events, Sync Ports, Read Models).
15
+ * @returns {{ [moduleName]: { diagram: string, useCases: Object } }}
16
+ * Map of module → { diagram (Mermaid text), useCases (detail metadata per UC) }
17
+ */
18
+ function generateBlueprintDiagrams(domainConfigs, systemConfig = {}) {
19
+ const orchestration = systemConfig?.orchestration || {};
20
+ const isTemporalMode = !!(orchestration.enabled && orchestration.engine === 'temporal');
21
+ const systemWorkflows = isTemporalMode ? (systemConfig.workflows || []) : [];
22
+
23
+ const result = {};
24
+ for (const [moduleName, config] of Object.entries(domainConfigs)) {
25
+ result[moduleName] = generateModuleBlueprint(moduleName, config, {
26
+ isTemporalMode,
27
+ systemWorkflows,
28
+ });
29
+ }
30
+ return result;
31
+ }
32
+
33
+ // ── Helpers ───────────────────────────────────────────────────────────────────
34
+
35
+ function toPascal(name) {
36
+ if (!name) return name;
37
+ return name.charAt(0).toUpperCase() + name.slice(1);
38
+ }
39
+
40
+ /** Sanitize a string for use as a Mermaid node ID (alphanumeric + underscore). */
41
+ function toNodeId(prefix, name) {
42
+ return prefix + '_' + name.replace(/[^a-zA-Z0-9]/g, '_');
43
+ }
44
+
45
+ /** Escape label text for Mermaid (quotes inside labels). */
46
+ function esc(text) {
47
+ return text.replace(/"/g, '#quot;');
48
+ }
49
+
50
+ /** Truncate fields list: show first N fields, then "..." */
51
+ function summarizeFields(fields, max) {
52
+ const filtered = (fields || []).filter(
53
+ (f) => !AUDIT_FIELDS.has(f.name) && f.name !== 'id'
54
+ );
55
+ const shown = filtered.slice(0, max).map((f) => f.name);
56
+ if (filtered.length > max) shown.push('...');
57
+ return shown.join(' · ');
58
+ }
59
+
60
+ const AUDIT_FIELDS = new Set(['createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
61
+
62
+ // ── Subgraph style colors ─────────────────────────────────────────────────────
63
+
64
+ const COLORS = {
65
+ api: { bg: '#1a2a4a', border: '#4A90D9', text: '#4A90D9' },
66
+ asyncIn: { bg: '#1a3a2a', border: '#27AE60', text: '#27AE60' },
67
+ commands: { bg: '#1e2533', border: '#5B7BA5', text: '#8EAECB' },
68
+ queries: { bg: '#1e2533', border: '#5B7BA5', text: '#8EAECB' },
69
+ aggregate: { bg: '#2a1a3a', border: '#8E44AD', text: '#B07CC6' },
70
+ eventsOut: { bg: '#2a2215', border: '#E67E22', text: '#E67E22' },
71
+ ports: { bg: '#152a2a', border: '#16A085', text: '#16A085' },
72
+ readModels: { bg: '#1e2228', border: '#7F8C8D', text: '#95A5A6' },
73
+ // Temporal-specific
74
+ activities: { bg: '#1a2a3a', border: '#3498DB', text: '#3498DB' },
75
+ workflows: { bg: '#2a1a2a', border: '#9B59B6', text: '#9B59B6' },
76
+ wfParticipation: { bg: '#1e2a1e', border: '#2ECC71', text: '#2ECC71' },
77
+ };
78
+
79
+ // ── Per-module blueprint builder ──────────────────────────────────────────────
80
+
81
+ function generateModuleBlueprint(moduleName, config, opts = {}) {
82
+ const { isTemporalMode = false, systemWorkflows = [] } = opts;
83
+ const aggregates = config.aggregates || [];
84
+ if (aggregates.length === 0) return { diagram: '', useCases: {} };
85
+
86
+ const lines = ['flowchart TD'];
87
+ const edges = [];
88
+ const styles = [];
89
+ const clicks = []; // click-to-navigate directives
90
+ const useCaseDetails = {}; // ucName → detail object
91
+
92
+ // ── 1. Collect endpoints ────────────────────────────────────────────────
93
+ const endpoints = config.endpoints;
94
+ const epOperations = []; // { method, path, useCase }
95
+ if (endpoints && endpoints.versions) {
96
+ for (const ver of endpoints.versions) {
97
+ for (const op of ver.operations || []) {
98
+ epOperations.push({
99
+ method: op.method,
100
+ path: op.path,
101
+ useCase: op.useCase,
102
+ version: ver.version,
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ // Group endpoints by base resource for compact display
109
+ const epGroups = groupEndpointsByResource(epOperations, endpoints?.basePath || '');
110
+
111
+ // ── 2. Collect listeners ────────────────────────────────────────────────
112
+ const listeners = config.listeners || [];
113
+
114
+ // ── 3. Derive use cases ─────────────────────────────────────────────────
115
+ const commandUCs = new Map(); // useCase → nodeId
116
+ const queryUCs = new Map();
117
+ const seenUCs = new Set();
118
+
119
+ for (const op of epOperations) {
120
+ if (!op.useCase || seenUCs.has(op.useCase)) continue;
121
+ seenUCs.add(op.useCase);
122
+ if (op.method === 'GET') {
123
+ queryUCs.set(op.useCase, toNodeId('Q', op.useCase));
124
+ } else {
125
+ commandUCs.set(op.useCase, toNodeId('CMD', op.useCase));
126
+ }
127
+ }
128
+ for (const listener of listeners) {
129
+ if (!listener.useCase || seenUCs.has(listener.useCase)) continue;
130
+ seenUCs.add(listener.useCase);
131
+ commandUCs.set(listener.useCase, toNodeId('CMD', listener.useCase));
132
+ }
133
+
134
+ // ── 4. Collect events (outgoing) ────────────────────────────────────────
135
+ const outEvents = [];
136
+ for (const agg of aggregates) {
137
+ for (const ev of agg.events || []) {
138
+ outEvents.push(ev);
139
+ }
140
+ }
141
+
142
+ // ── 5. Collect ports ────────────────────────────────────────────────────
143
+ const ports = config.ports || [];
144
+ const portsByService = new Map();
145
+ for (const port of ports) {
146
+ const svc = port.service || 'UnknownService';
147
+ if (!portsByService.has(svc)) portsByService.set(svc, []);
148
+ portsByService.get(svc).push(port);
149
+ }
150
+
151
+ // ── 6. Collect read models ──────────────────────────────────────────────
152
+ const readModels = config.readModels || [];
153
+
154
+ // ── 7. Collect Temporal data (only when orchestration.engine === 'temporal') ──
155
+ const localActivities = isTemporalMode ? (config.activities || []) : [];
156
+ const localWorkflows = isTemporalMode ? (config.workflows || []) : [];
157
+
158
+ // Cross-module workflow participation: system workflows with at least one
159
+ // step targeting this module
160
+ const wfParticipation = [];
161
+ if (isTemporalMode) {
162
+ for (const wf of systemWorkflows) {
163
+ const involvedSteps = (wf.steps || []).filter((s) => s.target === moduleName);
164
+ if (involvedSteps.length > 0) {
165
+ wfParticipation.push({
166
+ name: wf.name,
167
+ saga: !!wf.saga,
168
+ triggerModule: wf.trigger?.module || null,
169
+ steps: involvedSteps,
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ // Events that notify (trigger) workflows
176
+ const eventNotifies = [];
177
+ if (isTemporalMode) {
178
+ for (const agg of aggregates) {
179
+ for (const ev of agg.events || []) {
180
+ if (ev.notifies && ev.notifies.length > 0) {
181
+ eventNotifies.push({ event: ev.name, workflows: ev.notifies.map((n) => n.workflow) });
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ // ── Build subgraphs ────────────────────────────────────────────────────
188
+
189
+ // ── API Surface ─────────────────────────────────────────────────────────
190
+ if (epGroups.length > 0) {
191
+ lines.push('');
192
+ lines.push(' subgraph API["🌐 API Surface"]');
193
+ for (const group of epGroups) {
194
+ const nodeId = toNodeId('EP', group.resource || 'root');
195
+ const methodsLabel = group.methods.join(' · ');
196
+ lines.push(` ${nodeId}["${esc(methodsLabel)}"]`);
197
+ }
198
+ lines.push(' end');
199
+ styles.push(
200
+ ...styleSubgraph('API', COLORS.api)
201
+ );
202
+ }
203
+
204
+ // ── Incoming Events ─────────────────────────────────────────────────────
205
+ if (listeners.length > 0) {
206
+ lines.push('');
207
+ lines.push(' subgraph ASYNC_IN["📥 Incoming Events"]');
208
+ for (const listener of listeners) {
209
+ const nodeId = toNodeId('EI', listener.event);
210
+ const producer = listener.producer ? ` ← ${listener.producer}` : '';
211
+ lines.push(
212
+ ` ${nodeId}["${esc(listener.event + producer)}"]`
213
+ );
214
+ }
215
+ lines.push(' end');
216
+ styles.push(...styleSubgraph('ASYNC_IN', COLORS.asyncIn));
217
+ }
218
+
219
+ // ── Commands ────────────────────────────────────────────────────────────
220
+ if (commandUCs.size > 0) {
221
+ lines.push('');
222
+ lines.push(' subgraph COMMANDS["⚙️ Commands"]');
223
+ for (const [uc, nodeId] of commandUCs) {
224
+ lines.push(` ${nodeId}["${esc(uc)}"]`);
225
+ clicks.push(` click ${nodeId} __evaNodeClick "${esc(uc)}"`);
226
+ }
227
+ lines.push(' end');
228
+ styles.push(...styleSubgraph('COMMANDS', COLORS.commands));
229
+ }
230
+
231
+ // ── Queries ─────────────────────────────────────────────────────────────
232
+ if (queryUCs.size > 0) {
233
+ lines.push('');
234
+ lines.push(' subgraph QUERIES["🔍 Queries"]');
235
+ for (const [uc, nodeId] of queryUCs) {
236
+ lines.push(` ${nodeId}["${esc(uc)}"]`);
237
+ clicks.push(` click ${nodeId} __evaNodeClick "${esc(uc)}"`);
238
+ }
239
+ lines.push(' end');
240
+ styles.push(...styleSubgraph('QUERIES', COLORS.queries));
241
+ }
242
+
243
+ // ── Aggregate ───────────────────────────────────────────────────────────
244
+ for (const agg of aggregates) {
245
+ const aggId = `AGG_${agg.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
246
+ lines.push('');
247
+ lines.push(` subgraph ${aggId}["📦 ${esc(agg.name)}"]`);
248
+ lines.push(' direction TB');
249
+
250
+ const entities = agg.entities || [];
251
+ const valueObjects = agg.valueObjects || [];
252
+ const enums = agg.enums || [];
253
+
254
+ // Entities
255
+ for (const entity of entities) {
256
+ const entId = toNodeId('ENT', entity.name);
257
+ const marker = entity.isRoot ? '🔶 ' : '';
258
+ const fieldsSummary = summarizeFields(entity.fields, 4);
259
+ const label = fieldsSummary
260
+ ? `${marker}${toPascal(entity.name)}\\n${fieldsSummary}`
261
+ : `${marker}${toPascal(entity.name)}`;
262
+ lines.push(` ${entId}["${esc(label)}"]`);
263
+
264
+ // Cross-aggregate references
265
+ for (const field of entity.fields || []) {
266
+ if (field.reference) {
267
+ const refMod = field.reference.module;
268
+ const refAgg = field.reference.aggregate;
269
+ const refLabel =
270
+ refMod && refMod !== moduleName
271
+ ? `${field.name} → ${refAgg} (${refMod})`
272
+ : `${field.name} → ${refAgg}`;
273
+ const refId = toNodeId('REF', field.name + '_' + refAgg);
274
+ lines.push(` ${refId}("🔗 ${esc(refLabel)}"):::refNode`);
275
+ edges.push(` ${entId} -.- ${refId}`);
276
+ }
277
+ }
278
+ }
279
+
280
+ // Value Objects
281
+ for (const vo of valueObjects) {
282
+ const voId = toNodeId('VO', vo.name);
283
+ const fieldsSummary = summarizeFields(vo.fields, 3);
284
+ const label = fieldsSummary
285
+ ? `💎 ${toPascal(vo.name)}\\n${fieldsSummary}`
286
+ : `💎 ${toPascal(vo.name)}`;
287
+ lines.push(` ${voId}["${esc(label)}"]`);
288
+ }
289
+
290
+ // Enums — show transition flow inline if available
291
+ for (const en of enums) {
292
+ const enumId = toNodeId('ENUM', en.name);
293
+ let label = `🔄 ${toPascal(en.name)}`;
294
+ if (en.transitions && en.transitions.length > 0) {
295
+ const transitionMap = buildTransitionSummary(en);
296
+ label += `\\n${transitionMap}`;
297
+ } else if (en.values) {
298
+ label += `\\n${en.values.join(' · ')}`;
299
+ }
300
+ lines.push(` ${enumId}["${esc(label)}"]`);
301
+ }
302
+
303
+ // Intra-aggregate edges (entity→VO, entity→enum, entity→entity)
304
+ for (const entity of entities) {
305
+ const entId = toNodeId('ENT', entity.name);
306
+ for (const rel of entity.relationships || []) {
307
+ const target = toPascal(rel.target || rel.targetEntity || '');
308
+ if (!target) continue;
309
+ const targetEntity = entities.find(
310
+ (e) => toPascal(e.name) === target
311
+ );
312
+ if (targetEntity) {
313
+ const targetId = toNodeId('ENT', targetEntity.name);
314
+ const relLabel = rel.type === 'OneToMany' ? '1:N' : rel.type === 'OneToOne' ? '1:1' : '';
315
+ edges.push(` ${entId} -->|"${relLabel}"| ${targetId}`);
316
+ }
317
+ }
318
+ // Field-type references to VOs and Enums
319
+ for (const field of entity.fields || []) {
320
+ if (AUDIT_FIELDS.has(field.name)) continue;
321
+ const fType = toPascal(field.type);
322
+ const matchingVO = valueObjects.find((v) => toPascal(v.name) === fType);
323
+ if (matchingVO) {
324
+ edges.push(` ${entId} -.->|"VO"| ${toNodeId('VO', matchingVO.name)}`);
325
+ }
326
+ const matchingEnum = enums.find((e) => toPascal(e.name) === fType);
327
+ if (matchingEnum) {
328
+ edges.push(` ${entId} -.->|"status"| ${toNodeId('ENUM', matchingEnum.name)}`);
329
+ }
330
+ }
331
+ }
332
+
333
+ lines.push(' end');
334
+ styles.push(...styleSubgraph(aggId, COLORS.aggregate));
335
+ }
336
+
337
+ // ── Outgoing Events ─────────────────────────────────────────────────────
338
+ if (outEvents.length > 0) {
339
+ lines.push('');
340
+ lines.push(' subgraph EVENTS_OUT["📤 Outgoing Events"]');
341
+ for (const ev of outEvents) {
342
+ const evId = toNodeId('EO', ev.name);
343
+ const payload = (ev.fields || [])
344
+ .filter((f) => !f.name.match(/Id$/i) || ev.fields.length <= 2)
345
+ .slice(0, 3)
346
+ .map((f) => f.name)
347
+ .join(' · ');
348
+ const label = payload
349
+ ? `${ev.name}\\n${payload}`
350
+ : ev.name;
351
+ lines.push(` ${evId}["${esc(label)}"]`);
352
+ }
353
+ lines.push(' end');
354
+ styles.push(...styleSubgraph('EVENTS_OUT', COLORS.eventsOut));
355
+ }
356
+
357
+ // ── Sync Ports ──────────────────────────────────────────────────────────
358
+ if (portsByService.size > 0) {
359
+ lines.push('');
360
+ lines.push(' subgraph PORTS["🔗 Sync Ports"]');
361
+ for (const [svc, methods] of portsByService) {
362
+ const svcId = toNodeId('PORT', svc);
363
+ const httpMethods = methods
364
+ .map((m) => {
365
+ const parts = (m.http || '').split(' ');
366
+ return parts.length >= 2 ? `${parts[0]} ${parts[1]}` : m.name;
367
+ })
368
+ .join('\\n');
369
+ const target = methods[0]?.target ? ` → ${methods[0].target}` : '';
370
+ lines.push(` ${svcId}["${esc(svc + target)}\\n${esc(httpMethods)}"]`);
371
+ }
372
+ lines.push(' end');
373
+ styles.push(...styleSubgraph('PORTS', COLORS.ports));
374
+ }
375
+
376
+ // ── Read Models ─────────────────────────────────────────────────────────
377
+ if (readModels.length > 0) {
378
+ lines.push('');
379
+ lines.push(' subgraph READ_MODELS["📦 Read Models"]');
380
+ for (const rm of readModels) {
381
+ const rmId = toNodeId('RM', rm.name);
382
+ const actions = [...new Set((rm.syncedBy || []).map((s) => s.action))].join(' · ');
383
+ const source = rm.source ? ` ← ${rm.source.module}` : '';
384
+ lines.push(` ${rmId}["${esc(rm.name + source)}\\n${esc(actions)}"]`);
385
+ }
386
+ lines.push(' end');
387
+ styles.push(...styleSubgraph('READ_MODELS', COLORS.readModels));
388
+ }
389
+
390
+ // ── Activities Exposed (Temporal) ───────────────────────────────────────
391
+ if (localActivities.length > 0) {
392
+ lines.push('');
393
+ lines.push(' subgraph ACTIVITIES["🎯 Activities Exposed"]');
394
+ for (const act of localActivities) {
395
+ const actId = toNodeId('ACT', act.name);
396
+ const typeTag = act.type === 'heavy' ? '⚙️' : '⚡';
397
+ const compTag = act.compensation ? ' ↩️' : '';
398
+ const timeout = act.timeout ? ` (${act.timeout})` : '';
399
+ const label = `${typeTag} ${act.name}${compTag}${timeout}`;
400
+ lines.push(` ${actId}["${esc(label)}"]`);
401
+ }
402
+ lines.push(' end');
403
+ styles.push(...styleSubgraph('ACTIVITIES', COLORS.activities));
404
+ }
405
+
406
+ // ── Local Workflows (Temporal, single-module) ───────────────────────────
407
+ if (localWorkflows.length > 0) {
408
+ lines.push('');
409
+ lines.push(' subgraph LOCAL_WF["🔄 Local Workflows"]');
410
+ for (const wf of localWorkflows) {
411
+ const wfId = toNodeId('LWF', wf.name);
412
+ const stepCount = (wf.steps || []).length;
413
+ const trigger = wf.trigger?.on ? ` on:${wf.trigger.on}` : '';
414
+ const label = `${wf.name}${trigger}\\n${stepCount} step${stepCount !== 1 ? 's' : ''}`;
415
+ lines.push(` ${wfId}["${esc(label)}"]`);
416
+ }
417
+ lines.push(' end');
418
+ styles.push(...styleSubgraph('LOCAL_WF', COLORS.workflows));
419
+ }
420
+
421
+ // ── Workflow Participation (Temporal, cross-module) ─────────────────────
422
+ if (wfParticipation.length > 0) {
423
+ lines.push('');
424
+ lines.push(' subgraph WF_PART["⚡ Workflow Participation"]');
425
+ for (const wf of wfParticipation) {
426
+ const wfId = toNodeId('WFP', wf.name);
427
+ const sagaTag = wf.saga ? '🔄 saga' : '';
428
+ const actNames = wf.steps.map((s) => s.activity).join(', ');
429
+ const from = wf.triggerModule ? ` ← ${wf.triggerModule}` : '';
430
+ const label = `${wf.name}${from}\\n${actNames}${sagaTag ? '\\n' + sagaTag : ''}`;
431
+ lines.push(` ${wfId}["${esc(label)}"]`);
432
+ }
433
+ lines.push(' end');
434
+ styles.push(...styleSubgraph('WF_PART', COLORS.wfParticipation));
435
+ }
436
+
437
+ // ── Edges ───────────────────────────────────────────────────────────────
438
+
439
+ lines.push('');
440
+ lines.push(' %% ── Connections ──────────────────────────────');
441
+
442
+ // EP groups → Use Cases
443
+ for (const group of epGroups) {
444
+ const epNodeId = toNodeId('EP', group.resource || 'root');
445
+ for (const uc of group.useCases) {
446
+ const targetId = queryUCs.get(uc) || commandUCs.get(uc);
447
+ if (targetId) {
448
+ edges.push(` ${epNodeId} --> ${targetId}`);
449
+ }
450
+ }
451
+ }
452
+
453
+ // Incoming Events → Use Cases
454
+ for (const listener of listeners) {
455
+ const eiId = toNodeId('EI', listener.event);
456
+ const targetId = commandUCs.get(listener.useCase);
457
+ if (targetId) {
458
+ edges.push(` ${eiId} --> ${targetId}`);
459
+ }
460
+ }
461
+
462
+ // Commands → Aggregate (first aggregate root)
463
+ const firstAgg = aggregates[0];
464
+ if (firstAgg && commandUCs.size > 0) {
465
+ const aggId = `AGG_${firstAgg.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
466
+ for (const [, nodeId] of commandUCs) {
467
+ edges.push(` ${nodeId} --> ${aggId}`);
468
+ }
469
+ }
470
+
471
+ // Aggregate → Outgoing Events
472
+ if (firstAgg && outEvents.length > 0) {
473
+ const aggId = `AGG_${firstAgg.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
474
+ for (const ev of outEvents) {
475
+ edges.push(` ${aggId} --> ${toNodeId('EO', ev.name)}`);
476
+ }
477
+ }
478
+
479
+ // Commands → Ports (dotted — sync calls)
480
+ if (portsByService.size > 0) {
481
+ // Connect all commands to all ports (in a real scenario, specific UCs call specific ports)
482
+ // but from domain.yaml alone we can't determine which UC calls which port.
483
+ // Use dotted lines from the aggregate to ports to indicate dependency.
484
+ if (firstAgg) {
485
+ const aggId = `AGG_${firstAgg.name.replace(/[^a-zA-Z0-9]/g, '_')}`;
486
+ for (const [svc] of portsByService) {
487
+ edges.push(` ${aggId} -.->|"sync"| ${toNodeId('PORT', svc)}`);
488
+ }
489
+ }
490
+ }
491
+
492
+ // ── Temporal edges ────────────────────────────────────────────────────
493
+ if (isTemporalMode) {
494
+ const aggId = firstAgg ? `AGG_${firstAgg.name.replace(/[^a-zA-Z0-9]/g, '_')}` : null;
495
+
496
+ // Aggregate → Activities (exposes)
497
+ if (aggId && localActivities.length > 0) {
498
+ for (const act of localActivities) {
499
+ edges.push(` ${aggId} -.->|"exposes"| ${toNodeId('ACT', act.name)}`);
500
+ }
501
+ }
502
+
503
+ // Outgoing Events → Workflows (via notifies)
504
+ for (const en of eventNotifies) {
505
+ const evId = toNodeId('EO', en.event);
506
+ for (const wfName of en.workflows) {
507
+ // Target can be a local workflow or a system workflow
508
+ const localWf = localWorkflows.find((w) => w.name === wfName);
509
+ if (localWf) {
510
+ edges.push(` ${evId} -->|"triggers"| ${toNodeId('LWF', wfName)}`);
511
+ }
512
+ const sysWf = wfParticipation.find((w) => w.name === wfName);
513
+ if (sysWf) {
514
+ edges.push(` ${evId} -->|"triggers"| ${toNodeId('WFP', wfName)}`);
515
+ }
516
+ // If workflow is cross-module but this module doesn't participate,
517
+ // still show the trigger edge to a standalone node
518
+ if (!localWf && !sysWf) {
519
+ const extId = toNodeId('WFX', wfName);
520
+ // Only add the node once
521
+ if (!edges.some((e) => e.includes(extId))) {
522
+ lines.push(` ${extId}("🌐 ${esc(wfName)}"):::refNode`);
523
+ }
524
+ edges.push(` ${evId} -->|"triggers"| ${extId}`);
525
+ }
526
+ }
527
+ }
528
+
529
+ // Workflow Participation → Activities (invokes)
530
+ for (const wf of wfParticipation) {
531
+ const wfId = toNodeId('WFP', wf.name);
532
+ for (const step of wf.steps) {
533
+ const actNode = localActivities.find((a) => a.name === step.activity);
534
+ if (actNode) {
535
+ const asyncTag = step.type === 'async' ? ' async' : '';
536
+ edges.push(` ${wfId} -->|"invokes${asyncTag}"| ${toNodeId('ACT', step.activity)}`);
537
+ }
538
+ }
539
+ }
540
+
541
+ // Local Workflow → Activities (step activities)
542
+ for (const wf of localWorkflows) {
543
+ const wfId = toNodeId('LWF', wf.name);
544
+ for (const step of wf.steps || []) {
545
+ if (step.activity) {
546
+ const actNode = localActivities.find((a) => a.name === step.activity);
547
+ if (actNode) {
548
+ edges.push(` ${wfId} -->|"step"| ${toNodeId('ACT', step.activity)}`);
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ // Compensation edges (activity → compensation activity)
555
+ for (const act of localActivities) {
556
+ if (act.compensation) {
557
+ const compAct = localActivities.find((a) => a.name === act.compensation);
558
+ if (compAct) {
559
+ edges.push(` ${toNodeId('ACT', act.name)} -.->|"compensates"| ${toNodeId('ACT', act.compensation)}`);
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ // Deduplicate edges
566
+ const uniqueEdges = [...new Set(edges)];
567
+ lines.push(...uniqueEdges);
568
+
569
+ // ── Styles ──────────────────────────────────────────────────────────────
570
+ lines.push('');
571
+ lines.push(' %% ── Styles ──────────────────────────────────');
572
+ lines.push(...styles);
573
+ lines.push(' classDef refNode fill:#2a2a3a,stroke:#7F8C8D,stroke-width:1px,color:#95A5A6,font-size:11px');
574
+
575
+ // ── Click directives ────────────────────────────────────────────────────
576
+ if (clicks.length > 0) {
577
+ lines.push('');
578
+ lines.push(' %% ── Click handlers ─────────────────────────');
579
+ lines.push(...clicks);
580
+ }
581
+
582
+ // ── Build use case detail metadata ──────────────────────────────────────
583
+ const allTransitions = [];
584
+ const allEvents = [];
585
+ for (const agg of aggregates) {
586
+ for (const en of agg.enums || []) {
587
+ for (const tr of en.transitions || []) {
588
+ const froms = Array.isArray(tr.from) ? tr.from : [tr.from];
589
+ allTransitions.push({ method: tr.method, froms, to: tr.to, guard: tr.guard, enum: en.name });
590
+ }
591
+ }
592
+ for (const ev of agg.events || []) {
593
+ allEvents.push(ev);
594
+ }
595
+ }
596
+
597
+ const rootEntity = aggregates[0]?.entities?.find((e) => e.isRoot) || aggregates[0]?.entities?.[0];
598
+ const aggName = aggregates[0]?.name || moduleName;
599
+
600
+ // Helper: derive standard CRUD descriptions
601
+ function describeStandardUC(ucName) {
602
+ const entity = aggName;
603
+ if (ucName === `Create${entity}`) return `Crea una nueva instancia de ${entity} y la persiste en el repositorio.`;
604
+ if (ucName === `Update${entity}`) return `Actualiza los campos modificables de un ${entity} existente.`;
605
+ if (ucName === `Delete${entity}`) return `Elimina un ${entity} del repositorio${rootEntity?.hasSoftDelete ? ' (soft delete)' : ''}.`;
606
+ if (ucName === `Get${entity}`) return `Obtiene un ${entity} por su identificador.`;
607
+ if (ucName.startsWith('FindAll')) return `Lista todos los ${entity} disponibles con soporte de paginación.`;
608
+ return null;
609
+ }
610
+
611
+ for (const [ucName, nodeId] of [...commandUCs, ...queryUCs]) {
612
+ const type = queryUCs.has(ucName) ? 'query' : 'command';
613
+ const detail = { name: ucName, type };
614
+
615
+ // Endpoint info
616
+ const ep = epOperations.find((o) => o.useCase === ucName);
617
+ if (ep) {
618
+ const basePath = config.endpoints?.basePath || '';
619
+ detail.endpoint = {
620
+ method: ep.method,
621
+ path: (basePath + (ep.path || '')).replace(/\/+/g, '/'),
622
+ version: ep.version || null,
623
+ };
624
+ }
625
+
626
+ // Listener info
627
+ const listener = listeners.find((l) => l.useCase === ucName);
628
+ if (listener) {
629
+ detail.triggeredBy = {
630
+ event: listener.event,
631
+ producer: listener.producer || null,
632
+ topic: listener.topic || null,
633
+ };
634
+ }
635
+
636
+ // Aggregate
637
+ detail.aggregate = aggName;
638
+
639
+ // State transitions triggered
640
+ const matchedTransitions = allTransitions.filter((t) => {
641
+ // Match by method name similarity to UC name
642
+ // e.g. confirm → ConfirmOrder, cancel → CancelOrder
643
+ return ucName.toLowerCase().includes(t.method.toLowerCase());
644
+ });
645
+ if (matchedTransitions.length > 0) {
646
+ detail.stateTransitions = matchedTransitions.map((t) => ({
647
+ method: t.method,
648
+ from: t.froms.join(', '),
649
+ to: t.to,
650
+ guard: t.guard || null,
651
+ enum: t.enum,
652
+ }));
653
+ }
654
+
655
+ // Events emitted
656
+ const matchedEvents = allEvents.filter((ev) => {
657
+ // Match by triggers
658
+ if (ev.triggers) {
659
+ return ev.triggers.some((trigger) => ucName.toLowerCase().includes(trigger.toLowerCase()));
660
+ }
661
+ // Match by lifecycle
662
+ if (ev.lifecycle) {
663
+ const lcMap = { create: 'Create', update: 'Update', delete: 'Delete', softDelete: 'Delete' };
664
+ return ucName.startsWith(lcMap[ev.lifecycle] || '');
665
+ }
666
+ return false;
667
+ });
668
+ if (matchedEvents.length > 0) {
669
+ detail.eventsEmitted = matchedEvents.map((ev) => ({
670
+ name: ev.name,
671
+ fields: (ev.fields || []).map((f) => f.name + ': ' + f.type),
672
+ }));
673
+ }
674
+
675
+ // Ports used (we can't know exactly which UC calls which port from domain.yaml,
676
+ // but we list all available ports as "available sync dependencies")
677
+ if (ports.length > 0) {
678
+ detail.availablePorts = [];
679
+ for (const [svc, methods] of portsByService) {
680
+ detail.availablePorts.push({
681
+ service: svc,
682
+ target: methods[0]?.target || null,
683
+ methods: methods.map((m) => m.http || m.name),
684
+ });
685
+ }
686
+ }
687
+
688
+ // Temporal: workflows triggered by events emitted from this UC
689
+ if (isTemporalMode && detail.eventsEmitted) {
690
+ const triggeredWfs = [];
691
+ for (const emitted of detail.eventsEmitted) {
692
+ const match = eventNotifies.find((en) => en.event === emitted.name);
693
+ if (match) {
694
+ triggeredWfs.push(...match.workflows);
695
+ }
696
+ }
697
+ if (triggeredWfs.length > 0) {
698
+ detail.triggersWorkflows = triggeredWfs;
699
+ }
700
+ }
701
+
702
+ // Temporal: activities exposed by this module
703
+ if (isTemporalMode && localActivities.length > 0) {
704
+ detail.activitiesExposed = localActivities.map((a) => ({
705
+ name: a.name,
706
+ type: a.type || 'light',
707
+ compensation: a.compensation || null,
708
+ }));
709
+ }
710
+
711
+ // Temporal: cross-module workflows that invoke this module
712
+ if (isTemporalMode && wfParticipation.length > 0) {
713
+ detail.workflowParticipation = wfParticipation.map((wf) => ({
714
+ workflow: wf.name,
715
+ saga: wf.saga,
716
+ triggerModule: wf.triggerModule,
717
+ activities: wf.steps.map((s) => s.activity),
718
+ }));
719
+ }
720
+
721
+ // Request fields (from root entity, non-readOnly, non-audit for commands)
722
+ if (type === 'command' && rootEntity && ep) {
723
+ const isCreate = ucName.startsWith('Create');
724
+ const reqFields = (rootEntity.fields || [])
725
+ .filter((f) => !AUDIT_FIELDS.has(f.name) && f.name !== 'id' && f.name !== 'deletedAt')
726
+ .filter((f) => !f.readOnly)
727
+ .map((f) => ({ name: f.name, type: f.type, required: !!(f.validations?.length) }));
728
+ if (reqFields.length > 0) detail.requestFields = reqFields;
729
+ }
730
+
731
+ // Description
732
+ const stdDesc = describeStandardUC(ucName);
733
+ if (stdDesc) {
734
+ detail.description = stdDesc;
735
+ detail.isStandard = true;
736
+ } else {
737
+ // Custom use case — build description from context
738
+ const parts = [];
739
+ if (detail.triggeredBy) {
740
+ parts.push(`Se ejecuta al recibir el evento ${detail.triggeredBy.event}${detail.triggeredBy.producer ? ' del módulo ' + detail.triggeredBy.producer : ''}.`);
741
+ }
742
+ if (detail.stateTransitions) {
743
+ for (const t of detail.stateTransitions) {
744
+ parts.push(`Invoca ${t.method}() que transiciona ${t.enum} de [${t.from}] → ${t.to}${t.guard ? ' (guard: ' + t.guard + ')' : ''}.`);
745
+ }
746
+ }
747
+ if (detail.eventsEmitted) {
748
+ parts.push(`Emite: ${detail.eventsEmitted.map((e) => e.name).join(', ')}.`);
749
+ }
750
+ if (detail.triggersWorkflows) {
751
+ parts.push(`Dispara workflow${detail.triggersWorkflows.length > 1 ? 's' : ''}: ${detail.triggersWorkflows.join(', ')}.`);
752
+ }
753
+ if (ep && !detail.triggeredBy) {
754
+ parts.unshift(`Endpoint: ${ep.method} ${(config.endpoints?.basePath || '') + (ep.path || '')}`);
755
+ }
756
+ detail.description = parts.length > 0
757
+ ? parts.join(' ')
758
+ : 'Caso de uso custom — requiere implementación manual del handler.';
759
+ detail.isStandard = false;
760
+ }
761
+
762
+ useCaseDetails[ucName] = detail;
763
+ }
764
+
765
+ return {
766
+ diagram: lines.join('\n'),
767
+ useCases: useCaseDetails,
768
+ };
769
+ }
770
+
771
+ // ── Group endpoints by resource path ──────────────────────────────────────────
772
+
773
+ function groupEndpointsByResource(operations, basePath) {
774
+ // Group by resource segment: e.g. /orders/{id}/confirm → "orders"
775
+ // Operations on root path (/ or /{id}) → group "root"
776
+ // Operations on sub-paths (/{id}/confirm) → include action in method label
777
+ const groups = new Map(); // resource → { methods: [], useCases: [] }
778
+
779
+ for (const op of operations) {
780
+ const fullPath = (basePath + (op.path || '')).replace(/\/+/g, '/');
781
+ const segments = fullPath.split('/').filter(Boolean);
782
+
783
+ // Determine resource key and method label
784
+ let resource = 'root';
785
+ let methodLabel = op.method;
786
+
787
+ // If path has action segments beyond /{id} (e.g. /{id}/confirm)
788
+ const actionSegments = segments.filter(
789
+ (s) => !s.startsWith('{') && s !== segments[0]
790
+ );
791
+ if (actionSegments.length > 0) {
792
+ methodLabel = `${op.method}·${actionSegments[actionSegments.length - 1]}`;
793
+ }
794
+
795
+ if (!groups.has(resource)) {
796
+ groups.set(resource, { resource, methods: [], useCases: [] });
797
+ }
798
+ const group = groups.get(resource);
799
+ if (!group.methods.includes(methodLabel)) {
800
+ group.methods.push(methodLabel);
801
+ }
802
+ if (op.useCase && !group.useCases.includes(op.useCase)) {
803
+ group.useCases.push(op.useCase);
804
+ }
805
+ }
806
+
807
+ return [...groups.values()];
808
+ }
809
+
810
+ // ── Build transition summary for enum ─────────────────────────────────────────
811
+
812
+ function buildTransitionSummary(en) {
813
+ if (!en.transitions || en.transitions.length === 0) {
814
+ return en.values ? en.values.join(' · ') : '';
815
+ }
816
+
817
+ // Build a compact transition map: STATE → STATE, STATE
818
+ // Group by 'from' to show branching
819
+ const fromMap = new Map();
820
+ for (const t of en.transitions) {
821
+ const froms = Array.isArray(t.from) ? t.from : [t.from];
822
+ for (const from of froms) {
823
+ if (!fromMap.has(from)) fromMap.set(from, []);
824
+ fromMap.get(from).push(t.to);
825
+ }
826
+ }
827
+
828
+ const parts = [];
829
+ for (const [from, tos] of fromMap) {
830
+ parts.push(`${from} → ${tos.join(', ')}`);
831
+ }
832
+
833
+ return parts.join('\\n');
834
+ }
835
+
836
+ // ── Style helpers ─────────────────────────────────────────────────────────────
837
+
838
+ function styleSubgraph(subgraphId, color) {
839
+ return [
840
+ ` style ${subgraphId} fill:${color.bg},stroke:${color.border},stroke-width:2px,color:${color.text}`,
841
+ ];
842
+ }
843
+
844
+ module.exports = { generateBlueprintDiagrams };