@diagrammo/dgmo 0.4.2 → 0.4.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 (60) hide show
  1. package/.claude/skills/dgmo-chart/SKILL.md +28 -0
  2. package/.claude/skills/dgmo-generate/SKILL.md +1 -0
  3. package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
  4. package/.cursorrules +27 -2
  5. package/.github/copilot-instructions.md +36 -3
  6. package/.windsurfrules +27 -2
  7. package/README.md +12 -3
  8. package/dist/cli.cjs +197 -154
  9. package/dist/index.cjs +8647 -3447
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +503 -58
  12. package/dist/index.d.ts +503 -58
  13. package/dist/index.js +8379 -3200
  14. package/dist/index.js.map +1 -1
  15. package/docs/ai-integration.md +1 -1
  16. package/docs/language-reference.md +336 -17
  17. package/docs/migration-sequence-color-to-tags.md +98 -0
  18. package/package.json +1 -1
  19. package/src/c4/renderer.ts +1 -20
  20. package/src/class/renderer.ts +1 -11
  21. package/src/cli.ts +40 -0
  22. package/src/d3.ts +92 -2
  23. package/src/dgmo-router.ts +11 -0
  24. package/src/echarts.ts +74 -8
  25. package/src/er/parser.ts +29 -3
  26. package/src/er/renderer.ts +1 -15
  27. package/src/graph/flowchart-parser.ts +7 -30
  28. package/src/graph/flowchart-renderer.ts +62 -69
  29. package/src/graph/layout.ts +5 -0
  30. package/src/graph/state-parser.ts +388 -0
  31. package/src/graph/state-renderer.ts +496 -0
  32. package/src/graph/types.ts +4 -2
  33. package/src/index.ts +42 -1
  34. package/src/infra/compute.ts +1113 -0
  35. package/src/infra/layout.ts +578 -0
  36. package/src/infra/parser.ts +559 -0
  37. package/src/infra/renderer.ts +1553 -0
  38. package/src/infra/roles.ts +60 -0
  39. package/src/infra/serialize.ts +67 -0
  40. package/src/infra/types.ts +221 -0
  41. package/src/infra/validation.ts +192 -0
  42. package/src/initiative-status/layout.ts +56 -61
  43. package/src/initiative-status/renderer.ts +13 -13
  44. package/src/kanban/renderer.ts +1 -24
  45. package/src/org/layout.ts +28 -37
  46. package/src/org/parser.ts +16 -1
  47. package/src/org/renderer.ts +159 -121
  48. package/src/org/resolver.ts +90 -23
  49. package/src/palettes/color-utils.ts +30 -0
  50. package/src/render.ts +2 -0
  51. package/src/sequence/parser.ts +202 -42
  52. package/src/sequence/renderer.ts +576 -113
  53. package/src/sequence/tag-resolution.ts +163 -0
  54. package/src/sharing.ts +8 -0
  55. package/src/sitemap/collapse.ts +187 -0
  56. package/src/sitemap/layout.ts +738 -0
  57. package/src/sitemap/parser.ts +489 -0
  58. package/src/sitemap/renderer.ts +774 -0
  59. package/src/sitemap/types.ts +42 -0
  60. package/src/utils/tag-groups.ts +119 -0
@@ -0,0 +1,578 @@
1
+ // ============================================================
2
+ // Infra Chart Layout Engine
3
+ // ============================================================
4
+ //
5
+ // Uses dagre for LR/TB DAG layout. Groups are implemented as
6
+ // post-layout bounding box wrappers around their children.
7
+
8
+ import dagre from '@dagrejs/dagre';
9
+ import type {
10
+ ComputedInfraModel,
11
+ ComputedInfraNode,
12
+ ComputedInfraEdge,
13
+ InfraGroup,
14
+ } from './types';
15
+
16
+ // ============================================================
17
+ // Layout types
18
+ // ============================================================
19
+
20
+ export interface InfraLayoutNode {
21
+ id: string;
22
+ label: string;
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ computedRps: number;
28
+ overloaded: boolean;
29
+ rateLimited: boolean;
30
+ isEdge: boolean;
31
+ groupId: string | null;
32
+ computedLatencyMs: number;
33
+ computedLatencyPercentiles: ComputedInfraNode['computedLatencyPercentiles'];
34
+ computedUptime: number;
35
+ computedAvailability: number;
36
+ computedAvailabilityPercentiles: ComputedInfraNode['computedAvailabilityPercentiles'];
37
+ computedInstances: number;
38
+ computedConcurrentInvocations: number;
39
+ computedCbState: ComputedInfraNode['computedCbState'];
40
+ childHealthState?: ComputedInfraNode['childHealthState'];
41
+ properties: ComputedInfraNode['properties'];
42
+ queueMetrics?: ComputedInfraNode['queueMetrics'];
43
+ tags: Record<string, string>;
44
+ lineNumber: number;
45
+ }
46
+
47
+ export interface InfraLayoutEdge {
48
+ sourceId: string;
49
+ targetId: string;
50
+ label: string;
51
+ computedRps: number;
52
+ split: number;
53
+ points: { x: number; y: number }[];
54
+ lineNumber: number;
55
+ }
56
+
57
+ export interface InfraLayoutGroup {
58
+ id: string;
59
+ label: string;
60
+ x: number;
61
+ y: number;
62
+ width: number;
63
+ height: number;
64
+ instances?: number | string;
65
+ lineNumber: number;
66
+ }
67
+
68
+ export interface InfraLayoutResult {
69
+ nodes: InfraLayoutNode[];
70
+ edges: InfraLayoutEdge[];
71
+ groups: InfraLayoutGroup[];
72
+ /** Diagram-level options (e.g., default-latency-ms, default-uptime). */
73
+ options: Record<string, string>;
74
+ width: number;
75
+ height: number;
76
+ }
77
+
78
+ // ============================================================
79
+ // Sizing constants
80
+ // ============================================================
81
+
82
+ const MIN_NODE_WIDTH = 140;
83
+ const NODE_HEADER_HEIGHT = 28;
84
+ const META_LINE_HEIGHT = 14;
85
+ const NODE_SEPARATOR_GAP = 4;
86
+ const NODE_PAD_BOTTOM = 10;
87
+ const ROLE_DOT_ROW = 12;
88
+ const COLLAPSE_BAR_HEIGHT = 6;
89
+ const CHAR_WIDTH = 7;
90
+ const META_CHAR_WIDTH = 6;
91
+ const PADDING_X = 24;
92
+ const GROUP_PADDING = 20;
93
+ const GROUP_HEADER_HEIGHT = 24;
94
+ const EDGE_MARGIN = 60;
95
+
96
+ // ============================================================
97
+ // Node sizing
98
+ // ============================================================
99
+
100
+ /** Display property keys shown as key: value rows. */
101
+ const DISPLAY_KEYS = new Set([
102
+ 'cache-hit', 'firewall-block', 'ratelimit-rps',
103
+ 'latency-ms', 'uptime', 'instances', 'max-rps',
104
+ 'cb-error-threshold', 'cb-latency-threshold-ms',
105
+ 'concurrency', 'duration-ms', 'cold-start-ms',
106
+ 'buffer', 'drain-rate', 'retention-hours', 'partitions',
107
+ ]);
108
+
109
+ /** Display names for width estimation. */
110
+ const DISPLAY_NAMES: Record<string, string> = {
111
+ 'cache-hit': 'cache hit', 'firewall-block': 'fw block',
112
+ 'ratelimit-rps': 'rate limit', 'latency-ms': 'latency', 'uptime': 'uptime',
113
+ 'instances': 'instances', 'max-rps': 'capacity',
114
+ 'cb-error-threshold': 'CB error', 'cb-latency-threshold-ms': 'CB latency',
115
+ 'concurrency': 'concurrency', 'duration-ms': 'duration', 'cold-start-ms': 'cold start',
116
+ 'buffer': 'buffer', 'drain-rate': 'drain', 'retention-hours': 'retention', 'partitions': 'partitions',
117
+ };
118
+
119
+ function countDisplayProps(node: ComputedInfraNode, expanded: boolean, options?: Record<string, string>): number {
120
+ // Declared properties are only shown when the node is selected (expanded)
121
+ if (!expanded) return 0;
122
+ let count = node.properties.filter((p) => DISPLAY_KEYS.has(p.key)).length;
123
+ // Count diagram-level default rows for properties the node doesn't explicitly declare
124
+ if (options) {
125
+ const hasLatency = node.properties.some((p) => p.key === 'latency-ms');
126
+ const hasUptime = node.properties.some((p) => p.key === 'uptime');
127
+ const isServerless = node.properties.some((p) => p.key === 'concurrency');
128
+ const defaultLatency = parseFloat(options['default-latency-ms'] ?? '') || 0;
129
+ const defaultUptime = parseFloat(options['default-uptime'] ?? '') || 0;
130
+ if (!hasLatency && !isServerless && defaultLatency > 0) count++;
131
+ if (!hasUptime && defaultUptime > 0 && defaultUptime < 100) count++;
132
+ }
133
+ return count;
134
+ }
135
+
136
+ /** Count computed rows shown below declared props. When expanded, shows p50/p90/p99; otherwise just p99. */
137
+ function countComputedRows(node: ComputedInfraNode, expanded: boolean): number {
138
+ let count = 0;
139
+ // Serverless instances row
140
+ if (node.computedConcurrentInvocations > 0) count += 1;
141
+ const p = node.computedLatencyPercentiles;
142
+ if (p.p50 > 0 || p.p90 > 0 || p.p99 > 0) count += expanded ? 3 : 1; // all percentiles or just p99
143
+ if (node.computedUptime < 1) {
144
+ const declaredUptime = node.properties.find((p) => p.key === 'uptime');
145
+ const declaredVal = declaredUptime ? Number(declaredUptime.value) / 100 : 1;
146
+ const differs = Math.abs(node.computedUptime - declaredVal) > 0.000001;
147
+ if (differs || node.isEdge) count += 1;
148
+ }
149
+ if (node.computedAvailability < 1) count += 1;
150
+ // CB state row when circuit breaker is open
151
+ if (node.computedCbState === 'open') count += 1;
152
+ // Queue computed rows: lag + overflow
153
+ if (node.queueMetrics) {
154
+ if (node.queueMetrics.fillRate > 0) count += 1; // lag row
155
+ if (node.queueMetrics.fillRate > 0 && node.queueMetrics.timeToOverflow < Infinity) count += 1; // overflow row
156
+ }
157
+ return count;
158
+ }
159
+
160
+ function hasRoles(node: ComputedInfraNode): boolean {
161
+ if (node.isEdge) return false;
162
+ return node.properties.some((p) => DISPLAY_KEYS.has(p.key));
163
+ }
164
+
165
+ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?: Record<string, string>): number {
166
+ // Account for badge text (e.g., "3x") in header width — serverless nodes no longer show a badge
167
+ const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1
168
+ ? node.computedInstances : 0;
169
+ const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
170
+ const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH + PADDING_X;
171
+
172
+ // Collect all key names (including "RPS" and computed rows) to compute aligned value column
173
+ const allKeys: string[] = [];
174
+ if (node.computedRps > 0) allKeys.push('RPS');
175
+ // Declared property keys only included when expanded
176
+ if (expanded) {
177
+ for (const p of node.properties) {
178
+ const dk = DISPLAY_NAMES[p.key];
179
+ if (dk) allKeys.push(dk);
180
+ }
181
+ // Default property keys
182
+ if (options) {
183
+ const hasLatency = node.properties.some((p) => p.key === 'latency-ms');
184
+ const hasUptime = node.properties.some((p) => p.key === 'uptime');
185
+ const isServerless = node.properties.some((p) => p.key === 'concurrency');
186
+ if (!hasLatency && !isServerless && (parseFloat(options['default-latency-ms'] ?? '') || 0) > 0) allKeys.push('latency');
187
+ if (!hasUptime && (parseFloat(options['default-uptime'] ?? '') || 0) > 0 && parseFloat(options['default-uptime'] ?? '') < 100) allKeys.push('uptime');
188
+ }
189
+ }
190
+ // Computed rows
191
+ const computedRows = countComputedRows(node, expanded);
192
+ if (computedRows > 0) {
193
+ if (node.computedConcurrentInvocations > 0) allKeys.push('instances');
194
+ const perc = node.computedLatencyPercentiles;
195
+ if (perc.p50 > 0 || perc.p90 > 0 || perc.p99 > 0) {
196
+ if (expanded) {
197
+ allKeys.push('p50', 'p90', 'p99');
198
+ } else {
199
+ allKeys.push('p99');
200
+ }
201
+ }
202
+ if (node.computedUptime < 1) {
203
+ const declaredUptime = node.properties.find((p) => p.key === 'uptime');
204
+ const declaredVal = declaredUptime ? Number(declaredUptime.value) / 100 : 1;
205
+ if (Math.abs(node.computedUptime - declaredVal) > 0.000001 || node.isEdge) {
206
+ allKeys.push('eff. uptime');
207
+ }
208
+ }
209
+ if (node.computedAvailability < 1) allKeys.push('availability');
210
+ if (node.computedCbState === 'open') allKeys.push('CB');
211
+ if (node.queueMetrics) {
212
+ if (node.queueMetrics.fillRate > 0) allKeys.push('lag');
213
+ if (node.queueMetrics.fillRate > 0 && node.queueMetrics.timeToOverflow < Infinity) allKeys.push('overflow');
214
+ }
215
+ }
216
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH, labelWidth);
217
+
218
+ const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
219
+ // key + ": " + value
220
+ let maxRowWidth = 0;
221
+ if (node.computedRps > 0) {
222
+ // RPS row may show "29.3k / 50k" when an effective cap exists
223
+ const nodeMaxRps = getNumProp(node, 'max-rps', 0);
224
+ const nodeRateLimit = getNumProp(node, 'ratelimit-rps', 0);
225
+ const nodeConcurrency = getNumProp(node, 'concurrency', 0);
226
+ const nodeDurationMs = getNumProp(node, 'duration-ms', 100);
227
+ const serverlessCap = nodeConcurrency > 0 ? nodeConcurrency / (nodeDurationMs / 1000) : 0;
228
+ const effectiveCap = serverlessCap > 0 ? serverlessCap
229
+ : nodeMaxRps > 0 && nodeRateLimit > 0
230
+ ? Math.min(nodeMaxRps * node.computedInstances, nodeRateLimit)
231
+ : nodeMaxRps > 0 ? nodeMaxRps * node.computedInstances
232
+ : nodeRateLimit > 0 ? nodeRateLimit
233
+ : 0;
234
+ const rpsVal = effectiveCap > 0 && !node.isEdge
235
+ ? `${formatRpsShort(node.computedRps)} / ${formatRpsShort(effectiveCap)}`
236
+ : formatRps(node.computedRps);
237
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + rpsVal.length) * META_CHAR_WIDTH);
238
+ }
239
+ // Declared property value widths only when expanded
240
+ if (expanded) {
241
+ for (const p of node.properties) {
242
+ const dk = DISPLAY_NAMES[p.key];
243
+ if (!dk) continue;
244
+ const numVal = typeof p.value === 'number' ? p.value : parseFloat(String(p.value)) || 0;
245
+ const PCT_KEYS = ['cache-hit', 'firewall-block', 'uptime', 'cb-error-threshold'];
246
+ const valLen = (p.key === 'max-rps' || p.key === 'ratelimit-rps')
247
+ ? formatRpsShort(numVal).length
248
+ : (p.key === 'latency-ms' || p.key === 'cb-latency-threshold-ms' || p.key === 'duration-ms' || p.key === 'cold-start-ms')
249
+ ? formatMs(numVal).length
250
+ : PCT_KEYS.includes(p.key)
251
+ ? `${numVal}%`.length
252
+ : String(p.value).length;
253
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH);
254
+ }
255
+ }
256
+ // Computed row widths (e.g., "p99: 120ms")
257
+ if (computedRows > 0) {
258
+ const perc = node.computedLatencyPercentiles;
259
+ const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p99];
260
+ for (const ms of msValues) {
261
+ if (ms > 0) {
262
+ const valLen = formatMs(ms).length;
263
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH);
264
+ }
265
+ }
266
+ if (node.computedUptime < 1) {
267
+ const valLen = formatUptime(node.computedUptime).length;
268
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH);
269
+ }
270
+ if (node.computedAvailability < 1) {
271
+ const valLen = formatUptime(node.computedAvailability).length;
272
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH);
273
+ }
274
+ // CB state row ("CB: OPEN") — inverted pill, use full text width
275
+ if (node.computedCbState === 'open') {
276
+ maxRowWidth = Math.max(maxRowWidth, 'CB: OPEN'.length * META_CHAR_WIDTH + 8);
277
+ }
278
+ }
279
+
280
+ return Math.max(MIN_NODE_WIDTH, labelWidth, maxRowWidth + 20);
281
+ }
282
+
283
+ function computeNodeHeight(node: ComputedInfraNode, expanded: boolean, options?: Record<string, string>): number {
284
+ const propCount = countDisplayProps(node, expanded, options);
285
+ const computedCount = countComputedRows(node, expanded);
286
+ const hasRps = node.computedRps > 0;
287
+ if (propCount === 0 && computedCount === 0 && !hasRps) return NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM;
288
+
289
+ let h = NODE_HEADER_HEIGHT + NODE_SEPARATOR_GAP;
290
+ // Computed section: RPS + computed rows
291
+ const computedSectionCount = (hasRps ? 1 : 0) + computedCount;
292
+ h += computedSectionCount * META_LINE_HEIGHT;
293
+ // Separator between computed and declared sections
294
+ if (computedSectionCount > 0 && propCount > 0) h += NODE_SEPARATOR_GAP;
295
+ // Declared property rows
296
+ h += propCount * META_LINE_HEIGHT;
297
+ // Role dots row
298
+ if (hasRoles(node)) h += ROLE_DOT_ROW;
299
+ h += NODE_PAD_BOTTOM;
300
+ // Collapsed group nodes have a collapse bar at the bottom — add space so dots aren't obscured
301
+ if (node.id.startsWith('[')) h += COLLAPSE_BAR_HEIGHT;
302
+ return h;
303
+ }
304
+
305
+ function formatRps(rps: number): string {
306
+ if (rps >= 1000) return `${(rps / 1000).toFixed(1)}k rps`;
307
+ return `${Math.round(rps)} rps`;
308
+ }
309
+
310
+ function formatRpsShort(rps: number): string {
311
+ if (rps >= 1000) return `${(rps / 1000).toFixed(1)}k`;
312
+ return `${Math.round(rps)}`;
313
+ }
314
+
315
+ function getNumProp(node: ComputedInfraNode, key: string, fallback: number): number {
316
+ const p = node.properties.find((pr) => pr.key === key);
317
+ if (!p) return fallback;
318
+ return typeof p.value === 'number' ? p.value : parseFloat(String(p.value)) || fallback;
319
+ }
320
+
321
+ function formatMs(ms: number): string {
322
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
323
+ return `${Math.round(ms)}ms`;
324
+ }
325
+
326
+ function formatUptime(fraction: number): string {
327
+ const pct = fraction * 100;
328
+ if (pct >= 99.99) return '99.99%';
329
+ if (pct >= 99.9) return `${pct.toFixed(2)}%`;
330
+ if (pct >= 99) return `${pct.toFixed(1)}%`;
331
+ return `${pct.toFixed(1)}%`;
332
+ }
333
+
334
+ // ============================================================
335
+ // Layout engine
336
+ // ============================================================
337
+
338
+ export function layoutInfra(computed: ComputedInfraModel, selectedNodeId?: string | null, collapsedNodes?: Set<string> | null): InfraLayoutResult {
339
+ if (computed.nodes.length === 0) {
340
+ return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
341
+ }
342
+
343
+ const g = new dagre.graphlib.Graph();
344
+ g.setGraph({
345
+ rankdir: computed.direction === 'TB' ? 'TB' : 'LR',
346
+ nodesep: 50,
347
+ ranksep: 100,
348
+ edgesep: 20,
349
+ });
350
+ g.setDefaultEdgeLabel(() => ({}));
351
+
352
+ // Build set of grouped node IDs for inflating dimensions
353
+ const groupedNodeIds = new Set<string>();
354
+ for (const node of computed.nodes) {
355
+ if (node.groupId) groupedNodeIds.add(node.id);
356
+ }
357
+
358
+ // Extra space dagre must reserve for the group bounding box
359
+ const GROUP_INFLATE = GROUP_PADDING * 2 + GROUP_HEADER_HEIGHT;
360
+ const isLR = computed.direction !== 'TB';
361
+
362
+ // Add nodes — inflate grouped nodes so dagre accounts for group boxes
363
+ const widthMap = new Map<string, number>();
364
+ const heightMap = new Map<string, number>();
365
+ for (const node of computed.nodes) {
366
+ const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
367
+ const expanded = !isNodeCollapsed && node.id === selectedNodeId;
368
+ const width = computeNodeWidth(node, expanded, computed.options);
369
+ const height = isNodeCollapsed
370
+ ? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM
371
+ : computeNodeHeight(node, expanded, computed.options);
372
+ widthMap.set(node.id, width);
373
+ heightMap.set(node.id, height);
374
+ const inGroup = groupedNodeIds.has(node.id);
375
+ g.setNode(node.id, {
376
+ label: node.label,
377
+ width: inGroup && !isLR ? width + GROUP_INFLATE : width,
378
+ height: inGroup && isLR ? height + GROUP_INFLATE : height,
379
+ });
380
+ }
381
+
382
+ // Add edges — skip edges targeting groups (resolve to children instead)
383
+ const groupChildIds = new Set<string>();
384
+ for (const node of computed.nodes) {
385
+ if (node.groupId) groupChildIds.add(node.id);
386
+ }
387
+
388
+ // Build group child lookup
389
+ const groupChildren = new Map<string, string[]>();
390
+ for (const node of computed.nodes) {
391
+ if (node.groupId) {
392
+ const list = groupChildren.get(node.groupId) ?? [];
393
+ list.push(node.id);
394
+ groupChildren.set(node.groupId, list);
395
+ }
396
+ }
397
+
398
+ for (const edge of computed.edges) {
399
+ // If target is a group, add edges to all children of that group
400
+ const children = groupChildren.get(edge.targetId);
401
+ if (children && children.length > 0) {
402
+ for (const childId of children) {
403
+ g.setEdge(edge.sourceId, childId, { label: edge.label });
404
+ }
405
+ } else {
406
+ g.setEdge(edge.sourceId, edge.targetId, { label: edge.label });
407
+ }
408
+ }
409
+
410
+ // Run layout
411
+ dagre.layout(g);
412
+
413
+ // Extract positioned nodes
414
+ const layoutNodes: InfraLayoutNode[] = computed.nodes.map((node) => {
415
+ const pos = g.node(node.id);
416
+ return {
417
+ id: node.id,
418
+ label: node.label,
419
+ x: pos.x,
420
+ y: pos.y,
421
+ width: widthMap.get(node.id) ?? MIN_NODE_WIDTH,
422
+ height: heightMap.get(node.id) ?? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM,
423
+ computedRps: node.computedRps,
424
+ overloaded: node.overloaded,
425
+ rateLimited: node.rateLimited,
426
+ isEdge: node.isEdge,
427
+ groupId: node.groupId,
428
+ computedLatencyMs: node.computedLatencyMs,
429
+ computedLatencyPercentiles: node.computedLatencyPercentiles,
430
+ computedUptime: node.computedUptime,
431
+ computedAvailability: node.computedAvailability,
432
+ computedAvailabilityPercentiles: node.computedAvailabilityPercentiles,
433
+ computedInstances: node.computedInstances,
434
+ computedConcurrentInvocations: node.computedConcurrentInvocations,
435
+ computedCbState: node.computedCbState,
436
+ childHealthState: node.childHealthState,
437
+ queueMetrics: node.queueMetrics,
438
+ properties: node.properties,
439
+ tags: node.tags,
440
+ lineNumber: node.lineNumber,
441
+ };
442
+ });
443
+
444
+ // Extract edge waypoints
445
+ const layoutEdges: InfraLayoutEdge[] = [];
446
+ for (const edge of computed.edges) {
447
+ const children = groupChildren.get(edge.targetId);
448
+ if (children && children.length > 0) {
449
+ // Use the first child's edge points as representative
450
+ const edgeData = g.edge(edge.sourceId, children[0]);
451
+ layoutEdges.push({
452
+ sourceId: edge.sourceId,
453
+ targetId: edge.targetId,
454
+ label: edge.label,
455
+ computedRps: edge.computedRps,
456
+ split: edge.split,
457
+ points: edgeData?.points ?? [],
458
+ lineNumber: edge.lineNumber,
459
+ });
460
+ } else {
461
+ const edgeData = g.edge(edge.sourceId, edge.targetId);
462
+ layoutEdges.push({
463
+ sourceId: edge.sourceId,
464
+ targetId: edge.targetId,
465
+ label: edge.label,
466
+ computedRps: edge.computedRps,
467
+ split: edge.split,
468
+ points: edgeData?.points ?? [],
469
+ lineNumber: edge.lineNumber,
470
+ });
471
+ }
472
+ }
473
+
474
+ // Compute group bounding boxes from children
475
+ const layoutGroups: InfraLayoutGroup[] = computed.groups.map((group) => {
476
+ const childNodes = layoutNodes.filter((n) => n.groupId === group.id);
477
+ if (childNodes.length === 0) {
478
+ return {
479
+ id: group.id,
480
+ label: group.label,
481
+ x: 0,
482
+ y: 0,
483
+ width: MIN_NODE_WIDTH,
484
+ height: NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM,
485
+ instances: group.instances,
486
+ lineNumber: group.lineNumber,
487
+ };
488
+ }
489
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
490
+ for (const child of childNodes) {
491
+ const left = child.x - child.width / 2;
492
+ const right = child.x + child.width / 2;
493
+ const top = child.y - child.height / 2;
494
+ const bottom = child.y + child.height / 2;
495
+ if (left < minX) minX = left;
496
+ if (right > maxX) maxX = right;
497
+ if (top < minY) minY = top;
498
+ if (bottom > maxY) maxY = bottom;
499
+ }
500
+ return {
501
+ id: group.id,
502
+ label: group.label,
503
+ x: minX - GROUP_PADDING,
504
+ y: minY - GROUP_PADDING - GROUP_HEADER_HEIGHT,
505
+ width: maxX - minX + GROUP_PADDING * 2,
506
+ height: maxY - minY + GROUP_PADDING * 2 + GROUP_HEADER_HEIGHT,
507
+ instances: group.instances,
508
+ lineNumber: group.lineNumber,
509
+ };
510
+ });
511
+
512
+ // Compute total dimensions
513
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
514
+ for (const node of layoutNodes) {
515
+ const left = node.x - node.width / 2;
516
+ const right = node.x + node.width / 2;
517
+ const top = node.y - node.height / 2;
518
+ const bottom = node.y + node.height / 2;
519
+ if (left < minX) minX = left;
520
+ if (right > maxX) maxX = right;
521
+ if (top < minY) minY = top;
522
+ if (bottom > maxY) maxY = bottom;
523
+ }
524
+ for (const group of layoutGroups) {
525
+ if (group.x < minX) minX = group.x;
526
+ if (group.x + group.width > maxX) maxX = group.x + group.width;
527
+ if (group.y < minY) minY = group.y;
528
+ if (group.y + group.height > maxY) maxY = group.y + group.height;
529
+ }
530
+ for (const edge of layoutEdges) {
531
+ for (const pt of edge.points) {
532
+ if (pt.x < minX) minX = pt.x;
533
+ if (pt.x > maxX) maxX = pt.x;
534
+ if (pt.y < minY) minY = pt.y;
535
+ if (pt.y > maxY) maxY = pt.y;
536
+ }
537
+ // Account for edge label width at midpoint
538
+ if (edge.label) {
539
+ const midIdx = Math.floor(edge.points.length / 2);
540
+ const midPt = edge.points[midIdx];
541
+ if (midPt) {
542
+ const halfWidth = (edge.label.length * 6.5 + 8) / 2;
543
+ if (midPt.x - halfWidth < minX) minX = midPt.x - halfWidth;
544
+ if (midPt.x + halfWidth > maxX) maxX = midPt.x + halfWidth;
545
+ }
546
+ }
547
+ }
548
+
549
+ // Shift everything to start at EDGE_MARGIN
550
+ const shiftX = -minX + EDGE_MARGIN;
551
+ const shiftY = -minY + EDGE_MARGIN;
552
+ for (const node of layoutNodes) {
553
+ node.x += shiftX;
554
+ node.y += shiftY;
555
+ }
556
+ for (const edge of layoutEdges) {
557
+ for (const pt of edge.points) {
558
+ pt.x += shiftX;
559
+ pt.y += shiftY;
560
+ }
561
+ }
562
+ for (const group of layoutGroups) {
563
+ group.x += shiftX;
564
+ group.y += shiftY;
565
+ }
566
+
567
+ const totalWidth = (maxX + shiftX) + EDGE_MARGIN;
568
+ const totalHeight = (maxY + shiftY) + EDGE_MARGIN;
569
+
570
+ return {
571
+ nodes: layoutNodes,
572
+ edges: layoutEdges,
573
+ groups: layoutGroups,
574
+ options: computed.options,
575
+ width: totalWidth,
576
+ height: totalHeight,
577
+ };
578
+ }