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,632 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sweet Search v2.3 - Unified Search Pipeline with Auto-Warm Server
4
+ * Server: /tmp/sweet-search.sock (Unix), port 9876 (TCP)
5
+ * See search-server.js for server implementation.
6
+ *
7
+ * SOLID refactor: Functions extracted into search-fusion, search-boost,
8
+ * search-format, search-semantic, search-hybrid, search-postprocess,
9
+ * search-server, search-cli modules and wired back via prototype.
10
+ */
11
+
12
+ import fs from 'fs/promises';
13
+ import { existsSync } from 'fs';
14
+ import path from 'path';
15
+ import { DB_PATHS, PERFORMANCE_TARGETS, LOGGING, BINARY_HNSW_CONFIG, HCGS_CONFIG, LATE_INTERACTION_CONFIG, EMBEDDING_CONFIG, SEISMIC_CONFIG, CASCADE_CONFIG, loadProjectConfig, shouldUseLocalReranker } from '../infrastructure/config/index.js';
16
+ import { getGlobalLocalReranker } from '../ranking/local-reranker.js';
17
+ import { QueryRouter, routeQuery } from '../query/query-router.js';
18
+ import { GraphSearch } from '../graph/graph-search.js';
19
+ import { SYMBOL_KIND_WEIGHTS, DEFINITION_TYPES } from '../infrastructure/constants.js';
20
+ import { HNSWIndex } from '../vector-store/hnsw-index.js';
21
+ import { BinaryHNSWIndex } from '../vector-store/binary-hnsw-index.js';
22
+ import { Reranker } from '../ranking/flashrank.js';
23
+ import { LateInteractionIndex } from '../ranking/late-interaction-index.js';
24
+ import { getEmbedding, getBinaryEmbedding, truncateForHNSW, int8CosineSimilarity, warmup as warmupEmbedding, isWarm, registerAutoPersistOnExit } from '../embedding/embedding-service.js';
25
+ import { FloatVectorStore, getFloatStorePath } from '../vector-store/float-vector-store.js';
26
+ import { recordQueryTelemetry } from '../embedding/embedding-cache.js';
27
+ import { CodebaseRepository } from '../infrastructure/codebase-repository.js';
28
+ import { CodeGraphRepository } from '../infrastructure/code-graph-repository.js';
29
+ import { loadSparseGramIndex } from '../infrastructure/native-sparse-gram.js';
30
+ import { expandResults } from '../graph/graph-expansion.js';
31
+ import { applyMMR, shouldApplyMMR, getLambdaForIntent, MMR_CONFIG } from '../ranking/mmr.js';
32
+ import { QualityScorer, setRepoMapModule } from '../ranking/quality-scorer.js';
33
+ import { pageRank, loadGraph, buildAdjacency } from '../graph/repo-map.js';
34
+ import { classifyIntent, getIntentPolicy } from '../query/intent-router.js';
35
+
36
+ // SOLID extracted modules
37
+ import * as fusion from './search-fusion.js';
38
+ import * as boost from './search-boost.js';
39
+ import * as format from './search-format.js';
40
+ import * as semantic from './search-semantic.js';
41
+ import * as hybrid from './search-hybrid.js';
42
+ import * as postprocess from './search-postprocess.js';
43
+ import * as pattern from './search-pattern.js';
44
+
45
+ export { ROUTE_ALPHAS } from './search-fusion.js';
46
+
47
+ // =============================================================================
48
+ // STRUCTURAL QUERY PARSING (entity + type extraction for --structural flag)
49
+ // =============================================================================
50
+
51
+ const STRUCTURAL_PATTERNS = {
52
+ callers: /\b(?:(?:what|who|show|find|list)\s+(?:calls?|callers?|calling|invokes?|references?|uses)\s+(?:of\s+|to\s+)?|callers?\s+of\s+|what\s+uses\s+|usages?\s+of\s+|references?\s+to\s+)(\w+)/i,
53
+ callers2: /\bwhere\s+is\s+(\w+)\s+called\b/i,
54
+ callees: /\bwhat\s+does\s+(\w+)\s+(?:call|invoke|use|depend\s+on|import)\b/i,
55
+ callees2: /\b(?:callees?\s+of|dependencies\s+of|(?:methods?|functions?)\s+called\s+by)\s+(\w+)/i,
56
+ implementations: /\b(?:(?:implementations?|implementers?|implementors?|subclasses?|subtypes?)\s+of|(?:classes?|types?)\s+(?:that\s+)?(?:implementing?|extending?)|(?:who|what)\s+(?:extends?|implements?))\s+(\w+)/i,
57
+ impact: /\b(?:impact\s+of\s+(?:changing|modifying|refactoring|updating|deleting|removing|renaming|moving)|(?:what\s+)?depends?\s+on|(?:will|what)\s+breaks?\s+if\s+(?:I|we)\s+(?:change|modify|update|delete|remove)|affected\s+by\s+(?:changes?\s+to|modifying)?|(?:downstream|ripple)\s+effects?\s+of|what\s+needs?\s+to\s+change\s+if\s+(?:I|we)\s+(?:refactor|modify))\s+(\w+)/i,
58
+ };
59
+
60
+ /**
61
+ * Parse a structural query to extract the type and target entity.
62
+ * Used when structural mode is forced via explicit flag.
63
+ *
64
+ * @param {string} query
65
+ * @returns {{ structuralType: string|null, targetEntity: string|null }}
66
+ */
67
+ function parseStructuralQuery(query) {
68
+ for (const [type, pattern] of Object.entries(STRUCTURAL_PATTERNS)) {
69
+ const match = query.match(pattern);
70
+ if (match) {
71
+ // Find last non-undefined capture group
72
+ let entity = null;
73
+ for (let i = match.length - 1; i >= 1; i--) {
74
+ if (match[i] !== undefined) { entity = match[i]; break; }
75
+ }
76
+ if (entity) {
77
+ // Normalize subtypes: callers2→callers, callees2→callees
78
+ const normalized = type.replace(/\d+$/, '');
79
+ return { structuralType: normalized, targetEntity: entity };
80
+ }
81
+ }
82
+ }
83
+ // No pattern matched — caller should fall back to hybrid
84
+ return { structuralType: null, targetEntity: null };
85
+ }
86
+
87
+ export class SweetSearch {
88
+ constructor(options = {}) {
89
+ const projectRoot = options.projectRoot || process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd();
90
+ this.projectRoot = projectRoot;
91
+ const projectConfig = loadProjectConfig(projectRoot);
92
+ const projectCascade = projectConfig.cascade || {};
93
+ const envOrProject = (envKey, cascadeKey, configKey) =>
94
+ process.env[envKey] != null ? CASCADE_CONFIG[configKey] : projectCascade[cascadeKey];
95
+
96
+ this.graphSearch = new GraphSearch(options.graphDbPath || DB_PATHS.codeGraph);
97
+ this.codeGraphRepo = new CodeGraphRepository(options.graphDbPath || DB_PATHS.codeGraph);
98
+ this.hnswIndex = new HNSWIndex({ indexPath: options.hnswPath || DB_PATHS.hnswIndex });
99
+ this.binaryHnswIndex = new BinaryHNSWIndex({ indexPath: options.binaryHnswPath || DB_PATHS.binaryHnswIndex });
100
+ this.reranker = new Reranker(options);
101
+ this.lateInteractionIndex = new LateInteractionIndex(options.lateInteractionOptions || {});
102
+ this.router = new QueryRouter();
103
+ this.codebaseDbPath = options.codebaseDbPath || DB_PATHS.codebase;
104
+ this.sparseGramIndexPath = options.sparseGramIndexPath || DB_PATHS.sparseGramIndex;
105
+ this.verbose = options.verbose ?? LOGGING.verbose;
106
+ this.timing = options.timing ?? LOGGING.timing;
107
+ this.use3Stage = options.use3Stage ?? true;
108
+ this.stage1Candidates = options.stage1Candidates ?? BINARY_HNSW_CONFIG.retrieval.stage1Candidates;
109
+ this.stage2Candidates = options.stage2Candidates ?? BINARY_HNSW_CONFIG.retrieval.stage2Candidates;
110
+ this.stage3Candidates = options.stage3Candidates ?? BINARY_HNSW_CONFIG.retrieval.stage3Candidates;
111
+ this.useLateInteraction = options.useLateInteraction ?? LATE_INTERACTION_CONFIG.enabled;
112
+ this.lateInteractionBlendWeight = options.lateInteractionBlendWeight ?? LATE_INTERACTION_CONFIG.blendWeight ?? 0.3;
113
+ this.returnSummaryFirst = options.returnSummaryFirst ?? HCGS_CONFIG.returnSummaryFirst;
114
+ this.summaryTokenBudget = options.summaryTokenBudget ?? HCGS_CONFIG.summaryTokenBudget;
115
+ this.fullCodeTokenBudget = options.fullCodeTokenBudget ?? HCGS_CONFIG.fullCodeTokenBudget;
116
+ // SEISMIC sparse vector path (lazy-loaded when SEISMIC_CONFIG.enabled)
117
+ this._seismicIndex = null;
118
+ // Direct-access float vector store for Stage 2.5 (replaces SQLite on hot path)
119
+ this.floatVectorStore = new FloatVectorStore();
120
+ this.qualityWeight = options.qualityWeight ?? 0;
121
+ // Cascade scoring (Section 26): MaxSim → gate → conditional CE
122
+ this.cascadeEnabled = options.cascadeEnabled
123
+ ?? envOrProject('SWEET_SEARCH_CASCADE_ENABLED', 'enabled', 'enabled')
124
+ ?? CASCADE_CONFIG.enabled;
125
+ this.cascadeCeTopK = options.cascadeCeTopK
126
+ ?? envOrProject('SWEET_SEARCH_CASCADE_CE_TOP_K', 'ceTopK', 'ceTopK')
127
+ ?? CASCADE_CONFIG.ceTopK;
128
+ this.cascadeGateThreshold = options.cascadeGateThreshold
129
+ ?? envOrProject('SWEET_SEARCH_CASCADE_GATE_THRESHOLD', 'gateThreshold', 'gateThreshold')
130
+ ?? CASCADE_CONFIG.gateThreshold;
131
+ this.cascadeForceFullCE = options.cascadeForceFullCE
132
+ ?? envOrProject('SWEET_SEARCH_FORCE_FULL_CE', 'forceFullCrossEncoder', 'forceFullCrossEncoder')
133
+ ?? CASCADE_CONFIG.forceFullCrossEncoder;
134
+ this.cascadeShadowMode = options.cascadeShadowMode
135
+ ?? CASCADE_CONFIG.shadowMode;
136
+ setRepoMapModule({ pageRank, loadGraph, buildAdjacency });
137
+ this._qualityScorer = null;
138
+ this.codebaseRepo = new CodebaseRepository(this.codebaseDbPath);
139
+ this.sparseGramIndex = null;
140
+ this.grepInitialized = false;
141
+ this.initialized = false;
142
+ }
143
+
144
+ /** @deprecated Use codebaseRepo methods instead. Bridge for legacy callers. */
145
+ get codebaseDb() {
146
+ // TODO: Remove once all callers migrate to codebaseRepo
147
+ return this.codebaseRepo;
148
+ }
149
+
150
+ /** Initialize all search components */
151
+ async init() {
152
+ if (this.initialized) return;
153
+ const start = Date.now();
154
+
155
+ this.hasGraphIndex = existsSync(DB_PATHS.codeGraph);
156
+ this.hasHnswIndex = existsSync(DB_PATHS.hnswIndex.replace('.idx', '.meta.json'));
157
+ this.hasBinaryHnswIndex = existsSync(DB_PATHS.binaryHnswIndex.replace('.idx', '.meta.json'));
158
+ this.hasCodebaseIndex = existsSync(this.codebaseDbPath);
159
+ this.hasLateInteractionIndex = existsSync(this.lateInteractionIndex.indexPath);
160
+ this.hasSparseGramIndex = existsSync(this.sparseGramIndexPath);
161
+
162
+ if (!this.hasGraphIndex && !this.hasCodebaseIndex) {
163
+ throw new Error('No search indexes found. Run indexing first.');
164
+ }
165
+
166
+ if (this.hasBinaryHnswIndex && this.use3Stage) {
167
+ try {
168
+ await this.binaryHnswIndex.load();
169
+ const stats = this.binaryHnswIndex.getStats();
170
+ this.log(`BinaryHNSW: Loaded ${stats.totalVectors} vectors (${stats.memorySizeMB} MB)`);
171
+ } catch (err) {
172
+ const isPipelineMismatch = err.message.includes('Pipeline version mismatch');
173
+ if (isPipelineMismatch) {
174
+ console.error(`[SweetSearch] REBUILD REQUIRED: ${err.message}`);
175
+ console.error('[SweetSearch] Run: node core/artifact-builder.js (or re-index) to rebuild.');
176
+ }
177
+ this.log(`BinaryHNSW: Failed to load: ${err.message}`);
178
+ this.hasBinaryHnswIndex = false;
179
+ }
180
+
181
+ // Float store loads separately — a corrupt/missing float store must NOT
182
+ // disable the entire 3-stage pipeline. Stage 2.5 falls back to SQLite.
183
+ if (this.hasBinaryHnswIndex) {
184
+ try {
185
+ const floatStorePath = getFloatStorePath(DB_PATHS.binaryHnswIndex);
186
+ const floatLoaded = await this.floatVectorStore.load(floatStorePath);
187
+ if (floatLoaded) {
188
+ const fStats = this.floatVectorStore.getStats();
189
+ this.log(`FloatStore: Loaded ${fStats.count} vectors (${fStats.memorySizeMB} MB)`);
190
+ } else {
191
+ this.log('FloatStore: Not found, Stage 2.5 will use SQLite fallback');
192
+ }
193
+ } catch (err) {
194
+ this.log(`FloatStore: Failed to load (Stage 2.5 will use SQLite fallback): ${err.message}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ if (this.hasHnswIndex) {
200
+ try {
201
+ await this.hnswIndex.load(undefined, { mmap: true });
202
+ this.log(`HNSW: Loaded ${this.hnswIndex.getStats().totalVectors} vectors (mmap)`);
203
+ } catch (err) {
204
+ this.log(`HNSW: Failed to load: ${err.message}`);
205
+ this.hasHnswIndex = false;
206
+ }
207
+ }
208
+
209
+ if (this.hasLateInteractionIndex && this.useLateInteraction) {
210
+ try {
211
+ await this.lateInteractionIndex.init();
212
+ const stats = this.lateInteractionIndex.getStats();
213
+ this.log(`LateInteraction: Loaded ${stats.documents} documents (${stats.estimatedSizeMB} MB, ${stats.avgTokensPerDoc} avg tokens)`);
214
+
215
+ // Preheat LI ONNX inference model (~900ms cold start otherwise).
216
+ // The index loads token vectors; this loads the query encoder model.
217
+ const { encodeQuery } = await import('../ranking/late-interaction-model.js');
218
+ await encodeQuery('warmup');
219
+ this.log('LateInteraction: ONNX model preheated');
220
+ } catch (err) {
221
+ this.log(`LateInteraction: Failed to load: ${err.message}`);
222
+ this.hasLateInteractionIndex = false;
223
+ }
224
+ }
225
+
226
+ if (this.hasSparseGramIndex) {
227
+ try {
228
+ this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
229
+ if (this.sparseGramIndex) {
230
+ const stats = this.sparseGramIndex.getStats();
231
+ this.log(
232
+ `SparseGram: Loaded ${stats.grams} grams across ${stats.totalFiles} files ` +
233
+ `(${stats.postings} postings${stats.usedFallbackWeights ? ', fallback weights' : ''})`
234
+ );
235
+ } else {
236
+ this.hasSparseGramIndex = false;
237
+ }
238
+ } catch (err) {
239
+ this.log(`SparseGram: Failed to load: ${err.message}`);
240
+ this.hasSparseGramIndex = false;
241
+ this.sparseGramIndex = null;
242
+ }
243
+ }
244
+
245
+ await warmupEmbedding({ initVocabulary: true, initSemanticCache: true });
246
+
247
+ if (shouldUseLocalReranker()) {
248
+ try {
249
+ const localReranker = getGlobalLocalReranker();
250
+ await localReranker.init();
251
+ this.log('LocalReranker: Pre-initialized (gte-reranker-modernbert-base INT8)');
252
+ } catch (err) {
253
+ this.log(`LocalReranker: Pre-init failed: ${err.message}`);
254
+ }
255
+ }
256
+
257
+ this.initialized = true;
258
+ this.log(`SweetSearch: Initialized in ${Date.now() - start}ms`);
259
+ }
260
+
261
+ async initGrepOnly() {
262
+ if (this.grepInitialized || this.initialized) return;
263
+ const start = Date.now();
264
+
265
+ this.hasCodebaseIndex = existsSync(this.codebaseDbPath);
266
+ this.hasSparseGramIndex = existsSync(this.sparseGramIndexPath);
267
+ if (this.hasSparseGramIndex) {
268
+ try {
269
+ this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
270
+ if (this.sparseGramIndex) {
271
+ const stats = this.sparseGramIndex.getStats();
272
+ this.log(
273
+ `SparseGram: Loaded ${stats.grams} grams across ${stats.totalFiles} files ` +
274
+ `(${stats.postings} postings${stats.usedFallbackWeights ? ', fallback weights' : ''})`
275
+ );
276
+ } else {
277
+ this.hasSparseGramIndex = false;
278
+ }
279
+ } catch (err) {
280
+ this.log(`SparseGram: Failed to load: ${err.message}`);
281
+ this.hasSparseGramIndex = false;
282
+ this.sparseGramIndex = null;
283
+ }
284
+ }
285
+
286
+ this.grepInitialized = true;
287
+ this.log(`SweetSearch: Grep-only initialized in ${Date.now() - start}ms`);
288
+ }
289
+
290
+ /** Main search entry point. */
291
+ async search(query, options = {}) {
292
+ const {
293
+ k = 10, mode: requestedMode = 'auto', regex = '', expand = true, rerank = true,
294
+ fusion: fusionOpt = 'cc', useLateInteraction = this.useLateInteraction,
295
+ graphExpand = 'none', graphExpandOptions = {},
296
+ adaptiveHop2 = true, intent = 'none', qualityWeight = this.qualityWeight,
297
+ } = options;
298
+ const mode = requestedMode === 'grep' ? 'grep' : (regex ? 'pattern' : requestedMode);
299
+
300
+ if (mode === 'grep') {
301
+ await this.initGrepOnly();
302
+ } else {
303
+ await this.init();
304
+ }
305
+
306
+ const start = Date.now();
307
+ const stats = { query };
308
+
309
+ // P2.2: Intent-aware retrieval routing
310
+ let effectiveGraphExpand = graphExpand;
311
+ let intentResult;
312
+ let intentPolicy = null;
313
+ if (intent === 'auto') {
314
+ intentResult = classifyIntent(query);
315
+ } else if (intent && intent !== 'none') {
316
+ intentResult = { intent, confidence: 1, scores: {} };
317
+ }
318
+ if (intentResult) {
319
+ intentPolicy = getIntentPolicy(intentResult.intent);
320
+ stats.intent = {
321
+ classified: intentResult.intent, confidence: intentResult.confidence,
322
+ expandMode: intentPolicy.expandMode, maxResults: intentPolicy.maxResults,
323
+ };
324
+ if (graphExpand === 'none' && intentPolicy.expandMode !== 'none') {
325
+ effectiveGraphExpand = intentPolicy.expandMode;
326
+ }
327
+ }
328
+
329
+ // Step 1: Route query
330
+ const routing = mode === 'auto' ? routeQuery(query) : null;
331
+ let searchMode;
332
+ if (mode === 'auto') {
333
+ searchMode = routing.mode;
334
+ stats.routing = { mode: routing.mode, confidence: routing.confidence, latency_us: routing.routingLatency_us };
335
+ } else {
336
+ searchMode = mode;
337
+ stats.routing = {
338
+ mode,
339
+ forced: true,
340
+ ...(regex && requestedMode !== 'auto' && requestedMode !== 'pattern'
341
+ ? { requestedMode, regexForced: true }
342
+ : {}),
343
+ };
344
+ }
345
+ this.log(`Search mode: ${searchMode}`);
346
+
347
+ // Step 2: Execute search based on mode
348
+ let results;
349
+ let semanticStats = null;
350
+
351
+ switch (searchMode) {
352
+ case 'grep': {
353
+ const grepResult = await this.bareGrep(query, routing, {
354
+ ...options,
355
+ regex: regex || query,
356
+ });
357
+ results = grepResult.results;
358
+ Object.assign(stats, grepResult.stats);
359
+ stats.total_ms = grepResult.stats.total_ms ?? (Date.now() - start);
360
+ return { results, stats };
361
+ }
362
+ case 'pattern': {
363
+ const patternResult = await this.patternSearch(query, routing, options);
364
+ // Agent mode returns a fully packaged response — bypass post-retrieval.
365
+ if (patternResult.format === 'agent') {
366
+ Object.assign(stats, patternResult.stats);
367
+ stats.total_ms = stats.total_ms ?? (Date.now() - start);
368
+ return patternResult;
369
+ }
370
+ results = patternResult.results;
371
+ Object.assign(stats, patternResult.stats);
372
+ break;
373
+ }
374
+ case 'structural':
375
+ results = await this.structuralSearch(query, routing, options);
376
+ stats.path = 'structural';
377
+ stats.structuralType = routing?.structuralType;
378
+ stats.targetEntity = routing?.targetEntity;
379
+ break;
380
+ case 'lexical': {
381
+ const lexResult = await this.lexicalSearch(query, { k, expand });
382
+ results = lexResult.results;
383
+ stats.path = 'lexical';
384
+ stats.confidence = lexResult.stats?.confidence;
385
+ stats.lexicalMode = lexResult.stats?.mode;
386
+ // When expansion was deferred (ambiguous + expand=true), ensure
387
+ // postprocess expansion runs by promoting graphExpand from 'none'.
388
+ if (stats.confidence === 'ambiguous' && expand && effectiveGraphExpand === 'none') {
389
+ effectiveGraphExpand = '2hop';
390
+ }
391
+ break;
392
+ }
393
+ case 'semantic': {
394
+ const semanticResult = await this.semanticSearch(query, { k, rerank, useLateInteraction });
395
+ results = semanticResult.results;
396
+ semanticStats = semanticResult.stats;
397
+ stats.path = 'semantic';
398
+ break;
399
+ }
400
+ case 'hybrid':
401
+ default: {
402
+ const hybridResult = await this.hybridSearchV2(query, { k, useLateInteraction, routing });
403
+ results = hybridResult.results || hybridResult;
404
+ semanticStats = hybridResult.semanticStats || null;
405
+ stats.path = 'hybrid';
406
+ stats.fusion = hybridResult.fusionStats?.method || 'cc';
407
+ stats.fusionFallback = hybridResult.fusionStats?.fallbackReason || null;
408
+ stats.lexicalLatencyMs = hybridResult.fusionStats?.lexicalLatencyMs ?? null;
409
+ break;
410
+ }
411
+ }
412
+
413
+ // Enable adaptive 2-hop expansion for semantic + hybrid (graph completion
414
+ // improves recall; cascade handles precision). Lexical ambiguous case is
415
+ // handled inside its switch block (depends on stats.confidence).
416
+ if ((searchMode === 'semantic' || searchMode === 'hybrid')
417
+ && effectiveGraphExpand === 'none' && expand) {
418
+ effectiveGraphExpand = '2hop';
419
+ }
420
+
421
+ // Step 3: Post-retrieval processing (delegated to extracted module)
422
+ return this._applyPostRetrieval(results, query, options, {
423
+ stats, semanticStats, searchMode, effectiveGraphExpand, intentPolicy, start,
424
+ });
425
+ }
426
+
427
+ /** Structural search path (GraphRAG structural queries — opt-in via explicit flag) */
428
+ async structuralSearch(query, routing, options = {}) {
429
+ // Extract type+entity from routing (if available) or from query patterns
430
+ const parsed = routing && routing.structuralType
431
+ ? routing
432
+ : parseStructuralQuery(query);
433
+ const { structuralType, targetEntity } = parsed;
434
+
435
+ if (!structuralType || !targetEntity) {
436
+ this.log('Structural: no pattern match, falling back to hybrid');
437
+ return this.hybridSearchV2(query, options);
438
+ }
439
+
440
+ const start = performance.now();
441
+ let result;
442
+ switch (structuralType) {
443
+ case 'callers': result = await this.graphSearch.findCallers(targetEntity); break;
444
+ case 'callees': result = await this.graphSearch.findCallees(targetEntity); break;
445
+ case 'implementations': result = await this.graphSearch.findImplementations(targetEntity); break;
446
+ case 'impact': result = await this.graphSearch.findImpact(targetEntity); break;
447
+ default: return this.hybridSearchV2(query, options);
448
+ }
449
+ const elapsed = performance.now() - start;
450
+ this.log(`Structural (${structuralType}): ${elapsed.toFixed(1)}ms, ${result.results.length} results`);
451
+ return result.results.map(r => ({ ...r, searchPath: 'structural', structuralType }));
452
+ }
453
+
454
+ /** Lexical search path (FTS5/BM25 + Graph) */
455
+ async lexicalSearch(query, options = {}) {
456
+ const { k = 10, expand = true } = options;
457
+ if (!this.hasGraphIndex) {
458
+ this.log('Lexical search unavailable: no graph index');
459
+ return { results: [], stats: { confidence: 'exact' } };
460
+ }
461
+ const { results, stats } = await this.graphSearch.graphExpandedSearch(query, {
462
+ k, expand, deferExpansion: expand,
463
+ });
464
+ this.log(`Lexical: ${stats.bm25_ms}ms BM25, ${stats.graph_ms || 0}ms graph (confidence: ${stats.confidence})`);
465
+ return {
466
+ results: results.map(r => ({ ...r, searchPath: 'lexical' })),
467
+ stats,
468
+ };
469
+ }
470
+
471
+ /** Semantic search dispatcher. Delegates to 3Stage or Standard based on config. */
472
+ async semanticSearch(query, options = {}) {
473
+ const { k = 10, rerank = true, useLateInteraction = this.useLateInteraction } = options;
474
+ if (this.hasBinaryHnswIndex && this.use3Stage) {
475
+ return this.semanticSearch3Stage(query, { k, rerank, useLateInteraction });
476
+ }
477
+ return this.semanticSearchStandard(query, { k, rerank });
478
+ }
479
+
480
+ /** O(N) vector scan fallback (when HNSW not available). Filters stale entities. */
481
+ async vectorScan(queryEmbedding, limit = 100) {
482
+ // Ephemeral scan — opens, reads, closes (no persistent connection)
483
+ const rows = this.codebaseRepo.scanAllVectors();
484
+ const candidates = [];
485
+
486
+ const staleEntityIds = this.hasGraphIndex
487
+ ? await this.graphSearch.getStaleEntityIds().catch(err => {
488
+ this.log(`A1: Could not load stale entity list: ${err.message}`);
489
+ return new Set();
490
+ })
491
+ : new Set();
492
+
493
+ for (const row of rows) {
494
+ const embeddingBuffer = row.embedding;
495
+ if (!embeddingBuffer) continue;
496
+ const embedding = new Float32Array(embeddingBuffer.buffer, embeddingBuffer.byteOffset, embeddingBuffer.length / 4);
497
+ const score = this.cosineSimilarity(queryEmbedding, Array.from(embedding));
498
+ let metadata = {};
499
+ try { metadata = row.metadata ? JSON.parse(row.metadata) : {}; } catch (err) {
500
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
501
+ }
502
+ candidates.push({ id: row.id, content: row.text || '', score, metadata, entity_id: metadata.entity_id || null });
503
+ }
504
+
505
+ const activeResults = candidates.filter(r => {
506
+ if (r.entity_id && staleEntityIds.has(r.entity_id)) return false;
507
+ if (r.metadata?.entity_id && staleEntityIds.has(r.metadata.entity_id)) return false;
508
+ return true;
509
+ });
510
+ this.log(`A1: Vector scan found ${candidates.length} candidates, ${activeResults.length} active (${staleEntityIds.size} stale entities filtered)`);
511
+ return activeResults.sort((a, b) => b.score - a.score).slice(0, limit);
512
+ }
513
+
514
+ /** Load document content for CE reranking — pure chunk text, no metadata noise */
515
+ async loadDocumentContent(candidates, query = '') {
516
+ return candidates.map(c => c.content || c.text || '');
517
+ }
518
+
519
+ /** Cosine similarity — throws on dimension mismatch instead of silently truncating. */
520
+ cosineSimilarity(a, b) {
521
+ if (a.length !== b.length) {
522
+ throw new Error(`cosineSimilarity dimension mismatch: ${a.length} vs ${b.length}`);
523
+ }
524
+ let dot = 0, normA = 0, normB = 0;
525
+ for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; }
526
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
527
+ }
528
+
529
+ /**
530
+ * Fix 5.5: Load float embeddings from SQLite for a set of chunk IDs.
531
+ * Uses batch WHERE IN query for efficiency (single round-trip).
532
+ * Returns Map<id, Float32Array>.
533
+ */
534
+ async _loadFloatVectors(ids) {
535
+ if (!this.codebaseDbPath || !existsSync(this.codebaseDbPath)) return null;
536
+ if (!ids.length) return new Map();
537
+ try {
538
+ return this.codebaseRepo.getEmbeddingsByIds(ids);
539
+ } catch (err) {
540
+ this.log(`_loadFloatVectors failed: ${err.message}`);
541
+ return new Map();
542
+ }
543
+ }
544
+
545
+ // approximateLateInteractionScore removed — replaced by real per-token MaxSim via LateOn-Code
546
+ // in search-semantic.js (Phase 4 of LATE_INTERACTION.md)
547
+
548
+ /** Log message (if verbose). Uses stderr to avoid corrupting JSON output. */
549
+ log(message) { if (this.verbose) console.error(`[SweetSearch] ${message}`); }
550
+
551
+ /** Log performance stats */
552
+ logPerformance(stats) {
553
+ const target = PERFORMANCE_TARGETS.latency;
554
+ let status = 'OK';
555
+ if (stats.path === 'lexical' && stats.total_ms > target.lexicalP50 * 2) status = 'SLOW';
556
+ if (stats.path === 'semantic' && stats.total_ms > target.semanticP50 * 2) status = 'SLOW';
557
+ console.error(`[Perf] ${stats.path} | ${stats.total_ms}ms | ${status}`);
558
+ }
559
+
560
+ /** Close all connections */
561
+ close() {
562
+ this.graphSearch.close();
563
+ this.codebaseRepo.close();
564
+ this.codeGraphRepo.close();
565
+ }
566
+ }
567
+
568
+ // Prototype wiring (extracted module functions)
569
+ Object.assign(SweetSearch.prototype, {
570
+ getResultKey: fusion.getResultKey,
571
+ minMaxNormalize: fusion.minMaxNormalize,
572
+ quantileNormalize: fusion.quantileNormalize,
573
+ convexCombination: fusion.convexCombination,
574
+ shouldFallbackToRRF: fusion.shouldFallbackToRRF,
575
+ rrfFusion: fusion.rrfFusion,
576
+ robustCCFusion: fusion.robustCCFusion,
577
+ variance: fusion.variance,
578
+ getBoostIntent: boost.getBoostIntent,
579
+ applyPostFusionBoosts: boost.applyPostFusionBoosts,
580
+ computeDefinitionBoost: boost.computeDefinitionBoost,
581
+ computeSyntaxBoost: boost.computeSyntaxBoost,
582
+ computePositionBoost: boost.computePositionBoost,
583
+ extractQueryTokens: boost.extractQueryTokens,
584
+ formatResults: format.formatResults,
585
+ formatStructuralResults: format.formatStructuralResults,
586
+ formatSummaryFirst: format.formatSummaryFirst,
587
+ formatMiddleRes: format.formatMiddleRes,
588
+ enrichWithSummaries: format.enrichWithSummaries,
589
+ semanticSearch3Stage: semantic.semanticSearch3Stage,
590
+ semanticSearchStandard: semantic.semanticSearchStandard,
591
+ shouldSkipRerank: semantic.shouldSkipRerank,
592
+ getAdaptiveCandidateCount: semantic.getAdaptiveCandidateCount,
593
+ hybridSearchV2: hybrid.hybridSearchV2,
594
+ hybridSearch: hybrid.hybridSearch,
595
+ patternSearch: pattern.patternSearch,
596
+ bareGrep: pattern.bareGrep,
597
+ getChunkLocationMap: pattern.getChunkLocationMap,
598
+ _applyPostRetrieval: postprocess.applyPostRetrieval,
599
+ });
600
+
601
+ SweetSearch.BOOST_POLICY = boost.BOOST_POLICY;
602
+
603
+ // Module-level singleton (warm cache)
604
+ let _warmSearcher = null;
605
+ let _warmInitPromise = null;
606
+
607
+ /** Get or create a warm SweetSearch instance (singleton). */
608
+ export async function getWarmSearcher(options = {}) {
609
+ if (_warmSearcher && _warmSearcher.initialized) return _warmSearcher;
610
+ if (_warmInitPromise) { await _warmInitPromise; return _warmSearcher; }
611
+ _warmSearcher = new SweetSearch(options);
612
+ _warmInitPromise = _warmSearcher.init();
613
+ await _warmInitPromise;
614
+ return _warmSearcher;
615
+ }
616
+
617
+ /** Quick search using warm cache (for programmatic use) */
618
+ export async function warmSearch(query, options = {}) {
619
+ const searcher = await getWarmSearcher();
620
+ return searcher.search(query, options);
621
+ }
622
+
623
+ // CLI guard
624
+ if (import.meta.url === `file://${process.argv[1]}`) {
625
+ const args = process.argv.slice(2);
626
+ const { runCli } = await import('./search-cli.js');
627
+ await runCli(args);
628
+ }
629
+
630
+ export default SweetSearch;
631
+ /** @deprecated Use SweetSearch instead. SmartSearch is a legacy alias. */
632
+ export { SweetSearch as SmartSearch };