sweet-search 0.0.1 → 2.3.0

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 (161) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +23 -0
  3. package/core/cli.js +51 -0
  4. package/core/config.js +27 -0
  5. package/core/embedding/embedding-cache.js +467 -0
  6. package/core/embedding/embedding-local-model.js +845 -0
  7. package/core/embedding/embedding-remote.js +492 -0
  8. package/core/embedding/embedding-service.js +712 -0
  9. package/core/embedding/embedding-telemetry.js +219 -0
  10. package/core/embedding/index.js +40 -0
  11. package/core/graph/community-detector.js +294 -0
  12. package/core/graph/graph-expansion.js +839 -0
  13. package/core/graph/graph-extractor.js +2304 -0
  14. package/core/graph/graph-search.js +2148 -0
  15. package/core/graph/hcgs-generator.js +666 -0
  16. package/core/graph/index.js +16 -0
  17. package/core/graph/leiden-algorithm.js +547 -0
  18. package/core/graph/relationship-resolver.js +366 -0
  19. package/core/graph/repo-map.js +408 -0
  20. package/core/graph/summary-manager.js +549 -0
  21. package/core/indexing/artifact-builder.js +1054 -0
  22. package/core/indexing/ast-chunker.js +709 -0
  23. package/core/indexing/chunking/chunk-builder.js +170 -0
  24. package/core/indexing/chunking/markdown-chunker.js +503 -0
  25. package/core/indexing/chunking/plaintext-chunker.js +104 -0
  26. package/core/indexing/dedup/dedup-phase.js +159 -0
  27. package/core/indexing/dedup/exemplar-selector.js +65 -0
  28. package/core/indexing/document-chunker.js +56 -0
  29. package/core/indexing/incremental-parser.js +390 -0
  30. package/core/indexing/incremental-tracker.js +761 -0
  31. package/core/indexing/index-codebase-v21.js +472 -0
  32. package/core/indexing/index-maintainer.mjs +1674 -0
  33. package/core/indexing/index.js +90 -0
  34. package/core/indexing/indexer-ann.js +1077 -0
  35. package/core/indexing/indexer-build.js +742 -0
  36. package/core/indexing/indexer-phases.js +800 -0
  37. package/core/indexing/indexer-pool.js +764 -0
  38. package/core/indexing/indexer-sparse-gram.js +98 -0
  39. package/core/indexing/indexer-utils.js +536 -0
  40. package/core/indexing/indexer-worker.js +148 -0
  41. package/core/indexing/li-skip-policy.js +225 -0
  42. package/core/indexing/merkle-tracker.js +244 -0
  43. package/core/indexing/model-pool.js +166 -0
  44. package/core/infrastructure/code-graph-repository.js +120 -0
  45. package/core/infrastructure/codebase-repository.js +131 -0
  46. package/core/infrastructure/config/dedup.js +54 -0
  47. package/core/infrastructure/config/embedding.js +298 -0
  48. package/core/infrastructure/config/graph.js +80 -0
  49. package/core/infrastructure/config/index.js +82 -0
  50. package/core/infrastructure/config/indexing.js +8 -0
  51. package/core/infrastructure/config/platform.js +254 -0
  52. package/core/infrastructure/config/ranking.js +221 -0
  53. package/core/infrastructure/config/search.js +396 -0
  54. package/core/infrastructure/config/translation.js +89 -0
  55. package/core/infrastructure/config/vector-store.js +114 -0
  56. package/core/infrastructure/constants.js +86 -0
  57. package/core/infrastructure/coreml-cascade.js +909 -0
  58. package/core/infrastructure/coreml-cascade.json +46 -0
  59. package/core/infrastructure/coreml-provider.js +81 -0
  60. package/core/infrastructure/db-utils.js +69 -0
  61. package/core/infrastructure/dedup-hashing.js +83 -0
  62. package/core/infrastructure/hardware-capability.js +332 -0
  63. package/core/infrastructure/index.js +104 -0
  64. package/core/infrastructure/language-patterns/maps.js +121 -0
  65. package/core/infrastructure/language-patterns/registry-core.js +323 -0
  66. package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
  67. package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
  68. package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
  69. package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
  70. package/core/infrastructure/language-patterns/registry.js +19 -0
  71. package/core/infrastructure/language-patterns.js +141 -0
  72. package/core/infrastructure/llm-provider.js +733 -0
  73. package/core/infrastructure/manifest.json +46 -0
  74. package/core/infrastructure/maxsim.wasm +0 -0
  75. package/core/infrastructure/model-fetcher.js +423 -0
  76. package/core/infrastructure/model-registry.js +214 -0
  77. package/core/infrastructure/native-inference.js +587 -0
  78. package/core/infrastructure/native-resolver.js +187 -0
  79. package/core/infrastructure/native-sparse-gram.js +257 -0
  80. package/core/infrastructure/native-tokenizer.js +160 -0
  81. package/core/infrastructure/onnx-mutex.js +45 -0
  82. package/core/infrastructure/onnx-session-utils.js +261 -0
  83. package/core/infrastructure/ort-pipeline.js +111 -0
  84. package/core/infrastructure/project-detector.js +102 -0
  85. package/core/infrastructure/quantization.js +410 -0
  86. package/core/infrastructure/simd-distance.js +502 -0
  87. package/core/infrastructure/simd-distance.wasm +0 -0
  88. package/core/infrastructure/tree-sitter-provider.js +665 -0
  89. package/core/infrastructure/webgpu-maxsim.js +222 -0
  90. package/core/query/index.js +35 -0
  91. package/core/query/intent-detector.js +201 -0
  92. package/core/query/intent-router.js +156 -0
  93. package/core/query/query-router-catboost.js +222 -0
  94. package/core/query/query-router-ml.js +266 -0
  95. package/core/query/query-router.js +213 -0
  96. package/core/ranking/cascaded-scorer.js +379 -0
  97. package/core/ranking/flashrank.js +810 -0
  98. package/core/ranking/index.js +49 -0
  99. package/core/ranking/late-interaction-index.js +2383 -0
  100. package/core/ranking/late-interaction-model.js +812 -0
  101. package/core/ranking/local-reranker.js +374 -0
  102. package/core/ranking/mmr.js +379 -0
  103. package/core/ranking/quality-scorer.js +363 -0
  104. package/core/search/context-expander.js +1167 -0
  105. package/core/search/dedup/sibling-expander.js +327 -0
  106. package/core/search/index.js +16 -0
  107. package/core/search/search-boost.js +259 -0
  108. package/core/search/search-cli.js +544 -0
  109. package/core/search/search-format.js +282 -0
  110. package/core/search/search-fusion.js +327 -0
  111. package/core/search/search-hybrid.js +204 -0
  112. package/core/search/search-pattern-chunks.js +337 -0
  113. package/core/search/search-pattern-planner.js +439 -0
  114. package/core/search/search-pattern-prefilter.js +412 -0
  115. package/core/search/search-pattern-ripgrep.js +663 -0
  116. package/core/search/search-pattern.js +463 -0
  117. package/core/search/search-postprocess.js +452 -0
  118. package/core/search/search-semantic.js +706 -0
  119. package/core/search/search-server.js +554 -0
  120. package/core/search/session-daemon-prewarm.mjs +164 -0
  121. package/core/search/session-warmup.js +595 -0
  122. package/core/search/sweet-search.js +632 -0
  123. package/core/search/warmup-metrics.js +532 -0
  124. package/core/start-server.js +6 -0
  125. package/core/training/query-router/features/extractor.js +762 -0
  126. package/core/training/query-router/features/multilingual-patterns.js +431 -0
  127. package/core/training/query-router/features/text-segmenter.js +303 -0
  128. package/core/training/query-router/features/unicode-utils.js +383 -0
  129. package/core/training/query-router/output/v45_router_d4.js +11521 -0
  130. package/core/training/query-router/output/v46_router_d4.js +11498 -0
  131. package/core/vector-store/binary-heap.js +227 -0
  132. package/core/vector-store/binary-hnsw-index.js +1004 -0
  133. package/core/vector-store/float-vector-store.js +234 -0
  134. package/core/vector-store/hnsw-index.js +580 -0
  135. package/core/vector-store/index.js +39 -0
  136. package/core/vector-store/seismic-index.js +498 -0
  137. package/core/vocabulary/index.js +84 -0
  138. package/core/vocabulary/vocab-constants.js +20 -0
  139. package/core/vocabulary/vocab-miner-extractors.js +375 -0
  140. package/core/vocabulary/vocab-miner-nl.js +404 -0
  141. package/core/vocabulary/vocab-miner-utils.js +146 -0
  142. package/core/vocabulary/vocab-miner.js +574 -0
  143. package/core/vocabulary/vocab-prewarm-cli.js +110 -0
  144. package/core/vocabulary/vocab-ranker.js +492 -0
  145. package/core/vocabulary/vocab-warmer.js +523 -0
  146. package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
  147. package/core/vocabulary/vocabulary-utils.js +704 -0
  148. package/crates/wasm-router/pkg/package.json +13 -0
  149. package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
  150. package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
  151. package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
  152. package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
  153. package/mcp/config-gen.js +121 -0
  154. package/mcp/server.js +335 -0
  155. package/mcp/tool-handlers.js +476 -0
  156. package/package.json +131 -9
  157. package/scripts/benchmark-harness.js +794 -0
  158. package/scripts/init.js +1058 -0
  159. package/scripts/smoke-test.js +435 -0
  160. package/scripts/uninstall.js +478 -0
  161. package/scripts/verify-runtime.js +176 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * CatBoost Query Router - Production ML Router with Reject Option
3
+ *
4
+ * This router uses the trained CatBoost model (v45_depth4) with proper
5
+ * uncertainty-based reject option to improve utility accuracy.
6
+ *
7
+ * Performance: ~50μs inference (vs 1.8μs ID3, but higher accuracy)
8
+ * Utility Accuracy: 98%+ on 255-query benchmark (3-class, with reject option)
9
+ */
10
+
11
+ // Import the trained CatBoost router
12
+ import { routeQueryCatBoost, LABELS } from '../training/query-router/output/v46_router_d4.js';
13
+
14
+ // Import the feature extractor (must match training features)
15
+ import { extractAllFeatures } from '../training/query-router/features/extractor.js';
16
+
17
+ // =============================================================================
18
+ // REJECT OPTION CONFIGURATION
19
+ // =============================================================================
20
+ // When model confidence/margin is low, fall back to HYBRID (safe max-recall)
21
+ // Thresholds tuned to maximize utility accuracy on 315-query benchmark
22
+
23
+ const REJECT_OPTION = {
24
+ enabled: true, // Improves utility from 92.7% to 94.3%
25
+
26
+ // Per-class thresholds: different classes need different treatment
27
+ // LEXICAL needs aggressive rejection (foreign identifiers look like English)
28
+ // STRUCTURAL needs strict thresholds (very few false positives)
29
+ // SEMANTIC is fairly safe, moderate thresholds
30
+ thresholds: {
31
+ LEXICAL: { confidence: 0.92, margin: 0.40 }, // Aggressive - catch foreign identifiers
32
+ SEMANTIC: { confidence: 0.75, margin: 0.25 }, // Moderate
33
+ },
34
+ };
35
+
36
+ /** Maximum query length to prevent DoS attacks */
37
+ const MAX_QUERY_LENGTH = 100000; // 100KB
38
+
39
+ // =============================================================================
40
+ // SOFTMAX AND POST-PROCESSING
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Compute softmax probabilities from raw scores.
45
+ * Uses stable softmax to avoid numerical overflow.
46
+ *
47
+ * @param {number[]} scores - Raw scores from CatBoost
48
+ * @returns {number[]} - Probability distribution
49
+ */
50
+ function softmax(scores) {
51
+ const maxScore = Math.max(...scores);
52
+ const expScores = scores.map(s => Math.exp(s - maxScore));
53
+ const sumExp = expScores.reduce((a, b) => a + b, 0);
54
+ return expScores.map(e => e / sumExp);
55
+ }
56
+
57
+ /**
58
+ * Apply reject option: route to HYBRID when uncertain.
59
+ *
60
+ * @param {{mode: string, confidence: number, scores: number[]}} result - CatBoost result
61
+ * @returns {string} - Final routing mode
62
+ */
63
+ function applyRejectOption(result) {
64
+ if (!REJECT_OPTION.enabled) return result.mode;
65
+
66
+ const { mode, scores } = result;
67
+
68
+ // Already HYBRID - no change
69
+ if (mode === 'HYBRID') return mode;
70
+
71
+ // Compute softmax probabilities from scores
72
+ const probs = softmax(scores);
73
+ const modeIdx = LABELS.indexOf(mode);
74
+ const confidence = probs[modeIdx];
75
+
76
+ // Get thresholds for this prediction class
77
+ const thresholds = REJECT_OPTION.thresholds[mode];
78
+ if (!thresholds) return mode;
79
+
80
+ // Check confidence threshold
81
+ if (confidence < thresholds.confidence) {
82
+ return 'HYBRID';
83
+ }
84
+
85
+ // Check margin (difference between top-1 and top-2)
86
+ const sorted = [...probs].sort((a, b) => b - a);
87
+ const margin = sorted[0] - sorted[1];
88
+ if (margin < thresholds.margin) {
89
+ return 'HYBRID';
90
+ }
91
+
92
+ return mode;
93
+ }
94
+
95
+ // =============================================================================
96
+ // PUBLIC API
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Extract features for CatBoost routing.
101
+ *
102
+ * @param {string} query - The query to route
103
+ * @returns {number[]|null} - Feature vector for CatBoost, or null for invalid input
104
+ */
105
+ export function extractCatBoostFeatures(query) {
106
+ // Input validation
107
+ if (query == null || typeof query !== 'string') {
108
+ return null;
109
+ }
110
+ if (query.length > MAX_QUERY_LENGTH) {
111
+ return null;
112
+ }
113
+ return extractAllFeatures(query);
114
+ }
115
+
116
+ /**
117
+ * Route query using CatBoost ML model with reject option.
118
+ *
119
+ * @param {string} query - The search query
120
+ * @returns {{mode: string, confidence: number, rawMode: string, rejected: boolean, scores: number[], error?: string}}
121
+ */
122
+ export function routeQueryCatBoostWithReject(query) {
123
+ // Input validation
124
+ if (query == null || typeof query !== 'string') {
125
+ return {
126
+ mode: 'hybrid',
127
+ confidence: 0,
128
+ rawMode: 'hybrid',
129
+ rejected: false,
130
+ scores: [0, 0, 0],
131
+ probs: [0.33, 0.33, 0.34],
132
+ method: 'invalid_input',
133
+ error: 'Query must be a non-null string',
134
+ };
135
+ }
136
+
137
+ if (query.length > MAX_QUERY_LENGTH) {
138
+ return {
139
+ mode: 'hybrid',
140
+ confidence: 0,
141
+ rawMode: 'hybrid',
142
+ rejected: false,
143
+ scores: [0, 0, 0],
144
+ probs: [0.33, 0.33, 0.34],
145
+ method: 'query_too_long',
146
+ error: `Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`,
147
+ };
148
+ }
149
+
150
+ const trimmed = query.trim();
151
+ if (trimmed.length === 0) {
152
+ return {
153
+ mode: 'hybrid',
154
+ confidence: 0,
155
+ rawMode: 'hybrid',
156
+ rejected: false,
157
+ scores: [0, 0, 0],
158
+ probs: [0.33, 0.33, 0.34],
159
+ method: 'empty_query',
160
+ };
161
+ }
162
+
163
+ const features = extractAllFeatures(trimmed);
164
+ const result = routeQueryCatBoost(features);
165
+
166
+ // Compute proper confidence from softmax
167
+ const probs = softmax(result.scores);
168
+ const modeIdx = LABELS.indexOf(result.mode);
169
+ const confidence = probs[modeIdx];
170
+
171
+ // Apply reject option
172
+ const finalMode = applyRejectOption({ ...result, confidence });
173
+ const rejected = finalMode !== result.mode;
174
+
175
+ return {
176
+ mode: finalMode.toLowerCase(),
177
+ confidence,
178
+ rawMode: result.mode.toLowerCase(),
179
+ rejected,
180
+ scores: result.scores,
181
+ probs,
182
+ method: 'catboost',
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Route query using CatBoost (simplified interface, compatible with ID3 router).
188
+ *
189
+ * @param {number[]} features - Pre-extracted features (if available)
190
+ * @returns {{mode: string, confidence: number, method: string}}
191
+ */
192
+ export function routeQueryML(features) {
193
+ const result = routeQueryCatBoost(features);
194
+
195
+ // Compute proper confidence from softmax
196
+ const probs = softmax(result.scores);
197
+ const modeIdx = LABELS.indexOf(result.mode);
198
+ const confidence = probs[modeIdx];
199
+
200
+ // Apply reject option
201
+ const finalMode = applyRejectOption({ ...result, confidence });
202
+
203
+ return {
204
+ mode: finalMode.toLowerCase(),
205
+ confidence,
206
+ method: 'catboost',
207
+ };
208
+ }
209
+
210
+ // Re-export LABELS for compatibility
211
+ export { LABELS };
212
+
213
+ // Confidence threshold for fallback (not used directly with reject option)
214
+ export const ML_CONFIDENCE_THRESHOLD = 0.5;
215
+
216
+ export default {
217
+ routeQueryCatBoostWithReject,
218
+ routeQueryML,
219
+ extractCatBoostFeatures,
220
+ LABELS,
221
+ ML_CONFIDENCE_THRESHOLD,
222
+ };
@@ -0,0 +1,266 @@
1
+ /**
2
+ * ML Query Router - Auto-generated from Decision Tree
3
+ *
4
+ * DO NOT EDIT - This file is auto-generated by training/models/decision-tree.js
5
+ * Generated: 2026-01-04
6
+ *
7
+ * Performance: ~0.11μs inference (500x faster than 50μs target)
8
+ * Accuracy: 94.26% on test set
9
+ */
10
+
11
+ /**
12
+ * Feature indices for reference:
13
+ * 0: hasCamelBoundary
14
+ * 1: hasUnderscoreWord
15
+ * 2: startsWithUppercase
16
+ * 3: hasConsecutiveUppercase
17
+ * 4: isSingleToken
18
+ * 5: hasDotNotation
19
+ * 6: hasParentheses
20
+ * 7: hasFileExtension
21
+ * 8: hasPathSeparator
22
+ * 9: tokenCount
23
+ * 10: avgTokenLength
24
+ * 11: uppercaseRatio
25
+ * 12: digitRatio
26
+ * 13: nonAsciiRatio
27
+ * 14: hasNonLatinScript
28
+ * 15: hasCallerPattern
29
+ * 16: hasCalleePattern
30
+ * 17: hasImplementationPattern
31
+ * 18: hasImpactPattern
32
+ * 19: hasIdentifierInQuery
33
+ * 20: startsWithQuestionWord
34
+ * 21: endsWithQuestionMark
35
+ * 22: hasSemanticConceptWord
36
+ * 23: hasExplanationRequest
37
+ * 24: hasHowDoesWorkPattern
38
+ */
39
+
40
+ /**
41
+ * Extract all 25 features from a query for ML routing.
42
+ * @param {string} query - The query to extract features from
43
+ * @returns {Float32Array} - 25-element feature vector
44
+ */
45
+ export function extractMLFeatures(query) {
46
+ const trimmed = query.trim();
47
+ const chars = trimmed.replace(/\s/g, '');
48
+ const tokens = trimmed.split(/\s+/);
49
+ const charLen = Math.max(1, chars.length);
50
+ const lower = trimmed.toLowerCase();
51
+
52
+ return new Float32Array([
53
+ // === Base Features (0-14) ===
54
+ /[a-z][A-Z]/.test(trimmed) ? 1 : 0, // 0: hasCamelBoundary
55
+ /_[a-z]/.test(trimmed) ? 1 : 0, // 1: hasUnderscoreWord
56
+ /^[A-Z]/.test(trimmed) ? 1 : 0, // 2: startsWithUppercase
57
+ /[A-Z]{2,}/.test(trimmed) ? 1 : 0, // 3: hasConsecutiveUppercase
58
+ !trimmed.includes(' ') && trimmed.length > 2 ? 1 : 0, // 4: isSingleToken
59
+ /\w\.\w/.test(trimmed) ? 1 : 0, // 5: hasDotNotation
60
+ /\(\)/.test(trimmed) ? 1 : 0, // 6: hasParentheses
61
+ /\.(java|ts|tsx|js|jsx|py|go|rs|cpp|c|h|rb|php|swift|kt|proto|yml|yaml|json|xml|md)$/i.test(trimmed) ? 1 : 0, // 7: hasFileExtension
62
+ /[/\\]/.test(trimmed) ? 1 : 0, // 8: hasPathSeparator
63
+ tokens.length, // 9: tokenCount
64
+ chars.length / Math.max(1, tokens.length), // 10: avgTokenLength
65
+ (chars.match(/[A-Z]/g) || []).length / charLen, // 11: uppercaseRatio
66
+ (chars.match(/[0-9]/g) || []).length / charLen, // 12: digitRatio
67
+ (chars.match(/[^\x00-\x7F]/g) || []).length / charLen, // 13: nonAsciiRatio
68
+ /[\u4e00-\u9fff\u0400-\u04ff\u0600-\u06ff\u0980-\u09ff\u3040-\u30ff\uac00-\ud7af]/.test(trimmed) ? 1 : 0, // 14: hasNonLatinScript
69
+
70
+ // === Structural Features (15-19) ===
71
+ /\b(callers?\s+of|what\s+calls|who\s+calls|usages?\s+of|references?\s+to|where\s+is\s+\w+\s+called)\b/.test(lower) ? 1 : 0, // 15: hasCallerPattern
72
+ /\b(callees?\s+of|what\s+does\s+\w+\s+call|dependencies\s+of|methods?\s+called\s+by)\b/.test(lower) ? 1 : 0, // 16: hasCalleePattern
73
+ /\b(implementations?\s+of|classes?\s+implementing|who\s+extends|subtypes?\s+of)\b/.test(lower) ? 1 : 0, // 17: hasImplementationPattern
74
+ /\b(impact\s+of|affected\s+by|depends?\s+on|downstream\s+effects?|will\s+break)\b/.test(lower) ? 1 : 0, // 18: hasImpactPattern
75
+ /[A-Z][a-z]+[A-Z]|[a-z]+[A-Z]/.test(query) ? 1 : 0, // 19: hasIdentifierInQuery
76
+
77
+ // === Semantic Features (20-24) ===
78
+ /^(how|what|why|where|when|which|who|can|does|is|are|should)\s/.test(lower) ? 1 : 0, // 20: startsWithQuestionWord
79
+ query.trim().endsWith('?') ? 1 : 0, // 21: endsWithQuestionMark
80
+ /\b(flow|process|mechanism|strategy|pattern|approach|logic|algorithm|system|detection|validation|authentication|handling)\b/.test(lower) ? 1 : 0, // 22: hasSemanticConceptWord
81
+ /\b(explain|describe|understand|overview|architecture|design)\b/.test(lower) ? 1 : 0, // 23: hasExplanationRequest
82
+ /how\s+does\s+.+\s+work/.test(lower) ? 1 : 0, // 24: hasHowDoesWorkPattern
83
+ ]);
84
+ }
85
+
86
+ /**
87
+ * Route query using trained ML decision tree.
88
+ * @param {Float32Array|number[]} features - 25-element feature vector from extractMLFeatures()
89
+ * @returns {{mode: string, confidence: number, tier: number, method: string}}
90
+ */
91
+ export function routeQueryML(features) {
92
+ if (features[4] /* isSingleToken */ <= 0.500000) {
93
+ if (features[0] /* hasCamelBoundary */ <= 0.500000) {
94
+ if (features[9] /* tokenCount */ <= 2.500000) {
95
+ if (features[22] /* hasSemanticConceptWord */ <= 0.500000) {
96
+ if (features[23] /* hasExplanationRequest */ <= 0.500000) {
97
+ if (features[13] /* nonAsciiRatio */ <= 0.500000) {
98
+ if (features[3] /* hasConsecutiveUppercase */ <= 0.500000) {
99
+ if (features[10] /* avgTokenLength */ <= 5.750000) {
100
+ return { mode: 'semantic', confidence: 0.5000, tier: 5, method: 'ml' };
101
+ } else {
102
+ return { mode: 'hybrid', confidence: 0.8649, tier: 5, method: 'ml' };
103
+ }
104
+ } else {
105
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
106
+ }
107
+ } else {
108
+ if (features[10] /* avgTokenLength */ <= 8.750000) {
109
+ if (features[10] /* avgTokenLength */ <= 7.750000) {
110
+ return { mode: 'hybrid', confidence: 0.6667, tier: 5, method: 'ml' };
111
+ } else {
112
+ return { mode: 'semantic', confidence: 0.5000, tier: 5, method: 'ml' };
113
+ }
114
+ } else {
115
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
116
+ }
117
+ }
118
+ } else {
119
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
120
+ }
121
+ } else {
122
+ if (features[2] /* startsWithUppercase */ <= 0.500000) {
123
+ if (features[10] /* avgTokenLength */ <= 9.250000) {
124
+ if (features[10] /* avgTokenLength */ <= 8.750000) {
125
+ if (features[10] /* avgTokenLength */ <= 7.750000) {
126
+ return { mode: 'semantic', confidence: 0.6250, tier: 5, method: 'ml' };
127
+ } else {
128
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
129
+ }
130
+ } else {
131
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
132
+ }
133
+ } else {
134
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
135
+ }
136
+ } else {
137
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
138
+ }
139
+ }
140
+ } else {
141
+ if (features[13] /* nonAsciiRatio */ <= 0.051587) {
142
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
143
+ } else {
144
+ if (features[13] /* nonAsciiRatio */ <= 0.566667) {
145
+ if (features[9] /* tokenCount */ <= 3.500000) {
146
+ return { mode: 'hybrid', confidence: 0.6667, tier: 5, method: 'ml' };
147
+ } else {
148
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
149
+ }
150
+ } else {
151
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
152
+ }
153
+ }
154
+ }
155
+ } else {
156
+ if (features[2] /* startsWithUppercase */ <= 0.500000) {
157
+ if (features[9] /* tokenCount */ <= 3.500000) {
158
+ if (features[13] /* nonAsciiRatio */ <= 0.159091) {
159
+ return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
160
+ } else {
161
+ if (features[13] /* nonAsciiRatio */ <= 0.346591) {
162
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
163
+ } else {
164
+ if (features[13] /* nonAsciiRatio */ <= 0.491379) {
165
+ return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
166
+ } else {
167
+ if (features[10] /* avgTokenLength */ <= 7.666667) {
168
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
169
+ } else {
170
+ return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
171
+ }
172
+ }
173
+ }
174
+ }
175
+ } else {
176
+ if (features[18] /* hasImpactPattern */ <= 0.500000) {
177
+ if (features[16] /* hasCalleePattern */ <= 0.500000) {
178
+ if (features[13] /* nonAsciiRatio */ <= 0.267857) {
179
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
180
+ } else {
181
+ if (features[13] /* nonAsciiRatio */ <= 0.592033) {
182
+ return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
183
+ } else {
184
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
185
+ }
186
+ }
187
+ } else {
188
+ return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
189
+ }
190
+ } else {
191
+ return { mode: 'structural', confidence: 1.0000, tier: 5, method: 'ml' };
192
+ }
193
+ }
194
+ } else {
195
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
196
+ }
197
+ }
198
+ } else {
199
+ if (features[13] /* nonAsciiRatio */ <= 0.111111) {
200
+ if (features[10] /* avgTokenLength */ <= 23.000000) {
201
+ if (features[10] /* avgTokenLength */ <= 17.500000) {
202
+ return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
203
+ } else {
204
+ if (features[0] /* hasCamelBoundary */ <= 0.500000) {
205
+ if (features[2] /* startsWithUppercase */ <= 0.500000) {
206
+ return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
207
+ } else {
208
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
209
+ }
210
+ } else {
211
+ return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
212
+ }
213
+ }
214
+ } else {
215
+ if (features[2] /* startsWithUppercase */ <= 0.500000) {
216
+ return { mode: 'lexical', confidence: 1.0000, tier: 5, method: 'ml' };
217
+ } else {
218
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
219
+ }
220
+ }
221
+ } else {
222
+ if (features[0] /* hasCamelBoundary */ <= 0.500000) {
223
+ if (features[10] /* avgTokenLength */ <= 7.500000) {
224
+ if (features[10] /* avgTokenLength */ <= 5.000000) {
225
+ return { mode: 'hybrid', confidence: 0.6000, tier: 5, method: 'ml' };
226
+ } else {
227
+ if (features[10] /* avgTokenLength */ <= 6.500000) {
228
+ return { mode: 'semantic', confidence: 0.6667, tier: 5, method: 'ml' };
229
+ } else {
230
+ return { mode: 'semantic', confidence: 0.7500, tier: 5, method: 'ml' };
231
+ }
232
+ }
233
+ } else {
234
+ return { mode: 'semantic', confidence: 1.0000, tier: 5, method: 'ml' };
235
+ }
236
+ } else {
237
+ return { mode: 'hybrid', confidence: 1.0000, tier: 5, method: 'ml' };
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * ML confidence threshold for accepting ML routing.
245
+ * Below this, fall back to hybrid mode.
246
+ */
247
+ export const ML_CONFIDENCE_THRESHOLD = 0.7;
248
+
249
+ /**
250
+ * Route query end-to-end: extract features and route.
251
+ * @param {string} query - The query to route
252
+ * @returns {{mode: string, confidence: number, tier: number, method: string}}
253
+ */
254
+ export function routeWithML(query) {
255
+ const features = extractMLFeatures(query);
256
+ const result = routeQueryML(features);
257
+
258
+ // Apply confidence threshold
259
+ if (result.confidence < ML_CONFIDENCE_THRESHOLD) {
260
+ return { mode: 'hybrid', confidence: result.confidence, tier: 6, method: 'ml_fallback' };
261
+ }
262
+
263
+ return result;
264
+ }
265
+
266
+ export default { extractMLFeatures, routeQueryML, routeWithML, ML_CONFIDENCE_THRESHOLD };
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Query Router - CatBoost WASM (Production)
3
+ *
4
+ * Single high-performance router using compiled CatBoost model in WebAssembly.
5
+ *
6
+ * Performance:
7
+ * - Latency: ~10μs per query (22x faster than JS)
8
+ * - Size: 225KB WASM (4.5x smaller than 1MB JS)
9
+ * - Accuracy: 100% match rate with reference implementation
10
+ * - Throughput: ~100k queries/sec
11
+ *
12
+ * The WASM module includes:
13
+ * - CatBoost model (499 trees, depth 4)
14
+ * - Feature extraction (50 features)
15
+ * - Reject option for uncertainty handling
16
+ */
17
+
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, join } from 'path';
20
+ import { createRequire } from 'module';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+
24
+ // =============================================================================
25
+ // CONSTANTS
26
+ // =============================================================================
27
+
28
+ /** Maximum query length to prevent DoS attacks */
29
+ const MAX_QUERY_LENGTH = 100000; // 100KB
30
+
31
+ // =============================================================================
32
+ // WASM MODULE LOADING
33
+ // =============================================================================
34
+
35
+ let wasmModule = null;
36
+ let wasmLoadError = null;
37
+
38
+ /**
39
+ * Load the WASM module synchronously on first use.
40
+ * The WASM file is loaded via require() for Node.js compatibility.
41
+ */
42
+ function loadWasm() {
43
+ if (wasmModule) return wasmModule;
44
+ if (wasmLoadError) throw wasmLoadError;
45
+
46
+ try {
47
+ // Use createRequire for ES module compatibility with CommonJS WASM glue
48
+ const require = createRequire(import.meta.url);
49
+ const wasmPath = join(__dirname, '..', '..', 'crates', 'wasm-router', 'pkg', 'query_router_wasm.js');
50
+ wasmModule = require(wasmPath);
51
+ return wasmModule;
52
+ } catch (err) {
53
+ wasmLoadError = new Error(`Failed to load WASM router: ${err.message}`);
54
+ throw wasmLoadError;
55
+ }
56
+ }
57
+
58
+ // =============================================================================
59
+ // QUERY ROUTER CLASS
60
+ // =============================================================================
61
+
62
+ export class QueryRouter {
63
+ constructor(options = {}) {
64
+ this.wasm = null;
65
+ this.initialized = false;
66
+ }
67
+
68
+ /**
69
+ * Ensure WASM module is loaded.
70
+ */
71
+ ensureLoaded() {
72
+ if (!this.wasm) {
73
+ this.wasm = loadWasm();
74
+ this.initialized = true;
75
+ }
76
+ return this.wasm;
77
+ }
78
+
79
+ /**
80
+ * Route a query to the optimal search path.
81
+ *
82
+ * Pipeline:
83
+ * 1. Input validation - Handles null, undefined, empty, too-long queries
84
+ * 2. File path check (~0.1μs) - Fast path for file extensions
85
+ * 3. WASM CatBoost ML (~10μs) - 3-class routing (lexical/semantic/hybrid)
86
+ *
87
+ * Structural mode is opt-in only (explicit --structural flag).
88
+ *
89
+ * @param {string} query - The search query
90
+ * @returns {{mode: string, confidence: number, method: string, routingLatency_us: number}}
91
+ */
92
+ route(query) {
93
+ const start = performance.now();
94
+
95
+ // === INPUT VALIDATION ===
96
+ // Handle null, undefined, and non-string inputs
97
+ if (query == null || typeof query !== 'string') {
98
+ return {
99
+ mode: 'hybrid',
100
+ confidence: 0,
101
+ method: 'invalid_input',
102
+ routingLatency_us: Math.round((performance.now() - start) * 1000),
103
+ error: 'Query must be a non-null string',
104
+ };
105
+ }
106
+
107
+ // Handle queries exceeding maximum length (DoS protection)
108
+ if (query.length > MAX_QUERY_LENGTH) {
109
+ return {
110
+ mode: 'hybrid',
111
+ confidence: 0,
112
+ method: 'query_too_long',
113
+ routingLatency_us: Math.round((performance.now() - start) * 1000),
114
+ error: `Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`,
115
+ };
116
+ }
117
+
118
+ const trimmed = query.trim();
119
+
120
+ // Handle empty or whitespace-only queries
121
+ if (trimmed.length === 0) {
122
+ return {
123
+ mode: 'hybrid',
124
+ confidence: 0,
125
+ method: 'empty_query',
126
+ routingLatency_us: Math.round((performance.now() - start) * 1000),
127
+ };
128
+ }
129
+
130
+ // === FILE PATH CHECK (~0.1μs) ===
131
+ // File extensions and paths are always lexical
132
+ if (/\.(java|js|jsx|ts|tsx|py|go|rs|kt|swift|rb|php|c|cpp|h|proto|json|xml|yml|yaml|md|sql)$/i.test(trimmed) ||
133
+ /[/\\]/.test(trimmed)) {
134
+ return {
135
+ mode: 'lexical',
136
+ confidence: 0.95,
137
+ method: 'file_pattern',
138
+ routingLatency_us: Math.round((performance.now() - start) * 1000),
139
+ };
140
+ }
141
+
142
+ // === WASM CATBOOST ML (~10μs) ===
143
+ let result = null;
144
+ try {
145
+ const wasm = this.ensureLoaded();
146
+ result = wasm.route_query(trimmed);
147
+
148
+ const mode = result.mode_str.toLowerCase();
149
+ const confidence = result.confidence;
150
+ const rejected = result.rejected;
151
+
152
+ return {
153
+ mode: rejected ? 'hybrid' : mode,
154
+ confidence,
155
+ method: rejected ? 'wasm_rejected' : 'wasm_catboost',
156
+ routingLatency_us: Math.round((performance.now() - start) * 1000),
157
+ };
158
+ } catch (err) {
159
+ // Fallback to hybrid if WASM fails
160
+ console.error(`[QueryRouter] WASM error: ${err.message}`);
161
+ return {
162
+ mode: 'hybrid',
163
+ confidence: 0.5,
164
+ method: 'fallback_error',
165
+ routingLatency_us: Math.round((performance.now() - start) * 1000),
166
+ };
167
+ } finally {
168
+ // Always free WASM result to prevent memory leak
169
+ if (result && typeof result.free === 'function') {
170
+ try {
171
+ result.free();
172
+ } catch {
173
+ // Ignore free errors (may already be freed or invalid)
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // =============================================================================
181
+ // CONVENIENCE EXPORTS
182
+ // =============================================================================
183
+
184
+ // Default singleton instance
185
+ const defaultRouter = new QueryRouter();
186
+
187
+ /**
188
+ * Route a query using the default router.
189
+ * @param {string} query - The search query
190
+ * @returns {{mode: string, confidence: number, method: string, routingLatency_us: number}}
191
+ */
192
+ export function routeQuery(query) {
193
+ return defaultRouter.route(query);
194
+ }
195
+
196
+ /**
197
+ * Extract features from a query (for debugging/testing).
198
+ * @param {string} query - The query to extract features from
199
+ * @returns {Float32Array|null} - Feature vector, or null for invalid input
200
+ */
201
+ export function extractFeatures(query) {
202
+ // Input validation
203
+ if (query == null || typeof query !== 'string') {
204
+ return null;
205
+ }
206
+ if (query.length > MAX_QUERY_LENGTH) {
207
+ return null;
208
+ }
209
+ const wasm = loadWasm();
210
+ return wasm.extract_features_js(query);
211
+ }
212
+
213
+ export default QueryRouter;