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.
- package/core/graph/graph-extractor.js +24 -14
- package/core/graph/hcgs-generator.js +4 -3
- package/core/incremental-indexing/application/production-reconciler.mjs +29 -9
- package/core/incremental-indexing/domain/encoder-input.mjs +9 -4
- package/core/indexing/ast-chunker.js +65 -62
- package/core/indexing/indexer-build.js +18 -4
- package/core/indexing/indexer-phases.js +45 -24
- package/core/indexing/indexer-utils.js +96 -6
- package/core/indexing/indexing-file-policy.js +30 -7
- package/core/infrastructure/code-graph-repository.js +18 -18
- package/core/infrastructure/config/search.js +18 -1
- package/core/infrastructure/db-utils.js +29 -0
- package/core/infrastructure/language-patterns/maps.js +37 -0
- package/core/infrastructure/simd-distance.js +35 -21
- package/core/infrastructure/tombstone-bitmap-reader.js +3 -1
- package/core/ranking/file-kind-ranking.js +14 -2
- package/core/ranking/late-interaction-index.js +86 -15
- package/core/ranking/mmr.js +15 -10
- package/core/search/search-anchor.js +21 -3
- package/core/search/search-boost.js +25 -13
- package/core/search/search-fusion.js +13 -5
- package/core/search/search-read-semantic.js +13 -12
- package/core/search/search-server.js +2 -1
- package/core/vector-store/binary-hnsw-index.js +13 -4
- package/core/vector-store/float-vector-store.js +9 -5
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +11 -0
- package/package.json +8 -8
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync } from 'fs';
|
|
11
|
-
import {
|
|
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
|
|
91
|
-
|
|
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
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
830
|
+
const staleRow = prepareCached(db, staleSql).get(...staleArgs);
|
|
831
831
|
return staleRow ? { staleSince: staleRow.stale_since || null } : null;
|
|
832
832
|
}
|
|
833
|
-
const row = db
|
|
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}',
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
197
|
-
if (needed >
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
945
|
+
try { await fs.unlink(sidecarPath); } catch (_e) { /* not present */ }
|
|
944
946
|
return;
|
|
945
947
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
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
|
|