@diagrammo/dgmo 0.8.5 → 0.8.7

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 (65) hide show
  1. package/.claude/commands/dgmo.md +34 -27
  2. package/.cursorrules +20 -2
  3. package/.github/copilot-instructions.md +20 -2
  4. package/.windsurfrules +20 -2
  5. package/AGENTS.md +23 -3
  6. package/README.md +0 -1
  7. package/dist/cli.cjs +189 -190
  8. package/dist/editor.cjs +3 -18
  9. package/dist/editor.cjs.map +1 -1
  10. package/dist/editor.js +3 -18
  11. package/dist/editor.js.map +1 -1
  12. package/dist/highlight.cjs +4 -21
  13. package/dist/highlight.cjs.map +1 -1
  14. package/dist/highlight.js +4 -21
  15. package/dist/highlight.js.map +1 -1
  16. package/dist/index.cjs +2791 -2999
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +56 -56
  19. package/dist/index.d.ts +56 -56
  20. package/dist/index.js +2786 -2992
  21. package/dist/index.js.map +1 -1
  22. package/docs/ai-integration.md +1 -1
  23. package/docs/language-reference.md +112 -121
  24. package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
  25. package/package.json +1 -1
  26. package/src/boxes-and-lines/collapse.ts +78 -0
  27. package/src/boxes-and-lines/layout.ts +319 -0
  28. package/src/boxes-and-lines/parser.ts +697 -0
  29. package/src/boxes-and-lines/renderer.ts +848 -0
  30. package/src/boxes-and-lines/types.ts +40 -0
  31. package/src/c4/parser.ts +10 -5
  32. package/src/c4/renderer.ts +232 -56
  33. package/src/chart.ts +9 -4
  34. package/src/cli.ts +6 -5
  35. package/src/completion.ts +25 -33
  36. package/src/d3.ts +26 -27
  37. package/src/dgmo-router.ts +3 -7
  38. package/src/echarts.ts +38 -2
  39. package/src/editor/keywords.ts +4 -19
  40. package/src/er/parser.ts +10 -4
  41. package/src/gantt/parser.ts +10 -4
  42. package/src/gantt/renderer.ts +3 -5
  43. package/src/index.ts +17 -26
  44. package/src/infra/parser.ts +10 -5
  45. package/src/infra/renderer.ts +2 -2
  46. package/src/kanban/parser.ts +10 -5
  47. package/src/kanban/renderer.ts +43 -18
  48. package/src/org/parser.ts +7 -4
  49. package/src/org/renderer.ts +40 -29
  50. package/src/sequence/parser.ts +11 -5
  51. package/src/sequence/renderer.ts +114 -45
  52. package/src/sitemap/parser.ts +8 -4
  53. package/src/sitemap/renderer.ts +137 -57
  54. package/src/utils/legend-svg.ts +44 -20
  55. package/src/utils/parsing.ts +1 -1
  56. package/src/utils/tag-groups.ts +59 -15
  57. package/gallery/fixtures/initiative-status-full.dgmo +0 -46
  58. package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
  59. package/gallery/fixtures/initiative-status.dgmo +0 -9
  60. package/src/initiative-status/collapse.ts +0 -76
  61. package/src/initiative-status/filter.ts +0 -63
  62. package/src/initiative-status/layout.ts +0 -650
  63. package/src/initiative-status/parser.ts +0 -629
  64. package/src/initiative-status/renderer.ts +0 -1199
  65. package/src/initiative-status/types.ts +0 -57
@@ -16,7 +16,7 @@ export interface TagGroup {
16
16
  name: string;
17
17
  alias?: string;
18
18
  entries: TagEntry[];
19
- /** First value in the tag declaration is the default (nodes without metadata get this) */
19
+ /** Default value for nodes without explicit metadata. First entry unless another is marked `default`. */
20
20
  defaultValue?: string;
21
21
  lineNumber: number;
22
22
  }
@@ -30,6 +30,26 @@ export interface TagBlockMatch {
30
30
  inlineValues?: string[];
31
31
  }
32
32
 
33
+ // ── Default Modifier ────────────────────────────────────────
34
+
35
+ /**
36
+ * Strip trailing `default` keyword from a tag entry string.
37
+ * Returns the cleaned text and whether the keyword was present.
38
+ *
39
+ * Examples:
40
+ * "NA(gray) default" → { text: "NA(gray)", isDefault: true }
41
+ * "Done(green)" → { text: "Done(green)", isDefault: false }
42
+ */
43
+ export function stripDefaultModifier(text: string): {
44
+ text: string;
45
+ isDefault: boolean;
46
+ } {
47
+ if (/\bdefault\s*$/.test(text)) {
48
+ return { text: text.replace(/\s+default\s*$/, '').trim(), isDefault: true };
49
+ }
50
+ return { text, isDefault: false };
51
+ }
52
+
33
53
  // ── Regexes ─────────────────────────────────────────────────
34
54
 
35
55
  /** Canonical syntax: line starting with `tag` keyword (no colon). */
@@ -89,10 +109,15 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
89
109
  // BEFORE any value tokens (values have `(color)` suffixes or appear after we see a comma).
90
110
 
91
111
  // First check for explicit `alias` keyword: `tag Name alias X`
92
- const aliasKeywordIdx = tokens.findIndex((t, i) => i > 0 && t.toLowerCase() === 'alias');
112
+ const aliasKeywordIdx = tokens.findIndex(
113
+ (t, i) => i > 0 && t.toLowerCase() === 'alias'
114
+ );
93
115
  if (aliasKeywordIdx > 0 && aliasKeywordIdx + 1 < tokens.length) {
94
116
  // Everything before `alias` is the name, the token after `alias` is the alias
95
- name = tokens.slice(0, aliasKeywordIdx).map(t => stripQuotes(t)).join(' ');
117
+ name = tokens
118
+ .slice(0, aliasKeywordIdx)
119
+ .map((t) => stripQuotes(t))
120
+ .join(' ');
96
121
  alias = tokens[aliasKeywordIdx + 1];
97
122
  restStartIdx = aliasKeywordIdx + 2;
98
123
  } else {
@@ -103,7 +128,11 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
103
128
 
104
129
  if (tokens.length === 1) {
105
130
  // Just `tag Name` — no alias, no values
106
- } else if (tokens.length === 2 && isAliasToken(tokens[1]) && !commaInRemaining) {
131
+ } else if (
132
+ tokens.length === 2 &&
133
+ isAliasToken(tokens[1]) &&
134
+ !commaInRemaining
135
+ ) {
107
136
  // `tag Priority p` — alias only, no values
108
137
  alias = tokens[1];
109
138
  restStartIdx = 2;
@@ -130,11 +159,17 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
130
159
  if (valueStart > 1 && isAliasToken(tokens[valueStart - 1])) {
131
160
  alias = tokens[valueStart - 1];
132
161
  // Name is everything from token[0] to token[valueStart-2]
133
- name = tokens.slice(0, valueStart - 1).map(t => stripQuotes(t)).join(' ');
162
+ name = tokens
163
+ .slice(0, valueStart - 1)
164
+ .map((t) => stripQuotes(t))
165
+ .join(' ');
134
166
  restStartIdx = valueStart;
135
167
  } else {
136
168
  // No alias — name is everything before values
137
- name = tokens.slice(0, valueStart).map(t => stripQuotes(t)).join(' ');
169
+ name = tokens
170
+ .slice(0, valueStart)
171
+ .map((t) => stripQuotes(t))
172
+ .join(' ');
138
173
  restStartIdx = valueStart;
139
174
  }
140
175
  }
@@ -146,7 +181,10 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
146
181
  if (restStartIdx < tokens.length) {
147
182
  // Rejoin and split by comma for inline values
148
183
  const valueStr = tokens.slice(restStartIdx).join(' ');
149
- inlineValues = valueStr.split(',').map(v => v.trim()).filter(Boolean);
184
+ inlineValues = valueStr
185
+ .split(',')
186
+ .map((v) => v.trim())
187
+ .filter(Boolean);
150
188
  }
151
189
 
152
190
  // Check for trailing color hint on name (without inline values)
@@ -163,7 +201,8 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
163
201
  name,
164
202
  alias,
165
203
  colorHint,
166
- inlineValues: inlineValues && inlineValues.length > 0 ? inlineValues : undefined,
204
+ inlineValues:
205
+ inlineValues && inlineValues.length > 0 ? inlineValues : undefined,
167
206
  };
168
207
  }
169
208
 
@@ -204,9 +243,8 @@ export function resolveTagColor(
204
243
  if (!metaValue) return '#999999';
205
244
 
206
245
  return (
207
- group.entries.find(
208
- (e) => e.value.toLowerCase() === metaValue.toLowerCase()
209
- )?.color ?? '#999999'
246
+ group.entries.find((e) => e.value.toLowerCase() === metaValue.toLowerCase())
247
+ ?.color ?? '#999999'
210
248
  );
211
249
  }
212
250
 
@@ -223,7 +261,10 @@ export function resolveTagColor(
223
261
  * @param suggestFn Optional did-you-mean suggestion function
224
262
  */
225
263
  export function validateTagValues(
226
- entities: ReadonlyArray<{ metadata: Record<string, string>; lineNumber: number }>,
264
+ entities: ReadonlyArray<{
265
+ metadata: Record<string, string>;
266
+ lineNumber: number;
267
+ }>,
227
268
  tagGroups: ReadonlyArray<TagGroup>,
228
269
  pushWarning: (lineNumber: number, message: string) => void,
229
270
  suggestFn?: (input: string, candidates: readonly string[]) => string | null
@@ -244,8 +285,8 @@ export function validateTagValues(
244
285
  // Suppress warning if the value is a prefix of any valid entry —
245
286
  // the user is likely still typing (live parse during editing).
246
287
  const valueLower = value.toLowerCase();
247
- const isPrefix = group.entries.some(
248
- (e) => e.value.toLowerCase().startsWith(valueLower)
288
+ const isPrefix = group.entries.some((e) =>
289
+ e.value.toLowerCase().startsWith(valueLower)
249
290
  );
250
291
  if (!isPrefix) {
251
292
  const defined = group.entries.map((e) => e.value);
@@ -281,7 +322,10 @@ export function injectDefaultTagMetadata(
281
322
  const defaults: { key: string; value: string }[] = [];
282
323
  for (const group of tagGroups) {
283
324
  if (group.defaultValue) {
284
- defaults.push({ key: group.name.toLowerCase(), value: group.defaultValue });
325
+ defaults.push({
326
+ key: group.name.toLowerCase(),
327
+ value: group.defaultValue,
328
+ });
285
329
  }
286
330
  }
287
331
  if (defaults.length === 0) return;
@@ -1,46 +0,0 @@
1
- initiative-status Platform Modernization — Full Status
2
-
3
- tag Phase alias p
4
- Discovery(green) default
5
- Build(blue)
6
- Launch(orange)
7
- Maintenance(purple)
8
-
9
- tag Team alias t
10
- Frontend(blue)
11
- Backend(green)
12
- Platform(teal)
13
- Data(purple)
14
-
15
- [Identity] | t: Backend
16
- Auth Service | done, p: Build
17
- SSO Integration | doing, p: Build
18
- -> Auth Service | done
19
- MFA Rollout | blocked, p: Launch
20
- -> SSO Integration | doing
21
-
22
- [Payments] | t: Platform
23
- Payment Gateway | doing, p: Build
24
- Billing UI | todo, p: Build, t: Frontend
25
- -> Payment Gateway | doing
26
- Invoice API | todo, p: Launch
27
- -> Payment Gateway | doing
28
- -> Billing UI | todo
29
-
30
- [Search] | t: Backend
31
- Search API | done, p: Build
32
- Search UI | doing, p: Build, t: Frontend
33
- -> Search API | done
34
- Search Analytics | na, p: Maintenance, t: Data
35
-
36
- [Observability] | t: Platform
37
- Metrics Pipeline | done, p: Discovery
38
- Alert Rules | done, p: Build
39
- -> Metrics Pipeline | done
40
- Dashboard | doing, p: Build, t: Frontend
41
- -> Metrics Pipeline | done
42
- -> Alert Rules | done
43
-
44
- Auth Service -> Payment Gateway: validates | done
45
- SSO Integration -> Search API: provides identity | done
46
- Metrics Pipeline -> Search API: monitors | done
@@ -1,29 +0,0 @@
1
- initiative-status Platform Migration
2
-
3
- tag Phase alias p
4
- Discovery default
5
- Build
6
- Launch
7
-
8
- tag Team alias t
9
- Frontend
10
- Backend
11
- Platform
12
-
13
- [Identity]
14
- Auth Service | done, p: Discovery, t: Backend
15
- SSO Integration | doing, p: Build, t: Backend
16
- -> Auth Service | done
17
-
18
- [Payments]
19
- Payment Gateway | doing, p: Build, t: Platform
20
- Billing UI | todo, p: Build, t: Frontend
21
- -> Payment Gateway | doing
22
-
23
- [Search]
24
- Search API | todo, p: Launch, t: Backend
25
- Search UI | todo, p: Launch, t: Frontend
26
- -> Search API
27
-
28
- Auth Service -> Payment Gateway: validates | done
29
- SSO Integration -> Search API: provides identity | todo
@@ -1,9 +0,0 @@
1
- initiative-status Q2 Platform Roadmap
2
-
3
- Auth Service | done
4
- Payment Gateway | doing
5
- -> Auth Service | done
6
- Search API | todo
7
- -> Auth Service | done
8
- Billing UI | blocked
9
- -> Payment Gateway | doing
@@ -1,76 +0,0 @@
1
- import type { ParsedInitiativeStatus, InitiativeStatus, ISGroup } from './types';
2
- import { rollUpStatus } from './layout';
3
-
4
- // ============================================================
5
- // CollapseResult — returned by collapseInitiativeStatus
6
- // ============================================================
7
-
8
- export interface CollapseResult {
9
- parsed: ParsedInitiativeStatus;
10
- collapsedGroupStatuses: Map<string, InitiativeStatus>;
11
- originalGroups: ISGroup[];
12
- }
13
-
14
- // ============================================================
15
- // collapseInitiativeStatus — pure transform
16
- //
17
- // Returns a new ParsedInitiativeStatus with:
18
- // - Children of collapsed groups removed from nodes
19
- // - Edges redirected: source/target pointing to hidden nodes
20
- // → point to the group label
21
- // - Internal edges (both endpoints in same collapsed group) dropped
22
- // - Duplicate edges (same source, target, label) deduplicated
23
- // (first occurrence kept)
24
- // - Collapsed groups removed from groups[] (layout handles them
25
- // as regular nodes)
26
- // - collapsedGroupStatuses: worst-case status per collapsed group
27
- // ============================================================
28
-
29
- export function collapseInitiativeStatus(
30
- parsed: ParsedInitiativeStatus,
31
- collapsedGroups: Set<string>
32
- ): CollapseResult {
33
- const originalGroups = parsed.groups;
34
-
35
- if (collapsedGroups.size === 0) {
36
- return { parsed, collapsedGroupStatuses: new Map(), originalGroups };
37
- }
38
-
39
- // Build node → collapsed group lookup
40
- const nodeToGroup = new Map<string, string>();
41
- const collapsedGroupStatuses = new Map<string, InitiativeStatus>();
42
-
43
- for (const group of parsed.groups) {
44
- if (!collapsedGroups.has(group.label)) continue;
45
- const children = group.nodeLabels
46
- .map((l) => parsed.nodes.find((n) => n.label === l))
47
- .filter((n): n is (typeof parsed.nodes)[0] => n !== undefined);
48
- for (const node of children) nodeToGroup.set(node.label, group.label);
49
- collapsedGroupStatuses.set(group.label, rollUpStatus(children));
50
- }
51
-
52
- // Filter nodes: remove children of collapsed groups
53
- const nodes = parsed.nodes.filter((n) => !nodeToGroup.has(n.label));
54
-
55
- // Remap and deduplicate edges
56
- const edgeKeys = new Set<string>();
57
- const edges: typeof parsed.edges = [];
58
- for (const edge of parsed.edges) {
59
- const src = nodeToGroup.get(edge.source) ?? edge.source;
60
- const tgt = nodeToGroup.get(edge.target) ?? edge.target;
61
- if (src === tgt) continue; // internal edge → drop
62
- const key = `${src}|${tgt}|${edge.label ?? ''}`;
63
- if (edgeKeys.has(key)) continue; // duplicate → drop
64
- edgeKeys.add(key);
65
- edges.push({ ...edge, source: src, target: tgt });
66
- }
67
-
68
- // Keep only expanded groups in groups[]
69
- const groups = parsed.groups.filter((g) => !collapsedGroups.has(g.label));
70
-
71
- return {
72
- parsed: { ...parsed, nodes, edges, groups },
73
- collapsedGroupStatuses,
74
- originalGroups,
75
- };
76
- }
@@ -1,63 +0,0 @@
1
- // ============================================================
2
- // Initiative Status — Tag-Based Filter
3
- //
4
- // Immutable graph transform: returns a new ParsedInitiativeStatus
5
- // with hidden-value nodes removed, their edges dropped,
6
- // and group.nodeLabels cleaned.
7
- // ============================================================
8
-
9
- import type { ParsedInitiativeStatus } from './types';
10
-
11
- /**
12
- * Filter an initiative-status graph by hiding nodes whose tag metadata
13
- * matches any hidden value. Returns a new (immutable copy) ParsedInitiativeStatus.
14
- *
15
- * @param parsed Fully-resolved parsed result (defaults already injected)
16
- * @param hiddenTagValues Map<groupKey, Set<hiddenValues>> — all keys/values lowercase
17
- * @returns Filtered copy; original is not mutated
18
- */
19
- export function filterInitiativeStatusByTags(
20
- parsed: ParsedInitiativeStatus,
21
- hiddenTagValues: Map<string, Set<string>>
22
- ): ParsedInitiativeStatus {
23
- // Fast path: no filtering
24
- if (hiddenTagValues.size === 0) return parsed;
25
-
26
- // Build set of hidden node labels
27
- const hiddenNodeLabels = new Set<string>();
28
- for (const node of parsed.nodes) {
29
- for (const [groupKey, hiddenValues] of hiddenTagValues) {
30
- const nodeValue = node.metadata[groupKey];
31
- if (nodeValue && hiddenValues.has(nodeValue.toLowerCase())) {
32
- hiddenNodeLabels.add(node.label);
33
- break;
34
- }
35
- }
36
- }
37
-
38
- // No nodes hidden — return input unchanged
39
- if (hiddenNodeLabels.size === 0) return parsed;
40
-
41
- // Filter nodes
42
- const nodes = parsed.nodes.filter((n) => !hiddenNodeLabels.has(n.label));
43
-
44
- // Filter edges: remove edges where source OR target is hidden
45
- const edges = parsed.edges.filter(
46
- (e) => !hiddenNodeLabels.has(e.source) && !hiddenNodeLabels.has(e.target)
47
- );
48
-
49
- // Clean group nodeLabels; remove empty groups
50
- const groups = parsed.groups
51
- .map((g) => ({
52
- ...g,
53
- nodeLabels: g.nodeLabels.filter((l) => !hiddenNodeLabels.has(l)),
54
- }))
55
- .filter((g) => g.nodeLabels.length > 0);
56
-
57
- return {
58
- ...parsed,
59
- nodes,
60
- edges,
61
- groups,
62
- };
63
- }