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,523 @@
1
+ /**
2
+ * Vocabulary Warmer Module (Step 4 of Vocabulary Prewarm)
3
+ *
4
+ * Per-mode warmup functions that exercise caches and indexes with
5
+ * real codebase vocabulary. Three warmup tracks:
6
+ *
7
+ * warmLexical - FTS5 page cache via MATCH queries
8
+ * warmSemantic - Embedding generation + HNSW traversal
9
+ * warmHybrid - Full hybrid pipeline with representative queries
10
+ * warmFromCache - Light tier: load cached artifacts, no embedding gen
11
+ *
12
+ * The heavy-tier orchestrator (runFullWarmup, saveBinaryArtifact) lives in
13
+ * ./vocab-warmup-orchestrator.js and is re-exported here for backward compat.
14
+ *
15
+ * All functions return timing/stats objects. Failures never crash the caller.
16
+ */
17
+
18
+ import { existsSync } from 'fs';
19
+ import fs from 'fs/promises';
20
+ import path from 'path';
21
+ import { applyReadPragmas } from '../infrastructure/db-utils.js';
22
+
23
+ import Database from 'better-sqlite3';
24
+ import { DB_PATHS, EMBEDDING_CONFIG } from '../infrastructure/config/index.js';
25
+ import { generateEmbeddings, truncateForHNSW } from '../embedding/embedding-service.js';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants — imported from vocab-constants.js (shared with orchestrator,
29
+ // no circular dependency). Re-exported for backward compatibility.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ import { DATA_DIR, ARTIFACT_PATHS } from './vocab-constants.js';
33
+ export { DATA_DIR, ARTIFACT_PATHS };
34
+
35
+ const DEFAULT_TIME_BUDGET_MS = 2000;
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Backward-compatible orchestrator wrappers.
39
+ // Dynamic import avoids a static warmup-orchestrator <-> warmer cycle.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export async function runFullWarmup(options = {}) {
43
+ const mod = await import('./vocab-warmup-orchestrator.js');
44
+ return mod.runFullWarmup(options);
45
+ }
46
+
47
+ export async function saveBinaryArtifact(binPath, metaPath, embeddingMap) {
48
+ const mod = await import('./vocab-warmup-orchestrator.js');
49
+ return mod.saveBinaryArtifact(binPath, metaPath, embeddingMap);
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // 4a. Lexical Warmup
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Warm FTS5 page cache by executing MATCH queries with real codebase identifiers.
58
+ *
59
+ * @param {Array<{term: string, score: number}>} terms - PageRank-ranked identifiers
60
+ * @param {string} [dbPath] - Path to code-graph.db
61
+ * @returns {Promise<{queriesRun: number, elapsedMs: number}>}
62
+ */
63
+ export async function warmLexical(terms, dbPath) {
64
+ const start = performance.now();
65
+ const resolvedPath = dbPath || DB_PATHS.codeGraph;
66
+ let queriesRun = 0;
67
+
68
+ if (!terms || terms.length === 0 || !existsSync(resolvedPath)) {
69
+ return { queriesRun: 0, elapsedMs: Math.round(performance.now() - start) };
70
+ }
71
+
72
+ let db;
73
+ try {
74
+ db = new Database(resolvedPath, { readonly: true, timeout: 5000 });
75
+ applyReadPragmas(db);
76
+
77
+ // Check FTS5 table exists
78
+ const ftsCheck = db.prepare(
79
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='entities_fts'"
80
+ ).get();
81
+ if (!ftsCheck) {
82
+ return { queriesRun: 0, elapsedMs: Math.round(performance.now() - start), skip: 'no FTS5 table' };
83
+ }
84
+
85
+ // Keep warmup read-only; skip FTS5 optimize (write operation).
86
+
87
+ // Prepare statements
88
+ const matchStmt = db.prepare(
89
+ 'SELECT rowid FROM entities_fts WHERE name MATCH ? LIMIT 1'
90
+ );
91
+
92
+ // Run MATCH queries in a read transaction for top-N terms
93
+ const topTerms = terms.slice(0, 50);
94
+ const matchedIds = [];
95
+
96
+ const runQueries = db.transaction(() => {
97
+ for (const entry of topTerms) {
98
+ const term = typeof entry === 'string' ? entry : entry.term;
99
+ if (!term || term.length < 2) continue;
100
+
101
+ // Wrap in FTS5 double-quoted phrase to neutralise all operator chars
102
+ // (AND, OR, NOT, NEAR, -, +, ^, etc.). Internal double-quotes are
103
+ // escaped by doubling them per the FTS5 string-literal spec.
104
+ const safeTerm = term.replace(/"/g, '""'); // escape internal quotes for FTS5
105
+ if (!safeTerm) continue;
106
+
107
+ try {
108
+ const row = matchStmt.get('"' + safeTerm + '"');
109
+ queriesRun++;
110
+ if (row) matchedIds.push(row.rowid);
111
+ } catch (err) {
112
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
113
+ }
114
+
115
+ // Time budget check
116
+ if (performance.now() - start > DEFAULT_TIME_BUDGET_MS) break;
117
+ }
118
+ });
119
+
120
+ runQueries();
121
+
122
+ // Touch relationship table to warm join pages
123
+ if (matchedIds.length > 0) {
124
+ const subset = matchedIds.slice(0, 20);
125
+ const placeholders = subset.map(() => '?').join(',');
126
+ try {
127
+ db.prepare(
128
+ `SELECT count(*) FROM relationships WHERE source_id IN (${placeholders})`
129
+ ).get(...subset);
130
+ } catch (err) {
131
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
132
+ }
133
+ }
134
+ } catch (err) {
135
+ // Warmup failures are non-fatal
136
+ return { queriesRun, elapsedMs: Math.round(performance.now() - start), error: err.message };
137
+ } finally {
138
+ if (db) try { db.close(); } catch (err) {
139
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
140
+ }
141
+ }
142
+
143
+ return { queriesRun, elapsedMs: Math.round(performance.now() - start) };
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // 4b. Semantic Warmup
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /**
151
+ * Pre-compute embeddings for hub entities and community phrases, warm HNSW.
152
+ *
153
+ * Track A: Hub entity embeddings (enrichEmbeddingText format)
154
+ * Track B: Community phrase + question variant embeddings
155
+ *
156
+ * @param {Array<{term: string}>} hubEntities - High-PageRank entities (warm_mode "both")
157
+ * @param {Array<{phrase: string, variants?: string[]}>} communityPhrases
158
+ * @param {object} [options] - { hnswIndex, dimension, provider, db }
159
+ * @param {string} [options.provider] - Embedding provider override (passed to generateEmbeddings)
160
+ * @param {import('better-sqlite3').Database} [options.db] - Pre-opened code-graph.db to reuse
161
+ * @returns {Promise<{embeddingsGenerated: number, hnswTraversals: number, elapsedMs: number, embeddings: Map}>}
162
+ */
163
+ export async function warmSemantic(hubEntities, communityPhrases, options = {}) {
164
+ const start = performance.now();
165
+ const { hnswIndex, dimension, provider, db } = options;
166
+ let embeddingsGenerated = 0;
167
+ let hnswTraversals = 0;
168
+
169
+ // Collect all texts to embed
170
+ const textsToEmbed = [];
171
+ const textLabels = []; // parallel array tracking origin
172
+
173
+ // Track A: Hub entity embeddings using enriched text format (F4)
174
+ // Query entity metadata from code-graph.db for scope/type context
175
+ const entityMeta = _loadEntityMetadata(hubEntities, { db });
176
+ if (hubEntities && hubEntities.length > 0) {
177
+ for (const ent of hubEntities) {
178
+ const term = typeof ent === 'string' ? ent : ent.term;
179
+ if (!term) continue;
180
+ const meta = entityMeta.get(term);
181
+ // F4: Build enriched text matching indexer's enrichEmbeddingText() output
182
+ // Format: # file_path \n # Scope: parent > symbol \n # Defines: type symbol
183
+ const parts = [];
184
+ if (meta) {
185
+ if (meta.file_path) parts.push(`# ${meta.file_path}`);
186
+ if (meta.parentSymbol) {
187
+ parts.push(`# Scope: ${meta.parentType || 'unknown'} ${meta.parentSymbol} > ${term}`);
188
+ }
189
+ parts.push(`# Defines: ${meta.type || 'symbol'} ${term}`);
190
+ if (meta.language) parts.push(`# Language: ${meta.language}`);
191
+ }
192
+ parts.push(term);
193
+ const enrichedText = parts.join('\n');
194
+ textsToEmbed.push(enrichedText);
195
+ textLabels.push('hub');
196
+ }
197
+ }
198
+
199
+ // Track B: Community phrases + variants
200
+ if (communityPhrases && communityPhrases.length > 0) {
201
+ for (const cp of communityPhrases) {
202
+ const phrase = typeof cp === 'string' ? cp : cp.phrase;
203
+ if (phrase) {
204
+ textsToEmbed.push(phrase);
205
+ textLabels.push('phrase');
206
+ }
207
+ // Also embed question variants
208
+ const variants = cp.variants || [];
209
+ for (const v of variants) {
210
+ textsToEmbed.push(v);
211
+ textLabels.push('variant');
212
+ }
213
+ }
214
+ }
215
+
216
+ if (textsToEmbed.length === 0) {
217
+ return { embeddingsGenerated: 0, hnswTraversals: 0, elapsedMs: Math.round(performance.now() - start) };
218
+ }
219
+
220
+ // Generate embeddings (provider-agnostic via embedding-service)
221
+ let allEmbeddings;
222
+ try {
223
+ allEmbeddings = provider
224
+ ? await generateEmbeddings(textsToEmbed, provider)
225
+ : await generateEmbeddings(textsToEmbed);
226
+ embeddingsGenerated = allEmbeddings.length;
227
+ } catch (err) {
228
+ return {
229
+ embeddingsGenerated: 0,
230
+ hnswTraversals: 0,
231
+ elapsedMs: Math.round(performance.now() - start),
232
+ error: err.message,
233
+ };
234
+ }
235
+
236
+ // Build a map of text -> embedding for persistence
237
+ const embeddingMap = new Map();
238
+ for (let i = 0; i < textsToEmbed.length; i++) {
239
+ if (allEmbeddings[i]) {
240
+ embeddingMap.set(textsToEmbed[i], allEmbeddings[i]);
241
+ }
242
+ }
243
+
244
+ // Warm HNSW traversal paths if index is available
245
+ if (hnswIndex && typeof hnswIndex.search === 'function') {
246
+ const hnswDim = dimension || EMBEDDING_CONFIG.hnswDimension;
247
+ for (const emb of allEmbeddings) {
248
+ if (!emb) continue;
249
+ try {
250
+ const truncated = emb.length > hnswDim ? truncateForHNSW(emb) : emb;
251
+ await hnswIndex.search(truncated, 10);
252
+ hnswTraversals++;
253
+ } catch (err) {
254
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
255
+ }
256
+ }
257
+ }
258
+
259
+ return {
260
+ embeddingsGenerated,
261
+ hnswTraversals,
262
+ elapsedMs: Math.round(performance.now() - start),
263
+ embeddings: embeddingMap,
264
+ };
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // 4c. Hybrid Warmup
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /**
272
+ * Exercise the full hybrid pipeline with representative queries.
273
+ *
274
+ * @param {Array<{query: string, communityId?: number}>} representativeQueries - 10-20 queries
275
+ * @param {object} [searcher] - SweetSearch instance (must be pre-initialized)
276
+ * @returns {Promise<{queriesRun: number, elapsedMs: number}>}
277
+ */
278
+ export async function warmHybrid(representativeQueries, searcher) {
279
+ const start = performance.now();
280
+ let queriesRun = 0;
281
+
282
+ if (!representativeQueries || representativeQueries.length === 0) {
283
+ return { queriesRun: 0, elapsedMs: Math.round(performance.now() - start) };
284
+ }
285
+
286
+ if (!searcher || typeof searcher.search !== 'function') {
287
+ return { queriesRun: 0, elapsedMs: Math.round(performance.now() - start), skip: 'no searcher' };
288
+ }
289
+
290
+ for (const entry of representativeQueries) {
291
+ const query = typeof entry === 'string' ? entry : entry.query;
292
+ if (!query) continue;
293
+
294
+ try {
295
+ const result = await Promise.race([
296
+ searcher.search(query, { mode: 'hybrid', k: 5 }),
297
+ new Promise((_, reject) => setTimeout(() => reject(new Error('query timeout')), 5000)),
298
+ ]);
299
+ void result; // consumed for side-effect (cache warm); suppress unused-var linters
300
+ queriesRun++;
301
+ } catch (err) {
302
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
303
+ }
304
+
305
+ // Don't let hybrid warmup run too long
306
+ if (performance.now() - start > 30_000) break;
307
+ }
308
+
309
+ return { queriesRun, elapsedMs: Math.round(performance.now() - start) };
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // 4d. Cache-Based Warmup (Light Tier)
314
+ // ---------------------------------------------------------------------------
315
+
316
+ /**
317
+ * Light warmup: load cached binary artifacts and warm FTS5/HNSW without
318
+ * generating new embeddings. Called during session preheat.
319
+ *
320
+ * @param {object} [options]
321
+ * @param {number} [options.maxFts5Queries=50]
322
+ * @param {number} [options.maxHnswTraversals=100]
323
+ * @param {object} [options.hnswIndex] - HNSW index instance for traversal
324
+ * @returns {Promise<{fts5Queries: number, hnswTraversals: number, elapsedMs: number}>}
325
+ */
326
+ export async function warmFromCache(options = {}) {
327
+ const start = performance.now();
328
+ const maxFts5 = options.maxFts5Queries ?? 50;
329
+ const maxHnsw = options.maxHnswTraversals ?? 100;
330
+ let fts5Queries = 0;
331
+ let hnswTraversals = 0;
332
+
333
+ // Load identifier binary metadata
334
+ let idTerms = [];
335
+ try {
336
+ if (existsSync(ARTIFACT_PATHS.identifiersBin) && existsSync(ARTIFACT_PATHS.identifiersMeta)) {
337
+ const meta = JSON.parse(await fs.readFile(ARTIFACT_PATHS.identifiersMeta, 'utf-8'));
338
+ idTerms = meta.terms || [];
339
+ }
340
+ } catch (err) {
341
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
342
+ }
343
+
344
+ // Load semantic seeds binary
345
+ let seedEmbeddings = [];
346
+ try {
347
+ if (existsSync(ARTIFACT_PATHS.semanticSeedsBin) && existsSync(ARTIFACT_PATHS.semanticSeedsMeta)) {
348
+ const meta = JSON.parse(await fs.readFile(ARTIFACT_PATHS.semanticSeedsMeta, 'utf-8'));
349
+ const dim = meta.dimension || 256;
350
+ const buf = await fs.readFile(ARTIFACT_PATHS.semanticSeedsBin);
351
+ const headerSize = 32;
352
+ const count = meta.termCount || 0;
353
+
354
+ for (let i = 0; i < Math.min(count, maxHnsw); i++) {
355
+ const offset = headerSize + i * dim * 4;
356
+ if (offset + dim * 4 <= buf.length) {
357
+ // P1.4 FIX: Guard against unaligned byteOffset from Buffer pool.
358
+ const byteOff = buf.byteOffset + offset;
359
+ let emb;
360
+ if (byteOff % 4 === 0) {
361
+ emb = new Float32Array(buf.buffer, byteOff, dim);
362
+ } else {
363
+ const copy = new Uint8Array(dim * 4);
364
+ copy.set(buf.subarray(offset, offset + dim * 4));
365
+ emb = new Float32Array(copy.buffer);
366
+ }
367
+ seedEmbeddings.push(Array.from(emb));
368
+ }
369
+ }
370
+ }
371
+ } catch (err) {
372
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
373
+ }
374
+
375
+ // Graceful fallback if nothing cached
376
+ if (idTerms.length === 0 && seedEmbeddings.length === 0) {
377
+ return { fts5Queries: 0, hnswTraversals: 0, elapsedMs: Math.round(performance.now() - start), skip: 'no cached artifacts' };
378
+ }
379
+
380
+ // FTS5 warmup with cached identifier names
381
+ if (idTerms.length > 0) {
382
+ const lexResult = await warmLexical(
383
+ idTerms.slice(0, maxFts5).map(t => ({ term: t, score: 1 }))
384
+ );
385
+ fts5Queries = lexResult.queriesRun;
386
+ }
387
+
388
+ // HNSW traversal with cached embeddings
389
+ const hnswIndex = options.hnswIndex;
390
+ if (hnswIndex && typeof hnswIndex.search === 'function' && seedEmbeddings.length > 0) {
391
+ for (const emb of seedEmbeddings.slice(0, maxHnsw)) {
392
+ try {
393
+ await hnswIndex.search(emb, 10);
394
+ hnswTraversals++;
395
+ } catch (err) {
396
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
397
+ break;
398
+ }
399
+ }
400
+ }
401
+
402
+ return {
403
+ fts5Queries,
404
+ hnswTraversals,
405
+ elapsedMs: Math.round(performance.now() - start),
406
+ };
407
+ }
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Internal Helpers
411
+ // ---------------------------------------------------------------------------
412
+
413
+ /**
414
+ * F4: Load entity metadata from code-graph.db for hub entities.
415
+ * Fetches type, file_path, and parent scope info to build enriched
416
+ * embedding text matching the indexer's enrichEmbeddingText() format:
417
+ * # file_path
418
+ * # Scope: parent_type > symbol (or # Defines: type symbol)
419
+ * # Language: language
420
+ *
421
+ * @param {Array<{term: string}|string>} hubEntities
422
+ * @param {object} [options]
423
+ * @param {import('better-sqlite3').Database} [options.db] - Pre-opened DB to reuse (caller must close)
424
+ * @returns {Map<string, {type: string, file_path: string, language: string|null, parentType: string|null, parentSymbol: string|null}>}
425
+ */
426
+ export function _loadEntityMetadata(hubEntities, options = {}) {
427
+ const meta = new Map();
428
+ if (!hubEntities || hubEntities.length === 0) return meta;
429
+
430
+ // Collect unique term names for a single batch query.
431
+ const terms = [];
432
+ for (const ent of hubEntities) {
433
+ const term = typeof ent === 'string' ? ent : ent.term;
434
+ if (term) terms.push(term);
435
+ }
436
+ if (terms.length === 0) return meta;
437
+
438
+ // Reuse caller's DB connection if provided; otherwise open our own.
439
+ const externalDb = options.db || null;
440
+ let db = externalDb;
441
+ try {
442
+ if (!db) {
443
+ const dbPath = DB_PATHS.codeGraph;
444
+ if (!existsSync(dbPath)) return meta;
445
+ db = new Database(dbPath, { readonly: true, timeout: 5000 });
446
+ applyReadPragmas(db);
447
+ }
448
+
449
+ // Batch query: fetch all entities + parent scope in one shot.
450
+ // Chunk into batches of 500 to stay within SQLite variable limits.
451
+ const BATCH_SIZE = 500;
452
+ for (let i = 0; i < terms.length; i += BATCH_SIZE) {
453
+ const batch = terms.slice(i, i + BATCH_SIZE);
454
+ const placeholders = batch.map(() => '?').join(',');
455
+ const rows = db.prepare(
456
+ `WITH ranked AS (
457
+ SELECT
458
+ e.name,
459
+ e.type,
460
+ e.file_path,
461
+ p.name AS parent_name,
462
+ p.type AS parent_type,
463
+ ROW_NUMBER() OVER (
464
+ PARTITION BY e.name
465
+ ORDER BY
466
+ CASE r.type
467
+ WHEN 'childOf' THEN 1
468
+ WHEN 'memberOf' THEN 2
469
+ WHEN 'nestedIn' THEN 3
470
+ ELSE 4
471
+ END,
472
+ COALESCE(p.name, ''),
473
+ COALESCE(p.type, ''),
474
+ COALESCE(e.file_path, ''),
475
+ e.id,
476
+ COALESCE(p.id, 0)
477
+ ) AS rn
478
+ FROM entities e
479
+ LEFT JOIN relationships r ON r.source_id = e.id AND r.type IN ('childOf','memberOf','nestedIn')
480
+ LEFT JOIN entities p ON p.id = r.target_id
481
+ WHERE e.name IN (${placeholders})
482
+ )
483
+ SELECT name, type, file_path, parent_name, parent_type
484
+ FROM ranked
485
+ WHERE rn = 1`
486
+ ).all(...batch);
487
+
488
+ for (const row of rows) {
489
+ const ext = row.file_path ? path.extname(row.file_path).slice(1) : null;
490
+ meta.set(row.name, {
491
+ type: row.type,
492
+ file_path: row.file_path,
493
+ language: ext || null,
494
+ parentType: row.parent_type || null,
495
+ parentSymbol: row.parent_name || null,
496
+ });
497
+ }
498
+ }
499
+ } catch (err) {
500
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
501
+ } finally {
502
+ // Only close if we opened it ourselves
503
+ if (db && !externalDb) {
504
+ try { db.close(); } catch (err) {
505
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
506
+ }
507
+ }
508
+ }
509
+
510
+ return meta;
511
+ }
512
+
513
+ // ---------------------------------------------------------------------------
514
+ // Exports
515
+ // ---------------------------------------------------------------------------
516
+
517
+ export default {
518
+ warmLexical,
519
+ warmSemantic,
520
+ warmHybrid,
521
+ warmFromCache,
522
+ runFullWarmup,
523
+ };