@diagrammo/dgmo 0.33.0 → 0.35.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 (59) hide show
  1. package/.claude/commands/dgmo.md +17 -32
  2. package/.github/copilot-instructions.md +1 -1
  3. package/README.md +7 -1
  4. package/SKILL.md +1 -1
  5. package/dist/advanced.cjs +791 -614
  6. package/dist/advanced.d.cts +8 -51
  7. package/dist/advanced.d.ts +8 -51
  8. package/dist/advanced.js +791 -615
  9. package/dist/auto.cjs +793 -292
  10. package/dist/auto.js +95 -95
  11. package/dist/auto.mjs +793 -292
  12. package/dist/cli.cjs +145 -328
  13. package/dist/index.cjs +792 -291
  14. package/dist/index.js +792 -291
  15. package/docs/ai-integration.md +46 -26
  16. package/docs/language-reference.md +2 -2
  17. package/package.json +3 -3
  18. package/src/advanced.ts +0 -12
  19. package/src/boxes-and-lines/label-placement.ts +339 -0
  20. package/src/boxes-and-lines/layout-search.ts +16 -2
  21. package/src/boxes-and-lines/layout.ts +32 -7
  22. package/src/boxes-and-lines/renderer.ts +67 -139
  23. package/src/c4/parser.ts +15 -6
  24. package/src/chart.ts +1 -1
  25. package/src/class/parser.ts +6 -2
  26. package/src/cli-banner.ts +2 -3
  27. package/src/cli.ts +539 -1223
  28. package/src/cycle/parser.ts +4 -1
  29. package/src/dgmo-router.ts +16 -2
  30. package/src/diagnostics.ts +0 -0
  31. package/src/echarts.ts +5 -1
  32. package/src/er/parser.ts +5 -1
  33. package/src/gantt/parser.ts +13 -4
  34. package/src/graph/flowchart-parser.ts +5 -1
  35. package/src/graph/state-parser.ts +5 -1
  36. package/src/infra/parser.ts +10 -6
  37. package/src/journey-map/parser.ts +5 -2
  38. package/src/kanban/parser.ts +6 -2
  39. package/src/map/context-labels.ts +29 -19
  40. package/src/map/layout.ts +403 -108
  41. package/src/map/legend-band.ts +28 -0
  42. package/src/map/parser.ts +13 -4
  43. package/src/map/renderer.ts +74 -23
  44. package/src/map/resolver.ts +2 -2
  45. package/src/org/parser.ts +8 -2
  46. package/src/pyramid/parser.ts +4 -1
  47. package/src/raci/parser.ts +4 -1
  48. package/src/ring/parser.ts +4 -1
  49. package/src/sequence/parser.ts +6 -3
  50. package/src/sitemap/parser.ts +12 -3
  51. package/src/tech-radar/parser.ts +6 -3
  52. package/src/utils/parsing.ts +2 -2
  53. package/src/utils/tag-groups.ts +3 -3
  54. package/src/visualizations/parse.ts +9 -3
  55. package/src/migrate/data-comma.ts +0 -117
  56. package/src/migrate/embedded.ts +0 -238
  57. package/src/migrate/index.ts +0 -295
  58. package/src/migrate/line-classifier.ts +0 -128
  59. package/src/migrate/line-transformer.ts +0 -218
@@ -78,7 +78,6 @@ const GROUP_LABEL_ZONE = 32;
78
78
  const RAMP_FLOOR = 15;
79
79
  const VALUE_FONT_SIZE = 11;
80
80
 
81
- type D3G = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
82
81
  type D3Svg = d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
83
82
 
84
83
  // ── Edge path generators ───────────────────────────────────
@@ -352,78 +351,6 @@ function ensureArrowMarkers(
352
351
  }
353
352
  }
354
353
 
355
- // ── Edge label placement ───────────────────────────────────
356
-
357
- /** Point at the half-way arc length along an edge polyline — the geometric
358
- * centre of the connector, so a label sits in the gap BETWEEN the two nodes
359
- * (ELK's own label anchor drifts toward the target and ends up clipped under
360
- * it). Falls back gracefully for degenerate point lists. */
361
- function edgePolylineMidpoint(
362
- points: ReadonlyArray<{ readonly x: number; readonly y: number }>
363
- ): { x: number; y: number } {
364
- if (points.length === 0) return { x: 0, y: 0 };
365
- if (points.length === 1) return { x: points[0]!.x, y: points[0]!.y };
366
- let total = 0;
367
- const segLen: number[] = [];
368
- for (let k = 1; k < points.length; k++) {
369
- const len = Math.hypot(
370
- points[k]!.x - points[k - 1]!.x,
371
- points[k]!.y - points[k - 1]!.y
372
- );
373
- segLen.push(len);
374
- total += len;
375
- }
376
- let half = total / 2;
377
- for (let k = 1; k < points.length; k++) {
378
- const len = segLen[k - 1]!;
379
- if (half <= len || k === points.length - 1) {
380
- const t = len === 0 ? 0 : Math.min(1, half / len);
381
- return {
382
- x: points[k - 1]!.x + (points[k]!.x - points[k - 1]!.x) * t,
383
- y: points[k - 1]!.y + (points[k]!.y - points[k - 1]!.y) * t,
384
- };
385
- }
386
- half -= len;
387
- }
388
- const last = points[points.length - 1]!;
389
- return { x: last.x, y: last.y };
390
- }
391
-
392
- // ── Edge label overlap resolution ──────────────────────────
393
-
394
- function resolveEdgeLabelOverlaps(
395
- labels: { x: number; y: number; width: number; height: number }[]
396
- ): void {
397
- const MAX_PASSES = 8;
398
- const PAD = 4;
399
- for (let pass = 0; pass < MAX_PASSES; pass++) {
400
- let moved = false;
401
- for (let i = 0; i < labels.length; i++) {
402
- for (let j = i + 1; j < labels.length; j++) {
403
- // In-bounds by loop guard.
404
- const a = labels[i]!;
405
- const b = labels[j]!;
406
- const dx = Math.abs(a.x - b.x);
407
- const dy = Math.abs(a.y - b.y);
408
- const overlapX = (a.width + b.width) / 2 + PAD - dx;
409
- const overlapY = (a.height + b.height) / 2 + PAD - dy;
410
- if (overlapX > 0 && overlapY > 0) {
411
- const shift = overlapY / 2 + 1;
412
- if (a.y < b.y) {
413
- a.y -= shift;
414
- b.y += shift;
415
- } else {
416
- a.y += shift;
417
- b.y -= shift;
418
- }
419
- moved = true;
420
- }
421
- }
422
- }
423
- if (!moved) break;
424
- }
425
- }
426
-
427
354
  // ── Main render function ───────────────────────────────────
428
355
 
429
356
  interface BLRenderOptions {
@@ -847,18 +774,8 @@ export function renderBoxesAndLines(
847
774
  }
848
775
 
849
776
  // ── Render edges ───────────────────────────────────────
850
- // Collect label positions for overlap resolution
851
- const labelPositions: {
852
- x: number;
853
- y: number;
854
- width: number;
855
- height: number;
856
- idx: number;
857
- }[] = [];
858
-
859
- // Store edge group elements for label pass
860
- const edgeGroups = new Map<number, D3G>();
861
-
777
+ // Edge labels are placed in layout (label-placement.ts) and drawn AFTER the
778
+ // node loop (see the bl-edge-labels layer) so boxes never clip them.
862
779
  for (let i = 0; i < layout.edges.length; i++) {
863
780
  // In-bounds by loop guard.
864
781
  const le = layout.edges[i]!;
@@ -885,7 +802,6 @@ export function renderBoxesAndLines(
885
802
  .append('g')
886
803
  .attr('class', 'bl-edge-group')
887
804
  .attr('data-line-number', String(le.lineNumber));
888
- edgeGroups.set(i, edgeG as unknown as D3G);
889
805
 
890
806
  const markerId = `bl-arrow-${color.replace('#', '')}`;
891
807
  const cx = nodeLayout.x;
@@ -950,7 +866,6 @@ export function renderBoxesAndLines(
950
866
  .append('g')
951
867
  .attr('class', 'bl-edge-group')
952
868
  .attr('data-line-number', String(le.lineNumber));
953
- edgeGroups.set(i, edgeG as unknown as D3G);
954
869
 
955
870
  const markerId = `bl-arrow-${color.replace('#', '')}`;
956
871
  const gen = le.straight
@@ -971,58 +886,6 @@ export function renderBoxesAndLines(
971
886
  const revId = `bl-arrow-rev-${color.replace('#', '')}`;
972
887
  path.attr('marker-start', `url(#${revId})`);
973
888
  }
974
-
975
- // Edge label — centred on the connector's polyline midpoint (the gap
976
- // between the two nodes), NOT ELK's target-biased anchor. For parallel
977
- // edges, nudge above/below so each line's label clears the line.
978
- if (le.label && le.points.length > 0) {
979
- const lw = measureText(le.label, sEdgeLabelFontSize);
980
- const labelH = sEdgeLabelFontSize + 6;
981
- const mid = edgePolylineMidpoint(le.points);
982
- let ly = mid.y;
983
- if (le.parallelCount > 1 && le.yOffset !== 0) {
984
- ly += (le.yOffset < 0 ? -labelH : labelH) * 0.5;
985
- }
986
- labelPositions.push({
987
- x: mid.x,
988
- y: ly,
989
- width: lw + 8,
990
- height: labelH,
991
- idx: i,
992
- });
993
- }
994
- }
995
-
996
- // Resolve overlaps
997
- resolveEdgeLabelOverlaps(labelPositions);
998
-
999
- // Render edge labels into their edge groups
1000
- for (const lp of labelPositions) {
1001
- // In-bounds: lp.idx was set from a valid index into layout.edges above.
1002
- const le = layout.edges[lp.idx]!;
1003
- if (!le.label) continue;
1004
-
1005
- const edgeG = edgeGroups.get(lp.idx);
1006
- const target = edgeG ?? diagramG;
1007
-
1008
- target
1009
- .append('rect')
1010
- .attr('x', lp.x - lp.width / 2)
1011
- .attr('y', lp.y - lp.height / 2)
1012
- .attr('width', lp.width)
1013
- .attr('height', lp.height)
1014
- .attr('rx', 3)
1015
- .attr('fill', palette.bg)
1016
- .attr('opacity', 0.85);
1017
-
1018
- target
1019
- .append('text')
1020
- .attr('x', lp.x)
1021
- .attr('y', lp.y + sEdgeLabelFontSize / 3)
1022
- .attr('text-anchor', 'middle')
1023
- .attr('font-size', sEdgeLabelFontSize)
1024
- .attr('fill', palette.textMuted)
1025
- .text(le.label);
1026
889
  }
1027
890
 
1028
891
  // ── Render nodes ───────────────────────────────────────
@@ -1396,6 +1259,71 @@ export function renderBoxesAndLines(
1396
1259
  }
1397
1260
  }
1398
1261
 
1262
+ // ── Render edge labels ─────────────────────────────────
1263
+ // Drawn AFTER nodes (and groups) so the halo + text always paint on top and
1264
+ // are never clipped by a box. Positions/wrapping come from label-placement
1265
+ // (layout); this is a pure consumer. Labels stay in their own layer rather
1266
+ // than inside each edge group, but carry data-line-number so line-hover/click
1267
+ // still highlights the matching label.
1268
+ const labelLayer = diagramG.append('g').attr('class', 'bl-edge-labels');
1269
+ const labelLineHeight = sEdgeLabelFontSize * 1.3;
1270
+ for (const le of layout.edges) {
1271
+ if (!le.labelLines || le.labelLines.length === 0) continue;
1272
+ if (le.labelX === undefined || le.labelY === undefined) continue;
1273
+ const lx = le.labelX;
1274
+ const ly = le.labelY;
1275
+
1276
+ // Honour the same tag-value hiding as the edges themselves.
1277
+ if (hidden.size > 0) {
1278
+ let isHidden = false;
1279
+ for (const [groupKey, hiddenVals] of hidden) {
1280
+ const val = le.metadata[groupKey];
1281
+ if (val && hiddenVals.has(val.toLowerCase())) {
1282
+ isHidden = true;
1283
+ break;
1284
+ }
1285
+ }
1286
+ if (isHidden) continue;
1287
+ }
1288
+
1289
+ const lw = le.labelWidth ?? 0;
1290
+ const lh = le.labelHeight ?? sEdgeLabelFontSize + 6;
1291
+ const lines = le.labelLines;
1292
+ const labelG = labelLayer
1293
+ .append('g')
1294
+ .attr('class', 'bl-edge-label')
1295
+ .attr('data-line-number', String(le.lineNumber));
1296
+
1297
+ labelG
1298
+ .append('rect')
1299
+ .attr('x', lx - lw / 2)
1300
+ .attr('y', ly - lh / 2)
1301
+ .attr('width', lw)
1302
+ .attr('height', lh)
1303
+ .attr('rx', 3)
1304
+ .attr('fill', palette.bg)
1305
+ // No border; semi-transparent so the line stays faintly visible behind the
1306
+ // label while the (full-opacity) text on top stays crisp.
1307
+ .attr('opacity', 0.72);
1308
+
1309
+ const text = labelG
1310
+ .append('text')
1311
+ .attr('x', lx)
1312
+ .attr('text-anchor', 'middle')
1313
+ .attr('font-family', FONT_FAMILY)
1314
+ .attr('font-size', sEdgeLabelFontSize)
1315
+ .attr('fill', palette.textMuted);
1316
+ const firstY =
1317
+ ly - ((lines.length - 1) / 2) * labelLineHeight + sEdgeLabelFontSize / 3;
1318
+ lines.forEach((line, k) => {
1319
+ text
1320
+ .append('tspan')
1321
+ .attr('x', lx)
1322
+ .attr('y', firstY + k * labelLineHeight)
1323
+ .text(line);
1324
+ });
1325
+ }
1326
+
1399
1327
  // ── Render legend ──────────────────────────────────────
1400
1328
  const hasDescriptions = parsed.nodes.some(
1401
1329
  (n) => n.description && n.description.length > 0
package/src/c4/parser.ts CHANGED
@@ -516,7 +516,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
516
516
  } else {
517
517
  pushError(
518
518
  lineNumber,
519
- `"container ${refName}" must be inside a deployment node`
519
+ `"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
520
520
  );
521
521
  }
522
522
  continue;
@@ -585,7 +585,10 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
585
585
  indent,
586
586
  });
587
587
  } else {
588
- pushError(lineNumber, `"${sectionType}" must be inside an element`);
588
+ pushError(
589
+ lineNumber,
590
+ `"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
591
+ );
589
592
  }
590
593
  continue;
591
594
  }
@@ -618,7 +621,10 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
618
621
  indent,
619
622
  });
620
623
  } else {
621
- pushError(lineNumber, `Group [${groupName}] must be inside an element`);
624
+ pushError(
625
+ lineNumber,
626
+ `Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
627
+ );
622
628
  }
623
629
  continue;
624
630
  }
@@ -1049,7 +1055,10 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
1049
1055
  }
1050
1056
  desc.push(descResult.text);
1051
1057
  } else {
1052
- pushError(lineNumber, `Unexpected content: "${trimmed}"`);
1058
+ pushError(
1059
+ lineNumber,
1060
+ `Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (§8)`
1061
+ );
1053
1062
  }
1054
1063
  }
1055
1064
 
@@ -1154,7 +1163,7 @@ function validateRelationshipTargets(
1154
1163
  for (const el of elements) {
1155
1164
  for (const rel of el.relationships) {
1156
1165
  if (!knownNames.has(normalizeName(rel.target))) {
1157
- let msg = `Relationship target "${rel.target}" not found`;
1166
+ let msg = `Relationship target "${rel.target}" not found — declare it first (e.g. '${rel.target} is a container'), then reference it in the arrow`;
1158
1167
  const hint = suggest(normalizeName(rel.target), allNames);
1159
1168
  if (hint) msg += `. ${hint}`;
1160
1169
  pushWarning(rel.lineNumber, msg, 'warning');
@@ -1193,7 +1202,7 @@ function validateDeploymentRefs(
1193
1202
  for (const node of nodes) {
1194
1203
  for (const ref of node.containerRefs) {
1195
1204
  if (!knownNames.has(normalizeName(ref))) {
1196
- let msg = `Deployment reference "container ${ref}" not found`;
1205
+ let msg = `Deployment reference "container ${ref}" not found — declare the container element first, then reference it under the deployment node`;
1197
1206
  const hint = suggest(normalizeName(ref), allNames);
1198
1207
  if (hint) msg += `. ${hint}`;
1199
1208
  pushWarning(node.lineNumber, msg, 'warning');
package/src/chart.ts CHANGED
@@ -174,7 +174,7 @@ export function parseChart(
174
174
  result.diagnostics.push(
175
175
  makeDgmoError(
176
176
  lineNumber,
177
- `'${trimmed}' — ## syntax is no longer supported. Use [Group] containers instead`
177
+ `'${trimmed}' removed — use a bracket container '[${trimmed.replace(/^#+\s*/, '')}]' instead (## headers are no longer supported).`
178
178
  )
179
179
  );
180
180
  continue;
@@ -308,7 +308,7 @@ export function parseClassDiagram(
308
308
  result.diagnostics.push(
309
309
  makeDgmoError(
310
310
  lineNumber,
311
- '"no-auto-color" has been removed.',
311
+ '"no-auto-color" was removed — colors are always auto-assigned from the palette; delete this line.',
312
312
  'warning'
313
313
  )
314
314
  );
@@ -458,7 +458,11 @@ export function parseClassDiagram(
458
458
 
459
459
  // Catch-all: nothing matched this line
460
460
  result.diagnostics.push(
461
- makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, 'warning')
461
+ makeDgmoError(
462
+ lineNumber,
463
+ `Unexpected line: '${trimmed}'. Classes start uppercase at indent 0 ('Ship'); members are indented and colon-typed ('+ name: string', '+ sail(): void'); relationships are indented arrows ('--|> Vessel'). (§10)`,
464
+ 'warning'
465
+ )
462
466
  );
463
467
  }
464
468
 
package/src/cli-banner.ts CHANGED
@@ -7,9 +7,8 @@
7
7
  // sampled from the default `slate` palette (corporate blue → teal →
8
8
  // steel cyan) so the wordmark matches the brand.
9
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.
10
+ // Color only when stdout is a TTY and NO_COLOR is unset; otherwise prints
11
+ // plain ASCII so pipes/CI stay clean.
13
12
 
14
13
  import { getPalette } from './palettes';
15
14