@diagrammo/dgmo 0.34.0 → 0.36.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 (55) hide show
  1. package/.claude/commands/dgmo.md +41 -43
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +12 -7
  5. package/dist/advanced.cjs +460 -463
  6. package/dist/advanced.d.cts +2 -51
  7. package/dist/advanced.d.ts +2 -51
  8. package/dist/advanced.js +460 -464
  9. package/dist/auto.cjs +461 -140
  10. package/dist/auto.js +91 -91
  11. package/dist/auto.mjs +461 -140
  12. package/dist/cli.cjs +143 -326
  13. package/dist/index.cjs +460 -139
  14. package/dist/index.js +460 -139
  15. package/docs/ai-integration.md +47 -25
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +2 -2
  18. package/src/advanced.ts +0 -12
  19. package/src/c4/parser.ts +15 -6
  20. package/src/chart.ts +1 -1
  21. package/src/class/parser.ts +6 -2
  22. package/src/cli-banner.ts +2 -3
  23. package/src/cli.ts +539 -1223
  24. package/src/cycle/parser.ts +4 -1
  25. package/src/dgmo-router.ts +16 -2
  26. package/src/diagnostics.ts +0 -0
  27. package/src/echarts.ts +5 -1
  28. package/src/er/parser.ts +5 -1
  29. package/src/gantt/parser.ts +13 -4
  30. package/src/graph/flowchart-parser.ts +5 -1
  31. package/src/graph/state-parser.ts +5 -1
  32. package/src/infra/parser.ts +10 -6
  33. package/src/journey-map/parser.ts +5 -2
  34. package/src/kanban/parser.ts +6 -2
  35. package/src/map/context-labels.ts +29 -19
  36. package/src/map/layout.ts +403 -108
  37. package/src/map/legend-band.ts +28 -0
  38. package/src/map/parser.ts +13 -4
  39. package/src/map/renderer.ts +74 -23
  40. package/src/map/resolver.ts +2 -2
  41. package/src/org/parser.ts +8 -2
  42. package/src/pyramid/parser.ts +4 -1
  43. package/src/raci/parser.ts +4 -1
  44. package/src/ring/parser.ts +4 -1
  45. package/src/sequence/parser.ts +6 -3
  46. package/src/sitemap/parser.ts +12 -3
  47. package/src/tech-radar/parser.ts +6 -3
  48. package/src/utils/parsing.ts +2 -2
  49. package/src/utils/tag-groups.ts +3 -3
  50. package/src/visualizations/parse.ts +9 -3
  51. package/src/migrate/data-comma.ts +0 -117
  52. package/src/migrate/embedded.ts +0 -238
  53. package/src/migrate/index.ts +0 -295
  54. package/src/migrate/line-classifier.ts +0 -128
  55. package/src/migrate/line-transformer.ts +0 -218
@@ -352,7 +352,10 @@ export function parseCycle(content: string): ParsedCycle {
352
352
  }
353
353
 
354
354
  // Indented line with no context
355
- warn(lineNum, `Unexpected indented line: "${trimmed}".`);
355
+ warn(
356
+ lineNum,
357
+ `Indented line "${trimmed}" has no step above it — add a step on a non-indented line first, then indent its details. (§21)`
358
+ );
356
359
  continue;
357
360
  }
358
361
  }
@@ -14,7 +14,7 @@ import { looksLikePert } from './pert/parser';
14
14
  // part of the derived chartTypeParsers, which come from the registry).
15
15
  import { parseVisualization } from './visualizations/parse';
16
16
  import { parseFirstLine } from './utils/parsing';
17
- import { makeDgmoError, suggest } from './diagnostics';
17
+ import { makeDgmoError, suggest, dedupeDiagnostics } from './diagnostics';
18
18
  import type { DgmoError } from './diagnostics';
19
19
  import { chartTypes } from './chart-types';
20
20
  import {
@@ -145,7 +145,7 @@ export function getAllChartTypes(): string[] {
145
145
  /**
146
146
  * Canonical descriptions for every supported chart type. Derived from
147
147
  * `chartTypes` so there is exactly one place to update when adding a new
148
- * type. Consumed by the CLI `--chart-types` flag, the editor autocomplete
148
+ * type. Consumed by the CLI `dgmo types` command, the editor autocomplete
149
149
  * popup, and the MCP `list_chart_types` tool.
150
150
  */
151
151
  export const CHART_TYPE_DESCRIPTIONS: Record<string, string> =
@@ -185,6 +185,20 @@ const ALL_KNOWN_TYPES: ReadonlySet<string> = new Set(knownChartTypeIds);
185
185
  export function parseDgmo(content: string): {
186
186
  diagnostics: DgmoError[];
187
187
  chartType: string | null;
188
+ } {
189
+ // Dedupe at the parse boundary so one offending line never reports the same
190
+ // problem N times — keeps the fix-loop signal clean for the CLI, the editor,
191
+ // and the MCP `validate_diagram` tool.
192
+ const result = parseDgmoUndeduped(content);
193
+ return {
194
+ diagnostics: dedupeDiagnostics(result.diagnostics),
195
+ chartType: result.chartType,
196
+ };
197
+ }
198
+
199
+ function parseDgmoUndeduped(content: string): {
200
+ diagnostics: DgmoError[];
201
+ chartType: string | null;
188
202
  } {
189
203
  const chartType = parseDgmoChartType(content);
190
204
 
Binary file
package/src/echarts.ts CHANGED
@@ -964,7 +964,11 @@ function parseExtendedChartFull(
964
964
 
965
965
  // Catch-all: nothing matched this line
966
966
  result.diagnostics.push(
967
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, 'warning')
967
+ makeDgmoError(
968
+ lineNumber,
969
+ `Unexpected line: '${trimmed}'. Expected a data row ('Label value', space-separated) or a known option.`,
970
+ 'warning'
971
+ )
968
972
  );
969
973
  }
970
974
 
package/src/er/parser.ts CHANGED
@@ -572,7 +572,11 @@ export function parseERDiagram(
572
572
 
573
573
  // Catch-all: nothing matched this line
574
574
  result.diagnostics.push(
575
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, 'warning')
575
+ makeDgmoError(
576
+ lineNumber,
577
+ `Unexpected line: '${trimmed}'. A table is a bare name at indent 0 ('users'); columns are indented and SPACE-separated, no colon ('id int pk'); relationships are indented ('1--* orders'). (§9)`,
578
+ 'warning'
579
+ )
576
580
  );
577
581
  }
578
582
 
@@ -382,7 +382,10 @@ export function parseGantt(
382
382
  // Skip comments inside holidays
383
383
  if (COMMENT_RE.test(line)) continue;
384
384
 
385
- warn(lineNumber, `Unrecognized holiday entry: "${line}"`);
385
+ warn(
386
+ lineNumber,
387
+ `Unrecognized holiday entry: "${line}". Use a date '2024-02-19 Presidents Day' or a range '2024-05-27 -> 2024-05-29 Memorial Weekend'. (§13.3)`
388
+ );
386
389
  continue;
387
390
  }
388
391
  }
@@ -413,7 +416,10 @@ export function parseGantt(
413
416
  lineNumber,
414
417
  });
415
418
  } else {
416
- warn(lineNumber, `Unrecognized era entry: "${line}"`);
419
+ warn(
420
+ lineNumber,
421
+ `Unrecognized era entry: "${line}". Use 'YYYY-MM-DD -> YYYY-MM-DD Label [color]' (e.g. '2026-04-06 -> 2026-04-10 Conference purple'). (§13.5)`
422
+ );
417
423
  }
418
424
  continue;
419
425
  }
@@ -444,7 +450,10 @@ export function parseGantt(
444
450
  lineNumber,
445
451
  });
446
452
  } else {
447
- warn(lineNumber, `Unrecognized marker entry: "${line}"`);
453
+ warn(
454
+ lineNumber,
455
+ `Unrecognized marker entry: "${line}". Use 'YYYY-MM-DD Label [color]' (e.g. '2026-03-27 Board Review' or '2026-06-15 Release green'). (§13.6)`
456
+ );
448
457
  }
449
458
  continue;
450
459
  }
@@ -664,7 +673,7 @@ export function parseGantt(
664
673
  } else {
665
674
  softError(
666
675
  lineNumber,
667
- `Arrow reference "${content.name}" has no parent task context.`
676
+ `Arrow '-> ${content.name}' has no source task — indent it under the task it follows (e.g. 'Design 5bd' then indented '-> ${content.name} 3bd'). (§13)`
668
677
  );
669
678
  }
670
679
  }
@@ -629,7 +629,11 @@ export function parseFlowchart(
629
629
 
630
630
  if (key === 'no-color') {
631
631
  result.diagnostics.push(
632
- makeDgmoError(lineNumber, '"no-color" has been removed.', 'warning')
632
+ makeDgmoError(
633
+ lineNumber,
634
+ '"no-color" was removed — node colors are assigned automatically by shape; delete this line.',
635
+ 'warning'
636
+ )
633
637
  );
634
638
  continue;
635
639
  }
@@ -389,7 +389,11 @@ export function parseState(
389
389
 
390
390
  if (key === 'no-color') {
391
391
  result.diagnostics.push(
392
- makeDgmoError(lineNumber, '"no-color" has been removed.', 'warning')
392
+ makeDgmoError(
393
+ lineNumber,
394
+ '"no-color" was removed — state colors are assigned automatically; delete this line.',
395
+ 'warning'
396
+ )
393
397
  );
394
398
  continue;
395
399
  }
@@ -560,7 +560,7 @@ export function parseInfra(content: string): ParsedInfra {
560
560
  }
561
561
  warn(
562
562
  lineNumber,
563
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
563
+ `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}' — a value is a bare word with an optional trailing color (e.g. 'Production red'); no ':' or '|'.`
564
564
  );
565
565
  continue;
566
566
  }
@@ -654,7 +654,7 @@ export function parseInfra(content: string): ParsedInfra {
654
654
  const n = deprecatedFanout[1];
655
655
  setError(
656
656
  lineNumber,
657
- `'x${n}' fanout syntax is no longer supported — use '| fanout: ${n}' instead`
657
+ `'x${n}' fanout syntax is no longer supported — use 'fanout: ${n}' instead`
658
658
  );
659
659
  continue;
660
660
  }
@@ -869,8 +869,12 @@ export function parseInfra(content: string): ParsedInfra {
869
869
  const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
870
870
  if (hint || valueLooksNumeric) {
871
871
  // Likely a typo — warn
872
- let msg = `Unknown property '${key}'.`;
872
+ let msg = `Unknown infra property '${key}'.`;
873
873
  if (hint) msg += ` ${hint}`;
874
+ else
875
+ msg +=
876
+ ` Properties are colon-keyed 'key: value' from the known schema` +
877
+ ` (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
874
878
  warn(lineNumber, msg);
875
879
  } else if (!currentNode.isEdge) {
876
880
  // Likely prose — collect as description
@@ -884,7 +888,7 @@ export function parseInfra(content: string): ParsedInfra {
884
888
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
885
889
  warn(
886
890
  lineNumber,
887
- `Property '${key}' is only valid on the entry point (Edge/Internet).`
891
+ `Property '${key}' belongs on the entry point — declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
888
892
  );
889
893
  }
890
894
 
@@ -1014,8 +1018,8 @@ export function parseInfra(content: string): ParsedInfra {
1014
1018
  warn(
1015
1019
  lineNumber,
1016
1020
  currentGroup
1017
- ? `Unexpected line: '${trimmed}'. Expected an indented component name.`
1018
- : `Unexpected line: '${trimmed}'. Expected a component name, [Group Name], tag group, or option.`
1021
+ ? `Unexpected line: '${trimmed}'. Inside a group, indent a component name under the '[Group]'.`
1022
+ : `Unexpected line: '${trimmed}'. Expected a node ('Api Gateway'), a group ('[Backend]'), a tag group ('tag Tier as t'), or an option ('default-rps 100'); node properties go indented under a node as 'key: value'.`
1019
1023
  );
1020
1024
  }
1021
1025
 
@@ -378,7 +378,7 @@ export function parseJourneyMap(
378
378
  if (trimmed.includes('|') || !PHASE_RE.test(trimmed)) {
379
379
  warn(
380
380
  lineNumber,
381
- 'Steps outside phases will be ignored when phases are present'
381
+ `Step "${trimmed}" sits outside any phase and is ignored — indent it under a '[Phase]' header (per §22).`
382
382
  );
383
383
  continue;
384
384
  }
@@ -450,7 +450,10 @@ export function parseJourneyMap(
450
450
 
451
451
  // Unrecognized line
452
452
  if (indent > 0 && !currentPhase && hasPhases) {
453
- warn(lineNumber, `Unexpected indented line outside of a phase`);
453
+ warn(
454
+ lineNumber,
455
+ `Indented line outside any phase — add a '[Phase]' header above it, or unindent it to a top-level step. (§22)`
456
+ );
454
457
  }
455
458
  }
456
459
 
@@ -58,7 +58,8 @@ const KNOWN_OPTIONS = new Set(['hide', 'active-tag']);
58
58
  /** Known kanban boolean options (bare keyword = on). */
59
59
  const KNOWN_BOOLEANS = new Set<string>(['solid-fill', 'no-title']);
60
60
  const REMOVED_BOOLEANS: Record<string, string> = {
61
- 'no-auto-color': '"no-auto-color" has been removed.',
61
+ 'no-auto-color':
62
+ '"no-auto-color" was removed — colors are always auto-assigned from the palette; delete this line.',
62
63
  };
63
64
 
64
65
  // ============================================================
@@ -364,7 +365,10 @@ export function parseKanban(
364
365
  }
365
366
 
366
367
  if (!currentColumn) {
367
- warn(lineNumber, 'Card line found before any column');
368
+ warn(
369
+ lineNumber,
370
+ 'Card line found before any column. Declare a column first with \'[Column Name]\', then indent cards beneath it. (§11)'
371
+ );
368
372
  continue;
369
373
  }
370
374
 
@@ -51,6 +51,14 @@ export interface ContextLabelArgs {
51
51
  readonly height: number;
52
52
  readonly waterBodies?: WaterBodies | undefined;
53
53
  readonly countries: readonly CountryCandidate[];
54
+ /** Muted, contrast-floored land tone for country labels — computed in layout
55
+ * against the neutral basemap and shared with the orientation backdrop so a
56
+ * context country and an orientation region read identically. Absent ⇒ the
57
+ * legacy `palette.textMuted`. */
58
+ readonly countryTone?: string | undefined;
59
+ /** Muted, contrast-floored tone for water labels (against the water fill).
60
+ * Absent ⇒ the legacy `mix(blue, textMuted, 50)`. */
61
+ readonly waterTone?: string | undefined;
54
62
  readonly palette: PaletteColors;
55
63
  readonly project: (lon: number, lat: number) => [number, number] | null;
56
64
  /** Collision test against every committed data/region/POI/route obstacle. */
@@ -80,15 +88,15 @@ const EDGE_CLAMP_MARGIN = 8; // px inset for edge-clamped ocean labels
80
88
  const EDGE_CLAMP_OVERSHOOT = 0.35; // max off-frame overshoot (× dim) to still clamp
81
89
 
82
90
  // Country labels scale with their projected footprint: a big landmass (Canada,
83
- // Mexico on a US view) reads as a large, FADED backdrop name; a small one stays
84
- // at the base font, fully muted. Size metric is the footprint's linear extent
85
- // (√bbox-area) as a fraction of the canvas's linear extent (√canvas-area), so
86
- // the ramp is resolution-independent. Below MIN ⇒ base font / no extra fade;
87
- // at/above MAX ⇒ max font / max fade; linear between.
91
+ // Mexico on a US view) reads as a larger backdrop name; a small one stays at the
92
+ // base font. Size metric is the footprint's linear extent (√bbox-area) as a
93
+ // fraction of the canvas's linear extent (√canvas-area), so the ramp is
94
+ // resolution-independent. Below MIN ⇒ base font; at/above MAX ⇒ max font; linear
95
+ // between. Colour does NOT change with size — subordination is by muting (the
96
+ // contrast-floored tone) + size only, never by fading toward bg (Phase 1).
88
97
  const COUNTRY_FONT_MAX = 22; // px ceiling for the largest footprint
89
98
  const COUNTRY_SIZE_FRAC_MIN = 0.06; // footprint linear-frac at base font
90
99
  const COUNTRY_SIZE_FRAC_MAX = 0.32; // footprint linear-frac at max font
91
- const COUNTRY_FADE_MAX = 45; // % blend toward bg at max font (subdue big names)
92
100
 
93
101
  // Multi-position country dodging (map-context-neighbor-labels). A country gets
94
102
  // several interior on-its-own-land positions so its label can dodge a colliding
@@ -287,6 +295,8 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
287
295
  waterBodies,
288
296
  countries,
289
297
  palette,
298
+ countryTone,
299
+ waterTone,
290
300
  project,
291
301
  collides,
292
302
  contentPoints,
@@ -306,10 +316,14 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
306
316
  const budget = labelBudget(width, height, band);
307
317
  if (budget <= 0) return [];
308
318
 
309
- // Subordinate cartographic colours (palette-derived, no hex; resvg-safe via
310
- // pre-computed mix()). Water = muted blue-gray italic; country = muted gray.
311
- const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
312
- const countryColor = palette.textMuted;
319
+ // Subordinate cartographic colours. Both are muted but CONTRAST-FLOORED in the
320
+ // caller (layout) against the substrate they sit on, and shared with the
321
+ // orientation backdrop so a context country and an orientation region match.
322
+ // Water stays the blue-gray italic convention; country = the muted land tone.
323
+ // Fallbacks keep the pre-floor behaviour for unit tests that omit the tones.
324
+ const waterColor =
325
+ waterTone ?? mix(palette.colors.blue, palette.textMuted, 50);
326
+ const countryColor = countryTone ?? palette.textMuted;
313
327
  const haloColor = palette.bg;
314
328
 
315
329
  type Candidate = {
@@ -475,15 +489,11 @@ export function placeContextLabels(args: ContextLabelArgs): PlacedLabel[] {
475
489
  )
476
490
  );
477
491
  const fontSize = Math.round(FONT + t * (COUNTRY_FONT_MAX - FONT));
478
- const fade = Math.round(t * COUNTRY_FADE_MAX);
479
- // Blend `fade`% TOWARD the bg (subdue big names). `mix(a,b,pct)` weights `a` by
480
- // `pct`, so the bg fraction must be `100 - fade` — i.e. a small country (fade≈0)
481
- // stays fully muted/dark and only a big landmass fades to a soft backdrop. (The
482
- // earlier `mix(countryColor, bg, fade)` was inverted: it lightened the SMALL
483
- // names toward white instead, which read as illegible once proximity started
484
- // surfacing small neighbours like Belarus/Georgia.)
485
- const color =
486
- fade > 0 ? mix(countryColor, palette.bg, 100 - fade) : countryColor;
492
+ // No fade. Subordination is by MUTING (the floored tone) + smaller size, not
493
+ // by fading toward bg — fading a floored tone toward the background would
494
+ // re-break the contrast it was floored to guarantee (the old behaviour washed
495
+ // big names like Canada toward illegibility). Footprint still drives font size.
496
+ const color = countryColor;
487
497
  // Always the full country name — never an ISO abbreviation. If the name
488
498
  // doesn't fit the footprint, drop the label rather than abbreviate.
489
499
  const text = c.name;