@fragments-sdk/mcp 0.10.0 → 0.10.1
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/dist/bin.js +2 -3
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-YJTMK4JY.js → chunk-KGFM5SCE.js} +26 -33
- package/dist/chunk-KGFM5SCE.js.map +1 -0
- package/dist/{dist-TTCI6TME.js → dist-LVC53MY5.js} +48 -10
- package/dist/{dist-TTCI6TME.js.map → dist-LVC53MY5.js.map} +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/server.js +1 -2
- package/package.json +5 -5
- package/dist/chunk-4SVS3AA3.js +0 -78
- package/dist/chunk-4SVS3AA3.js.map +0 -1
- package/dist/chunk-YJTMK4JY.js.map +0 -1
- package/dist/constants-BLN4SSNH.js +0 -10
- package/dist/constants-BLN4SSNH.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -17,10 +17,7 @@ import {
|
|
|
17
17
|
telemetryMiddleware,
|
|
18
18
|
tokensFromCompiledTokenData,
|
|
19
19
|
validateSnapshot
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import {
|
|
22
|
-
BRAND
|
|
23
|
-
} from "./chunk-4SVS3AA3.js";
|
|
20
|
+
} from "./chunk-KGFM5SCE.js";
|
|
24
21
|
import {
|
|
25
22
|
generateRulesFiles
|
|
26
23
|
} from "./chunk-WDQPNHZ2.js";
|
|
@@ -201,6 +198,9 @@ var DEFAULT_SEARCH_CONFIG = {
|
|
|
201
198
|
vectorSearchUrl: "https://combative-jay-834.convex.site/search",
|
|
202
199
|
vectorSearchTimeoutMs: 3e3
|
|
203
200
|
};
|
|
201
|
+
|
|
202
|
+
// src/index.ts
|
|
203
|
+
import { BRAND } from "@fragments-sdk/core";
|
|
204
204
|
export {
|
|
205
205
|
AutoExtractionAdapter,
|
|
206
206
|
BRAND,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/custom-json.ts","../src/orama-index.ts","../src/scoring.ts","../src/search-config.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { mcpSnapshotSchema, type McpSnapshot } from '@fragments-sdk/core';\nimport type {\n CompiledBlock,\n CompiledFragment,\n CompiledTokenData,\n} from '@fragments-sdk/context/types';\nimport type { SerializedComponentGraph } from '@fragments-sdk/context/graph';\nimport type { DesignSystemData } from '../types.js';\nimport type { DataAdapter } from './types.js';\nimport {\n blockFromCompiledBlock,\n buildCapabilities,\n componentFromCompiledFragment,\n tokensFromCompiledTokenData,\n validateSnapshot,\n} from './snapshot-converters.js';\n\nexport class CustomJsonAdapter implements DataAdapter {\n readonly name = 'custom-json';\n\n constructor(private filePath: string) {}\n\n discover(): string[] {\n return existsSync(this.filePath) ? [this.filePath] : [];\n }\n\n async load(_projectRoot = ''): Promise<DesignSystemData> {\n if (!existsSync(this.filePath)) {\n throw new Error(`Custom data file not found: ${this.filePath}`);\n }\n\n const content = await readFile(this.filePath, 'utf-8');\n const raw = JSON.parse(content) as Record<string, unknown>;\n\n if (\n raw.schemaVersion === 1 &&\n typeof raw.sourceType === 'string' &&\n raw.components &&\n typeof raw.components === 'object'\n ) {\n const snapshot = mcpSnapshotSchema.parse(raw) as McpSnapshot;\n return {\n snapshot,\n components: snapshot.components,\n blocks: snapshot.blocks,\n tokens: snapshot.tokens,\n graph: snapshot.graph as SerializedComponentGraph | undefined,\n performanceSummary: snapshot.performanceSummary,\n packageMap: snapshot.packageMap,\n defaultPackageName: snapshot.defaultPackageName,\n capabilities: new Set(snapshot.capabilities),\n };\n }\n\n if (!raw.components || typeof raw.components !== 'object') {\n throw new Error(\n `Invalid design system data: \"components\" field is required and must be an object. ` +\n `File: ${this.filePath}`,\n );\n }\n\n const packageMap =\n (raw.packageMap as Record<string, string> | undefined) ?? {};\n const defaultPackageName = raw.defaultPackageName as string | undefined;\n const metadata = raw.metadata as\n | {\n designSystemName?: string;\n packageName?: string;\n importPath?: string;\n revision?: string;\n updatedAt?: string;\n }\n | undefined;\n const components = Object.fromEntries(\n Object.entries(raw.components as Record<string, CompiledFragment>).map(\n ([key, fragment]) => [\n key,\n componentFromCompiledFragment({\n id: key,\n fragment,\n sourceType: 'custom-json',\n packageName: packageMap[fragment.meta.name] ?? defaultPackageName,\n importPath: packageMap[fragment.meta.name] ?? defaultPackageName,\n }),\n ],\n ),\n );\n const blocks = raw.blocks\n ? Object.fromEntries(\n Object.entries(raw.blocks as Record<string, CompiledBlock>).map(\n ([key, block]) => [key, blockFromCompiledBlock(key, block)],\n ),\n )\n : undefined;\n const tokens = raw.tokens\n ? tokensFromCompiledTokenData(raw.tokens as CompiledTokenData)\n : undefined;\n const snapshot = validateSnapshot({\n schemaVersion: 1,\n sourceType: 'custom-json',\n sourceLabel: this.filePath,\n capabilities: buildCapabilities({\n components,\n blocks,\n tokens,\n graph: raw.graph,\n performanceSummary: raw.performanceSummary,\n }),\n metadata: {\n designSystemName: metadata?.designSystemName,\n packageName: metadata?.packageName ?? defaultPackageName,\n importPath: metadata?.importPath ?? defaultPackageName,\n revision: metadata?.revision,\n updatedAt: metadata?.updatedAt,\n },\n components,\n blocks,\n tokens,\n graph: raw.graph as SerializedComponentGraph | undefined,\n performanceSummary:\n raw.performanceSummary as McpSnapshot['performanceSummary'],\n packageMap,\n defaultPackageName,\n });\n\n return {\n snapshot,\n components: snapshot.components,\n blocks: snapshot.blocks,\n tokens: snapshot.tokens,\n graph: snapshot.graph as SerializedComponentGraph | undefined,\n performanceSummary: snapshot.performanceSummary,\n packageMap: snapshot.packageMap,\n defaultPackageName: snapshot.defaultPackageName,\n capabilities: new Set(snapshot.capabilities),\n };\n }\n}\n","/**\n * Orama-powered BM25 search indexes for components, blocks, and tokens.\n *\n * Replaces the bag-of-words keyword scorer with Orama's built-in BM25 engine,\n * which provides IDF weighting (automatically downweights common terms like\n * \"page\", \"section\", \"content\"), stemming, and field-level boosting.\n */\n\nimport { create, insertMultiple, search } from '@orama/orama';\nimport type { AnyOrama } from '@orama/orama';\nimport type { McpBlock, McpComponent, McpTokenData } from '@fragments-sdk/core';\nimport type {\n CompiledBlock,\n CompiledFragment,\n CompiledTokenData,\n} from '@fragments-sdk/context/types';\nimport type { ScoredResult, EntryKind } from './search.js';\n\n// ---------------------------------------------------------------------------\n// Synonym map (shared with search.ts — imported via re-export)\n// ---------------------------------------------------------------------------\n\nexport const SYNONYM_MAP: Record<string, string[]> = {\n 'form': ['input', 'field', 'submit', 'validation'],\n 'input': ['form', 'field', 'text', 'entry'],\n 'button': ['action', 'click', 'submit', 'trigger'],\n 'action': ['button', 'click', 'trigger'],\n 'submit': ['button', 'form', 'action', 'send'],\n 'alert': ['notification', 'message', 'warning', 'error', 'feedback'],\n 'notification': ['alert', 'message', 'toast'],\n 'feedback': ['form', 'comment', 'review', 'rating'],\n 'card': ['container', 'panel', 'box', 'content'],\n 'toggle': ['switch', 'checkbox', 'boolean', 'on/off'],\n 'switch': ['toggle', 'checkbox', 'boolean'],\n 'badge': ['tag', 'label', 'status', 'indicator'],\n 'status': ['badge', 'indicator', 'state'],\n 'login': ['auth', 'signin', 'authentication', 'form'],\n 'auth': ['login', 'signin', 'authentication'],\n 'chat': ['message', 'conversation', 'ai'],\n 'table': ['data', 'grid', 'list', 'rows'],\n 'textarea': ['text', 'input', 'multiline', 'area', 'comment'],\n 'area': ['textarea', 'multiline', 'text'],\n 'landing': ['page', 'hero', 'marketing', 'section', 'layout'],\n 'hero': ['landing', 'marketing', 'banner', 'headline', 'section'],\n 'marketing': ['landing', 'hero', 'pricing', 'testimonial', 'cta'],\n 'cta': ['marketing', 'banner', 'action', 'button'],\n 'testimonial': ['marketing', 'review', 'quote', 'feedback'],\n 'layout': ['stack', 'grid', 'box', 'container', 'page'],\n 'page': ['layout', 'landing', 'section', 'container'],\n 'section': ['hero', 'feature', 'testimonial', 'cta', 'faq'],\n 'pricing': ['card', 'plan', 'tier', 'marketing'],\n 'plan': ['pricing', 'card', 'tier', 'subscription'],\n 'dashboard': ['metrics', 'stats', 'chart', 'card', 'grid'],\n 'metrics': ['dashboard', 'stats', 'progress', 'number'],\n 'stats': ['metrics', 'dashboard', 'progress', 'badge'],\n 'chart': ['dashboard', 'metrics', 'data', 'graph'],\n};\n\n// ---------------------------------------------------------------------------\n// Query expansion via synonyms\n// ---------------------------------------------------------------------------\n\n/**\n * Expand a query string with synonym terms.\n * Returns the expanded query as a single string suitable for Orama `term`.\n */\nexport function expandQuery(query: string): string {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const expanded = new Set(terms);\n for (const term of terms) {\n const synonyms = SYNONYM_MAP[term];\n if (synonyms) {\n for (const syn of synonyms) expanded.add(syn);\n }\n }\n return Array.from(expanded).join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// Shared two-pass search pattern\n// ---------------------------------------------------------------------------\n\ninterface TwoPassConfig {\n index: AnyOrama;\n query: string;\n properties: string[];\n boost: Record<string, number>;\n limit: number;\n kind: EntryKind;\n}\n\n/**\n * Two-pass BM25 search: original terms weighted 2x, synonym-expanded terms 1x.\n * Prevents synonym noise from outranking truly relevant results.\n */\nexport function twoPassSearch(config: TwoPassConfig): ScoredResult[] {\n const { index, query, properties, boost, limit, kind } = config;\n\n const baseConfig = {\n mode: 'fulltext' as const,\n properties,\n boost,\n limit,\n };\n\n const originalTermsQuery = query.toLowerCase().split(/\\s+/).filter(Boolean).join(' ');\n const expandedQuery = expandQuery(query);\n\n // First pass: original terms only (moderate threshold to filter weak matches)\n const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });\n // Second pass: synonym-expanded (permissive — scoring handles relevance via 1x weight)\n const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });\n\n type Hit = { score: number; document: { name: string } };\n const origHits = (originalResults as unknown as { hits: Hit[] }).hits;\n const expHits = (expandedResults as unknown as { hits: Hit[] }).hits;\n\n // Combine scores: original terms get 2x weight\n const scoreMap = new Map<string, number>();\n for (const hit of origHits) {\n scoreMap.set(hit.document.name, (hit.score || 0) * 2);\n }\n for (const hit of expHits) {\n const name = hit.document.name;\n const existing = scoreMap.get(name) ?? 0;\n scoreMap.set(name, existing + (hit.score || 0));\n }\n\n const scored: ScoredResult[] = [];\n for (const [name, score] of scoreMap) {\n if (score > 0) {\n scored.push({ name, kind, rank: scored.length, score });\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n scored.forEach((s, i) => { s.rank = i; });\n\n return scored;\n}\n\n// ---------------------------------------------------------------------------\n// Component index\n// ---------------------------------------------------------------------------\n\nconst componentSchema = {\n name: 'string' as const,\n description: 'string' as const,\n category: 'string' as const,\n tags: 'string' as const,\n whenUsed: 'string' as const,\n patterns: 'string' as const,\n variants: 'string' as const,\n status: 'string' as const,\n};\n\nexport type ComponentIndex = AnyOrama;\n\nfunction isCompiledFragment(\n value: McpComponent | CompiledFragment,\n): value is CompiledFragment {\n return 'meta' in value;\n}\n\nfunction normalizeComponent(\n value: McpComponent | CompiledFragment,\n): McpComponent {\n if (!isCompiledFragment(value)) return value;\n return {\n id: value.filePath ?? value.meta.name,\n name: value.meta.name,\n description: value.meta.description ?? '',\n category: value.meta.category ?? 'uncategorized',\n status: value.meta.status ?? 'stable',\n tags: value.meta.tags ?? [],\n props: {},\n propsSummary:\n value.propsSummary ??\n value.contract?.propsSummary ??\n Object.entries(value.props ?? {}).map(\n ([propName, prop]) =>\n `${propName}${prop.required ? ' (required)' : ''}: ${prop.type}`,\n ),\n examples: (value.variants ?? []).map((variant) => ({\n name: variant.name,\n description: variant.description,\n code: variant.code,\n })),\n relations: (value.relations ?? []).map((relation) => ({\n componentName: relation.component,\n relationship: relation.relationship,\n note: relation.note,\n })),\n compoundChildren: [],\n guidance: {\n when: value.usage?.when ?? [],\n whenNot: value.usage?.whenNot ?? [],\n guidelines: value.usage?.guidelines ?? [],\n accessibility: value.usage?.accessibility ?? [],\n dos: value.usage?.when ?? [],\n donts: value.usage?.whenNot ?? [],\n patterns: [],\n },\n sourceType: 'fragments-json',\n sourcePath: value.sourcePath ?? value.filePath,\n metadata: {\n a11yRules: value.contract?.a11yRules ?? [],\n scenarioTags: value.contract?.scenarioTags ?? [],\n },\n };\n}\n\nexport function buildComponentIndex(\n fragments: Array<McpComponent | CompiledFragment>,\n): ComponentIndex {\n const db = create({ schema: componentSchema, language: 'english' });\n const normalized = fragments.map(normalizeComponent);\n\n const docs = normalized.map((f) => ({\n name: f.name,\n description: f.description ?? '',\n category: f.category ?? '',\n tags: (f.tags ?? []).join(' '),\n whenUsed: (f.guidance.when ?? []).join(' '),\n patterns: (f.guidance.patterns ?? [])\n .map(\n (pattern: McpComponent['guidance']['patterns'][number]) =>\n `${pattern.name} ${pattern.description || ''}`,\n )\n .join(' '),\n variants: f.examples\n .map(\n (example: McpComponent['examples'][number]) =>\n `${example.name} ${example.description || ''}`,\n )\n .join(' '),\n status: f.status ?? 'stable',\n }));\n\n insertMultiple(db, docs);\n return db;\n}\n\nexport function searchComponents(\n query: string,\n index: ComponentIndex,\n fragments: Array<McpComponent | CompiledFragment>,\n limit = 50,\n): ScoredResult[] {\n const normalized = fragments.map(normalizeComponent);\n const boostConfig = {\n mode: 'fulltext' as const,\n properties: ['name', 'whenUsed', 'description', 'patterns', 'category', 'tags', 'variants'],\n boost: {\n name: 3,\n whenUsed: 2.5,\n description: 2,\n patterns: 1.5,\n category: 1.5,\n tags: 1.5,\n variants: 1,\n },\n limit,\n };\n\n // Two-pass search: original terms weighted 2x, synonym terms 1x\n // This prevents synonym noise (\"box\", \"container\") from outranking truly relevant results\n const originalTermsList = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const originalTermsQuery = originalTermsList.join(' ');\n const expandedQuery = expandQuery(query);\n\n // First pass: original terms, moderate threshold; second pass: synonyms, permissive\n const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });\n const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });\n\n type Hit = { score: number; document: { name: string } };\n const origHits = (originalResults as unknown as { hits: Hit[] }).hits;\n const expHits = (expandedResults as unknown as { hits: Hit[] }).hits;\n\n // Combine scores: original terms get 2x weight\n const scoreMap = new Map<string, number>();\n for (const hit of origHits) {\n scoreMap.set(hit.document.name, (hit.score || 0) * 2);\n }\n for (const hit of expHits) {\n const name = hit.document.name;\n const existing = scoreMap.get(name) ?? 0;\n scoreMap.set(name, existing + (hit.score || 0));\n }\n\n // Build a name→fragment lookup for status bonuses\n const fragmentMap = new Map<string, McpComponent>();\n for (const f of normalized) {\n fragmentMap.set(f.name.toLowerCase(), f);\n }\n\n // Exact name match bonus: if a component name matches an original query term exactly,\n // boost it above similar-named components (e.g., \"Button\" > \"ButtonGroup\" for \"button action\")\n const originalTermsSet = new Set(originalTermsList);\n\n // Build scored results with status + name match adjustments\n const scored: ScoredResult[] = [];\n for (const [name, rawScore] of scoreMap) {\n let score = rawScore;\n const nameLower = name.toLowerCase();\n const fragment = fragmentMap.get(nameLower);\n\n // Exact name match bonus — strong signal that this specific component was requested\n // Must be large enough to overcome BM25 score differences from compound names\n // (e.g., \"ButtonGroup\" matching more fields than \"Button\")\n if (originalTermsSet.has(nameLower)) {\n score += 25;\n }\n\n // Apply status bonus/penalty (matches old scorer)\n if (fragment) {\n if (fragment.status === 'stable') score += 5;\n else if (fragment.status === 'beta') score += 2;\n if (fragment.status === 'deprecated') score -= 25;\n }\n\n if (score > 0) {\n scored.push({ name, kind: 'component' as EntryKind, rank: scored.length, score });\n }\n }\n\n // Sort by score descending and assign ranks\n scored.sort((a, b) => b.score - a.score);\n scored.forEach((s, i) => { s.rank = i; });\n\n return scored;\n}\n\n// ---------------------------------------------------------------------------\n// Block index\n// ---------------------------------------------------------------------------\n\nconst blockSchema = {\n name: 'string' as const,\n description: 'string' as const,\n category: 'string' as const,\n tags: 'string' as const,\n components: 'string' as const,\n};\n\nexport type BlockIndex = AnyOrama;\n\nfunction normalizeBlock(value: McpBlock | CompiledBlock): McpBlock {\n if ('id' in value) return value;\n return {\n id: value.filePath ?? value.name,\n name: value.name,\n description: value.description ?? '',\n category: value.category ?? 'uncategorized',\n components: value.components ?? [],\n tags: value.tags ?? [],\n code: value.code ?? '',\n };\n}\n\nexport function buildBlockIndex(\n blocks: Array<McpBlock | CompiledBlock>,\n): BlockIndex {\n const db = create({ schema: blockSchema, language: 'english' });\n const normalized = blocks.map(normalizeBlock);\n\n const docs = normalized.map((b) => ({\n name: b.name,\n description: b.description ?? '',\n category: b.category ?? '',\n tags: (b.tags ?? []).join(' '),\n components: b.components.join(' '),\n }));\n\n insertMultiple(db, docs);\n return db;\n}\n\nexport function searchBlocks(\n query: string,\n index: BlockIndex,\n limit = 50,\n): ScoredResult[] {\n return twoPassSearch({\n index,\n query,\n properties: ['name', 'description', 'components', 'tags', 'category'],\n boost: {\n name: 3,\n description: 2,\n components: 1.5,\n tags: 1.5,\n category: 1.5,\n },\n limit,\n kind: 'block' as EntryKind,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Token index\n// ---------------------------------------------------------------------------\n\nconst tokenSchema = {\n name: 'string' as const,\n category: 'string' as const,\n description: 'string' as const,\n};\n\nexport type TokenIndex = AnyOrama;\n\nfunction normalizeTokenData(\n tokenData: McpTokenData | CompiledTokenData,\n): McpTokenData {\n if ('flat' in tokenData) return tokenData;\n const categories = Object.fromEntries(\n Object.entries(tokenData.categories).map(([category, entries]) => [\n category,\n entries.map((entry) => ({\n name: entry.name,\n category,\n value: typeof entry.value === 'string' ? entry.value : undefined,\n description: entry.description,\n })),\n ]),\n );\n return {\n prefix: tokenData.prefix,\n total: tokenData.total,\n categories,\n flat: Object.values(categories).flat(),\n };\n}\n\nexport function buildTokenIndex(\n tokenData: McpTokenData | CompiledTokenData,\n): TokenIndex {\n const db = create({ schema: tokenSchema, language: 'english' });\n const normalizedData = normalizeTokenData(tokenData);\n\n const docs: Array<{ name: string; category: string; description: string }> = [];\n for (const [cat, tokens] of Object.entries(normalizedData.categories) as Array<\n [string, McpTokenData['categories'][string]]\n >) {\n for (const token of tokens) {\n docs.push({\n name: token.name,\n category: cat,\n description: token.description ?? '',\n });\n }\n }\n\n insertMultiple(db, docs);\n return db;\n}\n\nexport function searchTokens(\n query: string,\n index: TokenIndex,\n limit = 50,\n): ScoredResult[] {\n return twoPassSearch({\n index,\n query,\n properties: ['name', 'category', 'description'],\n boost: {\n name: 2.5,\n category: 2,\n description: 1.5,\n },\n limit,\n kind: 'token' as EntryKind,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Semantic category extraction for implement tool token matching\n// ---------------------------------------------------------------------------\n\n/** Maps use-case keywords to relevant token categories */\nexport const USE_CASE_TOKEN_CATEGORIES: Record<string, string[]> = {\n 'table': ['spacing', 'borders', 'surfaces', 'text'],\n 'data': ['spacing', 'borders', 'surfaces'],\n 'grid': ['spacing', 'layout'],\n 'form': ['spacing', 'borders', 'radius', 'focus'],\n 'input': ['spacing', 'borders', 'radius', 'focus'],\n 'card': ['surfaces', 'shadows', 'radius', 'borders', 'spacing'],\n 'button': ['colors', 'radius', 'spacing', 'focus'],\n 'layout': ['spacing', 'layout', 'surfaces'],\n 'dashboard': ['spacing', 'surfaces', 'borders', 'shadows'],\n 'chat': ['spacing', 'surfaces', 'radius', 'shadows'],\n 'modal': ['shadows', 'surfaces', 'radius', 'spacing'],\n 'dialog': ['shadows', 'surfaces', 'radius', 'spacing'],\n 'navigation': ['spacing', 'surfaces', 'borders'],\n 'sidebar': ['spacing', 'surfaces', 'borders'],\n 'hero': ['spacing', 'typography', 'colors'],\n 'landing': ['spacing', 'typography', 'colors'],\n 'pricing': ['spacing', 'surfaces', 'borders', 'radius'],\n 'auth': ['spacing', 'borders', 'radius', 'focus'],\n 'login': ['spacing', 'borders', 'radius', 'focus'],\n 'dark': ['colors', 'surfaces'],\n 'theme': ['colors', 'surfaces', 'text'],\n};\n\n/**\n * Extract relevant token categories from a use-case query.\n * Returns category names that should be included in implement results.\n */\nexport function extractTokenCategories(query: string): string[] {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const categories = new Set<string>();\n\n for (const term of terms) {\n const cats = USE_CASE_TOKEN_CATEGORIES[term];\n if (cats) {\n for (const cat of cats) categories.add(cat);\n }\n }\n\n // If nothing matched, return common defaults\n if (categories.size === 0) {\n return ['spacing', 'colors', 'surfaces'];\n }\n\n return Array.from(categories);\n}\n","/**\n * Scoring utilities for MCP tool result ranking.\n *\n * Extracted as pure functions for easy testing and reuse across\n * discover, implement, and other search-driven tools.\n */\n\nimport type { ScoredResult } from './search.js';\n\n// ---------------------------------------------------------------------------\n// Confidence assignment — ratio-to-max (scale-independent)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimum absolute score floor. If the best result scores below this,\n * results are considered a no-match rather than low-confidence garbage.\n *\n * BM25 scores for a 60-component corpus typically range 10-100+ for real\n * matches (including name-match bonuses of +25 and status bonuses of +5).\n * A maxScore below 5 means no meaningful term overlap was found — only\n * status bonuses and/or minimal synonym noise.\n *\n * Note: this threshold is calibrated for BM25 keyword scores. When hybrid\n * search uses RRF fusion (premium API key), scores are in a different range\n * (~0.01-0.05) and the threshold should be bypassed via a maxScore > 1 guard.\n */\nexport const MINIMUM_SCORE_THRESHOLD = 5;\n\n/**\n * Assign a confidence level based on how a score compares to the best result.\n *\n * Uses ratio-to-max instead of absolute thresholds so the same function works\n * for both keyword scores (8-50+) and RRF scores (0.01-0.02).\n *\n * - high: score >= 70% of maxScore (\"this is almost certainly what you need\")\n * - medium: score >= 40% of maxScore (\"this could be useful\")\n * - low: score < 40% of maxScore (\"tangentially related\")\n */\nexport function assignConfidence(\n score: number,\n maxScore: number,\n): 'high' | 'medium' | 'low' {\n if (maxScore <= 0) return 'low';\n const ratio = score / maxScore;\n if (ratio >= 0.7) return 'high';\n if (ratio >= 0.4) return 'medium';\n return 'low';\n}\n\n/**\n * Check if the best score in a result set meets the minimum quality threshold.\n * Returns true if results are meaningful, false if they're noise.\n */\nexport function meetsMinimumThreshold(maxScore: number): boolean {\n return maxScore >= MINIMUM_SCORE_THRESHOLD;\n}\n\n// ---------------------------------------------------------------------------\n// Levenshtein distance — for fuzzy \"did you mean?\" suggestions\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the Levenshtein edit distance between two strings.\n * Used for suggesting closest component name matches on typos.\n */\nexport function levenshtein(a: string, b: string): number {\n const la = a.length;\n const lb = b.length;\n const dp: number[] = Array.from({ length: lb + 1 }, (_, i) => i);\n\n for (let i = 1; i <= la; i++) {\n let prev = i - 1;\n dp[0] = i;\n for (let j = 1; j <= lb; j++) {\n const temp = dp[j];\n dp[j] = a[i - 1] === b[j - 1]\n ? prev\n : 1 + Math.min(prev, dp[j], dp[j - 1]);\n prev = temp;\n }\n }\n\n return dp[lb];\n}\n\n/**\n * Find the closest matching name from a list, using Levenshtein distance.\n * Returns the closest match if it's within `maxDistance`, or null otherwise.\n */\nexport function findClosestMatch(\n input: string,\n candidates: string[],\n maxDistance = 3,\n): string | null {\n const inputLower = input.toLowerCase();\n let bestMatch: string | null = null;\n let bestDist = maxDistance + 1;\n\n for (const candidate of candidates) {\n const candidateLower = candidate.toLowerCase();\n const dist = levenshtein(inputLower, candidateLower);\n if (dist < bestDist) {\n bestDist = dist;\n bestMatch = candidate;\n } else if (dist === bestDist && bestMatch) {\n // Tie-break: prefer the candidate whose length is closer to input length\n const currentLenDiff = Math.abs(bestMatch.length - input.length);\n const newLenDiff = Math.abs(candidate.length - input.length);\n if (newLenDiff < currentLenDiff) {\n bestMatch = candidate;\n }\n }\n }\n\n return bestDist <= maxDistance ? bestMatch : null;\n}\n\n// ---------------------------------------------------------------------------\n// Block-first component ranking\n// ---------------------------------------------------------------------------\n\n/** Points added per block occurrence (e.g., Card in 3 blocks → +15) */\nexport const BLOCK_BOOST_PER_OCCURRENCE = 5;\n\n/**\n * Count how many blocks each component appears in.\n *\n * Returns a Map<lowercase component name, count>.\n * This is the core signal for block-first ranking: if Card appears in 5/5\n * marketing blocks, it's clearly more important than Sidebar which appears in 0.\n */\nexport function buildBlockComponentFrequency(\n blocks: Array<{ components: string[] }>,\n): Map<string, number> {\n const freq = new Map<string, number>();\n for (const block of blocks) {\n for (const comp of block.components) {\n const key = comp.toLowerCase();\n freq.set(key, (freq.get(key) ?? 0) + 1);\n }\n }\n return freq;\n}\n\n/**\n * Boost component search results by how frequently they appear in matching blocks.\n *\n * Mutates scores in-place and re-sorts. Components that appear in more blocks\n * get a proportionally higher boost (freq * BLOCK_BOOST_PER_OCCURRENCE).\n *\n * Returns the mutated+sorted array for convenience.\n */\nexport function boostByBlockFrequency(\n results: ScoredResult[],\n freq: Map<string, number>,\n): ScoredResult[] {\n for (const result of results) {\n const count = freq.get(result.name.toLowerCase()) ?? 0;\n if (count > 0) {\n result.score += count * BLOCK_BOOST_PER_OCCURRENCE;\n }\n }\n results.sort((a, b) => b.score - a.score);\n results.forEach((r, i) => { r.rank = i; });\n return results;\n}\n","import { SYNONYM_MAP, USE_CASE_TOKEN_CATEGORIES } from './orama-index.js';\nimport { MINIMUM_SCORE_THRESHOLD, BLOCK_BOOST_PER_OCCURRENCE } from './scoring.js';\n\nexport interface SearchConfig {\n synonyms?: Record<string, string[]>;\n useCaseTokenCategories?: Record<string, string[]>;\n minimumScoreThreshold?: number;\n blockBoostPerOccurrence?: number;\n vectorSearchUrl?: string;\n vectorSearchTimeoutMs?: number;\n}\n\nexport const DEFAULT_SEARCH_CONFIG: Required<SearchConfig> = {\n synonyms: SYNONYM_MAP,\n useCaseTokenCategories: USE_CASE_TOKEN_CATEGORIES,\n minimumScoreThreshold: MINIMUM_SCORE_THRESHOLD,\n blockBoostPerOccurrence: BLOCK_BOOST_PER_OCCURRENCE,\n vectorSearchUrl: 'https://combative-jay-834.convex.site/search',\n vectorSearchTimeoutMs: 3000,\n};\n"],"mappings":"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","names":["snapshot"]}
|
|
1
|
+
{"version":3,"sources":["../src/adapters/custom-json.ts","../src/orama-index.ts","../src/scoring.ts","../src/search-config.ts","../src/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { mcpSnapshotSchema, type McpSnapshot } from '@fragments-sdk/core';\nimport type {\n CompiledBlock,\n CompiledFragment,\n CompiledTokenData,\n} from '@fragments-sdk/context/types';\nimport type { SerializedComponentGraph } from '@fragments-sdk/context/graph';\nimport type { DesignSystemData } from '../types.js';\nimport type { DataAdapter } from './types.js';\nimport {\n blockFromCompiledBlock,\n buildCapabilities,\n componentFromCompiledFragment,\n tokensFromCompiledTokenData,\n validateSnapshot,\n} from './snapshot-converters.js';\n\nexport class CustomJsonAdapter implements DataAdapter {\n readonly name = 'custom-json';\n\n constructor(private filePath: string) {}\n\n discover(): string[] {\n return existsSync(this.filePath) ? [this.filePath] : [];\n }\n\n async load(_projectRoot = ''): Promise<DesignSystemData> {\n if (!existsSync(this.filePath)) {\n throw new Error(`Custom data file not found: ${this.filePath}`);\n }\n\n const content = await readFile(this.filePath, 'utf-8');\n const raw = JSON.parse(content) as Record<string, unknown>;\n\n if (\n raw.schemaVersion === 1 &&\n typeof raw.sourceType === 'string' &&\n raw.components &&\n typeof raw.components === 'object'\n ) {\n const snapshot = mcpSnapshotSchema.parse(raw) as McpSnapshot;\n return {\n snapshot,\n components: snapshot.components,\n blocks: snapshot.blocks,\n tokens: snapshot.tokens,\n graph: snapshot.graph as SerializedComponentGraph | undefined,\n performanceSummary: snapshot.performanceSummary,\n packageMap: snapshot.packageMap,\n defaultPackageName: snapshot.defaultPackageName,\n capabilities: new Set(snapshot.capabilities),\n };\n }\n\n if (!raw.components || typeof raw.components !== 'object') {\n throw new Error(\n `Invalid design system data: \"components\" field is required and must be an object. ` +\n `File: ${this.filePath}`,\n );\n }\n\n const packageMap =\n (raw.packageMap as Record<string, string> | undefined) ?? {};\n const defaultPackageName = raw.defaultPackageName as string | undefined;\n const metadata = raw.metadata as\n | {\n designSystemName?: string;\n packageName?: string;\n importPath?: string;\n revision?: string;\n updatedAt?: string;\n }\n | undefined;\n const components = Object.fromEntries(\n Object.entries(raw.components as Record<string, CompiledFragment>).map(\n ([key, fragment]) => [\n key,\n componentFromCompiledFragment({\n id: key,\n fragment,\n sourceType: 'custom-json',\n packageName: packageMap[fragment.meta.name] ?? defaultPackageName,\n importPath: packageMap[fragment.meta.name] ?? defaultPackageName,\n }),\n ],\n ),\n );\n const blocks = raw.blocks\n ? Object.fromEntries(\n Object.entries(raw.blocks as Record<string, CompiledBlock>).map(\n ([key, block]) => [key, blockFromCompiledBlock(key, block)],\n ),\n )\n : undefined;\n const tokens = raw.tokens\n ? tokensFromCompiledTokenData(raw.tokens as CompiledTokenData)\n : undefined;\n const snapshot = validateSnapshot({\n schemaVersion: 1,\n sourceType: 'custom-json',\n sourceLabel: this.filePath,\n capabilities: buildCapabilities({\n components,\n blocks,\n tokens,\n graph: raw.graph,\n performanceSummary: raw.performanceSummary,\n }),\n metadata: {\n designSystemName: metadata?.designSystemName,\n packageName: metadata?.packageName ?? defaultPackageName,\n importPath: metadata?.importPath ?? defaultPackageName,\n revision: metadata?.revision,\n updatedAt: metadata?.updatedAt,\n },\n components,\n blocks,\n tokens,\n graph: raw.graph as SerializedComponentGraph | undefined,\n performanceSummary:\n raw.performanceSummary as McpSnapshot['performanceSummary'],\n packageMap,\n defaultPackageName,\n });\n\n return {\n snapshot,\n components: snapshot.components,\n blocks: snapshot.blocks,\n tokens: snapshot.tokens,\n graph: snapshot.graph as SerializedComponentGraph | undefined,\n performanceSummary: snapshot.performanceSummary,\n packageMap: snapshot.packageMap,\n defaultPackageName: snapshot.defaultPackageName,\n capabilities: new Set(snapshot.capabilities),\n };\n }\n}\n","/**\n * Orama-powered BM25 search indexes for components, blocks, and tokens.\n *\n * Replaces the bag-of-words keyword scorer with Orama's built-in BM25 engine,\n * which provides IDF weighting (automatically downweights common terms like\n * \"page\", \"section\", \"content\"), stemming, and field-level boosting.\n */\n\nimport { create, insertMultiple, search } from '@orama/orama';\nimport type { AnyOrama } from '@orama/orama';\nimport type { McpBlock, McpComponent, McpTokenData } from '@fragments-sdk/core';\nimport type {\n CompiledBlock,\n CompiledFragment,\n CompiledTokenData,\n} from '@fragments-sdk/context/types';\nimport type { ScoredResult, EntryKind } from './search.js';\n\n// ---------------------------------------------------------------------------\n// Synonym map (shared with search.ts — imported via re-export)\n// ---------------------------------------------------------------------------\n\nexport const SYNONYM_MAP: Record<string, string[]> = {\n 'form': ['input', 'field', 'submit', 'validation'],\n 'input': ['form', 'field', 'text', 'entry'],\n 'button': ['action', 'click', 'submit', 'trigger'],\n 'action': ['button', 'click', 'trigger'],\n 'submit': ['button', 'form', 'action', 'send'],\n 'alert': ['notification', 'message', 'warning', 'error', 'feedback'],\n 'notification': ['alert', 'message', 'toast'],\n 'feedback': ['form', 'comment', 'review', 'rating'],\n 'card': ['container', 'panel', 'box', 'content'],\n 'toggle': ['switch', 'checkbox', 'boolean', 'on/off'],\n 'switch': ['toggle', 'checkbox', 'boolean'],\n 'badge': ['tag', 'label', 'status', 'indicator'],\n 'status': ['badge', 'indicator', 'state'],\n 'login': ['auth', 'signin', 'authentication', 'form'],\n 'auth': ['login', 'signin', 'authentication'],\n 'chat': ['message', 'conversation', 'ai'],\n 'table': ['data', 'grid', 'list', 'rows'],\n 'textarea': ['text', 'input', 'multiline', 'area', 'comment'],\n 'area': ['textarea', 'multiline', 'text'],\n 'landing': ['page', 'hero', 'marketing', 'section', 'layout'],\n 'hero': ['landing', 'marketing', 'banner', 'headline', 'section'],\n 'marketing': ['landing', 'hero', 'pricing', 'testimonial', 'cta'],\n 'cta': ['marketing', 'banner', 'action', 'button'],\n 'testimonial': ['marketing', 'review', 'quote', 'feedback'],\n 'layout': ['stack', 'grid', 'box', 'container', 'page'],\n 'page': ['layout', 'landing', 'section', 'container'],\n 'section': ['hero', 'feature', 'testimonial', 'cta', 'faq'],\n 'pricing': ['card', 'plan', 'tier', 'marketing'],\n 'plan': ['pricing', 'card', 'tier', 'subscription'],\n 'dashboard': ['metrics', 'stats', 'chart', 'card', 'grid'],\n 'metrics': ['dashboard', 'stats', 'progress', 'number'],\n 'stats': ['metrics', 'dashboard', 'progress', 'badge'],\n 'chart': ['dashboard', 'metrics', 'data', 'graph'],\n};\n\n// ---------------------------------------------------------------------------\n// Query expansion via synonyms\n// ---------------------------------------------------------------------------\n\n/**\n * Expand a query string with synonym terms.\n * Returns the expanded query as a single string suitable for Orama `term`.\n */\nexport function expandQuery(query: string): string {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const expanded = new Set(terms);\n for (const term of terms) {\n const synonyms = SYNONYM_MAP[term];\n if (synonyms) {\n for (const syn of synonyms) expanded.add(syn);\n }\n }\n return Array.from(expanded).join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// Shared two-pass search pattern\n// ---------------------------------------------------------------------------\n\ninterface TwoPassConfig {\n index: AnyOrama;\n query: string;\n properties: string[];\n boost: Record<string, number>;\n limit: number;\n kind: EntryKind;\n}\n\n/**\n * Two-pass BM25 search: original terms weighted 2x, synonym-expanded terms 1x.\n * Prevents synonym noise from outranking truly relevant results.\n */\nexport function twoPassSearch(config: TwoPassConfig): ScoredResult[] {\n const { index, query, properties, boost, limit, kind } = config;\n\n const baseConfig = {\n mode: 'fulltext' as const,\n properties,\n boost,\n limit,\n };\n\n const originalTermsQuery = query.toLowerCase().split(/\\s+/).filter(Boolean).join(' ');\n const expandedQuery = expandQuery(query);\n\n // First pass: original terms only (moderate threshold to filter weak matches)\n const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });\n // Second pass: synonym-expanded (permissive — scoring handles relevance via 1x weight)\n const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });\n\n type Hit = { score: number; document: { name: string } };\n const origHits = (originalResults as unknown as { hits: Hit[] }).hits;\n const expHits = (expandedResults as unknown as { hits: Hit[] }).hits;\n\n // Combine scores: original terms get 2x weight\n const scoreMap = new Map<string, number>();\n for (const hit of origHits) {\n scoreMap.set(hit.document.name, (hit.score || 0) * 2);\n }\n for (const hit of expHits) {\n const name = hit.document.name;\n const existing = scoreMap.get(name) ?? 0;\n scoreMap.set(name, existing + (hit.score || 0));\n }\n\n const scored: ScoredResult[] = [];\n for (const [name, score] of scoreMap) {\n if (score > 0) {\n scored.push({ name, kind, rank: scored.length, score });\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n scored.forEach((s, i) => { s.rank = i; });\n\n return scored;\n}\n\n// ---------------------------------------------------------------------------\n// Component index\n// ---------------------------------------------------------------------------\n\nconst componentSchema = {\n name: 'string' as const,\n description: 'string' as const,\n category: 'string' as const,\n tags: 'string' as const,\n whenUsed: 'string' as const,\n patterns: 'string' as const,\n variants: 'string' as const,\n status: 'string' as const,\n};\n\nexport type ComponentIndex = AnyOrama;\n\nfunction isCompiledFragment(\n value: McpComponent | CompiledFragment,\n): value is CompiledFragment {\n return 'meta' in value;\n}\n\nfunction normalizeComponent(\n value: McpComponent | CompiledFragment,\n): McpComponent {\n if (!isCompiledFragment(value)) return value;\n return {\n id: value.filePath ?? value.meta.name,\n name: value.meta.name,\n description: value.meta.description ?? '',\n category: value.meta.category ?? 'uncategorized',\n status: value.meta.status ?? 'stable',\n tags: value.meta.tags ?? [],\n props: {},\n propsSummary:\n value.propsSummary ??\n value.contract?.propsSummary ??\n Object.entries(value.props ?? {}).map(\n ([propName, prop]) =>\n `${propName}${prop.required ? ' (required)' : ''}: ${prop.type}`,\n ),\n examples: (value.variants ?? []).map((variant) => ({\n name: variant.name,\n description: variant.description,\n code: variant.code,\n })),\n relations: (value.relations ?? []).map((relation) => ({\n componentName: relation.component,\n relationship: relation.relationship,\n note: relation.note,\n })),\n compoundChildren: [],\n guidance: {\n when: value.usage?.when ?? [],\n whenNot: value.usage?.whenNot ?? [],\n guidelines: value.usage?.guidelines ?? [],\n accessibility: value.usage?.accessibility ?? [],\n dos: value.usage?.when ?? [],\n donts: value.usage?.whenNot ?? [],\n patterns: [],\n },\n sourceType: 'fragments-json',\n sourcePath: value.sourcePath ?? value.filePath,\n metadata: {\n a11yRules: value.contract?.a11yRules ?? [],\n scenarioTags: value.contract?.scenarioTags ?? [],\n },\n };\n}\n\nexport function buildComponentIndex(\n fragments: Array<McpComponent | CompiledFragment>,\n): ComponentIndex {\n const db = create({ schema: componentSchema, language: 'english' });\n const normalized = fragments.map(normalizeComponent);\n\n const docs = normalized.map((f) => ({\n name: f.name,\n description: f.description ?? '',\n category: f.category ?? '',\n tags: (f.tags ?? []).join(' '),\n whenUsed: (f.guidance.when ?? []).join(' '),\n patterns: (f.guidance.patterns ?? [])\n .map(\n (pattern: McpComponent['guidance']['patterns'][number]) =>\n `${pattern.name} ${pattern.description || ''}`,\n )\n .join(' '),\n variants: f.examples\n .map(\n (example: McpComponent['examples'][number]) =>\n `${example.name} ${example.description || ''}`,\n )\n .join(' '),\n status: f.status ?? 'stable',\n }));\n\n insertMultiple(db, docs);\n return db;\n}\n\nexport function searchComponents(\n query: string,\n index: ComponentIndex,\n fragments: Array<McpComponent | CompiledFragment>,\n limit = 50,\n): ScoredResult[] {\n const normalized = fragments.map(normalizeComponent);\n const boostConfig = {\n mode: 'fulltext' as const,\n properties: ['name', 'whenUsed', 'description', 'patterns', 'category', 'tags', 'variants'],\n boost: {\n name: 3,\n whenUsed: 2.5,\n description: 2,\n patterns: 1.5,\n category: 1.5,\n tags: 1.5,\n variants: 1,\n },\n limit,\n };\n\n // Two-pass search: original terms weighted 2x, synonym terms 1x\n // This prevents synonym noise (\"box\", \"container\") from outranking truly relevant results\n const originalTermsList = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const originalTermsQuery = originalTermsList.join(' ');\n const expandedQuery = expandQuery(query);\n\n // First pass: original terms, moderate threshold; second pass: synonyms, permissive\n const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });\n const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });\n\n type Hit = { score: number; document: { name: string } };\n const origHits = (originalResults as unknown as { hits: Hit[] }).hits;\n const expHits = (expandedResults as unknown as { hits: Hit[] }).hits;\n\n // Combine scores: original terms get 2x weight\n const scoreMap = new Map<string, number>();\n for (const hit of origHits) {\n scoreMap.set(hit.document.name, (hit.score || 0) * 2);\n }\n for (const hit of expHits) {\n const name = hit.document.name;\n const existing = scoreMap.get(name) ?? 0;\n scoreMap.set(name, existing + (hit.score || 0));\n }\n\n // Build a name→fragment lookup for status bonuses\n const fragmentMap = new Map<string, McpComponent>();\n for (const f of normalized) {\n fragmentMap.set(f.name.toLowerCase(), f);\n }\n\n // Exact name match bonus: if a component name matches an original query term exactly,\n // boost it above similar-named components (e.g., \"Button\" > \"ButtonGroup\" for \"button action\")\n const originalTermsSet = new Set(originalTermsList);\n\n // Build scored results with status + name match adjustments\n const scored: ScoredResult[] = [];\n for (const [name, rawScore] of scoreMap) {\n let score = rawScore;\n const nameLower = name.toLowerCase();\n const fragment = fragmentMap.get(nameLower);\n\n // Exact name match bonus — strong signal that this specific component was requested\n // Must be large enough to overcome BM25 score differences from compound names\n // (e.g., \"ButtonGroup\" matching more fields than \"Button\")\n if (originalTermsSet.has(nameLower)) {\n score += 25;\n }\n\n // Apply status bonus/penalty (matches old scorer)\n if (fragment) {\n if (fragment.status === 'stable') score += 5;\n else if (fragment.status === 'beta') score += 2;\n if (fragment.status === 'deprecated') score -= 25;\n }\n\n if (score > 0) {\n scored.push({ name, kind: 'component' as EntryKind, rank: scored.length, score });\n }\n }\n\n // Sort by score descending and assign ranks\n scored.sort((a, b) => b.score - a.score);\n scored.forEach((s, i) => { s.rank = i; });\n\n return scored;\n}\n\n// ---------------------------------------------------------------------------\n// Block index\n// ---------------------------------------------------------------------------\n\nconst blockSchema = {\n name: 'string' as const,\n description: 'string' as const,\n category: 'string' as const,\n tags: 'string' as const,\n components: 'string' as const,\n};\n\nexport type BlockIndex = AnyOrama;\n\nfunction normalizeBlock(value: McpBlock | CompiledBlock): McpBlock {\n if ('id' in value) return value;\n return {\n id: value.filePath ?? value.name,\n name: value.name,\n description: value.description ?? '',\n category: value.category ?? 'uncategorized',\n components: value.components ?? [],\n tags: value.tags ?? [],\n code: value.code ?? '',\n };\n}\n\nexport function buildBlockIndex(\n blocks: Array<McpBlock | CompiledBlock>,\n): BlockIndex {\n const db = create({ schema: blockSchema, language: 'english' });\n const normalized = blocks.map(normalizeBlock);\n\n const docs = normalized.map((b) => ({\n name: b.name,\n description: b.description ?? '',\n category: b.category ?? '',\n tags: (b.tags ?? []).join(' '),\n components: b.components.join(' '),\n }));\n\n insertMultiple(db, docs);\n return db;\n}\n\nexport function searchBlocks(\n query: string,\n index: BlockIndex,\n limit = 50,\n): ScoredResult[] {\n return twoPassSearch({\n index,\n query,\n properties: ['name', 'description', 'components', 'tags', 'category'],\n boost: {\n name: 3,\n description: 2,\n components: 1.5,\n tags: 1.5,\n category: 1.5,\n },\n limit,\n kind: 'block' as EntryKind,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Token index\n// ---------------------------------------------------------------------------\n\nconst tokenSchema = {\n name: 'string' as const,\n category: 'string' as const,\n description: 'string' as const,\n};\n\nexport type TokenIndex = AnyOrama;\n\nfunction normalizeTokenData(\n tokenData: McpTokenData | CompiledTokenData,\n): McpTokenData {\n if ('flat' in tokenData) return tokenData;\n const categories = Object.fromEntries(\n Object.entries(tokenData.categories).map(([category, entries]) => [\n category,\n entries.map((entry) => ({\n name: entry.name,\n category,\n value: typeof entry.value === 'string' ? entry.value : undefined,\n description: entry.description,\n })),\n ]),\n );\n return {\n prefix: tokenData.prefix,\n total: tokenData.total,\n categories,\n flat: Object.values(categories).flat(),\n };\n}\n\nexport function buildTokenIndex(\n tokenData: McpTokenData | CompiledTokenData,\n): TokenIndex {\n const db = create({ schema: tokenSchema, language: 'english' });\n const normalizedData = normalizeTokenData(tokenData);\n\n const docs: Array<{ name: string; category: string; description: string }> = [];\n for (const [cat, tokens] of Object.entries(normalizedData.categories) as Array<\n [string, McpTokenData['categories'][string]]\n >) {\n for (const token of tokens) {\n docs.push({\n name: token.name,\n category: cat,\n description: token.description ?? '',\n });\n }\n }\n\n insertMultiple(db, docs);\n return db;\n}\n\nexport function searchTokens(\n query: string,\n index: TokenIndex,\n limit = 50,\n): ScoredResult[] {\n return twoPassSearch({\n index,\n query,\n properties: ['name', 'category', 'description'],\n boost: {\n name: 2.5,\n category: 2,\n description: 1.5,\n },\n limit,\n kind: 'token' as EntryKind,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Semantic category extraction for implement tool token matching\n// ---------------------------------------------------------------------------\n\n/** Maps use-case keywords to relevant token categories */\nexport const USE_CASE_TOKEN_CATEGORIES: Record<string, string[]> = {\n 'table': ['spacing', 'borders', 'surfaces', 'text'],\n 'data': ['spacing', 'borders', 'surfaces'],\n 'grid': ['spacing', 'layout'],\n 'form': ['spacing', 'borders', 'radius', 'focus'],\n 'input': ['spacing', 'borders', 'radius', 'focus'],\n 'card': ['surfaces', 'shadows', 'radius', 'borders', 'spacing'],\n 'button': ['colors', 'radius', 'spacing', 'focus'],\n 'layout': ['spacing', 'layout', 'surfaces'],\n 'dashboard': ['spacing', 'surfaces', 'borders', 'shadows'],\n 'chat': ['spacing', 'surfaces', 'radius', 'shadows'],\n 'modal': ['shadows', 'surfaces', 'radius', 'spacing'],\n 'dialog': ['shadows', 'surfaces', 'radius', 'spacing'],\n 'navigation': ['spacing', 'surfaces', 'borders'],\n 'sidebar': ['spacing', 'surfaces', 'borders'],\n 'hero': ['spacing', 'typography', 'colors'],\n 'landing': ['spacing', 'typography', 'colors'],\n 'pricing': ['spacing', 'surfaces', 'borders', 'radius'],\n 'auth': ['spacing', 'borders', 'radius', 'focus'],\n 'login': ['spacing', 'borders', 'radius', 'focus'],\n 'dark': ['colors', 'surfaces'],\n 'theme': ['colors', 'surfaces', 'text'],\n};\n\n/**\n * Extract relevant token categories from a use-case query.\n * Returns category names that should be included in implement results.\n */\nexport function extractTokenCategories(query: string): string[] {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const categories = new Set<string>();\n\n for (const term of terms) {\n const cats = USE_CASE_TOKEN_CATEGORIES[term];\n if (cats) {\n for (const cat of cats) categories.add(cat);\n }\n }\n\n // If nothing matched, return common defaults\n if (categories.size === 0) {\n return ['spacing', 'colors', 'surfaces'];\n }\n\n return Array.from(categories);\n}\n","/**\n * Scoring utilities for MCP tool result ranking.\n *\n * Extracted as pure functions for easy testing and reuse across\n * discover, implement, and other search-driven tools.\n */\n\nimport type { ScoredResult } from './search.js';\n\n// ---------------------------------------------------------------------------\n// Confidence assignment — ratio-to-max (scale-independent)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimum absolute score floor. If the best result scores below this,\n * results are considered a no-match rather than low-confidence garbage.\n *\n * BM25 scores for a 60-component corpus typically range 10-100+ for real\n * matches (including name-match bonuses of +25 and status bonuses of +5).\n * A maxScore below 5 means no meaningful term overlap was found — only\n * status bonuses and/or minimal synonym noise.\n *\n * Note: this threshold is calibrated for BM25 keyword scores. When hybrid\n * search uses RRF fusion (premium API key), scores are in a different range\n * (~0.01-0.05) and the threshold should be bypassed via a maxScore > 1 guard.\n */\nexport const MINIMUM_SCORE_THRESHOLD = 5;\n\n/**\n * Assign a confidence level based on how a score compares to the best result.\n *\n * Uses ratio-to-max instead of absolute thresholds so the same function works\n * for both keyword scores (8-50+) and RRF scores (0.01-0.02).\n *\n * - high: score >= 70% of maxScore (\"this is almost certainly what you need\")\n * - medium: score >= 40% of maxScore (\"this could be useful\")\n * - low: score < 40% of maxScore (\"tangentially related\")\n */\nexport function assignConfidence(\n score: number,\n maxScore: number,\n): 'high' | 'medium' | 'low' {\n if (maxScore <= 0) return 'low';\n const ratio = score / maxScore;\n if (ratio >= 0.7) return 'high';\n if (ratio >= 0.4) return 'medium';\n return 'low';\n}\n\n/**\n * Check if the best score in a result set meets the minimum quality threshold.\n * Returns true if results are meaningful, false if they're noise.\n */\nexport function meetsMinimumThreshold(maxScore: number): boolean {\n return maxScore >= MINIMUM_SCORE_THRESHOLD;\n}\n\n// ---------------------------------------------------------------------------\n// Levenshtein distance — for fuzzy \"did you mean?\" suggestions\n// ---------------------------------------------------------------------------\n\n/**\n * Compute the Levenshtein edit distance between two strings.\n * Used for suggesting closest component name matches on typos.\n */\nexport function levenshtein(a: string, b: string): number {\n const la = a.length;\n const lb = b.length;\n const dp: number[] = Array.from({ length: lb + 1 }, (_, i) => i);\n\n for (let i = 1; i <= la; i++) {\n let prev = i - 1;\n dp[0] = i;\n for (let j = 1; j <= lb; j++) {\n const temp = dp[j];\n dp[j] = a[i - 1] === b[j - 1]\n ? prev\n : 1 + Math.min(prev, dp[j], dp[j - 1]);\n prev = temp;\n }\n }\n\n return dp[lb];\n}\n\n/**\n * Find the closest matching name from a list, using Levenshtein distance.\n * Returns the closest match if it's within `maxDistance`, or null otherwise.\n */\nexport function findClosestMatch(\n input: string,\n candidates: string[],\n maxDistance = 3,\n): string | null {\n const inputLower = input.toLowerCase();\n let bestMatch: string | null = null;\n let bestDist = maxDistance + 1;\n\n for (const candidate of candidates) {\n const candidateLower = candidate.toLowerCase();\n const dist = levenshtein(inputLower, candidateLower);\n if (dist < bestDist) {\n bestDist = dist;\n bestMatch = candidate;\n } else if (dist === bestDist && bestMatch) {\n // Tie-break: prefer the candidate whose length is closer to input length\n const currentLenDiff = Math.abs(bestMatch.length - input.length);\n const newLenDiff = Math.abs(candidate.length - input.length);\n if (newLenDiff < currentLenDiff) {\n bestMatch = candidate;\n }\n }\n }\n\n return bestDist <= maxDistance ? bestMatch : null;\n}\n\n// ---------------------------------------------------------------------------\n// Block-first component ranking\n// ---------------------------------------------------------------------------\n\n/** Points added per block occurrence (e.g., Card in 3 blocks → +15) */\nexport const BLOCK_BOOST_PER_OCCURRENCE = 5;\n\n/**\n * Count how many blocks each component appears in.\n *\n * Returns a Map<lowercase component name, count>.\n * This is the core signal for block-first ranking: if Card appears in 5/5\n * marketing blocks, it's clearly more important than Sidebar which appears in 0.\n */\nexport function buildBlockComponentFrequency(\n blocks: Array<{ components: string[] }>,\n): Map<string, number> {\n const freq = new Map<string, number>();\n for (const block of blocks) {\n for (const comp of block.components) {\n const key = comp.toLowerCase();\n freq.set(key, (freq.get(key) ?? 0) + 1);\n }\n }\n return freq;\n}\n\n/**\n * Boost component search results by how frequently they appear in matching blocks.\n *\n * Mutates scores in-place and re-sorts. Components that appear in more blocks\n * get a proportionally higher boost (freq * BLOCK_BOOST_PER_OCCURRENCE).\n *\n * Returns the mutated+sorted array for convenience.\n */\nexport function boostByBlockFrequency(\n results: ScoredResult[],\n freq: Map<string, number>,\n): ScoredResult[] {\n for (const result of results) {\n const count = freq.get(result.name.toLowerCase()) ?? 0;\n if (count > 0) {\n result.score += count * BLOCK_BOOST_PER_OCCURRENCE;\n }\n }\n results.sort((a, b) => b.score - a.score);\n results.forEach((r, i) => { r.rank = i; });\n return results;\n}\n","import { SYNONYM_MAP, USE_CASE_TOKEN_CATEGORIES } from './orama-index.js';\nimport { MINIMUM_SCORE_THRESHOLD, BLOCK_BOOST_PER_OCCURRENCE } from './scoring.js';\n\nexport interface SearchConfig {\n synonyms?: Record<string, string[]>;\n useCaseTokenCategories?: Record<string, string[]>;\n minimumScoreThreshold?: number;\n blockBoostPerOccurrence?: number;\n vectorSearchUrl?: string;\n vectorSearchTimeoutMs?: number;\n}\n\nexport const DEFAULT_SEARCH_CONFIG: Required<SearchConfig> = {\n synonyms: SYNONYM_MAP,\n useCaseTokenCategories: USE_CASE_TOKEN_CATEGORIES,\n minimumScoreThreshold: MINIMUM_SCORE_THRESHOLD,\n blockBoostPerOccurrence: BLOCK_BOOST_PER_OCCURRENCE,\n vectorSearchUrl: 'https://combative-jay-834.convex.site/search',\n vectorSearchTimeoutMs: 3000,\n};\n","// Server\nexport { createMcpServer, startMcpServer, createSandboxServer } from './server.js';\n\n// Types\nexport type { McpServerConfig, DesignSystemData, ToolHandler, ToolResult, ToolContext, SearchIndexes } from './types.js';\n\n// Registry\nexport { ToolRegistry } from './registry.js';\nexport type { ToolAvailability, AvailabilityContext } from './registry.js';\n\n// Middleware\nexport { executeWithMiddleware, telemetryMiddleware } from './middleware.js';\nexport type { Middleware, MiddlewareContext, MiddlewareNext } from './middleware.js';\n\n// Adapters\nexport type { DataAdapter } from './adapters/types.js';\nexport { FragmentsJsonAdapter } from './adapters/fragments-json.js';\nexport { CustomJsonAdapter } from './adapters/custom-json.js';\nexport { AutoExtractionAdapter } from './adapters/auto-extract.js';\n\n// Config\nexport { loadConfigFile } from './config.js';\nexport type { DsMcpConfigFile } from './config.js';\n\n// Rules generation\nexport { generateRulesFiles } from './rules.js';\nexport type { RulesFormat, RulesGeneratorOptions } from './rules.js';\n\n// Search config\nexport type { SearchConfig } from './search-config.js';\nexport { DEFAULT_SEARCH_CONFIG } from './search-config.js';\n\n// Tool sets\nexport { BUILTIN_TOOLS, CORE_TOOLS, VIEWER_TOOLS, INFRA_TOOLS } from './tools/index.js';\n\n// Constants\nexport { BRAND } from '@fragments-sdk/core';\nexport type { Brand } from '@fragments-sdk/core';\n"],"mappings":"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","names":["snapshot"]}
|
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragments-sdk/mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"license": "FSL-1.1-MIT",
|
|
5
5
|
"description": "Standalone MCP validator for Fragments design systems",
|
|
6
6
|
"mcpName": "io.github.ConanMcN/fragments-mcp",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
63
63
|
"@orama/orama": "^3.1.18",
|
|
64
64
|
"typescript": "^5.7.2",
|
|
65
|
-
"@fragments-sdk/classifier": "0.2.
|
|
66
|
-
"@fragments-sdk/core": "3.
|
|
67
|
-
"@fragments-sdk/context": "0.8.
|
|
65
|
+
"@fragments-sdk/classifier": "0.2.1",
|
|
66
|
+
"@fragments-sdk/core": "3.2.0",
|
|
67
|
+
"@fragments-sdk/context": "0.8.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/node": "^22.0.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"tsup": "^8.3.5",
|
|
73
73
|
"tsx": "^4.19.0",
|
|
74
74
|
"vitest": "^2.1.8",
|
|
75
|
-
"@fragments-sdk/extract": "0.2.
|
|
75
|
+
"@fragments-sdk/extract": "0.2.1"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"build": "tsup",
|
package/dist/chunk-4SVS3AA3.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// src/constants.ts
|
|
2
|
-
var BRAND = {
|
|
3
|
-
/** Display name (e.g., "Fragments") */
|
|
4
|
-
name: "Fragments",
|
|
5
|
-
/** Lowercase name for file paths and CLI (e.g., "fragments") */
|
|
6
|
-
nameLower: "fragments",
|
|
7
|
-
/** File extension for fragment definition files — V2 canonical format */
|
|
8
|
-
fileExtension: ".contract.json",
|
|
9
|
-
/** Legacy file extension for segments (still supported for migration) */
|
|
10
|
-
legacyFileExtension: ".segment.tsx",
|
|
11
|
-
/** JSON file extension for compiled output */
|
|
12
|
-
jsonExtension: ".fragment.json",
|
|
13
|
-
/** Default output file name (e.g., "fragments.json") */
|
|
14
|
-
outFile: "fragments.json",
|
|
15
|
-
/** Config file name (e.g., "fragments.config.ts") */
|
|
16
|
-
configFile: "fragments.config.ts",
|
|
17
|
-
/** Legacy config file name (still supported for migration) */
|
|
18
|
-
legacyConfigFile: "segments.config.ts",
|
|
19
|
-
/** CLI command name (e.g., "fragments") */
|
|
20
|
-
cliCommand: "fragments",
|
|
21
|
-
/** Package scope (e.g., "@fragments") */
|
|
22
|
-
packageScope: "@fragments",
|
|
23
|
-
/** Directory for storing fragments, registry, and cache */
|
|
24
|
-
dataDir: ".fragments",
|
|
25
|
-
/** Components subdirectory within .fragments/ */
|
|
26
|
-
componentsDir: "components",
|
|
27
|
-
/** Registry file name */
|
|
28
|
-
registryFile: "registry.json",
|
|
29
|
-
/** Context file name (AI-ready markdown) */
|
|
30
|
-
contextFile: "context.md",
|
|
31
|
-
/** Screenshots subdirectory */
|
|
32
|
-
screenshotsDir: "screenshots",
|
|
33
|
-
/** Cache subdirectory (gitignored) */
|
|
34
|
-
cacheDir: "cache",
|
|
35
|
-
/** Diff output subdirectory (gitignored) */
|
|
36
|
-
diffDir: "diff",
|
|
37
|
-
/** Manifest filename */
|
|
38
|
-
manifestFile: "manifest.json",
|
|
39
|
-
/** Prefix for localStorage keys (e.g., "fragments-") */
|
|
40
|
-
storagePrefix: "fragments-",
|
|
41
|
-
/** Static viewer HTML file name */
|
|
42
|
-
viewerHtmlFile: "fragments-viewer.html",
|
|
43
|
-
/** MCP tool name prefix (e.g., "fragments_") */
|
|
44
|
-
mcpToolPrefix: "fragments_",
|
|
45
|
-
/** File extension for block definition files */
|
|
46
|
-
blockFileExtension: ".block.ts",
|
|
47
|
-
/** @deprecated Use blockFileExtension instead */
|
|
48
|
-
recipeFileExtension: ".recipe.ts",
|
|
49
|
-
/** Vite plugin namespace */
|
|
50
|
-
vitePluginNamespace: "fragments-core-shim"
|
|
51
|
-
};
|
|
52
|
-
var DEFAULTS = {
|
|
53
|
-
/** Default viewport dimensions */
|
|
54
|
-
viewport: {
|
|
55
|
-
width: 1280,
|
|
56
|
-
height: 800
|
|
57
|
-
},
|
|
58
|
-
/** Default diff threshold (percentage) */
|
|
59
|
-
diffThreshold: 5,
|
|
60
|
-
/** Browser pool size */
|
|
61
|
-
poolSize: 3,
|
|
62
|
-
/** Idle timeout before browser shutdown (ms) - 5 minutes */
|
|
63
|
-
idleTimeoutMs: 5 * 60 * 1e3,
|
|
64
|
-
/** Delay after render before capture (ms) */
|
|
65
|
-
captureDelayMs: 100,
|
|
66
|
-
/** Font loading timeout (ms) */
|
|
67
|
-
fontTimeoutMs: 3e3,
|
|
68
|
-
/** Default theme */
|
|
69
|
-
theme: "light",
|
|
70
|
-
/** Dev server port */
|
|
71
|
-
port: 6006
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export {
|
|
75
|
-
BRAND,
|
|
76
|
-
DEFAULTS
|
|
77
|
-
};
|
|
78
|
-
//# sourceMappingURL=chunk-4SVS3AA3.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Brand constants for easy rebranding if domain availability requires it.\n * All naming throughout the codebase should reference these constants.\n *\n * Inlined from @fragments-sdk/cli to avoid pulling in the full CLI dependency.\n */\nexport const BRAND = {\n /** Display name (e.g., \"Fragments\") */\n name: \"Fragments\",\n\n /** Lowercase name for file paths and CLI (e.g., \"fragments\") */\n nameLower: \"fragments\",\n\n /** File extension for fragment definition files — V2 canonical format */\n fileExtension: \".contract.json\",\n\n /** Legacy file extension for segments (still supported for migration) */\n legacyFileExtension: \".segment.tsx\",\n\n /** JSON file extension for compiled output */\n jsonExtension: \".fragment.json\",\n\n /** Default output file name (e.g., \"fragments.json\") */\n outFile: \"fragments.json\",\n\n /** Config file name (e.g., \"fragments.config.ts\") */\n configFile: \"fragments.config.ts\",\n\n /** Legacy config file name (still supported for migration) */\n legacyConfigFile: \"segments.config.ts\",\n\n /** CLI command name (e.g., \"fragments\") */\n cliCommand: \"fragments\",\n\n /** Package scope (e.g., \"@fragments\") */\n packageScope: \"@fragments\",\n\n /** Directory for storing fragments, registry, and cache */\n dataDir: \".fragments\",\n\n /** Components subdirectory within .fragments/ */\n componentsDir: \"components\",\n\n /** Registry file name */\n registryFile: \"registry.json\",\n\n /** Context file name (AI-ready markdown) */\n contextFile: \"context.md\",\n\n /** Screenshots subdirectory */\n screenshotsDir: \"screenshots\",\n\n /** Cache subdirectory (gitignored) */\n cacheDir: \"cache\",\n\n /** Diff output subdirectory (gitignored) */\n diffDir: \"diff\",\n\n /** Manifest filename */\n manifestFile: \"manifest.json\",\n\n /** Prefix for localStorage keys (e.g., \"fragments-\") */\n storagePrefix: \"fragments-\",\n\n /** Static viewer HTML file name */\n viewerHtmlFile: \"fragments-viewer.html\",\n\n /** MCP tool name prefix (e.g., \"fragments_\") */\n mcpToolPrefix: \"fragments_\",\n\n /** File extension for block definition files */\n blockFileExtension: \".block.ts\",\n\n /** @deprecated Use blockFileExtension instead */\n recipeFileExtension: \".recipe.ts\",\n\n /** Vite plugin namespace */\n vitePluginNamespace: \"fragments-core-shim\",\n} as const;\n\nexport type Brand = typeof BRAND;\n\n/**\n * Default configuration values for the service.\n *\n * Inlined from @fragments-sdk/cli to avoid pulling in the full CLI dependency.\n */\nexport const DEFAULTS = {\n /** Default viewport dimensions */\n viewport: {\n width: 1280,\n height: 800,\n },\n\n /** Default diff threshold (percentage) */\n diffThreshold: 5,\n\n /** Browser pool size */\n poolSize: 3,\n\n /** Idle timeout before browser shutdown (ms) - 5 minutes */\n idleTimeoutMs: 5 * 60 * 1000,\n\n /** Delay after render before capture (ms) */\n captureDelayMs: 100,\n\n /** Font loading timeout (ms) */\n fontTimeoutMs: 3000,\n\n /** Default theme */\n theme: \"light\" as const,\n\n /** Dev server port */\n port: 6006,\n} as const;\n\nexport type Defaults = typeof DEFAULTS;\n"],"mappings":";AAMO,IAAM,QAAQ;AAAA;AAAA,EAEnB,MAAM;AAAA;AAAA,EAGN,WAAW;AAAA;AAAA,EAGX,eAAe;AAAA;AAAA,EAGf,qBAAqB;AAAA;AAAA,EAGrB,eAAe;AAAA;AAAA,EAGf,SAAS;AAAA;AAAA,EAGT,YAAY;AAAA;AAAA,EAGZ,kBAAkB;AAAA;AAAA,EAGlB,YAAY;AAAA;AAAA,EAGZ,cAAc;AAAA;AAAA,EAGd,SAAS;AAAA;AAAA,EAGT,eAAe;AAAA;AAAA,EAGf,cAAc;AAAA;AAAA,EAGd,aAAa;AAAA;AAAA,EAGb,gBAAgB;AAAA;AAAA,EAGhB,UAAU;AAAA;AAAA,EAGV,SAAS;AAAA;AAAA,EAGT,cAAc;AAAA;AAAA,EAGd,eAAe;AAAA;AAAA,EAGf,gBAAgB;AAAA;AAAA,EAGhB,eAAe;AAAA;AAAA,EAGf,oBAAoB;AAAA;AAAA,EAGpB,qBAAqB;AAAA;AAAA,EAGrB,qBAAqB;AACvB;AASO,IAAM,WAAW;AAAA;AAAA,EAEtB,UAAU;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,eAAe;AAAA;AAAA,EAGf,UAAU;AAAA;AAAA,EAGV,eAAe,IAAI,KAAK;AAAA;AAAA,EAGxB,gBAAgB;AAAA;AAAA,EAGhB,eAAe;AAAA;AAAA,EAGf,OAAO;AAAA;AAAA,EAGP,MAAM;AACR;","names":[]}
|