sweet-search 2.6.8 → 2.6.9

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.
@@ -201,6 +201,18 @@ function envFloat(name, fallback, min = 0, max = 1) {
201
201
  return Number.isFinite(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
202
202
  }
203
203
 
204
+ // Query-term derivation is pure in `query`; memoize the last query so the
205
+ // per-result map in applyPostFusionBoosts doesn't re-split identical input.
206
+ let _lastIdTermsQuery = null;
207
+ let _lastIdTerms = null;
208
+ function identifierTermsForQuery(query) {
209
+ if (query !== _lastIdTermsQuery) {
210
+ _lastIdTermsQuery = query;
211
+ _lastIdTerms = new Set(splitIdentifierTerms(query));
212
+ }
213
+ return _lastIdTerms;
214
+ }
215
+
204
216
  function splitIdentifierTerms(value) {
205
217
  return String(value || '')
206
218
  .replace(/_[0-9a-f]{8}(?=\.[^.]+$|$)/gi, '')
@@ -233,7 +245,7 @@ export function computeIdentifierAgreementBoost(result, query) {
233
245
  const weight = envFloat('SWEET_SEARCH_IDENTIFIER_AGREEMENT_BOOST', 0.40, 0, 1);
234
246
  if (weight === 0) return 1.0;
235
247
 
236
- const queryTerms = new Set(splitIdentifierTerms(query));
248
+ const queryTerms = identifierTermsForQuery(query);
237
249
  if (queryTerms.size === 0) return 1.0;
238
250
 
239
251
  const fileName = (result.file || result.path || result.metadata?.file || '')
@@ -290,22 +302,22 @@ export function computeDefinitionBoost(result, queryLower, queryTokens) {
290
302
  /**
291
303
  * Compute syntax boost (PHASE_1_FIXES helper)
292
304
  */
305
+ const DEFINITION_SIGNATURE_PATTERNS = [
306
+ /\b(?:public|private|protected)?\s*(?:abstract|final)?\s*class\s+(\w+)/,
307
+ /\b(?:public|private|protected)?\s*interface\s+(\w+)/,
308
+ /\b(?:public|private|protected)?\s*enum\s+(\w+)/,
309
+ /\bclass\s+(\w+)/,
310
+ /\bfunction\s+(\w+)/,
311
+ /\bexport\s+(?:default\s+)?(?:class|function)\s+(\w+)/,
312
+ /\binterface\s+(\w+)/,
313
+ /\btype\s+(\w+)\s*=/,
314
+ ];
315
+
293
316
  export function computeSyntaxBoost(result, queryTokens) {
294
317
  const signature = (result.signature || '').toLowerCase();
295
318
  if (!signature) return 1.0;
296
319
 
297
- const definitionPatterns = [
298
- /\b(?:public|private|protected)?\s*(?:abstract|final)?\s*class\s+(\w+)/,
299
- /\b(?:public|private|protected)?\s*interface\s+(\w+)/,
300
- /\b(?:public|private|protected)?\s*enum\s+(\w+)/,
301
- /\bclass\s+(\w+)/,
302
- /\bfunction\s+(\w+)/,
303
- /\bexport\s+(?:default\s+)?(?:class|function)\s+(\w+)/,
304
- /\binterface\s+(\w+)/,
305
- /\btype\s+(\w+)\s*=/,
306
- ];
307
-
308
- for (const pattern of definitionPatterns) {
320
+ for (const pattern of DEFINITION_SIGNATURE_PATTERNS) {
309
321
  const match = signature.match(pattern);
310
322
  if (match && match[1]) {
311
323
  const definedName = match[1].toLowerCase();
@@ -109,20 +109,24 @@ export function quantileNormalize(scores, lowQuantile = 0.05, highQuantile = 0.9
109
109
  export function convexCombination(lexicalResults, semanticResults, routeType = 'mixed', options = {}) {
110
110
  const alpha = options.alpha ?? ROUTE_ALPHAS[routeType] ?? 0.5;
111
111
 
112
- // Build score maps
112
+ // Build score + result maps in one key derivation per result
113
113
  const lexicalScoreMap = new Map();
114
114
  const semanticScoreMap = new Map();
115
+ const lexResultMap = new Map();
116
+ const semResultMap = new Map();
115
117
  const allKeys = new Set();
116
118
 
117
119
  for (const r of lexicalResults) {
118
120
  const key = this.getResultKey(r);
119
121
  lexicalScoreMap.set(key, r.score);
122
+ lexResultMap.set(key, r);
120
123
  allKeys.add(key);
121
124
  }
122
125
 
123
126
  for (const r of semanticResults) {
124
127
  const key = this.getResultKey(r);
125
128
  semanticScoreMap.set(key, r.score);
129
+ semResultMap.set(key, r);
126
130
  allKeys.add(key);
127
131
  }
128
132
 
@@ -148,8 +152,6 @@ export function convexCombination(lexicalResults, semanticResults, routeType = '
148
152
 
149
153
  // Compute CC scores
150
154
  const ccResults = [];
151
- const lexResultMap = new Map(lexicalResults.map(r => [this.getResultKey(r), r]));
152
- const semResultMap = new Map(semanticResults.map(r => [this.getResultKey(r), r]));
153
155
 
154
156
  for (const key of allKeys) {
155
157
  const lexScore = lexicalScoreMap.get(key);
@@ -253,7 +255,10 @@ export function rrfFusion(lexicalResults, semanticResults, k = 60) {
253
255
  });
254
256
 
255
257
  return [...results.values()]
256
- .map(r => ({ ...r, score: scores.get(this.getResultKey(r)), ccScore: scores.get(this.getResultKey(r)) }))
258
+ .map(r => {
259
+ const score = scores.get(this.getResultKey(r));
260
+ return { ...r, score, ccScore: score };
261
+ })
257
262
  .sort((a, b) => b.score - a.score);
258
263
  }
259
264
 
@@ -310,7 +315,10 @@ export function robustCCFusion(lexicalResults, semanticResults, routeType = 'mix
310
315
 
311
316
  return {
312
317
  results: [...results.values()]
313
- .map(r => ({ ...r, score: combinedScores.get(this.getResultKey(r)), ccScore: combinedScores.get(this.getResultKey(r)), alpha }))
318
+ .map(r => {
319
+ const score = combinedScores.get(this.getResultKey(r));
320
+ return { ...r, score, ccScore: score, alpha };
321
+ })
314
322
  .sort((a, b) => b.score - a.score),
315
323
  method: 'cc_robust',
316
324
  fallbackReason: null,
@@ -155,8 +155,7 @@ function _lateInteractionIndexPath(projectRoot, manifest) {
155
155
  return path.join(_stateDirForProject(root), path.basename(DB_PATHS.lateInteraction));
156
156
  }
157
157
 
158
- function _sourceStaleness(projectRoot, filePathRel) {
159
- const manifest = _readReconcileManifest(projectRoot);
158
+ function _sourceStaleness(projectRoot, filePathRel, manifest = _readReconcileManifest(projectRoot)) {
160
159
  const publishedMs = Date.parse(manifest?.publishedAt || '');
161
160
  if (!Number.isFinite(publishedMs)) return null;
162
161
  try {
@@ -178,9 +177,8 @@ function _sourceStaleness(projectRoot, filePathRel) {
178
177
  }
179
178
 
180
179
  const _repos = new Map();
181
- function _getRepo(projectRoot) {
180
+ function _getRepo(projectRoot, manifest = _readReconcileManifest(projectRoot)) {
182
181
  const key = _projectKey(projectRoot);
183
- const manifest = _readReconcileManifest(projectRoot);
184
182
  const dbPath = _codebasePathForProject(projectRoot, manifest);
185
183
  const baseDbPath = _defaultCodebasePathForProject(projectRoot);
186
184
  let entry = _repos.get(key);
@@ -202,9 +200,8 @@ let _liIndex = null;
202
200
  let _liInitPromise = null;
203
201
  let _liProjectKey = null;
204
202
  let _liManifestEpoch = null;
205
- async function _getLateInteractionIndex(projectRoot) {
203
+ async function _getLateInteractionIndex(projectRoot, manifest = _readReconcileManifest(projectRoot)) {
206
204
  const projectKey = _projectKey(projectRoot);
207
- const manifest = _readReconcileManifest(projectRoot);
208
205
  const manifestEpoch = Number.isInteger(manifest?.epoch) ? manifest.epoch : null;
209
206
  const samePin = _liProjectKey === projectKey && _liManifestEpoch === manifestEpoch;
210
207
  if (_liIndex !== null && samePin) return _liIndex || null;
@@ -331,8 +328,8 @@ function _escapeRegex(s) {
331
328
  // Candidate enumeration — load chunk metadata + per-chunk on-disk text slice
332
329
  // ---------------------------------------------------------------------------
333
330
 
334
- async function _loadFileChunks(filePathRel, projectRoot) {
335
- const repo = _getRepo(projectRoot);
331
+ async function _loadFileChunks(filePathRel, projectRoot, manifest = _readReconcileManifest(projectRoot)) {
332
+ const repo = _getRepo(projectRoot, manifest);
336
333
  if (!repo) return { chunks: [], language: null };
337
334
  const rows = repo.getChunksByFilePath(filePathRel);
338
335
  if (rows.length === 0) return { chunks: [], language: null };
@@ -445,9 +442,9 @@ function _scoreSymbol(chunks, queryTerms, queryRaw) {
445
442
  return scores;
446
443
  }
447
444
 
448
- async function _scoreLateInteraction(chunks, query, projectRoot, lateInteractionIndexOverride = null) {
445
+ async function _scoreLateInteraction(chunks, query, projectRoot, lateInteractionIndexOverride = null, manifest = undefined) {
449
446
  if (chunks.length === 0) return { scores: new Map(), ran: false };
450
- const liIndex = lateInteractionIndexOverride || await _getLateInteractionIndex(projectRoot);
447
+ const liIndex = lateInteractionIndexOverride || await _getLateInteractionIndex(projectRoot, manifest);
451
448
  if (!liIndex) return { scores: new Map(), ran: false };
452
449
 
453
450
  // Only score chunks whose IDs actually appear in the LI index. Use the
@@ -636,7 +633,10 @@ async function _readSemanticUnpinned(req) {
636
633
  const projectRoot = req.projectRoot || process.cwd();
637
634
  _ensurePersistedLiModelApplied(projectRoot);
638
635
  const filePathRel = _projectRelative(req.path, projectRoot);
639
- const staleness = _sourceStaleness(projectRoot, filePathRel);
636
+ // One manifest read per request — _sourceStaleness/_getRepo/_getLateInteractionIndex
637
+ // each re-read the same file otherwise (it cannot change mid-request usefully).
638
+ const reconcileManifest = _readReconcileManifest(projectRoot);
639
+ const staleness = _sourceStaleness(projectRoot, filePathRel, reconcileManifest);
640
640
 
641
641
  const topK = req.topK ?? DEFAULTS.topK;
642
642
  const threshold = req.threshold ?? DEFAULTS.threshold;
@@ -646,7 +646,7 @@ async function _readSemanticUnpinned(req) {
646
646
  const verbose = !!req.verbose;
647
647
 
648
648
  const tLoad0 = performance.now();
649
- const { chunks, language, totalLines, fileText } = await _loadFileChunks(filePathRel, projectRoot);
649
+ const { chunks, language, totalLines, fileText } = await _loadFileChunks(filePathRel, projectRoot, reconcileManifest);
650
650
  const tLoad1 = performance.now();
651
651
 
652
652
  // No chunks at all → fall back to plain read so the caller still gets
@@ -692,6 +692,7 @@ async function _readSemanticUnpinned(req) {
692
692
  req.query,
693
693
  projectRoot,
694
694
  req._lateInteractionIndex || null,
695
+ reconcileManifest,
695
696
  );
696
697
  const tLi1 = performance.now();
697
698
 
@@ -37,6 +37,8 @@ export const SEARCH_SERVER_MAX_URL_LENGTH = 16_384;
37
37
  export const SEARCH_SERVER_MAX_QUERY_LENGTH = 2_000;
38
38
  export const SEARCH_SERVER_MAX_READ_PATH_LENGTH = 8_192;
39
39
 
40
+ const AGENT_FORMATS = new Set(['agent', 'agent_preview', 'agent_full', 'agent_full_xl']);
41
+
40
42
  function canonicalProjectRoot(root) {
41
43
  const resolved = path.resolve(root || process.cwd());
42
44
  try {
@@ -686,7 +688,6 @@ export async function startServer() {
686
688
 
687
689
  // Agent mode: context packaging (ColGrep agent format)
688
690
  const rawFormat = url.searchParams.get('format');
689
- const AGENT_FORMATS = new Set(['agent', 'agent_preview', 'agent_full', 'agent_full_xl']);
690
691
  const agentFormat = AGENT_FORMATS.has(rawFormat) ? rawFormat : undefined;
691
692
  const tokenBudget = url.searchParams.has('budget')
692
693
  ? parseInt(url.searchParams.get('budget'), 10)
@@ -236,6 +236,11 @@ export class BinaryHNSWIndex {
236
236
 
237
237
  // Pre-allocated visited list (generation-stamped, reused across searches)
238
238
  this._visitedList = new VisitedList(this.maxElements);
239
+ // Pooled search heaps — searchLayer/searchLayerQuery are synchronous and
240
+ // never re-entered, so one pair (auto-growing) serves all calls. Same
241
+ // single-flight contract as _visitedList.
242
+ this._candHeap = new TypedMinHeap(64);
243
+ this._resHeap = new TypedMaxHeap(64);
239
244
 
240
245
  // Asymmetric binary quantization calibration data
241
246
  this.centroid = null; // Float32Array — dataset centroid
@@ -652,11 +657,13 @@ export class BinaryHNSWIndex {
652
657
  const startDist = hammingDistance(this.vectors[startNode].binary, targetBinary);
653
658
 
654
659
  // candidates = min-heap (explore closest first)
655
- const candidates = new TypedMinHeap(ef * 4);
660
+ const candidates = this._candHeap;
661
+ candidates.clear();
656
662
  candidates.insert(startNode, startDist);
657
663
 
658
664
  // results = max-heap of size ef (peek furthest, replaceMax when closer found)
659
- const results = new TypedMaxHeap(ef + 1);
665
+ const results = this._resHeap;
666
+ results.clear();
660
667
  results.insert(startNode, startDist);
661
668
 
662
669
  while (candidates.size > 0) {
@@ -894,8 +901,10 @@ export class BinaryHNSWIndex {
894
901
  visited.mark(startNode);
895
902
 
896
903
  const startDist = hammingDistance(this.vectors[startNode].binary, queryBinary);
897
- const candidates = new TypedMinHeap(ef * 4);
898
- const results = new TypedMaxHeap(ef + 1);
904
+ const candidates = this._candHeap;
905
+ candidates.clear();
906
+ const results = this._resHeap;
907
+ results.clear();
899
908
  candidates.insert(startNode, startDist);
900
909
  results.insert(startNode, startDist);
901
910
 
@@ -117,12 +117,16 @@ export class FloatVectorStore {
117
117
  const idsPath = binPath.replace(/\.bin$/, '.ids.json');
118
118
  if (!existsSync(idsPath)) return false;
119
119
 
120
- // Read binary
120
+ // Read binary. Large reads return an unpooled Buffer that owns its whole
121
+ // ArrayBuffer — adopt it directly; only pooled (offset/short) buffers need
122
+ // the defensive copy.
121
123
  const fileBuffer = await readFile(binPath);
122
- this.buffer = fileBuffer.buffer.slice(
123
- fileBuffer.byteOffset,
124
- fileBuffer.byteOffset + fileBuffer.byteLength
125
- );
124
+ this.buffer = (fileBuffer.byteOffset === 0 && fileBuffer.byteLength === fileBuffer.buffer.byteLength)
125
+ ? fileBuffer.buffer
126
+ : fileBuffer.buffer.slice(
127
+ fileBuffer.byteOffset,
128
+ fileBuffer.byteOffset + fileBuffer.byteLength
129
+ );
126
130
 
127
131
  // Parse header
128
132
  const header = new DataView(this.buffer, 0, HEADER_SIZE);
@@ -19,6 +19,17 @@ import {
19
19
  parseLineRange, looksLikeOption,
20
20
  } from './_ss-argparse.mjs';
21
21
 
22
+ // Diagnostic-log isolation (agent-facing tools). The Sweet Search engine emits
23
+ // model/index load banners via console.log → stdout ("LateInteraction: Loaded…",
24
+ // "BinaryHNSW: Loaded…", "Warming up embedding…", "✓ Vocabulary loaded…"). When the
25
+ // engine runs IN-PROCESS in this wrapper, that stdout IS the agent's tool result, so a
26
+ // cold start crowds out the actual hits and the agent falls back to native tools
27
+ // (diagnosed root cause of the sweet≈native tie). The wrappers emit REAL results only
28
+ // via process.stdout.write, so redirect every console.log to stderr; the ss-* shell
29
+ // wrapper's `2>` suppresses it on success → the agent sees clean results, warm or cold.
30
+ // (Production search/grep/find already avoid this: the daemon is spawned stdio:'ignore'.)
31
+ console.log = (...args) => process.stderr.write(args.map(a => (typeof a === 'string' ? a : String(a))).join(' ') + '\n');
32
+
22
33
  // 8-char SHA1 prefix is enough for grouping identical queries across
23
34
  // benchmark runs without bloating artifacts.
24
35
  function shortQueryHash(q) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sweet-search",
3
- "version": "2.6.8",
3
+ "version": "2.6.9",
4
4
  "description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
5
5
  "type": "module",
6
6
  "main": "core/search/sweet-search.js",
@@ -167,13 +167,13 @@
167
167
  "vitest": "^4.0.16"
168
168
  },
169
169
  "optionalDependencies": {
170
- "@sweet-search/native-darwin-arm64": "2.6.8",
171
- "@sweet-search/native-darwin-x64": "2.6.8",
172
- "@sweet-search/native-linux-arm64-gnu": "2.6.8",
173
- "@sweet-search/native-linux-arm64-gnu-cuda": "2.6.8",
174
- "@sweet-search/native-linux-x64-gnu": "2.6.8",
175
- "@sweet-search/native-linux-x64-gnu-cuda": "2.6.8",
176
- "@sweet-search/bg-priority": "2.6.8"
170
+ "@sweet-search/native-darwin-arm64": "2.6.9",
171
+ "@sweet-search/native-darwin-x64": "2.6.9",
172
+ "@sweet-search/native-linux-arm64-gnu": "2.6.9",
173
+ "@sweet-search/native-linux-arm64-gnu-cuda": "2.6.9",
174
+ "@sweet-search/native-linux-x64-gnu": "2.6.9",
175
+ "@sweet-search/native-linux-x64-gnu-cuda": "2.6.9",
176
+ "@sweet-search/bg-priority": "2.6.9"
177
177
  },
178
178
  "engines": {
179
179
  "node": ">=18.0.0"