@diagrammo/dgmo 0.8.3 → 0.8.5

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 (122) 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 +452 -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 +188 -185
  9. package/dist/editor.cjs +338 -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 +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. package/src/utils/parsing.ts +75 -31
@@ -13,9 +13,17 @@ import type {
13
13
  } from './types';
14
14
  import { VALID_STATUSES, STATUS_ALIASES } from './types';
15
15
  import { inferParticipantType } from '../sequence/participant-inference';
16
- import { matchTagBlockHeading, injectDefaultTagMetadata, validateTagValues } from '../utils/tag-groups';
16
+ import {
17
+ matchTagBlockHeading,
18
+ injectDefaultTagMetadata,
19
+ validateTagValues,
20
+ } from '../utils/tag-groups';
17
21
  import type { TagGroup } from '../utils/tag-groups';
18
- import { extractColor, parseFirstLine, ALL_CHART_TYPES, OPTION_NOCOLON_RE } from '../utils/parsing';
22
+ import {
23
+ extractColor,
24
+ parseFirstLine,
25
+ OPTION_NOCOLON_RE,
26
+ } from '../utils/parsing';
19
27
 
20
28
  // ============================================================
21
29
  // Heuristic — does this content look like an initiative-status diagram?
@@ -38,10 +46,14 @@ export function looksLikeInitiativeStatus(content: string): boolean {
38
46
  // Skip new-style first line (bare chart type name)
39
47
  if (parseFirstLine(trimmed)) continue;
40
48
  if (trimmed.includes('->')) hasArrow = true;
41
- if (/\|\s*(done|doing|wip|blocked|paused|waiting|todo|na)\s*$/i.test(trimmed)) hasStatus = true;
49
+ if (
50
+ /\|\s*(done|doing|wip|blocked|paused|waiting|todo|na)\s*$/i.test(trimmed)
51
+ )
52
+ hasStatus = true;
42
53
  // Indented arrow is a strong signal — only initiative-status uses this
43
54
  const isIndented = line.length > 0 && line !== trimmed && /^\s/.test(line);
44
- if (isIndented && (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed))) hasIndentedArrow = true;
55
+ if (isIndented && (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)))
56
+ hasIndentedArrow = true;
45
57
  if (hasArrow && hasStatus) return true;
46
58
  }
47
59
  return hasIndentedArrow;
@@ -67,7 +79,11 @@ export function parseNodeMetadata(
67
79
  aliasMap: Map<string, string>,
68
80
  lineNum?: number,
69
81
  diagnostics?: DgmoError[]
70
- ): { status: InitiativeStatus; metadata: Record<string, string>; hadStatusWord: boolean } {
82
+ ): {
83
+ status: InitiativeStatus;
84
+ metadata: Record<string, string>;
85
+ hadStatusWord: boolean;
86
+ } {
71
87
  const metadata: Record<string, string> = {};
72
88
  let status: InitiativeStatus = null;
73
89
  let hadStatusWord = false;
@@ -125,7 +141,12 @@ export function parseNodeMetadata(
125
141
  // Parser
126
142
  // ============================================================
127
143
 
128
- function parseStatus(raw: string, line: number, diagnostics: DgmoError[]): InitiativeStatus {
144
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
145
+ function _parseStatus(
146
+ raw: string,
147
+ line: number,
148
+ diagnostics: DgmoError[]
149
+ ): InitiativeStatus {
129
150
  const trimmed = raw.trim().toLowerCase();
130
151
  if (!trimmed) return 'na';
131
152
  const canonical = STATUS_ALIASES[trimmed] ?? trimmed;
@@ -191,7 +212,10 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
191
212
  const firstLineResult = parseFirstLine(trimmed);
192
213
  if (firstLineResult && !contentStarted) {
193
214
  if (firstLineResult.chartType !== 'initiative-status') {
194
- const diag = makeDgmoError(lineNum, `Expected chart type "initiative-status", got "${firstLineResult.chartType}"`);
215
+ const diag = makeDgmoError(
216
+ lineNum,
217
+ `Expected chart type "initiative-status", got "${firstLineResult.chartType}"`
218
+ );
195
219
  result.diagnostics.push(diag);
196
220
  result.error = formatDgmoError(diag);
197
221
  return result;
@@ -212,7 +236,10 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
212
236
  const colonIdx = pair.indexOf(':');
213
237
  if (colonIdx > 0) {
214
238
  const groupKey = pair.substring(0, colonIdx).trim().toLowerCase();
215
- const value = pair.substring(colonIdx + 1).trim().toLowerCase();
239
+ const value = pair
240
+ .substring(colonIdx + 1)
241
+ .trim()
242
+ .toLowerCase();
216
243
  if (groupKey && value) {
217
244
  if (!result.initialHiddenTagValues.has(groupKey)) {
218
245
  result.initialHiddenTagValues.set(groupKey, new Set());
@@ -243,7 +270,11 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
243
270
  if (tagBlockMatch) {
244
271
  if (contentStarted) {
245
272
  result.diagnostics.push(
246
- makeDgmoError(lineNum, 'Tag groups must appear before diagram content', 'error')
273
+ makeDgmoError(
274
+ lineNum,
275
+ 'Tag groups must appear before diagram content',
276
+ 'error'
277
+ )
247
278
  );
248
279
  continue;
249
280
  }
@@ -254,7 +285,10 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
254
285
  lineNumber: lineNum,
255
286
  };
256
287
  if (tagBlockMatch.alias) {
257
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
288
+ aliasMap.set(
289
+ tagBlockMatch.alias.toLowerCase(),
290
+ tagBlockMatch.name.toLowerCase()
291
+ );
258
292
  }
259
293
  // Handle inline values from single-line tag declaration
260
294
  if (tagBlockMatch.inlineValues) {
@@ -292,7 +326,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
292
326
  continue;
293
327
  }
294
328
  // Non-indented line after tag group — close and fall through
295
- currentTagGroup = null;
329
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
296
330
  }
297
331
 
298
332
  // Group header: [Group Name] or [Group Name] | metadata
@@ -343,13 +377,22 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
343
377
  if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
344
378
  if (!lastNodeLabel) {
345
379
  result.diagnostics.push(
346
- makeDgmoError(lineNum, 'Indented edge has no preceding node to use as source', 'warning')
380
+ makeDgmoError(
381
+ lineNum,
382
+ 'Indented edge has no preceding node to use as source',
383
+ 'warning'
384
+ )
347
385
  );
348
386
  continue;
349
387
  }
350
388
  edgeText = `${lastNodeLabel} ${trimmed}`;
351
389
  }
352
- const edge = parseEdgeLine(edgeText, lineNum, aliasMap, result.diagnostics);
390
+ const edge = parseEdgeLine(
391
+ edgeText,
392
+ lineNum,
393
+ aliasMap,
394
+ result.diagnostics
395
+ );
353
396
  if (edge) result.edges.push(edge);
354
397
  continue;
355
398
  }
@@ -358,28 +401,32 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
358
401
  contentStarted = true;
359
402
  currentTagGroup = null;
360
403
  const node = parseNodeLine(trimmed, lineNum, aliasMap, result.diagnostics);
361
- if (node) {
362
- lastNodeLabel = node.label;
363
- if (nodeLabels.has(node.label)) {
364
- result.diagnostics.push(
365
- makeDgmoError(lineNum, `Duplicate node "${node.label}"`, 'warning')
366
- );
367
- } else {
368
- nodeLabels.add(node.label);
369
- }
370
- // Cascade group metadata into node (group provides defaults, node overrides)
371
- if (currentGroup && isIndented && currentGroup.metadata) {
372
- for (const [key, val] of Object.entries(currentGroup.metadata)) {
373
- if (!(key in node.metadata)) {
374
- node.metadata[key] = val;
375
- }
404
+ if (!node) {
405
+ result.diagnostics.push(
406
+ makeDgmoError(lineNum, `Unexpected line: '${trimmed}'.`, 'warning')
407
+ );
408
+ continue;
409
+ }
410
+ lastNodeLabel = node.label;
411
+ if (nodeLabels.has(node.label)) {
412
+ result.diagnostics.push(
413
+ makeDgmoError(lineNum, `Duplicate node "${node.label}"`, 'warning')
414
+ );
415
+ } else {
416
+ nodeLabels.add(node.label);
417
+ }
418
+ // Cascade group metadata into node (group provides defaults, node overrides)
419
+ if (currentGroup && isIndented && currentGroup.metadata) {
420
+ for (const [key, val] of Object.entries(currentGroup.metadata)) {
421
+ if (!(key in node.metadata)) {
422
+ node.metadata[key] = val;
376
423
  }
377
424
  }
378
- result.nodes.push(node);
379
- // Add to current group if indented
380
- if (currentGroup && isIndented) {
381
- currentGroup.nodeLabels.push(node.label);
382
- }
425
+ }
426
+ result.nodes.push(node);
427
+ // Add to current group if indented
428
+ if (currentGroup && isIndented) {
429
+ currentGroup.nodeLabels.push(node.label);
383
430
  }
384
431
  }
385
432
 
@@ -392,20 +439,40 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
392
439
  for (const edge of result.edges) {
393
440
  if (!nodeLabels.has(edge.source)) {
394
441
  result.diagnostics.push(
395
- makeDgmoError(edge.lineNumber, `Edge source "${edge.source}" is not a declared node`, 'warning')
442
+ makeDgmoError(
443
+ edge.lineNumber,
444
+ `Edge source "${edge.source}" is not a declared node`,
445
+ 'warning'
446
+ )
396
447
  );
397
448
  // Auto-create an implicit node
398
449
  if (!result.nodes.some((n) => n.label === edge.source)) {
399
- result.nodes.push({ label: edge.source, status: 'na', shape: inferParticipantType(edge.source), lineNumber: edge.lineNumber, metadata: {} });
450
+ result.nodes.push({
451
+ label: edge.source,
452
+ status: 'na',
453
+ shape: inferParticipantType(edge.source),
454
+ lineNumber: edge.lineNumber,
455
+ metadata: {},
456
+ });
400
457
  nodeLabels.add(edge.source);
401
458
  }
402
459
  }
403
460
  if (!nodeLabels.has(edge.target)) {
404
461
  result.diagnostics.push(
405
- makeDgmoError(edge.lineNumber, `Edge target "${edge.target}" is not a declared node`, 'warning')
462
+ makeDgmoError(
463
+ edge.lineNumber,
464
+ `Edge target "${edge.target}" is not a declared node`,
465
+ 'warning'
466
+ )
406
467
  );
407
468
  if (!result.nodes.some((n) => n.label === edge.target)) {
408
- result.nodes.push({ label: edge.target, status: 'na', shape: inferParticipantType(edge.target), lineNumber: edge.lineNumber, metadata: {} });
469
+ result.nodes.push({
470
+ label: edge.target,
471
+ status: 'na',
472
+ shape: inferParticipantType(edge.target),
473
+ lineNumber: edge.lineNumber,
474
+ metadata: {},
475
+ });
409
476
  nodeLabels.add(edge.target);
410
477
  }
411
478
  }
@@ -437,7 +504,12 @@ function parseNodeLine(
437
504
  const label = trimmed.slice(0, pipeIdx).trim();
438
505
  const metaSegment = trimmed.slice(pipeIdx + 1).trim();
439
506
  if (!label) return null;
440
- const { status, metadata, hadStatusWord } = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
507
+ const { status, metadata, hadStatusWord } = parseNodeMetadata(
508
+ metaSegment,
509
+ aliasMap,
510
+ lineNum,
511
+ diagnostics
512
+ );
441
513
  return {
442
514
  label,
443
515
  // Unknown status bare word → keep null; no bare word at all → default 'na'
@@ -447,7 +519,13 @@ function parseNodeLine(
447
519
  metadata,
448
520
  };
449
521
  }
450
- return { label: trimmed, status: 'na', shape: inferParticipantType(trimmed), lineNumber: lineNum, metadata: {} };
522
+ return {
523
+ label: trimmed,
524
+ status: 'na',
525
+ shape: inferParticipantType(trimmed),
526
+ lineNumber: lineNum,
527
+ metadata: {},
528
+ };
451
529
  }
452
530
 
453
531
  function parseEdgeLine(
@@ -475,8 +553,15 @@ function parseEdgeLine(
475
553
  const pipeIdx = targetRest.indexOf('|');
476
554
  if (pipeIdx >= 0) {
477
555
  const metaSegment = targetRest.slice(pipeIdx + 1).trim();
478
- const parsed = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
479
- status = parsed.hadStatusWord ? (parsed.status ?? null) : (parsed.status ?? 'na');
556
+ const parsed = parseNodeMetadata(
557
+ metaSegment,
558
+ aliasMap,
559
+ lineNum,
560
+ diagnostics
561
+ );
562
+ status = parsed.hadStatusWord
563
+ ? (parsed.status ?? null)
564
+ : (parsed.status ?? 'na');
480
565
  metadata = parsed.metadata;
481
566
  targetRest = targetRest.slice(0, pipeIdx).trim();
482
567
  }
@@ -499,7 +584,9 @@ function parseEdgeLine(
499
584
  let rest = trimmed.slice(arrowIdx + 2).trim();
500
585
 
501
586
  if (!source || !rest) {
502
- diagnostics.push(makeDgmoError(lineNum, 'Edge is missing source or target'));
587
+ diagnostics.push(
588
+ makeDgmoError(lineNum, 'Edge is missing source or target')
589
+ );
503
590
  return null;
504
591
  }
505
592
 
@@ -509,8 +596,15 @@ function parseEdgeLine(
509
596
  const pipeIdx = rest.indexOf('|');
510
597
  if (pipeIdx >= 0) {
511
598
  const metaSegment = rest.slice(pipeIdx + 1).trim();
512
- const parsed = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
513
- status = parsed.hadStatusWord ? (parsed.status ?? null) : (parsed.status ?? 'na');
599
+ const parsed = parseNodeMetadata(
600
+ metaSegment,
601
+ aliasMap,
602
+ lineNum,
603
+ diagnostics
604
+ );
605
+ status = parsed.hadStatusWord
606
+ ? (parsed.status ?? null)
607
+ : (parsed.status ?? 'na');
514
608
  metadata = parsed.metadata;
515
609
  rest = rest.slice(0, pipeIdx).trim();
516
610
  }
@@ -26,13 +26,9 @@ const COLUMN_RE = /^\[(.+?)\](?:\s*\(([^)]+)\))?\s*(?:\|\s*(.+))?$/;
26
26
  const LEGACY_COLUMN_RE = /^==\s+(.+?)\s*(?:\[wip:\s*(\d+)\])?\s*==$/;
27
27
 
28
28
  /** Known kanban options (key-value). */
29
- const KNOWN_OPTIONS = new Set([
30
- 'hide',
31
- ]);
29
+ const KNOWN_OPTIONS = new Set(['hide']);
32
30
  /** Known kanban boolean options (bare keyword = on). */
33
- const KNOWN_BOOLEANS = new Set<string>([
34
- 'no-auto-color',
35
- ]);
31
+ const KNOWN_BOOLEANS = new Set<string>(['no-auto-color']);
36
32
 
37
33
  // ============================================================
38
34
  // Parser
@@ -138,7 +134,10 @@ export function parseKanban(
138
134
  lineNumber,
139
135
  };
140
136
  if (tagBlockMatch.alias) {
141
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
137
+ aliasMap.set(
138
+ tagBlockMatch.alias.toLowerCase(),
139
+ tagBlockMatch.name.toLowerCase()
140
+ );
142
141
  }
143
142
  result.tagGroups.push(currentTagGroup);
144
143
  continue;
@@ -157,7 +156,10 @@ export function parseKanban(
157
156
  }
158
157
  }
159
158
  // Bare boolean option (single keyword, no value)
160
- if (KNOWN_BOOLEANS.has(trimmed.toLowerCase()) && !COLUMN_RE.test(trimmed)) {
159
+ if (
160
+ KNOWN_BOOLEANS.has(trimmed.toLowerCase()) &&
161
+ !COLUMN_RE.test(trimmed)
162
+ ) {
161
163
  result.options[trimmed.toLowerCase()] = 'on';
162
164
  continue;
163
165
  }
@@ -199,7 +201,12 @@ export function parseKanban(
199
201
  if (LEGACY_COLUMN_RE.test(trimmed)) {
200
202
  const legacyMatch = trimmed.match(LEGACY_COLUMN_RE)!;
201
203
  const name = legacyMatch[1].replace(/\s*\(.*\)\s*$/, '').trim();
202
- result.diagnostics.push(makeDgmoError(lineNumber, `'== ${name} ==' is no longer supported. Use '[${name}]' instead`));
204
+ result.diagnostics.push(
205
+ makeDgmoError(
206
+ lineNumber,
207
+ `'== ${name} ==' is no longer supported. Use '[${name}]' instead`
208
+ )
209
+ );
203
210
  continue;
204
211
  }
205
212
 
@@ -224,7 +231,7 @@ export function parseKanban(
224
231
  columnCounter++;
225
232
  const colName = columnMatch[1].trim();
226
233
  const colColor = columnMatch[2]
227
- ? resolveColor(columnMatch[2].trim(), palette) ?? undefined
234
+ ? (resolveColor(columnMatch[2].trim(), palette) ?? undefined)
228
235
  : undefined;
229
236
 
230
237
  // Parse pipe metadata (e.g., "| wip: 3, t: Sprint1")
@@ -233,7 +240,10 @@ export function parseKanban(
233
240
  const pipeStr = columnMatch[3];
234
241
  if (pipeStr) {
235
242
  const pipeSegments = ['', pipeStr];
236
- Object.assign(columnMetadata, parsePipeMetadata(pipeSegments, aliasMap));
243
+ Object.assign(
244
+ columnMetadata,
245
+ parsePipeMetadata(pipeSegments, aliasMap)
246
+ );
237
247
  // Extract wip from metadata
238
248
  if (columnMetadata.wip) {
239
249
  const wipVal = parseInt(columnMetadata.wip, 10);
@@ -300,8 +310,8 @@ export function parseKanban(
300
310
  continue;
301
311
  }
302
312
 
303
- // Un-indented non-column line in content phase — could be stray text
304
- // For permissiveness, skip silently
313
+ // Un-indented non-column line in content phase — stray text
314
+ warn(lineNumber, `Unexpected line: '${trimmed}'.`);
305
315
  }
306
316
 
307
317
  // Finalize last card's endLineNumber
@@ -329,7 +339,8 @@ export function parseKanban(
329
339
  for (const col of result.columns) {
330
340
  for (const card of col.cards) {
331
341
  for (const [tagKey, tagValue] of Object.entries(card.tags)) {
332
- const groupKey = aliasMap.get(tagKey.toLowerCase()) ?? tagKey.toLowerCase();
342
+ const groupKey =
343
+ aliasMap.get(tagKey.toLowerCase()) ?? tagKey.toLowerCase();
333
344
  const validValues = tagValueSets.get(groupKey);
334
345
  if (validValues && !validValues.has(tagValue.toLowerCase())) {
335
346
  const entries = result.tagGroups