@diagrammo/dgmo 0.6.2 → 0.7.0

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 (61) hide show
  1. package/.claude/commands/dgmo.md +231 -13
  2. package/AGENTS.md +148 -0
  3. package/dist/cli.cjs +341 -165
  4. package/dist/index.cjs +4900 -1685
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +259 -18
  7. package/dist/index.d.ts +259 -18
  8. package/dist/index.js +4642 -1436
  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 +7 -11
  14. package/src/class/layout.ts +0 -1
  15. package/src/class/parser.ts +28 -0
  16. package/src/class/renderer.ts +189 -34
  17. package/src/cli.ts +566 -25
  18. package/src/colors.ts +3 -3
  19. package/src/completion.ts +58 -0
  20. package/src/d3.ts +179 -122
  21. package/src/dgmo-router.ts +3 -58
  22. package/src/echarts.ts +96 -55
  23. package/src/er/parser.ts +30 -1
  24. package/src/er/renderer.ts +12 -7
  25. package/src/gantt/calculator.ts +677 -0
  26. package/src/gantt/parser.ts +761 -0
  27. package/src/gantt/renderer.ts +2125 -0
  28. package/src/gantt/resolver.ts +144 -0
  29. package/src/gantt/types.ts +168 -0
  30. package/src/graph/flowchart-parser.ts +27 -4
  31. package/src/graph/flowchart-renderer.ts +1 -2
  32. package/src/graph/state-parser.ts +0 -1
  33. package/src/graph/state-renderer.ts +1 -3
  34. package/src/index.ts +37 -0
  35. package/src/infra/compute.ts +0 -7
  36. package/src/infra/layout.ts +0 -2
  37. package/src/infra/parser.ts +46 -4
  38. package/src/infra/renderer.ts +49 -27
  39. package/src/initiative-status/filter.ts +63 -0
  40. package/src/initiative-status/layout.ts +319 -67
  41. package/src/initiative-status/parser.ts +200 -25
  42. package/src/initiative-status/renderer.ts +298 -35
  43. package/src/initiative-status/types.ts +6 -0
  44. package/src/kanban/parser.ts +0 -2
  45. package/src/org/layout.ts +22 -59
  46. package/src/org/renderer.ts +11 -36
  47. package/src/palettes/dracula.ts +60 -0
  48. package/src/palettes/index.ts +8 -6
  49. package/src/palettes/monokai.ts +60 -0
  50. package/src/palettes/registry.ts +4 -2
  51. package/src/sequence/parser.ts +14 -11
  52. package/src/sequence/renderer.ts +5 -6
  53. package/src/sequence/tag-resolution.ts +0 -1
  54. package/src/sharing.ts +8 -0
  55. package/src/sitemap/layout.ts +1 -14
  56. package/src/sitemap/parser.ts +1 -2
  57. package/src/sitemap/renderer.ts +4 -7
  58. package/src/utils/arrows.ts +7 -7
  59. package/src/utils/duration.ts +212 -0
  60. package/src/utils/export-container.ts +40 -0
  61. package/src/utils/legend-constants.ts +1 -0
@@ -0,0 +1,144 @@
1
+ // ============================================================
2
+ // Gantt Dot-Notation Task Resolver
3
+ // ============================================================
4
+ //
5
+ // Resolves `-> TargetName` dependency references to actual tasks.
6
+ // Implements greedy right-to-left dot splitting for disambiguation.
7
+
8
+ import type { GanttTask, GanttNode } from './types';
9
+
10
+ export interface ResolverMatch {
11
+ task: GanttTask;
12
+ }
13
+
14
+ export interface ResolverError {
15
+ kind: 'not_found' | 'ambiguous';
16
+ message: string;
17
+ }
18
+
19
+ export type ResolverResult = ResolverMatch | ResolverError;
20
+
21
+ export function isResolverError(r: ResolverResult): r is ResolverError {
22
+ return 'kind' in r;
23
+ }
24
+
25
+ /**
26
+ * Collect all tasks from a tree of GanttNodes, annotating each with its
27
+ * fully qualified group path (e.g., ["Backend", "API"]).
28
+ */
29
+ export function collectTasks(nodes: GanttNode[]): GanttTask[] {
30
+ const tasks: GanttTask[] = [];
31
+ function walk(children: GanttNode[]) {
32
+ for (const node of children) {
33
+ if (node.kind === 'task') {
34
+ tasks.push(node);
35
+ } else if (node.kind === 'group' || node.kind === 'parallel') {
36
+ walk(node.children);
37
+ }
38
+ }
39
+ }
40
+ walk(nodes);
41
+ return tasks;
42
+ }
43
+
44
+ /**
45
+ * Resolve a dependency target name to a task.
46
+ *
47
+ * Resolution strategy (greedy right-to-left):
48
+ * 1. Try the full string as an exact task label match
49
+ * 2. If no match, split at the last dot → group prefix + task label
50
+ * 3. Recurse for deeper paths
51
+ *
52
+ * Returns a match or an error with helpful suggestions.
53
+ */
54
+ export function resolveTaskName(
55
+ name: string,
56
+ allTasks: GanttTask[],
57
+ ): ResolverResult {
58
+ const trimmed = name.trim();
59
+
60
+ // 1. Try exact label match (no dots involved)
61
+ const exactMatches = allTasks.filter(t => t.label === trimmed);
62
+ if (exactMatches.length === 1) {
63
+ return { task: exactMatches[0] };
64
+ }
65
+ if (exactMatches.length > 1) {
66
+ // Multiple tasks with same name — need disambiguation
67
+ const suggestions = exactMatches.map(t =>
68
+ t.groupPath.length > 0 ? `${t.groupPath.join('.')}.${t.label}` : t.label
69
+ );
70
+ return {
71
+ kind: 'ambiguous',
72
+ message: `Multiple tasks match "${trimmed}". Did you mean ${suggestions.map(s => `\`${s}\``).join(' or ')}?`,
73
+ };
74
+ }
75
+
76
+ // 2. Try dot-notation: split at last dot (greedy right-to-left)
77
+ const lastDotIdx = trimmed.lastIndexOf('.');
78
+ if (lastDotIdx > 0) {
79
+ const groupPrefix = trimmed.substring(0, lastDotIdx);
80
+ const taskLabel = trimmed.substring(lastDotIdx + 1);
81
+
82
+ // Find tasks whose label matches and whose group path ends with the prefix
83
+ const matches = allTasks.filter(t => {
84
+ if (t.label !== taskLabel) return false;
85
+ return matchesGroupPath(t.groupPath, groupPrefix);
86
+ });
87
+
88
+ if (matches.length === 1) {
89
+ return { task: matches[0] };
90
+ }
91
+ if (matches.length > 1) {
92
+ const suggestions = matches.map(t =>
93
+ t.groupPath.length > 0 ? `${t.groupPath.join('.')}.${t.label}` : t.label
94
+ );
95
+ return {
96
+ kind: 'ambiguous',
97
+ message: `Multiple tasks match "${trimmed}". Did you mean ${suggestions.map(s => `\`${s}\``).join(' or ')}?`,
98
+ };
99
+ }
100
+
101
+ // Try further left splits (for dots in group names)
102
+ // e.g., "U.S. Operations.Task A" — last dot split tried "U.S. Operations" + "Task A"
103
+ // Now try "U.S." + "Operations.Task A" — but that doesn't help.
104
+ // The greedy approach handles this: "U.S. Operations" is the group name.
105
+ // If the group name itself contains dots, the last dot split already tried the correct split.
106
+ }
107
+
108
+ // 3. No match found — try case-insensitive as a fallback for suggestions
109
+ const caseInsensitive = allTasks.filter(t =>
110
+ t.label.toLowerCase() === trimmed.toLowerCase()
111
+ );
112
+ if (caseInsensitive.length > 0) {
113
+ return {
114
+ kind: 'not_found',
115
+ message: `No task found with name "${trimmed}". Did you mean "${caseInsensitive[0].label}" (case mismatch)?`,
116
+ };
117
+ }
118
+
119
+ return {
120
+ kind: 'not_found',
121
+ message: `No task found with name "${trimmed}".`,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Check if a task's group path matches a dot-separated prefix.
127
+ * The prefix can be a single group name or a dot-separated path.
128
+ * Matching is done from the end of the group path.
129
+ *
130
+ * Example: groupPath = ["Backend", "API"], prefix = "Backend" → true
131
+ * Example: groupPath = ["Backend", "API"], prefix = "API" → true
132
+ * Example: groupPath = ["Backend", "API"], prefix = "Backend.API" → true
133
+ */
134
+ function matchesGroupPath(groupPath: string[], prefix: string): boolean {
135
+ // Simple case: prefix is a single segment
136
+ if (!prefix.includes('.')) {
137
+ return groupPath.some(g => g === prefix);
138
+ }
139
+
140
+ // Multi-segment prefix: try matching from the start of the group path
141
+ const pathStr = groupPath.join('.');
142
+ // Check if the full prefix matches any contiguous section of the path
143
+ return pathStr === prefix || pathStr.endsWith('.' + prefix) || pathStr.startsWith(prefix + '.') || pathStr.includes('.' + prefix + '.');
144
+ }
@@ -0,0 +1,168 @@
1
+ // ============================================================
2
+ // Gantt Chart Types
3
+ // ============================================================
4
+
5
+ import type { DgmoError } from '../diagnostics';
6
+ import type { TagGroup } from '../utils/tag-groups';
7
+
8
+ // ── Duration ────────────────────────────────────────────────
9
+
10
+ /** Calendar units: d (days), w (weeks), m (months), q (quarters), y (years). bd = business days. */
11
+ export type DurationUnit = 'd' | 'bd' | 'w' | 'm' | 'q' | 'y';
12
+
13
+ export interface Duration {
14
+ amount: number;
15
+ unit: DurationUnit;
16
+ }
17
+
18
+ export interface Offset {
19
+ duration: Duration;
20
+ direction: 1 | -1;
21
+ }
22
+
23
+ // ── Parsed Elements ─────────────────────────────────────────
24
+
25
+ export interface GanttDependency {
26
+ targetName: string; // raw string from `-> X` or `-> Group.X`
27
+ offset?: Offset;
28
+ lineNumber: number;
29
+ }
30
+
31
+ export interface GanttTask {
32
+ id: string; // unique, generated during parse (e.g. "group:taskIdx")
33
+ label: string;
34
+ duration: Duration | null; // null for explicit-date-only tasks
35
+ explicitStart?: string; // YYYY-MM-DD from `2024-01-15 -> 30d:` or `2024-01-15:`
36
+ uncertain: boolean;
37
+ progress: number | null; // 0-100 or null
38
+ offset?: Offset; // task-level offset: shifts start date forward (+) or backward (-)
39
+ dependencies: GanttDependency[];
40
+ metadata: Record<string, string>; // tag values from pipe metadata
41
+ lineNumber: number;
42
+ groupPath: string[]; // e.g. ["Backend", "API"] for nested groups
43
+ comment?: string; // accumulated // comment lines
44
+ }
45
+
46
+ export interface GanttGroup {
47
+ name: string;
48
+ color: string | null;
49
+ metadata: Record<string, string>;
50
+ lineNumber: number;
51
+ children: GanttNode[];
52
+ }
53
+
54
+ export interface GanttParallelBlock {
55
+ kind: 'parallel';
56
+ lineNumber: number;
57
+ children: GanttNode[];
58
+ }
59
+
60
+ /** A node in the gantt tree: either a task, group, or parallel block. */
61
+ export type GanttNode =
62
+ | ({ kind: 'task' } & GanttTask)
63
+ | ({ kind: 'group' } & GanttGroup)
64
+ | GanttParallelBlock;
65
+
66
+ // ── Holidays ────────────────────────────────────────────────
67
+
68
+ export type Weekday = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
69
+
70
+ export interface HolidayDate {
71
+ date: string; // YYYY-MM-DD
72
+ label: string;
73
+ lineNumber: number;
74
+ }
75
+
76
+ export interface HolidayRange {
77
+ startDate: string; // YYYY-MM-DD
78
+ endDate: string; // YYYY-MM-DD
79
+ label: string;
80
+ lineNumber: number;
81
+ }
82
+
83
+ export interface GanttHolidays {
84
+ dates: HolidayDate[];
85
+ ranges: HolidayRange[];
86
+ workweek: Weekday[]; // default: ['mon', 'tue', 'wed', 'thu', 'fri']
87
+ }
88
+
89
+ // ── Eras & Markers (reuse timeline types) ───────────────────
90
+
91
+ export interface GanttEra {
92
+ startDate: string;
93
+ endDate: string;
94
+ label: string;
95
+ color: string | null;
96
+ }
97
+
98
+ export interface GanttMarker {
99
+ date: string;
100
+ label: string;
101
+ color: string | null;
102
+ lineNumber: number;
103
+ }
104
+
105
+ // ── Chart Options ───────────────────────────────────────────
106
+
107
+ export interface GanttOptions {
108
+ start: string | null; // YYYY[-MM[-DD]] or null for relative timeline
109
+ title: string | null;
110
+ titleLineNumber: number | null;
111
+ orientation: 'horizontal' | 'vertical';
112
+ todayMarker: 'off' | 'on' | string; // 'on' = current date, string = YYYY-MM-DD
113
+ criticalPath: boolean;
114
+ dependencies: boolean;
115
+ sort: 'default' | 'tag';
116
+ defaultSwimlaneGroup: string | null; // tag group name from `sort: tag:Team`
117
+ }
118
+
119
+ // ── Parsed Result ───────────────────────────────────────────
120
+
121
+ export interface ParsedGantt {
122
+ nodes: GanttNode[]; // top-level tree (groups, tasks, parallel blocks)
123
+ holidays: GanttHolidays;
124
+ tagGroups: TagGroup[];
125
+ eras: GanttEra[];
126
+ markers: GanttMarker[];
127
+ options: GanttOptions;
128
+ diagnostics: DgmoError[];
129
+ error: string | null;
130
+ }
131
+
132
+ // ── Resolved Schedule ───────────────────────────────────────
133
+
134
+ export interface ResolvedTask {
135
+ task: GanttTask;
136
+ startDate: Date;
137
+ endDate: Date;
138
+ isCriticalPath: boolean;
139
+ isUncertain: boolean; // true if task.uncertain OR any predecessor is uncertain
140
+ isMilestone: boolean;
141
+ groupPath: string[];
142
+ effectiveMetadata: Record<string, string>; // merged with inherited tags
143
+ }
144
+
145
+ export interface ResolvedGroup {
146
+ name: string;
147
+ color: string | null;
148
+ metadata: Record<string, string>;
149
+ startDate: Date;
150
+ endDate: Date;
151
+ progress: number | null; // aggregate progress (weighted average)
152
+ lineNumber: number;
153
+ depth: number;
154
+ }
155
+
156
+ export interface ResolvedSchedule {
157
+ tasks: ResolvedTask[];
158
+ groups: ResolvedGroup[];
159
+ startDate: Date;
160
+ endDate: Date;
161
+ holidays: GanttHolidays;
162
+ tagGroups: TagGroup[];
163
+ eras: GanttEra[];
164
+ markers: GanttMarker[];
165
+ options: GanttOptions;
166
+ diagnostics: DgmoError[];
167
+ error: string | null;
168
+ }
@@ -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
@@ -217,10 +217,13 @@ export type {
217
217
  } from './initiative-status/layout';
218
218
 
219
219
  export { renderInitiativeStatus, renderInitiativeStatusForExport } from './initiative-status/renderer';
220
+ export type { ISRenderOptions } from './initiative-status/renderer';
220
221
 
221
222
  export { collapseInitiativeStatus } from './initiative-status/collapse';
222
223
  export type { CollapseResult } from './initiative-status/collapse';
223
224
 
225
+ export { filterInitiativeStatusByTags } from './initiative-status/filter';
226
+
224
227
  export { parseSitemap, looksLikeSitemap } from './sitemap/parser';
225
228
 
226
229
  export type {
@@ -259,6 +262,30 @@ export { renderInfra, parseAndLayoutInfra, computeInfraLegendGroups } from './in
259
262
  export type { InfraLegendGroup } from './infra/renderer';
260
263
  export type { CollapsedSitemapResult } from './sitemap/collapse';
261
264
 
265
+ // ── Gantt Chart ───────────────────────────────────────────
266
+ export { parseGantt } from './gantt/parser';
267
+ export { calculateSchedule } from './gantt/calculator';
268
+ export { renderGantt, buildTagLaneRowList } from './gantt/renderer';
269
+ export type { GanttInteractiveOptions, GanttRow, GanttGroupRow, GanttTaskRow, GanttLaneHeaderRow } from './gantt/renderer';
270
+ export { resolveTaskName, collectTasks } from './gantt/resolver';
271
+ export type {
272
+ ParsedGantt,
273
+ GanttTask,
274
+ GanttGroup,
275
+ GanttParallelBlock,
276
+ GanttNode,
277
+ GanttDependency,
278
+ GanttHolidays,
279
+ GanttEra,
280
+ GanttMarker,
281
+ GanttOptions,
282
+ Duration,
283
+ DurationUnit,
284
+ ResolvedSchedule,
285
+ ResolvedTask,
286
+ ResolvedGroup,
287
+ } from './gantt/types';
288
+
262
289
  export { collapseOrgTree } from './org/collapse';
263
290
  export type { CollapsedOrgResult } from './org/collapse';
264
291
 
@@ -361,6 +388,16 @@ export type {
361
388
  DecodedDiagramUrl,
362
389
  } from './sharing';
363
390
 
391
+ // ============================================================
392
+ // Completion (symbol extraction API)
393
+ // ============================================================
394
+
395
+ export {
396
+ registerExtractor,
397
+ extractDiagramSymbols,
398
+ } from './completion';
399
+ export type { DiagramSymbols, ExtractFn } from './completion';
400
+
364
401
  // ============================================================
365
402
  // Branding
366
403
  // ============================================================
@@ -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
+ }