@diagrammo/dgmo 0.6.2 → 0.6.3

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 (44) hide show
  1. package/.claude/commands/dgmo.md +231 -13
  2. package/AGENTS.md +148 -0
  3. package/dist/cli.cjs +327 -153
  4. package/dist/index.cjs +305 -177
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +24 -3
  7. package/dist/index.d.ts +24 -3
  8. package/dist/index.js +303 -177
  9. package/dist/index.js.map +1 -1
  10. package/package.json +5 -3
  11. package/src/c4/layout.ts +0 -5
  12. package/src/c4/parser.ts +0 -16
  13. package/src/c4/renderer.ts +1 -5
  14. package/src/class/layout.ts +0 -1
  15. package/src/class/parser.ts +28 -0
  16. package/src/class/renderer.ts +5 -26
  17. package/src/cli.ts +563 -14
  18. package/src/completion.ts +58 -0
  19. package/src/d3.ts +58 -106
  20. package/src/dgmo-router.ts +0 -57
  21. package/src/echarts.ts +96 -55
  22. package/src/er/parser.ts +30 -1
  23. package/src/er/renderer.ts +1 -2
  24. package/src/graph/flowchart-parser.ts +27 -4
  25. package/src/graph/flowchart-renderer.ts +1 -2
  26. package/src/graph/state-parser.ts +0 -1
  27. package/src/graph/state-renderer.ts +1 -3
  28. package/src/index.ts +10 -0
  29. package/src/infra/compute.ts +0 -7
  30. package/src/infra/layout.ts +0 -2
  31. package/src/infra/parser.ts +46 -4
  32. package/src/infra/renderer.ts +1 -15
  33. package/src/initiative-status/renderer.ts +5 -25
  34. package/src/kanban/parser.ts +0 -2
  35. package/src/org/layout.ts +0 -4
  36. package/src/org/renderer.ts +7 -28
  37. package/src/sequence/parser.ts +14 -11
  38. package/src/sequence/renderer.ts +0 -2
  39. package/src/sequence/tag-resolution.ts +0 -1
  40. package/src/sitemap/layout.ts +1 -14
  41. package/src/sitemap/parser.ts +1 -2
  42. package/src/sitemap/renderer.ts +0 -3
  43. package/src/utils/arrows.ts +7 -7
  44. package/src/utils/export-container.ts +40 -0
package/src/echarts.ts CHANGED
@@ -86,6 +86,7 @@ export interface ParsedExtendedChart {
86
86
 
87
87
  import type { PaletteColors } from './palettes';
88
88
  import { getSeriesColors, getSegmentColors } from './palettes';
89
+ import { mix } from './palettes/color-utils';
89
90
  import { parseChart } from './chart';
90
91
  import type { ParsedChart, ChartEra } from './chart';
91
92
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
@@ -98,6 +99,7 @@ import { collectIndentedValues, extractColor, measureIndent, parseSeriesNames }
98
99
 
99
100
  const EMPHASIS_SELF = { focus: 'self' as const, blurScope: 'global' as const };
100
101
  const CHART_BASE: Pick<EChartsOption, 'backgroundColor' | 'animation'> = { backgroundColor: 'transparent', animation: false };
102
+ const CHART_BORDER_WIDTH = 2;
101
103
 
102
104
  // ============================================================
103
105
  // Parser
@@ -487,10 +489,12 @@ export function buildExtendedChartOption(
487
489
 
488
490
  // Chord diagram
489
491
  if (parsed.type === 'chord') {
492
+ const bg = isDark ? palette.surface : palette.bg;
490
493
  return buildChordOption(
491
494
  parsed,
492
495
  textColor,
493
496
  colors,
497
+ bg,
494
498
  titleConfig,
495
499
  tooltipTheme
496
500
  );
@@ -512,6 +516,7 @@ export function buildExtendedChartOption(
512
516
 
513
517
  // Scatter plot
514
518
  if (parsed.type === 'scatter') {
519
+ const bg = isDark ? palette.surface : palette.bg;
515
520
  return buildScatterOption(
516
521
  parsed,
517
522
  palette,
@@ -519,6 +524,7 @@ export function buildExtendedChartOption(
519
524
  axisLineColor,
520
525
  gridOpacity,
521
526
  colors,
527
+ bg,
522
528
  titleConfig,
523
529
  tooltipTheme
524
530
  );
@@ -526,10 +532,12 @@ export function buildExtendedChartOption(
526
532
 
527
533
  // Funnel chart
528
534
  if (parsed.type === 'funnel') {
535
+ const bg = isDark ? palette.surface : palette.bg;
529
536
  return buildFunnelOption(
530
537
  parsed,
531
538
  textColor,
532
539
  colors,
540
+ bg,
533
541
  titleConfig,
534
542
  tooltipTheme
535
543
  );
@@ -539,6 +547,7 @@ export function buildExtendedChartOption(
539
547
  return buildHeatmapOption(
540
548
  parsed,
541
549
  palette,
550
+ isDark,
542
551
  textColor,
543
552
  axisLineColor,
544
553
  titleConfig,
@@ -616,6 +625,7 @@ function buildChordOption(
616
625
  parsed: ParsedExtendedChart,
617
626
  textColor: string,
618
627
  colors: string[],
628
+ bg: string,
619
629
  titleConfig: EChartsOption['title'],
620
630
  tooltipTheme: Record<string, unknown>
621
631
  ): EChartsOption {
@@ -647,12 +657,13 @@ function buildChordOption(
647
657
  }
648
658
 
649
659
  // Create category data for nodes with colors
650
- const categories = nodeNames.map((name, index) => ({
651
- name,
652
- itemStyle: {
653
- color: colors[index % colors.length],
654
- },
655
- }));
660
+ const categories = nodeNames.map((name, index) => {
661
+ const stroke = colors[index % colors.length];
662
+ return {
663
+ name,
664
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
665
+ };
666
+ });
656
667
 
657
668
  return {
658
669
  ...CHART_BASE,
@@ -876,6 +887,7 @@ function buildScatterOption(
876
887
  axisLineColor: string,
877
888
  gridOpacity: number,
878
889
  colors: string[],
890
+ bg: string,
879
891
  titleConfig: EChartsOption['title'],
880
892
  tooltipTheme: Record<string, unknown>
881
893
  ): EChartsOption {
@@ -919,7 +931,9 @@ function buildScatterOption(
919
931
  const data = categoryPoints.map((p) => ({
920
932
  name: p.name,
921
933
  value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
922
- ...(p.color && { itemStyle: { color: p.color } }),
934
+ ...(p.color && {
935
+ itemStyle: { color: mix(p.color, bg, 30), borderColor: p.color, borderWidth: CHART_BORDER_WIDTH },
936
+ }),
923
937
  }));
924
938
 
925
939
  return {
@@ -929,23 +943,24 @@ function buildScatterOption(
929
943
  ...(hasSize
930
944
  ? { symbolSize: (val: number[]) => val[2] }
931
945
  : { symbolSize: defaultSize }),
932
- itemStyle: { color: catColor },
946
+ itemStyle: { color: mix(catColor, bg, 30), borderColor: catColor, borderWidth: CHART_BORDER_WIDTH },
933
947
  label: labelConfig,
934
948
  emphasis: emphasisConfig,
935
949
  };
936
950
  });
937
951
  } else {
938
952
  // Single series — per-point colors
939
- const data = points.map((p, index) => ({
940
- name: p.name,
941
- value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
942
- ...(hasSize
943
- ? { symbolSize: p.size ?? defaultSize }
944
- : { symbolSize: defaultSize }),
945
- itemStyle: {
946
- color: p.color ?? colors[index % colors.length],
947
- },
948
- }));
953
+ const data = points.map((p, index) => {
954
+ const stroke = p.color ?? colors[index % colors.length];
955
+ return {
956
+ name: p.name,
957
+ value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
958
+ ...(hasSize
959
+ ? { symbolSize: p.size ?? defaultSize }
960
+ : { symbolSize: defaultSize }),
961
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
962
+ };
963
+ });
949
964
 
950
965
  series = [
951
966
  {
@@ -1065,11 +1080,13 @@ function buildScatterOption(
1065
1080
  function buildHeatmapOption(
1066
1081
  parsed: ParsedExtendedChart,
1067
1082
  palette: PaletteColors,
1083
+ isDark: boolean,
1068
1084
  textColor: string,
1069
1085
  axisLineColor: string,
1070
1086
  titleConfig: EChartsOption['title'],
1071
1087
  tooltipTheme: Record<string, unknown>
1072
1088
  ): EChartsOption {
1089
+ const bg = isDark ? palette.surface : palette.bg;
1073
1090
  const heatmapRows = parsed.heatmapRows ?? [];
1074
1091
  const columns = parsed.columns ?? [];
1075
1092
  const rowLabels = heatmapRows.map((r) => r.label);
@@ -1144,10 +1161,10 @@ function buildHeatmapOption(
1144
1161
  top: 'center',
1145
1162
  inRange: {
1146
1163
  color: [
1147
- palette.primary,
1148
- palette.colors.cyan,
1149
- palette.colors.yellow,
1150
- palette.colors.orange,
1164
+ mix(palette.primary, bg, 30),
1165
+ mix(palette.colors.cyan, bg, 30),
1166
+ mix(palette.colors.yellow, bg, 30),
1167
+ mix(palette.colors.orange, bg, 30),
1151
1168
  ],
1152
1169
  },
1153
1170
  textStyle: {
@@ -1158,9 +1175,13 @@ function buildHeatmapOption(
1158
1175
  {
1159
1176
  type: 'heatmap',
1160
1177
  data,
1178
+ itemStyle: {
1179
+ borderWidth: 2,
1180
+ borderColor: bg,
1181
+ },
1161
1182
  label: {
1162
1183
  show: true,
1163
- color: '#ffffff',
1184
+ color: textColor,
1164
1185
  fontSize: 14,
1165
1186
  fontWeight: 'bold' as const,
1166
1187
  },
@@ -1183,6 +1204,7 @@ function buildFunnelOption(
1183
1204
  parsed: ParsedExtendedChart,
1184
1205
  textColor: string,
1185
1206
  colors: string[],
1207
+ bg: string,
1186
1208
  titleConfig: EChartsOption['title'],
1187
1209
  tooltipTheme: Record<string, unknown>
1188
1210
  ): EChartsOption {
@@ -1190,14 +1212,18 @@ function buildFunnelOption(
1190
1212
  const sorted = [...parsed.data].sort((a, b) => b.value - a.value);
1191
1213
  const topValue = sorted.length > 0 ? sorted[0].value : 1;
1192
1214
 
1193
- const data = sorted.map((d) => ({
1194
- name: d.label,
1195
- value: d.value,
1196
- itemStyle: {
1197
- color: d.color ?? colors[parsed.data.indexOf(d) % colors.length],
1198
- borderWidth: 0,
1199
- },
1200
- }));
1215
+ const data = sorted.map((d) => {
1216
+ const stroke = d.color ?? colors[parsed.data.indexOf(d) % colors.length];
1217
+ return {
1218
+ name: d.label,
1219
+ value: d.value,
1220
+ itemStyle: {
1221
+ color: mix(stroke, bg, 30),
1222
+ borderColor: stroke,
1223
+ borderWidth: CHART_BORDER_WIDTH,
1224
+ },
1225
+ };
1226
+ });
1201
1227
 
1202
1228
  // Build lookup for tooltip: previous step value (in sorted order)
1203
1229
  const prevValueMap = new Map<string, number>();
@@ -1394,12 +1420,13 @@ export function buildSimpleChartOption(
1394
1420
  if (parsed.error) return {};
1395
1421
 
1396
1422
  const { textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme } = buildChartCommons(parsed, palette, isDark);
1423
+ const bg = isDark ? palette.surface : palette.bg;
1397
1424
 
1398
1425
  switch (parsed.type) {
1399
1426
  case 'bar':
1400
- return buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth);
1427
+ return buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth);
1401
1428
  case 'bar-stacked':
1402
- return buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth);
1429
+ return buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth);
1403
1430
  case 'line':
1404
1431
  return parsed.seriesNames
1405
1432
  ? buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth)
@@ -1407,13 +1434,13 @@ export function buildSimpleChartOption(
1407
1434
  case 'area':
1408
1435
  return buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth);
1409
1436
  case 'pie':
1410
- return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), titleConfig, tooltipTheme, false);
1437
+ return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme, false);
1411
1438
  case 'doughnut':
1412
- return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), titleConfig, tooltipTheme, true);
1439
+ return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme, true);
1413
1440
  case 'radar':
1414
- return buildRadarOption(parsed, palette, textColor, gridOpacity, colors, titleConfig, tooltipTheme);
1441
+ return buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig, tooltipTheme);
1415
1442
  case 'polar-area':
1416
- return buildPolarAreaOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), titleConfig, tooltipTheme);
1443
+ return buildPolarAreaOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme);
1417
1444
  }
1418
1445
  }
1419
1446
 
@@ -1439,6 +1466,7 @@ function buildBarOption(
1439
1466
  splitLineColor: string,
1440
1467
  gridOpacity: number,
1441
1468
  colors: string[],
1469
+ bg: string,
1442
1470
  titleConfig: EChartsOption['title'],
1443
1471
  tooltipTheme: Record<string, unknown>,
1444
1472
  chartWidth?: number
@@ -1446,10 +1474,13 @@ function buildBarOption(
1446
1474
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
1447
1475
  const isHorizontal = parsed.orientation === 'horizontal';
1448
1476
  const labels = parsed.data.map((d) => d.label);
1449
- const data = parsed.data.map((d, i) => ({
1450
- value: d.value,
1451
- itemStyle: { color: d.color ?? colors[i % colors.length] },
1452
- }));
1477
+ const data = parsed.data.map((d, i) => {
1478
+ const stroke = d.color ?? colors[i % colors.length];
1479
+ return {
1480
+ value: d.value,
1481
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1482
+ };
1483
+ });
1453
1484
 
1454
1485
  // When category labels are on the y-axis (horizontal bars), they can be wide —
1455
1486
  // compute a nameGap that clears the longest label so the ylabel doesn't overlap.
@@ -1701,15 +1732,19 @@ function buildPieOption(
1701
1732
  parsed: ParsedChart,
1702
1733
  textColor: string,
1703
1734
  colors: string[],
1735
+ bg: string,
1704
1736
  titleConfig: EChartsOption['title'],
1705
1737
  tooltipTheme: Record<string, unknown>,
1706
1738
  isDoughnut: boolean
1707
1739
  ): EChartsOption {
1708
- const data = parsed.data.map((d, i) => ({
1709
- name: d.label,
1710
- value: d.value,
1711
- itemStyle: { color: d.color ?? colors[i % colors.length] },
1712
- }));
1740
+ const data = parsed.data.map((d, i) => {
1741
+ const stroke = d.color ?? colors[i % colors.length];
1742
+ return {
1743
+ name: d.label,
1744
+ value: d.value,
1745
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1746
+ };
1747
+ });
1713
1748
 
1714
1749
  return {
1715
1750
  ...CHART_BASE,
@@ -1741,12 +1776,13 @@ function buildPieOption(
1741
1776
  function buildRadarOption(
1742
1777
  parsed: ParsedChart,
1743
1778
  palette: PaletteColors,
1779
+ isDark: boolean,
1744
1780
  textColor: string,
1745
1781
  gridOpacity: number,
1746
- colors: string[],
1747
1782
  titleConfig: EChartsOption['title'],
1748
1783
  tooltipTheme: Record<string, unknown>
1749
1784
  ): EChartsOption {
1785
+ const bg = isDark ? palette.surface : palette.bg;
1750
1786
  const radarColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
1751
1787
  const values = parsed.data.map((d) => d.value);
1752
1788
  const maxValue = Math.max(...values) * 1.15;
@@ -1785,7 +1821,7 @@ function buildRadarOption(
1785
1821
  {
1786
1822
  value: values,
1787
1823
  name: parsed.series ?? 'Value',
1788
- areaStyle: { color: radarColor, opacity: 0.25 },
1824
+ areaStyle: { color: mix(radarColor, bg, 30) },
1789
1825
  lineStyle: { color: radarColor },
1790
1826
  itemStyle: { color: radarColor },
1791
1827
  symbol: 'circle',
@@ -1811,14 +1847,18 @@ function buildPolarAreaOption(
1811
1847
  parsed: ParsedChart,
1812
1848
  textColor: string,
1813
1849
  colors: string[],
1850
+ bg: string,
1814
1851
  titleConfig: EChartsOption['title'],
1815
1852
  tooltipTheme: Record<string, unknown>
1816
1853
  ): EChartsOption {
1817
- const data = parsed.data.map((d, i) => ({
1818
- name: d.label,
1819
- value: d.value,
1820
- itemStyle: { color: d.color ?? colors[i % colors.length] },
1821
- }));
1854
+ const data = parsed.data.map((d, i) => {
1855
+ const stroke = d.color ?? colors[i % colors.length];
1856
+ return {
1857
+ name: d.label,
1858
+ value: d.value,
1859
+ itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1860
+ };
1861
+ });
1822
1862
 
1823
1863
  return {
1824
1864
  ...CHART_BASE,
@@ -1855,6 +1895,7 @@ function buildBarStackedOption(
1855
1895
  splitLineColor: string,
1856
1896
  gridOpacity: number,
1857
1897
  colors: string[],
1898
+ bg: string,
1858
1899
  titleConfig: EChartsOption['title'],
1859
1900
  tooltipTheme: Record<string, unknown>,
1860
1901
  chartWidth?: number
@@ -1874,12 +1915,12 @@ function buildBarStackedOption(
1874
1915
  type: 'bar' as const,
1875
1916
  stack: 'total',
1876
1917
  data,
1877
- itemStyle: { color },
1918
+ itemStyle: { color: mix(color, bg, 30), borderColor: color, borderWidth: CHART_BORDER_WIDTH },
1878
1919
  label: {
1879
1920
  show: true,
1880
1921
  position: 'inside' as const,
1881
1922
  formatter: '{c}',
1882
- color: '#ffffff',
1923
+ color: textColor,
1883
1924
  fontSize: 14,
1884
1925
  fontWeight: 'bold' as const,
1885
1926
  fontFamily: FONT_FAMILY,
package/src/er/parser.ts CHANGED
@@ -7,7 +7,6 @@ import type { TagGroup } from '../utils/tag-groups';
7
7
  import type {
8
8
  ParsedERDiagram,
9
9
  ERTable,
10
- ERColumn,
11
10
  ERConstraint,
12
11
  ERCardinality,
13
12
  } from './types';
@@ -463,3 +462,33 @@ export function looksLikeERDiagram(content: string): boolean {
463
462
 
464
463
  return false;
465
464
  }
465
+
466
+ // ============================================================
467
+ // Symbol extraction (for completion API)
468
+ // ============================================================
469
+
470
+ import type { DiagramSymbols } from '../completion';
471
+
472
+ /**
473
+ * Extract table names (entities) and ER keywords from document text.
474
+ * Used by the dgmo completion API for ghost hints and popup completions.
475
+ */
476
+ export function extractSymbols(docText: string): DiagramSymbols {
477
+ const entities: string[] = [];
478
+ let inMetadata = true;
479
+ for (const rawLine of docText.split('\n')) {
480
+ const line = rawLine.trim();
481
+ if (inMetadata && /^chart\s*:/i.test(line)) continue;
482
+ if (inMetadata && /^[a-z-]+\s*:/i.test(line)) continue; // metadata key
483
+ inMetadata = false;
484
+ if (line.length === 0) continue;
485
+ if (/^\s/.test(rawLine)) continue; // indented = column definition, not table
486
+ const m = TABLE_DECL_RE.exec(line);
487
+ if (m) entities.push(m[1]!);
488
+ }
489
+ return {
490
+ kind: 'er',
491
+ entities,
492
+ keywords: ['pk', 'fk', 'unique', 'nullable', '1', '*', '?'],
493
+ };
494
+ }
@@ -17,13 +17,12 @@ import {
17
17
  LEGEND_CAPSULE_PAD,
18
18
  LEGEND_DOT_R,
19
19
  LEGEND_ENTRY_FONT_SIZE,
20
- LEGEND_ENTRY_FONT_W,
21
20
  LEGEND_ENTRY_DOT_GAP,
22
21
  LEGEND_ENTRY_TRAIL,
23
22
  LEGEND_GROUP_GAP,
24
23
  } from '../utils/legend-constants';
25
24
  import type { ParsedERDiagram, ERConstraint } from './types';
26
- import type { ERLayoutResult, ERLayoutNode, ERLayoutEdge } from './layout';
25
+ import type { ERLayoutResult } from './layout';
27
26
  import { parseERDiagram } from './parser';
28
27
  import { layoutERDiagram } from './layout';
29
28
  import { classifyEREntities, ROLE_COLORS, ROLE_LABELS, ROLE_ORDER } from './classify';
@@ -90,10 +90,6 @@ function parseNodeRef(
90
90
  */
91
91
  function splitArrows(line: string): string[] {
92
92
  const segments: string[] = [];
93
- // Match: optional `-label(color)->` or just `->`
94
- // We scan left to right looking for `->` and work backwards to find the `-` start.
95
- const arrowRe = /(?:^|\s)-([^>\s(][^(>]*?)?\s*(?:\(([^)]+)\))?\s*->|(?:^|\s)->/g;
96
-
97
93
  let lastIndex = 0;
98
94
  // Simpler approach: find all `->` positions, then determine if there's a label prefix
99
95
  const arrowPositions: { start: number; end: number; label?: string; color?: string }[] = [];
@@ -482,3 +478,30 @@ export function looksLikeFlowchart(content: string): boolean {
482
478
 
483
479
  return shapeNearArrow;
484
480
  }
481
+
482
+ // ============================================================
483
+ // Symbol extraction (for completion API)
484
+ // ============================================================
485
+
486
+ import type { DiagramSymbols } from '../completion';
487
+
488
+ // Node ID: identifier at line start followed by a shape delimiter or space (arrow line)
489
+ const NODE_ID_RE = /^([a-zA-Z_][\w-]*)[\s([</{]/;
490
+
491
+ /**
492
+ * Extract node IDs (entities) from flowchart document text.
493
+ * Used by the dgmo completion API for ghost hints and popup completions.
494
+ */
495
+ export function extractSymbols(docText: string): DiagramSymbols {
496
+ const entities: string[] = [];
497
+ let inMetadata = true;
498
+ for (const rawLine of docText.split('\n')) {
499
+ const line = rawLine.trim();
500
+ if (inMetadata && /^[a-z-]+\s*:/i.test(line)) continue;
501
+ inMetadata = false;
502
+ if (line.length === 0 || /^\s/.test(rawLine)) continue;
503
+ const m = NODE_ID_RE.exec(line);
504
+ if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
505
+ }
506
+ return { kind: 'flowchart', entities, keywords: [] };
507
+ }
@@ -8,7 +8,7 @@ import { FONT_FAMILY } from '../fonts';
8
8
  import type { PaletteColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
10
  import type { ParsedGraph, GraphShape } from './types';
11
- import type { LayoutResult, LayoutNode, LayoutEdge } from './layout';
11
+ import type { LayoutResult, LayoutNode } from './layout';
12
12
  import { parseFlowchart } from './flowchart-parser';
13
13
  import { layoutGraph } from './layout';
14
14
 
@@ -246,7 +246,6 @@ export function renderFlowchart(
246
246
 
247
247
  // Center the diagram in the area below the title
248
248
  const scaledW = diagramW * scale;
249
- const scaledH = diagramH * scale;
250
249
  const offsetX = (width - scaledW) / 2;
251
250
  const offsetY = titleHeight + DIAGRAM_PADDING;
252
251
 
@@ -5,7 +5,6 @@ import { measureIndent, extractColor } from '../utils/parsing';
5
5
  import type {
6
6
  ParsedGraph,
7
7
  GraphNode,
8
- GraphEdge,
9
8
  GraphGroup,
10
9
  GraphDirection,
11
10
  } from './types';
@@ -8,7 +8,7 @@ import { FONT_FAMILY } from '../fonts';
8
8
  import type { PaletteColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
10
  import type { ParsedGraph } from './types';
11
- import type { LayoutResult, LayoutNode, LayoutEdge } from './layout';
11
+ import type { LayoutResult, LayoutNode } from './layout';
12
12
  import { parseState } from './state-parser';
13
13
  import { layoutGraph } from './layout';
14
14
 
@@ -76,8 +76,6 @@ function selfLoopPath(node: LayoutNode): string {
76
76
  // Main renderer
77
77
  // ============================================================
78
78
 
79
- type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
80
-
81
79
  export function renderState(
82
80
  container: HTMLDivElement,
83
81
  graph: ParsedGraph,
package/src/index.ts CHANGED
@@ -361,6 +361,16 @@ export type {
361
361
  DecodedDiagramUrl,
362
362
  } from './sharing';
363
363
 
364
+ // ============================================================
365
+ // Completion (symbol extraction API)
366
+ // ============================================================
367
+
368
+ export {
369
+ registerExtractor,
370
+ extractDiagramSymbols,
371
+ } from './completion';
372
+ export type { DiagramSymbols, ExtractFn } from './completion';
373
+
364
374
  // ============================================================
365
375
  // Branding
366
376
  // ============================================================
@@ -22,8 +22,6 @@ import type {
22
22
  InfraAvailabilityPercentiles,
23
23
  InfraProperty,
24
24
  } from './types';
25
- import { INFRA_BEHAVIOR_KEYS } from './types';
26
-
27
25
  // ============================================================
28
26
  // Helpers
29
27
  // ============================================================
@@ -71,11 +69,6 @@ function serverlessCapacity(node: InfraNode): number {
71
69
  return concurrency / (durationMs / 1000);
72
70
  }
73
71
 
74
- /** Backward-compatible helper used by overload detection. */
75
- function getInstances(node: InfraNode): number {
76
- return getInstanceRange(node).min;
77
- }
78
-
79
72
  /** Compute dynamic instance count based on load and max-rps. */
80
73
  function computeDynamicInstances(node: InfraNode, computedRps: number): number {
81
74
  const { min, max } = getInstanceRange(node);
@@ -9,8 +9,6 @@ import dagre from '@dagrejs/dagre';
9
9
  import type {
10
10
  ComputedInfraModel,
11
11
  ComputedInfraNode,
12
- ComputedInfraEdge,
13
- InfraGroup,
14
12
  } from './types';
15
13
 
16
14
  // ============================================================
@@ -11,11 +11,8 @@ import { measureIndent } from '../utils/parsing';
11
11
  import type {
12
12
  ParsedInfra,
13
13
  InfraNode,
14
- InfraEdge,
15
14
  InfraGroup,
16
15
  InfraTagGroup,
17
- InfraTagValue,
18
- InfraProperty,
19
16
  } from './types';
20
17
  import { INFRA_BEHAVIOR_KEYS, EDGE_ONLY_KEYS } from './types';
21
18
 
@@ -116,7 +113,6 @@ export function parseInfra(content: string): ParsedInfra {
116
113
  };
117
114
 
118
115
  const nodeMap = new Map<string, InfraNode>();
119
- const edgeNodeId = 'edge';
120
116
 
121
117
  const setError = (line: number, message: string) => {
122
118
  const diag = makeDgmoError(line, message);
@@ -573,3 +569,49 @@ export function parseInfra(content: string): ParsedInfra {
573
569
 
574
570
  return result;
575
571
  }
572
+
573
+ // ============================================================
574
+ // Symbol extraction (for completion API)
575
+ // ============================================================
576
+
577
+ import type { DiagramSymbols } from '../completion';
578
+
579
+ /**
580
+ * Extract component names (entities) from infra document text.
581
+ * Used by the dgmo completion API for ghost hints and popup completions.
582
+ */
583
+ export function extractSymbols(docText: string): DiagramSymbols {
584
+ const entities: string[] = [];
585
+ let inMetadata = true;
586
+ let inTagGroup = false;
587
+ for (const rawLine of docText.split('\n')) {
588
+ const line = rawLine.trim();
589
+ if (line.length === 0) continue;
590
+ const indented = /^\s/.test(rawLine);
591
+
592
+ // Metadata phase: skip until first non-metadata root-level line.
593
+ // All lines (including indented) are skipped while inMetadata = true.
594
+ if (inMetadata) {
595
+ if (!indented && !/^[a-z-]+\s*:/i.test(line)) inMetadata = false;
596
+ else continue;
597
+ }
598
+
599
+ if (!indented) {
600
+ // Root-level: tag group declaration, group header, or component
601
+ if (/^tag\s*:/i.test(line)) { inTagGroup = true; continue; }
602
+ inTagGroup = false;
603
+ if (/^\[/.test(line)) continue; // group header
604
+ const m = COMPONENT_RE.exec(line);
605
+ if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
606
+ } else {
607
+ // Indented: skip tag values, connections, and properties; extract grouped components
608
+ if (inTagGroup) continue;
609
+ if (/^->/.test(line)) continue; // simple connection
610
+ if (/^-[^>]+-?>/.test(line)) continue; // labeled connection
611
+ if (/^\w[\w-]*\s*:/.test(line)) continue; // property (key: value)
612
+ const m = COMPONENT_RE.exec(line);
613
+ if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
614
+ }
615
+ }
616
+ return { kind: 'infra', entities, keywords: [] };
617
+ }