sweet-search 2.5.1 → 2.5.3
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/core/cli.js +45 -0
- package/core/embedding/embedding-cache.js +90 -4
- package/core/embedding/embedding-service.js +27 -5
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +37 -7
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +10 -2
- package/core/infrastructure/init-config.js +138 -0
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +277 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +273 -54
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +414 -9
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/start-server.js +13 -2
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +427 -23
- package/scripts/write-claude-rules.js +110 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
|
-
import { existsSync } from 'fs';
|
|
13
|
+
import { existsSync, readFileSync } from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
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
16
|
import { getGlobalLocalReranker } from '../ranking/local-reranker.js';
|
|
@@ -22,7 +22,7 @@ import { BinaryHNSWIndex } from '../vector-store/binary-hnsw-index.js';
|
|
|
22
22
|
import { Reranker } from '../ranking/flashrank.js';
|
|
23
23
|
import { LateInteractionIndex } from '../ranking/late-interaction-index.js';
|
|
24
24
|
import { resolveSearchRerankPolicy } from '../ranking/late-interaction-policy.js';
|
|
25
|
-
import { readPersistedLiPolicy } from '../infrastructure/index.js';
|
|
25
|
+
import { applyPersistedLiModel, readPersistedLiPolicy } from '../infrastructure/index.js';
|
|
26
26
|
import { getEmbedding, getBinaryEmbedding, truncateForHNSW, int8CosineSimilarity, warmup as warmupEmbedding, isWarm, registerAutoPersistOnExit } from '../embedding/embedding-service.js';
|
|
27
27
|
import { FloatVectorStore, getFloatStorePath } from '../vector-store/float-vector-store.js';
|
|
28
28
|
import { recordQueryTelemetry } from '../embedding/embedding-cache.js';
|
|
@@ -44,6 +44,7 @@ import * as hybrid from './search-hybrid.js';
|
|
|
44
44
|
import * as postprocess from './search-postprocess.js';
|
|
45
45
|
import * as pattern from './search-pattern.js';
|
|
46
46
|
import { packageForAgent } from './context-expander.js';
|
|
47
|
+
import { beginPinnedRead, endPinnedRead } from './search-reader-pin.js';
|
|
47
48
|
|
|
48
49
|
export { ROUTE_ALPHAS } from './search-fusion.js';
|
|
49
50
|
|
|
@@ -67,6 +68,18 @@ const STRUCTURAL_PATTERNS = {
|
|
|
67
68
|
* @param {string} query
|
|
68
69
|
* @returns {{ structuralType: string|null, targetEntity: string|null }}
|
|
69
70
|
*/
|
|
71
|
+
// Per-stage profiling hooks. No-op unless `globalThis.__stageTimings` is set
|
|
72
|
+
// by scripts/profile-search-stages.mjs.
|
|
73
|
+
function __ptStart() {
|
|
74
|
+
return globalThis.__stageTimings ? performance.now() : null;
|
|
75
|
+
}
|
|
76
|
+
function __ptEnd(stage, t0) {
|
|
77
|
+
if (t0 == null || !globalThis.__stageTimings) return;
|
|
78
|
+
const ms = performance.now() - t0;
|
|
79
|
+
const buf = globalThis.__stageTimings;
|
|
80
|
+
(buf[stage] = buf[stage] || []).push(ms);
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
function parseStructuralQuery(query) {
|
|
71
84
|
for (const [type, pattern] of Object.entries(STRUCTURAL_PATTERNS)) {
|
|
72
85
|
const match = query.match(pattern);
|
|
@@ -91,12 +104,33 @@ export class SweetSearch {
|
|
|
91
104
|
constructor(options = {}) {
|
|
92
105
|
const projectRoot = options.projectRoot || process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd();
|
|
93
106
|
this.projectRoot = projectRoot;
|
|
107
|
+
// Honor the user's persisted `runtime.li.model` choice from
|
|
108
|
+
// `.sweet-search/config.json` BEFORE we read `LATE_INTERACTION_CONFIG.model`
|
|
109
|
+
// for activeConfigModel below or any downstream consumer (encodeQuery,
|
|
110
|
+
// LateInteractionIndex header check, native LI loader, CoreML cascade
|
|
111
|
+
// dispatcher). Without this an edge-only init silently activates the
|
|
112
|
+
// standard model path on every search. Env var still wins; see
|
|
113
|
+
// applyPersistedLiModel for the full precedence ladder.
|
|
114
|
+
this._liModelApply = applyPersistedLiModel(projectRoot);
|
|
115
|
+
const explicitLiModel = options.lateInteractionOptions?.modelId;
|
|
116
|
+
if (typeof explicitLiModel === 'string' && LATE_INTERACTION_CONFIG.models[explicitLiModel]) {
|
|
117
|
+
const before = LATE_INTERACTION_CONFIG.model;
|
|
118
|
+
LATE_INTERACTION_CONFIG.model = explicitLiModel;
|
|
119
|
+
this._liModelApply = {
|
|
120
|
+
applied: explicitLiModel,
|
|
121
|
+
before,
|
|
122
|
+
source: 'option',
|
|
123
|
+
persistedModel: this._liModelApply.persistedModel,
|
|
124
|
+
changed: explicitLiModel !== before,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
94
127
|
const projectConfig = loadProjectConfig(projectRoot);
|
|
95
128
|
const projectCascade = projectConfig.cascade || {};
|
|
96
129
|
const envOrProject = (envKey, cascadeKey, configKey) =>
|
|
97
130
|
process.env[envKey] != null ? CASCADE_CONFIG[configKey] : projectCascade[cascadeKey];
|
|
98
131
|
|
|
99
132
|
this.graphDbPath = options.graphDbPath || DB_PATHS.codeGraph;
|
|
133
|
+
this._manifestGraphDbPath = this.graphDbPath;
|
|
100
134
|
this.graphSearch = new GraphSearch(this.graphDbPath);
|
|
101
135
|
this.codeGraphRepo = new CodeGraphRepository(this.graphDbPath);
|
|
102
136
|
this.hnswPath = options.hnswPath || DB_PATHS.hnswIndex;
|
|
@@ -107,6 +141,8 @@ export class SweetSearch {
|
|
|
107
141
|
this.lateInteractionIndex = new LateInteractionIndex(options.lateInteractionOptions || {});
|
|
108
142
|
this.router = new QueryRouter();
|
|
109
143
|
this.codebaseDbPath = options.codebaseDbPath || DB_PATHS.codebase;
|
|
144
|
+
this._manifestCodebaseDbPath = this.codebaseDbPath;
|
|
145
|
+
this._manifestStateDir = path.dirname(this._manifestCodebaseDbPath);
|
|
110
146
|
this.sparseGramIndexPath = options.sparseGramIndexPath || DB_PATHS.sparseGramIndex;
|
|
111
147
|
this.verbose = options.verbose ?? LOGGING.verbose;
|
|
112
148
|
this.timing = options.timing ?? LOGGING.timing;
|
|
@@ -159,10 +195,23 @@ export class SweetSearch {
|
|
|
159
195
|
?? CASCADE_CONFIG.shadowMode;
|
|
160
196
|
setRepoMapModule({ pageRank, loadGraph, buildAdjacency });
|
|
161
197
|
this._qualityScorer = null;
|
|
162
|
-
this.codebaseRepo = new CodebaseRepository(this.
|
|
198
|
+
this.codebaseRepo = new CodebaseRepository(this._manifestCodebaseDbPath);
|
|
163
199
|
this.sparseGramIndex = null;
|
|
200
|
+
this._sparseGramLoadedPath = null;
|
|
164
201
|
this.grepInitialized = false;
|
|
165
202
|
this.initialized = false;
|
|
203
|
+
this._lateInteractionOptions = { ...(options.lateInteractionOptions || {}) };
|
|
204
|
+
this._artifactManifestEpoch = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_clearChunkLocationCache() {
|
|
208
|
+
this._chunkLocationMap = null;
|
|
209
|
+
this._chunkLocationMapSize = 0;
|
|
210
|
+
this._chunkLocationMapIndex = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_clearCodebaseChunkTypeCache() {
|
|
214
|
+
this._codebaseChunkTypeMap = null;
|
|
166
215
|
}
|
|
167
216
|
|
|
168
217
|
/** @deprecated Use codebaseRepo methods instead. Bridge for legacy callers. */
|
|
@@ -176,6 +225,8 @@ export class SweetSearch {
|
|
|
176
225
|
if (this.initialized) return;
|
|
177
226
|
const start = Date.now();
|
|
178
227
|
|
|
228
|
+
this._syncManifestPaths(this._readReconcileManifest());
|
|
229
|
+
|
|
179
230
|
this.hasGraphIndex = existsSync(this.graphDbPath);
|
|
180
231
|
this.hasHnswIndex = existsSync(this.hnswPath.replace('.idx', '.meta.json'));
|
|
181
232
|
this.hasBinaryHnswIndex = existsSync(this.binaryHnswPath.replace('.idx', '.meta.json'));
|
|
@@ -301,6 +352,7 @@ export class SweetSearch {
|
|
|
301
352
|
try {
|
|
302
353
|
this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
|
|
303
354
|
if (this.sparseGramIndex) {
|
|
355
|
+
this._sparseGramLoadedPath = this.sparseGramIndexPath;
|
|
304
356
|
const stats = this.sparseGramIndex.getStats();
|
|
305
357
|
this.log(
|
|
306
358
|
`SparseGram: Loaded ${stats.grams} grams across ${stats.totalFiles} files ` +
|
|
@@ -313,11 +365,27 @@ export class SweetSearch {
|
|
|
313
365
|
this.log(`SparseGram: Failed to load: ${err.message}`);
|
|
314
366
|
this.hasSparseGramIndex = false;
|
|
315
367
|
this.sparseGramIndex = null;
|
|
368
|
+
this._sparseGramLoadedPath = null;
|
|
316
369
|
}
|
|
317
370
|
}
|
|
318
371
|
|
|
319
372
|
await warmupEmbedding({ initVocabulary: true, initSemanticCache: true });
|
|
320
373
|
|
|
374
|
+
// Pre-build the call-graph ref-count index so the first search query
|
|
375
|
+
// doesn't pay its 10-50 ms construction cost. This piggybacks on the
|
|
376
|
+
// existing warmup phase — model load already takes ~700 ms, so this
|
|
377
|
+
// adds at most a few ms to init and removes the cold-start spike from
|
|
378
|
+
// the search hot path. Skipped when the graph DB doesn't exist (e.g.
|
|
379
|
+
// grep-only mode).
|
|
380
|
+
if (this.hasGraphIndex && this.codeGraphRepo
|
|
381
|
+
&& typeof this.codeGraphRepo.prebuildRefCountIndex === 'function') {
|
|
382
|
+
try {
|
|
383
|
+
this.codeGraphRepo.prebuildRefCountIndex();
|
|
384
|
+
} catch {
|
|
385
|
+
// Index build is purely an optimisation; failure is non-fatal.
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
321
389
|
if (shouldUseLocalReranker()) {
|
|
322
390
|
try {
|
|
323
391
|
const localReranker = getGlobalLocalReranker();
|
|
@@ -329,19 +397,228 @@ export class SweetSearch {
|
|
|
329
397
|
}
|
|
330
398
|
|
|
331
399
|
this.initialized = true;
|
|
400
|
+
this._artifactManifestEpoch = this._readReconcileManifest()?.epoch ?? null;
|
|
332
401
|
this.log(`SweetSearch: Initialized in ${Date.now() - start}ms`);
|
|
333
402
|
}
|
|
334
403
|
|
|
404
|
+
_readReconcileManifest() {
|
|
405
|
+
try {
|
|
406
|
+
const manifestPath = path.join(this._manifestStateDir, 'reconcile-manifest.json');
|
|
407
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
408
|
+
return Number.isInteger(manifest?.epoch) ? manifest : null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
_resolveStatePath(filePath) {
|
|
415
|
+
if (!filePath) return null;
|
|
416
|
+
if (path.isAbsolute(filePath)) return filePath;
|
|
417
|
+
return path.join(this._manifestStateDir, filePath);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_manifestArtifactPaths(manifest) {
|
|
421
|
+
if (!manifest) return {};
|
|
422
|
+
let liIndexPath = null;
|
|
423
|
+
const liDescriptor = manifest.lateInteraction?.path
|
|
424
|
+
|| manifest.lateInteraction?.indexPath
|
|
425
|
+
|| manifest.lateInteraction?.manifest;
|
|
426
|
+
if (liDescriptor) {
|
|
427
|
+
const resolved = this._resolveStatePath(liDescriptor);
|
|
428
|
+
const segmentDir = path.dirname(resolved);
|
|
429
|
+
liIndexPath = segmentDir.endsWith('.segments')
|
|
430
|
+
? segmentDir.slice(0, -'.segments'.length)
|
|
431
|
+
: resolved;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
codebaseDbPath: this._resolveStatePath(manifest.vectors?.path),
|
|
435
|
+
graphDbPath: this._resolveStatePath(manifest.codeGraph?.path),
|
|
436
|
+
hnswPath: this._resolveStatePath(manifest.hnsw?.path),
|
|
437
|
+
hnswStalePath: this._resolveStatePath(manifest.hnsw?.stale),
|
|
438
|
+
binaryHnswPath: this._resolveStatePath(manifest.binaryHnsw?.path),
|
|
439
|
+
binaryHnswStalePath: this._resolveStatePath(manifest.binaryHnsw?.stale),
|
|
440
|
+
lateInteractionIndexPath: liIndexPath,
|
|
441
|
+
sparseGramIndexPath: this._resolveStatePath(manifest.sparseGram?.base),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_syncManifestPaths(manifest) {
|
|
446
|
+
const paths = this._manifestArtifactPaths(manifest);
|
|
447
|
+
if (!manifest) {
|
|
448
|
+
this.sparseGramDeltas = null;
|
|
449
|
+
this.sparseGramWeightsId = null;
|
|
450
|
+
}
|
|
451
|
+
if (paths.codebaseDbPath && paths.codebaseDbPath !== this.codebaseDbPath) {
|
|
452
|
+
this.codebaseRepo?.close?.();
|
|
453
|
+
this.codebaseDbPath = paths.codebaseDbPath;
|
|
454
|
+
this.codebaseRepo = new CodebaseRepository(this._manifestCodebaseDbPath);
|
|
455
|
+
this._clearCodebaseChunkTypeCache();
|
|
456
|
+
}
|
|
457
|
+
if (paths.graphDbPath && paths.graphDbPath !== this.graphDbPath) {
|
|
458
|
+
this.graphSearch?.close?.();
|
|
459
|
+
this.codeGraphRepo?.close?.();
|
|
460
|
+
this.graphDbPath = paths.graphDbPath;
|
|
461
|
+
this.graphSearch = new GraphSearch(this._manifestGraphDbPath);
|
|
462
|
+
this.codeGraphRepo = new CodeGraphRepository(this._manifestGraphDbPath);
|
|
463
|
+
}
|
|
464
|
+
if (paths.hnswPath && (paths.hnswPath !== this.hnswPath || paths.hnswStalePath !== this.hnswIndex?.stalePath)) {
|
|
465
|
+
this.hnswPath = paths.hnswPath;
|
|
466
|
+
this.hnswIndex = new HNSWIndex({ indexPath: this.hnswPath, stalePath: paths.hnswStalePath || `${this.hnswPath}.stale.bin` });
|
|
467
|
+
}
|
|
468
|
+
if (paths.binaryHnswPath && (paths.binaryHnswPath !== this.binaryHnswPath || paths.binaryHnswStalePath !== this.binaryHnswIndex?.stalePath)) {
|
|
469
|
+
this.binaryHnswPath = paths.binaryHnswPath;
|
|
470
|
+
this.binaryHnswIndex = new BinaryHNSWIndex({ indexPath: this.binaryHnswPath, stalePath: paths.binaryHnswStalePath || `${this.binaryHnswPath}.stale.bin` });
|
|
471
|
+
}
|
|
472
|
+
if (paths.lateInteractionIndexPath && paths.lateInteractionIndexPath !== this.lateInteractionIndex?.indexPath) {
|
|
473
|
+
this._lateInteractionOptions = { ...this._lateInteractionOptions, indexPath: paths.lateInteractionIndexPath };
|
|
474
|
+
this.lateInteractionIndex = new LateInteractionIndex(this._lateInteractionOptions);
|
|
475
|
+
this._clearChunkLocationCache();
|
|
476
|
+
}
|
|
477
|
+
if (paths.sparseGramIndexPath && paths.sparseGramIndexPath !== this.sparseGramIndexPath) {
|
|
478
|
+
this.sparseGramIndexPath = paths.sparseGramIndexPath;
|
|
479
|
+
this.sparseGramIndex = null;
|
|
480
|
+
this._sparseGramLoadedPath = null;
|
|
481
|
+
}
|
|
482
|
+
if (typeof manifest?.sparseGram?.weightsId === 'string') {
|
|
483
|
+
this.sparseGramWeightsId = manifest.sparseGram.weightsId;
|
|
484
|
+
}
|
|
485
|
+
if (Array.isArray(manifest?.sparseGram?.deltas)) {
|
|
486
|
+
this.sparseGramDeltas = manifest.sparseGram.deltas.filter((entry) => typeof entry === 'string');
|
|
487
|
+
} else if (manifest?.sparseGram) {
|
|
488
|
+
this.sparseGramDeltas = null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async _reloadManifestArtifacts(manifest, options = {}) {
|
|
493
|
+
this._syncManifestPaths(manifest);
|
|
494
|
+
const reloadScope = options.reloadScope || 'all';
|
|
495
|
+
const grepOnly = reloadScope === 'grep';
|
|
496
|
+
|
|
497
|
+
if (!grepOnly) {
|
|
498
|
+
this.hasBinaryHnswIndex = existsSync(this.binaryHnswPath.replace('.idx', '.meta.json'));
|
|
499
|
+
}
|
|
500
|
+
if (!grepOnly && this.hasBinaryHnswIndex && this.use3Stage) {
|
|
501
|
+
try {
|
|
502
|
+
const nextBinary = new BinaryHNSWIndex({
|
|
503
|
+
indexPath: this.binaryHnswPath,
|
|
504
|
+
stalePath: this.binaryHnswIndex?.stalePath || `${this.binaryHnswPath}.stale.bin`,
|
|
505
|
+
});
|
|
506
|
+
await nextBinary.load();
|
|
507
|
+
this.binaryHnswIndex = nextBinary;
|
|
508
|
+
this.floatVectorStore = new FloatVectorStore();
|
|
509
|
+
await this.floatVectorStore.load(getFloatStorePath(this.binaryHnswPath));
|
|
510
|
+
} catch (err) {
|
|
511
|
+
this.log(`BinaryHNSW: Failed to reload after manifest publish: ${err.message}`);
|
|
512
|
+
this.hasBinaryHnswIndex = false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!grepOnly) {
|
|
517
|
+
this.hasHnswIndex = existsSync(this.hnswPath.replace('.idx', '.meta.json'));
|
|
518
|
+
}
|
|
519
|
+
if (!grepOnly && this.hasHnswIndex) {
|
|
520
|
+
try {
|
|
521
|
+
const nextHnsw = new HNSWIndex({
|
|
522
|
+
indexPath: this.hnswPath,
|
|
523
|
+
stalePath: this.hnswIndex?.stalePath || `${this.hnswPath}.stale.bin`,
|
|
524
|
+
});
|
|
525
|
+
await nextHnsw.load(undefined, { mmap: true });
|
|
526
|
+
this.hnswIndex = nextHnsw;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
this.log(`HNSW: Failed to reload after manifest publish: ${err.message}`);
|
|
529
|
+
this.hasHnswIndex = false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (!grepOnly) {
|
|
534
|
+
this.hasLateInteractionIndex = existsSync(this.lateInteractionIndex.indexPath);
|
|
535
|
+
}
|
|
536
|
+
if (!grepOnly && this.hasLateInteractionIndex) {
|
|
537
|
+
try {
|
|
538
|
+
const nextLi = new LateInteractionIndex(this._lateInteractionOptions);
|
|
539
|
+
await nextLi.init();
|
|
540
|
+
this.lateInteractionIndex = nextLi;
|
|
541
|
+
this._clearChunkLocationCache();
|
|
542
|
+
const liManifest = {
|
|
543
|
+
modelId: this.lateInteractionIndex.modelId ?? null,
|
|
544
|
+
tokenDim: this.lateInteractionIndex.tokenDim ?? null,
|
|
545
|
+
modelMismatch: this.lateInteractionIndex.modelMismatch === true,
|
|
546
|
+
exists: true,
|
|
547
|
+
};
|
|
548
|
+
const resolved = resolveSearchRerankPolicy({
|
|
549
|
+
optionOverride: this._liPolicyOptionOverride,
|
|
550
|
+
env: process.env,
|
|
551
|
+
persisted: this._liPolicyPersisted,
|
|
552
|
+
indexManifest: liManifest,
|
|
553
|
+
activeConfigModel: LATE_INTERACTION_CONFIG.model,
|
|
554
|
+
});
|
|
555
|
+
this._liPolicyResolved = resolved;
|
|
556
|
+
this.useLateInteraction = LATE_INTERACTION_CONFIG.enabled && resolved.effective;
|
|
557
|
+
} catch (err) {
|
|
558
|
+
this.log(`LateInteraction: Failed to reload after manifest publish: ${err.message}`);
|
|
559
|
+
this.hasLateInteractionIndex = false;
|
|
560
|
+
this.useLateInteraction = false;
|
|
561
|
+
this._clearChunkLocationCache();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.hasSparseGramIndex = existsSync(this.sparseGramIndexPath);
|
|
566
|
+
if (this.hasSparseGramIndex) {
|
|
567
|
+
try {
|
|
568
|
+
this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
|
|
569
|
+
this._sparseGramLoadedPath = this.sparseGramIndex ? this.sparseGramIndexPath : null;
|
|
570
|
+
} catch (err) {
|
|
571
|
+
this.log(`SparseGram: Failed to reload after manifest publish: ${err.message}`);
|
|
572
|
+
this.hasSparseGramIndex = false;
|
|
573
|
+
this.sparseGramIndex = null;
|
|
574
|
+
this._sparseGramLoadedPath = null;
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
this.sparseGramIndex = null;
|
|
578
|
+
this._sparseGramLoadedPath = null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async _refreshManifestPins(options = {}) {
|
|
583
|
+
const manifest = this._readReconcileManifest();
|
|
584
|
+
const previousArtifactEpoch = this._artifactManifestEpoch;
|
|
585
|
+
const previousCodebaseEpoch = this.codebaseRepo?.getManifestEpoch?.();
|
|
586
|
+
const manifestEpoch = Number.isInteger(manifest?.epoch) ? manifest.epoch : null;
|
|
587
|
+
const shouldReloadArtifacts = manifestEpoch !== null
|
|
588
|
+
&& previousArtifactEpoch !== manifestEpoch
|
|
589
|
+
&& (this.initialized || this.grepInitialized);
|
|
590
|
+
this._syncManifestPaths(manifest);
|
|
591
|
+
const codebaseEpoch = this.codebaseRepo?.refreshManifestEpoch?.();
|
|
592
|
+
if (previousCodebaseEpoch !== codebaseEpoch) {
|
|
593
|
+
this._clearCodebaseChunkTypeCache();
|
|
594
|
+
}
|
|
595
|
+
const graphEpoch = this.graphSearch?.refreshManifestEpoch?.();
|
|
596
|
+
this.codeGraphRepo?.refreshManifestEpoch?.();
|
|
597
|
+
// Regex/sparse-gram helpers are not repository-backed, so expose the
|
|
598
|
+
// query-pinned epoch on the searcher for their delta overlay reader.
|
|
599
|
+
this.manifestEpoch = manifestEpoch !== null
|
|
600
|
+
? manifestEpoch
|
|
601
|
+
: (Number.isInteger(codebaseEpoch) ? codebaseEpoch : graphEpoch);
|
|
602
|
+
if (shouldReloadArtifacts) {
|
|
603
|
+
await this._reloadManifestArtifacts(manifest, options);
|
|
604
|
+
}
|
|
605
|
+
if (manifestEpoch !== null) {
|
|
606
|
+
this._artifactManifestEpoch = manifestEpoch;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
335
610
|
async initGrepOnly() {
|
|
336
611
|
if (this.grepInitialized || this.initialized) return;
|
|
337
612
|
const start = Date.now();
|
|
338
613
|
|
|
614
|
+
this._syncManifestPaths(this._readReconcileManifest());
|
|
339
615
|
this.hasCodebaseIndex = existsSync(this.codebaseDbPath);
|
|
340
616
|
this.hasSparseGramIndex = existsSync(this.sparseGramIndexPath);
|
|
341
617
|
if (this.hasSparseGramIndex) {
|
|
342
618
|
try {
|
|
343
619
|
this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
|
|
344
620
|
if (this.sparseGramIndex) {
|
|
621
|
+
this._sparseGramLoadedPath = this.sparseGramIndexPath;
|
|
345
622
|
const stats = this.sparseGramIndex.getStats();
|
|
346
623
|
this.log(
|
|
347
624
|
`SparseGram: Loaded ${stats.grams} grams across ${stats.totalFiles} files ` +
|
|
@@ -354,10 +631,12 @@ export class SweetSearch {
|
|
|
354
631
|
this.log(`SparseGram: Failed to load: ${err.message}`);
|
|
355
632
|
this.hasSparseGramIndex = false;
|
|
356
633
|
this.sparseGramIndex = null;
|
|
634
|
+
this._sparseGramLoadedPath = null;
|
|
357
635
|
}
|
|
358
636
|
}
|
|
359
637
|
|
|
360
638
|
this.grepInitialized = true;
|
|
639
|
+
this._artifactManifestEpoch = this._readReconcileManifest()?.epoch ?? null;
|
|
361
640
|
this.log(`SweetSearch: Grep-only initialized in ${Date.now() - start}ms`);
|
|
362
641
|
}
|
|
363
642
|
|
|
@@ -376,7 +655,14 @@ export class SweetSearch {
|
|
|
376
655
|
} else {
|
|
377
656
|
await this.init();
|
|
378
657
|
}
|
|
658
|
+
await this._refreshManifestPins({ reloadScope: mode === 'grep' ? 'grep' : 'all' });
|
|
379
659
|
|
|
660
|
+
const readPin = beginPinnedRead({
|
|
661
|
+
stateDir: this._manifestStateDir,
|
|
662
|
+
epoch: this.manifestEpoch,
|
|
663
|
+
meta: { tool: 'search', mode, query: String(query).slice(0, 200) },
|
|
664
|
+
});
|
|
665
|
+
try {
|
|
380
666
|
const start = Date.now();
|
|
381
667
|
const stats = { query };
|
|
382
668
|
|
|
@@ -405,7 +691,12 @@ export class SweetSearch {
|
|
|
405
691
|
let searchMode;
|
|
406
692
|
if (mode === 'auto') {
|
|
407
693
|
searchMode = routing.mode;
|
|
408
|
-
stats.routing = {
|
|
694
|
+
stats.routing = {
|
|
695
|
+
mode: routing.mode,
|
|
696
|
+
confidence: routing.confidence,
|
|
697
|
+
latency_us: routing.routingLatency_us,
|
|
698
|
+
method: routing.method,
|
|
699
|
+
};
|
|
409
700
|
} else {
|
|
410
701
|
searchMode = mode;
|
|
411
702
|
stats.routing = {
|
|
@@ -422,6 +713,27 @@ export class SweetSearch {
|
|
|
422
713
|
let results;
|
|
423
714
|
let semanticStats = null;
|
|
424
715
|
|
|
716
|
+
// Search-scoped caches for the per-result helpers in
|
|
717
|
+
// applyResultDemotions. Shared across the two demotion sites (one inside
|
|
718
|
+
// hybridSearchV2, one inside _applyPostRetrieval). Most top-K chunks
|
|
719
|
+
// appear in BOTH sites' input sets, so cross-call reuse stacks on top of
|
|
720
|
+
// the intra-call memoization in file-kind-ranking.js. Freshly allocated
|
|
721
|
+
// per search() call — never reused across queries.
|
|
722
|
+
// _entityKindCache : SQLite enclosing/contained entity lookup
|
|
723
|
+
// _entityNameCache : SQLite findEntityWithNameInRange (symbol-target)
|
|
724
|
+
// _resultTextCache : readFileSync source-span
|
|
725
|
+
// _fullFileTextCache : readFileSync FULL file (test-support detection)
|
|
726
|
+
// _isTestSupportCache : isTestSupportFile() per-file verdict
|
|
727
|
+
// _isTestChunkCache : isTestChunk() per-chunk verdict
|
|
728
|
+
// _fileKindCache : detectFileKind() per-file verdict
|
|
729
|
+
const _entityKindCache = new Map();
|
|
730
|
+
const _entityNameCache = new Map();
|
|
731
|
+
const _resultTextCache = new Map();
|
|
732
|
+
const _fullFileTextCache = new Map();
|
|
733
|
+
const _isTestSupportCache = new Map();
|
|
734
|
+
const _isTestChunkCache = new Map();
|
|
735
|
+
const _fileKindCache = new Map();
|
|
736
|
+
|
|
425
737
|
switch (searchMode) {
|
|
426
738
|
case 'grep': {
|
|
427
739
|
const grepResult = await this.bareGrep(query, routing, {
|
|
@@ -465,7 +777,13 @@ export class SweetSearch {
|
|
|
465
777
|
break;
|
|
466
778
|
}
|
|
467
779
|
case 'semantic': {
|
|
468
|
-
const semanticResult = await this.semanticSearch(query, {
|
|
780
|
+
const semanticResult = await this.semanticSearch(query, {
|
|
781
|
+
k,
|
|
782
|
+
rerank,
|
|
783
|
+
useLateInteraction,
|
|
784
|
+
format: options.format,
|
|
785
|
+
ablations: options.ablations,
|
|
786
|
+
});
|
|
469
787
|
results = semanticResult.results;
|
|
470
788
|
semanticStats = semanticResult.stats;
|
|
471
789
|
stats.path = 'semantic';
|
|
@@ -473,13 +791,40 @@ export class SweetSearch {
|
|
|
473
791
|
}
|
|
474
792
|
case 'hybrid':
|
|
475
793
|
default: {
|
|
476
|
-
const hybridResult = await this.hybridSearchV2(query, {
|
|
794
|
+
const hybridResult = await this.hybridSearchV2(query, {
|
|
795
|
+
k,
|
|
796
|
+
useLateInteraction,
|
|
797
|
+
format: options.format,
|
|
798
|
+
routing,
|
|
799
|
+
ablations: options.ablations,
|
|
800
|
+
useMMR: options.useMMR,
|
|
801
|
+
allowQueryRewrite: options.allowQueryRewrite,
|
|
802
|
+
allowKeywordFallback: options.allowKeywordFallback,
|
|
803
|
+
confidenceFloor: options.confidenceFloor,
|
|
804
|
+
fileKindWindow: options.fileKindWindow,
|
|
805
|
+
hybridDocFactor: options.hybridDocFactor,
|
|
806
|
+
hybridTestFactor: options.hybridTestFactor,
|
|
807
|
+
hybridTypeFactor: options.hybridTypeFactor,
|
|
808
|
+
hybridAncillaryFactor: options.hybridAncillaryFactor,
|
|
809
|
+
hybridTinyAncillaryFactor: options.hybridTinyAncillaryFactor,
|
|
810
|
+
resultDemotionWindow: options.resultDemotionWindow,
|
|
811
|
+
_entityKindCache,
|
|
812
|
+
_entityNameCache,
|
|
813
|
+
_resultTextCache,
|
|
814
|
+
_fullFileTextCache,
|
|
815
|
+
_isTestSupportCache,
|
|
816
|
+
_isTestChunkCache,
|
|
817
|
+
_fileKindCache,
|
|
818
|
+
});
|
|
477
819
|
results = hybridResult.results || hybridResult;
|
|
478
820
|
semanticStats = hybridResult.semanticStats || null;
|
|
479
821
|
stats.path = 'hybrid';
|
|
480
822
|
stats.fusion = hybridResult.fusionStats?.method || 'cc';
|
|
481
823
|
stats.fusionFallback = hybridResult.fusionStats?.fallbackReason || null;
|
|
482
824
|
stats.lexicalLatencyMs = hybridResult.fusionStats?.lexicalLatencyMs ?? null;
|
|
825
|
+
if (hybridResult.fusionStats?.queryRewrite) {
|
|
826
|
+
stats.queryRewrite = hybridResult.fusionStats.queryRewrite;
|
|
827
|
+
}
|
|
483
828
|
break;
|
|
484
829
|
}
|
|
485
830
|
}
|
|
@@ -492,9 +837,62 @@ export class SweetSearch {
|
|
|
492
837
|
effectiveGraphExpand = '2hop';
|
|
493
838
|
}
|
|
494
839
|
|
|
840
|
+
// Empty-result rescue (added 2026-05-07 — FreshStack uv diagnosis).
|
|
841
|
+
// The joint hybrid pipeline can return [] in two cascading-failure
|
|
842
|
+
// scenarios:
|
|
843
|
+
// (a) BM25 lexical returns nothing (FTS5 tokenization quirk on multi-
|
|
844
|
+
// word NL queries with stop-words like "trace how X uses Y"), AND
|
|
845
|
+
// (b) the dense path returns candidates that ALL trip post-fusion
|
|
846
|
+
// demotions to ≈0 score, AND
|
|
847
|
+
// (c) RRF fallback inside hybridSearchV2 also produces nothing because
|
|
848
|
+
// its keyword splitter sees the same FTS-empty result.
|
|
849
|
+
// Diagnosed on UV-FLOW-1 / UV-FLOW-4 (post-cutoff uv): both well-formed
|
|
850
|
+
// NL queries with concrete tokens (uv, add, dependency, pyproject, toml)
|
|
851
|
+
// returned in 3ms. That is a query-pipeline pathology, not a corpus gap.
|
|
852
|
+
// The principled fix (cascading retrieval — Thakur et al. BEIR 2024;
|
|
853
|
+
// Lin & Ma "Tiered Retrieval" 2025): when all upstream paths produce
|
|
854
|
+
// empty, fall back to ONE pure-dense call on the raw query string with
|
|
855
|
+
// no rerank, no graph, no fusion — this guarantees we always return
|
|
856
|
+
// something for any NL query the encoder can embed. Disable via
|
|
857
|
+
// `ablations: ['no-empty-rescue']`.
|
|
858
|
+
const emptyRescueAllowed = !(options.ablations && (
|
|
859
|
+
options.ablations instanceof Set
|
|
860
|
+
? options.ablations.has('no-empty-rescue')
|
|
861
|
+
: Array.isArray(options.ablations) && options.ablations.includes('no-empty-rescue')
|
|
862
|
+
));
|
|
863
|
+
if (emptyRescueAllowed
|
|
864
|
+
&& Array.isArray(results)
|
|
865
|
+
&& results.length === 0
|
|
866
|
+
&& (searchMode === 'hybrid' || searchMode === 'semantic' || searchMode === 'lexical')
|
|
867
|
+
&& expand) {
|
|
868
|
+
try {
|
|
869
|
+
const rescue = await this.semanticSearch(query, {
|
|
870
|
+
k: Math.max(k, 10),
|
|
871
|
+
rerank: false,
|
|
872
|
+
useLateInteraction: false,
|
|
873
|
+
});
|
|
874
|
+
const rescuedResults = rescue.results || [];
|
|
875
|
+
if (rescuedResults.length > 0) {
|
|
876
|
+
results = rescuedResults.map(r => ({ ...r, searchPath: 'empty-rescue-dense' }));
|
|
877
|
+
stats.emptyRescue = {
|
|
878
|
+
triggered: true,
|
|
879
|
+
recovered: rescuedResults.length,
|
|
880
|
+
mode: 'pure-dense-raw',
|
|
881
|
+
};
|
|
882
|
+
this.log(`Empty-rescue: hybrid returned 0; pure-dense recovered ${rescuedResults.length} candidates`);
|
|
883
|
+
} else {
|
|
884
|
+
stats.emptyRescue = { triggered: true, recovered: 0 };
|
|
885
|
+
}
|
|
886
|
+
} catch (err) {
|
|
887
|
+
stats.emptyRescue = { triggered: true, error: err.message };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
495
891
|
// Step 3: Post-retrieval processing (delegated to extracted module)
|
|
496
892
|
const postRetrievalResult = await this._applyPostRetrieval(results, query, options, {
|
|
497
|
-
stats, semanticStats, searchMode, effectiveGraphExpand, intentPolicy, start,
|
|
893
|
+
stats, semanticStats, searchMode, effectiveGraphExpand, intentPolicy, start, fromSearch: true,
|
|
894
|
+
_entityKindCache, _entityNameCache, _resultTextCache, _fullFileTextCache,
|
|
895
|
+
_isTestSupportCache, _isTestChunkCache, _fileKindCache,
|
|
498
896
|
});
|
|
499
897
|
|
|
500
898
|
// Step 4: Agent packaging (lexical/semantic/hybrid/structural).
|
|
@@ -506,6 +904,7 @@ export class SweetSearch {
|
|
|
506
904
|
if (agentFormats.has(options.format)) {
|
|
507
905
|
const finalResults = postRetrievalResult.results || [];
|
|
508
906
|
const finalStats = postRetrievalResult.stats || {};
|
|
907
|
+
const __t_pkg = __ptStart();
|
|
509
908
|
const agentResponse = packageForAgent(finalResults, {
|
|
510
909
|
...finalStats,
|
|
511
910
|
candidatePoolSize: finalStats.results_count ?? finalResults.length,
|
|
@@ -520,12 +919,16 @@ export class SweetSearch {
|
|
|
520
919
|
projectRoot: this.projectRoot,
|
|
521
920
|
ablations: options.ablations,
|
|
522
921
|
});
|
|
922
|
+
__ptEnd('packageForAgent', __t_pkg);
|
|
523
923
|
// Preserve the underlying retrieval stats so callers can inspect both layers
|
|
524
924
|
agentResponse.stats = finalStats;
|
|
525
925
|
return agentResponse;
|
|
526
926
|
}
|
|
527
927
|
|
|
528
928
|
return postRetrievalResult;
|
|
929
|
+
} finally {
|
|
930
|
+
endPinnedRead(readPin);
|
|
931
|
+
}
|
|
529
932
|
}
|
|
530
933
|
|
|
531
934
|
/** Structural search path (GraphRAG structural queries — opt-in via explicit flag) */
|
|
@@ -575,10 +978,11 @@ export class SweetSearch {
|
|
|
575
978
|
/** Semantic search dispatcher. Delegates to 3Stage or Standard based on config. */
|
|
576
979
|
async semanticSearch(query, options = {}) {
|
|
577
980
|
const { k = 10, rerank = true, useLateInteraction = this.useLateInteraction } = options;
|
|
981
|
+
const semanticOptions = { ...options, k, rerank, useLateInteraction };
|
|
578
982
|
if (this.hasBinaryHnswIndex && this.use3Stage) {
|
|
579
|
-
return this.semanticSearch3Stage(query,
|
|
983
|
+
return this.semanticSearch3Stage(query, semanticOptions);
|
|
580
984
|
}
|
|
581
|
-
return this.semanticSearchStandard(query,
|
|
985
|
+
return this.semanticSearchStandard(query, semanticOptions);
|
|
582
986
|
}
|
|
583
987
|
|
|
584
988
|
/** O(N) vector scan fallback (when HNSW not available). Filters stale entities. */
|
|
@@ -681,6 +1085,7 @@ Object.assign(SweetSearch.prototype, {
|
|
|
681
1085
|
variance: fusion.variance,
|
|
682
1086
|
getBoostIntent: boost.getBoostIntent,
|
|
683
1087
|
applyPostFusionBoosts: boost.applyPostFusionBoosts,
|
|
1088
|
+
computeIdentifierAgreementBoost: boost.computeIdentifierAgreementBoost,
|
|
684
1089
|
computeDefinitionBoost: boost.computeDefinitionBoost,
|
|
685
1090
|
computeSyntaxBoost: boost.computeSyntaxBoost,
|
|
686
1091
|
computePositionBoost: boost.computePositionBoost,
|
|
@@ -32,18 +32,20 @@ lifecycle end-to-end:
|
|
|
32
32
|
fighting with the GPU models about to be loaded.
|
|
33
33
|
2. **Detect best backend** via `hardware-capability.js` —
|
|
34
34
|
`coreml-cascade` on M3+ Apple Silicon, `candle-metal` on M1/M2,
|
|
35
|
-
`candle-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
`candle-cuda` on Linux + NVIDIA, and the optimized **ORT INT8 CPU** path
|
|
36
|
+
on any host with no usable accelerator (no GPU models are loaded there).
|
|
37
|
+
3. **Load GPU models + warmup forward pass** (accelerator hosts only) —
|
|
38
|
+
compiles Metal pipelines, CoreML variant bundles, and BLAS thread pools
|
|
39
|
+
so the first indexing batch pays no cold-start cost.
|
|
39
40
|
4. **Index the codebase** — code graph, vector embeddings, HNSW,
|
|
40
41
|
late-interaction index, quantized artifacts, sparse-gram index.
|
|
41
42
|
5. **Kill GPU models** — releases Metal queues and Neural Engine.
|
|
42
43
|
6. **Load + warmup ORT CPU models** — both embedding and LI get one dummy
|
|
43
44
|
forward pass so the first query after indexing is warm.
|
|
44
45
|
|
|
45
|
-
On small-changeset incremental runs (under 20 files)
|
|
46
|
-
|
|
46
|
+
On small-changeset incremental runs (under 20 files) — and on any host with
|
|
47
|
+
no usable accelerator — the indexer skips the GPU swap entirely and indexes on
|
|
48
|
+
the optimized ORT INT8 CPU path.
|
|
47
49
|
|
|
48
50
|
## Usage
|
|
49
51
|
|
package/core/start-server.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Minimal server-start entry point — avoids the circular import in sweet-search.js.
|
|
3
|
-
// Used by the Rust CLI's auto_start_server() to spawn the background server
|
|
3
|
+
// Used by the Rust CLI's auto_start_server() to spawn the background server,
|
|
4
|
+
// and by the SessionStart daemon-prewarm hook (core/search/session-daemon-prewarm.mjs)
|
|
5
|
+
// when Claude Code opens a new session.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
// Apply the user's persisted `runtime.li.model` from .sweet-search/config.json
|
|
8
|
+
// BEFORE importing search-server (which transitively imports session-warmup,
|
|
9
|
+
// which gates warmup steps on `LATE_INTERACTION_CONFIG.enabled` and triggers a
|
|
10
|
+
// warmup search using `LATE_INTERACTION_CONFIG.model`). Without this, an
|
|
11
|
+
// edge-only init still spawns a daemon that prewarms the standard model.
|
|
12
|
+
const projectRoot = process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd();
|
|
13
|
+
const { applyPersistedLiModel } = await import('./infrastructure/init-config.js');
|
|
14
|
+
applyPersistedLiModel(projectRoot);
|
|
15
|
+
|
|
16
|
+
const { startServer } = await import('./search/search-server.js');
|
|
6
17
|
await startServer();
|