@blueprint-chart/mcp 0.1.1 → 0.1.4

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 (131) hide show
  1. package/README.md +31 -15
  2. package/dist/cli.js +15 -2
  3. package/dist/dsl/capabilityMatrix.d.ts +22 -0
  4. package/dist/dsl/capabilityMatrix.js +37 -0
  5. package/dist/dsl/capabilityMatrix.test.d.ts +1 -0
  6. package/dist/dsl/capabilityMatrix.test.js +49 -0
  7. package/dist/dsl/chartTypes.d.ts +16 -0
  8. package/dist/dsl/chartTypes.js +37 -0
  9. package/dist/dsl/chartTypes.test.d.ts +1 -0
  10. package/dist/dsl/chartTypes.test.js +32 -0
  11. package/dist/dsl/dataKey.d.ts +25 -0
  12. package/dist/dsl/dataKey.js +42 -0
  13. package/dist/dsl/dataKey.test.d.ts +1 -0
  14. package/dist/dsl/dataKey.test.js +35 -0
  15. package/dist/dsl/goalRanking.d.ts +7 -0
  16. package/dist/dsl/goalRanking.js +76 -0
  17. package/dist/dsl/goalRanking.test.d.ts +1 -0
  18. package/dist/dsl/goalRanking.test.js +83 -0
  19. package/dist/dsl/parseErrorHints.d.ts +12 -0
  20. package/dist/dsl/parseErrorHints.js +32 -0
  21. package/dist/dsl/parseErrorHints.test.d.ts +1 -0
  22. package/dist/dsl/parseErrorHints.test.js +26 -0
  23. package/dist/dsl/semanticWarnings.d.ts +7 -0
  24. package/dist/dsl/semanticWarnings.js +66 -0
  25. package/dist/dsl/semanticWarnings.test.d.ts +1 -0
  26. package/dist/dsl/semanticWarnings.test.js +32 -0
  27. package/dist/dsl/suggest.d.ts +1 -0
  28. package/dist/dsl/suggest.js +66 -0
  29. package/dist/dsl/suggest.test.d.ts +1 -0
  30. package/dist/dsl/suggest.test.js +34 -0
  31. package/dist/dsl/universalProperties.d.ts +30 -0
  32. package/dist/dsl/universalProperties.js +52 -0
  33. package/dist/dsl/universalProperties.test.d.ts +1 -0
  34. package/dist/dsl/universalProperties.test.js +26 -0
  35. package/dist/dsl/validate.d.ts +10 -0
  36. package/dist/dsl/validate.js +68 -0
  37. package/dist/dsl/validate.test.d.ts +1 -0
  38. package/dist/dsl/validate.test.js +73 -0
  39. package/dist/errors.d.ts +20 -1
  40. package/dist/errors.js +1 -0
  41. package/dist/errors.test.js +21 -0
  42. package/dist/lib/zodToJsonSchema.d.ts +10 -5
  43. package/dist/lib/zodToJsonSchema.js +14 -6
  44. package/dist/links/buildUrls.d.ts +14 -0
  45. package/dist/links/buildUrls.js +20 -0
  46. package/dist/links/buildUrls.test.d.ts +1 -0
  47. package/dist/links/buildUrls.test.js +28 -0
  48. package/dist/links/editorConfig.d.ts +4 -0
  49. package/dist/links/editorConfig.js +15 -0
  50. package/dist/links/editorConfig.test.d.ts +1 -0
  51. package/dist/links/editorConfig.test.js +28 -0
  52. package/dist/links/encode.d.ts +11 -0
  53. package/dist/links/encode.js +19 -0
  54. package/dist/links/encode.test.d.ts +1 -0
  55. package/dist/links/encode.test.js +37 -0
  56. package/dist/parse.js +14 -6
  57. package/dist/parse.test.js +8 -0
  58. package/dist/prompts/authorChart.js +23 -18
  59. package/dist/prompts/authorChart.test.js +6 -0
  60. package/dist/render/diagnose.d.ts +19 -0
  61. package/dist/render/diagnose.js +100 -0
  62. package/dist/render/diagnose.test.d.ts +1 -0
  63. package/dist/render/diagnose.test.js +53 -0
  64. package/dist/render/frame.d.ts +10 -0
  65. package/dist/render/frame.js +10 -0
  66. package/dist/render/frame.test.d.ts +1 -0
  67. package/dist/render/frame.test.js +12 -0
  68. package/dist/render/jsdomEnv.d.ts +2 -1
  69. package/dist/render/jsdomEnv.js +14 -1
  70. package/dist/render/jsdomEnv.test.js +36 -2
  71. package/dist/render/renderSceneState.d.ts +5 -1
  72. package/dist/render/renderSceneState.js +4 -3
  73. package/dist/render/renderSceneState.test.js +13 -7
  74. package/dist/render/validatePipeline.d.ts +23 -0
  75. package/dist/render/validatePipeline.js +41 -0
  76. package/dist/render/validatePipeline.test.d.ts +1 -0
  77. package/dist/render/validatePipeline.test.js +34 -0
  78. package/dist/resources/docsReader.d.ts +4 -1
  79. package/dist/resources/docsReader.js +23 -6
  80. package/dist/resources/docsReader.test.js +27 -2
  81. package/dist/resources/index.d.ts +1 -1
  82. package/dist/resources/samples.d.ts +1 -2
  83. package/dist/server.d.ts +9 -0
  84. package/dist/server.js +75 -5
  85. package/dist/server.test.js +105 -4
  86. package/dist/tools/describeChartType.d.ts +41 -0
  87. package/dist/tools/describeChartType.js +143 -0
  88. package/dist/tools/describeChartType.test.d.ts +1 -0
  89. package/dist/tools/describeChartType.test.js +78 -0
  90. package/dist/tools/exportChart.d.ts +17 -0
  91. package/dist/tools/exportChart.js +31 -0
  92. package/dist/tools/exportChart.test.d.ts +1 -0
  93. package/dist/tools/exportChart.test.js +43 -0
  94. package/dist/tools/getExample.d.ts +20 -0
  95. package/dist/tools/getExample.js +55 -0
  96. package/dist/tools/getExample.test.d.ts +1 -0
  97. package/dist/tools/getExample.test.js +40 -0
  98. package/dist/tools/getGrammar.d.ts +17 -0
  99. package/dist/tools/getGrammar.js +38 -0
  100. package/dist/tools/getGrammar.test.d.ts +1 -0
  101. package/dist/tools/getGrammar.test.js +35 -0
  102. package/dist/tools/inspect.d.ts +8 -1
  103. package/dist/tools/inspect.js +40 -7
  104. package/dist/tools/inspect.test.js +62 -13
  105. package/dist/tools/listChartTypes.d.ts +14 -0
  106. package/dist/tools/listChartTypes.js +42 -0
  107. package/dist/tools/listChartTypes.test.d.ts +1 -0
  108. package/dist/tools/listChartTypes.test.js +42 -0
  109. package/dist/tools/listPalettes.d.ts +13 -0
  110. package/dist/tools/listPalettes.js +12 -0
  111. package/dist/tools/listPalettes.test.d.ts +1 -0
  112. package/dist/tools/listPalettes.test.js +15 -0
  113. package/dist/tools/recommend.js +3 -1
  114. package/dist/tools/recommend.test.js +40 -0
  115. package/dist/tools/render.d.ts +14 -12
  116. package/dist/tools/render.js +96 -28
  117. package/dist/tools/render.test.js +137 -1
  118. package/dist/tools/searchExamples.d.ts +28 -0
  119. package/dist/tools/searchExamples.js +54 -0
  120. package/dist/tools/searchExamples.test.d.ts +1 -0
  121. package/dist/tools/searchExamples.test.js +32 -0
  122. package/dist/tools/validate.d.ts +9 -3
  123. package/dist/tools/validate.js +11 -1
  124. package/dist/tools/validate.test.js +33 -11
  125. package/dist/transports/http.d.ts +4 -2
  126. package/dist/transports/http.js +232 -23
  127. package/dist/transports/http.test.js +158 -22
  128. package/package.json +5 -3
  129. package/public/apple-touch-icon.png +0 -0
  130. package/public/favicon.png +0 -0
  131. package/public/favicon.svg +9 -0
@@ -0,0 +1,143 @@
1
+ import { z } from 'zod';
2
+ import { getChartOptions, samples } from '@blueprint-chart/lib';
3
+ import { getDoc, listDocs } from '@blueprint-chart/docs';
4
+ import { aliasesFor, canonicalChartType, listCanonicalChartTypes } from '../dsl/chartTypes';
5
+ import { nearestSuggestion } from '../dsl/suggest';
6
+ import { UNIVERSAL_PROPERTIES, UNIVERSAL_PROPERTY_META } from '../dsl/universalProperties';
7
+ import { ErrorCode, toolError, toolOk } from '../errors';
8
+ import { publicDocUrl } from '../resources/docsReader';
9
+ import { lookupCapability, statusOf } from '../dsl/capabilityMatrix';
10
+ export const DescribeChartTypeInputSchema = z.object({
11
+ chartType: z.string(),
12
+ }).strict();
13
+ function extractDocSections(name) {
14
+ const entries = listDocs('charts');
15
+ const entry = entries.find(e => e.slug === name);
16
+ if (!entry) {
17
+ return { summary: '', whenToUse: [], whenNotToUse: [], example: '' };
18
+ }
19
+ let content;
20
+ try {
21
+ content = getDoc('charts', name).content;
22
+ }
23
+ catch {
24
+ return { summary: '', whenToUse: [], whenNotToUse: [], example: '' };
25
+ }
26
+ const summary = content.match(/^>\s*(.+)$/m)?.[1]?.trim() ?? '';
27
+ const sectionBullets = (heading) => {
28
+ const re = new RegExp(`##\\s+${heading}[\\s\\S]*?(?=^##\\s+|\\Z)`, 'm');
29
+ const block = content.match(re)?.[0] ?? '';
30
+ return Array.from(block.matchAll(/^-\s+(.+)$/gm)).map(m => m[1].trim());
31
+ };
32
+ const example = content.match(/```bpc\n([\s\S]*?)```/)?.[1]?.trim() ?? '';
33
+ return {
34
+ summary,
35
+ whenToUse: sectionBullets('When to use'),
36
+ whenNotToUse: sectionBullets('When NOT to use'),
37
+ example,
38
+ };
39
+ }
40
+ function mapOption(opt) {
41
+ const prop = {
42
+ key: opt.key,
43
+ type: String(opt.type),
44
+ };
45
+ if (opt.label) {
46
+ prop.description = opt.label;
47
+ }
48
+ if (opt.choices !== undefined && opt.choices.length > 0) {
49
+ prop.choices = opt.choices.map(c => String(c.value));
50
+ }
51
+ if (opt.default !== undefined) {
52
+ prop.default = opt.default;
53
+ }
54
+ return prop;
55
+ }
56
+ function buildProperties(canonical) {
57
+ const registeredOptions = getChartOptions(canonical);
58
+ const registeredKeys = new Set(registeredOptions.map(o => o.key));
59
+ const props = registeredOptions.map(mapOption);
60
+ // Merge in universal properties that are not already present in the
61
+ // per-type registry. This ensures discoverable properties like `sort`,
62
+ // `title`, and `colors` appear for every chart type.
63
+ for (const key of UNIVERSAL_PROPERTIES) {
64
+ if (!registeredKeys.has(key)) {
65
+ const meta = UNIVERSAL_PROPERTY_META[key];
66
+ if (meta) {
67
+ props.push({ key, ...meta });
68
+ }
69
+ else {
70
+ props.push({ key, type: 'text' });
71
+ }
72
+ }
73
+ }
74
+ return props;
75
+ }
76
+ function inferDataShape(name, example) {
77
+ if (example.includes('_series')) {
78
+ return { kind: 'multi-series', example };
79
+ }
80
+ if (example.includes('data')) {
81
+ return { kind: 'single-series', example };
82
+ }
83
+ return { kind: 'unknown', example: 'data {\n "Label" = 1.0\n}' };
84
+ }
85
+ const DIRECTIVE_DOCS = [
86
+ { name: 'highlight', description: 'Emphasise one category/series, e.g. `highlight "China"`.' },
87
+ { name: 'colorize', description: 'Override colour for a category/series, e.g. `colorize "China" { color = "#f00" }`.' },
88
+ { name: 'annotation', description: 'Attach a callout to a data point, e.g. `annotation "2009" { text = "…" }`.' },
89
+ { name: 'transform', description: 'Reshape data, e.g. `transform sort { column = "value" direction = descending }`.' },
90
+ { name: 'scene', description: 'Add a narrative step that overrides data/properties, e.g. `scene "Step 2" { … }`.' },
91
+ ];
92
+ function buildDirectives(canonical) {
93
+ return DIRECTIVE_DOCS.map((d) => {
94
+ const cell = lookupCapability(canonical, d.name);
95
+ const directive = {
96
+ name: d.name,
97
+ status: statusOf(cell),
98
+ description: d.description,
99
+ };
100
+ if (cell.note) {
101
+ directive.note = cell.note;
102
+ }
103
+ return directive;
104
+ });
105
+ }
106
+ export function describeChartType(input) {
107
+ const parsed = DescribeChartTypeInputSchema.safeParse(input);
108
+ if (!parsed.success) {
109
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
110
+ }
111
+ const canonical = canonicalChartType(parsed.data.chartType);
112
+ if (!canonical) {
113
+ const known = listCanonicalChartTypes();
114
+ return toolError(ErrorCode.E_INPUT, [{
115
+ code: 'E_UNKNOWN_CHART_TYPE',
116
+ path: 'chartType',
117
+ message: `Unknown chart type "${parsed.data.chartType}". Known types: ${known.join(', ')}.`,
118
+ suggestion: nearestSuggestion(parsed.data.chartType, known),
119
+ context: { got: parsed.data.chartType, known },
120
+ }]);
121
+ }
122
+ const doc = extractDocSections(canonical);
123
+ const properties = buildProperties(canonical);
124
+ const directives = buildDirectives(canonical);
125
+ const sample = samples.find(s => s.chartType === canonical);
126
+ const exampleText = doc.example || sample?.dsl || '';
127
+ const output = {
128
+ name: canonical,
129
+ aliases: aliasesFor(canonical),
130
+ summary: doc.summary,
131
+ whenToUse: doc.whenToUse,
132
+ whenNotToUse: doc.whenNotToUse,
133
+ properties,
134
+ directives,
135
+ dataShape: inferDataShape(canonical, exampleText),
136
+ exampleSlug: sample?.id,
137
+ };
138
+ const docsUrl = publicDocUrl('charts', canonical);
139
+ if (docsUrl) {
140
+ output.docsUrl = docsUrl;
141
+ }
142
+ return toolOk(output);
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { describeChartType } from './describeChartType';
3
+ describe('describe_chart_type', () => {
4
+ afterEach(() => {
5
+ delete process.env.BLUEPRINT_CHART_DOCS_URL;
6
+ });
7
+ it('returns properties + summary for bar-horizontal', () => {
8
+ const r = describeChartType({ chartType: 'bar-horizontal' });
9
+ expect(r.ok).toBe(true);
10
+ if (r.ok) {
11
+ expect(r.data.name).toBe('bar-horizontal');
12
+ expect(r.data.aliases).toContain('horizontal-bar');
13
+ expect(r.data.summary.length).toBeGreaterThan(0);
14
+ expect(r.data.properties.length).toBeGreaterThan(0);
15
+ const sort = r.data.properties.find(p => p.key === 'sort');
16
+ expect(sort).toBeDefined();
17
+ expect(sort?.type).toBeDefined();
18
+ }
19
+ });
20
+ it('accepts an alias and normalizes', () => {
21
+ const r = describeChartType({ chartType: 'horizontal-bar' });
22
+ expect(r.ok).toBe(true);
23
+ if (r.ok) {
24
+ expect(r.data.name).toBe('bar-horizontal');
25
+ }
26
+ });
27
+ it('errors with structured suggestion on unknown name', () => {
28
+ const r = describeChartType({ chartType: 'bar' });
29
+ expect(r.ok).toBe(false);
30
+ if (!r.ok) {
31
+ expect(r.errors[0].code).toBe('E_UNKNOWN_CHART_TYPE');
32
+ expect(r.errors[0].suggestion).toMatch(/^bar-/);
33
+ }
34
+ });
35
+ it('includes a starter dataShape example', () => {
36
+ const r = describeChartType({ chartType: 'bar-vertical' });
37
+ expect(r.ok).toBe(true);
38
+ if (r.ok) {
39
+ expect(r.data.dataShape.kind).toMatch(/single-series|multi-series/);
40
+ expect(r.data.dataShape.example).toContain('data');
41
+ }
42
+ });
43
+ it('omits docsUrl when docs base is unset', () => {
44
+ const result = describeChartType({ chartType: 'bar-vertical' });
45
+ expect(result.ok).toBe(true);
46
+ if (result.ok) {
47
+ expect(result.data.docsUrl).toBeUndefined();
48
+ }
49
+ });
50
+ it('includes a top-level docsUrl when docs base is set', () => {
51
+ process.env.BLUEPRINT_CHART_DOCS_URL = 'https://docs.blueprintchart.com';
52
+ const result = describeChartType({ chartType: 'bar-vertical' });
53
+ expect(result.ok).toBe(true);
54
+ if (result.ok) {
55
+ expect(result.data.docsUrl).toBe('https://docs.blueprintchart.com/charts/bar-vertical');
56
+ }
57
+ });
58
+ });
59
+ describe('describe_chart_type directives', () => {
60
+ it('lists highlight/colorize/annotation directives with status', () => {
61
+ const r = describeChartType({ chartType: 'bar-vertical' });
62
+ expect(r.ok).toBe(true);
63
+ if (r.ok) {
64
+ const names = r.data.directives.map(d => d.name);
65
+ expect(names).toEqual(expect.arrayContaining(['highlight', 'colorize', 'annotation']));
66
+ const highlight = r.data.directives.find(d => d.name === 'highlight');
67
+ expect(highlight?.status).toBe('supported');
68
+ }
69
+ });
70
+ it('marks colorize supported on donut (W1c shipped per-slice colorize)', () => {
71
+ const r = describeChartType({ chartType: 'donut' });
72
+ expect(r.ok).toBe(true);
73
+ if (r.ok) {
74
+ const colorize = r.data.directives.find(d => d.name === 'colorize');
75
+ expect(colorize?.status).toBe('supported');
76
+ }
77
+ });
78
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { type FrameMetadata } from '../render/frame';
3
+ import { type ToolResult } from '../errors';
4
+ export declare const ExportChartInputSchema: z.ZodObject<{
5
+ source: z.ZodString;
6
+ }, "strict", z.ZodTypeAny, {
7
+ source: string;
8
+ }, {
9
+ source: string;
10
+ }>;
11
+ export type ExportChartInput = z.infer<typeof ExportChartInputSchema>;
12
+ export interface ExportChartOutput {
13
+ copyUrl: string;
14
+ embedUrl: string;
15
+ frame: FrameMetadata;
16
+ }
17
+ export declare function exportChart(input: unknown): ToolResult<ExportChartOutput>;
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { getEditorBaseUrl } from '../links/editorConfig';
3
+ import { buildCopyUrl, buildEmbedUrl } from '../links/buildUrls';
4
+ import { validatePipeline } from '../render/validatePipeline';
5
+ import { extractFrameMetadata } from '../render/frame';
6
+ import { ErrorCode, toolError, toolOk } from '../errors';
7
+ export const ExportChartInputSchema = z.object({
8
+ source: z.string(),
9
+ }).strict();
10
+ export function exportChart(input) {
11
+ const parsed = ExportChartInputSchema.safeParse(input);
12
+ if (!parsed.success) {
13
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
14
+ }
15
+ const editorBase = getEditorBaseUrl();
16
+ if (!editorBase) {
17
+ return toolError(ErrorCode.E_CONFIG, [{
18
+ code: 'E_EDITOR_URL_UNSET',
19
+ message: 'Link export is not configured. Set BLUEPRINT_CHART_EDITOR_URL to enable.',
20
+ }]);
21
+ }
22
+ const validated = validatePipeline(parsed.data.source);
23
+ if (!validated.ok) {
24
+ return validated.error;
25
+ }
26
+ return toolOk({
27
+ copyUrl: buildCopyUrl(parsed.data.source, editorBase),
28
+ embedUrl: buildEmbedUrl(parsed.data.source, editorBase),
29
+ frame: extractFrameMetadata(validated.ast),
30
+ });
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { exportChart } from './exportChart';
3
+ import { ErrorCode } from '../errors';
4
+ const VALID = 'chart bar-vertical {\n title = "Hi"\n data {\n "A" = 1\n }\n}\n';
5
+ afterEach(() => {
6
+ delete process.env.BLUEPRINT_CHART_EDITOR_URL;
7
+ });
8
+ describe('exportChart', () => {
9
+ it('returns E_CONFIG when the editor base URL is unset', () => {
10
+ const result = exportChart({ source: VALID });
11
+ expect(result.ok).toBe(false);
12
+ if (!result.ok) {
13
+ expect(result.code).toBe(ErrorCode.E_CONFIG);
14
+ expect(result.errors[0]?.message).toMatch(/BLUEPRINT_CHART_EDITOR_URL/);
15
+ }
16
+ });
17
+ it('returns E_INPUT when source is missing', () => {
18
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
19
+ const result = exportChart({});
20
+ expect(result.ok).toBe(false);
21
+ if (!result.ok) {
22
+ expect(result.code).toBe(ErrorCode.E_INPUT);
23
+ }
24
+ });
25
+ it('propagates E_PARSE from the shared validation pipeline', () => {
26
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
27
+ const result = exportChart({ source: 'chart bar-vertical {' });
28
+ expect(result.ok).toBe(false);
29
+ if (!result.ok) {
30
+ expect(result.code).toBe(ErrorCode.E_PARSE);
31
+ }
32
+ });
33
+ it('returns copy + embed URLs and frame for a valid source', () => {
34
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
35
+ const result = exportChart({ source: VALID });
36
+ expect(result.ok).toBe(true);
37
+ if (result.ok) {
38
+ expect(result.data.copyUrl).toMatch(/^https:\/\/blueprintchart\.com\/#\/copy\?bpc64=/);
39
+ expect(result.data.embedUrl).toMatch(/^https:\/\/blueprintchart\.com\/#\/render\?bpc64=/);
40
+ expect(result.data.frame.title).toBe('Hi');
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const GetExampleInputSchema: z.ZodObject<{
4
+ chartType: z.ZodOptional<z.ZodString>;
5
+ name: z.ZodOptional<z.ZodString>;
6
+ }, "strict", z.ZodTypeAny, {
7
+ name?: string | undefined;
8
+ chartType?: string | undefined;
9
+ }, {
10
+ name?: string | undefined;
11
+ chartType?: string | undefined;
12
+ }>;
13
+ export type GetExampleInput = z.infer<typeof GetExampleInputSchema>;
14
+ export interface GetExampleOutput {
15
+ id: string;
16
+ title: string;
17
+ chartType: string;
18
+ dsl: string;
19
+ }
20
+ export declare function getExample(input: unknown): ToolResult<GetExampleOutput>;
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { canonicalChartType } from '../dsl/chartTypes';
4
+ import { ErrorCode, toolError, toolOk } from '../errors';
5
+ export const GetExampleInputSchema = z.object({
6
+ chartType: z.string().optional(),
7
+ name: z.string().optional(),
8
+ }).strict();
9
+ const STARTER_SAMPLE_ID = 'letter-frequency';
10
+ export function getExample(input) {
11
+ const parsed = GetExampleInputSchema.safeParse(input);
12
+ if (!parsed.success) {
13
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
14
+ }
15
+ const { chartType, name } = parsed.data;
16
+ if (name) {
17
+ const found = samples.find(s => s.id === name);
18
+ if (!found) {
19
+ return toolError(ErrorCode.E_INPUT, [{
20
+ code: 'E_UNKNOWN_SAMPLE',
21
+ path: 'name',
22
+ message: `No sample with id "${name}". Try one of: ${samples.map(s => s.id).join(', ')}.`,
23
+ context: { knownIds: samples.map(s => s.id) },
24
+ }]);
25
+ }
26
+ return toolOk({ id: found.id, title: found.title, chartType: found.chartType, dsl: found.dsl });
27
+ }
28
+ if (chartType) {
29
+ const canonical = canonicalChartType(chartType);
30
+ if (!canonical) {
31
+ return toolError(ErrorCode.E_INPUT, [{
32
+ code: 'E_UNKNOWN_CHART_TYPE',
33
+ path: 'chartType',
34
+ message: `Unknown chart type "${chartType}".`,
35
+ context: { got: chartType },
36
+ }]);
37
+ }
38
+ const found = samples.find(s => s.chartType === canonical);
39
+ if (!found) {
40
+ return toolError(ErrorCode.E_INPUT, [{
41
+ code: 'E_NO_SAMPLE_FOR_TYPE',
42
+ path: 'chartType',
43
+ message: `No sample is shipped for chart type "${canonical}".`,
44
+ }]);
45
+ }
46
+ return toolOk({ id: found.id, title: found.title, chartType: found.chartType, dsl: found.dsl });
47
+ }
48
+ const starter = samples.find(s => s.id === STARTER_SAMPLE_ID) ?? samples[0];
49
+ if (!starter) {
50
+ return toolError(ErrorCode.E_INTERNAL, [{
51
+ message: 'No samples available — lib export is empty.',
52
+ }]);
53
+ }
54
+ return toolOk({ id: starter.id, title: starter.title, chartType: starter.chartType, dsl: starter.dsl });
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getExample } from './getExample';
3
+ describe('get_example', () => {
4
+ it('returns the starter sample when no args', () => {
5
+ const r = getExample({});
6
+ expect(r.ok).toBe(true);
7
+ if (r.ok) {
8
+ expect(r.data.id).toBe('letter-frequency');
9
+ expect(r.data.dsl).toContain('chart bar-vertical');
10
+ }
11
+ });
12
+ it('returns a sample by id', () => {
13
+ const r = getExample({ name: 'letter-frequency' });
14
+ expect(r.ok).toBe(true);
15
+ if (r.ok) {
16
+ expect(r.data.chartType).toBe('bar-vertical');
17
+ }
18
+ });
19
+ it('returns the first matching sample for a chartType', () => {
20
+ const r = getExample({ chartType: 'bar-horizontal' });
21
+ expect(r.ok).toBe(true);
22
+ if (r.ok) {
23
+ expect(r.data.chartType).toBe('bar-horizontal');
24
+ }
25
+ });
26
+ it('errors with E_INPUT for an unknown id', () => {
27
+ const r = getExample({ name: 'made-up-sample' });
28
+ expect(r.ok).toBe(false);
29
+ if (!r.ok) {
30
+ expect(r.code).toBe('E_INPUT');
31
+ }
32
+ });
33
+ it('errors with E_INPUT for an unknown chart type', () => {
34
+ const r = getExample({ chartType: 'bar' });
35
+ expect(r.ok).toBe(false);
36
+ if (!r.ok) {
37
+ expect(r.code).toBe('E_INPUT');
38
+ }
39
+ });
40
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ declare const SectionSchema: z.ZodDefault<z.ZodEnum<["all", "chart", "properties", "scenes", "annotations"]>>;
4
+ export declare const GetGrammarInputSchema: z.ZodObject<{
5
+ section: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "chart", "properties", "scenes", "annotations"]>>>;
6
+ }, "strict", z.ZodTypeAny, {
7
+ section?: "chart" | "properties" | "all" | "scenes" | "annotations" | undefined;
8
+ }, {
9
+ section?: "chart" | "properties" | "all" | "scenes" | "annotations" | undefined;
10
+ }>;
11
+ export type GetGrammarInput = z.infer<typeof GetGrammarInputSchema>;
12
+ export interface GetGrammarOutput {
13
+ section: z.infer<typeof SectionSchema>;
14
+ text: string;
15
+ }
16
+ export declare function getGrammar(input: unknown): ToolResult<GetGrammarOutput>;
17
+ export {};
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import { getDoc, listDocs } from '@blueprint-chart/docs';
3
+ import { ErrorCode, toolError, toolOk } from '../errors';
4
+ const SectionSchema = z.enum(['all', 'chart', 'properties', 'scenes', 'annotations']).default('all');
5
+ export const GetGrammarInputSchema = z.object({
6
+ section: SectionSchema.optional(),
7
+ }).strict();
8
+ const SECTION_TO_SLUG = {
9
+ chart: 'properties', // reference/dsl has no standalone chart-block doc; properties.md covers the block + properties
10
+ properties: 'properties',
11
+ scenes: 'scenes-and-transforms',
12
+ annotations: 'annotations',
13
+ };
14
+ export function getGrammar(input) {
15
+ const parsed = GetGrammarInputSchema.safeParse(input);
16
+ if (!parsed.success) {
17
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
18
+ }
19
+ const section = parsed.data.section ?? 'all';
20
+ if (section === 'all') {
21
+ const pages = listDocs('reference/dsl');
22
+ const text = pages.map((entry) => {
23
+ const { content } = getDoc('reference/dsl', entry.slug);
24
+ return `# ${entry.title}\n\n${content}`;
25
+ }).join('\n\n---\n\n');
26
+ return toolOk({ section: 'all', text });
27
+ }
28
+ const slug = SECTION_TO_SLUG[section];
29
+ try {
30
+ const { content } = getDoc('reference/dsl', slug);
31
+ return toolOk({ section, text: content });
32
+ }
33
+ catch (err) {
34
+ return toolError(ErrorCode.E_INPUT, [{
35
+ message: `Grammar section "${section}" not found: ${err instanceof Error ? err.message : String(err)}`,
36
+ }]);
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getGrammar } from './getGrammar';
3
+ describe('get_grammar', () => {
4
+ it('returns the full grammar by default', () => {
5
+ const r = getGrammar({});
6
+ expect(r.ok).toBe(true);
7
+ if (r.ok) {
8
+ expect(r.data.section).toBe('all');
9
+ expect(r.data.text.length).toBeGreaterThan(100);
10
+ }
11
+ });
12
+ it('returns a single section when requested', () => {
13
+ const r = getGrammar({ section: 'properties' });
14
+ expect(r.ok).toBe(true);
15
+ if (r.ok) {
16
+ expect(r.data.section).toBe('properties');
17
+ expect(r.data.text.toLowerCase()).toContain('properties');
18
+ }
19
+ });
20
+ it('errors with E_INPUT for an unknown section', () => {
21
+ const r = getGrammar({ section: 'totally-made-up' });
22
+ expect(r.ok).toBe(false);
23
+ });
24
+ });
25
+ describe('get_grammar section resolution', () => {
26
+ it('returns non-empty text for every enum section', () => {
27
+ for (const section of ['all', 'chart', 'properties', 'scenes', 'annotations']) {
28
+ const r = getGrammar({ section });
29
+ expect(r.ok, `section ${section} should resolve`).toBe(true);
30
+ if (r.ok) {
31
+ expect(r.data.text.length).toBeGreaterThan(0);
32
+ }
33
+ }
34
+ });
35
+ });
@@ -13,14 +13,21 @@ export interface SceneSummary {
13
13
  name?: string;
14
14
  hasTransition: boolean;
15
15
  }
16
+ export interface DataSummary {
17
+ rowCount: number;
18
+ entryCount: number;
19
+ labels: string[];
20
+ seriesNames: string[];
21
+ multiSeries: boolean;
22
+ }
16
23
  export interface InspectOutput {
17
24
  chartType: string;
18
25
  scenes: SceneSummary[];
26
+ data: DataSummary;
19
27
  hasAnnotations: boolean;
20
28
  hasColorizes: boolean;
21
29
  hasHighlights: boolean;
22
30
  hasAreaFills: boolean;
23
31
  seriesCount: number;
24
- rowCount: number;
25
32
  }
26
33
  export declare function inspectDsl(input: InspectInput): ToolResult<InspectOutput>;
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { astToDefinition } from '@blueprint-chart/lib';
3
3
  import { parseDsl } from '../parse';
4
4
  import { toolOk } from '../errors';
5
+ import { looksLikeQuotedLabel } from '../dsl/dataKey';
5
6
  export const InspectInputSchema = z.object({
6
7
  source: z.string(),
7
8
  });
@@ -13,25 +14,57 @@ function summarizeScenes(ast) {
13
14
  return scenes.map((scene, i) => ({
14
15
  index: i,
15
16
  name: scene.name ?? undefined,
16
- // SceneNode has no explicit `transition` field; transforms power animated
17
- // transitions in the lib, so non-empty transforms imply a transition.
18
17
  hasTransition: (scene.transforms?.length ?? 0) > 0,
19
18
  }));
20
19
  }
20
+ function summarizeData(ast) {
21
+ const entries = ast.data?.entries ?? [];
22
+ const seriesEntry = entries.find(e => e.key === '_series');
23
+ const seriesValues = seriesEntry
24
+ ? (seriesEntry.values ?? [seriesEntry.value])
25
+ : [];
26
+ const seriesNames = seriesValues.map(v => String(v).trim().replace(/^"|"$/g, ''));
27
+ const rowEntries = entries.filter(looksLikeQuotedLabel);
28
+ return {
29
+ rowCount: rowEntries.length,
30
+ entryCount: entries.length,
31
+ labels: rowEntries.map(e => e.key.replace(/^"|"$/g, '')),
32
+ seriesNames,
33
+ multiSeries: seriesNames.length > 0,
34
+ };
35
+ }
36
+ function countHighlights(ast) {
37
+ // `highlight "X" { … }` parses as ColorizeNode with fromHighlight:true,
38
+ // `highlight "X"` (no braces) parses as HighlightNode. Count both, at the
39
+ // chart level AND inside every scene (scenes carry their own highlights).
40
+ // unknown[]: only .length is read, so element type is irrelevant here
41
+ const countIn = (node) => {
42
+ const fromHighlightColorizes = (node.colorizes ?? []).filter(c => c.fromHighlight === true).length;
43
+ return (node.highlights?.length ?? 0) + fromHighlightColorizes;
44
+ };
45
+ const sceneTotal = (ast.scenes ?? []).reduce((sum, s) => sum + countIn(s), 0);
46
+ return countIn(ast) + sceneTotal;
47
+ }
48
+ function countNonHighlightColorizes(ast) {
49
+ const countIn = (cs) => (cs ?? []).filter(c => c.fromHighlight !== true).length;
50
+ const sceneTotal = (ast.scenes ?? []).reduce((sum, s) => sum + countIn(s.colorizes), 0);
51
+ return countIn(ast.colorizes) + sceneTotal;
52
+ }
21
53
  export function inspectDsl(input) {
22
54
  const parsed = parseDsl(input.source);
23
55
  if (!parsed.ok) {
24
56
  return parsed;
25
57
  }
26
- const def = astToDefinition(parsed.data.ast);
58
+ const ast = parsed.data.ast;
59
+ const def = astToDefinition(ast);
27
60
  return toolOk({
28
61
  chartType: def.chartType,
29
- scenes: summarizeScenes(parsed.data.ast),
62
+ scenes: summarizeScenes(ast),
63
+ data: summarizeData(ast),
30
64
  hasAnnotations: (def.annotations?.length ?? 0) > 0,
31
- hasColorizes: (def.colorizes?.length ?? 0) > 0,
32
- hasHighlights: (def.highlights?.length ?? 0) > 0,
65
+ hasColorizes: countNonHighlightColorizes(ast) > 0,
66
+ hasHighlights: countHighlights(ast) > 0,
33
67
  hasAreaFills: (def.areaFills?.length ?? 0) > 0,
34
68
  seriesCount: def.data.series?.length ?? 0,
35
- rowCount: def.data.labels?.length ?? 0,
36
69
  });
37
70
  }