sweet-search 2.5.2 → 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 (155) hide show
  1. package/core/cli.js +24 -3
  2. package/core/graph/graph-expansion.js +215 -36
  3. package/core/graph/graph-extractor.js +196 -11
  4. package/core/graph/graph-search.js +395 -92
  5. package/core/graph/hcgs-generator.js +2 -1
  6. package/core/graph/index.js +2 -0
  7. package/core/graph/repo-map.js +28 -6
  8. package/core/graph/structural-answer-cues.js +168 -0
  9. package/core/graph/structural-callsite-hints.js +40 -0
  10. package/core/graph/structural-context-format.js +40 -0
  11. package/core/graph/structural-context.js +450 -0
  12. package/core/graph/structural-forward-push.js +156 -0
  13. package/core/graph/structural-header-context.js +19 -0
  14. package/core/graph/structural-importance.js +148 -0
  15. package/core/graph/structural-pagerank.js +197 -0
  16. package/core/graph/summary-manager.js +13 -9
  17. package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
  18. package/core/incremental-indexing/application/file-watcher.mjs +197 -0
  19. package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
  20. package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
  21. package/core/incremental-indexing/application/operator-cli.mjs +554 -0
  22. package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
  23. package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
  24. package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
  25. package/core/incremental-indexing/application/reconciler.mjs +477 -0
  26. package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
  27. package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
  28. package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
  29. package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
  30. package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
  31. package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
  32. package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
  33. package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
  34. package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
  35. package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
  36. package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
  37. package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
  38. package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
  39. package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
  40. package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
  41. package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
  42. package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
  43. package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
  44. package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
  45. package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
  46. package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
  47. package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
  48. package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
  49. package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
  50. package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
  51. package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
  52. package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
  53. package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
  54. package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
  55. package/core/indexing/admission-policy.js +139 -0
  56. package/core/indexing/artifact-builder.js +29 -12
  57. package/core/indexing/ast-chunker.js +107 -30
  58. package/core/indexing/dedup/exemplar-selector.js +19 -1
  59. package/core/indexing/gitignore-filter.js +223 -0
  60. package/core/indexing/incremental-tracker.js +99 -30
  61. package/core/indexing/index-codebase-v21.js +6 -5
  62. package/core/indexing/index-maintainer.mjs +698 -6
  63. package/core/indexing/indexer-ann.js +99 -15
  64. package/core/indexing/indexer-build.js +158 -45
  65. package/core/indexing/indexer-empty-baseline.js +80 -0
  66. package/core/indexing/indexer-manifest.js +66 -0
  67. package/core/indexing/indexer-phases.js +56 -23
  68. package/core/indexing/indexer-sparse-gram.js +54 -13
  69. package/core/indexing/indexer-utils.js +26 -208
  70. package/core/indexing/indexing-file-policy.js +32 -7
  71. package/core/indexing/maintainer-launcher.mjs +137 -0
  72. package/core/indexing/merkle-tracker.js +251 -244
  73. package/core/indexing/model-pool.js +46 -5
  74. package/core/infrastructure/code-graph-repository.js +758 -6
  75. package/core/infrastructure/code-graph-visibility.js +157 -0
  76. package/core/infrastructure/codebase-repository.js +100 -13
  77. package/core/infrastructure/config/search.js +1 -1
  78. package/core/infrastructure/db-utils.js +118 -0
  79. package/core/infrastructure/dedup-hashing.js +10 -13
  80. package/core/infrastructure/hardware-capability.js +17 -7
  81. package/core/infrastructure/index.js +8 -2
  82. package/core/infrastructure/language-patterns/maps.js +4 -1
  83. package/core/infrastructure/language-patterns/registry-core.js +56 -17
  84. package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
  85. package/core/infrastructure/language-patterns.js +69 -0
  86. package/core/infrastructure/model-registry.js +20 -0
  87. package/core/infrastructure/native-inference.js +7 -12
  88. package/core/infrastructure/native-resolver.js +52 -37
  89. package/core/infrastructure/native-sparse-gram.js +261 -20
  90. package/core/infrastructure/native-tokenizer.js +6 -15
  91. package/core/infrastructure/simd-distance.js +10 -16
  92. package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
  93. package/core/infrastructure/structural-alias-resolver.js +122 -0
  94. package/core/infrastructure/structural-candidate-ranker.js +34 -0
  95. package/core/infrastructure/structural-context-repository.js +472 -0
  96. package/core/infrastructure/structural-context-utils.js +51 -0
  97. package/core/infrastructure/structural-graph-signals.js +121 -0
  98. package/core/infrastructure/structural-qualified-resolution.js +15 -0
  99. package/core/infrastructure/structural-source-definitions.js +100 -0
  100. package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
  101. package/core/infrastructure/tree-sitter-provider.js +811 -37
  102. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
  103. package/core/query/query-router.js +55 -5
  104. package/core/ranking/file-kind-ranking.js +2192 -15
  105. package/core/ranking/late-interaction-index.js +87 -12
  106. package/core/search/cli-decoration.js +290 -0
  107. package/core/search/context-expander.js +988 -78
  108. package/core/search/index.js +1 -0
  109. package/core/search/output-policy.js +275 -0
  110. package/core/search/search-anchor.js +499 -0
  111. package/core/search/search-boost.js +93 -1
  112. package/core/search/search-cli.js +61 -204
  113. package/core/search/search-hybrid.js +250 -10
  114. package/core/search/search-pattern-chunks.js +57 -8
  115. package/core/search/search-pattern-planner.js +68 -9
  116. package/core/search/search-pattern-prefilter.js +30 -10
  117. package/core/search/search-pattern-ripgrep.js +40 -4
  118. package/core/search/search-pattern-sparse-overlay.js +256 -0
  119. package/core/search/search-pattern.js +117 -29
  120. package/core/search/search-postprocess.js +479 -5
  121. package/core/search/search-read-semantic.js +260 -23
  122. package/core/search/search-read.js +82 -64
  123. package/core/search/search-reader-pin.js +71 -0
  124. package/core/search/search-rrf.js +279 -0
  125. package/core/search/search-semantic.js +110 -5
  126. package/core/search/search-server.js +130 -57
  127. package/core/search/search-trace.js +107 -0
  128. package/core/search/server-identity.js +93 -0
  129. package/core/search/session-daemon-prewarm.mjs +33 -10
  130. package/core/search/sweet-search.js +399 -7
  131. package/core/skills/sweet-index/SKILL.md +8 -6
  132. package/core/vector-store/binary-hnsw-index.js +194 -30
  133. package/core/vector-store/float-vector-store.js +96 -6
  134. package/core/vector-store/hnsw-index.js +220 -49
  135. package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
  136. package/eval/agent-read-workflows/bin/ss-find +15 -0
  137. package/eval/agent-read-workflows/bin/ss-grep +12 -0
  138. package/eval/agent-read-workflows/bin/ss-read +14 -0
  139. package/eval/agent-read-workflows/bin/ss-search +18 -0
  140. package/eval/agent-read-workflows/bin/ss-semantic +12 -0
  141. package/eval/agent-read-workflows/bin/ss-trace +11 -0
  142. package/mcp/read-tool.js +109 -0
  143. package/mcp/server.js +55 -15
  144. package/mcp/tool-handlers.js +14 -124
  145. package/mcp/trace-tool.js +81 -0
  146. package/package.json +25 -10
  147. package/scripts/hooks/intercept-read.mjs +55 -0
  148. package/scripts/hooks/remind-tools.mjs +40 -0
  149. package/scripts/init.js +698 -54
  150. package/scripts/inject-agent-instructions.js +431 -0
  151. package/scripts/install-prompt-reminders.js +188 -0
  152. package/scripts/install-tool-enforcement.js +220 -0
  153. package/scripts/smoke-test.js +12 -9
  154. package/scripts/uninstall.js +276 -18
  155. 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';
@@ -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);
@@ -99,12 +112,25 @@ export class SweetSearch {
99
112
  // standard model path on every search. Env var still wins; see
100
113
  // applyPersistedLiModel for the full precedence ladder.
101
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
+ }
102
127
  const projectConfig = loadProjectConfig(projectRoot);
103
128
  const projectCascade = projectConfig.cascade || {};
104
129
  const envOrProject = (envKey, cascadeKey, configKey) =>
105
130
  process.env[envKey] != null ? CASCADE_CONFIG[configKey] : projectCascade[cascadeKey];
106
131
 
107
132
  this.graphDbPath = options.graphDbPath || DB_PATHS.codeGraph;
133
+ this._manifestGraphDbPath = this.graphDbPath;
108
134
  this.graphSearch = new GraphSearch(this.graphDbPath);
109
135
  this.codeGraphRepo = new CodeGraphRepository(this.graphDbPath);
110
136
  this.hnswPath = options.hnswPath || DB_PATHS.hnswIndex;
@@ -115,6 +141,8 @@ export class SweetSearch {
115
141
  this.lateInteractionIndex = new LateInteractionIndex(options.lateInteractionOptions || {});
116
142
  this.router = new QueryRouter();
117
143
  this.codebaseDbPath = options.codebaseDbPath || DB_PATHS.codebase;
144
+ this._manifestCodebaseDbPath = this.codebaseDbPath;
145
+ this._manifestStateDir = path.dirname(this._manifestCodebaseDbPath);
118
146
  this.sparseGramIndexPath = options.sparseGramIndexPath || DB_PATHS.sparseGramIndex;
119
147
  this.verbose = options.verbose ?? LOGGING.verbose;
120
148
  this.timing = options.timing ?? LOGGING.timing;
@@ -167,10 +195,23 @@ export class SweetSearch {
167
195
  ?? CASCADE_CONFIG.shadowMode;
168
196
  setRepoMapModule({ pageRank, loadGraph, buildAdjacency });
169
197
  this._qualityScorer = null;
170
- this.codebaseRepo = new CodebaseRepository(this.codebaseDbPath);
198
+ this.codebaseRepo = new CodebaseRepository(this._manifestCodebaseDbPath);
171
199
  this.sparseGramIndex = null;
200
+ this._sparseGramLoadedPath = null;
172
201
  this.grepInitialized = false;
173
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;
174
215
  }
175
216
 
176
217
  /** @deprecated Use codebaseRepo methods instead. Bridge for legacy callers. */
@@ -184,6 +225,8 @@ export class SweetSearch {
184
225
  if (this.initialized) return;
185
226
  const start = Date.now();
186
227
 
228
+ this._syncManifestPaths(this._readReconcileManifest());
229
+
187
230
  this.hasGraphIndex = existsSync(this.graphDbPath);
188
231
  this.hasHnswIndex = existsSync(this.hnswPath.replace('.idx', '.meta.json'));
189
232
  this.hasBinaryHnswIndex = existsSync(this.binaryHnswPath.replace('.idx', '.meta.json'));
@@ -309,6 +352,7 @@ export class SweetSearch {
309
352
  try {
310
353
  this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
311
354
  if (this.sparseGramIndex) {
355
+ this._sparseGramLoadedPath = this.sparseGramIndexPath;
312
356
  const stats = this.sparseGramIndex.getStats();
313
357
  this.log(
314
358
  `SparseGram: Loaded ${stats.grams} grams across ${stats.totalFiles} files ` +
@@ -321,11 +365,27 @@ export class SweetSearch {
321
365
  this.log(`SparseGram: Failed to load: ${err.message}`);
322
366
  this.hasSparseGramIndex = false;
323
367
  this.sparseGramIndex = null;
368
+ this._sparseGramLoadedPath = null;
324
369
  }
325
370
  }
326
371
 
327
372
  await warmupEmbedding({ initVocabulary: true, initSemanticCache: true });
328
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
+
329
389
  if (shouldUseLocalReranker()) {
330
390
  try {
331
391
  const localReranker = getGlobalLocalReranker();
@@ -337,19 +397,228 @@ export class SweetSearch {
337
397
  }
338
398
 
339
399
  this.initialized = true;
400
+ this._artifactManifestEpoch = this._readReconcileManifest()?.epoch ?? null;
340
401
  this.log(`SweetSearch: Initialized in ${Date.now() - start}ms`);
341
402
  }
342
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
+
343
610
  async initGrepOnly() {
344
611
  if (this.grepInitialized || this.initialized) return;
345
612
  const start = Date.now();
346
613
 
614
+ this._syncManifestPaths(this._readReconcileManifest());
347
615
  this.hasCodebaseIndex = existsSync(this.codebaseDbPath);
348
616
  this.hasSparseGramIndex = existsSync(this.sparseGramIndexPath);
349
617
  if (this.hasSparseGramIndex) {
350
618
  try {
351
619
  this.sparseGramIndex = loadSparseGramIndex(this.sparseGramIndexPath);
352
620
  if (this.sparseGramIndex) {
621
+ this._sparseGramLoadedPath = this.sparseGramIndexPath;
353
622
  const stats = this.sparseGramIndex.getStats();
354
623
  this.log(
355
624
  `SparseGram: Loaded ${stats.grams} grams across ${stats.totalFiles} files ` +
@@ -362,10 +631,12 @@ export class SweetSearch {
362
631
  this.log(`SparseGram: Failed to load: ${err.message}`);
363
632
  this.hasSparseGramIndex = false;
364
633
  this.sparseGramIndex = null;
634
+ this._sparseGramLoadedPath = null;
365
635
  }
366
636
  }
367
637
 
368
638
  this.grepInitialized = true;
639
+ this._artifactManifestEpoch = this._readReconcileManifest()?.epoch ?? null;
369
640
  this.log(`SweetSearch: Grep-only initialized in ${Date.now() - start}ms`);
370
641
  }
371
642
 
@@ -384,7 +655,14 @@ export class SweetSearch {
384
655
  } else {
385
656
  await this.init();
386
657
  }
658
+ await this._refreshManifestPins({ reloadScope: mode === 'grep' ? 'grep' : 'all' });
387
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 {
388
666
  const start = Date.now();
389
667
  const stats = { query };
390
668
 
@@ -435,6 +713,27 @@ export class SweetSearch {
435
713
  let results;
436
714
  let semanticStats = null;
437
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
+
438
737
  switch (searchMode) {
439
738
  case 'grep': {
440
739
  const grepResult = await this.bareGrep(query, routing, {
@@ -478,7 +777,13 @@ export class SweetSearch {
478
777
  break;
479
778
  }
480
779
  case 'semantic': {
481
- 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
+ });
482
787
  results = semanticResult.results;
483
788
  semanticStats = semanticResult.stats;
484
789
  stats.path = 'semantic';
@@ -486,13 +791,40 @@ export class SweetSearch {
486
791
  }
487
792
  case 'hybrid':
488
793
  default: {
489
- 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
+ });
490
819
  results = hybridResult.results || hybridResult;
491
820
  semanticStats = hybridResult.semanticStats || null;
492
821
  stats.path = 'hybrid';
493
822
  stats.fusion = hybridResult.fusionStats?.method || 'cc';
494
823
  stats.fusionFallback = hybridResult.fusionStats?.fallbackReason || null;
495
824
  stats.lexicalLatencyMs = hybridResult.fusionStats?.lexicalLatencyMs ?? null;
825
+ if (hybridResult.fusionStats?.queryRewrite) {
826
+ stats.queryRewrite = hybridResult.fusionStats.queryRewrite;
827
+ }
496
828
  break;
497
829
  }
498
830
  }
@@ -505,9 +837,62 @@ export class SweetSearch {
505
837
  effectiveGraphExpand = '2hop';
506
838
  }
507
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
+
508
891
  // Step 3: Post-retrieval processing (delegated to extracted module)
509
892
  const postRetrievalResult = await this._applyPostRetrieval(results, query, options, {
510
- stats, semanticStats, searchMode, effectiveGraphExpand, intentPolicy, start,
893
+ stats, semanticStats, searchMode, effectiveGraphExpand, intentPolicy, start, fromSearch: true,
894
+ _entityKindCache, _entityNameCache, _resultTextCache, _fullFileTextCache,
895
+ _isTestSupportCache, _isTestChunkCache, _fileKindCache,
511
896
  });
512
897
 
513
898
  // Step 4: Agent packaging (lexical/semantic/hybrid/structural).
@@ -519,6 +904,7 @@ export class SweetSearch {
519
904
  if (agentFormats.has(options.format)) {
520
905
  const finalResults = postRetrievalResult.results || [];
521
906
  const finalStats = postRetrievalResult.stats || {};
907
+ const __t_pkg = __ptStart();
522
908
  const agentResponse = packageForAgent(finalResults, {
523
909
  ...finalStats,
524
910
  candidatePoolSize: finalStats.results_count ?? finalResults.length,
@@ -533,12 +919,16 @@ export class SweetSearch {
533
919
  projectRoot: this.projectRoot,
534
920
  ablations: options.ablations,
535
921
  });
922
+ __ptEnd('packageForAgent', __t_pkg);
536
923
  // Preserve the underlying retrieval stats so callers can inspect both layers
537
924
  agentResponse.stats = finalStats;
538
925
  return agentResponse;
539
926
  }
540
927
 
541
928
  return postRetrievalResult;
929
+ } finally {
930
+ endPinnedRead(readPin);
931
+ }
542
932
  }
543
933
 
544
934
  /** Structural search path (GraphRAG structural queries — opt-in via explicit flag) */
@@ -588,10 +978,11 @@ export class SweetSearch {
588
978
  /** Semantic search dispatcher. Delegates to 3Stage or Standard based on config. */
589
979
  async semanticSearch(query, options = {}) {
590
980
  const { k = 10, rerank = true, useLateInteraction = this.useLateInteraction } = options;
981
+ const semanticOptions = { ...options, k, rerank, useLateInteraction };
591
982
  if (this.hasBinaryHnswIndex && this.use3Stage) {
592
- return this.semanticSearch3Stage(query, { k, rerank, useLateInteraction });
983
+ return this.semanticSearch3Stage(query, semanticOptions);
593
984
  }
594
- return this.semanticSearchStandard(query, { k, rerank });
985
+ return this.semanticSearchStandard(query, semanticOptions);
595
986
  }
596
987
 
597
988
  /** O(N) vector scan fallback (when HNSW not available). Filters stale entities. */
@@ -694,6 +1085,7 @@ Object.assign(SweetSearch.prototype, {
694
1085
  variance: fusion.variance,
695
1086
  getBoostIntent: boost.getBoostIntent,
696
1087
  applyPostFusionBoosts: boost.applyPostFusionBoosts,
1088
+ computeIdentifierAgreementBoost: boost.computeIdentifierAgreementBoost,
697
1089
  computeDefinitionBoost: boost.computeDefinitionBoost,
698
1090
  computeSyntaxBoost: boost.computeSyntaxBoost,
699
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