@castandcrew/spotlight-mcp 0.1.3 → 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 (53) hide show
  1. package/README.md +137 -16
  2. package/data/index.json +56002 -1197
  3. package/dist/data/health.d.ts +33 -0
  4. package/dist/data/health.js +111 -0
  5. package/dist/data/health.js.map +1 -0
  6. package/dist/data/loader.d.ts +19 -0
  7. package/dist/data/loader.js +61 -4
  8. package/dist/data/loader.js.map +1 -1
  9. package/dist/lib/regex-safety.d.ts +14 -0
  10. package/dist/lib/regex-safety.js +34 -0
  11. package/dist/lib/regex-safety.js.map +1 -0
  12. package/dist/server.js +25 -1
  13. package/dist/server.js.map +1 -1
  14. package/dist/tools/_fuzzy.d.ts +49 -0
  15. package/dist/tools/_fuzzy.js +103 -0
  16. package/dist/tools/_fuzzy.js.map +1 -0
  17. package/dist/tools/_guard.d.ts +31 -0
  18. package/dist/tools/_guard.js +62 -0
  19. package/dist/tools/_guard.js.map +1 -0
  20. package/dist/tools/_synonyms.d.ts +1 -0
  21. package/dist/tools/_synonyms.js +28 -0
  22. package/dist/tools/_synonyms.js.map +1 -0
  23. package/dist/tools/ask_spotlight.js +67 -5
  24. package/dist/tools/ask_spotlight.js.map +1 -1
  25. package/dist/tools/get_component.js +14 -5
  26. package/dist/tools/get_component.js.map +1 -1
  27. package/dist/tools/get_migration_guide.js +4 -2
  28. package/dist/tools/get_migration_guide.js.map +1 -1
  29. package/dist/tools/get_pattern.js +22 -7
  30. package/dist/tools/get_pattern.js.map +1 -1
  31. package/dist/tools/get_recipe.js +4 -2
  32. package/dist/tools/get_recipe.js.map +1 -1
  33. package/dist/tools/get_token.js +5 -3
  34. package/dist/tools/get_token.js.map +1 -1
  35. package/dist/tools/identify_pattern.js +7 -5
  36. package/dist/tools/identify_pattern.js.map +1 -1
  37. package/dist/tools/list_deprecated.js +4 -2
  38. package/dist/tools/list_deprecated.js.map +1 -1
  39. package/dist/tools/search_components.js +9 -3
  40. package/dist/tools/search_components.js.map +1 -1
  41. package/dist/tools/search_tokens.js +8 -3
  42. package/dist/tools/search_tokens.js.map +1 -1
  43. package/dist/tools/search_utility_classes.js +4 -2
  44. package/dist/tools/search_utility_classes.js.map +1 -1
  45. package/dist/tools/validate_usage.js +25 -5
  46. package/dist/tools/validate_usage.js.map +1 -1
  47. package/dist/transports/http.js +21 -1
  48. package/dist/transports/http.js.map +1 -1
  49. package/dist/types.d.ts +8 -2
  50. package/dist/version.d.ts +1 -0
  51. package/dist/version.js +8 -0
  52. package/dist/version.js.map +1 -0
  53. package/package.json +10 -7
@@ -0,0 +1,33 @@
1
+ import { type IndexCollection } from './loader.js';
2
+ /**
3
+ * Collections that must be non-empty for the index to be considered healthy.
4
+ *
5
+ * `deprecated` is deliberately excluded: it is 0 in a *healthy* index too, because
6
+ * it is derived from components.filter(status === 'deprecated') and nothing is
7
+ * deprecated today. Guarding on it would break a working tool. Tools that read
8
+ * `deprecated` guard on `components` instead — that is what distinguishes a
9
+ * truthful "nothing is deprecated" from an empty index pretending to be one.
10
+ */
11
+ export declare const REQUIRED_COLLECTIONS: readonly ["components", "patterns", "tokens", "rules", "migration", "utilityClasses"];
12
+ export type IndexCounts = Record<IndexCollection, number>;
13
+ export interface IndexHealth {
14
+ indexPath: string;
15
+ version: string;
16
+ generated: string;
17
+ counts: IndexCounts;
18
+ /** Required collections that are empty. */
19
+ empty: IndexCollection[];
20
+ status: 'ok' | 'degraded';
21
+ /** Set only when the index could not be loaded at all. */
22
+ fatal?: string;
23
+ }
24
+ export declare function getIndexHealth(): IndexHealth;
25
+ /**
26
+ * The remedy branches on where the index came from: `pnpm index` is not runnable
27
+ * from the published tarball (package.json files is ["dist","data"]), so the
28
+ * generic advice would be a dead end for exactly the users who hit an empty
29
+ * published index.
30
+ */
31
+ export declare function remedyFor(indexPath: string): string;
32
+ /** Multi-line stderr banner. Printed at startup in both transports. */
33
+ export declare function formatBanner(health: IndexHealth, packageVersion: string): string;
@@ -0,0 +1,111 @@
1
+ import { loadIndex, resolveIndexPath, IndexUnusableError, INDEX_COLLECTIONS, } from './loader.js';
2
+ /**
3
+ * Collections that must be non-empty for the index to be considered healthy.
4
+ *
5
+ * `deprecated` is deliberately excluded: it is 0 in a *healthy* index too, because
6
+ * it is derived from components.filter(status === 'deprecated') and nothing is
7
+ * deprecated today. Guarding on it would break a working tool. Tools that read
8
+ * `deprecated` guard on `components` instead — that is what distinguishes a
9
+ * truthful "nothing is deprecated" from an empty index pretending to be one.
10
+ */
11
+ export const REQUIRED_COLLECTIONS = [
12
+ 'components',
13
+ 'patterns',
14
+ 'tokens',
15
+ 'rules',
16
+ 'migration',
17
+ 'utilityClasses',
18
+ ];
19
+ export function getIndexHealth() {
20
+ const indexPath = resolveIndexPath();
21
+ try {
22
+ const index = loadIndex();
23
+ const counts = Object.fromEntries(INDEX_COLLECTIONS.map(c => [c, index[c].length]));
24
+ const empty = REQUIRED_COLLECTIONS.filter(c => counts[c] === 0);
25
+ return {
26
+ indexPath,
27
+ version: index.version,
28
+ generated: index.generated,
29
+ counts,
30
+ empty,
31
+ status: empty.length ? 'degraded' : 'ok',
32
+ };
33
+ }
34
+ catch (e) {
35
+ return {
36
+ indexPath,
37
+ version: 'unknown',
38
+ generated: 'unknown',
39
+ counts: Object.fromEntries(INDEX_COLLECTIONS.map(c => [c, 0])),
40
+ empty: [...REQUIRED_COLLECTIONS],
41
+ status: 'degraded',
42
+ fatal: e instanceof IndexUnusableError ? e.message : String(e),
43
+ };
44
+ }
45
+ }
46
+ /** True when the index came from an installed package rather than a working checkout. */
47
+ function isInstalledPackage(indexPath) {
48
+ return /[/\\]node_modules[/\\]|[/\\]_npx[/\\]/.test(indexPath);
49
+ }
50
+ /**
51
+ * The remedy branches on where the index came from: `pnpm index` is not runnable
52
+ * from the published tarball (package.json files is ["dist","data"]), so the
53
+ * generic advice would be a dead end for exactly the users who hit an empty
54
+ * published index.
55
+ */
56
+ export function remedyFor(indexPath) {
57
+ return isInstalledPackage(indexPath)
58
+ ? 'This index came from an installed copy of @castandcrew/spotlight-mcp, which shipped ' +
59
+ 'without derived data. Point the server at a locally generated index instead: clone ' +
60
+ 'cast-and-crew/spotlight-mcp, run "pnpm install && pnpm index /path/to/common-ui-component-library ' +
61
+ '&& pnpm build", then either set SPOTLIGHT_INDEX_PATH=<checkout>/data/index.json or run ' +
62
+ '"node <checkout>/dist/index.js" directly instead of npx.'
63
+ : 'Regenerate the index from the upstream component library: ' +
64
+ 'pnpm index /path/to/common-ui-component-library (then: pnpm build)';
65
+ }
66
+ const LABEL_WIDTH = 14;
67
+ /** Multi-line stderr banner. Printed at startup in both transports. */
68
+ export function formatBanner(health, packageVersion) {
69
+ const rule = '─'.repeat(70);
70
+ if (health.fatal) {
71
+ return [
72
+ rule,
73
+ ` Spotlight MCP ${packageVersion} — INDEX UNUSABLE`,
74
+ '',
75
+ ` ${health.fatal}`,
76
+ '',
77
+ ` ${remedyFor(health.indexPath)}`,
78
+ '',
79
+ ' All 12 tools will return an error until this is fixed.',
80
+ rule,
81
+ ].join('\n');
82
+ }
83
+ if (health.status === 'ok') {
84
+ const c = health.counts;
85
+ return (`Spotlight MCP ${packageVersion} — index ok ` +
86
+ `(${c.components} components · ${c.tokens} tokens · ${c.utilityClasses} classes · ` +
87
+ `${c.patterns} patterns · ${c.rules} rules), generated ${health.generated}\n` +
88
+ ` ${health.indexPath}`);
89
+ }
90
+ const lines = INDEX_COLLECTIONS.map(c => {
91
+ const n = health.counts[c];
92
+ const flag = health.empty.includes(c)
93
+ ? ' ← EMPTY'
94
+ : c === 'deprecated' && n === 0
95
+ ? ' (0 is normal — nothing is deprecated today)'
96
+ : '';
97
+ return ` ${c.padEnd(LABEL_WIDTH)}: ${String(n).padStart(4)}${flag}`;
98
+ });
99
+ return [
100
+ rule,
101
+ ` Spotlight MCP ${packageVersion} — INDEX DEGRADED`,
102
+ ` ${health.indexPath} (generated ${health.generated})`,
103
+ '',
104
+ ...lines,
105
+ '',
106
+ ` Tools that need the empty collections will return an error; the rest still work.`,
107
+ ` ${remedyFor(health.indexPath)}`,
108
+ rule,
109
+ ].join('\n');
110
+ }
111
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/data/health.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,22 @@
1
1
  import type { SpotlightIndex } from '../types.js';
2
+ /**
3
+ * Every collection a Spotlight index must declare as an array. A file missing any
4
+ * of these is not an index, however well-formed its JSON is.
5
+ */
6
+ export declare const INDEX_COLLECTIONS: readonly ["components", "patterns", "tokens", "rules", "migration", "deprecated", "utilityClasses"];
7
+ export type IndexCollection = (typeof INDEX_COLLECTIONS)[number];
8
+ /**
9
+ * The index could not be loaded at all — missing file, unreadable, not JSON, or
10
+ * not shaped like an index. Distinct from a *degraded* index, which parses fine
11
+ * but has empty collections; that case is handled per-tool by the guard.
12
+ */
13
+ export declare class IndexUnusableError extends Error {
14
+ readonly indexPath: string;
15
+ constructor(indexPath: string, message: string);
16
+ }
17
+ /** Single source of truth for where the index comes from, so errors can name it. */
18
+ export declare function resolveIndexPath(): string;
2
19
  export declare function loadIndex(): SpotlightIndex;
3
20
  export declare function reloadIndex(): SpotlightIndex;
21
+ /** Drop the memoized index without immediately reloading. Used by the guard tests. */
22
+ export declare function clearIndexCache(): void;
@@ -2,18 +2,75 @@ import { readFileSync } from 'fs';
2
2
  import { resolve, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ /**
6
+ * Every collection a Spotlight index must declare as an array. A file missing any
7
+ * of these is not an index, however well-formed its JSON is.
8
+ */
9
+ export const INDEX_COLLECTIONS = [
10
+ 'components',
11
+ 'patterns',
12
+ 'tokens',
13
+ 'rules',
14
+ 'migration',
15
+ 'deprecated',
16
+ 'utilityClasses',
17
+ ];
18
+ /**
19
+ * The index could not be loaded at all — missing file, unreadable, not JSON, or
20
+ * not shaped like an index. Distinct from a *degraded* index, which parses fine
21
+ * but has empty collections; that case is handled per-tool by the guard.
22
+ */
23
+ export class IndexUnusableError extends Error {
24
+ indexPath;
25
+ constructor(indexPath, message) {
26
+ super(message);
27
+ this.indexPath = indexPath;
28
+ this.name = 'IndexUnusableError';
29
+ }
30
+ }
5
31
  let cached = null;
32
+ /** Single source of truth for where the index comes from, so errors can name it. */
33
+ export function resolveIndexPath() {
34
+ return process.env.SPOTLIGHT_INDEX_PATH
35
+ ? resolve(process.env.SPOTLIGHT_INDEX_PATH)
36
+ : resolve(__dirname, '../../data/index.json');
37
+ }
6
38
  export function loadIndex() {
7
39
  if (cached)
8
40
  return cached;
9
- const indexPath = process.env.SPOTLIGHT_INDEX_PATH
10
- ? resolve(process.env.SPOTLIGHT_INDEX_PATH)
11
- : resolve(__dirname, '../../data/index.json');
12
- cached = JSON.parse(readFileSync(indexPath, 'utf-8'));
41
+ const indexPath = resolveIndexPath();
42
+ let raw;
43
+ try {
44
+ raw = readFileSync(indexPath, 'utf-8');
45
+ }
46
+ catch (e) {
47
+ throw new IndexUnusableError(indexPath, `Cannot read the Spotlight index at ${indexPath} - ${e.message}`);
48
+ }
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(raw);
52
+ }
53
+ catch (e) {
54
+ throw new IndexUnusableError(indexPath, `${indexPath} is not valid JSON - ${e.message}`);
55
+ }
56
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
57
+ throw new IndexUnusableError(indexPath, `${indexPath} does not contain a JSON object.`);
58
+ }
59
+ const missing = INDEX_COLLECTIONS.filter(k => !Array.isArray(parsed[k]));
60
+ if (missing.length) {
61
+ throw new IndexUnusableError(indexPath, `${indexPath} is not a Spotlight index - missing or non-array key(s): ${missing.join(', ')}.`);
62
+ }
63
+ // Deliberately only cached on success: a later fix plus reloadIndex() recovers
64
+ // without restarting the server.
65
+ cached = parsed;
13
66
  return cached;
14
67
  }
15
68
  export function reloadIndex() {
16
69
  cached = null;
17
70
  return loadIndex();
18
71
  }
72
+ /** Drop the memoized index without immediately reloading. Used by the guard tests. */
73
+ export function clearIndexCache() {
74
+ cached = null;
75
+ }
19
76
  //# sourceMappingURL=loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/data/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAGpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,IAAI,MAAM,GAA0B,IAAI,CAAC;AAEzC,MAAM,UAAU,SAAS;IACvB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB;QAChD,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAC3C,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;IAChD,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAmB,CAAC;IACxE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,IAAI,CAAC;IACd,OAAO,SAAS,EAAE,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/data/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAGpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,YAAY;IACZ,UAAU;IACV,QAAQ;IACR,OAAO;IACP,WAAW;IACX,YAAY;IACZ,gBAAgB;CACR,CAAC;AAIX;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEhC;IADX,YACW,SAAiB,EAC1B,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,cAAS,GAAT,SAAS,CAAQ;QAI1B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,IAAI,MAAM,GAA0B,IAAI,CAAC;AAEzC,oFAAoF;AACpF,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,oBAAoB;QACrC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAC3C,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,SAAS,GAAG,gBAAgB,EAAE,CAAC;IAErC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,kBAAkB,CAC1B,SAAS,EACT,sCAAsC,SAAS,MAAO,CAAW,CAAC,OAAO,EAAE,CAC5E,CAAC;IACJ,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,kBAAkB,CAC1B,SAAS,EACT,GAAG,SAAS,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAC3D,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,kBAAkB,CAAC,SAAS,EAAE,GAAG,SAAS,kCAAkC,CAAC,CAAC;IAC1F,CAAC;IAED,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,CACtC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAE,MAAkC,CAAC,CAAC,CAAC,CAAC,CAC5D,CAAC;IACF,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,kBAAkB,CAC1B,SAAS,EACT,GAAG,SAAS,4DAA4D,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC9F,CAAC;IACJ,CAAC;IAED,+EAA+E;IAC/E,iCAAiC;IACjC,MAAM,GAAG,MAAwB,CAAC;IAClC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,IAAI,CAAC;IACd,OAAO,SAAS,EAAE,CAAC;AACrB,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Heuristic catastrophic-backtracking detector. Flags the classic shapes —
3
+ * a quantified group containing another quantified group or alternation
4
+ * (`(a+)+`, `(a|a)*`, `(a+)*b`) — that cause exponential worst-case matching
5
+ * in a backtracking engine. Not a formal proof of safety or danger, just a
6
+ * cheap gate on patterns nobody should be hand-authoring anyway.
7
+ */
8
+ export declare function isCatastrophicPattern(source: string): boolean;
9
+ export interface PatternCheckResult {
10
+ ok: boolean;
11
+ reason?: string;
12
+ }
13
+ /** Compiles the pattern and runs the heuristic. Used for both build-time and test-time checks. */
14
+ export declare function checkPattern(source: string): PatternCheckResult;
@@ -0,0 +1,34 @@
1
+ // SPT-1835 follow-up: Snyk flagged `new RegExp(rule.pattern).test(code)` in
2
+ // validate_usage as a ReDoS sink (CWE-400). `rule.pattern` is maintainer-authored
3
+ // (content/rules/*.md, docs/SPOTLIGHT_RULES.md) but `code` — the string it is
4
+ // tested against — is caller-supplied and unbounded, so a catastrophic pattern
5
+ // would let any caller hang the process. This checks patterns at index-build
6
+ // time, where refusing to write is cheap, instead of at request time.
7
+ /**
8
+ * Heuristic catastrophic-backtracking detector. Flags the classic shapes —
9
+ * a quantified group containing another quantified group or alternation
10
+ * (`(a+)+`, `(a|a)*`, `(a+)*b`) — that cause exponential worst-case matching
11
+ * in a backtracking engine. Not a formal proof of safety or danger, just a
12
+ * cheap gate on patterns nobody should be hand-authoring anyway.
13
+ */
14
+ export function isCatastrophicPattern(source) {
15
+ // A group, quantified, that itself contains a quantified sub-group or an
16
+ // alternation — the two shapes that create ambiguous repetition.
17
+ const nestedQuantifier = /\([^()]*[+*][^()]*\)[+*]/.test(source) || // (x+)+ / (x*)+ / (x+)* / (x*)*
18
+ /\([^()]*\|[^()]*\)[+*]\s*[^$]/.test(source); // (a|a)* followed by more pattern
19
+ return nestedQuantifier;
20
+ }
21
+ /** Compiles the pattern and runs the heuristic. Used for both build-time and test-time checks. */
22
+ export function checkPattern(source) {
23
+ try {
24
+ new RegExp(source);
25
+ }
26
+ catch (e) {
27
+ return { ok: false, reason: `does not compile: ${e}` };
28
+ }
29
+ if (isCatastrophicPattern(source)) {
30
+ return { ok: false, reason: 'looks catastrophically backtracking (nested quantifier or repeated alternation)' };
31
+ }
32
+ return { ok: true };
33
+ }
34
+ //# sourceMappingURL=regex-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex-safety.js","sourceRoot":"","sources":["../../src/lib/regex-safety.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,kFAAkF;AAClF,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,sEAAsE;AAEtE;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,yEAAyE;IACzE,iEAAiE;IACjE,MAAM,gBAAgB,GACpB,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,gCAAgC;QAC3E,+BAA+B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,mCAAmC;IAEnF,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAOD,kGAAkG;AAClG,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,EAAE,CAAC;IACzD,CAAC;IACD,IAAI,qBAAqB,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iFAAiF,EAAE,CAAC;IAClH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
package/dist/server.js CHANGED
@@ -11,10 +11,34 @@ import { registerSearchUtilityClasses } from './tools/search_utility_classes.js'
11
11
  import { registerAskSpotlight } from './tools/ask_spotlight.js';
12
12
  import { registerIdentifyPattern } from './tools/identify_pattern.js';
13
13
  import { registerGetRecipe } from './tools/get_recipe.js';
14
+ import { getIndexHealth, formatBanner } from './data/health.js';
15
+ import { PACKAGE_VERSION } from './version.js';
16
+ let bannerPrinted = false;
17
+ /**
18
+ * Report index health to stderr once per process.
19
+ *
20
+ * stderr only — stdout carries JSON-RPC in stdio mode. Never throws and never
21
+ * exits: if the server dies the client reports only "failed to connect" and the
22
+ * model never sees the reason, which is a silent failure with extra steps. An
23
+ * unusable index surfaces in-band instead, as an error from every tool that
24
+ * needs it.
25
+ */
26
+ function reportIndexHealth() {
27
+ if (bannerPrinted)
28
+ return;
29
+ bannerPrinted = true;
30
+ try {
31
+ console.error(formatBanner(getIndexHealth(), PACKAGE_VERSION));
32
+ }
33
+ catch {
34
+ // Diagnostics must never take the server down.
35
+ }
36
+ }
14
37
  export function createServer() {
38
+ reportIndexHealth();
15
39
  const server = new McpServer({
16
40
  name: 'spotlight-mcp',
17
- version: '0.1.0',
41
+ version: PACKAGE_VERSION,
18
42
  });
19
43
  registerGetComponent(server);
20
44
  registerSearchComponents(server);
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzB,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAChC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE1B,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,IAAI,aAAa,GAAG,KAAK,CAAC;AAE1B;;;;;;;;GAQG;AACH,SAAS,iBAAiB;IACxB,IAAI,aAAa;QAAE,OAAO;IAC1B,aAAa,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,iBAAiB,EAAE,CAAC;IAEpB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,eAAe;KACzB,CAAC,CAAC;IAEH,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzB,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAChC,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE1B,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type Fuse from 'fuse.js';
2
+ import type { FuseResult } from 'fuse.js';
3
+ /**
4
+ * Search a pre-configured Fuse instance the normal (whole-query) way first;
5
+ * if that yields nothing, fall back to searching term-by-term (OR semantics)
6
+ * and merge the results.
7
+ *
8
+ * Fuse.js scores a whole query string as a single fuzzy pattern. With
9
+ * multi-word queries (e.g. "alert warning inline notification banner") the
10
+ * full string sometimes fails to match any indexed field closely enough to
11
+ * clear the configured threshold, even though a single term ("alert") would
12
+ * match cleanly — so callers got `[]` for exactly the kind of query the model
13
+ * is most likely to send (SPT-1828).
14
+ *
15
+ * The whole-query search is tried first (and returned as-is when it finds
16
+ * anything) because it is more precise. Per-term OR is only used as a
17
+ * fallback for the zero-result case the bug describes.
18
+ *
19
+ * Use this for tools whose Fuse keys are short fields (names, descriptions,
20
+ * categories) — `search_components`, `search_tokens`. For `identify_pattern`,
21
+ * whose pool also weighs a large `content` field very low, use
22
+ * `searchOrPerTermStrict` instead (see its docstring for why).
23
+ */
24
+ export declare function searchOrPerTerm<T>(fuse: Fuse<T>, query: string, limit?: number): FuseResult<T>[];
25
+ /**
26
+ * Stricter per-term fallback for `identify_pattern`'s natural-language
27
+ * `useCase` queries.
28
+ *
29
+ * `identify_pattern`'s pool weighs a pattern's full `content` field very low
30
+ * (0.1) specifically so a big text blob only matters when several query
31
+ * terms line up together — plain per-term OR search defeats that: almost
32
+ * any short single word fuzzy-matches *something* inside a 10k+ character
33
+ * blob purely by chance (Bitap fuzzy matching on long strings), which both
34
+ * resurfaces noise for genuine queries and breaks the "no match" case for
35
+ * unrelated/nonsense queries (SPT-1828 regression check).
36
+ *
37
+ * To keep the fallback honest, this variant:
38
+ * 1. Drops filler words (short + stopwords) before splitting into terms.
39
+ * 2. Only keeps an item if a majority of the remaining terms (>=60%, min 2)
40
+ * independently matched it — coincidental single-term noise matches
41
+ * don't reach quorum.
42
+ * 3. Only keeps an item whose best per-term score is very tight (<= 0.15)
43
+ * — genuine matches on title/id/description land near 0, while noise
44
+ * from the diluted content field scores much worse.
45
+ *
46
+ * Falls back to the plain whole-query result (usually `[]`) if nothing
47
+ * clears both bars, which is what drives `identify_pattern`'s `noMatch`.
48
+ */
49
+ export declare function searchOrPerTermStrict<T>(fuse: Fuse<T>, query: string, limit?: number): FuseResult<T>[];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Run one Fuse search per whitespace-separated term and merge the results,
3
+ * keeping the best (lowest) score and a per-item match count when the same
4
+ * item is hit by more than one term.
5
+ */
6
+ function searchPerTerm(fuse, terms) {
7
+ const best = new Map();
8
+ for (const term of terms) {
9
+ for (const result of fuse.search(term)) {
10
+ const existing = best.get(result.item);
11
+ if (!existing) {
12
+ best.set(result.item, { result, hits: 1 });
13
+ }
14
+ else {
15
+ existing.hits += 1;
16
+ if ((result.score ?? 1) < (existing.result.score ?? 1))
17
+ existing.result = result;
18
+ }
19
+ }
20
+ }
21
+ return [...best.values()];
22
+ }
23
+ /**
24
+ * Search a pre-configured Fuse instance the normal (whole-query) way first;
25
+ * if that yields nothing, fall back to searching term-by-term (OR semantics)
26
+ * and merge the results.
27
+ *
28
+ * Fuse.js scores a whole query string as a single fuzzy pattern. With
29
+ * multi-word queries (e.g. "alert warning inline notification banner") the
30
+ * full string sometimes fails to match any indexed field closely enough to
31
+ * clear the configured threshold, even though a single term ("alert") would
32
+ * match cleanly — so callers got `[]` for exactly the kind of query the model
33
+ * is most likely to send (SPT-1828).
34
+ *
35
+ * The whole-query search is tried first (and returned as-is when it finds
36
+ * anything) because it is more precise. Per-term OR is only used as a
37
+ * fallback for the zero-result case the bug describes.
38
+ *
39
+ * Use this for tools whose Fuse keys are short fields (names, descriptions,
40
+ * categories) — `search_components`, `search_tokens`. For `identify_pattern`,
41
+ * whose pool also weighs a large `content` field very low, use
42
+ * `searchOrPerTermStrict` instead (see its docstring for why).
43
+ */
44
+ export function searchOrPerTerm(fuse, query, limit) {
45
+ const wholeQueryResults = fuse.search(query, limit ? { limit } : undefined);
46
+ if (wholeQueryResults.length > 0)
47
+ return wholeQueryResults;
48
+ const terms = query.trim().split(/\s+/).filter(Boolean);
49
+ if (terms.length <= 1)
50
+ return wholeQueryResults;
51
+ const merged = searchPerTerm(fuse, terms)
52
+ .map(({ result }) => result)
53
+ .sort((a, b) => (a.score ?? 1) - (b.score ?? 1));
54
+ return limit ? merged.slice(0, limit) : merged;
55
+ }
56
+ const FALLBACK_STOPWORDS = new Set([
57
+ 'a', 'an', 'the', 'to', 'of', 'is', 'no', 'when', 'with', 'for', 'and', 'or',
58
+ 'in', 'on', 'at', 'be', 'are', 'i', 'you', 'your', 'it', 'this', 'that', 'as',
59
+ ]);
60
+ /**
61
+ * Stricter per-term fallback for `identify_pattern`'s natural-language
62
+ * `useCase` queries.
63
+ *
64
+ * `identify_pattern`'s pool weighs a pattern's full `content` field very low
65
+ * (0.1) specifically so a big text blob only matters when several query
66
+ * terms line up together — plain per-term OR search defeats that: almost
67
+ * any short single word fuzzy-matches *something* inside a 10k+ character
68
+ * blob purely by chance (Bitap fuzzy matching on long strings), which both
69
+ * resurfaces noise for genuine queries and breaks the "no match" case for
70
+ * unrelated/nonsense queries (SPT-1828 regression check).
71
+ *
72
+ * To keep the fallback honest, this variant:
73
+ * 1. Drops filler words (short + stopwords) before splitting into terms.
74
+ * 2. Only keeps an item if a majority of the remaining terms (>=60%, min 2)
75
+ * independently matched it — coincidental single-term noise matches
76
+ * don't reach quorum.
77
+ * 3. Only keeps an item whose best per-term score is very tight (<= 0.15)
78
+ * — genuine matches on title/id/description land near 0, while noise
79
+ * from the diluted content field scores much worse.
80
+ *
81
+ * Falls back to the plain whole-query result (usually `[]`) if nothing
82
+ * clears both bars, which is what drives `identify_pattern`'s `noMatch`.
83
+ */
84
+ export function searchOrPerTermStrict(fuse, query, limit) {
85
+ const wholeQueryResults = fuse.search(query, limit ? { limit } : undefined);
86
+ if (wholeQueryResults.length > 0)
87
+ return wholeQueryResults;
88
+ const terms = query
89
+ .trim()
90
+ .toLowerCase()
91
+ .split(/\s+/)
92
+ .filter(t => t.length >= 3 && !FALLBACK_STOPWORDS.has(t));
93
+ if (terms.length < 2)
94
+ return wholeQueryResults;
95
+ const quorum = Math.max(2, Math.ceil(terms.length * 0.6));
96
+ const STRICT_SCORE_CEILING = 0.15;
97
+ const merged = searchPerTerm(fuse, terms)
98
+ .filter(({ result, hits }) => hits >= quorum && (result.score ?? 1) <= STRICT_SCORE_CEILING)
99
+ .map(({ result }) => result)
100
+ .sort((a, b) => (a.score ?? 1) - (b.score ?? 1));
101
+ return limit ? merged.slice(0, limit) : merged;
102
+ }
103
+ //# sourceMappingURL=_fuzzy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_fuzzy.js","sourceRoot":"","sources":["../../src/tools/_fuzzy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type IndexCollection } from '../data/loader.js';
2
+ import type { SpotlightIndex } from '../types.js';
3
+ export type ToolResult = {
4
+ content: {
5
+ type: 'text';
6
+ text: string;
7
+ }[];
8
+ isError?: boolean;
9
+ };
10
+ /**
11
+ * Returns null when every collection this tool depends on has data, otherwise a
12
+ * ready-to-return MCP error naming the problem and the fix.
13
+ *
14
+ * The "do not answer from memory" line is deliberate: the observed failure mode of
15
+ * an empty index is a model quietly substituting remembered or MUI component and
16
+ * token names for real Spotlight ones.
17
+ */
18
+ export declare function requireCollections(tool: string, ...needed: IndexCollection[]): ToolResult | null;
19
+ /**
20
+ * Guard + load in one call:
21
+ *
22
+ * const { index, error } = guardedIndex('get_component', 'components');
23
+ * if (error) return error;
24
+ */
25
+ export declare function guardedIndex(tool: string, ...needed: IndexCollection[]): {
26
+ index: SpotlightIndex;
27
+ error?: undefined;
28
+ } | {
29
+ index?: undefined;
30
+ error: ToolResult;
31
+ };
@@ -0,0 +1,62 @@
1
+ // Not a tool. Shared guard every tool calls before touching the index, so an
2
+ // empty or unusable index can never be reported as an honest negative answer.
3
+ import { loadIndex } from '../data/loader.js';
4
+ import { getIndexHealth, remedyFor } from '../data/health.js';
5
+ function errorResult(payload) {
6
+ return {
7
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
8
+ isError: true,
9
+ };
10
+ }
11
+ /**
12
+ * Returns null when every collection this tool depends on has data, otherwise a
13
+ * ready-to-return MCP error naming the problem and the fix.
14
+ *
15
+ * The "do not answer from memory" line is deliberate: the observed failure mode of
16
+ * an empty index is a model quietly substituting remembered or MUI component and
17
+ * token names for real Spotlight ones.
18
+ */
19
+ export function requireCollections(tool, ...needed) {
20
+ const health = getIndexHealth();
21
+ if (health.fatal) {
22
+ return errorResult({
23
+ error: 'SPOTLIGHT_INDEX_UNREADABLE',
24
+ tool,
25
+ message: `${health.fatal} This server has no design-system data. ` +
26
+ 'Do not answer from memory or invent component/token names — tell the user the index ' +
27
+ 'could not be loaded and how to fix it.',
28
+ indexPath: health.indexPath,
29
+ remedy: remedyFor(health.indexPath),
30
+ });
31
+ }
32
+ const missing = needed.filter(c => health.counts[c] === 0);
33
+ if (!missing.length)
34
+ return null;
35
+ return errorResult({
36
+ error: 'SPOTLIGHT_INDEX_EMPTY',
37
+ tool,
38
+ message: `The Spotlight index has no ${missing.join(' and no ')}. ` +
39
+ `"${tool}" cannot answer until the index is rebuilt. ` +
40
+ 'Do not answer from memory or invent component/token names — tell the user the index is ' +
41
+ 'empty and how to fix it.',
42
+ missing,
43
+ indexPath: health.indexPath,
44
+ indexVersion: health.version,
45
+ generated: health.generated,
46
+ counts: health.counts,
47
+ remedy: remedyFor(health.indexPath),
48
+ });
49
+ }
50
+ /**
51
+ * Guard + load in one call:
52
+ *
53
+ * const { index, error } = guardedIndex('get_component', 'components');
54
+ * if (error) return error;
55
+ */
56
+ export function guardedIndex(tool, ...needed) {
57
+ const error = requireCollections(tool, ...needed);
58
+ if (error)
59
+ return { error };
60
+ return { index: loadIndex() };
61
+ }
62
+ //# sourceMappingURL=_guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_guard.js","sourceRoot":"","sources":["../../src/tools/_guard.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,8EAA8E;AAC9E,OAAO,EAAE,SAAS,EAAwB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAQ9D,SAAS,WAAW,CAAC,OAAgC;IACnD,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QAC5E,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,GAAG,MAAyB;IAE5B,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;IAEhC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,WAAW,CAAC;YACjB,KAAK,EAAE,4BAA4B;YACnC,IAAI;YACJ,OAAO,EACL,GAAG,MAAM,CAAC,KAAK,0CAA0C;gBACzD,sFAAsF;gBACtF,wCAAwC;YAC1C,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC;SACpC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEjC,OAAO,WAAW,CAAC;QACjB,KAAK,EAAE,uBAAuB;QAC9B,IAAI;QACJ,OAAO,EACL,8BAA8B,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI;YAC1D,IAAI,IAAI,8CAA8C;YACtD,yFAAyF;YACzF,0BAA0B;QAC5B,OAAO;QACP,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,YAAY,EAAE,MAAM,CAAC,OAAO;QAC5B,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC;KACpC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,GAAG,MAAyB;IAE5B,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,CAAC;IAClD,IAAI,KAAK;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC;AAChC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function expandWithSynonyms(query: string): string;