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.
- package/AGENTS.md +2 -0
- package/DOMAIN_YAML_GUIDE.md +3 -1
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +272 -8
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-read-models.yaml +2 -2
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +31 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +308 -16
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-temporal-activity.js +968 -34
- package/src/commands/generate-temporal-flow.js +95 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/skills/build-system-yaml/SKILL.md +222 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
- package/src/skills/build-system-yaml/references/module-spec.md +57 -8
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/domain-validator.js +266 -4
- package/src/utils/naming.js +10 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/yaml-to-entity.js +10 -7
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelJpa.java.ejs +1 -1
- package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
- package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
- package/test-c2010.js +0 -49
- package/test-update-compat.js +0 -109
- 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 };
|