@svgrid/mcp 2.6.4 → 2.6.6

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.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SvGrid MCP server (stdio).
4
+ *
5
+ * Exposes the SvGrid example sources, docs, and curated API reference as
6
+ * Model Context Protocol tools. Point an MCP-capable client (Claude
7
+ * Desktop, Claude Code, etc.) at this server to give the model accurate,
8
+ * version-pinned answers about SvGrid - no hallucinated APIs, no stale
9
+ * blog-post output.
10
+ *
11
+ * Run with:
12
+ * npx @svgrid/mcp
13
+ */
14
+ export {};
package/dist/index.js CHANGED
@@ -15,8 +15,11 @@ import { createRequire } from 'node:module';
15
15
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
16
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
17
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
18
- import { apiReference, docs, examples } from './data.js';
18
+ import { apiReference, apiSurface, docs, examples } from './data.js';
19
19
  import { projectTools, handleProjectTool } from './project-tools.js';
20
+ import { checkSvGridCode } from './validate.js';
21
+ import { compileWithSvelte } from './compile-svelte.js';
22
+ import { rankDocs } from './search.js';
20
23
  import { checkLicenseKey, introspectDrizzle, introspectJson, scaffold, summarizeVerify, verifyScaffold, } from '@svgrid/enterprise/studio';
21
24
  /**
22
25
  * Soft commercial gate. Uses the SAME classifier as the browser
@@ -43,6 +46,28 @@ const DOCS_FOOTER = '\n\nSvGrid reference: full docs & 370+ live demos at https:
43
46
  function withDocs(text) {
44
47
  return { content: [{ type: 'text', text: text + DOCS_FOOTER }] };
45
48
  }
49
+ /**
50
+ * Shorten a blurb for a listing. Demo blurbs run to ~240 chars and 373 of them
51
+ * is most of a context window, so the listings carry a one-line version and
52
+ * get_example_source still returns the full text.
53
+ */
54
+ function trimBlurb(text, max = 120) {
55
+ const s = String(text ?? '').replace(/\s+/g, ' ').trim();
56
+ if (s.length <= max)
57
+ return s;
58
+ const cut = s.slice(0, max);
59
+ const space = cut.lastIndexOf(' ');
60
+ return (space > 40 ? cut.slice(0, space) : cut) + '...';
61
+ }
62
+ /** { value: count } over a key, ordered by descending count. */
63
+ function countBy(rows, key) {
64
+ const counts = new Map();
65
+ for (const row of rows) {
66
+ const k = key(row) || 'Other';
67
+ counts.set(k, (counts.get(k) ?? 0) + 1);
68
+ }
69
+ return Object.fromEntries([...counts].sort((a, b) => b[1] - a[1]));
70
+ }
46
71
  // Report the real package version to MCP clients (read from package.json, which
47
72
  // ships in the tarball at ../package.json relative to the built dist/index.js),
48
73
  // so serverInfo.version never drifts from the published version.
@@ -67,8 +92,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
67
92
  tools: [
68
93
  {
69
94
  name: 'list_examples',
70
- description: 'List every SvGrid example demo with id, title, and short blurb. Use to discover what is available before fetching source.',
71
- inputSchema: { type: 'object', properties: {} },
95
+ description: 'Find SvGrid example demos. Returns id, title, category and a one-line blurb (not source). Call with no arguments for a category index plus the first page; filter with `query` and/or `category` to find a specific demo, then call get_example_source with its id.',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ query: {
100
+ type: 'string',
101
+ description: 'Free-text filter over id, title, blurb and category, e.g. "kanban" or "server side".',
102
+ },
103
+ category: {
104
+ type: 'string',
105
+ description: 'Exact category, e.g. "Kanban" or "Inputs". Call with no arguments to see the available categories.',
106
+ },
107
+ limit: { type: 'number', description: 'Max results, default 25, max 100.', default: 25 },
108
+ },
109
+ },
72
110
  },
73
111
  {
74
112
  name: 'get_example_source',
@@ -81,8 +119,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
81
119
  },
82
120
  {
83
121
  name: 'list_docs',
84
- description: 'List every documentation page with slug and title. Slugs use forward slashes, e.g. "help/columns/column-definitions".',
85
- inputSchema: { type: 'object', properties: {} },
122
+ description: 'Find SvGrid documentation pages. Returns slug, title and section. Call with no arguments for a section index plus the first page; filter with `query` and/or `section`, then call get_doc with a slug. Slugs use forward slashes, e.g. "help/columns/column-definitions". To search page CONTENT rather than titles, use search_docs.',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ query: {
127
+ type: 'string',
128
+ description: 'Free-text filter over slug, title and section, e.g. "column" or "export".',
129
+ },
130
+ section: {
131
+ type: 'string',
132
+ description: 'Exact section, e.g. "Columns" or "Server data". Call with no arguments to see the available sections.',
133
+ },
134
+ limit: { type: 'number', description: 'Max results, default 30, max 100.', default: 30 },
135
+ },
136
+ },
86
137
  },
87
138
  {
88
139
  name: 'get_doc',
@@ -95,7 +146,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
95
146
  },
96
147
  {
97
148
  name: 'search_docs',
98
- description: 'Case-insensitive substring search across all SvGrid docs. Returns matching slugs with a one-line excerpt around the first hit.',
149
+ description: 'Ranked full-text search across all SvGrid docs. Matches the query term by term (so "row virtualization" finds a page phrasing it either way) and returns the best pages first, each with a relevance score and an excerpt around the hit. Use this to find grounding before writing SvGrid code.',
99
150
  inputSchema: {
100
151
  type: 'object',
101
152
  properties: {
@@ -110,6 +161,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
110
161
  description: 'Return the curated SvGrid public-API surface, grouped by category (components, headless, scheduler, data ops, export, row models, features, virtualization, accessibility, utilities).',
111
162
  inputSchema: { type: 'object', properties: {} },
112
163
  },
164
+ {
165
+ name: 'check_svgrid_code',
166
+ description: 'Verify SvGrid code BEFORE handing it to the user. Checks the source against the real exported surface of the installed version - <SvGrid> prop names, ColumnDef keys, grid API methods, importable symbols and theme files - plus Svelte 5 runes rules, and compiles it with the Svelte compiler when one is reachable. Returns line-numbered diagnostics with the exact replacement for each. Run this on every .svelte or .ts file you write that uses SvGrid, then fix what it reports and run it again.',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ source: { type: 'string', description: 'The full file contents to check.' },
171
+ filename: {
172
+ type: 'string',
173
+ description: 'File name, used to pick the rules that apply. Defaults to "Component.svelte". Use the real name when you have one (e.g. "src/routes/+page.svelte", "state.svelte.ts").',
174
+ },
175
+ },
176
+ required: ['source'],
177
+ },
178
+ },
113
179
  {
114
180
  name: 'introspect_source',
115
181
  description: 'SvGrid Studio (commercial): infer an EntitySchema from a data source. Pass a Drizzle schema file (kind:"drizzle", source: the file text) or sample rows (kind:"json", rows, name). Returns a DRAFT EntitySchema to review/refine before scaffolding code.',
@@ -160,9 +226,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
160
226
  if (projectResult)
161
227
  return projectResult;
162
228
  switch (name) {
229
+ // Returning all 373 demos cost ~31k tokens on the call this tool's own
230
+ // description invites a model to start with. Filtered and capped instead,
231
+ // and a bare call answers with the category index to drill into.
163
232
  case 'list_examples': {
164
- const items = examples.map((e) => ({ id: e.id, title: e.title, blurb: e.blurb, path: e.path }));
165
- return withDocs(JSON.stringify(items, null, 2));
233
+ const a = (args ?? {});
234
+ const limit = Math.max(1, Math.min(100, Number(a.limit ?? 25)));
235
+ const q = String(a.query ?? '').trim().toLowerCase();
236
+ const category = String(a.category ?? '').trim().toLowerCase();
237
+ const pool = examples.filter((e) => {
238
+ if (category && e.category.toLowerCase() !== category)
239
+ return false;
240
+ if (q && !`${e.id} ${e.title} ${e.blurb} ${e.category}`.toLowerCase().includes(q))
241
+ return false;
242
+ return true;
243
+ });
244
+ const shown = pool.slice(0, limit);
245
+ const body = {
246
+ total: pool.length,
247
+ shown: shown.length,
248
+ examples: shown.map((e) => ({
249
+ id: e.id,
250
+ title: e.title,
251
+ category: e.category,
252
+ blurb: trimBlurb(e.blurb),
253
+ })),
254
+ };
255
+ if (!q && !category)
256
+ body.categories = countBy(examples, (e) => e.category);
257
+ if (shown.length < pool.length) {
258
+ body.hint = `Showing ${shown.length} of ${pool.length}. Narrow with \`query\` or \`category\`, or raise \`limit\` (max 100).`;
259
+ }
260
+ if (!pool.length) {
261
+ body.hint = 'No match. Drop `category`, or try a broader `query`.';
262
+ }
263
+ return withDocs(JSON.stringify(body, null, 2));
166
264
  }
167
265
  case 'get_example_source': {
168
266
  const id = String(args?.id ?? '');
@@ -179,9 +277,36 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
179
277
  ],
180
278
  };
181
279
  }
280
+ // Same shape as list_examples, for the same reason: all 370 pages was
281
+ // ~12.7k tokens. `path` is dropped from the listing because it is always
282
+ // "docs/<slug>.md" and get_doc takes the slug.
182
283
  case 'list_docs': {
183
- const items = docs.map((d) => ({ slug: d.slug, title: d.title, path: d.path }));
184
- return withDocs(JSON.stringify(items, null, 2));
284
+ const a = (args ?? {});
285
+ const limit = Math.max(1, Math.min(100, Number(a.limit ?? 30)));
286
+ const q = String(a.query ?? '').trim().toLowerCase();
287
+ const section = String(a.section ?? '').trim().toLowerCase();
288
+ const pool = docs.filter((d) => {
289
+ if (section && d.section.toLowerCase() !== section)
290
+ return false;
291
+ if (q && !`${d.slug} ${d.title} ${d.section}`.toLowerCase().includes(q))
292
+ return false;
293
+ return true;
294
+ });
295
+ const shown = pool.slice(0, limit);
296
+ const body = {
297
+ total: pool.length,
298
+ shown: shown.length,
299
+ docs: shown.map((d) => ({ slug: d.slug, title: d.title, section: d.section })),
300
+ };
301
+ if (!q && !section)
302
+ body.sections = countBy(docs, (d) => d.section);
303
+ if (shown.length < pool.length) {
304
+ body.hint = `Showing ${shown.length} of ${pool.length}. Narrow with \`query\` or \`section\`, raise \`limit\` (max 100), or use search_docs to search page content.`;
305
+ }
306
+ if (!pool.length) {
307
+ body.hint = 'No match. Drop `section`, or try search_docs to search page content instead of titles.';
308
+ }
309
+ return withDocs(JSON.stringify(body, null, 2));
185
310
  }
186
311
  case 'get_doc': {
187
312
  const slug = String(args?.slug ?? '');
@@ -201,25 +326,27 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
201
326
  if (!query) {
202
327
  return { isError: true, content: [{ type: 'text', text: 'query is required' }] };
203
328
  }
204
- const q = query.toLowerCase();
205
- const hits = [];
206
- for (const d of docs) {
207
- const lower = d.markdown.toLowerCase();
208
- const idx = lower.indexOf(q);
209
- if (idx >= 0) {
210
- const start = Math.max(0, idx - 60);
211
- const end = Math.min(d.markdown.length, idx + q.length + 120);
212
- const excerpt = d.markdown.slice(start, end).replace(/\s+/g, ' ').trim();
213
- hits.push({ slug: d.slug, title: d.title, excerpt });
214
- if (hits.length >= limit)
215
- break;
216
- }
217
- }
218
- return withDocs(JSON.stringify({ query, total: hits.length, hits }, null, 2));
329
+ // Ranking lives in ./search.ts so the remote server answers the same
330
+ // query the same way.
331
+ const { hits, total, partial } = rankDocs(docs, query, limit);
332
+ return withDocs(JSON.stringify({ query, total, shown: hits.length, partial: partial || undefined, hits }, null, 2));
219
333
  }
220
334
  case 'get_api_reference': {
221
335
  return withDocs(JSON.stringify(apiReference, null, 2));
222
336
  }
337
+ case 'check_svgrid_code': {
338
+ const a = (args ?? {});
339
+ if (typeof a.source !== 'string' || !a.source.trim()) {
340
+ return errText('source (the file contents to check) is required');
341
+ }
342
+ const result = await checkSvGridCode(a.source, apiSurface, {
343
+ filename: a.filename,
344
+ compile: compileWithSvelte,
345
+ });
346
+ // No docs footer: this output is a work list, and a marketing line at the
347
+ // end of it is noise the model has to read past on every iteration.
348
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
349
+ }
223
350
  case 'introspect_source': {
224
351
  const a = (args ?? {});
225
352
  try {
@@ -0,0 +1,16 @@
1
+ type ToolResult = {
2
+ content: Array<{
3
+ type: 'text';
4
+ text: string;
5
+ }>;
6
+ isError?: boolean;
7
+ };
8
+ export type ProjectTool = {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: Record<string, unknown>;
12
+ };
13
+ export declare const projectTools: ProjectTool[];
14
+ /** Handle a studio_* project tool. Returns undefined if `name` isn't one of ours. */
15
+ export declare function handleProjectTool(name: string, args: Record<string, unknown>): ToolResult | undefined;
16
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Doc ranking, shared by the stdio server and the remote Worker so both answer
3
+ * the same query the same way.
4
+ *
5
+ * Pure and dependency-free: it takes the docs it should rank rather than
6
+ * importing them, because the Worker loads its corpus from static assets at
7
+ * request time while the stdio server has it bundled.
8
+ */
9
+ export type RankableDoc = {
10
+ slug: string;
11
+ title: string;
12
+ section: string;
13
+ markdown: string;
14
+ };
15
+ export type DocHit = {
16
+ slug: string;
17
+ title: string;
18
+ section: string;
19
+ score: number;
20
+ excerpt: string;
21
+ };
22
+ export declare function occurrences(haystack: string, needle: string): number;
23
+ /** Split a query into distinct lowercase terms, dropping one-character noise. */
24
+ export declare function queryTokens(query: string): string[];
25
+ /** A window of text around the first needle that appears, for search results. */
26
+ export declare function excerptAround(markdown: string, needles: string[]): string;
27
+ /**
28
+ * Rank docs for a query. Whole-phrase hits outrank term hits, and the slug is
29
+ * weighted heavily: a page named after the topic is the reference page for it,
30
+ * where a recipe that merely mentions it is not. Pages matching every term win
31
+ * outright; partial matches are only returned when nothing covers the query.
32
+ */
33
+ export declare function rankDocs<T extends RankableDoc>(docs: readonly T[], query: string, limit: number): {
34
+ hits: DocHit[];
35
+ total: number;
36
+ partial: boolean;
37
+ };
package/dist/search.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Doc ranking, shared by the stdio server and the remote Worker so both answer
3
+ * the same query the same way.
4
+ *
5
+ * Pure and dependency-free: it takes the docs it should rank rather than
6
+ * importing them, because the Worker loads its corpus from static assets at
7
+ * request time while the stdio server has it bundled.
8
+ */
9
+ export function occurrences(haystack, needle) {
10
+ if (!needle)
11
+ return 0;
12
+ let n = 0;
13
+ let i = haystack.indexOf(needle);
14
+ while (i !== -1) {
15
+ n += 1;
16
+ i = haystack.indexOf(needle, i + needle.length);
17
+ }
18
+ return n;
19
+ }
20
+ /** Split a query into distinct lowercase terms, dropping one-character noise. */
21
+ export function queryTokens(query) {
22
+ const tokens = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1))];
23
+ return tokens.length ? tokens : [query.toLowerCase().trim()];
24
+ }
25
+ /** A window of text around the first needle that appears, for search results. */
26
+ export function excerptAround(markdown, needles) {
27
+ const lower = markdown.toLowerCase();
28
+ let idx = -1;
29
+ for (const n of needles) {
30
+ idx = lower.indexOf(n);
31
+ if (idx >= 0)
32
+ break;
33
+ }
34
+ if (idx < 0)
35
+ idx = 0;
36
+ const start = Math.max(0, idx - 60);
37
+ const end = Math.min(markdown.length, idx + 180);
38
+ return markdown.slice(start, end).replace(/\s+/g, ' ').trim();
39
+ }
40
+ /**
41
+ * Rank docs for a query. Whole-phrase hits outrank term hits, and the slug is
42
+ * weighted heavily: a page named after the topic is the reference page for it,
43
+ * where a recipe that merely mentions it is not. Pages matching every term win
44
+ * outright; partial matches are only returned when nothing covers the query.
45
+ */
46
+ export function rankDocs(docs, query, limit) {
47
+ const phrase = query.toLowerCase();
48
+ const tokens = queryTokens(query);
49
+ const scored = [];
50
+ for (const d of docs) {
51
+ const title = d.title.toLowerCase();
52
+ const markdown = d.markdown.toLowerCase();
53
+ const headings = (d.markdown.match(/^#{1,6}\s+.*$/gm) ?? []).join('\n').toLowerCase();
54
+ const slugWords = d.slug.toLowerCase().replace(/[/-]/g, ' ');
55
+ let score = 0;
56
+ if (title.includes(phrase))
57
+ score += 100;
58
+ if (slugWords.includes(phrase))
59
+ score += 60;
60
+ if (headings.includes(phrase))
61
+ score += 30;
62
+ if (markdown.includes(phrase))
63
+ score += 20;
64
+ let matched = 0;
65
+ for (const t of tokens) {
66
+ const inTitle = title.includes(t);
67
+ const inHeading = headings.includes(t);
68
+ const count = occurrences(markdown, t);
69
+ if (inTitle || inHeading || count > 0)
70
+ matched += 1;
71
+ if (inTitle)
72
+ score += 25;
73
+ if (slugWords.includes(t))
74
+ score += 10;
75
+ if (inHeading)
76
+ score += 8;
77
+ // Capped so a long page cannot outrank a precise one on bulk alone.
78
+ score += Math.min(count, 5);
79
+ }
80
+ if (score > 0)
81
+ scored.push({ d, score, complete: matched === tokens.length });
82
+ }
83
+ const complete = scored.filter((s) => s.complete);
84
+ const pool = complete.length ? complete : scored;
85
+ const ranked = pool
86
+ .sort((a, b) => b.score - a.score || a.d.slug.localeCompare(b.d.slug))
87
+ .slice(0, limit);
88
+ return {
89
+ hits: ranked.map((s) => ({
90
+ slug: s.d.slug,
91
+ title: s.d.title,
92
+ section: s.d.section,
93
+ score: s.score,
94
+ excerpt: excerptAround(s.d.markdown, [phrase, ...tokens]),
95
+ })),
96
+ total: pool.length,
97
+ partial: complete.length === 0 && scored.length > 0,
98
+ };
99
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * check_svgrid_code - the verification half of the MCP server.
3
+ *
4
+ * Retrieval tools (docs, demos, API listing) only ever give a model something
5
+ * to read. This one closes the loop: the model writes SvGrid code, this checks
6
+ * it against the REAL exported surface of the installed version and hands back
7
+ * diagnostics it can act on. A wrong prop name, a symbol imported from the
8
+ * wrong package, or Svelte 4 syntax in a Svelte 5 component all come back with
9
+ * the exact replacement rather than "hmm, that didn't work" three edits later.
10
+ *
11
+ * Two layers:
12
+ * 1. Static analysis (this file) - pure, no dependencies, no filesystem, so
13
+ * it runs identically in the Node stdio server and in a Worker.
14
+ * 2. The Svelte compiler - injected by the caller when it is available
15
+ * (`compile` option). Node has it; the Worker does not, and skips.
16
+ *
17
+ * The API surface is generated from the workspace sources at build time by
18
+ * scripts/api-surface.mjs, so it cannot drift from what the package exports.
19
+ */
20
+ export type Severity = 'error' | 'warning' | 'info';
21
+ export type Diagnostic = {
22
+ /** Stable rule id, e.g. "svgrid/unknown-prop". */
23
+ rule: string;
24
+ severity: Severity;
25
+ /** 1-based line in the checked source. */
26
+ line: number;
27
+ message: string;
28
+ /** The concrete edit that fixes it, when there is one. */
29
+ fix?: string;
30
+ /** Doc slug or demo id to read for the full story. */
31
+ see?: string;
32
+ };
33
+ export type TypeMember = {
34
+ readonly name: string;
35
+ readonly optional: boolean;
36
+ readonly type: string;
37
+ };
38
+ export type ApiSurface = {
39
+ readonly gridVersion: string;
40
+ readonly enterpriseVersion: string;
41
+ readonly grid: {
42
+ readonly values: readonly string[];
43
+ readonly types: readonly string[];
44
+ readonly subpaths: readonly string[];
45
+ };
46
+ readonly enterprise: {
47
+ readonly values: readonly string[];
48
+ readonly types: readonly string[];
49
+ readonly subpaths: readonly string[];
50
+ };
51
+ readonly props: readonly TypeMember[];
52
+ readonly columnDef: readonly TypeMember[];
53
+ /** Methods on the free `SvGridApi`. */
54
+ readonly apiMethods: readonly string[];
55
+ /** Methods `installEnterprise(api)` adds on top. */
56
+ readonly enterpriseApiMethods: readonly string[];
57
+ readonly themes: readonly string[];
58
+ readonly features: readonly string[];
59
+ readonly rowModels: readonly string[];
60
+ };
61
+ export type CheckResult = {
62
+ ok: boolean;
63
+ /** Which version the code was checked against. */
64
+ checkedAgainst: string;
65
+ compiler: 'svelte' | 'unavailable' | 'not-svelte';
66
+ counts: {
67
+ errors: number;
68
+ warnings: number;
69
+ info: number;
70
+ };
71
+ diagnostics: Diagnostic[];
72
+ summary: string;
73
+ };
74
+ /** A compile pass supplied by the host when a Svelte compiler is reachable. */
75
+ export type CompileFn = (source: string, filename: string) => Promise<{
76
+ available: boolean;
77
+ diagnostics: Diagnostic[];
78
+ }>;
79
+ /**
80
+ * Blank out comments and the inside of strings, keeping every offset and line
81
+ * break intact. All structural scanning runs on this copy so a prop name in a
82
+ * doc comment or a `<SvGrid>` inside a template string never trips a rule.
83
+ */
84
+ export declare function blankOut(src: string): string;
85
+ /**
86
+ * Closest known name to `word`, or null when nothing is near enough. A
87
+ * case-only difference always wins; otherwise the edit distance has to be
88
+ * small relative to the word so "foo" does not "resolve" to "bar".
89
+ */
90
+ export declare function nearest(word: string, candidates: readonly string[]): string | null;
91
+ /** Run every static rule. Exported for tests and for hosts that skip compiling. */
92
+ export declare function checkStatic(source: string, surface: ApiSurface, filename?: string): Diagnostic[];
93
+ /**
94
+ * Check a snippet and report what a model should do next. `compile` is the
95
+ * optional second gate: when the host can reach a Svelte compiler, real parse
96
+ * errors are merged in with the static findings.
97
+ */
98
+ export declare function checkSvGridCode(source: string, surface: ApiSurface, opts?: {
99
+ filename?: string;
100
+ compile?: CompileFn;
101
+ }): Promise<CheckResult>;