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.
Files changed (159) hide show
  1. package/core/cli.js +45 -0
  2. package/core/embedding/embedding-cache.js +90 -4
  3. package/core/embedding/embedding-service.js +27 -5
  4. package/core/graph/graph-expansion.js +215 -36
  5. package/core/graph/graph-extractor.js +196 -11
  6. package/core/graph/graph-search.js +395 -92
  7. package/core/graph/hcgs-generator.js +2 -1
  8. package/core/graph/index.js +2 -0
  9. package/core/graph/repo-map.js +28 -6
  10. package/core/graph/structural-answer-cues.js +168 -0
  11. package/core/graph/structural-callsite-hints.js +40 -0
  12. package/core/graph/structural-context-format.js +40 -0
  13. package/core/graph/structural-context.js +450 -0
  14. package/core/graph/structural-forward-push.js +156 -0
  15. package/core/graph/structural-header-context.js +19 -0
  16. package/core/graph/structural-importance.js +148 -0
  17. package/core/graph/structural-pagerank.js +197 -0
  18. package/core/graph/summary-manager.js +13 -9
  19. package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
  20. package/core/incremental-indexing/application/file-watcher.mjs +197 -0
  21. package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
  22. package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
  23. package/core/incremental-indexing/application/operator-cli.mjs +554 -0
  24. package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
  25. package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
  26. package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
  27. package/core/incremental-indexing/application/reconciler.mjs +477 -0
  28. package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
  29. package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
  30. package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
  31. package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
  32. package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
  33. package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
  34. package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
  35. package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
  36. package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
  37. package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
  38. package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
  39. package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
  40. package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
  41. package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
  42. package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
  43. package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
  44. package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
  45. package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
  46. package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
  47. package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
  48. package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
  49. package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
  50. package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
  51. package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
  52. package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
  53. package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
  54. package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
  55. package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
  56. package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
  57. package/core/indexing/admission-policy.js +139 -0
  58. package/core/indexing/artifact-builder.js +29 -12
  59. package/core/indexing/ast-chunker.js +107 -30
  60. package/core/indexing/dedup/exemplar-selector.js +19 -1
  61. package/core/indexing/gitignore-filter.js +223 -0
  62. package/core/indexing/incremental-tracker.js +99 -30
  63. package/core/indexing/index-codebase-v21.js +37 -7
  64. package/core/indexing/index-maintainer.mjs +698 -6
  65. package/core/indexing/indexer-ann.js +99 -15
  66. package/core/indexing/indexer-build.js +158 -45
  67. package/core/indexing/indexer-empty-baseline.js +80 -0
  68. package/core/indexing/indexer-manifest.js +66 -0
  69. package/core/indexing/indexer-phases.js +56 -23
  70. package/core/indexing/indexer-sparse-gram.js +54 -13
  71. package/core/indexing/indexer-utils.js +26 -208
  72. package/core/indexing/indexing-file-policy.js +32 -7
  73. package/core/indexing/maintainer-launcher.mjs +137 -0
  74. package/core/indexing/merkle-tracker.js +251 -244
  75. package/core/indexing/model-pool.js +46 -5
  76. package/core/infrastructure/code-graph-repository.js +758 -6
  77. package/core/infrastructure/code-graph-visibility.js +157 -0
  78. package/core/infrastructure/codebase-repository.js +100 -13
  79. package/core/infrastructure/config/search.js +1 -1
  80. package/core/infrastructure/db-utils.js +118 -0
  81. package/core/infrastructure/dedup-hashing.js +10 -13
  82. package/core/infrastructure/hardware-capability.js +17 -7
  83. package/core/infrastructure/index.js +10 -2
  84. package/core/infrastructure/init-config.js +138 -0
  85. package/core/infrastructure/language-patterns/maps.js +4 -1
  86. package/core/infrastructure/language-patterns/registry-core.js +56 -17
  87. package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
  88. package/core/infrastructure/language-patterns.js +69 -0
  89. package/core/infrastructure/model-registry.js +20 -0
  90. package/core/infrastructure/native-inference.js +7 -12
  91. package/core/infrastructure/native-resolver.js +52 -37
  92. package/core/infrastructure/native-sparse-gram.js +261 -20
  93. package/core/infrastructure/native-tokenizer.js +6 -15
  94. package/core/infrastructure/simd-distance.js +10 -16
  95. package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
  96. package/core/infrastructure/structural-alias-resolver.js +122 -0
  97. package/core/infrastructure/structural-candidate-ranker.js +34 -0
  98. package/core/infrastructure/structural-context-repository.js +472 -0
  99. package/core/infrastructure/structural-context-utils.js +51 -0
  100. package/core/infrastructure/structural-graph-signals.js +121 -0
  101. package/core/infrastructure/structural-qualified-resolution.js +15 -0
  102. package/core/infrastructure/structural-source-definitions.js +100 -0
  103. package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
  104. package/core/infrastructure/tree-sitter-provider.js +811 -37
  105. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
  106. package/core/query/query-router.js +55 -5
  107. package/core/ranking/file-kind-ranking.js +2192 -15
  108. package/core/ranking/late-interaction-index.js +87 -12
  109. package/core/search/cli-decoration.js +290 -0
  110. package/core/search/context-expander.js +988 -78
  111. package/core/search/index.js +1 -0
  112. package/core/search/output-policy.js +275 -0
  113. package/core/search/search-anchor.js +499 -0
  114. package/core/search/search-boost.js +93 -1
  115. package/core/search/search-cli.js +61 -204
  116. package/core/search/search-hybrid.js +250 -10
  117. package/core/search/search-pattern-chunks.js +57 -8
  118. package/core/search/search-pattern-planner.js +68 -9
  119. package/core/search/search-pattern-prefilter.js +30 -10
  120. package/core/search/search-pattern-ripgrep.js +40 -4
  121. package/core/search/search-pattern-sparse-overlay.js +256 -0
  122. package/core/search/search-pattern.js +117 -29
  123. package/core/search/search-postprocess.js +479 -5
  124. package/core/search/search-read-semantic.js +277 -23
  125. package/core/search/search-read.js +82 -64
  126. package/core/search/search-reader-pin.js +71 -0
  127. package/core/search/search-rrf.js +279 -0
  128. package/core/search/search-semantic.js +110 -5
  129. package/core/search/search-server.js +273 -54
  130. package/core/search/search-trace.js +107 -0
  131. package/core/search/server-identity.js +93 -0
  132. package/core/search/session-daemon-prewarm.mjs +33 -10
  133. package/core/search/sweet-search.js +414 -9
  134. package/core/skills/sweet-index/SKILL.md +8 -6
  135. package/core/start-server.js +13 -2
  136. package/core/vector-store/binary-hnsw-index.js +194 -30
  137. package/core/vector-store/float-vector-store.js +96 -6
  138. package/core/vector-store/hnsw-index.js +220 -49
  139. package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
  140. package/eval/agent-read-workflows/bin/ss-find +15 -0
  141. package/eval/agent-read-workflows/bin/ss-grep +12 -0
  142. package/eval/agent-read-workflows/bin/ss-read +14 -0
  143. package/eval/agent-read-workflows/bin/ss-search +18 -0
  144. package/eval/agent-read-workflows/bin/ss-semantic +12 -0
  145. package/eval/agent-read-workflows/bin/ss-trace +11 -0
  146. package/mcp/read-tool.js +109 -0
  147. package/mcp/server.js +55 -15
  148. package/mcp/tool-handlers.js +14 -124
  149. package/mcp/trace-tool.js +81 -0
  150. package/package.json +25 -10
  151. package/scripts/hooks/intercept-read.mjs +55 -0
  152. package/scripts/hooks/remind-tools.mjs +40 -0
  153. package/scripts/init.js +698 -54
  154. package/scripts/inject-agent-instructions.js +431 -0
  155. package/scripts/install-prompt-reminders.js +188 -0
  156. package/scripts/install-tool-enforcement.js +220 -0
  157. package/scripts/smoke-test.js +12 -9
  158. package/scripts/uninstall.js +427 -23
  159. 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.codebaseDbPath);
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 = { mode: routing.mode, confidence: routing.confidence, latency_us: routing.routingLatency_us };
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, { k, rerank, useLateInteraction });
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, { k, useLateInteraction, routing });
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, { k, rerank, useLateInteraction });
983
+ return this.semanticSearch3Stage(query, semanticOptions);
580
984
  }
581
- return this.semanticSearchStandard(query, { k, rerank });
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-cpu` elsewhere.
36
- 3. **Load GPU models + warmup forward pass** compiles Metal pipelines,
37
- CoreML variant bundles, and BLAS thread pools so the first indexing
38
- batch pays no cold-start cost.
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), the indexer skips the
46
- GPU swap entirely — the load/warmup overhead would dwarf the actual work.
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
 
@@ -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
- import { startServer } from './search/search-server.js';
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();