@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
@@ -10,7 +10,12 @@ import {
10
10
  } from '../utils/export-container';
11
11
  import { ScaleContext } from '../utils/scaling';
12
12
  import type { PaletteColors } from '../palettes';
13
- import { contrastText, mix, shapeFill } from '../palettes/color-utils';
13
+ import {
14
+ contrastText,
15
+ mix,
16
+ shapeFill,
17
+ themeBaseBg,
18
+ } from '../palettes/color-utils';
14
19
  import { resolveTagColor } from '../utils/tag-groups';
15
20
  import type { ParsedOrg } from './parser';
16
21
  import type { OrgLayoutResult } from './layout';
@@ -26,6 +31,7 @@ import {
26
31
  EYE_CLOSED_PATH,
27
32
  } from '../utils/legend-constants';
28
33
  import { renderIntegratedLegend } from '../utils/legend-integration';
34
+ import { renderNodeCard, renderCollapseBar } from '../utils/card';
29
35
  import { measureText } from '../utils/text-measure';
30
36
  import { getMaxLegendReservedHeight } from '../utils/legend-layout';
31
37
  import type { LegendConfig } from '../utils/legend-types';
@@ -38,23 +44,25 @@ const DIAGRAM_PADDING = 20;
38
44
  const MAX_SCALE = 3;
39
45
  import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
40
46
  const TITLE_HEIGHT = 30;
41
- const LABEL_FONT_SIZE = 13;
42
- const META_FONT_SIZE = 11;
43
- const META_LINE_HEIGHT = 16;
44
- const HEADER_HEIGHT = 28;
45
- const SEPARATOR_GAP = 6;
46
- const EDGE_STROKE_WIDTH = 1.5;
47
- const NODE_STROKE_WIDTH = 1.5;
48
- const CARD_RADIUS = 6;
49
- const CONTAINER_RADIUS = 8;
50
- const CONTAINER_LABEL_FONT_SIZE = 13;
51
- const CONTAINER_META_FONT_SIZE = 11;
52
- const CONTAINER_META_LINE_HEIGHT = 16;
53
- const CONTAINER_HEADER_HEIGHT = 28;
54
-
55
- // Collapsed-node accent bar
56
- const COLLAPSE_BAR_HEIGHT = 6;
57
- const COLLAPSE_BAR_INSET = 0;
47
+ // Shared card / group / collapse constants (Story 111.1). org matches every
48
+ // convention default, so it imports the full set.
49
+ import {
50
+ LABEL_FONT_SIZE,
51
+ META_FONT_SIZE,
52
+ META_LINE_HEIGHT,
53
+ HEADER_HEIGHT,
54
+ SEPARATOR_GAP,
55
+ EDGE_STROKE_WIDTH,
56
+ NODE_STROKE_WIDTH,
57
+ CARD_RADIUS,
58
+ CONTAINER_RADIUS,
59
+ CONTAINER_LABEL_FONT_SIZE,
60
+ CONTAINER_META_FONT_SIZE,
61
+ CONTAINER_META_LINE_HEIGHT,
62
+ CONTAINER_HEADER_HEIGHT,
63
+ COLLAPSE_BAR_HEIGHT,
64
+ COLLAPSE_BAR_INSET,
65
+ } from '../utils/visual-conventions';
58
66
 
59
67
  // Ancestor breadcrumb trail (focus mode)
60
68
  const ANCESTOR_DOT_R = 4;
@@ -90,7 +98,7 @@ function containerFill(
90
98
  nodeColor?: string
91
99
  ): string {
92
100
  if (nodeColor) {
93
- return mix(nodeColor, isDark ? palette.surface : palette.bg, 10);
101
+ return mix(nodeColor, themeBaseBg(palette, isDark), 10);
94
102
  }
95
103
  return mix(palette.surface, palette.bg, 40);
96
104
  }
@@ -360,21 +368,16 @@ export function renderOrg(
360
368
  }
361
369
 
362
370
  if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
363
- const clipId = `clip-${c.nodeId}`;
364
- cG.append('clipPath')
365
- .attr('id', clipId)
366
- .append('rect')
367
- .attr('width', c.width)
368
- .attr('height', c.height)
369
- .attr('rx', sContainerRadius);
370
- cG.append('rect')
371
- .attr('x', sCollapseBarInset)
372
- .attr('y', c.height - sCollapseBarHeight)
373
- .attr('width', c.width - sCollapseBarInset * 2)
374
- .attr('height', sCollapseBarHeight)
375
- .attr('fill', containerStroke(palette, colorOff ? undefined : c.color))
376
- .attr('clip-path', `url(#${clipId})`)
377
- .attr('class', 'org-collapse-bar');
371
+ renderCollapseBar(cG, {
372
+ width: c.width,
373
+ height: c.height,
374
+ barHeight: sCollapseBarHeight,
375
+ inset: sCollapseBarInset,
376
+ rx: sContainerRadius,
377
+ fill: containerStroke(palette, colorOff ? undefined : c.color),
378
+ clipId: `clip-${c.nodeId}`,
379
+ className: 'org-collapse-bar',
380
+ });
378
381
  }
379
382
 
380
383
  // Focus icon (hover-reveal, interactive only) — for non-root containers with children
@@ -399,6 +402,13 @@ export function renderOrg(
399
402
  .attr('height', iconSize + 6)
400
403
  .attr('fill', 'transparent');
401
404
 
405
+ // Use the container's contrast text color so it stays legible on darker
406
+ // fills, mirroring the node icon.
407
+ const iconColor = contrastText(
408
+ fill,
409
+ palette.textOnFillLight,
410
+ palette.textOnFillDark
411
+ );
402
412
  const cx = iconSize / 2;
403
413
  const cy = iconSize / 2;
404
414
  focusG
@@ -407,14 +417,14 @@ export function renderOrg(
407
417
  .attr('cy', cy)
408
418
  .attr('r', iconSize / 2 - 1)
409
419
  .attr('fill', 'none')
410
- .attr('stroke', palette.textMuted)
420
+ .attr('stroke', iconColor)
411
421
  .attr('stroke-width', 1.5);
412
422
  focusG
413
423
  .append('circle')
414
424
  .attr('cx', cx)
415
425
  .attr('cy', cy)
416
426
  .attr('r', 2)
417
- .attr('fill', palette.textMuted);
427
+ .attr('fill', iconColor);
418
428
  }
419
429
  }
420
430
 
@@ -489,103 +499,52 @@ export function renderOrg(
489
499
  );
490
500
  const stroke = nodeStroke(palette, colorOff ? undefined : node.color);
491
501
 
492
- const rect = nodeG
493
- .append('rect')
494
- .attr('x', 0)
495
- .attr('y', 0)
496
- .attr('width', node.width)
497
- .attr('height', node.height)
498
- .attr('rx', sCardRadius)
499
- .attr('fill', fill)
500
- .attr('stroke', stroke)
501
- .attr('stroke-width', sNodeStrokeWidth);
502
-
503
- if (node.isContainer) {
504
- rect.attr('stroke-dasharray', '6 3');
505
- }
506
-
507
502
  const labelColor = contrastText(
508
503
  fill,
509
504
  palette.textOnFillLight,
510
505
  palette.textOnFillDark
511
506
  );
512
- nodeG
513
- .append('text')
514
- .attr('x', node.width / 2)
515
- .attr('y', sHeaderHeight / 2 + sLabelFontSize / 2 - 2)
516
- .attr('text-anchor', 'middle')
517
- .attr('fill', labelColor)
518
- .attr('font-size', sLabelFontSize)
519
- .attr('font-weight', 'bold')
520
- .text(node.label);
521
507
 
522
508
  const metaEntries = Object.entries(node.metadata);
523
- if (metaEntries.length > 0) {
524
- nodeG
525
- .append('line')
526
- .attr('x1', 0)
527
- .attr('y1', sHeaderHeight)
528
- .attr('x2', node.width)
529
- .attr('y2', sHeaderHeight)
530
- .attr('stroke', solid ? labelColor : stroke)
531
- .attr('stroke-opacity', 0.3)
532
- .attr('stroke-width', 1);
533
-
534
- const metaDisplayKeys = metaEntries.map(
535
- ([k]) => displayNames.get(k) ?? k
536
- );
537
- const maxKeyWidth = Math.max(
538
- ...metaDisplayKeys.map((k) => measureText(`${k}: `, sMetaFontSize))
539
- );
540
- const valueX = 10 + maxKeyWidth;
541
-
542
- const metaStartY = sHeaderHeight + sSeparatorGap + sMetaFontSize;
543
- for (let i = 0; i < metaEntries.length; i++) {
544
- const [, value] = metaEntries[i]!;
545
- const displayKey = metaDisplayKeys[i]!;
546
- const rowY = metaStartY + i * sMetaLineHeight;
547
-
548
- nodeG
549
- .append('text')
550
- .attr('x', 10)
551
- .attr('y', rowY)
552
- .attr('fill', labelColor)
553
- .attr('font-size', sMetaFontSize)
554
- .text(`${displayKey}: `);
555
-
556
- nodeG
557
- .append('text')
558
- .attr('x', valueX)
559
- .attr('y', rowY)
560
- .attr('fill', labelColor)
561
- .attr('font-size', sMetaFontSize)
562
- .text(value);
563
- }
564
- }
509
+ renderNodeCard(nodeG, {
510
+ width: node.width,
511
+ height: node.height,
512
+ rx: sCardRadius,
513
+ fill,
514
+ stroke,
515
+ strokeWidth: sNodeStrokeWidth,
516
+ ...(node.isContainer && { dashed: true }),
517
+ label: node.label,
518
+ labelColor,
519
+ labelFontSize: sLabelFontSize,
520
+ headerHeight: sHeaderHeight,
521
+ ...(metaEntries.length > 0 && {
522
+ meta: {
523
+ rows: metaEntries.map(
524
+ ([k, value]) => [displayNames.get(k) ?? k, value] as const
525
+ ),
526
+ fontSize: sMetaFontSize,
527
+ lineHeight: sMetaLineHeight,
528
+ separatorGap: sSeparatorGap,
529
+ separatorColor: solid ? labelColor : stroke,
530
+ textColor: labelColor,
531
+ },
532
+ }),
533
+ });
565
534
 
566
535
  if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
567
- const clipId = `clip-${node.id}`;
568
- nodeG
569
- .append('clipPath')
570
- .attr('id', clipId)
571
- .append('rect')
572
- .attr('width', node.width)
573
- .attr('height', node.height)
574
- .attr('rx', sCardRadius);
575
- nodeG
576
- .append('rect')
577
- .attr('x', sCollapseBarInset)
578
- .attr('y', node.height - sCollapseBarHeight)
579
- .attr('width', node.width - sCollapseBarInset * 2)
580
- .attr('height', sCollapseBarHeight)
581
- .attr(
582
- 'fill',
583
- solid
584
- ? labelColor
585
- : nodeStroke(palette, colorOff ? undefined : node.color)
586
- )
587
- .attr('clip-path', `url(#${clipId})`)
588
- .attr('class', 'org-collapse-bar');
536
+ renderCollapseBar(nodeG, {
537
+ width: node.width,
538
+ height: node.height,
539
+ barHeight: sCollapseBarHeight,
540
+ inset: sCollapseBarInset,
541
+ rx: sCardRadius,
542
+ fill: solid
543
+ ? labelColor
544
+ : nodeStroke(palette, colorOff ? undefined : node.color),
545
+ clipId: `clip-${node.id}`,
546
+ className: 'org-collapse-bar',
547
+ });
589
548
  }
590
549
 
591
550
  // Focus icon (hover-reveal, interactive only) — for non-root nodes with children
@@ -611,7 +570,10 @@ export function renderOrg(
611
570
  .attr('height', iconSize + 6)
612
571
  .attr('fill', 'transparent');
613
572
 
614
- // Scope/target icon: outer circle + inner dot
573
+ // Scope/target icon: outer circle + inner dot. Use the card's contrast
574
+ // text color so it stays legible on solid-fill dark cards, not just the
575
+ // light default surface.
576
+ const iconColor = labelColor;
615
577
  const cx = iconSize / 2;
616
578
  const cy = iconSize / 2;
617
579
  focusG
@@ -620,14 +582,14 @@ export function renderOrg(
620
582
  .attr('cy', cy)
621
583
  .attr('r', iconSize / 2 - 1)
622
584
  .attr('fill', 'none')
623
- .attr('stroke', palette.textMuted)
585
+ .attr('stroke', iconColor)
624
586
  .attr('stroke-width', 1.5);
625
587
  focusG
626
588
  .append('circle')
627
589
  .attr('cx', cx)
628
590
  .attr('cy', cy)
629
591
  .attr('r', 2)
630
- .attr('fill', palette.textMuted);
592
+ .attr('fill', iconColor);
631
593
  }
632
594
  }
633
595
 
@@ -1,3 +1,4 @@
1
+ import { CATEGORICAL_COLOR_ORDER } from '../colors';
1
2
  import type { PaletteColors } from './types';
2
3
 
3
4
  // ============================================================
@@ -304,6 +305,16 @@ export function contrastText(
304
305
  * `opts.solid` (per `option solid-fill`): bypass the 25% tint and return
305
306
  * the raw intent. Opt-in only; default behavior unchanged.
306
307
  */
308
+ /**
309
+ * The theme-aware base background a diagram's tinted shapes blend toward:
310
+ * `surface` in dark, page `bg` in light. Concentrates the
311
+ * `isDark ? palette.surface : palette.bg` pick repeated across ~20 renderers
312
+ * (Story 111.3).
313
+ */
314
+ export function themeBaseBg(palette: PaletteColors, isDark: boolean): string {
315
+ return isDark ? palette.surface : palette.bg;
316
+ }
317
+
307
318
  export function shapeFill(
308
319
  palette: PaletteColors,
309
320
  intent: string,
@@ -311,17 +322,21 @@ export function shapeFill(
311
322
  opts?: { solid?: boolean }
312
323
  ): string {
313
324
  if (opts?.solid) return intent;
314
- return mix(intent, isDark ? palette.surface : palette.bg, 25);
325
+ return mix(intent, themeBaseBg(palette, isDark), 25);
315
326
  }
316
327
 
317
328
  // ============================================================
318
329
  // Series Colors
319
330
  // ============================================================
320
331
 
321
- /** Derive the 8-color series rotation from a palette's named colors. */
332
+ /**
333
+ * Derive the 8-color series rotation from a palette's named colors, in the
334
+ * shared {@link CATEGORICAL_COLOR_ORDER} (RGB-seeded, max-contrast). Tag
335
+ * swatches and chart series colors thus share one canonical rotation.
336
+ */
322
337
  export function getSeriesColors(palette: PaletteColors): string[] {
323
338
  const c = palette.colors;
324
- return [c.blue, c.green, c.yellow, c.orange, c.purple, c.red, c.teal, c.cyan];
339
+ return CATEGORICAL_COLOR_ORDER.map((name) => c[name]!);
325
340
  }
326
341
 
327
342
  /**
@@ -403,7 +418,7 @@ export function politicalTints(
403
418
  isDark: boolean
404
419
  ): string[] {
405
420
  if (count <= 0) return [];
406
- const base = isDark ? palette.surface : palette.bg;
421
+ const base = themeBaseBg(palette, isDark);
407
422
  const c = palette.colors;
408
423
  // Land-first: greens/earth tones lead; water-like blue & cyan trail.
409
424
  const swatches = [
@@ -21,6 +21,7 @@ export {
21
21
  getSegmentColors,
22
22
  contrastText,
23
23
  shapeFill,
24
+ themeBaseBg,
24
25
  } from './color-utils';
25
26
 
26
27
  // Re-export palette definitions (alphabetical)
@@ -33,7 +33,7 @@ import * as d3Selection from 'd3-selection';
33
33
  import * as d3Shape from 'd3-shape';
34
34
  import { FONT_FAMILY } from '../fonts';
35
35
  import type { PaletteColors } from '../palettes';
36
- import { contrastText, mix, shapeFill } from '../palettes/color-utils';
36
+ import { contrastText, mix, shapeFill, themeBaseBg } from '../palettes/color-utils';
37
37
  import { ScaleContext } from '../utils/scaling';
38
38
  import {
39
39
  measureText,
@@ -87,7 +87,16 @@ const NODE_CELL_FONT_SIZE = 11;
87
87
  // row holds the name and is taller than the corner cells so the name
88
88
  // reads as the primary label (mirroring textbook proportions).
89
89
  const NODE_RADIUS = 6;
90
- const NODE_STROKE_WIDTH = 1.5;
90
+ // Shared card / group / collapse constants (Story 111.1). The explanatory
91
+ // comments below stay with the renderer; the values now live in one module.
92
+ import {
93
+ NODE_STROKE_WIDTH,
94
+ EDGE_STROKE_WIDTH,
95
+ CONTAINER_RADIUS,
96
+ CONTAINER_LABEL_FONT_SIZE,
97
+ CONTAINER_HEADER_HEIGHT,
98
+ COLLAPSE_BAR_HEIGHT,
99
+ } from '../utils/visual-conventions';
91
100
 
92
101
  // Analysis-block chrome (Summary / Activity Risk / Completion / Field
93
102
  // labels). These sit BELOW the diagram and shouldn't compete with it
@@ -99,7 +108,7 @@ function analysisBlockChrome(
99
108
  palette: PaletteColors,
100
109
  isDark: boolean
101
110
  ): { fill: string; stroke: string } {
102
- const surfaceBg = isDark ? palette.surface : palette.bg;
111
+ const surfaceBg = themeBaseBg(palette, isDark);
103
112
  return {
104
113
  fill: mix(palette.surface, palette.bg, 40),
105
114
  stroke: mix(palette.textMuted, surfaceBg, 35),
@@ -113,18 +122,14 @@ const NODE_BOTTOM_ROW_HEIGHT = 26;
113
122
  // stroke and a matching red arrowhead because the critical path is the
114
123
  // central concept of a PERT chart, and a binary `data-critical` attr
115
124
  // alone left it visually invisible to readers.
116
- const EDGE_STROKE_WIDTH = 1.5;
117
125
  const ARROWHEAD_W = 10;
118
126
  const ARROWHEAD_H = 7;
119
127
  // Group-rect treatment per §2: neutral surface fill on textMuted stroke,
120
128
  // solid border, rx=8, top-center 13pt 'bold' label inside a reserved
121
129
  // 28px header band — exactly matching org's container recipe.
122
- const CONTAINER_RADIUS = 8;
123
- const CONTAINER_LABEL_FONT_SIZE = 13;
124
- const CONTAINER_HEADER_HEIGHT = 28;
125
- // Collapse-bar height — see conventions doc §3 Pattern A/B (matches
126
- // org's `COLLAPSE_BAR_HEIGHT`). Universal "this is collapsed" signal.
127
- const COLLAPSE_BAR_HEIGHT = 6;
130
+ // CONTAINER_RADIUS/LABEL_FONT_SIZE/HEADER_HEIGHT + COLLAPSE_BAR_HEIGHT now
131
+ // imported from utils/visual-conventions (Story 111.1). Group-rect treatment
132
+ // per §2; collapse-bar height matches org per §3 Pattern A/B.
128
133
  // Always-on fade applied to bottom-20% (by duration) activity nodes
129
134
  // so the eye is drawn to the longer, schedule-dominating work first.
130
135
  // Less aggressive than FADE_OPACITY because these cards still need to
@@ -4,8 +4,8 @@
4
4
 
5
5
  import {
6
6
  bareDescriptionRemovedMessage,
7
- formatDgmoError,
8
7
  makeDgmoError,
8
+ makeFail,
9
9
  METADATA_DIAGNOSTIC_CODES,
10
10
  pipeOperatorRemovedMessage,
11
11
  } from '../diagnostics';
@@ -61,12 +61,7 @@ export function parsePyramid(content: string): ParsedPyramid {
61
61
  let headerParsed = false;
62
62
  let currentLayer: Writable<PyramidLayer> | null = null;
63
63
 
64
- const fail = (line: number, message: string): ParsedPyramid => {
65
- const diag = makeDgmoError(line, message);
66
- result.diagnostics.push(diag);
67
- result.error = formatDgmoError(diag);
68
- return result;
69
- };
64
+ const fail = makeFail(result);
70
65
 
71
66
  const warn = (
72
67
  line: number,
@@ -11,7 +11,7 @@ import { ScaleContext } from '../utils/scaling';
11
11
  import { initD3Chart, renderChartTitle } from '../utils/d3-helpers';
12
12
  import type { ParsedQuadrant, QuadrantLabel } from '../visualizations/types';
13
13
  import type { PaletteColors } from '../palettes';
14
- import { mix } from '../palettes/color-utils';
14
+ import { mix, themeBaseBg } from '../palettes/color-utils';
15
15
 
16
16
  // Quadrant Chart Renderer
17
17
  // ============================================================
@@ -99,7 +99,7 @@ export function renderQuadrant(
99
99
  .append('g')
100
100
  .attr('transform', `translate(${margin.left}, ${margin.top})`);
101
101
 
102
- const bg = isDark ? palette.surface : palette.bg;
102
+ const bg = themeBaseBg(palette, isDark);
103
103
 
104
104
  // Full palette color for a quadrant (used for border and label tinting)
105
105
  const getQuadrantColor = (
@@ -19,8 +19,8 @@
19
19
  // See `docs/dgmo-language-spec.md` § "RACI Matrix".
20
20
 
21
21
  import {
22
- formatDgmoError,
23
22
  makeDgmoError,
23
+ makeFail,
24
24
  METADATA_DIAGNOSTIC_CODES,
25
25
  pipeOperatorRemovedMessage,
26
26
  suggest,
@@ -181,12 +181,7 @@ export function parseRaci(
181
181
  error: null,
182
182
  };
183
183
 
184
- const fail = (line: number, message: string): ParsedRaci => {
185
- const diag = makeDgmoError(line, message);
186
- result.diagnostics.push(diag);
187
- result.error = formatDgmoError(diag);
188
- return result;
189
- };
184
+ const fail = makeFail(result);
190
185
 
191
186
  const warn = (line: number, message: string, code?: string): void => {
192
187
  result.diagnostics.push(makeDgmoError(line, message, 'warning', code));
@@ -30,7 +30,7 @@ import {
30
30
  TITLE_FONT_WEIGHT,
31
31
  TITLE_Y,
32
32
  } from '../utils/title-constants';
33
- import { contrastText, mix } from '../palettes/color-utils';
33
+ import { contrastText, mix, themeBaseBg } from '../palettes/color-utils';
34
34
  import type { PaletteColors } from '../palettes';
35
35
  import type { D3ExportDimensions } from '../utils/d3-types';
36
36
  import type {
@@ -267,7 +267,7 @@ const TINT_PCT = 25;
267
267
  * kanban `CARD_RADIUS = 6`). Keeps RACI markers visually consistent
268
268
  * with nodes elsewhere in the diagram language.
269
269
  */
270
- const NODE_STROKE_WIDTH = 1.5;
270
+ import { NODE_STROKE_WIDTH } from '../utils/visual-conventions'; // shared (Story 111.1)
271
271
  const NODE_RADIUS = 6;
272
272
 
273
273
  /**
@@ -394,7 +394,7 @@ export function renderRaci(
394
394
  if (tasksAll.length === 0 && parsed.phases.length === 0) return;
395
395
 
396
396
  const solid = parsed.options['solid-fill'] === 'on';
397
- const surfaceBg = isDark ? palette.surface : palette.bg;
397
+ const surfaceBg = themeBaseBg(palette, isDark);
398
398
 
399
399
  // --- ScaleContext: differential scaling ---
400
400
  const roleCount = Math.max(1, parsed.roles.length);
@@ -679,10 +679,10 @@ export function renderRaci(
679
679
  // each column has a subtle visual identity instead of every column
680
680
  // reading as the same neutral gray.
681
681
  const roleColor = parsed.roleColors[i] ?? autoAccent(i, palette);
682
- const bodyFill = mix(roleColor, isDark ? palette.surface : palette.bg, 16);
682
+ const bodyFill = mix(roleColor, themeBaseBg(palette, isDark), 16);
683
683
  const headerFill = mix(
684
684
  roleColor,
685
- isDark ? palette.surface : palette.bg,
685
+ themeBaseBg(palette, isDark),
686
686
  30
687
687
  );
688
688
  const colG = columnsG
@@ -4,8 +4,8 @@
4
4
 
5
5
  import {
6
6
  bareDescriptionRemovedMessage,
7
- formatDgmoError,
8
7
  makeDgmoError,
8
+ makeFail,
9
9
  METADATA_DIAGNOSTIC_CODES,
10
10
  pipeOperatorRemovedMessage,
11
11
  suggest,
@@ -48,12 +48,7 @@ export function parseRing(content: string): ParsedRing {
48
48
  let headerParsed = false;
49
49
  let currentLayer: Writable<RingLayer> | null = null;
50
50
 
51
- const fail = (line: number, message: string): ParsedRing => {
52
- const diag = makeDgmoError(line, message);
53
- result.diagnostics.push(diag);
54
- result.error = formatDgmoError(diag);
55
- return result;
56
- };
51
+ const fail = makeFail(result);
57
52
 
58
53
  const warn = (
59
54
  line: number,
@@ -10,6 +10,7 @@ import {
10
10
  akaRemovedMessage,
11
11
  formatDgmoError,
12
12
  makeDgmoError,
13
+ makeFail,
13
14
  METADATA_DIAGNOSTIC_CODES,
14
15
  NAME_DIAGNOSTIC_CODES,
15
16
  nameMergedMessage,
@@ -528,12 +529,7 @@ export function parseSequenceDgmo(
528
529
  return nameAliasMap.get(trimmed) ?? trimmed;
529
530
  };
530
531
 
531
- const fail = (line: number, message: string): ParsedSequenceDgmo => {
532
- const diag = makeDgmoError(line, message);
533
- result.diagnostics.push(diag);
534
- result.error = formatDgmoError(diag);
535
- return result;
536
- };
532
+ const fail = makeFail(result);
537
533
 
538
534
  /** Push a recoverable error and continue parsing. */
539
535
  const pushError = (line: number, message: string): void => {
@@ -554,6 +550,13 @@ export function parseSequenceDgmo(
554
550
  const lines = content.split('\n');
555
551
  let hasExplicitChart = false;
556
552
  let contentStarted = false;
553
+ // Whether the message body has begun (first message, section, block, or note).
554
+ // Unlike `contentStarted` — which any declaration trips to close the
555
+ // options/tag-group "headers first" window — `bodyStarted` stays false through
556
+ // the entire declaration preamble so bare and typed participant declarations
557
+ // can be freely interleaved. It gates bare-name declarations: once real body
558
+ // content appears, a bare word is treated as a stray line, not a participant.
559
+ let bodyStarted = false;
557
560
  let firstLineIndex = -1; // line index of the `sequence [Title]` first line (to skip in main loop)
558
561
 
559
562
  // Handle first non-empty, non-comment line for `sequence Title` syntax
@@ -997,6 +1000,7 @@ export function parseSequenceDgmo(
997
1000
  );
998
1001
  }
999
1002
  contentStarted = true;
1003
+ bodyStarted = true;
1000
1004
  const section: SequenceSection = {
1001
1005
  kind: 'section',
1002
1006
  // Capture group 1 guaranteed present after successful match.
@@ -1301,7 +1305,7 @@ export function parseSequenceDgmo(
1301
1305
  if (
1302
1306
  /^\S+$/.test(bareCore) &&
1303
1307
  !ARROW_PATTERN.test(bareCore) &&
1304
- (inGroup || !contentStarted || bareMeta)
1308
+ (inGroup || !bodyStarted || bareMeta)
1305
1309
  ) {
1306
1310
  contentStarted = true;
1307
1311
  const id = bareCore;
@@ -1369,6 +1373,7 @@ export function parseSequenceDgmo(
1369
1373
  }
1370
1374
  if (labeledArrow) {
1371
1375
  contentStarted = true;
1376
+ bodyStarted = true;
1372
1377
  const { from, to, label: rawLabel, async: isAsync } = labeledArrow;
1373
1378
  const fromKey = addParticipant(resolveAlias(from), lineNumber);
1374
1379
  const toKey = addParticipant(resolveAlias(to), lineNumber);
@@ -1445,6 +1450,7 @@ export function parseSequenceDgmo(
1445
1450
  const bareCall = bareCallSync || bareCallAsync;
1446
1451
  if (bareCall) {
1447
1452
  contentStarted = true;
1453
+ bodyStarted = true;
1448
1454
  // Capture groups 1 and 2 guaranteed present after successful match.
1449
1455
  const from = bareCall[1]!;
1450
1456
  const to = bareCall[2]!;
@@ -1470,6 +1476,7 @@ export function parseSequenceDgmo(
1470
1476
  const ifMatch = trimmed.match(/^if\s+(.+)$/i);
1471
1477
  if (ifMatch) {
1472
1478
  contentStarted = true;
1479
+ bodyStarted = true;
1473
1480
  const block: Writable<SequenceBlock> = {
1474
1481
  kind: 'block',
1475
1482
  type: 'if',
@@ -1488,6 +1495,7 @@ export function parseSequenceDgmo(
1488
1495
  const loopMatch = trimmed.match(/^loop\s+(.+)$/i);
1489
1496
  if (loopMatch) {
1490
1497
  contentStarted = true;
1498
+ bodyStarted = true;
1491
1499
  const block: Writable<SequenceBlock> = {
1492
1500
  kind: 'block',
1493
1501
  type: 'loop',
@@ -1506,6 +1514,7 @@ export function parseSequenceDgmo(
1506
1514
  const parallelMatch = trimmed.match(/^parallel(?:\s+(.+))?$/i);
1507
1515
  if (parallelMatch) {
1508
1516
  contentStarted = true;
1517
+ bodyStarted = true;
1509
1518
  const block: Writable<SequenceBlock> = {
1510
1519
  kind: 'block',
1511
1520
  type: 'parallel',
@@ -1629,6 +1638,7 @@ export function parseSequenceDgmo(
1629
1638
  lineNumber,
1630
1639
  endLineNumber: lineNumber,
1631
1640
  };
1641
+ bodyStarted = true;
1632
1642
  currentContainer().push(note);
1633
1643
  continue;
1634
1644
  }
@@ -1654,6 +1664,7 @@ export function parseSequenceDgmo(
1654
1664
  lineNumber,
1655
1665
  endLineNumber: i + 1, // i has advanced past the body lines (1-based)
1656
1666
  };
1667
+ bodyStarted = true;
1657
1668
  currentContainer().push(note);
1658
1669
  continue;
1659
1670
  }