@diagrammo/dgmo 0.30.0 → 0.32.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 (85) hide show
  1. package/.cursorrules +4 -1
  2. package/.github/copilot-instructions.md +4 -1
  3. package/.windsurfrules +4 -1
  4. package/README.md +21 -3
  5. package/SKILL.md +4 -1
  6. package/dist/advanced.cjs +1853 -623
  7. package/dist/advanced.d.cts +143 -16
  8. package/dist/advanced.d.ts +143 -16
  9. package/dist/advanced.js +1846 -623
  10. package/dist/auto.cjs +1640 -581
  11. package/dist/auto.js +99 -99
  12. package/dist/auto.mjs +1640 -581
  13. package/dist/cli.cjs +148 -147
  14. package/dist/index.cjs +1643 -662
  15. package/dist/index.js +1643 -662
  16. package/docs/ai-integration.md +4 -1
  17. package/docs/language-reference.md +282 -27
  18. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  19. package/gallery/fixtures/c4-full.dgmo +4 -5
  20. package/gallery/fixtures/c4.dgmo +2 -3
  21. package/package.json +7 -1
  22. package/src/advanced.ts +10 -0
  23. package/src/boxes-and-lines/focus.ts +257 -0
  24. package/src/boxes-and-lines/layout-search.ts +345 -65
  25. package/src/boxes-and-lines/layout.ts +11 -1
  26. package/src/boxes-and-lines/parser.ts +97 -4
  27. package/src/boxes-and-lines/renderer.ts +111 -8
  28. package/src/boxes-and-lines/types.ts +9 -0
  29. package/src/c4/parser.ts +8 -7
  30. package/src/c4/renderer.ts +7 -5
  31. package/src/chart-type-registry.ts +129 -4
  32. package/src/chart-types.ts +3 -3
  33. package/src/chart.ts +18 -1
  34. package/src/class/renderer.ts +4 -2
  35. package/src/cli-banner.ts +107 -0
  36. package/src/cli.ts +13 -0
  37. package/src/colors.ts +247 -2
  38. package/src/cycle/parser.ts +2 -7
  39. package/src/d3.ts +67 -54
  40. package/src/diagnostics.ts +17 -0
  41. package/src/dimensions.ts +9 -13
  42. package/src/echarts.ts +42 -14
  43. package/src/er/parser.ts +6 -1
  44. package/src/er/renderer.ts +4 -2
  45. package/src/gantt/parser.ts +44 -7
  46. package/src/graph/flowchart-parser.ts +77 -3
  47. package/src/graph/flowchart-renderer.ts +4 -2
  48. package/src/graph/state-renderer.ts +6 -4
  49. package/src/infra/parser.ts +80 -0
  50. package/src/infra/renderer.ts +8 -4
  51. package/src/journey-map/parser.ts +23 -8
  52. package/src/journey-map/renderer.ts +1 -1
  53. package/src/kanban/parser.ts +8 -7
  54. package/src/kanban/renderer.ts +1 -1
  55. package/src/map/context-labels.ts +134 -27
  56. package/src/map/geo.ts +10 -2
  57. package/src/map/layout.ts +259 -4
  58. package/src/map/parser.ts +2 -0
  59. package/src/map/renderer.ts +49 -25
  60. package/src/map/resolver.ts +68 -19
  61. package/src/mindmap/parser.ts +15 -7
  62. package/src/mindmap/renderer.ts +55 -15
  63. package/src/org/parser.ts +8 -7
  64. package/src/org/renderer.ts +89 -127
  65. package/src/palettes/color-utils.ts +19 -4
  66. package/src/palettes/index.ts +1 -0
  67. package/src/pert/renderer.ts +15 -10
  68. package/src/pyramid/parser.ts +2 -7
  69. package/src/quadrant/renderer.ts +2 -2
  70. package/src/raci/parser.ts +2 -7
  71. package/src/raci/renderer.ts +5 -5
  72. package/src/ring/parser.ts +2 -7
  73. package/src/sequence/parser.ts +18 -7
  74. package/src/sequence/renderer.ts +4 -4
  75. package/src/sitemap/parser.ts +8 -7
  76. package/src/sitemap/renderer.ts +37 -39
  77. package/src/tech-radar/parser.ts +2 -7
  78. package/src/timeline/renderer.ts +15 -5
  79. package/src/utils/card.ts +183 -0
  80. package/src/utils/parsing.ts +13 -1
  81. package/src/utils/scaling.ts +38 -81
  82. package/src/utils/tag-groups.ts +48 -10
  83. package/src/utils/visual-conventions.ts +61 -0
  84. package/src/visualizations/parse.ts +6 -1
  85. package/src/wireframe/parser.ts +6 -1
package/src/chart.ts CHANGED
@@ -405,7 +405,9 @@ export function parseChart(
405
405
  if (dataValues) {
406
406
  const { label: rawLabel, color: pointColor } = extractColor(
407
407
  dataValues.label,
408
- palette
408
+ palette,
409
+ result.diagnostics,
410
+ lineNumber
409
411
  );
410
412
  const [first, ...rest] = dataValues.values;
411
413
  result.data.push({
@@ -485,6 +487,21 @@ export function parseChart(
485
487
  );
486
488
  }
487
489
 
490
+ // Plain "bar" renders a single series per row — extra series are dropped
491
+ // silently by the renderer. Surface that instead of losing data quietly so
492
+ // the author switches to a multi-series type. (multi-line/line parse to
493
+ // type "line" and DO render every series, so they are unaffected.)
494
+ if (
495
+ !result.error &&
496
+ result.type === 'bar' &&
497
+ (result.seriesNames?.length ?? 0) > 1
498
+ ) {
499
+ warn(
500
+ result.seriesLineNumber ?? 1,
501
+ `Plain "bar" shows only the first series ("${result.seriesNames![0]}"); the other ${result.seriesNames!.length - 1} are dropped at render. Use "bar-stacked" for stacked bars or "multi-line" to plot every series.`
502
+ );
503
+ }
504
+
488
505
  if (!result.error && result.seriesNames) {
489
506
  const expectedCount = result.seriesNames.length;
490
507
  for (const dp of result.data) {
@@ -47,8 +47,10 @@ const MAX_SCALE = 3;
47
47
  const CLASS_FONT_SIZE = 13;
48
48
  const MEMBER_FONT_SIZE = 11;
49
49
  const EDGE_LABEL_FONT_SIZE = 11;
50
- const EDGE_STROKE_WIDTH = 1.5;
51
- const NODE_STROKE_WIDTH = 1.5;
50
+ import {
51
+ EDGE_STROKE_WIDTH,
52
+ NODE_STROKE_WIDTH,
53
+ } from '../utils/visual-conventions'; // shared (Story 111.1)
52
54
  const MEMBER_LINE_HEIGHT = 18;
53
55
  const COMPARTMENT_PADDING_Y = 8;
54
56
  const MEMBER_PADDING_X = 10;
@@ -0,0 +1,107 @@
1
+ // ============================================================
2
+ // CLI Banner — colorful ASCII logo for `dgmo`
3
+ // ============================================================
4
+ //
5
+ // Rendered as the header of `dgmo --help` and at the end of the
6
+ // install/init flows. Uses a horizontal true-color (24-bit) gradient
7
+ // sampled from the default `slate` palette (corporate blue → teal →
8
+ // steel cyan) so the wordmark matches the brand.
9
+ //
10
+ // Honors the same guards as `dgmo cat`: color only when stdout is a TTY
11
+ // and NO_COLOR is unset; otherwise prints plain ASCII so pipes/CI stay
12
+ // clean.
13
+
14
+ import { getPalette } from './palettes';
15
+
16
+ // ANSI Shadow letterform for "dgmo" (6 rows) + tagline.
17
+ const ART = [
18
+ '██████╗ ██████╗ ███╗ ███╗ ██████╗ ',
19
+ '██╔══██╗██╔════╝ ████╗ ████║██╔═══██╗',
20
+ '██║ ██║██║ ███╗██╔████╔██║██║ ██║',
21
+ '██║ ██║██║ ██║██║╚██╔╝██║██║ ██║',
22
+ '██████╔╝╚██████╔╝██║ ╚═╝ ██║╚██████╔╝',
23
+ '╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ',
24
+ ];
25
+
26
+ const TAGLINE = 'diagrams as code';
27
+
28
+ // Gradient stops, pulled by name from the live slate palette (light) so the
29
+ // wordmark tracks any palette edit instead of drifting from a hardcoded copy.
30
+ // blue → teal → green → amber: a vivid sweep across the categorical hues.
31
+ const GRADIENT_COLOR_NAMES = ['blue', 'teal', 'green', 'orange'] as const;
32
+
33
+ function hexToRgb(hex: string): [number, number, number] {
34
+ const h = hex.replace('#', '');
35
+ return [
36
+ parseInt(h.slice(0, 2), 16),
37
+ parseInt(h.slice(2, 4), 16),
38
+ parseInt(h.slice(4, 6), 16),
39
+ ];
40
+ }
41
+
42
+ const STOPS: Array<[number, number, number]> = (() => {
43
+ const colors = getPalette('slate').light.colors;
44
+ return GRADIENT_COLOR_NAMES.map((name) => hexToRgb(colors[name]));
45
+ })();
46
+
47
+ function lerp(a: number, b: number, t: number): number {
48
+ return Math.round(a + (b - a) * t);
49
+ }
50
+
51
+ /** Interpolate the multi-stop gradient at position t ∈ [0, 1]. */
52
+ function gradientAt(t: number): [number, number, number] {
53
+ const clamped = Math.max(0, Math.min(1, t));
54
+ const span = STOPS.length - 1;
55
+ const scaled = clamped * span;
56
+ const i = Math.min(span - 1, Math.floor(scaled));
57
+ const local = scaled - i;
58
+ const [r1, g1, b1] = STOPS[i]!;
59
+ const [r2, g2, b2] = STOPS[i + 1]!;
60
+ return [lerp(r1, r2, local), lerp(g1, g2, local), lerp(b1, b2, local)];
61
+ }
62
+
63
+ function fg(r: number, g: number, b: number): string {
64
+ return `\x1b[38;2;${r};${g};${b}m`;
65
+ }
66
+
67
+ const RESET = '\x1b[0m';
68
+
69
+ export interface BannerOptions {
70
+ /** Force-disable color regardless of TTY (default honors stdout TTY + NO_COLOR). */
71
+ color?: boolean;
72
+ }
73
+
74
+ /**
75
+ * Build the dgmo banner string. Colorized with a horizontal gradient when
76
+ * `color` is true; otherwise returns plain ASCII.
77
+ */
78
+ export function renderBanner(opts: BannerOptions = {}): string {
79
+ const useColor =
80
+ opts.color ?? (process.stdout.isTTY === true && !process.env['NO_COLOR']);
81
+
82
+ const width = Math.max(...ART.map((line) => line.length));
83
+
84
+ const lines = ART.map((line) => {
85
+ if (!useColor) return line;
86
+ let out = '';
87
+ for (let col = 0; col < line.length; col++) {
88
+ const ch = line[col];
89
+ // Don't paint spaces — keeps escape sequences minimal.
90
+ if (ch === ' ') {
91
+ out += ch;
92
+ continue;
93
+ }
94
+ const [r, g, b] = gradientAt(col / (width - 1));
95
+ out += fg(r, g, b) + ch;
96
+ }
97
+ return out + RESET;
98
+ });
99
+
100
+ // Right-align the tagline under the wordmark, muted.
101
+ const pad = Math.max(0, width - TAGLINE.length);
102
+ const tagline = useColor
103
+ ? `${' '.repeat(pad)}\x1b[2;3m${TAGLINE}${RESET}`
104
+ : `${' '.repeat(pad)}${TAGLINE}`;
105
+
106
+ return ['', ...lines, tagline, ''].join('\n');
107
+ }
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  migrateFile,
26
26
  } from './migrate';
27
27
  import { migrateEmbedded } from './migrate/embedded';
28
+ import { renderBanner } from './cli-banner';
28
29
 
29
30
  // Derived from the palette registry so new palettes are auto-included.
30
31
  const PALETTES = getAvailablePalettes().map((p) => p.id);
@@ -464,6 +465,7 @@ For architecture diagrams, sequence diagrams, flowcharts, and charts, use the \`
464
465
  `;
465
466
 
466
467
  function printHelp(): void {
468
+ console.log(renderBanner());
467
469
  console.log(`Usage: dgmo <input> [options]
468
470
  cat input.dgmo | dgmo [options]
469
471
  dgmo cat <file> Display file with syntax highlighting
@@ -648,10 +650,12 @@ function svgToPng(svg: string, background?: string): Buffer {
648
650
  }
649
651
 
650
652
  function noInput(): never {
653
+ console.error(renderBanner());
651
654
  const samplePath = resolve('sample.dgmo');
652
655
  if (existsSync(samplePath)) {
653
656
  console.error('Error: No input file specified');
654
657
  console.error(`Try: dgmo ${basename(samplePath)}`);
658
+ console.error('Run dgmo --help for all options.');
655
659
  process.exit(1);
656
660
  }
657
661
  writeFileSync(
@@ -925,6 +929,15 @@ async function main(): Promise<void> {
925
929
  return;
926
930
  }
927
931
 
932
+ if (
933
+ opts.installClaudeCodeIntegration ||
934
+ opts.installClaudeSkill ||
935
+ opts.installCodexIntegration ||
936
+ opts.installClaudeDesktopIntegration
937
+ ) {
938
+ console.log(renderBanner());
939
+ }
940
+
928
941
  if (opts.installClaudeCodeIntegration) {
929
942
  const claudeDir = join(homedir(), '.claude');
930
943
  if (!existsSync(claudeDir)) {
package/src/colors.ts CHANGED
@@ -60,6 +60,28 @@ export const RECOGNIZED_COLOR_NAMES = Object.freeze([
60
60
  'white',
61
61
  ] as const);
62
62
 
63
+ /**
64
+ * The canonical order in which the categorical (non-neutral) color names are
65
+ * auto-assigned: tag/group swatches (`autoTagColorCycle`) and data-chart
66
+ * series colors (`getSeriesColors`) both derive their rotation from this list.
67
+ *
68
+ * Seeded with the RGB primaries (`red, green, blue`) for an unmistakable first
69
+ * three, then each subsequent hue is chosen to fill the widest remaining gap on
70
+ * the color wheel — maximizing contrast between adjacent swatches. Neutrals
71
+ * (`gray`/`black`/`white`) are intentionally excluded so auto-picked colors
72
+ * always read as distinct legend swatches.
73
+ */
74
+ export const CATEGORICAL_COLOR_ORDER = Object.freeze([
75
+ 'red',
76
+ 'green',
77
+ 'blue',
78
+ 'yellow',
79
+ 'teal',
80
+ 'purple',
81
+ 'orange',
82
+ 'cyan',
83
+ ] as const);
84
+
63
85
  /**
64
86
  * Returns true iff `name` is one of the 11 recognized DGMO color names.
65
87
  */
@@ -95,6 +117,211 @@ export function resolveColor(
95
117
  import type { DgmoError } from './diagnostics';
96
118
  import { makeDgmoError, suggest } from './diagnostics';
97
119
 
120
+ /**
121
+ * Stable diagnostic code for "this token is not one of the 11 named palette
122
+ * colors" — covers both hex/CSS literals (emitted as `error`) and unrecognized
123
+ * bare words like `crimson` (emitted as `warning`). Consumers that want to
124
+ * HARD-BLOCK invalid colors regardless of severity (e.g. the MCP render gate)
125
+ * filter on this code rather than re-deriving the rule.
126
+ */
127
+ export const INVALID_COLOR_CODE = 'E_INVALID_COLOR';
128
+
129
+ /**
130
+ * CSS / X11 color names that are NOT one of DGMO's 11 — mapped to their hex so
131
+ * a "nearest valid color" hint can be computed. This is the blocklist that lets
132
+ * the trailing-token rule tell an *intended-but-invalid* color (`pink`,
133
+ * `crimson`, `navy`) apart from an ordinary label word (`Zinfandel`, `Blanc`):
134
+ * a lowercase trailing token found here is flagged, anything else stays label
135
+ * text. Our 11 valid names are deliberately excluded. Extend freely — it only
136
+ * sharpens detection. (Case-sensitive lowercase, matching the §1.5 color rule.)
137
+ */
138
+ export const INVALID_CSS_COLOR_HEX: Readonly<Record<string, string>> =
139
+ Object.freeze({
140
+ pink: '#ffc0cb',
141
+ hotpink: '#ff69b4',
142
+ deeppink: '#ff1493',
143
+ lightpink: '#ffb6c1',
144
+ palevioletred: '#db7093',
145
+ crimson: '#dc143c',
146
+ scarlet: '#ff2400',
147
+ firebrick: '#b22222',
148
+ darkred: '#8b0000',
149
+ maroon: '#800000',
150
+ salmon: '#fa8072',
151
+ lightsalmon: '#ffa07a',
152
+ darksalmon: '#e9967a',
153
+ coral: '#ff7f50',
154
+ lightcoral: '#f08080',
155
+ tomato: '#ff6347',
156
+ orangered: '#ff4500',
157
+ darkorange: '#ff8c00',
158
+ gold: '#ffd700',
159
+ goldenrod: '#daa520',
160
+ darkgoldenrod: '#b8860b',
161
+ khaki: '#f0e68c',
162
+ darkkhaki: '#bdb76b',
163
+ amber: '#ffbf00',
164
+ lavender: '#e6e6fa',
165
+ violet: '#ee82ee',
166
+ magenta: '#ff00ff',
167
+ fuchsia: '#ff00ff',
168
+ orchid: '#da70d6',
169
+ plum: '#dda0dd',
170
+ indigo: '#4b0082',
171
+ navy: '#000080',
172
+ midnightblue: '#191970',
173
+ darkblue: '#00008b',
174
+ mediumblue: '#0000cd',
175
+ royalblue: '#4169e1',
176
+ cornflowerblue: '#6495ed',
177
+ dodgerblue: '#1e90ff',
178
+ deepskyblue: '#00bfff',
179
+ skyblue: '#87ceeb',
180
+ lightskyblue: '#87cefa',
181
+ lightblue: '#add8e6',
182
+ powderblue: '#b0e0e6',
183
+ steelblue: '#4682b4',
184
+ slateblue: '#6a5acd',
185
+ cadetblue: '#5f9ea0',
186
+ turquoise: '#40e0d0',
187
+ aqua: '#00ffff',
188
+ aquamarine: '#7fffd4',
189
+ lime: '#00ff00',
190
+ limegreen: '#32cd32',
191
+ lightgreen: '#90ee90',
192
+ palegreen: '#98fb98',
193
+ seagreen: '#2e8b57',
194
+ mediumseagreen: '#3cb371',
195
+ forestgreen: '#228b22',
196
+ darkgreen: '#006400',
197
+ olive: '#808000',
198
+ olivedrab: '#6b8e23',
199
+ darkolivegreen: '#556b2f',
200
+ chartreuse: '#7fff00',
201
+ lawngreen: '#7cfc00',
202
+ springgreen: '#00ff7f',
203
+ greenyellow: '#adff2f',
204
+ brown: '#a52a2a',
205
+ sienna: '#a0522d',
206
+ chocolate: '#d2691e',
207
+ peru: '#cd853f',
208
+ tan: '#d2b48c',
209
+ beige: '#f5f5dc',
210
+ wheat: '#f5deb3',
211
+ ivory: '#fffff0',
212
+ silver: '#c0c0c0',
213
+ lightgray: '#d3d3d3',
214
+ lightgrey: '#d3d3d3',
215
+ darkgray: '#a9a9a9',
216
+ darkgrey: '#a9a9a9',
217
+ dimgray: '#696969',
218
+ dimgrey: '#696969',
219
+ slategray: '#708090',
220
+ slategrey: '#708090',
221
+ gainsboro: '#dcdcdc',
222
+ grey: '#808080',
223
+ });
224
+
225
+ /**
226
+ * Best-effort nearest recognized color NAME for an unsupported hex value.
227
+ * Matches by HUE (with a low-saturation cutoff routing to black/white/gray),
228
+ * NOT raw RGB distance to the muted palette hexes — vivid LLM colors like a
229
+ * `#3cb44b` green would otherwise snap to `gray` against a desaturated sage.
230
+ * Returns null for non-hex input (CSS function/keyword colors, no RGB to read).
231
+ * Used ONLY to enrich a diagnostic — never to silently accept the value.
232
+ */
233
+ export function nearestNamedColor(input: string): string | null {
234
+ // CSS color name → resolve to its hex first so hue-matching works on it too.
235
+ const cssHex = INVALID_CSS_COLOR_HEX[input.trim().toLowerCase()];
236
+ if (cssHex) input = cssHex;
237
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(input.trim());
238
+ if (!m) return null;
239
+ let h = m[1]!.toLowerCase();
240
+ if (h.length === 3)
241
+ h = h
242
+ .split('')
243
+ .map((c) => c + c)
244
+ .join('');
245
+ const r = parseInt(h.slice(0, 2), 16) / 255;
246
+ const g = parseInt(h.slice(2, 4), 16) / 255;
247
+ const b = parseInt(h.slice(4, 6), 16) / 255;
248
+ const max = Math.max(r, g, b);
249
+ const min = Math.min(r, g, b);
250
+ const delta = max - min;
251
+ const l = (max + min) / 2;
252
+ const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
253
+ // Low chroma → a neutral; pick by lightness.
254
+ if (s < 0.15) {
255
+ if (l < 0.2) return 'black';
256
+ if (l > 0.85) return 'white';
257
+ return 'gray';
258
+ }
259
+ let hue: number;
260
+ if (max === r) hue = 60 * (((g - b) / delta) % 6);
261
+ else if (max === g) hue = 60 * ((b - r) / delta + 2);
262
+ else hue = 60 * ((r - g) / delta + 4);
263
+ if (hue < 0) hue += 360;
264
+ // Canonical hue anchors for the 8 chromatic names (red appears at both 0 and
265
+ // 360 so wrap-around resolves correctly).
266
+ const anchors: readonly [string, number][] = [
267
+ ['red', 0],
268
+ ['orange', 30],
269
+ ['yellow', 55],
270
+ ['green', 120],
271
+ ['teal', 170],
272
+ ['cyan', 190],
273
+ ['blue', 225],
274
+ ['purple', 285],
275
+ ['red', 360],
276
+ ];
277
+ let best = 'red';
278
+ let bestD = Infinity;
279
+ for (const [name, deg] of anchors) {
280
+ const d = Math.abs(hue - deg);
281
+ if (d < bestD) {
282
+ bestD = d;
283
+ best = name;
284
+ }
285
+ }
286
+ return best;
287
+ }
288
+
289
+ /**
290
+ * True iff `token` is an INTENDED-but-invalid color: a hex/`rgb()`/`hsl()`
291
+ * literal, or a known CSS color name that isn't one of DGMO's 11. Lets the
292
+ * trailing-token rule flag `Rosé pink` / `Foo #e6194b` while leaving genuine
293
+ * label words (`Zinfandel`) untouched.
294
+ */
295
+ export function isInvalidColorToken(token: string): boolean {
296
+ if (/^(#|rgba?\(|hsla?\()/i.test(token)) return true;
297
+ const lower = token.toLowerCase();
298
+ return (
299
+ INVALID_CSS_COLOR_HEX[lower] !== undefined && !isRecognizedColorName(lower)
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Build an `E_INVALID_COLOR` diagnostic for an intended-but-invalid trailing
305
+ * color token, or return null if `token` isn't color-like (so it stays label
306
+ * text). Severity is `warning` so the library degrades gracefully (the value
307
+ * just keeps the word); the MCP render gate blocks on the code regardless.
308
+ * Used by `extractColor` to close the trailing-token "silent swallow" gap.
309
+ */
310
+ export function invalidColorDiagnostic(
311
+ token: string,
312
+ line: number
313
+ ): DgmoError | null {
314
+ if (!isInvalidColorToken(token)) return null;
315
+ const nearest = nearestNamedColor(token);
316
+ const near = nearest ? ` Nearest: ${nearest}.` : '';
317
+ return makeDgmoError(
318
+ line,
319
+ `Color "${token}" is not a valid DGMO color — DGMO accepts only these 11 named colors: ${RECOGNIZED_COLOR_NAMES.join(', ')} (no hex, no CSS color names).${near}`,
320
+ 'warning',
321
+ INVALID_COLOR_CODE
322
+ );
323
+ }
324
+
98
325
  /**
99
326
  * Resolves a color name and pushes a warning diagnostic on failure.
100
327
  * Returns the hex string for valid names, or `undefined` for unknown
@@ -109,13 +336,31 @@ export function resolveColorWithDiagnostic(
109
336
  ): string | undefined {
110
337
  const resolved = resolveColor(color, palette);
111
338
  if (resolved !== null) return resolved;
339
+ // Literal color values (hex `#e6194b`, `rgb(...)`, `hsl(...)`) are NOT part
340
+ // of the DGMO language — only the 11 named palette colors are accepted, so
341
+ // that a single source recolors correctly across every palette and theme.
342
+ // Flag these with a precise error rather than a typo suggestion.
343
+ if (/^(#|rgba?\(|hsla?\()/i.test(color)) {
344
+ const nearest = nearestNamedColor(color);
345
+ const near = nearest ? ` Nearest: ${nearest}.` : '';
346
+ diagnostics.push(
347
+ makeDgmoError(
348
+ line,
349
+ `Color "${color}" is not supported — DGMO does not accept hex or CSS color values. Use a named palette color: ${RECOGNIZED_COLOR_NAMES.join(', ')}.${near}`,
350
+ 'error',
351
+ INVALID_COLOR_CODE
352
+ )
353
+ );
354
+ return undefined;
355
+ }
112
356
  const hint = suggest(color, RECOGNIZED_COLOR_NAMES as readonly string[]);
113
357
  const suggestion = hint ? ` ${hint}` : '';
114
358
  diagnostics.push(
115
359
  makeDgmoError(
116
360
  line,
117
- `Unknown color "${color}". Allowed: ${RECOGNIZED_COLOR_NAMES.join(', ')}.${suggestion}`,
118
- 'warning'
361
+ `Unknown color "${color}". DGMO accepts only these 11 named colors: ${RECOGNIZED_COLOR_NAMES.join(', ')} (no hex, no CSS color names).${suggestion}`,
362
+ 'warning',
363
+ INVALID_COLOR_CODE
119
364
  )
120
365
  );
121
366
  return undefined;
@@ -3,8 +3,8 @@
3
3
  // ============================================================
4
4
 
5
5
  import {
6
- formatDgmoError,
7
6
  makeDgmoError,
7
+ makeFail,
8
8
  METADATA_DIAGNOSTIC_CODES,
9
9
  pipeOperatorRemovedMessage,
10
10
  } from '../diagnostics';
@@ -64,12 +64,7 @@ export function parseCycle(content: string): ParsedCycle {
64
64
  let currentEdge: Writable<CycleEdge> | null = null;
65
65
  // nodeBaseIndent tracking removed — indent-based nesting not used in cycle
66
66
 
67
- const fail = (line: number, message: string): ParsedCycle => {
68
- const diag = makeDgmoError(line, message);
69
- result.diagnostics.push(diag);
70
- result.error = formatDgmoError(diag);
71
- return result;
72
- };
67
+ const fail = makeFail(result);
73
68
 
74
69
  const warn = (
75
70
  line: number,