@diagrammo/dgmo 0.8.3 → 0.8.4

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 (112) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +185 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +153 -153
  9. package/dist/editor.cjs +336 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3336 -1055
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +3336 -1055
  18. package/dist/index.js.map +1 -1
  19. package/docs/language-reference.md +30 -29
  20. package/gallery/fixtures/arc.dgmo +18 -0
  21. package/gallery/fixtures/area.dgmo +19 -0
  22. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  23. package/gallery/fixtures/bar.dgmo +10 -0
  24. package/gallery/fixtures/c4-full.dgmo +52 -0
  25. package/gallery/fixtures/c4.dgmo +17 -0
  26. package/gallery/fixtures/chord.dgmo +12 -0
  27. package/gallery/fixtures/class-basic.dgmo +14 -0
  28. package/gallery/fixtures/class-full.dgmo +43 -0
  29. package/gallery/fixtures/doughnut.dgmo +8 -0
  30. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  31. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  32. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  33. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  35. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  36. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  37. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  38. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  39. package/gallery/fixtures/function.dgmo +8 -0
  40. package/gallery/fixtures/funnel.dgmo +7 -0
  41. package/gallery/fixtures/gantt-full.dgmo +49 -0
  42. package/gallery/fixtures/gantt.dgmo +42 -0
  43. package/gallery/fixtures/heatmap.dgmo +8 -0
  44. package/gallery/fixtures/infra-full.dgmo +78 -0
  45. package/gallery/fixtures/infra-overload.dgmo +25 -0
  46. package/gallery/fixtures/infra.dgmo +47 -0
  47. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  48. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  49. package/gallery/fixtures/initiative-status.dgmo +9 -0
  50. package/gallery/fixtures/line.dgmo +19 -0
  51. package/gallery/fixtures/multi-line.dgmo +11 -0
  52. package/gallery/fixtures/org-basic.dgmo +16 -0
  53. package/gallery/fixtures/org-full.dgmo +69 -0
  54. package/gallery/fixtures/org-teams.dgmo +25 -0
  55. package/gallery/fixtures/pie.dgmo +9 -0
  56. package/gallery/fixtures/polar-area.dgmo +8 -0
  57. package/gallery/fixtures/quadrant.dgmo +18 -0
  58. package/gallery/fixtures/radar.dgmo +8 -0
  59. package/gallery/fixtures/sankey.dgmo +31 -0
  60. package/gallery/fixtures/scatter.dgmo +21 -0
  61. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  62. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  63. package/gallery/fixtures/sequence.dgmo +35 -0
  64. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  65. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  66. package/gallery/fixtures/slope.dgmo +8 -0
  67. package/gallery/fixtures/spr-eras.dgmo +62 -0
  68. package/gallery/fixtures/state.dgmo +30 -0
  69. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  70. package/gallery/fixtures/timeline.dgmo +32 -0
  71. package/gallery/fixtures/venn.dgmo +10 -0
  72. package/gallery/fixtures/wordcloud.dgmo +24 -0
  73. package/package.json +51 -2
  74. package/src/c4/layout.ts +372 -90
  75. package/src/c4/parser.ts +100 -55
  76. package/src/chart.ts +91 -28
  77. package/src/class/parser.ts +41 -12
  78. package/src/cli.ts +168 -61
  79. package/src/completion.ts +378 -183
  80. package/src/d3.ts +887 -288
  81. package/src/dgmo-mermaid.ts +16 -13
  82. package/src/dgmo-router.ts +69 -23
  83. package/src/echarts.ts +646 -153
  84. package/src/editor/dgmo.grammar +69 -0
  85. package/src/editor/dgmo.grammar.d.ts +2 -0
  86. package/src/editor/dgmo.grammar.js +18 -0
  87. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  88. package/src/editor/dgmo.grammar.terms.js +35 -0
  89. package/src/editor/highlight.ts +36 -0
  90. package/src/editor/index.ts +28 -0
  91. package/src/editor/keywords.ts +220 -0
  92. package/src/editor/tokens.ts +30 -0
  93. package/src/er/parser.ts +48 -14
  94. package/src/er/renderer.ts +112 -53
  95. package/src/gantt/calculator.ts +91 -29
  96. package/src/gantt/parser.ts +197 -71
  97. package/src/gantt/renderer.ts +1120 -350
  98. package/src/graph/flowchart-parser.ts +46 -25
  99. package/src/graph/state-parser.ts +47 -17
  100. package/src/infra/parser.ts +157 -53
  101. package/src/infra/renderer.ts +723 -271
  102. package/src/initiative-status/parser.ts +138 -44
  103. package/src/kanban/parser.ts +25 -14
  104. package/src/org/layout.ts +111 -44
  105. package/src/org/parser.ts +69 -22
  106. package/src/palettes/index.ts +3 -2
  107. package/src/sequence/parser.ts +193 -61
  108. package/src/sitemap/parser.ts +65 -29
  109. package/src/utils/arrows.ts +2 -22
  110. package/src/utils/duration.ts +39 -21
  111. package/src/utils/legend-constants.ts +0 -2
  112. package/src/utils/parsing.ts +75 -31
package/src/c4/layout.ts CHANGED
@@ -3,9 +3,20 @@
3
3
  // ============================================================
4
4
 
5
5
  import dagre from '@dagrejs/dagre';
6
- import type { ParsedC4, C4Element, C4Relationship, C4ArrowType, C4Shape, C4DeploymentNode } from './types';
6
+ import type {
7
+ ParsedC4,
8
+ C4Element,
9
+ C4Relationship,
10
+ C4ArrowType,
11
+ C4Shape,
12
+ C4DeploymentNode,
13
+ } from './types';
7
14
  import type { TagGroup } from '../utils/tag-groups';
8
- import { LEGEND_PILL_FONT_SIZE, LEGEND_ENTRY_FONT_SIZE, measureLegendText } from '../utils/legend-constants';
15
+ import {
16
+ LEGEND_PILL_FONT_SIZE,
17
+ LEGEND_ENTRY_FONT_SIZE,
18
+ measureLegendText,
19
+ } from '../utils/legend-constants';
9
20
 
10
21
  // ============================================================
11
22
  // Types
@@ -141,7 +152,10 @@ function computeEdgePenalty(
141
152
  const tx = nodePositions.get(edge.target);
142
153
  if (sx == null || tx == null) continue;
143
154
  const dist = Math.abs(sx - tx);
144
- const weight = Math.min(degrees.get(edge.source) ?? 1, degrees.get(edge.target) ?? 1);
155
+ const weight = Math.min(
156
+ degrees.get(edge.source) ?? 1,
157
+ degrees.get(edge.target) ?? 1
158
+ );
145
159
  penalty += dist * weight;
146
160
  }
147
161
 
@@ -216,7 +230,12 @@ function reduceCrossings(
216
230
  const nodeGeometry = new Map<string, NodeGeometry>();
217
231
  for (const name of g.nodes()) {
218
232
  const pos = g.node(name);
219
- if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
233
+ if (pos)
234
+ nodeGeometry.set(name, {
235
+ y: pos.y,
236
+ width: pos.width,
237
+ height: pos.height,
238
+ });
220
239
  }
221
240
 
222
241
  // Group nodes by rank
@@ -265,7 +284,9 @@ function reduceCrossings(
265
284
  if (partition.length < 2) continue;
266
285
 
267
286
  // Collect the x-slots for this partition (sorted)
268
- const xSlots = partition.map((name) => g.node(name).x).sort((a, b) => a - b);
287
+ const xSlots = partition
288
+ .map((name) => g.node(name).x)
289
+ .sort((a, b) => a - b);
269
290
 
270
291
  // Build position map snapshot
271
292
  const basePositions = new Map<string, number>();
@@ -275,7 +296,12 @@ function reduceCrossings(
275
296
  }
276
297
 
277
298
  // Current penalty
278
- const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
299
+ const currentPenalty = computeEdgePenalty(
300
+ edgeList,
301
+ basePositions,
302
+ degrees,
303
+ nodeGeometry
304
+ );
279
305
 
280
306
  // Try permutations (feasible for partition sizes ≤ 8)
281
307
  let bestPerm = [...partition];
@@ -288,7 +314,12 @@ function reduceCrossings(
288
314
  for (let i = 0; i < perm.length; i++) {
289
315
  testPositions.set(perm[i]!, xSlots[i]!);
290
316
  }
291
- const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
317
+ const penalty = computeEdgePenalty(
318
+ edgeList,
319
+ testPositions,
320
+ degrees,
321
+ nodeGeometry
322
+ );
292
323
  if (penalty < bestPenalty) {
293
324
  bestPenalty = penalty;
294
325
  bestPerm = [...perm];
@@ -307,14 +338,27 @@ function reduceCrossings(
307
338
  for (let k = 0; k < workingOrder.length; k++) {
308
339
  testPositions.set(workingOrder[k]!, xSlots[k]!);
309
340
  }
310
- const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
311
-
312
- [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1]!, workingOrder[i]!];
341
+ const before = computeEdgePenalty(
342
+ edgeList,
343
+ testPositions,
344
+ degrees,
345
+ nodeGeometry
346
+ );
347
+
348
+ [workingOrder[i], workingOrder[i + 1]] = [
349
+ workingOrder[i + 1]!,
350
+ workingOrder[i]!,
351
+ ];
313
352
  const testPositions2 = new Map(basePositions);
314
353
  for (let k = 0; k < workingOrder.length; k++) {
315
354
  testPositions2.set(workingOrder[k]!, xSlots[k]!);
316
355
  }
317
- const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
356
+ const after = computeEdgePenalty(
357
+ edgeList,
358
+ testPositions2,
359
+ degrees,
360
+ nodeGeometry
361
+ );
318
362
 
319
363
  if (after < before) {
320
364
  improved = true;
@@ -323,7 +367,10 @@ function reduceCrossings(
323
367
  bestPerm = [...workingOrder];
324
368
  }
325
369
  } else {
326
- [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1]!, workingOrder[i]!];
370
+ [workingOrder[i], workingOrder[i + 1]] = [
371
+ workingOrder[i + 1]!,
372
+ workingOrder[i]!,
373
+ ];
327
374
  }
328
375
  }
329
376
  }
@@ -438,7 +485,7 @@ function buildOwnershipMap(elements: C4Element[]): Map<string, string> {
438
485
  */
439
486
  function collectAllRelationships(
440
487
  elements: C4Element[],
441
- ownerMap: Map<string, string>
488
+ _ownerMap: Map<string, string>
442
489
  ): { sourceName: string; rel: C4Relationship }[] {
443
490
  const result: { sourceName: string; rel: C4Relationship }[] = [];
444
491
 
@@ -469,7 +516,9 @@ function collectAllRelationships(
469
516
  * - Deduplicates: same source→target pair keeps only one (first seen).
470
517
  * - Explicit system-level relationships override rolled-up ones.
471
518
  */
472
- export function rollUpContextRelationships(parsed: ParsedC4): ContextRelationship[] {
519
+ export function rollUpContextRelationships(
520
+ parsed: ParsedC4
521
+ ): ContextRelationship[] {
473
522
  const ownerMap = buildOwnershipMap(parsed.elements);
474
523
  const allRels = collectAllRelationships(parsed.elements, ownerMap);
475
524
 
@@ -581,7 +630,12 @@ function wrapText(text: string, maxWidth: number, charWidth: number): string[] {
581
630
  }
582
631
 
583
632
  /** Keys to exclude from the below-divider metadata display. */
584
- const META_EXCLUDE_KEYS = new Set(['description', 'tech', 'technology', 'is a']);
633
+ const META_EXCLUDE_KEYS = new Set([
634
+ 'description',
635
+ 'tech',
636
+ 'technology',
637
+ 'is a',
638
+ ]);
585
639
 
586
640
  /** Collect displayable metadata entries for a container card. */
587
641
  export function collectCardMetadata(
@@ -629,7 +683,9 @@ export function computeC4NodeDimensions(
629
683
  // Widen card if metadata rows need more space
630
684
  const maxMetaWidth = Math.max(
631
685
  ...metaEntries.map(
632
- (e) => (e.key.length + 2 + e.value.length) * META_CHAR_WIDTH + CARD_H_PAD * 2
686
+ (e) =>
687
+ (e.key.length + 2 + e.value.length) * META_CHAR_WIDTH +
688
+ CARD_H_PAD * 2
633
689
  )
634
690
  );
635
691
  if (maxMetaWidth > width) width = Math.min(MAX_NODE_WIDTH, maxMetaWidth);
@@ -669,7 +725,9 @@ function computeLegendGroups(tagGroups: TagGroup[]): C4LegendGroup[] {
669
725
  if (entries.length === 0) continue;
670
726
 
671
727
  // Compute pill width: group name + entries
672
- const nameW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD * 2;
728
+ const nameW =
729
+ measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
730
+ LEGEND_PILL_PAD * 2;
673
731
  let capsuleW = LEGEND_CAPSULE_PAD;
674
732
  for (const e of entries) {
675
733
  capsuleW +=
@@ -738,7 +796,14 @@ export function layoutC4Context(
738
796
  );
739
797
 
740
798
  if (contextElements.length === 0) {
741
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
799
+ return {
800
+ nodes: [],
801
+ edges: [],
802
+ legend: [],
803
+ groupBoundaries: [],
804
+ width: 0,
805
+ height: 0,
806
+ };
742
807
  }
743
808
 
744
809
  // Roll up relationships
@@ -768,7 +833,10 @@ export function layoutC4Context(
768
833
  // Add edges — only between known nodes
769
834
  const validRels: ContextRelationship[] = [];
770
835
  for (const rel of contextRels) {
771
- if (nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)) {
836
+ if (
837
+ nameToElement.has(rel.sourceName) &&
838
+ nameToElement.has(rel.targetName)
839
+ ) {
772
840
  validRels.push(rel);
773
841
  g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
774
842
  }
@@ -786,7 +854,11 @@ export function layoutC4Context(
786
854
  // Extract positioned nodes
787
855
  const nodes: C4LayoutNode[] = contextElements.map((el) => {
788
856
  const pos = g.node(el.name);
789
- const color = resolveNodeColor(el, parsed.tagGroups, activeTagGroup ?? null);
857
+ const color = resolveNodeColor(
858
+ el,
859
+ parsed.tagGroups,
860
+ activeTagGroup ?? null
861
+ );
790
862
  const hasContainers =
791
863
  el.children.some((c) => c.type === 'container') ||
792
864
  el.groups.some((g) => g.children.some((c) => c.type === 'container'));
@@ -822,7 +894,10 @@ export function layoutC4Context(
822
894
  });
823
895
 
824
896
  // Compute bounding box of all content (nodes + edge points)
825
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
897
+ let minX = Infinity,
898
+ minY = Infinity,
899
+ maxX = -Infinity,
900
+ maxY = -Infinity;
826
901
  for (const node of nodes) {
827
902
  const left = node.x - node.width / 2;
828
903
  const top = node.y - node.height / 2;
@@ -880,7 +955,14 @@ export function layoutC4Context(
880
955
  if (legendBottom > totalHeight) totalHeight = legendBottom;
881
956
  }
882
957
 
883
- return { nodes, edges, legend: legendGroups, groupBoundaries: [], width: totalWidth, height: totalHeight };
958
+ return {
959
+ nodes,
960
+ edges,
961
+ legend: legendGroups,
962
+ groupBoundaries: [],
963
+ width: totalWidth,
964
+ height: totalHeight,
965
+ };
884
966
  }
885
967
 
886
968
  // ============================================================
@@ -901,7 +983,14 @@ export function layoutC4Containers(
901
983
  (el) => el.name.toLowerCase() === systemName.toLowerCase()
902
984
  );
903
985
  if (!system) {
904
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
986
+ return {
987
+ nodes: [],
988
+ edges: [],
989
+ legend: [],
990
+ groupBoundaries: [],
991
+ width: 0,
992
+ height: 0,
993
+ };
905
994
  }
906
995
 
907
996
  // Collect all containers: direct children + group children
@@ -916,7 +1005,14 @@ export function layoutC4Containers(
916
1005
  }
917
1006
 
918
1007
  if (containers.length === 0) {
919
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
1008
+ return {
1009
+ nodes: [],
1010
+ edges: [],
1011
+ legend: [],
1012
+ groupBoundaries: [],
1013
+ width: 0,
1014
+ height: 0,
1015
+ };
920
1016
  }
921
1017
 
922
1018
  const containerNames = new Set(containers.map((c) => c.name.toLowerCase()));
@@ -962,11 +1058,17 @@ export function layoutC4Containers(
962
1058
  }
963
1059
 
964
1060
  // Build element-to-group mapping for compound graph
965
- const elementToGroup = new Map<string, { name: string; lineNumber: number }>();
1061
+ const elementToGroup = new Map<
1062
+ string,
1063
+ { name: string; lineNumber: number }
1064
+ >();
966
1065
  for (const group of system.groups) {
967
1066
  for (const child of group.children) {
968
1067
  if (child.type === 'container') {
969
- elementToGroup.set(child.name, { name: group.name, lineNumber: group.lineNumber });
1068
+ elementToGroup.set(child.name, {
1069
+ name: group.name,
1070
+ lineNumber: group.lineNumber,
1071
+ });
970
1072
  }
971
1073
  }
972
1074
  }
@@ -1075,7 +1177,10 @@ export function layoutC4Containers(
1075
1177
 
1076
1178
  // Add edges to dagre
1077
1179
  for (const rel of containerRels) {
1078
- if (nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)) {
1180
+ if (
1181
+ nameToElement.has(rel.sourceName) &&
1182
+ nameToElement.has(rel.targetName)
1183
+ ) {
1079
1184
  g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
1080
1185
  }
1081
1186
  }
@@ -1090,7 +1195,10 @@ export function layoutC4Containers(
1090
1195
  reduceCrossings(
1091
1196
  g,
1092
1197
  containerRels
1093
- .filter((r) => nameToElement.has(r.sourceName) && nameToElement.has(r.targetName))
1198
+ .filter(
1199
+ (r) =>
1200
+ nameToElement.has(r.sourceName) && nameToElement.has(r.targetName)
1201
+ )
1094
1202
  .map((r) => ({ source: r.sourceName, target: r.targetName })),
1095
1203
  nodeGroupMap
1096
1204
  );
@@ -1099,7 +1207,11 @@ export function layoutC4Containers(
1099
1207
  const nodes: C4LayoutNode[] = [];
1100
1208
  for (const el of containers) {
1101
1209
  const pos = g.node(el.name);
1102
- const color = resolveNodeColor(el, parsed.tagGroups, activeTagGroup ?? null);
1210
+ const color = resolveNodeColor(
1211
+ el,
1212
+ parsed.tagGroups,
1213
+ activeTagGroup ?? null
1214
+ );
1103
1215
  const tech = el.metadata['tech'] ?? el.metadata['technology'];
1104
1216
  const hasComponents =
1105
1217
  el.children.some((c) => c.type === 'component') ||
@@ -1125,7 +1237,11 @@ export function layoutC4Containers(
1125
1237
 
1126
1238
  for (const el of externals) {
1127
1239
  const pos = g.node(el.name);
1128
- const color = resolveNodeColor(el, parsed.tagGroups, activeTagGroup ?? null);
1240
+ const color = resolveNodeColor(
1241
+ el,
1242
+ parsed.tagGroups,
1243
+ activeTagGroup ?? null
1244
+ );
1129
1245
  nodes.push({
1130
1246
  id: el.name,
1131
1247
  name: el.name,
@@ -1143,7 +1259,10 @@ export function layoutC4Containers(
1143
1259
 
1144
1260
  // Extract edges
1145
1261
  const edges: C4LayoutEdge[] = containerRels
1146
- .filter((rel) => nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName))
1262
+ .filter(
1263
+ (rel) =>
1264
+ nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)
1265
+ )
1147
1266
  .map((rel) => {
1148
1267
  const edgeData = g.edge(rel.sourceName, rel.targetName);
1149
1268
  return {
@@ -1159,7 +1278,10 @@ export function layoutC4Containers(
1159
1278
 
1160
1279
  // Compute boundary box from container nodes only
1161
1280
  const containerNodes = nodes.filter((n) => n.type === 'container');
1162
- let bMinX = Infinity, bMinY = Infinity, bMaxX = -Infinity, bMaxY = -Infinity;
1281
+ let bMinX = Infinity,
1282
+ bMinY = Infinity,
1283
+ bMaxX = -Infinity,
1284
+ bMaxY = -Infinity;
1163
1285
  for (const n of containerNodes) {
1164
1286
  const left = n.x - n.width / 2;
1165
1287
  const top = n.y - n.height / 2;
@@ -1177,15 +1299,18 @@ export function layoutC4Containers(
1177
1299
  lineNumber: system.lineNumber,
1178
1300
  x: bMinX - BOUNDARY_PAD,
1179
1301
  y: bMinY - BOUNDARY_PAD,
1180
- width: (bMaxX - bMinX) + BOUNDARY_PAD * 2,
1181
- height: (bMaxY - bMinY) + BOUNDARY_PAD * 2,
1302
+ width: bMaxX - bMinX + BOUNDARY_PAD * 2,
1303
+ height: bMaxY - bMinY + BOUNDARY_PAD * 2,
1182
1304
  };
1183
1305
 
1184
1306
  // Compute group boundaries from member node positions
1185
1307
  const groupBoundaries: C4LayoutBoundary[] = [];
1186
1308
  if (hasGroups) {
1187
1309
  const nodeMap = new Map(containerNodes.map((n) => [n.name, n]));
1188
- const seenGroups = new Map<string, { lineNumber: number; members: C4LayoutNode[] }>();
1310
+ const seenGroups = new Map<
1311
+ string,
1312
+ { lineNumber: number; members: C4LayoutNode[] }
1313
+ >();
1189
1314
  for (const [elName, grp] of elementToGroup) {
1190
1315
  const node = nodeMap.get(elName);
1191
1316
  if (!node) continue;
@@ -1196,7 +1321,10 @@ export function layoutC4Containers(
1196
1321
  }
1197
1322
  for (const [groupName, { lineNumber, members }] of seenGroups) {
1198
1323
  if (members.length === 0) continue;
1199
- let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
1324
+ let gMinX = Infinity,
1325
+ gMinY = Infinity,
1326
+ gMaxX = -Infinity,
1327
+ gMaxY = -Infinity;
1200
1328
  for (const m of members) {
1201
1329
  const left = m.x - m.width / 2;
1202
1330
  const top = m.y - m.height / 2;
@@ -1213,14 +1341,17 @@ export function layoutC4Containers(
1213
1341
  lineNumber,
1214
1342
  x: gMinX - GROUP_BOUNDARY_PAD,
1215
1343
  y: gMinY - GROUP_BOUNDARY_PAD,
1216
- width: (gMaxX - gMinX) + GROUP_BOUNDARY_PAD * 2,
1217
- height: (gMaxY - gMinY) + GROUP_BOUNDARY_PAD * 2,
1344
+ width: gMaxX - gMinX + GROUP_BOUNDARY_PAD * 2,
1345
+ height: gMaxY - gMinY + GROUP_BOUNDARY_PAD * 2,
1218
1346
  });
1219
1347
  }
1220
1348
  }
1221
1349
 
1222
1350
  // Compute bounding box of all content (nodes + boundary + group boundaries + edge points)
1223
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1351
+ let minX = Infinity,
1352
+ minY = Infinity,
1353
+ maxX = -Infinity,
1354
+ maxY = -Infinity;
1224
1355
  for (const node of nodes) {
1225
1356
  const left = node.x - node.width / 2;
1226
1357
  const top = node.y - node.height / 2;
@@ -1290,7 +1421,15 @@ export function layoutC4Containers(
1290
1421
  if (legendBottom > totalHeight) totalHeight = legendBottom;
1291
1422
  }
1292
1423
 
1293
- return { nodes, edges, legend: legendGroups, boundary, groupBoundaries, width: totalWidth, height: totalHeight };
1424
+ return {
1425
+ nodes,
1426
+ edges,
1427
+ legend: legendGroups,
1428
+ boundary,
1429
+ groupBoundaries,
1430
+ width: totalWidth,
1431
+ height: totalHeight,
1432
+ };
1294
1433
  }
1295
1434
 
1296
1435
  // ============================================================
@@ -1312,13 +1451,23 @@ export function layoutC4Components(
1312
1451
  (el) => el.name.toLowerCase() === systemName.toLowerCase()
1313
1452
  );
1314
1453
  if (!system) {
1315
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
1454
+ return {
1455
+ nodes: [],
1456
+ edges: [],
1457
+ legend: [],
1458
+ groupBoundaries: [],
1459
+ width: 0,
1460
+ height: 0,
1461
+ };
1316
1462
  }
1317
1463
 
1318
1464
  // Find the container within the system (direct children + group children)
1319
1465
  let targetContainer: C4Element | undefined;
1320
1466
  for (const child of system.children) {
1321
- if (child.type === 'container' && child.name.toLowerCase() === containerName.toLowerCase()) {
1467
+ if (
1468
+ child.type === 'container' &&
1469
+ child.name.toLowerCase() === containerName.toLowerCase()
1470
+ ) {
1322
1471
  targetContainer = child;
1323
1472
  break;
1324
1473
  }
@@ -1326,7 +1475,10 @@ export function layoutC4Components(
1326
1475
  if (!targetContainer) {
1327
1476
  for (const group of system.groups) {
1328
1477
  for (const child of group.children) {
1329
- if (child.type === 'container' && child.name.toLowerCase() === containerName.toLowerCase()) {
1478
+ if (
1479
+ child.type === 'container' &&
1480
+ child.name.toLowerCase() === containerName.toLowerCase()
1481
+ ) {
1330
1482
  targetContainer = child;
1331
1483
  break;
1332
1484
  }
@@ -1335,7 +1487,14 @@ export function layoutC4Components(
1335
1487
  }
1336
1488
  }
1337
1489
  if (!targetContainer) {
1338
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
1490
+ return {
1491
+ nodes: [],
1492
+ edges: [],
1493
+ legend: [],
1494
+ groupBoundaries: [],
1495
+ width: 0,
1496
+ height: 0,
1497
+ };
1339
1498
  }
1340
1499
 
1341
1500
  // Collect all components: direct children + group children
@@ -1350,7 +1509,14 @@ export function layoutC4Components(
1350
1509
  }
1351
1510
 
1352
1511
  if (components.length === 0) {
1353
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
1512
+ return {
1513
+ nodes: [],
1514
+ edges: [],
1515
+ legend: [],
1516
+ groupBoundaries: [],
1517
+ width: 0,
1518
+ height: 0,
1519
+ };
1354
1520
  }
1355
1521
 
1356
1522
  const componentNames = new Set(components.map((c) => c.name.toLowerCase()));
@@ -1400,7 +1566,8 @@ export function layoutC4Components(
1400
1566
  // otherwise roll up to system ancestor
1401
1567
  const sourceAncestor = ownerMap.get(sourceName) ?? sourceName;
1402
1568
  // If source is inside the same container, skip
1403
- if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase()) continue;
1569
+ if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase())
1570
+ continue;
1404
1571
  if (sourceAncestor.toLowerCase() === system.name.toLowerCase()) {
1405
1572
  // Source is in same system — try to resolve to container level
1406
1573
  const sourceLower = sourceName.toLowerCase();
@@ -1423,11 +1590,17 @@ export function layoutC4Components(
1423
1590
  }
1424
1591
 
1425
1592
  // Build element-to-group mapping for compound graph
1426
- const elementToGroup = new Map<string, { name: string; lineNumber: number }>();
1593
+ const elementToGroup = new Map<
1594
+ string,
1595
+ { name: string; lineNumber: number }
1596
+ >();
1427
1597
  for (const group of targetContainer.groups) {
1428
1598
  for (const child of group.children) {
1429
1599
  if (child.type === 'component') {
1430
- elementToGroup.set(child.name, { name: group.name, lineNumber: group.lineNumber });
1600
+ elementToGroup.set(child.name, {
1601
+ name: group.name,
1602
+ lineNumber: group.lineNumber,
1603
+ });
1431
1604
  }
1432
1605
  }
1433
1606
  }
@@ -1509,7 +1682,8 @@ export function layoutC4Components(
1509
1682
  if (!componentNames.has(rel.target.toLowerCase())) continue;
1510
1683
 
1511
1684
  const sourceAncestor = ownerMap.get(sourceName) ?? sourceName;
1512
- if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase()) continue;
1685
+ if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase())
1686
+ continue;
1513
1687
 
1514
1688
  // Resolve source to the external element we added
1515
1689
  let resolvedSource: string | undefined;
@@ -1545,7 +1719,10 @@ export function layoutC4Components(
1545
1719
 
1546
1720
  // Add edges to dagre
1547
1721
  for (const rel of componentRels) {
1548
- if (nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)) {
1722
+ if (
1723
+ nameToElement.has(rel.sourceName) &&
1724
+ nameToElement.has(rel.targetName)
1725
+ ) {
1549
1726
  g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
1550
1727
  }
1551
1728
  }
@@ -1560,7 +1737,10 @@ export function layoutC4Components(
1560
1737
  reduceCrossings(
1561
1738
  g,
1562
1739
  componentRels
1563
- .filter((r) => nameToElement.has(r.sourceName) && nameToElement.has(r.targetName))
1740
+ .filter(
1741
+ (r) =>
1742
+ nameToElement.has(r.sourceName) && nameToElement.has(r.targetName)
1743
+ )
1564
1744
  .map((r) => ({ source: r.sourceName, target: r.targetName })),
1565
1745
  nodeGroupMap
1566
1746
  );
@@ -1572,7 +1752,12 @@ export function layoutC4Components(
1572
1752
  const nodes: C4LayoutNode[] = [];
1573
1753
  for (const el of components) {
1574
1754
  const pos = g.node(el.name);
1575
- const color = resolveNodeColor(el, parsed.tagGroups, activeTagGroup ?? null, ancestors);
1755
+ const color = resolveNodeColor(
1756
+ el,
1757
+ parsed.tagGroups,
1758
+ activeTagGroup ?? null,
1759
+ ancestors
1760
+ );
1576
1761
  const tech = el.metadata['tech'] ?? el.metadata['technology'];
1577
1762
  const hasComponents =
1578
1763
  el.children.some((c) => c.type === 'component') ||
@@ -1598,7 +1783,11 @@ export function layoutC4Components(
1598
1783
 
1599
1784
  for (const el of externals) {
1600
1785
  const pos = g.node(el.name);
1601
- const color = resolveNodeColor(el, parsed.tagGroups, activeTagGroup ?? null);
1786
+ const color = resolveNodeColor(
1787
+ el,
1788
+ parsed.tagGroups,
1789
+ activeTagGroup ?? null
1790
+ );
1602
1791
  nodes.push({
1603
1792
  id: el.name,
1604
1793
  name: el.name,
@@ -1616,7 +1805,10 @@ export function layoutC4Components(
1616
1805
 
1617
1806
  // Extract edges
1618
1807
  const edges: C4LayoutEdge[] = componentRels
1619
- .filter((rel) => nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName))
1808
+ .filter(
1809
+ (rel) =>
1810
+ nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)
1811
+ )
1620
1812
  .map((rel) => {
1621
1813
  const edgeData = g.edge(rel.sourceName, rel.targetName);
1622
1814
  return {
@@ -1632,7 +1824,10 @@ export function layoutC4Components(
1632
1824
 
1633
1825
  // Compute boundary box from component nodes only
1634
1826
  const componentNodes = nodes.filter((n) => n.type === 'component');
1635
- let bMinX = Infinity, bMinY = Infinity, bMaxX = -Infinity, bMaxY = -Infinity;
1827
+ let bMinX = Infinity,
1828
+ bMinY = Infinity,
1829
+ bMaxX = -Infinity,
1830
+ bMaxY = -Infinity;
1636
1831
  for (const n of componentNodes) {
1637
1832
  const left = n.x - n.width / 2;
1638
1833
  const top = n.y - n.height / 2;
@@ -1650,15 +1845,18 @@ export function layoutC4Components(
1650
1845
  lineNumber: targetContainer.lineNumber,
1651
1846
  x: bMinX - BOUNDARY_PAD,
1652
1847
  y: bMinY - BOUNDARY_PAD,
1653
- width: (bMaxX - bMinX) + BOUNDARY_PAD * 2,
1654
- height: (bMaxY - bMinY) + BOUNDARY_PAD * 2,
1848
+ width: bMaxX - bMinX + BOUNDARY_PAD * 2,
1849
+ height: bMaxY - bMinY + BOUNDARY_PAD * 2,
1655
1850
  };
1656
1851
 
1657
1852
  // Compute group boundaries from member node positions
1658
1853
  const groupBoundaries: C4LayoutBoundary[] = [];
1659
1854
  if (hasGroups) {
1660
1855
  const nodeMap = new Map(componentNodes.map((n) => [n.name, n]));
1661
- const seenGroups = new Map<string, { lineNumber: number; members: C4LayoutNode[] }>();
1856
+ const seenGroups = new Map<
1857
+ string,
1858
+ { lineNumber: number; members: C4LayoutNode[] }
1859
+ >();
1662
1860
  for (const [elName, grp] of elementToGroup) {
1663
1861
  const node = nodeMap.get(elName);
1664
1862
  if (!node) continue;
@@ -1669,7 +1867,10 @@ export function layoutC4Components(
1669
1867
  }
1670
1868
  for (const [groupName, { lineNumber, members }] of seenGroups) {
1671
1869
  if (members.length === 0) continue;
1672
- let gMinX = Infinity, gMinY = Infinity, gMaxX = -Infinity, gMaxY = -Infinity;
1870
+ let gMinX = Infinity,
1871
+ gMinY = Infinity,
1872
+ gMaxX = -Infinity,
1873
+ gMaxY = -Infinity;
1673
1874
  for (const m of members) {
1674
1875
  const left = m.x - m.width / 2;
1675
1876
  const top = m.y - m.height / 2;
@@ -1686,14 +1887,17 @@ export function layoutC4Components(
1686
1887
  lineNumber,
1687
1888
  x: gMinX - GROUP_BOUNDARY_PAD,
1688
1889
  y: gMinY - GROUP_BOUNDARY_PAD,
1689
- width: (gMaxX - gMinX) + GROUP_BOUNDARY_PAD * 2,
1690
- height: (gMaxY - gMinY) + GROUP_BOUNDARY_PAD * 2,
1890
+ width: gMaxX - gMinX + GROUP_BOUNDARY_PAD * 2,
1891
+ height: gMaxY - gMinY + GROUP_BOUNDARY_PAD * 2,
1691
1892
  });
1692
1893
  }
1693
1894
  }
1694
1895
 
1695
1896
  // Compute bounding box of all content (nodes + boundary + group boundaries + edge points)
1696
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1897
+ let minX = Infinity,
1898
+ minY = Infinity,
1899
+ maxX = -Infinity,
1900
+ maxY = -Infinity;
1697
1901
  for (const node of nodes) {
1698
1902
  const left = node.x - node.width / 2;
1699
1903
  const top = node.y - node.height / 2;
@@ -1746,7 +1950,6 @@ export function layoutC4Components(
1746
1950
  let totalWidth = maxX - minX + MARGIN * 2;
1747
1951
  let totalHeight = maxY - minY + MARGIN * 2;
1748
1952
 
1749
-
1750
1953
  const legendGroups = computeLegendGroups(parsed.tagGroups);
1751
1954
 
1752
1955
  // Position legend below diagram
@@ -1764,7 +1967,15 @@ export function layoutC4Components(
1764
1967
  if (legendBottom > totalHeight) totalHeight = legendBottom;
1765
1968
  }
1766
1969
 
1767
- return { nodes, edges, legend: legendGroups, boundary, groupBoundaries, width: totalWidth, height: totalHeight };
1970
+ return {
1971
+ nodes,
1972
+ edges,
1973
+ legend: legendGroups,
1974
+ boundary,
1975
+ groupBoundaries,
1976
+ width: totalWidth,
1977
+ height: totalHeight,
1978
+ };
1768
1979
  }
1769
1980
 
1770
1981
  // ============================================================
@@ -1775,15 +1986,20 @@ export function layoutC4Components(
1775
1986
  * Resolve a container reference name to its C4Element by walking the parsed
1776
1987
  * element tree. Matches container names case-insensitively.
1777
1988
  */
1778
- function resolveContainerRef(parsed: ParsedC4, refName: string): C4Element | undefined {
1989
+ function resolveContainerRef(
1990
+ parsed: ParsedC4,
1991
+ refName: string
1992
+ ): C4Element | undefined {
1779
1993
  const lower = refName.toLowerCase();
1780
1994
  for (const el of parsed.elements) {
1781
1995
  for (const child of el.children) {
1782
- if (child.type === 'container' && child.name.toLowerCase() === lower) return child;
1996
+ if (child.type === 'container' && child.name.toLowerCase() === lower)
1997
+ return child;
1783
1998
  }
1784
1999
  for (const group of el.groups) {
1785
2000
  for (const child of group.children) {
1786
- if (child.type === 'container' && child.name.toLowerCase() === lower) return child;
2001
+ if (child.type === 'container' && child.name.toLowerCase() === lower)
2002
+ return child;
1787
2003
  }
1788
2004
  }
1789
2005
  }
@@ -1808,7 +2024,7 @@ function collectDeploymentRefs(
1808
2024
  parentId: string | null,
1809
2025
  refs: DeploymentRefEntry[],
1810
2026
  infraIds: Map<string, C4DeploymentNode>,
1811
- infraParents: Map<string, string | null>,
2027
+ infraParents: Map<string, string | null>
1812
2028
  ): void {
1813
2029
  for (const node of nodes) {
1814
2030
  const infraId = `__infra_${node.name}`;
@@ -1818,11 +2034,23 @@ function collectDeploymentRefs(
1818
2034
  for (const ref of node.containerRefs) {
1819
2035
  const el = resolveContainerRef(parsed, ref);
1820
2036
  if (el) {
1821
- refs.push({ refName: ref, element: el, infraId, deployLineNumber: node.lineNumber });
2037
+ refs.push({
2038
+ refName: ref,
2039
+ element: el,
2040
+ infraId,
2041
+ deployLineNumber: node.lineNumber,
2042
+ });
1822
2043
  }
1823
2044
  }
1824
2045
 
1825
- collectDeploymentRefs(node.children, parsed, infraId, refs, infraIds, infraParents);
2046
+ collectDeploymentRefs(
2047
+ node.children,
2048
+ parsed,
2049
+ infraId,
2050
+ refs,
2051
+ infraIds,
2052
+ infraParents
2053
+ );
1826
2054
  }
1827
2055
  }
1828
2056
 
@@ -1835,20 +2063,41 @@ function collectDeploymentRefs(
1835
2063
  */
1836
2064
  export function layoutC4Deployment(
1837
2065
  parsed: ParsedC4,
1838
- activeTagGroup?: string | null,
2066
+ activeTagGroup?: string | null
1839
2067
  ): C4LayoutResult {
1840
2068
  if (parsed.deployment.length === 0) {
1841
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
2069
+ return {
2070
+ nodes: [],
2071
+ edges: [],
2072
+ legend: [],
2073
+ groupBoundaries: [],
2074
+ width: 0,
2075
+ height: 0,
2076
+ };
1842
2077
  }
1843
2078
 
1844
2079
  // Collect all refs and infra node info
1845
2080
  const refs: DeploymentRefEntry[] = [];
1846
2081
  const infraIds = new Map<string, C4DeploymentNode>();
1847
2082
  const infraParents = new Map<string, string | null>();
1848
- collectDeploymentRefs(parsed.deployment, parsed, null, refs, infraIds, infraParents);
2083
+ collectDeploymentRefs(
2084
+ parsed.deployment,
2085
+ parsed,
2086
+ null,
2087
+ refs,
2088
+ infraIds,
2089
+ infraParents
2090
+ );
1849
2091
 
1850
2092
  if (refs.length === 0) {
1851
- return { nodes: [], edges: [], legend: [], groupBoundaries: [], width: 0, height: 0 };
2093
+ return {
2094
+ nodes: [],
2095
+ edges: [],
2096
+ legend: [],
2097
+ groupBoundaries: [],
2098
+ width: 0,
2099
+ height: 0,
2100
+ };
1852
2101
  }
1853
2102
 
1854
2103
  // Deduplicate refs by element name (a container can appear in multiple infra
@@ -1927,7 +2176,10 @@ export function layoutC4Deployment(
1927
2176
 
1928
2177
  // Add edges to dagre
1929
2178
  for (const rel of deployRels) {
1930
- if (nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)) {
2179
+ if (
2180
+ nameToElement.has(rel.sourceName) &&
2181
+ nameToElement.has(rel.targetName)
2182
+ ) {
1931
2183
  g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
1932
2184
  }
1933
2185
  }
@@ -1941,16 +2193,23 @@ export function layoutC4Deployment(
1941
2193
  reduceCrossings(
1942
2194
  g,
1943
2195
  deployRels
1944
- .filter((r) => nameToElement.has(r.sourceName) && nameToElement.has(r.targetName))
2196
+ .filter(
2197
+ (r) =>
2198
+ nameToElement.has(r.sourceName) && nameToElement.has(r.targetName)
2199
+ )
1945
2200
  .map((r) => ({ source: r.sourceName, target: r.targetName })),
1946
- nodeInfraMap,
2201
+ nodeInfraMap
1947
2202
  );
1948
2203
 
1949
2204
  // Extract positioned nodes
1950
2205
  const nodes: C4LayoutNode[] = [];
1951
2206
  for (const r of refEntries) {
1952
2207
  const pos = g.node(r.element.name);
1953
- const color = resolveNodeColor(r.element, parsed.tagGroups, activeTagGroup ?? null);
2208
+ const color = resolveNodeColor(
2209
+ r.element,
2210
+ parsed.tagGroups,
2211
+ activeTagGroup ?? null
2212
+ );
1954
2213
  const tech = r.element.metadata['tech'] ?? r.element.metadata['technology'];
1955
2214
  nodes.push({
1956
2215
  id: r.element.name,
@@ -1971,7 +2230,10 @@ export function layoutC4Deployment(
1971
2230
 
1972
2231
  // Extract edges
1973
2232
  const edges: C4LayoutEdge[] = deployRels
1974
- .filter((rel) => nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName))
2233
+ .filter(
2234
+ (rel) =>
2235
+ nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)
2236
+ )
1975
2237
  .map((rel) => {
1976
2238
  const edgeData = g.edge(rel.sourceName, rel.targetName);
1977
2239
  return {
@@ -1999,12 +2261,20 @@ export function layoutC4Deployment(
1999
2261
  }
2000
2262
 
2001
2263
  // Compute boundaries bottom-up: leaf infra nodes first, then parents.
2002
- const infraBounds = new Map<string, { x: number; y: number; width: number; height: number }>();
2003
-
2004
- function computeInfraBounds(infraId: string): { x: number; y: number; width: number; height: number } | null {
2264
+ const infraBounds = new Map<
2265
+ string,
2266
+ { x: number; y: number; width: number; height: number }
2267
+ >();
2268
+
2269
+ function computeInfraBounds(
2270
+ infraId: string
2271
+ ): { x: number; y: number; width: number; height: number } | null {
2005
2272
  if (infraBounds.has(infraId)) return infraBounds.get(infraId)!;
2006
2273
 
2007
- let bMinX = Infinity, bMinY = Infinity, bMaxX = -Infinity, bMaxY = -Infinity;
2274
+ let bMinX = Infinity,
2275
+ bMinY = Infinity,
2276
+ bMaxX = -Infinity,
2277
+ bMaxY = -Infinity;
2008
2278
  let hasContent = false;
2009
2279
 
2010
2280
  // Direct container ref members
@@ -2029,8 +2299,10 @@ export function layoutC4Deployment(
2029
2299
  hasContent = true;
2030
2300
  if (childBounds.x < bMinX) bMinX = childBounds.x;
2031
2301
  if (childBounds.y < bMinY) bMinY = childBounds.y;
2032
- if (childBounds.x + childBounds.width > bMaxX) bMaxX = childBounds.x + childBounds.width;
2033
- if (childBounds.y + childBounds.height > bMaxY) bMaxY = childBounds.y + childBounds.height;
2302
+ if (childBounds.x + childBounds.width > bMaxX)
2303
+ bMaxX = childBounds.x + childBounds.width;
2304
+ if (childBounds.y + childBounds.height > bMaxY)
2305
+ bMaxY = childBounds.y + childBounds.height;
2034
2306
  }
2035
2307
  }
2036
2308
  }
@@ -2040,8 +2312,8 @@ export function layoutC4Deployment(
2040
2312
  const bounds = {
2041
2313
  x: bMinX - BOUNDARY_PAD,
2042
2314
  y: bMinY - BOUNDARY_PAD,
2043
- width: (bMaxX - bMinX) + BOUNDARY_PAD * 2,
2044
- height: (bMaxY - bMinY) + BOUNDARY_PAD * 2,
2315
+ width: bMaxX - bMinX + BOUNDARY_PAD * 2,
2316
+ height: bMaxY - bMinY + BOUNDARY_PAD * 2,
2045
2317
  };
2046
2318
  infraBounds.set(infraId, bounds);
2047
2319
  return bounds;
@@ -2062,10 +2334,13 @@ export function layoutC4Deployment(
2062
2334
  }
2063
2335
 
2064
2336
  // Sort boundaries so outermost (largest area) are first — rendered bottom to top
2065
- groupBoundaries.sort((a, b) => (b.width * b.height) - (a.width * a.height));
2337
+ groupBoundaries.sort((a, b) => b.width * b.height - a.width * a.height);
2066
2338
 
2067
2339
  // Compute total bounding box
2068
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2340
+ let minX = Infinity,
2341
+ minY = Infinity,
2342
+ maxX = -Infinity,
2343
+ maxY = -Infinity;
2069
2344
  for (const node of nodes) {
2070
2345
  const left = node.x - node.width / 2;
2071
2346
  const top = node.y - node.height / 2;
@@ -2128,5 +2403,12 @@ export function layoutC4Deployment(
2128
2403
  if (legendBottom > totalHeight) totalHeight = legendBottom;
2129
2404
  }
2130
2405
 
2131
- return { nodes, edges, legend: legendGroups, groupBoundaries, width: totalWidth, height: totalHeight };
2406
+ return {
2407
+ nodes,
2408
+ edges,
2409
+ legend: legendGroups,
2410
+ groupBoundaries,
2411
+ width: totalWidth,
2412
+ height: totalHeight,
2413
+ };
2132
2414
  }