@diagrammo/dgmo 0.8.2 → 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 (120) 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 +189 -194
  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 +3699 -1564
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +7 -6
  18. package/dist/index.d.ts +7 -6
  19. package/dist/index.js +3699 -1564
  20. package/dist/index.js.map +1 -1
  21. package/docs/language-reference.md +822 -1060
  22. package/gallery/fixtures/arc.dgmo +18 -0
  23. package/gallery/fixtures/area.dgmo +19 -0
  24. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  25. package/gallery/fixtures/bar.dgmo +10 -0
  26. package/gallery/fixtures/c4-full.dgmo +52 -0
  27. package/gallery/fixtures/c4.dgmo +17 -0
  28. package/gallery/fixtures/chord.dgmo +12 -0
  29. package/gallery/fixtures/class-basic.dgmo +14 -0
  30. package/gallery/fixtures/class-full.dgmo +43 -0
  31. package/gallery/fixtures/doughnut.dgmo +8 -0
  32. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  33. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  35. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  36. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  37. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  38. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  39. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  40. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  41. package/gallery/fixtures/function.dgmo +8 -0
  42. package/gallery/fixtures/funnel.dgmo +7 -0
  43. package/gallery/fixtures/gantt-full.dgmo +49 -0
  44. package/gallery/fixtures/gantt.dgmo +42 -0
  45. package/gallery/fixtures/heatmap.dgmo +8 -0
  46. package/gallery/fixtures/infra-full.dgmo +78 -0
  47. package/gallery/fixtures/infra-overload.dgmo +25 -0
  48. package/gallery/fixtures/infra.dgmo +47 -0
  49. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  50. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  51. package/gallery/fixtures/initiative-status.dgmo +9 -0
  52. package/gallery/fixtures/line.dgmo +19 -0
  53. package/gallery/fixtures/multi-line.dgmo +11 -0
  54. package/gallery/fixtures/org-basic.dgmo +16 -0
  55. package/gallery/fixtures/org-full.dgmo +69 -0
  56. package/gallery/fixtures/org-teams.dgmo +25 -0
  57. package/gallery/fixtures/pie.dgmo +9 -0
  58. package/gallery/fixtures/polar-area.dgmo +8 -0
  59. package/gallery/fixtures/quadrant.dgmo +18 -0
  60. package/gallery/fixtures/radar.dgmo +8 -0
  61. package/gallery/fixtures/sankey.dgmo +31 -0
  62. package/gallery/fixtures/scatter.dgmo +21 -0
  63. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  64. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  65. package/gallery/fixtures/sequence.dgmo +35 -0
  66. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  67. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  68. package/gallery/fixtures/slope.dgmo +8 -0
  69. package/gallery/fixtures/spr-eras.dgmo +62 -0
  70. package/gallery/fixtures/state.dgmo +30 -0
  71. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  72. package/gallery/fixtures/timeline.dgmo +32 -0
  73. package/gallery/fixtures/venn.dgmo +10 -0
  74. package/gallery/fixtures/wordcloud.dgmo +24 -0
  75. package/package.json +51 -2
  76. package/src/c4/layout.ts +372 -90
  77. package/src/c4/parser.ts +113 -62
  78. package/src/chart.ts +149 -64
  79. package/src/class/parser.ts +84 -28
  80. package/src/class/renderer.ts +2 -2
  81. package/src/cli.ts +179 -77
  82. package/src/completion.ts +381 -182
  83. package/src/d3.ts +1026 -428
  84. package/src/dgmo-mermaid.ts +16 -13
  85. package/src/dgmo-router.ts +70 -24
  86. package/src/echarts.ts +682 -169
  87. package/src/editor/dgmo.grammar +69 -0
  88. package/src/editor/dgmo.grammar.d.ts +2 -0
  89. package/src/editor/dgmo.grammar.js +18 -0
  90. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  91. package/src/editor/dgmo.grammar.terms.js +35 -0
  92. package/src/editor/highlight.ts +36 -0
  93. package/src/editor/index.ts +28 -0
  94. package/src/editor/keywords.ts +220 -0
  95. package/src/editor/tokens.ts +30 -0
  96. package/src/er/parser.ts +55 -29
  97. package/src/er/renderer.ts +112 -53
  98. package/src/gantt/calculator.ts +91 -29
  99. package/src/gantt/parser.ts +291 -97
  100. package/src/gantt/renderer.ts +1120 -350
  101. package/src/graph/flowchart-parser.ts +48 -75
  102. package/src/graph/state-parser.ts +54 -27
  103. package/src/infra/parser.ts +161 -177
  104. package/src/infra/renderer.ts +723 -271
  105. package/src/infra/types.ts +0 -1
  106. package/src/initiative-status/parser.ts +144 -56
  107. package/src/kanban/parser.ts +27 -19
  108. package/src/org/layout.ts +111 -44
  109. package/src/org/parser.ts +71 -27
  110. package/src/org/resolver.ts +3 -3
  111. package/src/palettes/index.ts +3 -2
  112. package/src/render.ts +1 -2
  113. package/src/sequence/parser.ts +209 -100
  114. package/src/sitemap/parser.ts +73 -44
  115. package/src/utils/arrows.ts +2 -22
  116. package/src/utils/duration.ts +39 -21
  117. package/src/utils/legend-constants.ts +0 -2
  118. package/src/utils/parsing.ts +82 -72
  119. package/src/utils/tag-groups.ts +4 -41
  120. package/src/infra/serialize.ts +0 -67
@@ -11,14 +11,41 @@ import type { PaletteColors } from '../palettes';
11
11
  /** Complete set of recognized chart type identifiers. */
12
12
  export const ALL_CHART_TYPES = new Set([
13
13
  // data charts
14
- 'bar', 'line', 'pie', 'doughnut', 'area', 'polar-area', 'radar',
15
- 'bar-stacked', 'multi-line', 'scatter', 'sankey', 'chord', 'function',
16
- 'heatmap', 'funnel',
14
+ 'bar',
15
+ 'line',
16
+ 'pie',
17
+ 'doughnut',
18
+ 'area',
19
+ 'polar-area',
20
+ 'radar',
21
+ 'bar-stacked',
22
+ 'multi-line',
23
+ 'scatter',
24
+ 'sankey',
25
+ 'chord',
26
+ 'function',
27
+ 'heatmap',
28
+ 'funnel',
17
29
  // visualizations
18
- 'slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant',
30
+ 'slope',
31
+ 'wordcloud',
32
+ 'arc',
33
+ 'timeline',
34
+ 'venn',
35
+ 'quadrant',
19
36
  // diagrams
20
- 'sequence', 'flowchart', 'class', 'er', 'org', 'kanban', 'c4',
21
- 'initiative-status', 'state', 'sitemap', 'infra', 'gantt',
37
+ 'sequence',
38
+ 'flowchart',
39
+ 'class',
40
+ 'er',
41
+ 'org',
42
+ 'kanban',
43
+ 'c4',
44
+ 'initiative-status',
45
+ 'state',
46
+ 'sitemap',
47
+ 'infra',
48
+ 'gantt',
22
49
  ]);
23
50
 
24
51
  /** Measure leading whitespace of a line, normalizing tabs to 4 spaces. */
@@ -38,7 +65,7 @@ export const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
38
65
  /** Extract an optional trailing color suffix from a label, resolving via palette. */
39
66
  export function extractColor(
40
67
  label: string,
41
- palette?: PaletteColors,
68
+ palette?: PaletteColors
42
69
  ): { label: string; color?: string } {
43
70
  const m = label.match(COLOR_SUFFIX_RE);
44
71
  if (!m) return { label };
@@ -49,24 +76,9 @@ export function extractColor(
49
76
  };
50
77
  }
51
78
 
52
- /** @deprecated Matches `chart: <type>` header lines. Remove after all parsers migrate. */
53
- export const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
54
-
55
- /** @deprecated Matches `title: <text>` header lines. Remove after all parsers migrate. */
56
- export const TITLE_RE = /^title\s*:\s*(.+)/i;
57
-
58
- /** @deprecated Matches `option: value` header lines. Remove after all parsers migrate. */
59
- export const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
60
-
61
79
  /** Matches `option value` header lines (space-separated, no colon). */
62
80
  export const OPTION_NOCOLON_RE = /^([a-z][a-z0-9-]*)\s+(.+)$/i;
63
81
 
64
- /** Matches `# GroupName` lines — alternate group notation. */
65
- export const GROUP_HASH_RE = /^#\s+(.+)$/;
66
-
67
- /** Matches `## ...` lines — parse error with helpful hint. */
68
- export const DOUBLE_HASH_RE = /^##\s/;
69
-
70
82
  // ── New shared utilities ─────────────────────────────────────
71
83
 
72
84
  /**
@@ -76,29 +88,12 @@ export const DOUBLE_HASH_RE = /^##\s/;
76
88
  * Returns `null` if the first token is not a recognized chart type.
77
89
  */
78
90
  export function parseFirstLine(
79
- line: string,
91
+ line: string
80
92
  ): { chartType: string; title: string | undefined } | null {
81
93
  const trimmed = line.trim();
82
94
  if (!trimmed || trimmed.startsWith('//')) return null;
83
95
 
84
- // Try old-style `chart: type` first (for transition)
85
- const oldMatch = trimmed.match(CHART_TYPE_RE);
86
- if (oldMatch) {
87
- const parts = oldMatch[1].trim();
88
- // Could be `chart: gantt My Title` — first token is type
89
- const spaceIdx = parts.indexOf(' ');
90
- if (spaceIdx === -1) {
91
- const ct = parts.toLowerCase();
92
- return ALL_CHART_TYPES.has(ct) ? { chartType: ct, title: undefined } : null;
93
- }
94
- const ct = parts.substring(0, spaceIdx).toLowerCase();
95
- if (ALL_CHART_TYPES.has(ct)) {
96
- return { chartType: ct, title: parts.substring(spaceIdx + 1).trim() || undefined };
97
- }
98
- return null;
99
- }
100
-
101
- // New-style: first token is chart type, rest is title
96
+ // First token is chart type, rest is title
102
97
  const spaceIdx = trimmed.indexOf(' ');
103
98
  if (spaceIdx === -1) {
104
99
  const ct = trimmed.toLowerCase();
@@ -106,7 +101,10 @@ export function parseFirstLine(
106
101
  }
107
102
  const firstToken = trimmed.substring(0, spaceIdx).toLowerCase();
108
103
  if (!ALL_CHART_TYPES.has(firstToken)) return null;
109
- return { chartType: firstToken, title: trimmed.substring(spaceIdx + 1).trim() || undefined };
104
+ return {
105
+ chartType: firstToken,
106
+ title: trimmed.substring(spaceIdx + 1).trim() || undefined,
107
+ };
110
108
  }
111
109
 
112
110
  /** Result of `prescanOptions()` — options collected from a two-pass scan. */
@@ -137,7 +135,7 @@ export interface PrescanResult {
137
135
  export function prescanOptions(
138
136
  lines: string[],
139
137
  knownOptions: Set<string>,
140
- knownBooleans: Set<string> = new Set(),
138
+ knownBooleans: Set<string> = new Set()
141
139
  ): PrescanResult {
142
140
  const options: Record<string, string> = {};
143
141
  const booleans = new Set<string>();
@@ -152,12 +150,15 @@ export function prescanOptions(
152
150
 
153
151
  // Strip inline comments
154
152
  const commentIdx = trimmed.indexOf(' //');
155
- const effective = commentIdx >= 0 ? trimmed.substring(0, commentIdx).trim() : trimmed;
153
+ const effective =
154
+ commentIdx >= 0 ? trimmed.substring(0, commentIdx).trim() : trimmed;
156
155
  if (!effective) continue;
157
156
 
158
157
  // Extract first token
159
158
  const spaceIdx = effective.indexOf(' ');
160
- const firstToken = (spaceIdx === -1 ? effective : effective.substring(0, spaceIdx)).toLowerCase();
159
+ const firstToken = (
160
+ spaceIdx === -1 ? effective : effective.substring(0, spaceIdx)
161
+ ).toLowerCase();
161
162
 
162
163
  // Check for bare boolean (presence = on)
163
164
  if (spaceIdx === -1 && knownBooleans.has(firstToken)) {
@@ -210,8 +211,10 @@ export function normalizeGroupedNumber(token: string): string | null {
210
211
  */
211
212
  export function stripQuotes(token: string): string {
212
213
  if (token.length >= 2) {
213
- if ((token[0] === '"' && token[token.length - 1] === '"') ||
214
- (token[0] === "'" && token[token.length - 1] === "'")) {
214
+ if (
215
+ (token[0] === '"' && token[token.length - 1] === '"') ||
216
+ (token[0] === "'" && token[token.length - 1] === "'")
217
+ ) {
215
218
  return token.substring(1, token.length - 1);
216
219
  }
217
220
  }
@@ -228,7 +231,10 @@ export function tokenizeQuoteAware(input: string): string[] {
228
231
  let i = 0;
229
232
  while (i < input.length) {
230
233
  // Skip whitespace
231
- if (input[i] === ' ' || input[i] === '\t') { i++; continue; }
234
+ if (input[i] === ' ' || input[i] === '\t') {
235
+ i++;
236
+ continue;
237
+ }
232
238
 
233
239
  // Quoted token
234
240
  if (input[i] === '"' || input[i] === "'") {
@@ -261,7 +267,7 @@ export function tokenizeQuoteAware(input: string): string[] {
261
267
  */
262
268
  export function collectIndentedValues(
263
269
  lines: string[],
264
- startIndex: number,
270
+ startIndex: number
265
271
  ): { values: string[]; lineNumbers: number[]; newIndex: number } {
266
272
  const values: string[] = [];
267
273
  const lineNumbers: number[] = [];
@@ -293,7 +299,7 @@ export function parseSeriesNames(
293
299
  value: string,
294
300
  lines: string[],
295
301
  lineIndex: number,
296
- palette?: PaletteColors,
302
+ palette?: PaletteColors
297
303
  ): {
298
304
  series: string;
299
305
  names: string[];
@@ -304,10 +310,13 @@ export function parseSeriesNames(
304
310
  let rawNames: string[];
305
311
  let series: string;
306
312
  let newIndex = lineIndex;
307
- let nameLineNumbers: number[] = [];
313
+ let nameLineNumbers: number[] = []; // eslint-disable-line no-useless-assignment
308
314
  if (value) {
309
315
  series = value;
310
- rawNames = value.split(',').map((s) => s.trim()).filter(Boolean);
316
+ rawNames = value
317
+ .split(',')
318
+ .map((s) => s.trim())
319
+ .filter(Boolean);
311
320
  // Inline series names all share the same line number
312
321
  nameLineNumbers = rawNames.map(() => lineIndex + 1);
313
322
  } else {
@@ -330,18 +339,6 @@ export function parseSeriesNames(
330
339
  return { series, names, nameColors, nameLineNumbers, newIndex };
331
340
  }
332
341
 
333
- /**
334
- * Normalize a direction/orientation value to canonical form ('LR' | 'TB').
335
- * Accepts 'lr', 'tb', 'horizontal', 'vertical' (case-insensitive).
336
- * Returns null if the value is not recognized.
337
- */
338
- export function normalizeDirection(value: string): 'LR' | 'TB' | null {
339
- const v = value.trim().toLowerCase();
340
- if (v === 'lr' || v === 'horizontal') return 'LR';
341
- if (v === 'tb' || v === 'vertical') return 'TB';
342
- return null;
343
- }
344
-
345
342
  /**
346
343
  * Infer arrow color from label text.
347
344
  * Returns a named palette color or undefined if no inference applies.
@@ -350,30 +347,43 @@ export function normalizeDirection(value: string): 'LR' | 'TB' | null {
350
347
  export function inferArrowColor(label: string): string | undefined {
351
348
  const lower = label.toLowerCase();
352
349
  // Green: positive/affirmative
353
- if (lower === 'yes' || lower === 'success' || lower === 'ok' || lower === 'true') return 'green';
350
+ if (
351
+ lower === 'yes' ||
352
+ lower === 'success' ||
353
+ lower === 'ok' ||
354
+ lower === 'true'
355
+ )
356
+ return 'green';
354
357
  // Red: negative/failure
355
- if (lower === 'no' || lower === 'fail' || lower === 'error' || lower === 'false') return 'red';
358
+ if (
359
+ lower === 'no' ||
360
+ lower === 'fail' ||
361
+ lower === 'error' ||
362
+ lower === 'false'
363
+ )
364
+ return 'red';
356
365
  // Orange: uncertain/warning
357
366
  if (lower === 'maybe' || lower === 'warning') return 'orange';
358
367
  return undefined;
359
368
  }
360
369
 
361
- /** Warning message for multiple pipes on a single line. */
362
- export const MULTIPLE_PIPE_WARNING =
370
+ /** Error message for multiple pipes on a single line. */
371
+ export const MULTIPLE_PIPE_ERROR =
363
372
  'Use a single "|" to start metadata, then separate items with commas.';
364
373
 
365
374
  /**
366
375
  * Parse metadata from segments after the first (name) segment.
367
376
  * A single `|` separates the label from metadata; items after the pipe are comma-delimited.
368
- * Multiple pipes are treated as commas for backward compatibility but trigger a warning.
377
+ * Multiple pipes produce an error.
369
378
  */
370
379
  export function parsePipeMetadata(
371
380
  segments: string[],
372
381
  aliasMap: Map<string, string> = new Map(),
373
- warnMultiplePipes?: () => void,
382
+ errorMultiplePipes?: () => void
374
383
  ): Record<string, string> {
375
- if (segments.length > 2 && warnMultiplePipes) {
376
- warnMultiplePipes();
384
+ if (segments.length > 2) {
385
+ if (errorMultiplePipes) errorMultiplePipes();
386
+ return {};
377
387
  }
378
388
  const metadata: Record<string, string> = {};
379
389
  const raw = segments.slice(1).join(',');
@@ -26,25 +26,15 @@ export interface TagBlockMatch {
26
26
  name: string;
27
27
  alias: string | undefined;
28
28
  colorHint: string | undefined;
29
- /** true when the heading used `## …` (deprecated) */
30
- deprecated: boolean;
31
29
  /** Inline tag values parsed from single-line form (e.g., `tag Priority p High(red), Low(blue)`) */
32
30
  inlineValues?: string[];
33
31
  }
34
32
 
35
33
  // ── Regexes ─────────────────────────────────────────────────
36
34
 
37
- /** @deprecated Old syntax: `tag: GroupName [alias X] [(color)]` remove after migration. */
38
- export const TAG_BLOCK_RE =
39
- /^tag:\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/i;
40
-
41
- /** New canonical syntax: line starting with `tag` keyword (no colon). */
35
+ /** Canonical syntax: line starting with `tag` keyword (no colon). */
42
36
  export const TAG_BLOCK_NOCOLON_RE = /^tag\s+/i;
43
37
 
44
- /** @deprecated Legacy syntax: `## GroupName [alias X] [(color)]` */
45
- export const GROUP_HEADING_RE =
46
- /^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
47
-
48
38
  // ── Alias Inference ─────────────────────────────────────────
49
39
 
50
40
  /** Returns true if the token looks like an alias: 1-4 lowercase ASCII characters. */
@@ -54,9 +44,9 @@ function isAliasToken(token: string): boolean {
54
44
 
55
45
  // ── Matchers ────────────────────────────────────────────────
56
46
 
57
- /** Returns true if `trimmed` is a tag block heading in any syntax. */
47
+ /** Returns true if `trimmed` is a tag block heading. */
58
48
  export function isTagBlockHeading(trimmed: string): boolean {
59
- return TAG_BLOCK_NOCOLON_RE.test(trimmed) || TAG_BLOCK_RE.test(trimmed) || GROUP_HEADING_RE.test(trimmed);
49
+ return TAG_BLOCK_NOCOLON_RE.test(trimmed);
60
50
  }
61
51
 
62
52
  /**
@@ -173,7 +163,6 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
173
163
  name,
174
164
  alias,
175
165
  colorHint,
176
- deprecated: false,
177
166
  inlineValues: inlineValues && inlineValues.length > 0 ? inlineValues : undefined,
178
167
  };
179
168
  }
@@ -310,31 +299,5 @@ export function injectDefaultTagMetadata(
310
299
  // ── Matchers ────────────────────────────────────────────────
311
300
 
312
301
  export function matchTagBlockHeading(trimmed: string): TagBlockMatch | null {
313
- // Try new no-colon syntax first: `tag Name [alias] [Values...]`
314
- const nocolonResult = parseTagDeclaration(trimmed);
315
- if (nocolonResult) return nocolonResult;
316
-
317
- // Try old colon syntax: `tag: GroupName [alias X] [(color)]`
318
- const tagMatch = trimmed.match(TAG_BLOCK_RE);
319
- if (tagMatch) {
320
- return {
321
- name: tagMatch[1].trim(),
322
- alias: tagMatch[2] || undefined,
323
- colorHint: tagMatch[3] || undefined,
324
- deprecated: false,
325
- };
326
- }
327
-
328
- // Fall back to legacy ## syntax
329
- const groupMatch = trimmed.match(GROUP_HEADING_RE);
330
- if (groupMatch) {
331
- return {
332
- name: groupMatch[1].trim(),
333
- alias: groupMatch[2] || undefined,
334
- colorHint: groupMatch[3] || undefined,
335
- deprecated: true,
336
- };
337
- }
338
-
339
- return null;
302
+ return parseTagDeclaration(trimmed);
340
303
  }
@@ -1,67 +0,0 @@
1
- // ============================================================
2
- // Infra Scenario Serializer
3
- // ============================================================
4
- //
5
- // Converts interactive overrides into a `scenario:` DSL block.
6
- // Only includes properties that differ from the base diagram.
7
-
8
- import type { ParsedInfra, InfraComputeParams } from './types';
9
-
10
- /**
11
- * Serialize interactive overrides as a DSL `scenario:` block.
12
- * Returns an empty string if nothing differs from the base diagram.
13
- */
14
- export function serializeScenario(name: string, parsed: ParsedInfra, overrides: InfraComputeParams): string {
15
- const lines: string[] = [];
16
-
17
- // Edge RPS override
18
- const edgeNode = parsed.nodes.find((n) => n.isEdge);
19
- if (edgeNode && overrides.rps != null) {
20
- const baseRps = edgeNode.properties.find((p) => p.key === 'rps');
21
- const baseVal = baseRps ? (typeof baseRps.value === 'number' ? baseRps.value : parseFloat(String(baseRps.value)) || 0) : 0;
22
- if (overrides.rps !== baseVal) {
23
- lines.push(` ${edgeNode.id}`);
24
- lines.push(` rps: ${overrides.rps}`);
25
- }
26
- }
27
-
28
- // Instance overrides and property overrides per node
29
- const instanceOv = overrides.instanceOverrides ?? {};
30
- const propOv = overrides.propertyOverrides ?? {};
31
-
32
- for (const node of parsed.nodes) {
33
- if (node.isEdge) continue;
34
-
35
- const nodeLines: string[] = [];
36
-
37
- // Instance override
38
- if (instanceOv[node.id] != null) {
39
- const baseProp = node.properties.find((p) => p.key === 'instances');
40
- const baseVal = baseProp ? (typeof baseProp.value === 'number' ? baseProp.value : parseFloat(String(baseProp.value)) || 1) : 1;
41
- if (instanceOv[node.id] !== baseVal) {
42
- nodeLines.push(` instances: ${instanceOv[node.id]}`);
43
- }
44
- }
45
-
46
- // Property overrides
47
- const nodePropOv = propOv[node.id];
48
- if (nodePropOv) {
49
- for (const [key, val] of Object.entries(nodePropOv)) {
50
- const baseProp = node.properties.find((p) => p.key === key);
51
- const baseVal = baseProp ? (typeof baseProp.value === 'number' ? baseProp.value : parseFloat(String(baseProp.value)) || 0) : 0;
52
- if (val !== baseVal) {
53
- nodeLines.push(` ${key}: ${val}`);
54
- }
55
- }
56
- }
57
-
58
- if (nodeLines.length > 0) {
59
- lines.push(` ${node.id}`);
60
- lines.push(...nodeLines);
61
- }
62
- }
63
-
64
- if (lines.length === 0) return '';
65
-
66
- return `scenario: ${name}\n${lines.join('\n')}\n`;
67
- }