@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
@@ -62,7 +62,6 @@ export interface InfraNode {
62
62
  groupId: string | null;
63
63
  tags: Record<string, string>; // tagGroup -> tagValue
64
64
  isEdge: boolean; // true for the `edge` entry-point component
65
- nodeType?: string; // database, cache, queue, service, gateway, storage, function, network
66
65
  description?: string;
67
66
  lineNumber: number;
68
67
  }
@@ -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;
@@ -203,16 +227,19 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
203
227
  continue;
204
228
  }
205
229
 
206
- // hide directive (no colon): `hide phase Planning, phase Review`
230
+ // hide directive (colon syntax): `hide phase:Planning, phase:Review`
207
231
  const hideMatch = trimmed.match(/^hide\s+(.+)/i);
208
232
  if (hideMatch && !trimmed.match(/^hide\s*\|/)) {
209
- // Parse comma-separated tag-value pairs: `phase Planning, phase Review`
233
+ // Parse comma-separated tag:value pairs: `phase:Planning, phase:Review`
210
234
  const pairs = hideMatch[1].split(',');
211
235
  for (const pair of pairs) {
212
- const tokens = pair.trim().split(/\s+/);
213
- if (tokens.length >= 2) {
214
- const groupKey = tokens[0].toLowerCase();
215
- const value = tokens.slice(1).join(' ').toLowerCase();
236
+ const colonIdx = pair.indexOf(':');
237
+ if (colonIdx > 0) {
238
+ const groupKey = pair.substring(0, colonIdx).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());
@@ -231,7 +258,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
231
258
  const key = optMatch[1].toLowerCase();
232
259
  const value = optMatch[2].trim();
233
260
  // Only recognize known option keys (not node content)
234
- if (key === 'active-tag' || key === 'sort') {
261
+ if (key === 'active-tag') {
235
262
  result.options[key] = value;
236
263
  continue;
237
264
  }
@@ -243,13 +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')
247
- );
248
- continue;
249
- }
250
- if (tagBlockMatch.deprecated) {
251
- result.diagnostics.push(
252
- makeDgmoError(lineNum, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag ${tagBlockMatch.name}' instead`)
273
+ makeDgmoError(
274
+ lineNum,
275
+ 'Tag groups must appear before diagram content',
276
+ 'error'
277
+ )
253
278
  );
254
279
  continue;
255
280
  }
@@ -260,7 +285,10 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
260
285
  lineNumber: lineNum,
261
286
  };
262
287
  if (tagBlockMatch.alias) {
263
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
288
+ aliasMap.set(
289
+ tagBlockMatch.alias.toLowerCase(),
290
+ tagBlockMatch.name.toLowerCase()
291
+ );
264
292
  }
265
293
  // Handle inline values from single-line tag declaration
266
294
  if (tagBlockMatch.inlineValues) {
@@ -298,7 +326,7 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
298
326
  continue;
299
327
  }
300
328
  // Non-indented line after tag group — close and fall through
301
- currentTagGroup = null;
329
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
302
330
  }
303
331
 
304
332
  // Group header: [Group Name] or [Group Name] | metadata
@@ -349,13 +377,22 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
349
377
  if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
350
378
  if (!lastNodeLabel) {
351
379
  result.diagnostics.push(
352
- 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
+ )
353
385
  );
354
386
  continue;
355
387
  }
356
388
  edgeText = `${lastNodeLabel} ${trimmed}`;
357
389
  }
358
- const edge = parseEdgeLine(edgeText, lineNum, aliasMap, result.diagnostics);
390
+ const edge = parseEdgeLine(
391
+ edgeText,
392
+ lineNum,
393
+ aliasMap,
394
+ result.diagnostics
395
+ );
359
396
  if (edge) result.edges.push(edge);
360
397
  continue;
361
398
  }
@@ -364,28 +401,32 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
364
401
  contentStarted = true;
365
402
  currentTagGroup = null;
366
403
  const node = parseNodeLine(trimmed, lineNum, aliasMap, result.diagnostics);
367
- if (node) {
368
- lastNodeLabel = node.label;
369
- if (nodeLabels.has(node.label)) {
370
- result.diagnostics.push(
371
- makeDgmoError(lineNum, `Duplicate node "${node.label}"`, 'warning')
372
- );
373
- } else {
374
- nodeLabels.add(node.label);
375
- }
376
- // Cascade group metadata into node (group provides defaults, node overrides)
377
- if (currentGroup && isIndented && currentGroup.metadata) {
378
- for (const [key, val] of Object.entries(currentGroup.metadata)) {
379
- if (!(key in node.metadata)) {
380
- node.metadata[key] = val;
381
- }
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;
382
423
  }
383
424
  }
384
- result.nodes.push(node);
385
- // Add to current group if indented
386
- if (currentGroup && isIndented) {
387
- currentGroup.nodeLabels.push(node.label);
388
- }
425
+ }
426
+ result.nodes.push(node);
427
+ // Add to current group if indented
428
+ if (currentGroup && isIndented) {
429
+ currentGroup.nodeLabels.push(node.label);
389
430
  }
390
431
  }
391
432
 
@@ -398,20 +439,40 @@ export function parseInitiativeStatus(content: string): ParsedInitiativeStatus {
398
439
  for (const edge of result.edges) {
399
440
  if (!nodeLabels.has(edge.source)) {
400
441
  result.diagnostics.push(
401
- 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
+ )
402
447
  );
403
448
  // Auto-create an implicit node
404
449
  if (!result.nodes.some((n) => n.label === edge.source)) {
405
- 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
+ });
406
457
  nodeLabels.add(edge.source);
407
458
  }
408
459
  }
409
460
  if (!nodeLabels.has(edge.target)) {
410
461
  result.diagnostics.push(
411
- 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
+ )
412
467
  );
413
468
  if (!result.nodes.some((n) => n.label === edge.target)) {
414
- 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
+ });
415
476
  nodeLabels.add(edge.target);
416
477
  }
417
478
  }
@@ -443,7 +504,12 @@ function parseNodeLine(
443
504
  const label = trimmed.slice(0, pipeIdx).trim();
444
505
  const metaSegment = trimmed.slice(pipeIdx + 1).trim();
445
506
  if (!label) return null;
446
- const { status, metadata, hadStatusWord } = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
507
+ const { status, metadata, hadStatusWord } = parseNodeMetadata(
508
+ metaSegment,
509
+ aliasMap,
510
+ lineNum,
511
+ diagnostics
512
+ );
447
513
  return {
448
514
  label,
449
515
  // Unknown status bare word → keep null; no bare word at all → default 'na'
@@ -453,7 +519,13 @@ function parseNodeLine(
453
519
  metadata,
454
520
  };
455
521
  }
456
- 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
+ };
457
529
  }
458
530
 
459
531
  function parseEdgeLine(
@@ -481,8 +553,15 @@ function parseEdgeLine(
481
553
  const pipeIdx = targetRest.indexOf('|');
482
554
  if (pipeIdx >= 0) {
483
555
  const metaSegment = targetRest.slice(pipeIdx + 1).trim();
484
- const parsed = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
485
- 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');
486
565
  metadata = parsed.metadata;
487
566
  targetRest = targetRest.slice(0, pipeIdx).trim();
488
567
  }
@@ -505,7 +584,9 @@ function parseEdgeLine(
505
584
  let rest = trimmed.slice(arrowIdx + 2).trim();
506
585
 
507
586
  if (!source || !rest) {
508
- diagnostics.push(makeDgmoError(lineNum, 'Edge is missing source or target'));
587
+ diagnostics.push(
588
+ makeDgmoError(lineNum, 'Edge is missing source or target')
589
+ );
509
590
  return null;
510
591
  }
511
592
 
@@ -515,8 +596,15 @@ function parseEdgeLine(
515
596
  const pipeIdx = rest.indexOf('|');
516
597
  if (pipeIdx >= 0) {
517
598
  const metaSegment = rest.slice(pipeIdx + 1).trim();
518
- const parsed = parseNodeMetadata(metaSegment, aliasMap, lineNum, diagnostics);
519
- 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');
520
608
  metadata = parsed.metadata;
521
609
  rest = rest.slice(0, pipeIdx).trim();
522
610
  }
@@ -26,12 +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
- 'color-off', '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
- ]);
31
+ const KNOWN_BOOLEANS = new Set<string>(['no-auto-color']);
35
32
 
36
33
  // ============================================================
37
34
  // Parser
@@ -125,15 +122,11 @@ export function parseKanban(
125
122
  }
126
123
  }
127
124
 
128
- // Tag group heading — `tag: Name` (new) or `## Name` (deprecated)
129
- // Must be checked BEFORE OPTION_RE to prevent `tag: Rank` being swallowed as option
125
+ // Tag group heading — `tag Name`
126
+ // Must be checked BEFORE OPTION_RE to prevent `tag Rank` being swallowed as option
130
127
  if (!contentStarted) {
131
128
  const tagBlockMatch = matchTagBlockHeading(trimmed);
132
129
  if (tagBlockMatch) {
133
- if (tagBlockMatch.deprecated) {
134
- result.diagnostics.push(makeDgmoError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`));
135
- continue;
136
- }
137
130
  currentTagGroup = {
138
131
  name: tagBlockMatch.name,
139
132
  alias: tagBlockMatch.alias,
@@ -141,7 +134,10 @@ export function parseKanban(
141
134
  lineNumber,
142
135
  };
143
136
  if (tagBlockMatch.alias) {
144
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
137
+ aliasMap.set(
138
+ tagBlockMatch.alias.toLowerCase(),
139
+ tagBlockMatch.name.toLowerCase()
140
+ );
145
141
  }
146
142
  result.tagGroups.push(currentTagGroup);
147
143
  continue;
@@ -160,7 +156,10 @@ export function parseKanban(
160
156
  }
161
157
  }
162
158
  // Bare boolean option (single keyword, no value)
163
- if (KNOWN_BOOLEANS.has(trimmed.toLowerCase()) && !COLUMN_RE.test(trimmed)) {
159
+ if (
160
+ KNOWN_BOOLEANS.has(trimmed.toLowerCase()) &&
161
+ !COLUMN_RE.test(trimmed)
162
+ ) {
164
163
  result.options[trimmed.toLowerCase()] = 'on';
165
164
  continue;
166
165
  }
@@ -202,7 +201,12 @@ export function parseKanban(
202
201
  if (LEGACY_COLUMN_RE.test(trimmed)) {
203
202
  const legacyMatch = trimmed.match(LEGACY_COLUMN_RE)!;
204
203
  const name = legacyMatch[1].replace(/\s*\(.*\)\s*$/, '').trim();
205
- 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
+ );
206
210
  continue;
207
211
  }
208
212
 
@@ -227,7 +231,7 @@ export function parseKanban(
227
231
  columnCounter++;
228
232
  const colName = columnMatch[1].trim();
229
233
  const colColor = columnMatch[2]
230
- ? resolveColor(columnMatch[2].trim(), palette) ?? undefined
234
+ ? (resolveColor(columnMatch[2].trim(), palette) ?? undefined)
231
235
  : undefined;
232
236
 
233
237
  // Parse pipe metadata (e.g., "| wip: 3, t: Sprint1")
@@ -236,7 +240,10 @@ export function parseKanban(
236
240
  const pipeStr = columnMatch[3];
237
241
  if (pipeStr) {
238
242
  const pipeSegments = ['', pipeStr];
239
- Object.assign(columnMetadata, parsePipeMetadata(pipeSegments, aliasMap));
243
+ Object.assign(
244
+ columnMetadata,
245
+ parsePipeMetadata(pipeSegments, aliasMap)
246
+ );
240
247
  // Extract wip from metadata
241
248
  if (columnMetadata.wip) {
242
249
  const wipVal = parseInt(columnMetadata.wip, 10);
@@ -303,8 +310,8 @@ export function parseKanban(
303
310
  continue;
304
311
  }
305
312
 
306
- // Un-indented non-column line in content phase — could be stray text
307
- // For permissiveness, skip silently
313
+ // Un-indented non-column line in content phase — stray text
314
+ warn(lineNumber, `Unexpected line: '${trimmed}'.`);
308
315
  }
309
316
 
310
317
  // Finalize last card's endLineNumber
@@ -332,7 +339,8 @@ export function parseKanban(
332
339
  for (const col of result.columns) {
333
340
  for (const card of col.cards) {
334
341
  for (const [tagKey, tagValue] of Object.entries(card.tags)) {
335
- const groupKey = aliasMap.get(tagKey.toLowerCase()) ?? tagKey.toLowerCase();
342
+ const groupKey =
343
+ aliasMap.get(tagKey.toLowerCase()) ?? tagKey.toLowerCase();
336
344
  const validValues = tagValueSets.get(groupKey);
337
345
  if (validValues && !validValues.has(tagValue.toLowerCase())) {
338
346
  const entries = result.tagGroups