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.
- package/LICENSE +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +587 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- 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
|
+
};
|