@diagrammo/dgmo 0.8.23 → 0.8.26

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 (78) hide show
  1. package/.claude/commands/dgmo.md +43 -431
  2. package/.cursorrules +2 -2
  3. package/.windsurfrules +2 -2
  4. package/AGENTS.md +8 -5
  5. package/dist/cli.cjs +119 -114
  6. package/dist/editor.cjs +0 -2
  7. package/dist/editor.cjs.map +1 -1
  8. package/dist/editor.js +0 -2
  9. package/dist/editor.js.map +1 -1
  10. package/dist/highlight.cjs +0 -2
  11. package/dist/highlight.cjs.map +1 -1
  12. package/dist/highlight.js +0 -2
  13. package/dist/highlight.js.map +1 -1
  14. package/dist/index.cjs +719 -281
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +105 -18
  17. package/dist/index.d.ts +105 -18
  18. package/dist/index.js +709 -280
  19. package/dist/index.js.map +1 -1
  20. package/dist/internal.cjs +348 -51
  21. package/dist/internal.cjs.map +1 -1
  22. package/dist/internal.d.cts +93 -5
  23. package/dist/internal.d.ts +93 -5
  24. package/dist/internal.js +334 -38
  25. package/dist/internal.js.map +1 -1
  26. package/docs/guide/chart-area.md +17 -17
  27. package/docs/guide/chart-bar-stacked.md +12 -12
  28. package/docs/guide/chart-doughnut.md +10 -10
  29. package/docs/guide/chart-funnel.md +9 -9
  30. package/docs/guide/chart-heatmap.md +10 -10
  31. package/docs/guide/chart-kanban.md +2 -0
  32. package/docs/guide/chart-line.md +19 -19
  33. package/docs/guide/chart-multi-line.md +16 -16
  34. package/docs/guide/chart-pie.md +11 -11
  35. package/docs/guide/chart-polar-area.md +10 -10
  36. package/docs/guide/chart-radar.md +9 -9
  37. package/docs/guide/chart-scatter.md +24 -27
  38. package/docs/guide/index.md +3 -3
  39. package/docs/language-reference.md +46 -25
  40. package/fonts/Inter-Bold.ttf +0 -0
  41. package/fonts/Inter-Regular.ttf +0 -0
  42. package/fonts/LICENSE-Inter.txt +92 -0
  43. package/gallery/fixtures/bar-stacked.dgmo +12 -6
  44. package/gallery/fixtures/heatmap.dgmo +12 -6
  45. package/gallery/fixtures/multi-line.dgmo +11 -7
  46. package/gallery/fixtures/quadrant.dgmo +8 -8
  47. package/gallery/fixtures/scatter.dgmo +12 -12
  48. package/package.json +10 -3
  49. package/src/boxes-and-lines/parser.ts +13 -2
  50. package/src/boxes-and-lines/renderer.ts +22 -13
  51. package/src/chart-type-scoring.ts +162 -0
  52. package/src/chart-types.ts +437 -0
  53. package/src/cli.ts +147 -66
  54. package/src/completion.ts +0 -4
  55. package/src/d3.ts +40 -2
  56. package/src/dgmo-router.ts +85 -130
  57. package/src/editor/keywords.ts +0 -2
  58. package/src/fonts.ts +3 -2
  59. package/src/gantt/parser.ts +5 -1
  60. package/src/index.ts +24 -1
  61. package/src/infra/parser.ts +1 -1
  62. package/src/internal.ts +6 -2
  63. package/src/journey-map/layout.ts +8 -6
  64. package/src/journey-map/parser.ts +5 -1
  65. package/src/kanban/parser.ts +5 -1
  66. package/src/org/collapse.ts +1 -4
  67. package/src/org/parser.ts +1 -1
  68. package/src/org/renderer.ts +26 -17
  69. package/src/sequence/parser.ts +2 -2
  70. package/src/sequence/participant-inference.ts +0 -1
  71. package/src/sequence/renderer.ts +95 -263
  72. package/src/sharing.ts +0 -1
  73. package/src/sitemap/parser.ts +1 -1
  74. package/src/tech-radar/layout.ts +1 -2
  75. package/src/tech-radar/shared.ts +1 -37
  76. package/src/utils/tag-groups.ts +35 -5
  77. package/src/wireframe/parser.ts +3 -1
  78. package/src/tech-radar/index.ts +0 -14
@@ -768,6 +768,8 @@ kanban [Title]
768
768
 
769
769
  ### 10.2 Columns
770
770
 
771
+ Columns represent workflow stages and must flow left-to-right from least-done to most-done (e.g., Backlog → In Progress → Done). Every column should be a stage that cards pass through. Don't create columns for non-workflow concepts like gates, criteria, or definitions of done — use a tag instead (e.g., `type: Gate`).
772
+
771
773
  ```
772
774
  [Column Name]
773
775
  [Column Name](color) | wip: 3
@@ -1134,29 +1136,51 @@ marker
1134
1136
 
1135
1137
  ## 15. Data Charts
1136
1138
 
1139
+ ### Conventions shared across all data charts
1140
+
1141
+ Every section under §15 follows the same two rules.
1142
+
1143
+ **Rule A — data rows are space-separated.** Commas between values are tolerated for back-compat but not idiomatic. Thousands-separator commas *inside a single number* (`3,984,078.65`) are always supported.
1144
+
1145
+ ```
1146
+ Q1 400 700 300 500 ✅ preferred
1147
+ Q1 400, 700, 300, 500 ⚠ tolerated; use spaces
1148
+ ```
1149
+
1150
+ **Rule B — list-of-labelled-items directives (e.g. `series`, `columns`) prefer the indented one-per-line form.** Short one-line forms are tolerated for ≤3 items with no colour annotations or spaces.
1151
+
1152
+ ```
1153
+ series ✅ preferred
1154
+ Cloud Platform (blue)
1155
+ Legacy Suite (red)
1156
+ Mobile App (green)
1157
+
1158
+ series Cloud (blue), Legacy (red) ⚠ tolerated; prefer the block
1159
+ ```
1160
+
1161
+ Parsers accept either form. The rules above are authoring guidance.
1162
+
1137
1163
  ### 15.1 Simple Charts (bar, line, pie, doughnut, area, polar-area, radar, bar-stacked)
1138
1164
 
1139
1165
  **Declaration:** `bar [Title]`, `line [Title]`, etc.
1140
1166
 
1141
- **Series:**
1167
+ **Series** — follows Rule B (prefer the indented block):
1142
1168
  ```
1143
- series Name1 Name2
1144
1169
  series
1145
- Name1
1146
- Name2(color)
1170
+ Cloud Platform (blue)
1171
+ Legacy Suite (red)
1147
1172
  ```
1148
1173
 
1149
- Commas between series names are optional.
1174
+ Short one-line form is tolerated: `series Revenue` or `series A B`.
1150
1175
 
1151
- **Data rows (space-separated, NO colon):**
1176
+ **Data rows** follows Rule A:
1152
1177
  ```
1153
1178
  Label 100
1154
1179
  Label 100 200 300
1155
1180
  Label(color) 100
1181
+ Q1 400 700 300 500
1156
1182
  ```
1157
1183
 
1158
- Commas between values are optional. Thousands commas are supported (`3,984,078.65` is a valid number).
1159
-
1160
1184
  **Options (space-separated, NO colon):**
1161
1185
  ```
1162
1186
  title My Chart
@@ -1182,14 +1206,12 @@ era Day 1 -> Day 3 Rough Seas (red)
1182
1206
 
1183
1207
  ### 15.2 Scatter / Bubble Charts
1184
1208
 
1185
- **Data rows (space-separated, NO colon):**
1209
+ **Data rows** — follows §15 Rule A (space-separated):
1186
1210
  ```
1187
1211
  Name x y
1188
1212
  Name x y size
1189
1213
  ```
1190
1214
 
1191
- Commas between values are optional. Thousands commas supported.
1192
-
1193
1215
  **Categories:**
1194
1216
  ```
1195
1217
  [Caribbean](red)
@@ -1208,20 +1230,21 @@ Labels are on by default. Use `no-labels` to hide point names.
1208
1230
 
1209
1231
  ### 15.3 Heatmap
1210
1232
 
1211
- **Columns:**
1233
+ **Columns** — follows §15 Rule B (prefer the indented block for multiple columns):
1212
1234
  ```
1213
- columns Jan Feb Mar
1235
+ columns
1236
+ Jan
1237
+ Feb
1238
+ Mar
1214
1239
  ```
1215
1240
 
1216
- Commas between column names are optional.
1241
+ Short one-line form is tolerated: `columns Jan Feb Mar`.
1217
1242
 
1218
- **Data rows (space-separated, NO colon):**
1243
+ **Data rows** follows §15 Rule A:
1219
1244
  ```
1220
1245
  RowLabel 5 4 3
1221
1246
  ```
1222
1247
 
1223
- Commas between values are optional. Thousands commas supported.
1224
-
1225
1248
  ### 15.4 Function Charts (Colon REQUIRED)
1226
1249
 
1227
1250
  ```
@@ -1254,7 +1277,7 @@ Source -> Target 3500
1254
1277
  Source -- Target 2000
1255
1278
  ```
1256
1279
 
1257
- `->` = directed, `--` = undirected. Thousands commas supported in values.
1280
+ `->` = directed, `--` = undirected. Values follow §15 Rule A.
1258
1281
 
1259
1282
  ### 15.6 Chord Charts
1260
1283
 
@@ -1263,19 +1286,17 @@ Blackbeard -- Bonnet 150 // undirected
1263
1286
  Roberts -> Rackham 20 // directed
1264
1287
  ```
1265
1288
 
1266
- Thousands commas supported in values.
1289
+ Values follow §15 Rule A.
1267
1290
 
1268
1291
  ### 15.7 Funnel Charts
1269
1292
 
1270
- **Data rows (space-separated, NO colon):**
1293
+ **Data rows** — follows §15 Rule A (space-separated):
1271
1294
  ```
1272
1295
  Visits 1200
1273
1296
  Signups 800
1274
1297
  Purchases 200
1275
1298
  ```
1276
1299
 
1277
- Thousands commas supported.
1278
-
1279
1300
  ---
1280
1301
 
1281
1302
  ## 16. Visualizations
@@ -1297,7 +1318,7 @@ Roberts 12 52
1297
1318
  Before COVID
1298
1319
  After COVID
1299
1320
  ```
1300
- - Data rows: `Label value1 value2` — space-separated, no colons, no commas between values
1321
+ - Data rows: `Label value1 value2` — follows §15 Rule A (space-separated; commas between values tolerated for back-compat but not idiomatic)
1301
1322
  - Thousands commas within values supported (e.g., `1,000`)
1302
1323
  - Color annotations: `Label (color) value1 value2`
1303
1324
  - Minimum 2 periods required
@@ -1364,9 +1385,9 @@ Quartermaster 0.9 0.95
1364
1385
  Navigator 0.85 0.8
1365
1386
  ```
1366
1387
 
1367
- - Axis labels: `x-label Low, High` — comma-separated
1388
+ - Axis labels: `x-label Low, High` — comma-separated (low/high pair, not a data row; comma is the delimiter here by design)
1368
1389
  - Position labels: `top-right Label` — space-separated
1369
- - Data points: `Label x y` or `Label x, y` comma or space between coordinates
1390
+ - Data points: `Label x y` follows §15 Rule A (space-separated; `Label x, y` tolerated for back-compat)
1370
1391
 
1371
1392
  ---
1372
1393
 
Binary file
Binary file
@@ -0,0 +1,92 @@
1
+ Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION AND CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -2,9 +2,15 @@ bar-stacked Support Tickets by Priority
2
2
  x-label Month
3
3
  y-label Tickets
4
4
 
5
- series Critical (red), High (orange), Medium (yellow), Low (green), Info (blue)
6
- January 8, 24, 45, 62, 31
7
- February 12, 19, 51, 58, 28
8
- March 6, 22, 38, 71, 35
9
- April 10, 28, 42, 65, 40
10
- May 5, 15, 48, 70, 33
5
+ series
6
+ Critical (red)
7
+ High (orange)
8
+ Medium (yellow)
9
+ Low (green)
10
+ Info (blue)
11
+
12
+ January 8 24 45 62 31
13
+ February 12 19 51 58 28
14
+ March 6 22 38 71 35
15
+ April 10 28 42 65 40
16
+ May 5 15 48 70 33
@@ -1,8 +1,14 @@
1
1
  heatmap Deploy Frequency by Day and Hour
2
2
 
3
- columns Mon, Tue, Wed, Thu, Fri
4
- 6 AM 1, 2, 0, 1, 0
5
- 9 AM 5, 8, 6, 7, 4
6
- 12 PM 3, 4, 5, 3, 2
7
- 3 PM 8, 12, 9, 10, 6
8
- 6 PM 2, 3, 1, 2, 1
3
+ columns
4
+ Mon
5
+ Tue
6
+ Wed
7
+ Thu
8
+ Fri
9
+
10
+ 6 AM 1 2 0 1 0
11
+ 9 AM 5 8 6 7 4
12
+ 12 PM 3 4 5 3 2
13
+ 3 PM 8 12 9 10 6
14
+ 6 PM 2 3 1 2 1
@@ -2,10 +2,14 @@ multi-line Quarterly Revenue vs Operating Cost
2
2
  x-label Quarter
3
3
  y-label Amount ($M)
4
4
 
5
- series Revenue (blue), Operating Cost (red), Net Profit (green)
6
- Q1 2023 4.2, 3.1, 1.1
7
- Q2 2023 4.8, 3.3, 1.5
8
- Q3 2023 5.1, 3.5, 1.6
9
- Q4 2023 5.9, 3.7, 2.2
10
- Q1 2024 6.3, 3.9, 2.4
11
- Q2 2024 7.1, 4.2, 2.9
5
+ series
6
+ Revenue (blue)
7
+ Operating Cost (red)
8
+ Net Profit (green)
9
+
10
+ Q1 2023 4.2 3.1 1.1
11
+ Q2 2023 4.8 3.3 1.5
12
+ Q3 2023 5.1 3.5 1.6
13
+ Q4 2023 5.9 3.7 2.2
14
+ Q1 2024 6.3 3.9 2.4
15
+ Q2 2024 7.1 4.2 2.9
@@ -8,11 +8,11 @@ top-right Major Projects
8
8
  bottom-left Fill-ins
9
9
  bottom-right Avoid
10
10
 
11
- Dark Mode (blue) 0.25, 0.85
12
- API v2 (red) 0.8, 0.9
13
- Fix Typos 0.1, 0.15
14
- SSO Integration 0.75, 0.7
15
- Export CSV 0.3, 0.6
16
- Refactor Auth 0.85, 0.3
17
- Add Tooltips 0.15, 0.45
18
- Mobile App 0.9, 0.95
11
+ Dark Mode (blue) 0.25 0.85
12
+ API v2 (red) 0.8 0.9
13
+ Fix Typos 0.1 0.15
14
+ SSO Integration 0.75 0.7
15
+ Export CSV 0.3 0.6
16
+ Refactor Auth 0.85 0.3
17
+ Add Tooltips 0.15 0.45
18
+ Mobile App 0.9 0.95
@@ -3,19 +3,19 @@ x-label Funding ($M)
3
3
  y-label Annual Revenue ($M)
4
4
 
5
5
  [SaaS](blue)
6
- Acme Cloud 12, 8.5
7
- DataSync 5.2, 3.1
8
- CloudOps 25, 18.4
9
- PlatformX 8, 5.7
10
- NexGen 3.5, 1.2
6
+ Acme Cloud 12 8.5
7
+ DataSync 5.2 3.1
8
+ CloudOps 25 18.4
9
+ PlatformX 8 5.7
10
+ NexGen 3.5 1.2
11
11
 
12
12
  [Fintech](green)
13
- PayFlow 45, 32
14
- LendTech 18, 12.5
15
- CoinBase+ 60, 41
16
- QuickPay 9, 6.8
13
+ PayFlow 45 32
14
+ LendTech 18 12.5
15
+ CoinBase+ 60 41
16
+ QuickPay 9 6.8
17
17
 
18
18
  [HealthTech](red)
19
- MediScan 15, 7.2
20
- HealthAI 22, 14.1
21
- CareLink 7, 3.8
19
+ MediScan 15 7.2
20
+ HealthAI 22 14.1
21
+ CareLink 7 3.8
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.23",
3
+ "version": "0.8.26",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/diagrammo/dgmo.git"
9
+ },
10
+ "homepage": "https://github.com/diagrammo/dgmo#readme",
6
11
  "type": "module",
7
12
  "bin": {
8
13
  "dgmo": "dist/cli.cjs"
@@ -50,10 +55,12 @@
50
55
  "types": "./dist/internal.d.cts",
51
56
  "default": "./dist/internal.cjs"
52
57
  }
53
- }
58
+ },
59
+ "./fonts/*": "./fonts/*"
54
60
  },
55
61
  "files": [
56
62
  "dist",
63
+ "fonts",
57
64
  "src",
58
65
  "docs",
59
66
  "gallery/fixtures",
@@ -82,7 +89,7 @@
82
89
  "check:deadcode": "knip",
83
90
  "check:spelling": "cspell \"src/**/*.ts\" \"tests/**/*.ts\"",
84
91
  "check:all": "pnpm check:deadcode && pnpm check:spelling && pnpm check:duplication && pnpm check:circular && pnpm check:deps && pnpm check:security && pnpm build && pnpm check:publish && pnpm check:types",
85
- "check:circular": "madge --circular --extensions ts src/ --json | node -e \"const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const n=c.length; if(n>7){console.error('New circular deps found ('+n+' > 7 known type-only cycles)');process.exit(1)}else if(n>0){console.log(n+' known type-only/dynamic cycles (safe)')}else{console.log('No circular dependencies')}\"",
92
+ "check:circular": "madge --circular --extensions ts src/ --json | node -e \"const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const n=c.length; if(n>10){console.error('New circular deps found ('+n+' > 10 known type-only cycles)');process.exit(1)}else if(n>0){console.log(n+' known type-only/dynamic cycles (safe)')}else{console.log('No circular dependencies')}\"",
86
93
  "check:deps": "depcheck --ignores='@codemirror/language,@lezer/*,husky,lint-staged,tsup'",
87
94
  "check:security": "pnpm audit --prod",
88
95
  "check:publish": "publint",
@@ -97,6 +97,7 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
97
97
  const groupLabels = new Set<string>();
98
98
  let lastNodeLabel: string | null = null;
99
99
  let lastSourceIsGroup = false;
100
+ let lastNodeIndent = 0;
100
101
 
101
102
  // Description collection state
102
103
  let descState: {
@@ -481,9 +482,14 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
481
482
  // Indented shorthand: `-> Target` or `-label-> Target`
482
483
  if (trimmed.startsWith('->') || /^-[^>].*->/.test(trimmed)) {
483
484
  // If the edge is at group-child indent level, use the containing group
485
+ // UNLESS lastNodeLabel is a plain node (not a group) — then the edge
486
+ // is indented under that node and should source from it.
484
487
  const gs = currentGroupState();
485
488
  const inGroup = gs && indent > gs.indent;
486
- if (inGroup) {
489
+ // Edge is deeper than the last node → indented under that node, use it
490
+ const indentedUnderNode =
491
+ lastNodeLabel && !lastSourceIsGroup && indent > lastNodeIndent;
492
+ if (inGroup && !indentedUnderNode) {
487
493
  const sourcePrefix = `[${gs.group.label}]`;
488
494
  edgeText = `${sourcePrefix} ${trimmed}`;
489
495
  } else if (lastNodeLabel) {
@@ -530,6 +536,7 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
530
536
  }
531
537
  lastNodeLabel = node.label;
532
538
  lastSourceIsGroup = false;
539
+ lastNodeIndent = indent;
533
540
 
534
541
  const gs = currentGroupState();
535
542
  const isGroupChild = gs && indent > gs.indent;
@@ -616,7 +623,11 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
616
623
  if (result.tagGroups.length > 0) {
617
624
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
618
625
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
619
- validateTagGroupNames(result.tagGroups, pushWarning);
626
+ validateTagGroupNames(result.tagGroups, pushWarning, (line, msg) => {
627
+ const diag = makeDgmoError(line, msg);
628
+ result.diagnostics.push(diag);
629
+ if (!result.error) result.error = diag.message;
630
+ });
620
631
  }
621
632
 
622
633
  return result;
@@ -488,19 +488,28 @@ export function renderBoxesAndLines(
488
488
  .attr('clip-path', `url(#${clipId})`)
489
489
  .attr('class', 'bl-collapse-bar');
490
490
 
491
- // Label centered vertically
492
- groupG
493
- .append('text')
494
- .attr('class', 'bl-group-label')
495
- .attr('x', group.x)
496
- .attr('y', group.y)
497
- .attr('text-anchor', 'middle')
498
- .attr('dominant-baseline', 'central')
499
- .attr('font-family', FONT_FAMILY)
500
- .attr('font-size', GROUP_LABEL_FONT_SIZE)
501
- .attr('font-weight', '600')
502
- .attr('fill', palette.text)
503
- .text(group.label);
491
+ // Label centered vertically — wrap like regular nodes
492
+ const maxLabelLines = Math.max(
493
+ 2,
494
+ Math.floor((group.height - 16) / (MIN_NODE_FONT_SIZE * 1.3))
495
+ );
496
+ const fitted = fitLabelToHeader(group.label, group.width, maxLabelLines);
497
+ const lineH = fitted.fontSize * 1.3;
498
+ const totalH = fitted.lines.length * lineH;
499
+ for (let li = 0; li < fitted.lines.length; li++) {
500
+ groupG
501
+ .append('text')
502
+ .attr('class', 'bl-group-label')
503
+ .attr('x', group.x)
504
+ .attr('y', group.y - totalH / 2 + lineH / 2 + li * lineH)
505
+ .attr('text-anchor', 'middle')
506
+ .attr('dominant-baseline', 'central')
507
+ .attr('font-family', FONT_FAMILY)
508
+ .attr('font-size', fitted.fontSize)
509
+ .attr('font-weight', '600')
510
+ .attr('fill', palette.text)
511
+ .text(fitted.lines[li]);
512
+ }
504
513
  } else {
505
514
  // Expanded: background container with label
506
515
  groupG
@@ -0,0 +1,162 @@
1
+ // ============================================================
2
+ // Chart-type scoring (pluggability seam)
3
+ // ============================================================
4
+ //
5
+ // This module is the designated pluggability point for chart-type selection.
6
+ // The input/output shape (prompt string → ranked candidates + confidence) is
7
+ // stable; the internals can later swap to embedding-based similarity, a local
8
+ // tiny LM, or any other approach without changing the MCP tool surface or
9
+ // the `chartTypes` data model.
10
+
11
+ import { chartTypes, type ChartTypeMeta } from './chart-types';
12
+
13
+ const TYPOGRAPHIC_REPLACEMENTS: [RegExp, string][] = [
14
+ [/[‘’]/g, "'"], // curly single quotes
15
+ [/[“”]/g, '"'], // curly double quotes
16
+ [/[–—]/g, '-'], // en/em dash
17
+ [/×/g, 'x'], // unicode multiplication → ASCII (for "2×2" vs "2x2")
18
+ ];
19
+
20
+ /** Normalize a string to lowercase ASCII-ish tokens for matching. */
21
+ export function normalize(s: string): string[] {
22
+ let out = s.normalize('NFKD').toLowerCase();
23
+ for (const [re, repl] of TYPOGRAPHIC_REPLACEMENTS)
24
+ out = out.replace(re, repl);
25
+ return out.split(/[^a-z0-9]+/).filter(Boolean);
26
+ }
27
+
28
+ /**
29
+ * True if `triggerTokens` appears as a contiguous slice of `promptTokens`.
30
+ * Token-based (not substring) — prevents "scatter plot" matching "scattered
31
+ * the plot", "ER diagram" matching "water diagram", and similar traps.
32
+ */
33
+ export function matchesContiguously(
34
+ promptTokens: readonly string[],
35
+ triggerTokens: readonly string[]
36
+ ): boolean {
37
+ if (triggerTokens.length === 0 || triggerTokens.length > promptTokens.length)
38
+ return false;
39
+ outer: for (let i = 0; i <= promptTokens.length - triggerTokens.length; i++) {
40
+ for (let j = 0; j < triggerTokens.length; j++) {
41
+ if (promptTokens[i + j] !== triggerTokens[j]) continue outer;
42
+ }
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ export interface ChartTypeScore {
49
+ readonly type: ChartTypeMeta;
50
+ readonly score: number;
51
+ readonly matched: string[];
52
+ }
53
+
54
+ /**
55
+ * Score a single chart type against a prompt.
56
+ *
57
+ * Primary signal: contiguous trigger-phrase matches weighted by token count
58
+ * (longer phrases beat shorter ones). Secondary signal: description-word
59
+ * overlap at 0.25× weight — a tiebreak-only hint that rescues prompts which
60
+ * miss every trigger but touch description vocabulary. Triggers always win
61
+ * over descriptions because any trigger match is ≥1.0 and descriptions
62
+ * contribute ≤0.25 per token.
63
+ */
64
+ export function scoreChartType(
65
+ prompt: string,
66
+ type: ChartTypeMeta
67
+ ): { score: number; matched: string[] } {
68
+ const promptTokens = normalize(prompt);
69
+ const matched: string[] = [];
70
+ let score = 0;
71
+
72
+ for (const trigger of type.triggers) {
73
+ const triggerTokens = normalize(trigger);
74
+ if (matchesContiguously(promptTokens, triggerTokens)) {
75
+ matched.push(trigger);
76
+ score += triggerTokens.length;
77
+ }
78
+ }
79
+
80
+ const descTokens = new Set(normalize(type.description));
81
+ let descHits = 0;
82
+ for (const t of promptTokens) if (descTokens.has(t)) descHits++;
83
+ score += descHits * 0.25;
84
+
85
+ return { score, matched };
86
+ }
87
+
88
+ /**
89
+ * Minimum trigger-based score for a confident match. A result below this
90
+ * floor means no actual trigger fired — only description-rescue tokens
91
+ * contributed — so the caller should drop to the fallback list instead of
92
+ * returning a confident-looking wrong answer.
93
+ *
94
+ * 1.0 is the weight of a single-token trigger. Anything less came entirely
95
+ * from 0.25× description hits.
96
+ */
97
+ export const MIN_PRIMARY_SCORE = 1.0;
98
+
99
+ /**
100
+ * Minimum absolute score gap required before calling a match
101
+ * non-ambiguous. 0.5 ≈ two description-rescue tokens' worth, or half a
102
+ * trigger-token difference. Below this, the cliff between "medium" and
103
+ * "ambiguous" is effectively noise.
104
+ */
105
+ export const AMBIGUITY_THRESHOLD = 0.5;
106
+
107
+ export type Confidence = 'high' | 'medium' | 'ambiguous';
108
+
109
+ /**
110
+ * Confidence from the top two scores. Rules:
111
+ * 1. top < MIN_PRIMARY_SCORE → ambiguous (no real trigger matched)
112
+ * 2. second === 0 → high (nothing competes)
113
+ * 3. top ≥ 2 × second → high (top dominates)
114
+ * 4. top − second < AMBIGUITY_THRESHOLD → ambiguous (gap is noise)
115
+ * 5. otherwise → medium
116
+ */
117
+ export function confidence(top: number, second: number): Confidence {
118
+ if (top < MIN_PRIMARY_SCORE) return 'ambiguous';
119
+ if (second === 0) return 'high';
120
+ if (top >= second * 2) return 'high';
121
+ if (top - second < AMBIGUITY_THRESHOLD) return 'ambiguous';
122
+ return 'medium';
123
+ }
124
+
125
+ export interface SuggestionResult {
126
+ readonly ranked: readonly ChartTypeScore[];
127
+ readonly fallback: readonly ChartTypeMeta[];
128
+ readonly confidence: Confidence;
129
+ readonly fellBack: boolean;
130
+ }
131
+
132
+ /**
133
+ * Score every chart type against `prompt` and return a ranked suggestion
134
+ * bundle. Types with score 0 are filtered out. When the top score is below
135
+ * `MIN_PRIMARY_SCORE` (no real trigger fired), the caller should present
136
+ * the fallback list — `fellBack` is set to true in that case.
137
+ *
138
+ * Array order is preserved: scoring iterates `chartTypes` in source order
139
+ * and `.sort` is stable in V8, so ties go to the earlier entry — specialized
140
+ * types beat generic catch-alls by construction.
141
+ */
142
+ export function suggestChartTypes(prompt: string): SuggestionResult {
143
+ const scored: ChartTypeScore[] = [];
144
+ for (const type of chartTypes) {
145
+ const { score, matched } = scoreChartType(prompt, type);
146
+ if (score > 0) scored.push({ type, score, matched });
147
+ }
148
+ scored.sort((a, b) => b.score - a.score);
149
+
150
+ const fallback = chartTypes.filter((c) => c.fallback);
151
+
152
+ const topScore = scored[0]?.score ?? 0;
153
+ const secondScore = scored[1]?.score ?? 0;
154
+ const fellBack = topScore < MIN_PRIMARY_SCORE;
155
+
156
+ return {
157
+ ranked: scored,
158
+ fallback,
159
+ confidence: confidence(topScore, secondScore),
160
+ fellBack,
161
+ };
162
+ }