eva4j 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  2. package/.agents/skills/skill-creator/SKILL.md +485 -0
  3. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  4. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  5. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  6. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  7. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  8. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  9. package/.agents/skills/skill-creator/references/schemas.md +430 -0
  10. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  11. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  12. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  13. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  14. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  15. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  16. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  17. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  18. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  19. package/AGENTS.md +268 -6
  20. package/COMMAND_EVALUATION.md +15 -16
  21. package/DOMAIN_YAML_GUIDE.md +430 -14
  22. package/FUTURE_FEATURES.md +1627 -1168
  23. package/README.md +461 -13
  24. package/bin/eva4j.js +32 -14
  25. package/config/defaults.json +1 -0
  26. package/docs/commands/EVALUATE_SYSTEM.md +746 -261
  27. package/docs/commands/EXPORT_DIAGRAM.md +153 -0
  28. package/docs/commands/GENERATE_ENTITIES.md +599 -6
  29. package/docs/commands/INDEX.md +7 -0
  30. package/examples/domain-events.yaml +166 -20
  31. package/examples/domain-listeners.yaml +212 -0
  32. package/examples/domain-one-to-many.yaml +1 -0
  33. package/examples/domain-one-to-one.yaml +1 -0
  34. package/examples/domain-ports.yaml +414 -0
  35. package/examples/domain-soft-delete.yaml +47 -44
  36. package/examples/system/notification.yaml +147 -0
  37. package/examples/system/product.yaml +185 -0
  38. package/examples/system/system.yaml +112 -0
  39. package/examples/system-report.html +971 -0
  40. package/examples/system.yaml +46 -3
  41. package/package.json +2 -1
  42. package/src/agents/design-reviewer.agent.md +163 -0
  43. package/src/commands/build.js +714 -0
  44. package/src/commands/create.js +1 -0
  45. package/src/commands/detach.js +149 -108
  46. package/src/commands/evaluate-system.js +234 -8
  47. package/src/commands/export-diagram.js +522 -0
  48. package/src/commands/generate-entities.js +685 -66
  49. package/src/commands/generate-http-exchange.js +2 -0
  50. package/src/commands/generate-kafka-event.js +43 -10
  51. package/src/generators/base-generator.js +18 -6
  52. package/src/generators/postman-generator.js +188 -0
  53. package/src/generators/shared-generator.js +12 -2
  54. package/src/skills/build-system-yaml/SKILL.md +323 -0
  55. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
  56. package/src/skills/build-system-yaml/references/module-spec.md +367 -0
  57. package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
  58. package/src/utils/config-manager.js +54 -0
  59. package/src/utils/context-builder.js +1 -0
  60. package/src/utils/domain-diagram.js +192 -0
  61. package/src/utils/domain-validator.js +1020 -0
  62. package/src/utils/fake-data.js +376 -0
  63. package/src/utils/system-validator.js +319 -199
  64. package/src/utils/yaml-to-entity.js +272 -7
  65. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  66. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  67. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
  68. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  69. package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
  70. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  71. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  72. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  73. package/templates/base/docker/Dockerfile.ejs +21 -0
  74. package/templates/base/gradle/build.gradle.ejs +3 -2
  75. package/templates/base/root/AGENTS.md.ejs +306 -45
  76. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  77. package/templates/crud/Controller.java.ejs +4 -4
  78. package/templates/crud/CreateCommand.java.ejs +4 -0
  79. package/templates/crud/CreateCommandHandler.java.ejs +3 -6
  80. package/templates/crud/CreateItemDto.java.ejs +4 -0
  81. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  82. package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
  83. package/templates/crud/EndpointsController.java.ejs +6 -6
  84. package/templates/crud/FindByQuery.java.ejs +1 -1
  85. package/templates/crud/FindByQueryHandler.java.ejs +3 -9
  86. package/templates/crud/GetQueryHandler.java.ejs +2 -5
  87. package/templates/crud/ListQuery.java.ejs +1 -1
  88. package/templates/crud/ListQueryHandler.java.ejs +8 -14
  89. package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
  90. package/templates/crud/ScaffoldQuery.java.ejs +3 -2
  91. package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
  92. package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
  93. package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
  94. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
  95. package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
  96. package/templates/crud/UpdateCommand.java.ejs +4 -0
  97. package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
  98. package/templates/evaluate/report.html.ejs +394 -2
  99. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  100. package/templates/kafka-event/Event.java.ejs +9 -0
  101. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  102. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  103. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  104. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  105. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
  106. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  107. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  108. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  109. package/templates/mock/MockEvent.java.ejs +10 -0
  110. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  111. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  112. package/templates/mock/SpringEventListener.java.ejs +61 -0
  113. package/templates/ports/PortDomainModel.java.ejs +35 -0
  114. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  115. package/templates/ports/PortFeignClient.java.ejs +45 -0
  116. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  117. package/templates/ports/PortInterface.java.ejs +45 -0
  118. package/templates/ports/PortNestedType.java.ejs +28 -0
  119. package/templates/ports/PortRequestDto.java.ejs +30 -0
  120. package/templates/ports/PortResponseDto.java.ejs +28 -0
  121. package/templates/postman/Collection.json.ejs +1 -1
  122. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  123. package/templates/shared/annotations/LogAfter.java.ejs +2 -0
  124. package/templates/shared/annotations/LogBefore.java.ejs +2 -0
  125. package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
  126. package/templates/shared/annotations/LogLevel.java.ejs +8 -0
  127. package/templates/shared/annotations/LogTimer.java.ejs +1 -0
  128. package/templates/shared/annotations/Loggable.java.ejs +17 -0
  129. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
  130. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
  131. package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
  132. package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
  133. package/src/commands/generate-system.js +0 -243
  134. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
  135. package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
  136. package/templates/base/root/skill-build-system-yaml.ejs +0 -252
  137. package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
  138. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
  139. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
@@ -0,0 +1,522 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ // ─── Draw.io C4 Style Strings ──────────────────────────────────────────────
8
+
9
+ const STYLES = {
10
+ person: [
11
+ 'shape=mxgraph.c4.person2',
12
+ 'whiteSpace=wrap',
13
+ 'html=1',
14
+ 'container=1',
15
+ 'fontSize=11',
16
+ 'fontColor=#ffffff',
17
+ 'align=center',
18
+ 'strokeColor=#3C7FC0',
19
+ 'fillColor=#08427B',
20
+ 'metaEdit=1',
21
+ 'points=[[0.5,0,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0]]',
22
+ 'resizable=0',
23
+ ].join(';'),
24
+
25
+ container: [
26
+ 'rounded=1',
27
+ 'whiteSpace=wrap',
28
+ 'html=1',
29
+ 'container=1',
30
+ 'fontSize=11',
31
+ 'fontColor=#ffffff',
32
+ 'fillColor=#438DD5',
33
+ 'strokeColor=#3C7FC0',
34
+ 'metaEdit=1',
35
+ 'points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]]',
36
+ ].join(';'),
37
+
38
+ containerDb: [
39
+ 'shape=mxgraph.c4.dataStoreContainer2',
40
+ 'whiteSpace=wrap',
41
+ 'html=1',
42
+ 'container=1',
43
+ 'fontSize=11',
44
+ 'fontColor=#ffffff',
45
+ 'fillColor=#438DD5',
46
+ 'strokeColor=#3C7FC0',
47
+ 'metaEdit=1',
48
+ 'points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]]',
49
+ ].join(';'),
50
+
51
+ containerQueue: [
52
+ 'shape=cylinder3',
53
+ 'whiteSpace=wrap',
54
+ 'html=1',
55
+ 'container=1',
56
+ 'fontSize=11',
57
+ 'fontColor=#ffffff',
58
+ 'fillColor=#438DD5',
59
+ 'strokeColor=#3C7FC0',
60
+ 'size=15',
61
+ 'metaEdit=1',
62
+ 'boundedLbl=1',
63
+ 'points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]]',
64
+ ].join(';'),
65
+
66
+ systemExt: [
67
+ 'rounded=1',
68
+ 'whiteSpace=wrap',
69
+ 'html=1',
70
+ 'container=1',
71
+ 'fontSize=11',
72
+ 'fontColor=#ffffff',
73
+ 'fillColor=#999999',
74
+ 'strokeColor=#8A8A8A',
75
+ 'metaEdit=1',
76
+ 'points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]]',
77
+ ].join(';'),
78
+
79
+ system: [
80
+ 'rounded=1',
81
+ 'whiteSpace=wrap',
82
+ 'html=1',
83
+ 'container=1',
84
+ 'fontSize=11',
85
+ 'fontColor=#ffffff',
86
+ 'fillColor=#1168BD',
87
+ 'strokeColor=#0B4884',
88
+ 'metaEdit=1',
89
+ 'points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]]',
90
+ ].join(';'),
91
+
92
+ boundary: [
93
+ 'rounded=1',
94
+ 'fontSize=11',
95
+ 'whiteSpace=wrap',
96
+ 'html=1',
97
+ 'dashed=1',
98
+ 'arcSize=20',
99
+ 'fillColor=none',
100
+ 'strokeColor=#666666',
101
+ 'fontColor=#333333',
102
+ 'container=1',
103
+ 'pointerEvents=0',
104
+ 'collapsible=0',
105
+ 'recursiveResize=0',
106
+ ].join(';'),
107
+
108
+ relSync: [
109
+ 'endArrow=blockThin',
110
+ 'html=1',
111
+ 'fontSize=10',
112
+ 'fontColor=#404040',
113
+ 'strokeWidth=2',
114
+ 'endFill=1',
115
+ 'strokeColor=#2171B5',
116
+ 'elbow=vertical',
117
+ 'metaEdit=1',
118
+ 'endSize=14',
119
+ ].join(';'),
120
+
121
+ relAsync: [
122
+ 'endArrow=blockThin',
123
+ 'html=1',
124
+ 'fontSize=10',
125
+ 'fontColor=#404040',
126
+ 'strokeWidth=2',
127
+ 'endFill=1',
128
+ 'strokeColor=#E6550D',
129
+ 'elbow=vertical',
130
+ 'metaEdit=1',
131
+ 'endSize=14',
132
+ ].join(';'),
133
+
134
+ relPerson: [
135
+ 'endArrow=blockThin',
136
+ 'html=1',
137
+ 'fontSize=10',
138
+ 'fontColor=#404040',
139
+ 'strokeWidth=2',
140
+ 'endFill=1',
141
+ 'strokeColor=#2CA02C',
142
+ 'elbow=vertical',
143
+ 'metaEdit=1',
144
+ 'endSize=14',
145
+ ].join(';'),
146
+ };
147
+
148
+ // ─── XML Helpers ────────────────────────────────────────────────────────────
149
+
150
+ function esc(s) {
151
+ return s
152
+ .replace(/&/g, '&')
153
+ .replace(/</g, '&lt;')
154
+ .replace(/>/g, '&gt;')
155
+ .replace(/"/g, '&quot;');
156
+ }
157
+
158
+ function htmlLabel(lines) {
159
+ return lines.map((l) => `&lt;div&gt;${l}&lt;/div&gt;`).join('');
160
+ }
161
+
162
+ function label(name, typeTag, description) {
163
+ const parts = [`&lt;b&gt;${esc(name)}&lt;/b&gt;`];
164
+ if (typeTag) parts.push(`&lt;div&gt;${esc(typeTag)}&lt;/div&gt;`);
165
+ parts.push('&lt;div&gt;&lt;br&gt;&lt;/div&gt;');
166
+ if (description) parts.push(`&lt;div&gt;${esc(description)}&lt;/div&gt;`);
167
+ return parts.join('');
168
+ }
169
+
170
+ function relLabelXml(text, technology) {
171
+ let out = `&lt;b&gt;${esc(text)}&lt;/b&gt;`;
172
+ if (technology) out += `&lt;div&gt;[${esc(technology)}]&lt;/div&gt;`;
173
+ return out;
174
+ }
175
+
176
+ // ─── Mermaid C4 Parser ──────────────────────────────────────────────────────
177
+
178
+ function parseMermaidC4(content) {
179
+ const lines = content.split('\n').map((l) => l.trim());
180
+ const nodes = [];
181
+ const rels = [];
182
+ let title = '';
183
+ let boundaryId = null;
184
+ let boundaryLabel = '';
185
+ let insideBoundary = false;
186
+
187
+ for (const line of lines) {
188
+ if (!line || /^(C4Container|C4Context)$/.test(line)) continue;
189
+ if (line === '}') { insideBoundary = false; continue; }
190
+
191
+ // title
192
+ let m = line.match(/^title\s+(.+)$/);
193
+ if (m) { title = m[1]; continue; }
194
+
195
+ // System_Boundary(id, "label") {
196
+ m = line.match(/^System_Boundary\(\s*(\w+)\s*,\s*"([^"]+)"\s*\)\s*\{?\s*$/);
197
+ if (m) { boundaryId = m[1]; boundaryLabel = m[2]; insideBoundary = true; continue; }
198
+
199
+ // Person(id, "name", "desc")
200
+ m = line.match(/^Person\(\s*(\w+)\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$/);
201
+ if (m) { nodes.push({ type: 'person', id: m[1], name: m[2], description: m[3], parent: insideBoundary ? boundaryId : null }); continue; }
202
+
203
+ // System(id, "name", "desc")
204
+ m = line.match(/^System\(\s*(\w+)\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$/);
205
+ if (m) { nodes.push({ type: 'system', id: m[1], name: m[2], description: m[3], parent: insideBoundary ? boundaryId : null }); continue; }
206
+
207
+ // Container(id, "name", "tech", "desc")
208
+ m = line.match(/^Container\(\s*(\w+)\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$/);
209
+ if (m) { nodes.push({ type: 'container', id: m[1], name: m[2], technology: m[3], description: m[4], parent: insideBoundary ? boundaryId : null }); continue; }
210
+
211
+ // ContainerDb(id, "name", "tech", "desc")
212
+ m = line.match(/^ContainerDb\(\s*(\w+)\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$/);
213
+ if (m) { nodes.push({ type: 'containerDb', id: m[1], name: m[2], technology: m[3], description: m[4], parent: insideBoundary ? boundaryId : null }); continue; }
214
+
215
+ // ContainerQueue(id, "name", "tech", "desc")
216
+ m = line.match(/^ContainerQueue\(\s*(\w+)\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$/);
217
+ if (m) { nodes.push({ type: 'containerQueue', id: m[1], name: m[2], technology: m[3], description: m[4], parent: insideBoundary ? boundaryId : null }); continue; }
218
+
219
+ // System_Ext(id, "name", "desc")
220
+ m = line.match(/^System_Ext\(\s*(\w+)\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)$/);
221
+ if (m) { nodes.push({ type: 'systemExt', id: m[1], name: m[2], description: m[3], parent: insideBoundary ? boundaryId : null }); continue; }
222
+
223
+ // Rel(from, to, "label", "tech") — tech is optional
224
+ m = line.match(/^Rel\(\s*(\w+)\s*,\s*(\w+)\s*,\s*"([^"]+)"\s*(?:,\s*"([^"]*)")?\s*\)$/);
225
+ if (m) { rels.push({ from: m[1], to: m[2], label: m[3], technology: m[4] || '' }); continue; }
226
+ }
227
+
228
+ return { title, boundaryId, boundaryLabel, nodes, rels };
229
+ }
230
+
231
+ // ─── Layout Engine ──────────────────────────────────────────────────────────
232
+ //
233
+ // Top-to-bottom:
234
+ // Row 1 — Persons (centered)
235
+ // Row 2 — System Boundary { containers in grid }
236
+ // Row 2b — DB (left) + Broker (right) at bottom of boundary
237
+ // Row 3 — External systems (centered)
238
+ //
239
+ // C4Context (no containers):
240
+ // Row 1 — Persons
241
+ // Row 2 — System
242
+ // Row 3 — Externals
243
+ //
244
+
245
+ const DIMS = {
246
+ person: { w: 170, h: 180 },
247
+ container: { w: 240, h: 120 },
248
+ containerDb: { w: 240, h: 120 },
249
+ containerQueue: { w: 240, h: 120 },
250
+ systemExt: { w: 240, h: 120 },
251
+ system: { w: 240, h: 120 },
252
+ };
253
+
254
+ const GAP_X = 40;
255
+ const GAP_Y = 60;
256
+ const BOUNDARY_PAD = 40;
257
+
258
+ function computeLayout(parsed) {
259
+ const positions = {};
260
+
261
+ const persons = parsed.nodes.filter((n) => n.type === 'person');
262
+ const containers = parsed.nodes.filter((n) => n.type === 'container');
263
+ const dbs = parsed.nodes.filter((n) => n.type === 'containerDb');
264
+ const queues = parsed.nodes.filter((n) => n.type === 'containerQueue');
265
+ const externals = parsed.nodes.filter((n) => n.type === 'systemExt');
266
+ const systems = parsed.nodes.filter((n) => n.type === 'system');
267
+
268
+ const COLS = containers.length || 1;
269
+ const containerRows = 1;
270
+ const infraItems = [...dbs, ...queues];
271
+ const innerW = COLS * DIMS.container.w + (COLS - 1) * GAP_X;
272
+ const boundaryW = innerW + 2 * BOUNDARY_PAD;
273
+
274
+ let cursorY = 0;
275
+
276
+ // ── Row 1: Persons ──
277
+ if (persons.length > 0) {
278
+ const totalW = persons.length * DIMS.person.w + (persons.length - 1) * GAP_X;
279
+ const startX = (boundaryW - totalW) / 2;
280
+ persons.forEach((p, i) => {
281
+ positions[p.id] = {
282
+ x: Math.max(0, startX) + i * (DIMS.person.w + GAP_X),
283
+ y: cursorY,
284
+ w: DIMS.person.w,
285
+ h: DIMS.person.h,
286
+ };
287
+ });
288
+ cursorY += DIMS.person.h + GAP_Y;
289
+ }
290
+
291
+ // ── C4Context: System node (no containers) ──
292
+ if (systems.length > 0 && containers.length === 0) {
293
+ const totalW = systems.length * DIMS.system.w + (systems.length - 1) * GAP_X;
294
+ const refW = Math.max(boundaryW, 500);
295
+ const startX = (refW - totalW) / 2;
296
+ systems.forEach((s, i) => {
297
+ positions[s.id] = {
298
+ x: Math.max(0, startX) + i * (DIMS.system.w + GAP_X),
299
+ y: cursorY,
300
+ w: DIMS.system.w,
301
+ h: DIMS.system.h,
302
+ };
303
+ });
304
+ cursorY += DIMS.system.h + GAP_Y;
305
+ }
306
+
307
+ // ── Row 2: Containers (no boundary) ──
308
+ if (containers.length > 0) {
309
+ containers.forEach((c, i) => {
310
+ positions[c.id] = {
311
+ x: i * (DIMS.container.w + GAP_X),
312
+ y: cursorY,
313
+ w: DIMS.container.w,
314
+ h: DIMS.container.h,
315
+ };
316
+ });
317
+ cursorY += DIMS.container.h + GAP_Y;
318
+ }
319
+
320
+ // ── Row 3: DB (centered) ──
321
+ if (dbs.length > 0) {
322
+ const dbTotalW = dbs.length * DIMS.containerDb.w + (dbs.length - 1) * GAP_X;
323
+ const dbStartX = (innerW - dbTotalW) / 2;
324
+ dbs.forEach((d, i) => {
325
+ positions[d.id] = {
326
+ x: Math.max(0, dbStartX) + i * (DIMS.containerDb.w + GAP_X),
327
+ y: cursorY,
328
+ w: DIMS.containerDb.w,
329
+ h: DIMS.containerDb.h,
330
+ };
331
+ });
332
+ cursorY += DIMS.containerDb.h + GAP_Y;
333
+ }
334
+
335
+ // ── Row 4: Broker (centered) ──
336
+ if (queues.length > 0) {
337
+ const qTotalW = queues.length * DIMS.containerQueue.w + (queues.length - 1) * GAP_X;
338
+ const qStartX = (innerW - qTotalW) / 2;
339
+ queues.forEach((q, i) => {
340
+ positions[q.id] = {
341
+ x: Math.max(0, qStartX) + i * (DIMS.containerQueue.w + GAP_X),
342
+ y: cursorY,
343
+ w: DIMS.containerQueue.w,
344
+ h: DIMS.containerQueue.h,
345
+ };
346
+ });
347
+ cursorY += DIMS.containerQueue.h + GAP_Y;
348
+ }
349
+
350
+ // ── Row 5: External Systems ──
351
+ if (externals.length > 0) {
352
+ const totalW = externals.length * DIMS.systemExt.w + (externals.length - 1) * GAP_X;
353
+ const startX = (innerW - totalW) / 2;
354
+ externals.forEach((e, i) => {
355
+ positions[e.id] = {
356
+ x: Math.max(0, startX) + i * (DIMS.systemExt.w + GAP_X),
357
+ y: cursorY,
358
+ w: DIMS.systemExt.w,
359
+ h: DIMS.systemExt.h,
360
+ };
361
+ });
362
+ }
363
+
364
+ return { positions };
365
+ }
366
+
367
+ // ─── Draw.io XML Generator ─────────────────────────────────────────────────
368
+
369
+ function generateDrawioXml(parsed) {
370
+ const { positions } = computeLayout(parsed);
371
+ const cells = [];
372
+ let relCounter = 0;
373
+
374
+ cells.push(' <mxCell id="0"/>');
375
+ cells.push(' <mxCell id="1" parent="0"/>');
376
+
377
+ // Nodes (all in root layer, no boundary grouping)
378
+ for (const node of parsed.nodes) {
379
+ const pos = positions[node.id];
380
+ if (!pos) continue;
381
+
382
+ let style, lbl;
383
+ const parent = '1';
384
+
385
+ switch (node.type) {
386
+ case 'person':
387
+ style = STYLES.person;
388
+ lbl = label(node.name, '[Person]', node.description);
389
+ break;
390
+ case 'container':
391
+ style = STYLES.container;
392
+ lbl = label(node.name, `[Container: ${node.technology}]`, node.description);
393
+ break;
394
+ case 'containerDb':
395
+ style = STYLES.containerDb;
396
+ lbl = label(node.name, `[Container: ${node.technology}]`, node.description);
397
+ break;
398
+ case 'containerQueue':
399
+ style = STYLES.containerQueue;
400
+ lbl = label(node.name, `[Container: ${node.technology}]`, node.description);
401
+ break;
402
+ case 'systemExt':
403
+ style = STYLES.systemExt;
404
+ lbl = label(node.name, '[External System]', node.description);
405
+ break;
406
+ case 'system':
407
+ style = STYLES.system;
408
+ lbl = label(node.name, '[Software System]', node.description);
409
+ break;
410
+ default:
411
+ continue;
412
+ }
413
+
414
+ cells.push(
415
+ ` <mxCell id="${node.id}" value="${lbl}" style="${style}" vertex="1" parent="${parent}">`,
416
+ ` <mxGeometry x="${pos.x}" y="${pos.y}" width="${pos.w}" height="${pos.h}" as="geometry"/>`,
417
+ ' </mxCell>'
418
+ );
419
+ }
420
+
421
+ // Classify nodes for relationship coloring
422
+ const dbNodeIds = new Set(parsed.nodes.filter((n) => n.type === 'containerDb').map((n) => n.id));
423
+ const brokerNodeIds = new Set(parsed.nodes.filter((n) => n.type === 'containerQueue').map((n) => n.id));
424
+ const personNodeIds = new Set(parsed.nodes.filter((n) => n.type === 'person').map((n) => n.id));
425
+
426
+ // Filter out individual DB connections, replace with a single one
427
+ const filteredRels = parsed.rels.filter((r) => !dbNodeIds.has(r.from) && !dbNodeIds.has(r.to));
428
+
429
+ const containerNodes = parsed.nodes.filter((n) => n.type === 'container');
430
+ if (dbNodeIds.size > 0 && containerNodes.length > 0) {
431
+ const midContainer = containerNodes[Math.floor(containerNodes.length / 2)];
432
+ filteredRels.push({
433
+ from: midContainer.id,
434
+ to: [...dbNodeIds][0],
435
+ label: 'All modules: Reads/Writes',
436
+ technology: 'JDBC',
437
+ });
438
+ }
439
+
440
+ // Relationships — green for persons, orange for async, blue for sync
441
+ for (const rel of filteredRels) {
442
+ const id = `rel_${++relCounter}`;
443
+ const lbl = relLabelXml(rel.label, rel.technology);
444
+ const isPerson = personNodeIds.has(rel.from) || personNodeIds.has(rel.to);
445
+ const isAsync = brokerNodeIds.has(rel.from) || brokerNodeIds.has(rel.to);
446
+ const style = isPerson ? STYLES.relPerson : isAsync ? STYLES.relAsync : STYLES.relSync;
447
+ cells.push(
448
+ ` <mxCell id="${id}" value="${lbl}" style="${style}" edge="1" source="${rel.from}" target="${rel.to}" parent="1">`,
449
+ ' <mxGeometry as="geometry"/>',
450
+ ' </mxCell>'
451
+ );
452
+ }
453
+
454
+ const diagramName = esc(parsed.title || 'C4 Diagram');
455
+ return [
456
+ '<mxfile host="app.diagrams.net">',
457
+ ` <diagram name="${diagramName}" id="c4-diagram">`,
458
+ ' <mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="900" math="0" shadow="0">',
459
+ ' <root>',
460
+ ...cells,
461
+ ' </root>',
462
+ ' </mxGraphModel>',
463
+ ' </diagram>',
464
+ '</mxfile>',
465
+ ].join('\n');
466
+ }
467
+
468
+ // ─── Command Handler ────────────────────────────────────────────────────────
469
+
470
+ async function exportDiagramCommand(type, options) {
471
+ if (type !== 'diagram') {
472
+ console.error(chalk.red(`❌ Unknown export type: ${type}`));
473
+ console.log(chalk.gray('Usage: eva export diagram'));
474
+ process.exit(1);
475
+ }
476
+
477
+ const systemDir = path.join(process.cwd(), 'system');
478
+
479
+ const diagrams = [
480
+ { src: 'c4-container.mmd', out: 'c4-container.drawio', name: 'C4 Container' },
481
+ { src: 'c4-context.mmd', out: 'c4-context.drawio', name: 'C4 Context' },
482
+ ];
483
+
484
+ let generated = 0;
485
+
486
+ for (const diagram of diagrams) {
487
+ const srcPath = path.join(systemDir, diagram.src);
488
+
489
+ if (!await fs.pathExists(srcPath)) {
490
+ console.log(chalk.yellow(` ⚠ ${diagram.src} not found — skipping`));
491
+ continue;
492
+ }
493
+
494
+ const content = await fs.readFile(srcPath, 'utf8');
495
+ const parsed = parseMermaidC4(content);
496
+
497
+ if (parsed.nodes.length === 0) {
498
+ console.log(chalk.yellow(` ⚠ ${diagram.src} has no parseable C4 nodes — skipping`));
499
+ continue;
500
+ }
501
+
502
+ const xml = generateDrawioXml(parsed);
503
+ const outPath = path.join(systemDir, diagram.out);
504
+ await fs.writeFile(outPath, xml, 'utf8');
505
+
506
+ console.log(chalk.green(` ✅ ${diagram.name} → system/${diagram.out}`));
507
+ const edgeCount = (xml.match(/edge="1"/g) || []).length;
508
+ console.log(chalk.gray(` ${parsed.nodes.length} nodes, ${edgeCount} relationships`));
509
+ generated++;
510
+ }
511
+
512
+ if (generated === 0) {
513
+ console.error(chalk.red('❌ No C4 diagram files found in system/'));
514
+ console.log(chalk.gray('Expected: system/c4-container.mmd and/or system/c4-context.mmd'));
515
+ process.exit(1);
516
+ }
517
+
518
+ console.log('');
519
+ console.log(chalk.blue(`Open the .drawio files at ${chalk.bold('https://app.diagrams.net/')}`));
520
+ }
521
+
522
+ module.exports = exportDiagramCommand;