sweet-search 2.6.7 → 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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { existsSync, readFileSync } from 'fs';
11
- import { minimatch } from 'minimatch';
11
+ import { Minimatch } from 'minimatch';
12
12
  import { loadProjectConfig } from '../infrastructure/config/index.js';
13
13
 
14
14
  const MM_OPTS = { dot: true, nocase: false };
@@ -54,6 +54,31 @@ function getExcludes(projectRoot) {
54
54
  function resetCache() {
55
55
  _excludesByRoot.clear();
56
56
  _cachedExtraPatterns = null;
57
+ _excludeMatchersByRoot.clear();
58
+ _cachedExtraMatchers = null;
59
+ }
60
+
61
+ // Precompiled Minimatch instances — `minimatch(p, g, opts)` recompiles the
62
+ // glob to a regex on every call; `new Minimatch(g, opts).match(p)` is the
63
+ // documented equivalent with the compile amortized.
64
+ const _excludeMatchersByRoot = new Map();
65
+
66
+ function getExcludeMatchers(projectRoot) {
67
+ const key = projectRoot || '__cwd__';
68
+ let cached = _excludeMatchersByRoot.get(key);
69
+ if (cached) return cached;
70
+ cached = getExcludes(projectRoot)
71
+ .filter((g) => typeof g === 'string')
72
+ .map((g) => new Minimatch(g, MM_OPTS));
73
+ _excludeMatchersByRoot.set(key, cached);
74
+ return cached;
75
+ }
76
+
77
+ let _cachedExtraMatchers = null;
78
+ function getExtraMatchers() {
79
+ if (_cachedExtraMatchers !== null) return _cachedExtraMatchers;
80
+ _cachedExtraMatchers = loadExtraPatternsFromFile().map((g) => new Minimatch(g, MM_OPTS));
81
+ return _cachedExtraMatchers;
57
82
  }
58
83
 
59
84
  const GENERATED_MARKERS = [
@@ -87,13 +112,11 @@ function loadExtraPatternsFromFile() {
87
112
  export function isExcludedByConfig(filePath, projectRoot) {
88
113
  if (!filePath) return false;
89
114
  const p = normalizePath(filePath);
90
- const excludes = getExcludes(projectRoot);
91
- for (const g of excludes) {
92
- if (typeof g === 'string' && minimatch(p, g, MM_OPTS)) return true;
115
+ for (const m of getExcludeMatchers(projectRoot)) {
116
+ if (m.match(p)) return true;
93
117
  }
94
- const extras = loadExtraPatternsFromFile();
95
- for (const g of extras) {
96
- if (minimatch(p, g, MM_OPTS)) return true;
118
+ for (const m of getExtraMatchers()) {
119
+ if (m.match(p)) return true;
97
120
  }
98
121
  return false;
99
122
  }
@@ -10,7 +10,7 @@
10
10
 
11
11
  import Database from 'better-sqlite3';
12
12
  import { existsSync, statSync } from 'fs';
13
- import { applyReadPragmas } from './db-utils.js';
13
+ import { applyReadPragmas, prepareCached } from './db-utils.js';
14
14
  import { readAdjacentManifest, resolveManifestCodeGraphPath, sqlAliasPrefix } from './code-graph-visibility.js';
15
15
 
16
16
  export class CodeGraphRepository {
@@ -72,7 +72,7 @@ export class CodeGraphRepository {
72
72
  }
73
73
 
74
74
  _hasColumns(db, table, columns) {
75
- const rows = db.prepare(`PRAGMA table_info(${table})`).all();
75
+ const rows = prepareCached(db, `PRAGMA table_info(${table})`).all();
76
76
  const names = new Set(rows.map((row) => row.name));
77
77
  return columns.every((column) => names.has(column));
78
78
  }
@@ -138,7 +138,7 @@ export class CodeGraphRepository {
138
138
  const db = this._open();
139
139
  if (!db) return null;
140
140
  try {
141
- const row = db.prepare(`
141
+ const row = prepareCached(db, `
142
142
  SELECT id, name, type, start_line, end_line, parent_class
143
143
  FROM entities
144
144
  WHERE file_path = ?
@@ -207,7 +207,7 @@ export class CodeGraphRepository {
207
207
  // in the chunk range (catches entities like a multi-chunk struct whose
208
208
  // declaration starts in this chunk but body extends beyond — e.g. gin
209
209
  // Engine struct at gin.go:92-189 split across chunks 92-133 and 134-189).
210
- const containedRows = db.prepare(`
210
+ const containedRows = prepareCached(db, `
211
211
  SELECT name, type, start_line, end_line FROM entities
212
212
  WHERE file_path = ? AND start_line >= ? AND end_line <= ? AND ${this._entityVisibilitySql(db)}
213
213
  ORDER BY (end_line - start_line) DESC
@@ -219,7 +219,7 @@ export class CodeGraphRepository {
219
219
  return { name: row.name, type: row.type, startLine: row.start_line, endLine: row.end_line };
220
220
  }
221
221
  }
222
- const startsInRows = db.prepare(`
222
+ const startsInRows = prepareCached(db, `
223
223
  SELECT name, type, start_line, end_line FROM entities
224
224
  WHERE file_path = ? AND start_line >= ? AND start_line <= ? AND ${this._entityVisibilitySql(db)}
225
225
  ORDER BY (end_line - start_line) DESC
@@ -245,7 +245,7 @@ export class CodeGraphRepository {
245
245
  const db = this._open();
246
246
  if (!db) return null;
247
247
  try {
248
- const row = db.prepare(`
248
+ const row = prepareCached(db, `
249
249
  SELECT id, name, type, start_line, end_line, parent_class
250
250
  FROM entities
251
251
  WHERE file_path = ?
@@ -289,7 +289,7 @@ export class CodeGraphRepository {
289
289
  const db = this._open();
290
290
  if (!db) return [];
291
291
  try {
292
- const rows = db.prepare(`
292
+ const rows = prepareCached(db, `
293
293
  SELECT id, name, type, start_line, end_line, parent_class
294
294
  FROM entities
295
295
  WHERE file_path = ?
@@ -322,7 +322,7 @@ export class CodeGraphRepository {
322
322
  const db = this._open();
323
323
  if (!db) return null;
324
324
  try {
325
- const row = db.prepare(`
325
+ const row = prepareCached(db, `
326
326
  SELECT id, name, type, file_path, start_line, end_line, parent_class
327
327
  FROM entities
328
328
  WHERE id = ? AND ${this._entityVisibilitySql(db)}
@@ -379,7 +379,7 @@ export class CodeGraphRepository {
379
379
  const args = types
380
380
  ? [...this._entityVisibilityParams(db), sourceId, ...this._relationshipVisibilityParams(db), ...types, limit]
381
381
  : [...this._entityVisibilityParams(db), sourceId, ...this._relationshipVisibilityParams(db), limit];
382
- const rows = db.prepare(baseSql).all(...args);
382
+ const rows = prepareCached(db, baseSql).all(...args);
383
383
  return rows.map(r => ({
384
384
  type: r.rel_type,
385
385
  targetName: r.target_name,
@@ -445,7 +445,7 @@ export class CodeGraphRepository {
445
445
  const args = excludeFile
446
446
  ? [...uniq, ...types, ...this._entityVisibilityParams(db), excludeFile, limit]
447
447
  : [...uniq, ...types, ...this._entityVisibilityParams(db), limit];
448
- const rows = db.prepare(sql).all(...args);
448
+ const rows = prepareCached(db, sql).all(...args);
449
449
  // De-dup by name+type, keeping the first (smallest body).
450
450
  const seen = new Set();
451
451
  const out = [];
@@ -493,7 +493,7 @@ export class CodeGraphRepository {
493
493
  ORDER BY (end_line - start_line) ASC
494
494
  LIMIT ?
495
495
  `;
496
- const rows = db.prepare(sql).all(...uniq, ...types, ...this._entityVisibilityParams(db), limit);
496
+ const rows = prepareCached(db, sql).all(...uniq, ...types, ...this._entityVisibilityParams(db), limit);
497
497
  return rows.map(row => ({
498
498
  id: row.id,
499
499
  name: row.name,
@@ -549,7 +549,7 @@ export class CodeGraphRepository {
549
549
  ORDER BY (end_line - start_line) ASC
550
550
  LIMIT ?
551
551
  `;
552
- const rows = db.prepare(sql).all(...uniq, ...exclude, ...this._entityVisibilityParams(db), limit);
552
+ const rows = prepareCached(db, sql).all(...uniq, ...exclude, ...this._entityVisibilityParams(db), limit);
553
553
  return rows.map(row => ({
554
554
  id: row.id,
555
555
  name: row.name,
@@ -598,7 +598,7 @@ export class CodeGraphRepository {
598
598
  AND ${this._entityVisibilitySql(db)}
599
599
  GROUP BY lname
600
600
  `;
601
- const rows = db.prepare(sql).all(...uniq, ...exclude, ...this._entityVisibilityParams(db));
601
+ const rows = prepareCached(db, sql).all(...uniq, ...exclude, ...this._entityVisibilityParams(db));
602
602
  const map = new Map();
603
603
  for (const r of rows) map.set(r.lname, r.count);
604
604
  // Names with no row are absent — caller treats absent as 0.
@@ -640,7 +640,7 @@ export class CodeGraphRepository {
640
640
  const args = types
641
641
  ? [...this._entityVisibilityParams(db), targetId, ...this._relationshipVisibilityParams(db), ...types, limit]
642
642
  : [...this._entityVisibilityParams(db), targetId, ...this._relationshipVisibilityParams(db), limit];
643
- const rows = db.prepare(baseSql).all(...args);
643
+ const rows = prepareCached(db, baseSql).all(...args);
644
644
  return rows.map(r => ({
645
645
  type: r.rel_type,
646
646
  contextLine: r.context_line || null,
@@ -689,7 +689,7 @@ export class CodeGraphRepository {
689
689
  return Number.isFinite(n) && n > 0 ? n : 12;
690
690
  })();
691
691
  try {
692
- const rows = db.prepare(`
692
+ const rows = prepareCached(db, `
693
693
  SELECT r.target_name, COUNT(*) as cnt
694
694
  FROM relationships r
695
695
  LEFT JOIN entities e ON e.id = r.target_id AND ${this._entityVisibilitySql(db, 'e')}
@@ -798,7 +798,7 @@ export class CodeGraphRepository {
798
798
  if (!db) return null;
799
799
  try {
800
800
  if (this._hasColumns(db, 'entities', ['epoch_written', 'epoch_retired'])) {
801
- const visible = db.prepare(`
801
+ const visible = prepareCached(db, `
802
802
  SELECT 1 FROM entities
803
803
  WHERE file_path = ? AND ${this._entityVisibilitySql(db)}
804
804
  LIMIT 1
@@ -827,10 +827,10 @@ export class CodeGraphRepository {
827
827
  const staleArgs = this._manifestEpoch !== null
828
828
  ? [filePath, this._manifestEpoch, this._manifestEpoch]
829
829
  : [filePath];
830
- const staleRow = db.prepare(staleSql).get(...staleArgs);
830
+ const staleRow = prepareCached(db, staleSql).get(...staleArgs);
831
831
  return staleRow ? { staleSince: staleRow.stale_since || null } : null;
832
832
  }
833
- const row = db.prepare(`
833
+ const row = prepareCached(db, `
834
834
  SELECT stale_since, MIN(ROWID) as first_row
835
835
  FROM entities
836
836
  WHERE file_path = ?
@@ -67,7 +67,7 @@ export const FILE_PATTERNS = {
67
67
  '**/*.{cr,vala,hx,pas,nix,vim}', // Crystal / Vala / Haxe / Pascal / Nix / Vim
68
68
  '**/*.{elm,sol,tla,rdl,el,ejs}', // Elm / Solidity / TLA+ / SystemRDL / Emacs Lisp / EJS
69
69
  '**/*.{ql,qll}', // CodeQL
70
- '**/*.{zeek,bro}', // Zeek
70
+ '**/*.{zeek,bro,bif,pac,pac2,spicy,evt,hlt}', // Zeek + BiF + BinPAC + Spicy DSLs
71
71
  '**/*.{tcl,tk}', // Tcl
72
72
  '**/*.astro', // Astro (SFC)
73
73
  // GPU shaders
@@ -83,6 +83,23 @@ export const FILE_PATTERNS = {
83
83
  '**/*.php', // PHP
84
84
  '**/*.{swift,m,mm}', // Apple
85
85
  '**/*.{lua,zig,nim,ex,exs,dart}', // Other
86
+ // ── Task-bench coverage audit (2026-07): additional source / DSL / template / build ──
87
+ '**/*.{pyx,pxd,pxi}', // Cython
88
+ '**/*.{sbt,sc}', // Scala build / worksheets
89
+ '**/*.{rake,gemspec,podspec,ru}', // Ruby build/manifest DSLs
90
+ '**/*.{coffee,litcoffee}', // CoffeeScript
91
+ '**/*.{pcss,styl}', // PostCSS / Stylus
92
+ '**/*.{twig,liquid,njk,hbs,handlebars,mustache,gohtml,eex,heex,leex}', // HTML templating
93
+ '**/*.{pug,jade,haml,slim}', // Indentation templating
94
+ '**/*.{jinja,jinja2,j2,tera,tmpl,tpl,gotmpl}', // Text templating
95
+ '**/*.{props,targets,proj,vbproj,fsproj,resx,nuspec}', // .NET / MSBuild
96
+ '**/*.{m4,ac,am}', // Autotools
97
+ '**/*.{awk,sed}', // Text-processing scripts
98
+ '**/*.ipynb', // Jupyter notebooks (generic fixed-window chunking)
99
+ '**/*.in', // Autoconf/config templates (Makefile.in, *.py.in)
100
+ '**/*.sln', // Visual Studio solution
101
+ '**/go.mod', '**/go.work', // Go module / workspace manifests
102
+ '**/Rakefile', '**/Gemfile', // Ruby (extensionless)
86
103
  '**/*.{sh,bash,zsh,fish,ps1}', // Shell
87
104
  '**/*.sql', // SQL
88
105
  '**/*.proto', // Protobuf
@@ -17,6 +17,35 @@ import Database from 'better-sqlite3';
17
17
  */
18
18
  export const SAFE_IN_CLAUSE_BATCH = 999;
19
19
 
20
+ // Per-connection prepared-statement cache. Statements belong to their
21
+ // Database object, so the WeakMap self-invalidates when a connection object
22
+ // is dropped (closed connections are never passed back in by callers).
23
+ // Only use for .get/.all/.run statements — never .iterate (a cached statement
24
+ // mid-iteration cannot be re-entered).
25
+ const _stmtCaches = new WeakMap();
26
+
27
+ /**
28
+ * db.prepare with a per-connection cache — compiles each distinct SQL string
29
+ * once per Database object instead of once per call.
30
+ *
31
+ * @param {import('better-sqlite3').Database} db
32
+ * @param {string} sql
33
+ * @returns {import('better-sqlite3').Statement}
34
+ */
35
+ export function prepareCached(db, sql) {
36
+ let cache = _stmtCaches.get(db);
37
+ if (!cache) {
38
+ cache = new Map();
39
+ _stmtCaches.set(db, cache);
40
+ }
41
+ let stmt = cache.get(sql);
42
+ if (!stmt) {
43
+ stmt = db.prepare(sql);
44
+ cache.set(sql, stmt);
45
+ }
46
+ return stmt;
47
+ }
48
+
20
49
  /**
21
50
  * Apply read-path PRAGMA optimizations to a read-only database connection.
22
51
  *
@@ -155,6 +155,9 @@ export const EXTENSION_MAP = {
155
155
  '.vim': 'vim',
156
156
  '.tcl': 'tcl', '.tk': 'tcl',
157
157
  '.zeek': 'zeek', '.bro': 'zeek',
158
+ // Zeek build-time DSLs: BiF declarations, BinPAC + Spicy parser sources (generic chunking)
159
+ '.bif': 'zeek', '.pac': 'zeek', '.pac2': 'zeek',
160
+ '.spicy': 'spicy', '.evt': 'spicy', '.hlt': 'spicy',
158
161
 
159
162
  // Scientific / legacy (lowercase keys cover the uppercase .F/.F90/.S forms)
160
163
  '.f': 'fortran', '.for': 'fortran', '.f90': 'fortran', '.f95': 'fortran',
@@ -183,6 +186,37 @@ export const EXTENSION_MAP = {
183
186
  '.ninja': 'ninja',
184
187
  '.bzl': 'starlark', '.star': 'starlark',
185
188
 
189
+ // ── Task-bench coverage audit (2026-07): additional source / DSL / template / build ──
190
+ // Cython (Python-like syntax → reuse the wired python chunker)
191
+ '.pyx': 'python', '.pxd': 'python', '.pxi': 'python',
192
+ // Scala build definitions / worksheets (→ scala grammar)
193
+ '.sbt': 'scala', '.sc': 'scala',
194
+ // Ruby build & manifest DSLs (→ ruby grammar)
195
+ '.rake': 'ruby', '.gemspec': 'ruby', '.podspec': 'ruby', '.ru': 'ruby',
196
+ // CoffeeScript (no bundled grammar → generic chunking)
197
+ '.coffee': 'coffeescript', '.litcoffee': 'coffeescript',
198
+ // Style preprocessors
199
+ '.pcss': 'css', '.styl': 'stylus',
200
+ // HTML-based templating (reuse the html SFC path, like .vue/.svelte)
201
+ '.twig': 'html', '.liquid': 'html', '.njk': 'html', '.hbs': 'html',
202
+ '.handlebars': 'html', '.mustache': 'html', '.gohtml': 'html',
203
+ '.eex': 'html', '.heex': 'html', '.leex': 'html',
204
+ // Indentation-based templating (generic chunking; not HTML syntax)
205
+ '.pug': 'pug', '.jade': 'pug', '.haml': 'haml', '.slim': 'slim',
206
+ // Polymorphic text templating (any target text → generic chunking)
207
+ '.jinja': 'jinja', '.jinja2': 'jinja', '.j2': 'jinja',
208
+ '.tera': 'jinja', '.tmpl': 'jinja', '.tpl': 'jinja', '.gotmpl': 'jinja',
209
+ // .NET / MSBuild project & resource files (XML dialects → xml grammar)
210
+ '.props': 'xml', '.targets': 'xml', '.proj': 'xml',
211
+ '.vbproj': 'xml', '.fsproj': 'xml', '.resx': 'xml', '.nuspec': 'xml',
212
+ // Autotools / text-processing script sources (generic chunking)
213
+ '.m4': 'm4', '.ac': 'm4', '.am': 'makefile',
214
+ '.awk': 'awk', '.sed': 'sed',
215
+ // Jupyter notebooks — chunker-less id → parseGenericFile (fixed-window, lossless)
216
+ // over the raw JSON. Not JSON-AST-chunked (that would emit one giant cells chunk).
217
+ // Image-heavy notebooks self-filter via the admission maxFileSize (1MB) gate.
218
+ '.ipynb': 'jupyter',
219
+
186
220
  // Document formats (dispatched to DocumentChunker in ast-chunker.js)
187
221
  '.md': 'markdown', '.mdx': 'markdown', '.markdown': 'markdown',
188
222
  '.rst': 'rst',
@@ -201,4 +235,7 @@ export const FILENAME_MAP = {
201
235
  Earthfile: 'earthfile',
202
236
  justfile: 'just',
203
237
  Justfile: 'just',
238
+ // Ruby extensionless build files (→ ruby grammar)
239
+ Rakefile: 'ruby',
240
+ Gemfile: 'ruby',
204
241
  };
@@ -98,6 +98,23 @@ async function initWasm() {
98
98
  // Eager non-blocking init
99
99
  initWasm().catch(() => {});
100
100
 
101
+ // WASM memory views are cached; both modules declare fixed-size memory (no
102
+ // memory.grow anywhere), so the buffer identity check only fires if that
103
+ // ever changes. Same pattern as _wasmPerTokenLayout.
104
+ function wasmMemView() {
105
+ if (wasmMem === null || wasmMem.buffer !== wasmExports.memory.buffer) {
106
+ wasmMem = new Uint8Array(wasmExports.memory.buffer);
107
+ }
108
+ return wasmMem;
109
+ }
110
+
111
+ function maxsimMemView() {
112
+ if (maxsimMem === null || maxsimMem.buffer !== maxsimExports.memory.buffer) {
113
+ maxsimMem = new Uint8Array(maxsimExports.memory.buffer);
114
+ }
115
+ return maxsimMem;
116
+ }
117
+
101
118
  // =============================================================================
102
119
  // POPCOUNT LUT (JS fallback)
103
120
  // =============================================================================
@@ -115,10 +132,9 @@ export function wasmHammingDistance(a, b) {
115
132
  if (wasmExports) {
116
133
  const aPtr = DATA_OFFSET;
117
134
  const bPtr = DATA_OFFSET + a.length;
118
- // Re-acquire view in case memory grew
119
- wasmMem = new Uint8Array(wasmExports.memory.buffer);
120
- wasmMem.set(a, aPtr);
121
- wasmMem.set(b, bPtr);
135
+ const mem = wasmMemView();
136
+ mem.set(a, aPtr);
137
+ mem.set(b, bPtr);
122
138
  return wasmExports.hamming_distance(aPtr, bPtr, a.length);
123
139
  }
124
140
  // JS fallback
@@ -137,9 +153,9 @@ export function wasmInt8Cosine(a, b) {
137
153
  if (wasmExports) {
138
154
  const aPtr = DATA_OFFSET;
139
155
  const bPtr = DATA_OFFSET + a.length;
140
- wasmMem = new Uint8Array(wasmExports.memory.buffer);
141
- wasmMem.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), aPtr);
142
- wasmMem.set(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), bPtr);
156
+ const mem = wasmMemView();
157
+ mem.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), aPtr);
158
+ mem.set(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), bPtr);
143
159
 
144
160
  const dot = wasmExports.int8_dot(aPtr, bPtr, a.length);
145
161
  const normA = wasmExports.int8_norm_sq(aPtr, a.length);
@@ -193,18 +209,18 @@ export function wasmInt8BatchDot(query, candidates) {
193
209
  const alignedScoresPtr = (scoresPtr + 3) & ~3;
194
210
  const needed = alignedScoresPtr + count * 4;
195
211
 
196
- wasmMem = new Uint8Array(wasmExports.memory.buffer);
197
- if (needed > wasmMem.length) {
212
+ const mem = wasmMemView();
213
+ if (needed > mem.length) {
198
214
  return _jsInt8BatchDot(query, candidates);
199
215
  }
200
216
 
201
217
  // Copy query once
202
- wasmMem.set(new Uint8Array(query.buffer, query.byteOffset, query.byteLength), queryPtr);
218
+ mem.set(new Uint8Array(query.buffer, query.byteOffset, query.byteLength), queryPtr);
203
219
 
204
220
  // Copy all candidates as contiguous slab
205
221
  for (let i = 0; i < count; i++) {
206
222
  const v = candidates[i];
207
- wasmMem.set(new Uint8Array(v.buffer, v.byteOffset, v.byteLength), slabPtr + i * dim);
223
+ mem.set(new Uint8Array(v.buffer, v.byteOffset, v.byteLength), slabPtr + i * dim);
208
224
  }
209
225
 
210
226
  // Single WASM call: scores all candidates internally, writes to output buffer
@@ -267,9 +283,9 @@ export function wasmInt8Dot(a, b) {
267
283
  if (wasmExports) {
268
284
  const aPtr = DATA_OFFSET;
269
285
  const bPtr = DATA_OFFSET + a.length;
270
- wasmMem = new Uint8Array(wasmExports.memory.buffer);
271
- wasmMem.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), aPtr);
272
- wasmMem.set(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), bPtr);
286
+ const mem = wasmMemView();
287
+ mem.set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), aPtr);
288
+ mem.set(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), bPtr);
273
289
  return wasmExports.int8_dot(aPtr, bPtr, a.length);
274
290
  }
275
291
  // JS fallback
@@ -290,9 +306,9 @@ export function wasmAsymmetricDistance(docBinary, queryInt4, queryNormScaled) {
290
306
  if (wasmExports) {
291
307
  const docPtr = DATA_OFFSET;
292
308
  const queryPtr = DATA_OFFSET + docBinary.length;
293
- wasmMem = new Uint8Array(wasmExports.memory.buffer);
294
- wasmMem.set(docBinary, docPtr);
295
- wasmMem.set(new Uint8Array(queryInt4.buffer, queryInt4.byteOffset, queryInt4.byteLength), queryPtr);
309
+ const mem = wasmMemView();
310
+ mem.set(docBinary, docPtr);
311
+ mem.set(new Uint8Array(queryInt4.buffer, queryInt4.byteOffset, queryInt4.byteLength), queryPtr);
296
312
  return wasmExports.asymmetric_distance(docPtr, queryPtr, queryInt4.length, queryNormScaled);
297
313
  }
298
314
  // JS fallback
@@ -336,9 +352,7 @@ export function wasmMaxSimF32(queryFlat, docFlat, numQ, numD, dim) {
336
352
  if (!maxsimExports?.maxsim_f32) return null;
337
353
  const exports = maxsimExports;
338
354
 
339
- const mem = maxsimExports
340
- ? new Uint8Array(maxsimExports.memory.buffer)
341
- : (wasmMem = new Uint8Array(wasmExports.memory.buffer));
355
+ const mem = maxsimMemView();
342
356
 
343
357
  const qBytes = numQ * dim * 4;
344
358
  const dBytes = numD * dim * 4;
@@ -375,7 +389,7 @@ export function wasmMaxSimDequant(queryFlat, docInt8, numQ, numD, dim, min, scal
375
389
  const qBytes = numQ * dim * 4; // float32
376
390
  const dBytes = numD * dim; // int8 (4x smaller!)
377
391
 
378
- const mem = new Uint8Array(maxsimExports.memory.buffer);
392
+ const mem = maxsimMemView();
379
393
  if (qBytes + dBytes + 1024 > mem.length) return null;
380
394
 
381
395
  const qPtr = DATA_OFFSET;
@@ -49,8 +49,10 @@ export function loadBitmap(filePath) {
49
49
  if (raw.length < expectedLength) {
50
50
  throw new Error(`loadBitmap: ${filePath} truncated payload (${raw.length} bytes, expected ${expectedLength})`);
51
51
  }
52
+ // View, not copy: `raw` is otherwise unreferenced, so this only keeps the
53
+ // 64-byte header alive alongside the payload bytes.
52
54
  const payload = raw.subarray(HEADER_RESERVED, HEADER_RESERVED + payloadByteLength(capacity));
53
- return { capacity, payload: Buffer.from(payload) };
55
+ return { capacity, payload };
54
56
  }
55
57
 
56
58
  export function saveBitmap(filePath, bitmap) {
@@ -533,11 +533,23 @@ function envFloat(name, dflt) {
533
533
  return Number.isFinite(n) && n > 0 ? n : dflt;
534
534
  }
535
535
 
536
+ // Pure function of `preferred` (static ENTITY_KIND_KEYWORDS table) — memoize
537
+ // the last kind so the per-result demotion loop reuses one Set.
538
+ let _kindKeywordSetKey;
539
+ let _kindKeywordSet;
540
+ function kindKeywordSet(preferred) {
541
+ if (preferred !== _kindKeywordSetKey) {
542
+ _kindKeywordSetKey = preferred;
543
+ _kindKeywordSet = new Set((ENTITY_KIND_KEYWORDS[preferred] || []).map(normalizeType));
544
+ }
545
+ return _kindKeywordSet;
546
+ }
547
+
536
548
  function entityKindMultiplier(r, preferred, opts = {}) {
537
549
  if (!preferred) return 1;
538
550
  const kindBoost = envFloat('SWEET_SEARCH_KIND_BOOST', 1.10);
539
551
  const kindDemote = envFloat('SWEET_SEARCH_KIND_DEMOTE', 0.90);
540
- const wantSet = new Set((ENTITY_KIND_KEYWORDS[preferred] || []).map(normalizeType));
552
+ const wantSet = kindKeywordSet(preferred);
541
553
  const inferred = resolveEntityKindInfo(r, opts)?.type || '';
542
554
  const recorded = normalizeType(resolveResultType(r));
543
555
  const type = recorded && recorded !== 'code' && recorded !== 'chunk' ? recorded : normalizeType(inferred);
@@ -550,7 +562,7 @@ function namePrecisionMultiplier(r, preferred, nameHintsLower, opts = {}) {
550
562
  if (!preferred || nameHintsLower.size === 0) return 1;
551
563
  const exactBoost = envFloat('SWEET_SEARCH_NAME_EXACT_BOOST', 1.10);
552
564
  const substrBoost = envFloat('SWEET_SEARCH_NAME_SUBSTR_BOOST', 1.03);
553
- const wantSet = new Set((ENTITY_KIND_KEYWORDS[preferred] || []).map(normalizeType));
565
+ const wantSet = kindKeywordSet(preferred);
554
566
  const entityInfo = resolveEntityKindInfo(r, opts);
555
567
  const recorded = normalizeType(resolveResultType(r));
556
568
  const type = recorded && recorded !== 'code' && recorded !== 'chunk'
@@ -12,6 +12,7 @@
12
12
 
13
13
  import fs from 'fs/promises';
14
14
  import { existsSync, createWriteStream, createReadStream, statSync } from 'fs';
15
+ import readline from 'readline';
15
16
  import path from 'path';
16
17
  import { DB_PATHS, LATE_INTERACTION_CONFIG } from '../infrastructure/config/index.js';
17
18
  import { wasmMaxSimF32, wasmMaxSimDequantPerToken, wasmMaxSimDequant4Bit, nativeMaxSimBatch, nativeMaxSimBatchPerToken, nativeMaxSimBatch4Bit, initWasm, isNativeMaxSimAvailable, isNativePerTokenAvailable, isNative4BitAvailable } from '../infrastructure/simd-distance.js';
@@ -938,33 +939,90 @@ export class LateInteractionIndex {
938
939
  }
939
940
 
940
941
  async _saveAliasSidecar(indexPath = this.indexPath) {
942
+ const sidecarPath = this._aliasSidecarPath(indexPath);
941
943
  if (this.aliasPointers.size === 0) {
942
944
  // Remove any stale sidecar from a previous build.
943
- try { await fs.unlink(this._aliasSidecarPath(indexPath)); } catch (_e) { /* not present */ }
945
+ try { await fs.unlink(sidecarPath); } catch (_e) { /* not present */ }
944
946
  return;
945
947
  }
946
- const entries = [];
947
- for (const [aliasId, ptr] of this.aliasPointers) {
948
- entries.push({
949
- aliasId,
950
- exemplarId: ptr.exemplarId,
951
- clusterId: ptr.clusterId,
952
- metadata: ptr.metadata || {},
953
- });
948
+ // NDJSON (version 2): one header line, then one alias pointer per line,
949
+ // flushed through a write stream in bounded batches. The previous
950
+ // monolithic JSON.stringify of the whole payload hits V8's ~512 MB string
951
+ // ceiling ("Invalid string length") on extreme-dedup repos, and the
952
+ // matching readFile on load had the same ceiling.
953
+ const ws = createWriteStream(sidecarPath, { encoding: 'utf8' });
954
+ const finished = new Promise((resolve, reject) => {
955
+ ws.on('error', reject);
956
+ ws.on('finish', resolve);
957
+ });
958
+ const flush = (str) => new Promise((resolve, reject) => {
959
+ ws.write(str, (err) => (err ? reject(err) : resolve()));
960
+ });
961
+ try {
962
+ let lines = [JSON.stringify({ version: 2, count: this.aliasPointers.size })];
963
+ for (const [aliasId, ptr] of this.aliasPointers) {
964
+ lines.push(JSON.stringify({
965
+ aliasId,
966
+ exemplarId: ptr.exemplarId,
967
+ clusterId: ptr.clusterId,
968
+ metadata: ptr.metadata || {},
969
+ }));
970
+ if (lines.length >= 20000) {
971
+ await flush(lines.join('\n') + '\n');
972
+ lines = [];
973
+ }
974
+ }
975
+ if (lines.length > 0) await flush(lines.join('\n') + '\n');
976
+ ws.end();
977
+ await finished;
978
+ } catch (err) {
979
+ finished.catch(() => { /* surfaced via throw below */ });
980
+ ws.destroy();
981
+ throw err;
954
982
  }
955
- const payload = { version: 1, count: entries.length, aliases: entries };
956
- await fs.writeFile(this._aliasSidecarPath(indexPath), JSON.stringify(payload));
957
983
  }
958
984
 
959
985
  async _loadAliasSidecar(indexPath = this.indexPath) {
960
986
  const p = this._aliasSidecarPath(indexPath);
961
987
  if (!existsSync(p)) return;
988
+ // Streamed line-by-line so no single string approaches V8's ~512 MB
989
+ // ceiling. Two on-disk formats:
990
+ // v1 — one JSON.stringify'd { version: 1, aliases: [...] } line
991
+ // v2 — NDJSON: { version: 2, count } header, then one alias per line
992
+ //
993
+ // The input stream is destroyed in `finally`: early returns abandon the
994
+ // readline iterator, and rl.close() does NOT destroy its input — an
995
+ // fs read stream only auto-closes on 'end'/'error', so without the
996
+ // explicit destroy every early return leaks an fd (fatal only in
997
+ // long-lived processes like the daemon, but a leak everywhere).
998
+ const input = createReadStream(p, { encoding: 'utf8' });
999
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
962
1000
  try {
963
- const raw = await fs.readFile(p, 'utf-8');
964
- const payload = JSON.parse(raw);
965
- if (!payload || !Array.isArray(payload.aliases)) return;
966
1001
  this.aliasPointers.clear();
967
- for (const { aliasId, exemplarId, clusterId, metadata } of payload.aliases) {
1002
+ let first = true;
1003
+ let expected = 0;
1004
+ let parsed = 0;
1005
+ for await (const line of rl) {
1006
+ if (!line) continue;
1007
+ if (first) {
1008
+ first = false;
1009
+ const head = JSON.parse(line);
1010
+ if (head && Array.isArray(head.aliases)) {
1011
+ // v1 monolithic payload — the whole sidecar is this one line,
1012
+ // so JSON.parse succeeding IS the integrity check (any
1013
+ // truncation makes it unparseable).
1014
+ for (const { aliasId, exemplarId, clusterId, metadata } of head.aliases) {
1015
+ if (!this.documents.has(exemplarId)) continue; // orphan guard, see below
1016
+ this.aliasPointers.set(aliasId, { exemplarId, clusterId, metadata: metadata || {} });
1017
+ }
1018
+ return;
1019
+ }
1020
+ if (!head || head.version !== 2 || !Number.isFinite(head.count)) return;
1021
+ expected = head.count;
1022
+ continue;
1023
+ }
1024
+ const { aliasId, exemplarId, clusterId, metadata } = JSON.parse(line);
1025
+ parsed++;
968
1026
  // Orphan guard: drop aliases whose exemplar is no longer in documents.
969
1027
  // Happens if the file containing the exemplar was removed between
970
1028
  // save and load (incremental re-index removed the exemplar file
@@ -972,8 +1030,21 @@ export class LateInteractionIndex {
972
1030
  if (!this.documents.has(exemplarId)) continue;
973
1031
  this.aliasPointers.set(aliasId, { exemplarId, clusterId, metadata: metadata || {} });
974
1032
  }
1033
+ // Truncation guard: NDJSON has no whole-file parse to fail, so a file
1034
+ // cut exactly at a line boundary (crash mid-save, disk full) is valid
1035
+ // prefix NDJSON and would otherwise load a silent subset. The header's
1036
+ // `count` restores v1's all-or-nothing semantics. Compared against
1037
+ // PARSED lines, not kept aliases — the orphan guard intentionally
1038
+ // drops entries and must not trip this.
1039
+ if (parsed !== expected) {
1040
+ this.aliasPointers.clear();
1041
+ }
975
1042
  } catch (_e) {
976
1043
  // Malformed sidecar — treat as absent; aliases will be skipped at query time.
1044
+ this.aliasPointers.clear();
1045
+ } finally {
1046
+ rl.close();
1047
+ input.destroy();
977
1048
  }
978
1049
  }
979
1050