@diagrammo/dgmo 0.8.22 → 0.8.25
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.
- package/.claude/commands/dgmo.md +60 -72
- package/dist/cli.cjs +123 -116
- package/dist/editor.cjs +3 -2
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +3 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +3 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1649 -442
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -23
- package/dist/index.d.ts +196 -23
- package/dist/index.js +1631 -440
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +677 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +267 -0
- package/dist/internal.d.ts +267 -0
- package/dist/internal.js +633 -0
- package/dist/internal.js.map +1 -0
- package/docs/guide/chart-area.md +17 -17
- package/docs/guide/chart-bar-stacked.md +12 -12
- package/docs/guide/chart-cycle.md +156 -0
- package/docs/guide/chart-doughnut.md +10 -10
- package/docs/guide/chart-funnel.md +9 -9
- package/docs/guide/chart-heatmap.md +10 -10
- package/docs/guide/chart-journey-map.md +179 -0
- package/docs/guide/chart-kanban.md +2 -0
- package/docs/guide/chart-line.md +19 -19
- package/docs/guide/chart-multi-line.md +16 -16
- package/docs/guide/chart-pie.md +11 -11
- package/docs/guide/chart-polar-area.md +10 -10
- package/docs/guide/chart-pyramid.md +111 -0
- package/docs/guide/chart-radar.md +9 -9
- package/docs/guide/chart-scatter.md +24 -27
- package/docs/guide/index.md +3 -3
- package/docs/guide/registry.json +5 -0
- package/docs/language-reference.md +108 -26
- package/fonts/Inter-Bold.ttf +0 -0
- package/fonts/Inter-Regular.ttf +0 -0
- package/fonts/LICENSE-Inter.txt +92 -0
- package/gallery/fixtures/bar-stacked.dgmo +12 -6
- package/gallery/fixtures/heatmap.dgmo +12 -6
- package/gallery/fixtures/multi-line.dgmo +11 -7
- package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
- package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
- package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
- package/gallery/fixtures/quadrant.dgmo +8 -8
- package/gallery/fixtures/scatter.dgmo +12 -12
- package/package.json +14 -2
- package/src/boxes-and-lines/parser.ts +13 -2
- package/src/boxes-and-lines/renderer.ts +22 -13
- package/src/chart-type-scoring.ts +162 -0
- package/src/chart-types.ts +437 -0
- package/src/cli.ts +152 -101
- package/src/completion.ts +9 -48
- package/src/cycle/layout.ts +19 -28
- package/src/cycle/renderer.ts +59 -32
- package/src/cycle/types.ts +21 -0
- package/src/d3.ts +30 -3
- package/src/dgmo-router.ts +98 -73
- package/src/echarts.ts +1 -1
- package/src/editor/keywords.ts +3 -2
- package/src/fonts.ts +3 -2
- package/src/gantt/parser.ts +5 -1
- package/src/index.ts +37 -3
- package/src/infra/parser.ts +3 -3
- package/src/internal.ts +20 -0
- package/src/journey-map/layout.ts +7 -3
- package/src/journey-map/parser.ts +5 -1
- package/src/journey-map/renderer.ts +112 -47
- package/src/kanban/parser.ts +5 -1
- package/src/org/collapse.ts +82 -4
- package/src/org/parser.ts +1 -1
- package/src/org/renderer.ts +221 -4
- package/src/pyramid/parser.ts +172 -0
- package/src/pyramid/renderer.ts +684 -0
- package/src/pyramid/types.ts +28 -0
- package/src/render.ts +2 -8
- package/src/sequence/parser.ts +64 -22
- package/src/sequence/participant-inference.ts +0 -1
- package/src/sequence/renderer.ts +97 -265
- package/src/sharing.ts +0 -1
- package/src/sitemap/parser.ts +1 -1
- package/src/tech-radar/interactive.ts +54 -0
- package/src/utils/parsing.ts +1 -0
- package/src/utils/tag-groups.ts +35 -5
- package/src/wireframe/parser.ts +3 -1
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Q1
|
|
11
|
-
Q2
|
|
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
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pyramid The DIKW Pyramid
|
|
2
|
+
|
|
3
|
+
Wisdom | color: purple
|
|
4
|
+
Ethical judgment, acting with
|
|
5
|
+
insight — knowing *why*.
|
|
6
|
+
|
|
7
|
+
Knowledge | color: blue
|
|
8
|
+
Synthesized information —
|
|
9
|
+
knowing *how*.
|
|
10
|
+
|
|
11
|
+
Information | color: green
|
|
12
|
+
Processed data with context —
|
|
13
|
+
knowing *what*.
|
|
14
|
+
|
|
15
|
+
Data | color: yellow
|
|
16
|
+
Raw facts and measurements —
|
|
17
|
+
unprocessed signals.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pyramid Acquisition Funnel
|
|
2
|
+
|
|
3
|
+
inverted
|
|
4
|
+
|
|
5
|
+
Visitors | color: blue
|
|
6
|
+
Landed on the site from any channel
|
|
7
|
+
this month.
|
|
8
|
+
|
|
9
|
+
Signups | color: cyan
|
|
10
|
+
Provided an email and confirmed.
|
|
11
|
+
|
|
12
|
+
Activated | color: green
|
|
13
|
+
Completed the first meaningful action.
|
|
14
|
+
|
|
15
|
+
Paid | color: orange
|
|
16
|
+
Converted to a paid plan.
|
|
@@ -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
|
|
12
|
-
API v2 (red) 0.8
|
|
13
|
-
Fix Typos 0.1
|
|
14
|
-
SSO Integration 0.75
|
|
15
|
-
Export CSV 0.3
|
|
16
|
-
Refactor Auth 0.85
|
|
17
|
-
Add Tooltips 0.15
|
|
18
|
-
Mobile App 0.9
|
|
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
|
|
7
|
-
DataSync 5.2
|
|
8
|
-
CloudOps 25
|
|
9
|
-
PlatformX 8
|
|
10
|
-
NexGen 3.5
|
|
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
|
|
14
|
-
LendTech 18
|
|
15
|
-
CoinBase+ 60
|
|
16
|
-
QuickPay 9
|
|
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
|
|
20
|
-
HealthAI 22
|
|
21
|
-
CareLink 7
|
|
19
|
+
MediScan 15 7.2
|
|
20
|
+
HealthAI 22 14.1
|
|
21
|
+
CareLink 7 3.8
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.25",
|
|
4
4
|
"description": "DGMO diagram markup language — parser, renderer, and color system",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,10 +40,22 @@
|
|
|
40
40
|
"types": "./dist/highlight.d.cts",
|
|
41
41
|
"default": "./dist/highlight.cjs"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
},
|
|
44
|
+
"./internal": {
|
|
45
|
+
"import": {
|
|
46
|
+
"types": "./dist/internal.d.ts",
|
|
47
|
+
"default": "./dist/internal.js"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/internal.d.cts",
|
|
51
|
+
"default": "./dist/internal.cjs"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./fonts/*": "./fonts/*"
|
|
44
55
|
},
|
|
45
56
|
"files": [
|
|
46
57
|
"dist",
|
|
58
|
+
"fonts",
|
|
47
59
|
"src",
|
|
48
60
|
"docs",
|
|
49
61
|
"gallery/fixtures",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
}
|