@diagrammo/dgmo 0.8.21 → 0.8.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +145 -93
- package/dist/editor.cjs +20 -3
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +20 -3
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +15 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +15 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +20843 -14937
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -17
- package/dist/index.d.ts +426 -17
- package/dist/index.js +20795 -14912
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +380 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +179 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +337 -0
- package/dist/internal.js.map +1 -0
- package/docs/guide/chart-cycle.md +156 -0
- package/docs/guide/chart-journey-map.md +179 -0
- package/docs/guide/chart-pyramid.md +111 -0
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/registry.json +6 -0
- package/docs/language-reference.md +177 -6
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
- package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
- package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +11 -1
- package/src/boxes-and-lines/layout.ts +309 -33
- package/src/boxes-and-lines/parser.ts +86 -10
- package/src/boxes-and-lines/renderer.ts +250 -91
- package/src/boxes-and-lines/types.ts +1 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/cli.ts +5 -35
- package/src/completion.ts +233 -41
- package/src/cycle/layout.ts +723 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +566 -0
- package/src/cycle/types.ts +98 -0
- package/src/d3.ts +107 -8
- package/src/dgmo-router.ts +82 -3
- package/src/echarts.ts +8 -5
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +17 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/state-parser.ts +5 -10
- package/src/index.ts +63 -2
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +32 -8
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/internal.ts +16 -0
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1521 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +31 -15
- package/src/mindmap/parser.ts +12 -18
- package/src/mindmap/renderer.ts +14 -13
- package/src/mindmap/text-wrap.ts +22 -12
- package/src/mindmap/types.ts +2 -2
- package/src/org/collapse.ts +81 -0
- package/src/org/parser.ts +2 -6
- package/src/org/renderer.ts +212 -4
- package/src/pyramid/parser.ts +172 -0
- package/src/pyramid/renderer.ts +684 -0
- package/src/pyramid/types.ts +28 -0
- package/src/render.ts +2 -8
- package/src/sequence/parser.ts +62 -20
- package/src/sequence/renderer.ts +146 -40
- package/src/sharing.ts +1 -0
- package/src/sitemap/layout.ts +21 -6
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1112 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/legend-layout.ts +3 -1
- package/src/utils/parsing.ts +47 -7
- package/src/utils/tag-groups.ts +46 -60
package/src/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ export {
|
|
|
32
32
|
parseDgmo,
|
|
33
33
|
getRenderCategory,
|
|
34
34
|
isExtendedChartType,
|
|
35
|
+
getAllChartTypes,
|
|
36
|
+
CHART_TYPE_DESCRIPTIONS,
|
|
35
37
|
} from './dgmo-router';
|
|
36
38
|
export type { RenderCategory } from './dgmo-router';
|
|
37
39
|
|
|
@@ -63,6 +65,7 @@ export type {
|
|
|
63
65
|
|
|
64
66
|
export {
|
|
65
67
|
parseSequenceDgmo,
|
|
68
|
+
parseSequenceDgmo as parseSequenceDiagram,
|
|
66
69
|
looksLikeSequence,
|
|
67
70
|
isSequenceBlock,
|
|
68
71
|
isSequenceNote,
|
|
@@ -331,8 +334,12 @@ export type {
|
|
|
331
334
|
ResolvedGroup,
|
|
332
335
|
} from './gantt/types';
|
|
333
336
|
|
|
334
|
-
export { collapseOrgTree } from './org/collapse';
|
|
335
|
-
export type {
|
|
337
|
+
export { collapseOrgTree, focusOrgTree } from './org/collapse';
|
|
338
|
+
export type {
|
|
339
|
+
CollapsedOrgResult,
|
|
340
|
+
FocusOrgResult,
|
|
341
|
+
AncestorInfo,
|
|
342
|
+
} from './org/collapse';
|
|
336
343
|
|
|
337
344
|
export { parseMindmap } from './mindmap/parser';
|
|
338
345
|
export type {
|
|
@@ -358,6 +365,59 @@ export { layoutWireframe } from './wireframe/layout';
|
|
|
358
365
|
export type { WireframeLayout, WireframeLayoutNode } from './wireframe/layout';
|
|
359
366
|
export { renderWireframe } from './wireframe/renderer';
|
|
360
367
|
|
|
368
|
+
export { parseTechRadar } from './tech-radar/parser';
|
|
369
|
+
export { computeRadarLayout, getRadarGeometry } from './tech-radar/layout';
|
|
370
|
+
export {
|
|
371
|
+
renderTechRadar,
|
|
372
|
+
renderTechRadarForExport,
|
|
373
|
+
} from './tech-radar/renderer';
|
|
374
|
+
export {
|
|
375
|
+
renderQuadrantFocus,
|
|
376
|
+
renderQuadrantFocusForExport,
|
|
377
|
+
} from './tech-radar/interactive';
|
|
378
|
+
export type {
|
|
379
|
+
ParsedTechRadar,
|
|
380
|
+
TechRadarRing,
|
|
381
|
+
TechRadarQuadrant,
|
|
382
|
+
TechRadarBlip,
|
|
383
|
+
TechRadarLayoutPoint,
|
|
384
|
+
QuadrantPosition,
|
|
385
|
+
BlipTrend,
|
|
386
|
+
} from './tech-radar/types';
|
|
387
|
+
|
|
388
|
+
export { parseCycle } from './cycle/parser';
|
|
389
|
+
export { computeCycleLayout } from './cycle/layout';
|
|
390
|
+
export { renderCycle, renderCycleForExport } from './cycle/renderer';
|
|
391
|
+
export type { CycleRenderOptions } from './cycle/renderer';
|
|
392
|
+
export type {
|
|
393
|
+
ParsedCycle,
|
|
394
|
+
CycleNode,
|
|
395
|
+
CycleEdge,
|
|
396
|
+
CycleLayoutNode,
|
|
397
|
+
CycleLayoutEdge,
|
|
398
|
+
CycleLayoutResult,
|
|
399
|
+
} from './cycle/types';
|
|
400
|
+
|
|
401
|
+
export { parseJourneyMap } from './journey-map/parser';
|
|
402
|
+
export { layoutJourneyMap } from './journey-map/layout';
|
|
403
|
+
export type { JourneyMapLayout } from './journey-map/layout';
|
|
404
|
+
export {
|
|
405
|
+
renderJourneyMap,
|
|
406
|
+
renderJourneyMapForExport,
|
|
407
|
+
} from './journey-map/renderer';
|
|
408
|
+
export type {
|
|
409
|
+
ParsedJourneyMap,
|
|
410
|
+
JourneyMapPhase,
|
|
411
|
+
JourneyMapStep,
|
|
412
|
+
JourneyMapPersona,
|
|
413
|
+
JourneyMapAnnotation,
|
|
414
|
+
} from './journey-map/types';
|
|
415
|
+
export type { JourneyMapInteractiveOptions } from './journey-map/renderer';
|
|
416
|
+
|
|
417
|
+
export { parsePyramid } from './pyramid/parser';
|
|
418
|
+
export { renderPyramid, renderPyramidForExport } from './pyramid/renderer';
|
|
419
|
+
export type { ParsedPyramid, PyramidLayer } from './pyramid/types';
|
|
420
|
+
|
|
361
421
|
export { resolveOrgImports } from './org/resolver';
|
|
362
422
|
export type {
|
|
363
423
|
ReadFileFn,
|
|
@@ -436,6 +496,7 @@ export {
|
|
|
436
496
|
applyGroupOrdering,
|
|
437
497
|
groupMessagesBySection,
|
|
438
498
|
buildNoteMessageMap,
|
|
499
|
+
collectNoteLineNumbers,
|
|
439
500
|
} from './sequence/renderer';
|
|
440
501
|
export type {
|
|
441
502
|
RenderStep,
|
package/src/infra/layout.ts
CHANGED
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
// post-layout bounding box wrappers around their children.
|
|
7
7
|
|
|
8
8
|
import dagre from '@dagrejs/dagre';
|
|
9
|
-
import type {
|
|
10
|
-
ComputedInfraModel,
|
|
11
|
-
ComputedInfraNode,
|
|
12
|
-
} from './types';
|
|
9
|
+
import type { ComputedInfraModel, ComputedInfraNode } from './types';
|
|
13
10
|
|
|
14
11
|
// ============================================================
|
|
15
12
|
// Layout types
|
|
@@ -39,7 +36,7 @@ export interface InfraLayoutNode {
|
|
|
39
36
|
properties: ComputedInfraNode['properties'];
|
|
40
37
|
queueMetrics?: ComputedInfraNode['queueMetrics'];
|
|
41
38
|
tags: Record<string, string>;
|
|
42
|
-
description?: string;
|
|
39
|
+
description?: string[];
|
|
43
40
|
lineNumber: number;
|
|
44
41
|
}
|
|
45
42
|
|
|
@@ -101,24 +98,49 @@ const EDGE_MARGIN = 60;
|
|
|
101
98
|
|
|
102
99
|
/** Display property keys shown as key: value rows. */
|
|
103
100
|
const DISPLAY_KEYS = new Set([
|
|
104
|
-
'cache-hit',
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
101
|
+
'cache-hit',
|
|
102
|
+
'firewall-block',
|
|
103
|
+
'ratelimit-rps',
|
|
104
|
+
'latency-ms',
|
|
105
|
+
'uptime',
|
|
106
|
+
'instances',
|
|
107
|
+
'max-rps',
|
|
108
|
+
'cb-error-threshold',
|
|
109
|
+
'cb-latency-threshold-ms',
|
|
110
|
+
'concurrency',
|
|
111
|
+
'duration-ms',
|
|
112
|
+
'cold-start-ms',
|
|
113
|
+
'buffer',
|
|
114
|
+
'drain-rate',
|
|
115
|
+
'retention-hours',
|
|
116
|
+
'partitions',
|
|
109
117
|
]);
|
|
110
118
|
|
|
111
119
|
/** Display names for width estimation. */
|
|
112
120
|
const DISPLAY_NAMES: Record<string, string> = {
|
|
113
|
-
'cache-hit': 'cache hit',
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
'cache-hit': 'cache hit',
|
|
122
|
+
'firewall-block': 'firewall block',
|
|
123
|
+
'ratelimit-rps': 'rate limit RPS',
|
|
124
|
+
'latency-ms': 'latency',
|
|
125
|
+
uptime: 'uptime',
|
|
126
|
+
instances: 'instances',
|
|
127
|
+
'max-rps': 'max RPS',
|
|
128
|
+
'cb-error-threshold': 'CB error threshold',
|
|
129
|
+
'cb-latency-threshold-ms': 'CB latency threshold',
|
|
130
|
+
concurrency: 'concurrency',
|
|
131
|
+
'duration-ms': 'duration',
|
|
132
|
+
'cold-start-ms': 'cold start',
|
|
133
|
+
buffer: 'buffer',
|
|
134
|
+
'drain-rate': 'drain rate',
|
|
135
|
+
'retention-hours': 'retention',
|
|
136
|
+
partitions: 'partitions',
|
|
119
137
|
};
|
|
120
138
|
|
|
121
|
-
function countDisplayProps(
|
|
139
|
+
function countDisplayProps(
|
|
140
|
+
node: ComputedInfraNode,
|
|
141
|
+
expanded: boolean,
|
|
142
|
+
options?: Record<string, string>
|
|
143
|
+
): number {
|
|
122
144
|
// Declared properties are only shown when the node is selected (expanded)
|
|
123
145
|
if (!expanded) return 0;
|
|
124
146
|
let count = node.properties.filter((p) => DISPLAY_KEYS.has(p.key)).length;
|
|
@@ -154,7 +176,11 @@ function countComputedRows(node: ComputedInfraNode, expanded: boolean): number {
|
|
|
154
176
|
// Queue computed rows: lag + overflow
|
|
155
177
|
if (node.queueMetrics) {
|
|
156
178
|
if (node.queueMetrics.fillRate > 0) count += 1; // lag row
|
|
157
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
node.queueMetrics.fillRate > 0 &&
|
|
181
|
+
node.queueMetrics.timeToOverflow < Infinity
|
|
182
|
+
)
|
|
183
|
+
count += 1; // overflow row
|
|
158
184
|
}
|
|
159
185
|
return count;
|
|
160
186
|
}
|
|
@@ -164,10 +190,16 @@ function hasRoles(node: ComputedInfraNode): boolean {
|
|
|
164
190
|
return node.properties.some((p) => DISPLAY_KEYS.has(p.key));
|
|
165
191
|
}
|
|
166
192
|
|
|
167
|
-
function computeNodeWidth(
|
|
193
|
+
function computeNodeWidth(
|
|
194
|
+
node: ComputedInfraNode,
|
|
195
|
+
expanded: boolean,
|
|
196
|
+
options?: Record<string, string>
|
|
197
|
+
): number {
|
|
168
198
|
// Account for badge text (e.g., "3x") in header width — serverless nodes no longer show a badge
|
|
169
|
-
const badgeVal =
|
|
170
|
-
|
|
199
|
+
const badgeVal =
|
|
200
|
+
node.computedConcurrentInvocations === 0 && node.computedInstances > 1
|
|
201
|
+
? node.computedInstances
|
|
202
|
+
: 0;
|
|
171
203
|
const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
|
|
172
204
|
const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH + PADDING_X;
|
|
173
205
|
|
|
@@ -185,8 +217,18 @@ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?:
|
|
|
185
217
|
const hasLatency = node.properties.some((p) => p.key === 'latency-ms');
|
|
186
218
|
const hasUptime = node.properties.some((p) => p.key === 'uptime');
|
|
187
219
|
const isServerless = node.properties.some((p) => p.key === 'concurrency');
|
|
188
|
-
if (
|
|
189
|
-
|
|
220
|
+
if (
|
|
221
|
+
!hasLatency &&
|
|
222
|
+
!isServerless &&
|
|
223
|
+
(parseFloat(options['default-latency-ms'] ?? '') || 0) > 0
|
|
224
|
+
)
|
|
225
|
+
allKeys.push('latency');
|
|
226
|
+
if (
|
|
227
|
+
!hasUptime &&
|
|
228
|
+
(parseFloat(options['default-uptime'] ?? '') || 0) > 0 &&
|
|
229
|
+
parseFloat(options['default-uptime'] ?? '') < 100
|
|
230
|
+
)
|
|
231
|
+
allKeys.push('uptime');
|
|
190
232
|
}
|
|
191
233
|
}
|
|
192
234
|
// Computed rows
|
|
@@ -203,8 +245,13 @@ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?:
|
|
|
203
245
|
}
|
|
204
246
|
if (node.computedUptime < 1) {
|
|
205
247
|
const declaredUptime = node.properties.find((p) => p.key === 'uptime');
|
|
206
|
-
const declaredVal = declaredUptime
|
|
207
|
-
|
|
248
|
+
const declaredVal = declaredUptime
|
|
249
|
+
? Number(declaredUptime.value) / 100
|
|
250
|
+
: 1;
|
|
251
|
+
if (
|
|
252
|
+
Math.abs(node.computedUptime - declaredVal) > 0.000001 ||
|
|
253
|
+
node.isEdge
|
|
254
|
+
) {
|
|
208
255
|
allKeys.push('eff. uptime');
|
|
209
256
|
}
|
|
210
257
|
}
|
|
@@ -212,7 +259,11 @@ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?:
|
|
|
212
259
|
if (node.computedCbState === 'open') allKeys.push('CB');
|
|
213
260
|
if (node.queueMetrics) {
|
|
214
261
|
if (node.queueMetrics.fillRate > 0) allKeys.push('lag');
|
|
215
|
-
if (
|
|
262
|
+
if (
|
|
263
|
+
node.queueMetrics.fillRate > 0 &&
|
|
264
|
+
node.queueMetrics.timeToOverflow < Infinity
|
|
265
|
+
)
|
|
266
|
+
allKeys.push('overflow');
|
|
216
267
|
}
|
|
217
268
|
}
|
|
218
269
|
if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH, labelWidth);
|
|
@@ -226,33 +277,57 @@ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?:
|
|
|
226
277
|
const nodeRateLimit = getNumProp(node, 'ratelimit-rps', 0);
|
|
227
278
|
const nodeConcurrency = getNumProp(node, 'concurrency', 0);
|
|
228
279
|
const nodeDurationMs = getNumProp(node, 'duration-ms', 100);
|
|
229
|
-
const serverlessCap =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
280
|
+
const serverlessCap =
|
|
281
|
+
nodeConcurrency > 0 ? nodeConcurrency / (nodeDurationMs / 1000) : 0;
|
|
282
|
+
const effectiveCap =
|
|
283
|
+
serverlessCap > 0
|
|
284
|
+
? serverlessCap
|
|
285
|
+
: nodeMaxRps > 0 && nodeRateLimit > 0
|
|
286
|
+
? Math.min(nodeMaxRps * node.computedInstances, nodeRateLimit)
|
|
287
|
+
: nodeMaxRps > 0
|
|
288
|
+
? nodeMaxRps * node.computedInstances
|
|
289
|
+
: nodeRateLimit > 0
|
|
290
|
+
? nodeRateLimit
|
|
291
|
+
: 0;
|
|
292
|
+
const rpsVal =
|
|
293
|
+
effectiveCap > 0 && !node.isEdge
|
|
294
|
+
? `${formatRpsShort(node.computedRps)} / ${formatRpsShort(effectiveCap)}`
|
|
295
|
+
: formatRps(node.computedRps);
|
|
296
|
+
maxRowWidth = Math.max(
|
|
297
|
+
maxRowWidth,
|
|
298
|
+
(maxKeyLen + 2 + rpsVal.length) * META_CHAR_WIDTH
|
|
299
|
+
);
|
|
240
300
|
}
|
|
241
301
|
// Declared property value widths only when expanded
|
|
242
302
|
if (expanded) {
|
|
243
303
|
for (const p of node.properties) {
|
|
244
304
|
const dk = DISPLAY_NAMES[p.key];
|
|
245
305
|
if (!dk) continue;
|
|
246
|
-
const numVal =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
306
|
+
const numVal =
|
|
307
|
+
typeof p.value === 'number'
|
|
308
|
+
? p.value
|
|
309
|
+
: parseFloat(String(p.value)) || 0;
|
|
310
|
+
const PCT_KEYS = [
|
|
311
|
+
'cache-hit',
|
|
312
|
+
'firewall-block',
|
|
313
|
+
'uptime',
|
|
314
|
+
'cb-error-threshold',
|
|
315
|
+
];
|
|
316
|
+
const valLen =
|
|
317
|
+
p.key === 'max-rps' || p.key === 'ratelimit-rps'
|
|
318
|
+
? formatRpsShort(numVal).length
|
|
319
|
+
: p.key === 'latency-ms' ||
|
|
320
|
+
p.key === 'cb-latency-threshold-ms' ||
|
|
321
|
+
p.key === 'duration-ms' ||
|
|
322
|
+
p.key === 'cold-start-ms'
|
|
323
|
+
? formatMs(numVal).length
|
|
324
|
+
: PCT_KEYS.includes(p.key)
|
|
325
|
+
? `${numVal}%`.length
|
|
326
|
+
: String(p.value).length;
|
|
327
|
+
maxRowWidth = Math.max(
|
|
328
|
+
maxRowWidth,
|
|
329
|
+
(maxKeyLen + 2 + valLen) * META_CHAR_WIDTH
|
|
330
|
+
);
|
|
256
331
|
}
|
|
257
332
|
}
|
|
258
333
|
// Computed row widths (e.g., "p90: 520ms" or "p90: 520ms / 500ms" when SLO configured)
|
|
@@ -262,7 +337,10 @@ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?:
|
|
|
262
337
|
for (const ms of msValues) {
|
|
263
338
|
if (ms > 0) {
|
|
264
339
|
const valLen = formatMs(ms).length;
|
|
265
|
-
maxRowWidth = Math.max(
|
|
340
|
+
maxRowWidth = Math.max(
|
|
341
|
+
maxRowWidth,
|
|
342
|
+
(maxKeyLen + 2 + valLen) * META_CHAR_WIDTH
|
|
343
|
+
);
|
|
266
344
|
}
|
|
267
345
|
}
|
|
268
346
|
// p90 may show "<current> / <threshold>" when non-green. Always reserve combined width
|
|
@@ -271,41 +349,69 @@ function computeNodeWidth(node: ComputedInfraNode, expanded: boolean, options?:
|
|
|
271
349
|
const rawThreshold =
|
|
272
350
|
node.properties.find((p) => p.key === 'slo-p90-latency-ms')?.value ??
|
|
273
351
|
options?.['slo-p90-latency-ms'];
|
|
274
|
-
const threshold =
|
|
352
|
+
const threshold =
|
|
353
|
+
rawThreshold != null ? parseFloat(String(rawThreshold)) : NaN;
|
|
275
354
|
if (!isNaN(threshold) && threshold > 0) {
|
|
276
355
|
// formatMs here must produce the same string as formatMsShort in renderer.ts — both are identical.
|
|
277
356
|
// If either changes, the reserved width and the rendered text will diverge.
|
|
278
357
|
const combinedVal = `${formatMs(perc.p90)} / ${formatMs(threshold)}`;
|
|
279
|
-
maxRowWidth = Math.max(
|
|
358
|
+
maxRowWidth = Math.max(
|
|
359
|
+
maxRowWidth,
|
|
360
|
+
(maxKeyLen + 2 + combinedVal.length) * META_CHAR_WIDTH
|
|
361
|
+
);
|
|
280
362
|
}
|
|
281
363
|
}
|
|
282
364
|
if (node.computedUptime < 1) {
|
|
283
365
|
const valLen = formatUptime(node.computedUptime).length;
|
|
284
|
-
maxRowWidth = Math.max(
|
|
366
|
+
maxRowWidth = Math.max(
|
|
367
|
+
maxRowWidth,
|
|
368
|
+
(maxKeyLen + 2 + valLen) * META_CHAR_WIDTH
|
|
369
|
+
);
|
|
285
370
|
}
|
|
286
371
|
if (node.computedAvailability < 1) {
|
|
287
372
|
const valLen = formatUptime(node.computedAvailability).length;
|
|
288
|
-
maxRowWidth = Math.max(
|
|
373
|
+
maxRowWidth = Math.max(
|
|
374
|
+
maxRowWidth,
|
|
375
|
+
(maxKeyLen + 2 + valLen) * META_CHAR_WIDTH
|
|
376
|
+
);
|
|
289
377
|
}
|
|
290
378
|
// CB state row ("CB: OPEN") — inverted pill, use full text width
|
|
291
379
|
if (node.computedCbState === 'open') {
|
|
292
|
-
maxRowWidth = Math.max(
|
|
380
|
+
maxRowWidth = Math.max(
|
|
381
|
+
maxRowWidth,
|
|
382
|
+
'CB: OPEN'.length * META_CHAR_WIDTH + 8
|
|
383
|
+
);
|
|
293
384
|
}
|
|
294
385
|
}
|
|
295
386
|
|
|
296
387
|
const DESC_MAX_CHARS = 120;
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
388
|
+
const descLines =
|
|
389
|
+
expanded && node.description && !node.isEdge ? node.description : [];
|
|
390
|
+
let descWidth = 0;
|
|
391
|
+
for (const dl of descLines) {
|
|
392
|
+
const truncated =
|
|
393
|
+
dl.length > DESC_MAX_CHARS ? dl.slice(0, DESC_MAX_CHARS - 1) + '…' : dl;
|
|
394
|
+
descWidth = Math.max(
|
|
395
|
+
descWidth,
|
|
396
|
+
truncated.length * META_CHAR_WIDTH + PADDING_X
|
|
397
|
+
);
|
|
398
|
+
}
|
|
300
399
|
return Math.max(MIN_NODE_WIDTH, labelWidth, maxRowWidth + 20, descWidth);
|
|
301
400
|
}
|
|
302
401
|
|
|
303
|
-
function computeNodeHeight(
|
|
402
|
+
function computeNodeHeight(
|
|
403
|
+
node: ComputedInfraNode,
|
|
404
|
+
expanded: boolean,
|
|
405
|
+
options?: Record<string, string>
|
|
406
|
+
): number {
|
|
304
407
|
const propCount = countDisplayProps(node, expanded, options);
|
|
305
408
|
const computedCount = countComputedRows(node, expanded);
|
|
306
409
|
const hasRps = node.computedRps > 0;
|
|
307
|
-
const
|
|
308
|
-
|
|
410
|
+
const descLineCount =
|
|
411
|
+
expanded && node.description && !node.isEdge ? node.description.length : 0;
|
|
412
|
+
const descH = descLineCount * META_LINE_HEIGHT;
|
|
413
|
+
if (propCount === 0 && computedCount === 0 && !hasRps)
|
|
414
|
+
return NODE_HEADER_HEIGHT + descH + NODE_PAD_BOTTOM;
|
|
309
415
|
|
|
310
416
|
let h = NODE_HEADER_HEIGHT + descH + NODE_SEPARATOR_GAP;
|
|
311
417
|
// Computed section: RPS + computed rows
|
|
@@ -333,10 +439,16 @@ function formatRpsShort(rps: number): string {
|
|
|
333
439
|
return `${Math.round(rps)}`;
|
|
334
440
|
}
|
|
335
441
|
|
|
336
|
-
function getNumProp(
|
|
442
|
+
function getNumProp(
|
|
443
|
+
node: ComputedInfraNode,
|
|
444
|
+
key: string,
|
|
445
|
+
fallback: number
|
|
446
|
+
): number {
|
|
337
447
|
const p = node.properties.find((pr) => pr.key === key);
|
|
338
448
|
if (!p) return fallback;
|
|
339
|
-
return typeof p.value === 'number'
|
|
449
|
+
return typeof p.value === 'number'
|
|
450
|
+
? p.value
|
|
451
|
+
: parseFloat(String(p.value)) || fallback;
|
|
340
452
|
}
|
|
341
453
|
|
|
342
454
|
function formatMs(ms: number): string {
|
|
@@ -362,7 +474,7 @@ export function separateGroups(
|
|
|
362
474
|
groups: InfraLayoutGroup[],
|
|
363
475
|
nodes: InfraLayoutNode[],
|
|
364
476
|
isLR: boolean,
|
|
365
|
-
maxIterations = 20
|
|
477
|
+
maxIterations = 20
|
|
366
478
|
): Map<string, { dx: number; dy: number }> {
|
|
367
479
|
// Symmetric 2D rectangle intersection — no sorting needed, handles all
|
|
368
480
|
// relative positions correctly, stable after mid-pass shifts.
|
|
@@ -402,8 +514,16 @@ export function separateGroups(
|
|
|
402
514
|
|
|
403
515
|
// Accumulate the total delta for this group (used by fixEdgeWaypoints)
|
|
404
516
|
const prev = groupDeltas.get(groupToShift.id) ?? { dx: 0, dy: 0 };
|
|
405
|
-
if (isLR)
|
|
406
|
-
|
|
517
|
+
if (isLR)
|
|
518
|
+
groupDeltas.set(groupToShift.id, {
|
|
519
|
+
dx: prev.dx,
|
|
520
|
+
dy: prev.dy + shift,
|
|
521
|
+
});
|
|
522
|
+
else
|
|
523
|
+
groupDeltas.set(groupToShift.id, {
|
|
524
|
+
dx: prev.dx + shift,
|
|
525
|
+
dy: prev.dy,
|
|
526
|
+
});
|
|
407
527
|
|
|
408
528
|
for (const node of nodes) {
|
|
409
529
|
if (node.groupId === groupToShift.id) {
|
|
@@ -413,10 +533,15 @@ export function separateGroups(
|
|
|
413
533
|
}
|
|
414
534
|
}
|
|
415
535
|
}
|
|
416
|
-
if (!anyOverlap) {
|
|
536
|
+
if (!anyOverlap) {
|
|
537
|
+
converged = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
417
540
|
}
|
|
418
541
|
if (!converged && maxIterations > 0) {
|
|
419
|
-
console.warn(
|
|
542
|
+
console.warn(
|
|
543
|
+
`separateGroups: hit maxIterations (${maxIterations}) without fully resolving all group overlaps`
|
|
544
|
+
);
|
|
420
545
|
}
|
|
421
546
|
return groupDeltas;
|
|
422
547
|
}
|
|
@@ -424,7 +549,7 @@ export function separateGroups(
|
|
|
424
549
|
export function fixEdgeWaypoints(
|
|
425
550
|
edges: InfraLayoutEdge[],
|
|
426
551
|
nodes: InfraLayoutNode[],
|
|
427
|
-
groupDeltas: Map<string, { dx: number; dy: number }
|
|
552
|
+
groupDeltas: Map<string, { dx: number; dy: number }>
|
|
428
553
|
): void {
|
|
429
554
|
if (groupDeltas.size === 0) return;
|
|
430
555
|
const nodeToGroup = new Map<string, string | null>();
|
|
@@ -458,9 +583,21 @@ export function fixEdgeWaypoints(
|
|
|
458
583
|
// Layout engine
|
|
459
584
|
// ============================================================
|
|
460
585
|
|
|
461
|
-
export function layoutInfra(
|
|
586
|
+
export function layoutInfra(
|
|
587
|
+
computed: ComputedInfraModel,
|
|
588
|
+
expandedNodeIds?: Set<string> | null,
|
|
589
|
+
collapsedNodes?: Set<string> | null
|
|
590
|
+
): InfraLayoutResult {
|
|
462
591
|
if (computed.nodes.length === 0) {
|
|
463
|
-
return {
|
|
592
|
+
return {
|
|
593
|
+
nodes: [],
|
|
594
|
+
edges: [],
|
|
595
|
+
groups: [],
|
|
596
|
+
options: {},
|
|
597
|
+
direction: computed.direction,
|
|
598
|
+
width: 0,
|
|
599
|
+
height: 0,
|
|
600
|
+
};
|
|
464
601
|
}
|
|
465
602
|
|
|
466
603
|
const isLR = computed.direction !== 'TB';
|
|
@@ -487,7 +624,8 @@ export function layoutInfra(computed: ComputedInfraModel, expandedNodeIds?: Set<
|
|
|
487
624
|
const heightMap = new Map<string, number>();
|
|
488
625
|
for (const node of computed.nodes) {
|
|
489
626
|
const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
|
|
490
|
-
const expanded =
|
|
627
|
+
const expanded =
|
|
628
|
+
!isNodeCollapsed && (expandedNodeIds?.has(node.id) ?? false);
|
|
491
629
|
const width = computeNodeWidth(node, expanded, computed.options);
|
|
492
630
|
const height = isNodeCollapsed
|
|
493
631
|
? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM
|
|
@@ -614,7 +752,10 @@ export function layoutInfra(computed: ComputedInfraModel, expandedNodeIds?: Set<
|
|
|
614
752
|
lineNumber: group.lineNumber,
|
|
615
753
|
};
|
|
616
754
|
}
|
|
617
|
-
let minX = Infinity,
|
|
755
|
+
let minX = Infinity,
|
|
756
|
+
minY = Infinity,
|
|
757
|
+
maxX = -Infinity,
|
|
758
|
+
maxY = -Infinity;
|
|
618
759
|
for (const child of childNodes) {
|
|
619
760
|
const left = child.x - child.width / 2;
|
|
620
761
|
const right = child.x + child.width / 2;
|
|
@@ -642,7 +783,10 @@ export function layoutInfra(computed: ComputedInfraModel, expandedNodeIds?: Set<
|
|
|
642
783
|
fixEdgeWaypoints(layoutEdges, layoutNodes, groupDeltas);
|
|
643
784
|
|
|
644
785
|
// Compute total dimensions
|
|
645
|
-
let minX = Infinity,
|
|
786
|
+
let minX = Infinity,
|
|
787
|
+
minY = Infinity,
|
|
788
|
+
maxX = -Infinity,
|
|
789
|
+
maxY = -Infinity;
|
|
646
790
|
for (const node of layoutNodes) {
|
|
647
791
|
const left = node.x - node.width / 2;
|
|
648
792
|
const right = node.x + node.width / 2;
|
|
@@ -696,8 +840,8 @@ export function layoutInfra(computed: ComputedInfraModel, expandedNodeIds?: Set<
|
|
|
696
840
|
group.y += shiftY;
|
|
697
841
|
}
|
|
698
842
|
|
|
699
|
-
const totalWidth =
|
|
700
|
-
const totalHeight =
|
|
843
|
+
const totalWidth = maxX + shiftX + EDGE_MARGIN;
|
|
844
|
+
const totalHeight = maxY + shiftY + EDGE_MARGIN;
|
|
701
845
|
|
|
702
846
|
return {
|
|
703
847
|
nodes: layoutNodes,
|
package/src/infra/parser.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// and connections, [Group] containers, tag groups, pipe metadata.
|
|
8
8
|
|
|
9
9
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
10
|
+
import { tryStripDescriptionKeyword } from '../utils/description-helpers';
|
|
10
11
|
import { resolveColorWithDiagnostic } from '../colors';
|
|
11
12
|
import { parseInArrowLabel } from '../utils/arrows';
|
|
12
13
|
import {
|
|
@@ -112,8 +113,8 @@ function extractPipeMetadata(rest: string): {
|
|
|
112
113
|
const tags: Record<string, string> = {};
|
|
113
114
|
let clean = rest;
|
|
114
115
|
let match: RegExpExecArray | null;
|
|
115
|
-
|
|
116
|
-
while ((match =
|
|
116
|
+
PIPE_META_RE.lastIndex = 0;
|
|
117
|
+
while ((match = PIPE_META_RE.exec(rest)) !== null) {
|
|
117
118
|
tags[match[1].trim()] = match[2].trim();
|
|
118
119
|
clean = clean.replace(match[0], '');
|
|
119
120
|
}
|
|
@@ -610,17 +611,33 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
610
611
|
// description is display metadata, not a behavior key; silently ignored on edge nodes.
|
|
611
612
|
// Single-line only — no length enforcement, but keep it short for legibility.
|
|
612
613
|
if (key === 'description' && currentNode) {
|
|
613
|
-
if (!currentNode.isEdge)
|
|
614
|
+
if (!currentNode.isEdge) {
|
|
615
|
+
if (!currentNode.description) currentNode.description = [];
|
|
616
|
+
currentNode.description.push(rawVal);
|
|
617
|
+
}
|
|
614
618
|
continue;
|
|
615
619
|
}
|
|
616
620
|
|
|
617
|
-
//
|
|
621
|
+
// Unknown keys: decide between property typo warning vs description collection.
|
|
622
|
+
// Heuristic: if the key looks like a plausible property (alphanumeric-hyphen, close
|
|
623
|
+
// match to a known key, or the value looks numeric/percentage), warn as typo.
|
|
624
|
+
// Otherwise treat the whole line as description text.
|
|
618
625
|
if (!INFRA_BEHAVIOR_KEYS.has(key) && !EDGE_ONLY_KEYS.has(key)) {
|
|
619
626
|
const allKeys = [...INFRA_BEHAVIOR_KEYS, ...EDGE_ONLY_KEYS];
|
|
620
|
-
let msg = `Unknown property '${key}'.`;
|
|
621
627
|
const hint = suggest(key, allKeys);
|
|
622
|
-
|
|
623
|
-
|
|
628
|
+
const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
|
|
629
|
+
if (hint || valueLooksNumeric) {
|
|
630
|
+
// Likely a typo — warn
|
|
631
|
+
let msg = `Unknown property '${key}'.`;
|
|
632
|
+
if (hint) msg += ` ${hint}`;
|
|
633
|
+
warn(lineNumber, msg);
|
|
634
|
+
} else if (!currentNode.isEdge) {
|
|
635
|
+
// Likely prose — collect as description
|
|
636
|
+
if (!currentNode.description) currentNode.description = [];
|
|
637
|
+
currentNode.description.push(trimmed);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
624
641
|
}
|
|
625
642
|
|
|
626
643
|
// Validate edge-only keys
|
|
@@ -636,7 +653,14 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
636
653
|
continue;
|
|
637
654
|
}
|
|
638
655
|
|
|
639
|
-
// Unknown indented line
|
|
656
|
+
// Unknown indented line — try as keywordless description
|
|
657
|
+
if (!currentNode.isEdge) {
|
|
658
|
+
const descResult = tryStripDescriptionKeyword(trimmed);
|
|
659
|
+
const descText = descResult.isKeyword ? descResult.text : trimmed;
|
|
660
|
+
if (!currentNode.description) currentNode.description = [];
|
|
661
|
+
currentNode.description.push(descText);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
640
664
|
warn(
|
|
641
665
|
lineNumber,
|
|
642
666
|
`Unexpected line inside component '${currentNode.label}'.`
|