@wrongstack/tools 0.306.4 → 0.307.1
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/dist/audit.js +14 -8
- package/dist/auto-proceed-loop-guard.js +8 -2
- package/dist/bash.js +57 -22
- package/dist/builtin.d.ts +6 -6
- package/dist/builtin.js +11469 -9304
- package/dist/clarify.d.ts +32 -0
- package/dist/codebase-index/ast-invariant-engine.d.ts +102 -0
- package/dist/codebase-index/ast-symbol-mutator.d.ts +28 -0
- package/dist/codebase-index/background-indexer.d.ts +9 -0
- package/dist/codebase-index/codebase-ast-replace-tool.d.ts +31 -0
- package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +42 -0
- package/dist/codebase-index/codebase-invariant-check-tool.d.ts +23 -0
- package/dist/codebase-index/codebase-repo-map-tool.d.ts +22 -0
- package/dist/codebase-index/codebase-skeleton-tool.d.ts +36 -0
- package/dist/codebase-index/codebase-targeted-test-tool.d.ts +30 -0
- package/dist/codebase-index/index.d.ts +11 -1
- package/dist/codebase-index/index.js +4239 -1492
- package/dist/codebase-index/project-server-client-state.d.ts +79 -0
- package/dist/codebase-index/project-server-client.d.ts +3 -71
- package/dist/codebase-index/project-server-protocol.d.ts +1 -0
- package/dist/codebase-index/project-server.js +819 -933
- package/dist/codebase-index/repo-map.d.ts +20 -0
- package/dist/codebase-index/skeleton-extractor.d.ts +64 -0
- package/dist/codebase-index/tree-sitter-parser.d.ts +11 -0
- package/dist/codebase-index/worker.js +790 -914
- package/dist/codebase-index/writer-graph-helpers.d.ts +1 -1
- package/dist/codebase-index/writer-helpers.d.ts +12 -0
- package/dist/codebase-index/writer-mutations.d.ts +26 -0
- package/dist/codebase-index/writer-refs.d.ts +50 -0
- package/dist/codebase-index/writer-search.d.ts +30 -0
- package/dist/codebase-index/writer.d.ts +2 -320
- package/dist/edit.js +8602 -349
- package/dist/exec.js +73 -20
- package/dist/fetch.js +11 -9
- package/dist/format.js +14 -8
- package/dist/glob.js +5 -1
- package/dist/grep.js +19 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6178 -4037
- package/dist/install.js +14 -8
- package/dist/json.js +88 -15
- package/dist/kanban-board-actions.d.ts +4 -0
- package/dist/kanban-lifecycle-actions.d.ts +4 -0
- package/dist/kanban-serializer.d.ts +21 -0
- package/dist/kanban.js +985 -1034
- package/dist/languages/index.js +14 -8
- package/dist/lint.js +14 -8
- package/dist/logs.js +13 -3
- package/dist/next-steps.d.ts +8 -0
- package/dist/next-steps.js +18 -0
- package/dist/outdated.js +14 -8
- package/dist/pack.js +11461 -9304
- package/dist/patch.js +8345 -78
- package/dist/plan.js +1231 -1281
- package/dist/process-registry.js +14 -8
- package/dist/ps-slash.js +65 -30
- package/dist/read.js +822 -937
- package/dist/replace.js +8494 -224
- package/dist/search.js +21 -15
- package/dist/security-ast-scan-tool.d.ts +43 -0
- package/dist/session-kanban-graph.d.ts +9 -0
- package/dist/session-kanban-sync.d.ts +31 -0
- package/dist/session-kanban.d.ts +5 -141
- package/dist/session-kanban.js +367 -360
- package/dist/task.js +1157 -1207
- package/dist/test.js +14 -8
- package/dist/todo.js +2187 -2237
- package/dist/tool-diff.js +6 -1
- package/dist/tool-summary.js +4 -2
- package/dist/tool-tier.js +11469 -9304
- package/dist/typecheck.js +14 -8
- package/dist/write.js +8425 -160
- package/package.json +7 -7
|
@@ -566,17 +566,44 @@ function fallbackParse(filePath, content, lang) {
|
|
|
566
566
|
const col = line.length - trimmed.length + 1;
|
|
567
567
|
const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
|
|
568
568
|
if (fn?.[1]) {
|
|
569
|
-
addFallbackSymbol(symbols, {
|
|
569
|
+
addFallbackSymbol(symbols, {
|
|
570
|
+
filePath,
|
|
571
|
+
lang,
|
|
572
|
+
kind: trimmed.startsWith("func (") ? "method" : "function",
|
|
573
|
+
name: fn[1],
|
|
574
|
+
line: idx + 1,
|
|
575
|
+
col,
|
|
576
|
+
signature: trimmed,
|
|
577
|
+
scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
|
|
578
|
+
});
|
|
570
579
|
continue;
|
|
571
580
|
}
|
|
572
581
|
const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
|
|
573
582
|
if (typeDecl?.[1]) {
|
|
574
|
-
addFallbackSymbol(symbols, {
|
|
583
|
+
addFallbackSymbol(symbols, {
|
|
584
|
+
filePath,
|
|
585
|
+
lang,
|
|
586
|
+
kind: "type",
|
|
587
|
+
name: typeDecl[1],
|
|
588
|
+
line: idx + 1,
|
|
589
|
+
col,
|
|
590
|
+
signature: trimmed,
|
|
591
|
+
scope: packageName
|
|
592
|
+
});
|
|
575
593
|
continue;
|
|
576
594
|
}
|
|
577
595
|
const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
|
|
578
596
|
if (valueDecl?.[1] && valueDecl[2]) {
|
|
579
|
-
addFallbackSymbol(symbols, {
|
|
597
|
+
addFallbackSymbol(symbols, {
|
|
598
|
+
filePath,
|
|
599
|
+
lang,
|
|
600
|
+
kind: valueDecl[1],
|
|
601
|
+
name: valueDecl[2],
|
|
602
|
+
line: idx + 1,
|
|
603
|
+
col,
|
|
604
|
+
signature: trimmed,
|
|
605
|
+
scope: packageName
|
|
606
|
+
});
|
|
580
607
|
}
|
|
581
608
|
}
|
|
582
609
|
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
@@ -1027,7 +1054,10 @@ function parseGeneric(opts) {
|
|
|
1027
1054
|
const seen = /* @__PURE__ */ new Set();
|
|
1028
1055
|
const nlOffsets = newlineOffsets2(content);
|
|
1029
1056
|
for (const pattern of patterns) {
|
|
1030
|
-
const re = new RegExp(
|
|
1057
|
+
const re = new RegExp(
|
|
1058
|
+
pattern.re.source,
|
|
1059
|
+
pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`
|
|
1060
|
+
);
|
|
1031
1061
|
re.lastIndex = 0;
|
|
1032
1062
|
for (const match of content.matchAll(re)) {
|
|
1033
1063
|
if (symbols.length >= maxSymbols) break;
|
|
@@ -1134,7 +1164,10 @@ var init_generic_parser = __esm({
|
|
|
1134
1164
|
],
|
|
1135
1165
|
kotlin: [
|
|
1136
1166
|
{ re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
|
|
1137
|
-
{
|
|
1167
|
+
{
|
|
1168
|
+
re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g,
|
|
1169
|
+
kind: "class"
|
|
1170
|
+
}
|
|
1138
1171
|
],
|
|
1139
1172
|
scala: [
|
|
1140
1173
|
{ re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
|
|
@@ -1145,14 +1178,13 @@ var init_generic_parser = __esm({
|
|
|
1145
1178
|
{ re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
|
|
1146
1179
|
],
|
|
1147
1180
|
sql: [
|
|
1148
|
-
{
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
],
|
|
1153
|
-
toml: [
|
|
1154
|
-
{ re: /^\[([^\]]+)\]/gm, kind: "namespace" }
|
|
1181
|
+
{
|
|
1182
|
+
re: /\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|VIEW|INDEX|FUNCTION|PROCEDURE|TRIGGER)\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_"][\w."]*)/gi,
|
|
1183
|
+
kind: "type"
|
|
1184
|
+
}
|
|
1155
1185
|
],
|
|
1186
|
+
md: [{ re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }],
|
|
1187
|
+
toml: [{ re: /^\[([^\]]+)\]/gm, kind: "namespace" }],
|
|
1156
1188
|
html: [
|
|
1157
1189
|
{ re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
|
|
1158
1190
|
{ re: /<(?:script|template|style)\b/gi, kind: "namespace" }
|
|
@@ -1162,11 +1194,17 @@ var init_generic_parser = __esm({
|
|
|
1162
1194
|
{ re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
|
|
1163
1195
|
],
|
|
1164
1196
|
vue: [
|
|
1165
|
-
{
|
|
1197
|
+
{
|
|
1198
|
+
re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g,
|
|
1199
|
+
kind: "function"
|
|
1200
|
+
},
|
|
1166
1201
|
{ re: /<(?:script|template|style)\b/gi, kind: "namespace" }
|
|
1167
1202
|
],
|
|
1168
1203
|
svelte: [
|
|
1169
|
-
{
|
|
1204
|
+
{
|
|
1205
|
+
re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g,
|
|
1206
|
+
kind: "function"
|
|
1207
|
+
}
|
|
1170
1208
|
],
|
|
1171
1209
|
dart: [
|
|
1172
1210
|
{ re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
|
|
@@ -1380,12 +1418,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1380
1418
|
cachedPyBinary ??= resolvePython();
|
|
1381
1419
|
const pyBinary = await cachedPyBinary;
|
|
1382
1420
|
if (!pyBinary) return null;
|
|
1383
|
-
const { code, stdout } = await spawnPyParser(
|
|
1384
|
-
pyBinary,
|
|
1385
|
-
_cachedScriptPath,
|
|
1386
|
-
filePath,
|
|
1387
|
-
content
|
|
1388
|
-
);
|
|
1421
|
+
const { code, stdout } = await spawnPyParser(pyBinary, _cachedScriptPath, filePath, content);
|
|
1389
1422
|
if (code !== 0 || !stdout.trim()) {
|
|
1390
1423
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1391
1424
|
}
|
|
@@ -2579,7 +2612,8 @@ __export(tree_sitter_parser_exports, {
|
|
|
2579
2612
|
getGrammarWasmPath: () => getGrammarWasmPath,
|
|
2580
2613
|
isTreeSitterSupported: () => isTreeSitterSupported,
|
|
2581
2614
|
loadTreeSitterLanguage: () => loadTreeSitterLanguage,
|
|
2582
|
-
parseSymbols: () => parseSymbols8
|
|
2615
|
+
parseSymbols: () => parseSymbols8,
|
|
2616
|
+
parseTreeSitterAst: () => parseTreeSitterAst
|
|
2583
2617
|
});
|
|
2584
2618
|
import * as path9 from "node:path";
|
|
2585
2619
|
import { fileURLToPath } from "node:url";
|
|
@@ -2671,6 +2705,26 @@ async function __smokeRootType(opts) {
|
|
|
2671
2705
|
parser.delete();
|
|
2672
2706
|
}
|
|
2673
2707
|
}
|
|
2708
|
+
async function parseTreeSitterAst(opts) {
|
|
2709
|
+
const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
|
|
2710
|
+
if (!grammar) return null;
|
|
2711
|
+
try {
|
|
2712
|
+
const { Parser, Language, init } = await getRuntime();
|
|
2713
|
+
await init();
|
|
2714
|
+
const wasmPath = path9.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
|
|
2715
|
+
const languageObj = await Language.load(wasmPath);
|
|
2716
|
+
const parser = new Parser();
|
|
2717
|
+
parser.setLanguage(languageObj);
|
|
2718
|
+
const tree = parser.parse(opts.content);
|
|
2719
|
+
if (!tree) {
|
|
2720
|
+
parser.delete();
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
return { tree, parser };
|
|
2724
|
+
} catch {
|
|
2725
|
+
return null;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2674
2728
|
var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
|
|
2675
2729
|
var init_tree_sitter_parser = __esm({
|
|
2676
2730
|
"src/codebase-index/tree-sitter-parser.ts"() {
|
|
@@ -3482,10 +3536,7 @@ var LANG_IMPORTS = {
|
|
|
3482
3536
|
// quoted path. A stray match elsewhere resolves to no file and is dropped.
|
|
3483
3537
|
{ re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
|
|
3484
3538
|
],
|
|
3485
|
-
py: [
|
|
3486
|
-
{ re: /^[ \t]*import\s+([\w.]+)/gm },
|
|
3487
|
-
{ re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
|
|
3488
|
-
],
|
|
3539
|
+
py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
|
|
3489
3540
|
rs: [
|
|
3490
3541
|
// use a::b::C; | use a::b::{C, D}; → the path before any brace
|
|
3491
3542
|
{ re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
|
|
@@ -3822,10 +3873,7 @@ function defaultWorkerCount() {
|
|
|
3822
3873
|
return Math.max(1, Math.min(4, cores - 1));
|
|
3823
3874
|
}
|
|
3824
3875
|
function resolveWorkerScriptUrl() {
|
|
3825
|
-
for (const rel of [
|
|
3826
|
-
"./parser-worker-script.js",
|
|
3827
|
-
"./codebase-index/parser-worker-script.js"
|
|
3828
|
-
]) {
|
|
3876
|
+
for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
|
|
3829
3877
|
try {
|
|
3830
3878
|
const url = new URL(rel, import.meta.url);
|
|
3831
3879
|
if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
|
|
@@ -3841,7 +3889,6 @@ function getParserPool() {
|
|
|
3841
3889
|
}
|
|
3842
3890
|
|
|
3843
3891
|
// src/codebase-index/writer.ts
|
|
3844
|
-
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
3845
3892
|
import * as fs8 from "node:fs";
|
|
3846
3893
|
import * as path11 from "node:path";
|
|
3847
3894
|
|
|
@@ -3937,39 +3984,6 @@ var Bm25Index = class {
|
|
|
3937
3984
|
// src/codebase-index/writer.ts
|
|
3938
3985
|
init_languages();
|
|
3939
3986
|
|
|
3940
|
-
// src/codebase-index/lsp-kind.ts
|
|
3941
|
-
function lspKindToInternalKind(k) {
|
|
3942
|
-
switch (k) {
|
|
3943
|
-
case 5 /* Class */:
|
|
3944
|
-
return "class";
|
|
3945
|
-
case 6 /* Method */:
|
|
3946
|
-
return "method";
|
|
3947
|
-
case 7 /* Property */:
|
|
3948
|
-
case 8 /* Field */:
|
|
3949
|
-
return "property";
|
|
3950
|
-
case 9 /* Constructor */:
|
|
3951
|
-
return "class";
|
|
3952
|
-
case 10 /* Enum */:
|
|
3953
|
-
return "enum";
|
|
3954
|
-
case 11 /* Interface */:
|
|
3955
|
-
return "interface";
|
|
3956
|
-
case 12 /* Function */:
|
|
3957
|
-
return "function";
|
|
3958
|
-
case 13 /* Variable */:
|
|
3959
|
-
return "var";
|
|
3960
|
-
case 14 /* Constant */:
|
|
3961
|
-
return "const";
|
|
3962
|
-
case 22 /* EnumMember */:
|
|
3963
|
-
return "enum";
|
|
3964
|
-
case 26 /* TypeParameter */:
|
|
3965
|
-
return "type";
|
|
3966
|
-
case 3 /* Namespace */:
|
|
3967
|
-
return "namespace";
|
|
3968
|
-
default:
|
|
3969
|
-
return null;
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
|
|
3973
3987
|
// src/codebase-index/schema.ts
|
|
3974
3988
|
var SCHEMA_VERSION = 4;
|
|
3975
3989
|
|
|
@@ -4116,91 +4130,14 @@ function runSqliteWithRetry(fn) {
|
|
|
4116
4130
|
throw lastError;
|
|
4117
4131
|
}
|
|
4118
4132
|
|
|
4119
|
-
// src/codebase-index/vector-search.ts
|
|
4120
|
-
var RRF_K = 60;
|
|
4121
|
-
var VECTOR_DIMENSIONS = 384;
|
|
4122
|
-
var NGRAM_SIZE = 3;
|
|
4123
|
-
function embedText(text) {
|
|
4124
|
-
const vec = new Float32Array(VECTOR_DIMENSIONS);
|
|
4125
|
-
const normalized = text.toLowerCase().trim();
|
|
4126
|
-
if (normalized.length < NGRAM_SIZE) {
|
|
4127
|
-
const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
|
|
4128
|
-
for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
|
|
4129
|
-
const ngram = padded.slice(i, i + NGRAM_SIZE);
|
|
4130
|
-
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
4131
|
-
vec[bucket] += 1;
|
|
4132
|
-
}
|
|
4133
|
-
} else {
|
|
4134
|
-
for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
|
|
4135
|
-
const ngram = normalized.slice(i, i + NGRAM_SIZE);
|
|
4136
|
-
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
4137
|
-
vec[bucket] += 1;
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
let norm = 0;
|
|
4141
|
-
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
4142
|
-
norm += vec[i] * vec[i];
|
|
4143
|
-
}
|
|
4144
|
-
norm = Math.sqrt(norm);
|
|
4145
|
-
if (norm > 0) {
|
|
4146
|
-
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
4147
|
-
vec[i] /= norm;
|
|
4148
|
-
}
|
|
4149
|
-
}
|
|
4150
|
-
return vec;
|
|
4151
|
-
}
|
|
4152
|
-
function hashNgram(str) {
|
|
4153
|
-
let hash = 2166136261;
|
|
4154
|
-
for (let i = 0; i < str.length; i++) {
|
|
4155
|
-
hash ^= str.charCodeAt(i);
|
|
4156
|
-
hash = Math.imul(hash, 16777619);
|
|
4157
|
-
}
|
|
4158
|
-
return hash >>> 0;
|
|
4159
|
-
}
|
|
4160
|
-
function cosineSimilarity(a, b) {
|
|
4161
|
-
let dot = 0;
|
|
4162
|
-
const len = Math.min(a.length, b.length);
|
|
4163
|
-
for (let i = 0; i < len; i++) {
|
|
4164
|
-
dot += a[i] * b[i];
|
|
4165
|
-
}
|
|
4166
|
-
return dot;
|
|
4167
|
-
}
|
|
4168
|
-
function encodeVector(vec) {
|
|
4169
|
-
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
4170
|
-
}
|
|
4171
|
-
function decodeVector(buf) {
|
|
4172
|
-
const view = new DataView(
|
|
4173
|
-
buf.buffer,
|
|
4174
|
-
buf.byteOffset,
|
|
4175
|
-
buf.byteLength
|
|
4176
|
-
);
|
|
4177
|
-
const copy = new Float32Array(buf.byteLength / 4);
|
|
4178
|
-
for (let i = 0; i < copy.length; i++) {
|
|
4179
|
-
copy[i] = view.getFloat32(i * 4, true);
|
|
4180
|
-
}
|
|
4181
|
-
return copy;
|
|
4182
|
-
}
|
|
4183
|
-
function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
|
|
4184
|
-
const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
|
|
4185
|
-
const scored = [];
|
|
4186
|
-
for (const id of allIds) {
|
|
4187
|
-
const bm25Rank = bm25Ranks.get(id);
|
|
4188
|
-
const vecRank = vectorRanks.get(id);
|
|
4189
|
-
let score = 0;
|
|
4190
|
-
if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
|
|
4191
|
-
if (vecRank !== void 0) score += 1 / (k + vecRank);
|
|
4192
|
-
scored.push([id, score]);
|
|
4193
|
-
}
|
|
4194
|
-
scored.sort((a, b) => b[1] - a[1]);
|
|
4195
|
-
return scored;
|
|
4196
|
-
}
|
|
4197
|
-
|
|
4198
4133
|
// src/codebase-index/writer-admin.ts
|
|
4199
4134
|
import * as fs7 from "node:fs";
|
|
4200
4135
|
import * as path10 from "node:path";
|
|
4201
4136
|
var DB_FILE = "index.db";
|
|
4202
4137
|
function getAllIndexableWithStatement(stmt) {
|
|
4203
|
-
return stmt("SELECT id, text FROM symbols").all().map(
|
|
4138
|
+
return stmt("SELECT id, text FROM symbols").all().map(
|
|
4139
|
+
({ id, text }) => ({ id, text })
|
|
4140
|
+
);
|
|
4204
4141
|
}
|
|
4205
4142
|
function getMaxSymbolIdWithStatement(stmt) {
|
|
4206
4143
|
const rows = stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
@@ -4425,10 +4362,16 @@ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
|
|
|
4425
4362
|
}
|
|
4426
4363
|
return { fileNodes, symToFile, fileStats, ensureFileNode };
|
|
4427
4364
|
}
|
|
4428
|
-
function buildSymbolGraphNodes(symById, relatedIds,
|
|
4365
|
+
function buildSymbolGraphNodes(symById, relatedIds, localFiles, packageOf) {
|
|
4366
|
+
const local = new Set(
|
|
4367
|
+
[...typeof localFiles === "string" ? [localFiles] : localFiles].map(
|
|
4368
|
+
(file) => file.replace(/\\/g, "/")
|
|
4369
|
+
)
|
|
4370
|
+
);
|
|
4371
|
+
const isLocal = (file) => local.has(file.replace(/\\/g, "/"));
|
|
4429
4372
|
return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
|
|
4430
|
-
const aExternal = a.file
|
|
4431
|
-
const bExternal = b.file
|
|
4373
|
+
const aExternal = isLocal(a.file) ? 0 : 1;
|
|
4374
|
+
const bExternal = isLocal(b.file) ? 0 : 1;
|
|
4432
4375
|
return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
|
|
4433
4376
|
}).map((s) => ({
|
|
4434
4377
|
id: `sym:${s.id}`,
|
|
@@ -4442,7 +4385,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
|
|
|
4442
4385
|
line: s.line,
|
|
4443
4386
|
signature: s.signature,
|
|
4444
4387
|
scope: s.scope,
|
|
4445
|
-
external: s.file
|
|
4388
|
+
external: !isLocal(s.file)
|
|
4446
4389
|
}));
|
|
4447
4390
|
}
|
|
4448
4391
|
function addWeightedEdge(edgeMap, source, target, callType, weight) {
|
|
@@ -4477,6 +4420,52 @@ function materializeWeightedEdges(edgeMap, idPrefix) {
|
|
|
4477
4420
|
return edges;
|
|
4478
4421
|
}
|
|
4479
4422
|
|
|
4423
|
+
// src/codebase-index/writer-helpers.ts
|
|
4424
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
4425
|
+
function escapeLike(value) {
|
|
4426
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
4427
|
+
}
|
|
4428
|
+
function posixIndexPath(file) {
|
|
4429
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4430
|
+
}
|
|
4431
|
+
function indexedFileMatchSql(column = "file") {
|
|
4432
|
+
return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
|
|
4433
|
+
}
|
|
4434
|
+
function indexedFileMatchArgs(file) {
|
|
4435
|
+
const posix4 = posixIndexPath(file.trim());
|
|
4436
|
+
return [file, posix4, `%/${escapeLike(posix4)}`];
|
|
4437
|
+
}
|
|
4438
|
+
function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
|
|
4439
|
+
if (packageLabel === filter) return true;
|
|
4440
|
+
const posixFile = posixIndexPath(storedFile);
|
|
4441
|
+
const posixFilter = posixIndexPath(filter.trim());
|
|
4442
|
+
if (!posixFilter) return false;
|
|
4443
|
+
return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
|
|
4444
|
+
}
|
|
4445
|
+
function assignRefsToSymbols(refs, symbols) {
|
|
4446
|
+
if (refs.length === 0 || symbols.length === 0) return [];
|
|
4447
|
+
const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
|
|
4448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4449
|
+
const assigned = [];
|
|
4450
|
+
for (const ref of refs) {
|
|
4451
|
+
let owner;
|
|
4452
|
+
for (const symbol of ordered) {
|
|
4453
|
+
if (symbol.line > ref.line) break;
|
|
4454
|
+
owner = symbol;
|
|
4455
|
+
}
|
|
4456
|
+
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
4457
|
+
if (!owner || owner.id <= 0) continue;
|
|
4458
|
+
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
4459
|
+
if (seen.has(key)) continue;
|
|
4460
|
+
seen.add(key);
|
|
4461
|
+
assigned.push({ ...ref, fromId: owner.id });
|
|
4462
|
+
}
|
|
4463
|
+
return assigned;
|
|
4464
|
+
}
|
|
4465
|
+
function resolveIndexDir(projectRoot2, override) {
|
|
4466
|
+
return override ?? resolveWstackPaths({ projectRoot: projectRoot2 }).projectCodebaseIndex;
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4480
4469
|
// src/codebase-index/writer-ref-mapper.ts
|
|
4481
4470
|
function mapWriterRefRow(row) {
|
|
4482
4471
|
return {
|
|
@@ -4532,15 +4521,29 @@ function mapCallSiteRow(row) {
|
|
|
4532
4521
|
line: row.ref_line
|
|
4533
4522
|
};
|
|
4534
4523
|
}
|
|
4524
|
+
function resolveIndexedFiles(stmt, file) {
|
|
4525
|
+
const rows = stmt(
|
|
4526
|
+
`SELECT DISTINCT file FROM symbols WHERE ${indexedFileMatchSql("file")} ORDER BY length(file), file`
|
|
4527
|
+
).all(...indexedFileMatchArgs(file));
|
|
4528
|
+
return rows.map((row) => row.file);
|
|
4529
|
+
}
|
|
4535
4530
|
function resolveSymbolIds(stmt, symbolName, file) {
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4531
|
+
if (!file) {
|
|
4532
|
+
const rows2 = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(symbolName);
|
|
4533
|
+
return rows2.map((r) => r.id);
|
|
4534
|
+
}
|
|
4535
|
+
const indexedFiles = resolveIndexedFiles(stmt, file);
|
|
4536
|
+
if (indexedFiles.length === 0) return [];
|
|
4537
|
+
const placeholders = indexedFiles.map(() => "?").join(",");
|
|
4538
|
+
const rows = stmt(
|
|
4539
|
+
`SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders}) ORDER BY id`
|
|
4540
|
+
).all(symbolName, ...indexedFiles);
|
|
4539
4541
|
return rows.map((r) => r.id);
|
|
4540
4542
|
}
|
|
4541
4543
|
function findIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
4542
4544
|
const targetIds = resolveSymbolIds(stmt, symbolName, file);
|
|
4543
|
-
if (targetIds.length === 0)
|
|
4545
|
+
if (targetIds.length === 0)
|
|
4546
|
+
return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
|
|
4544
4547
|
let matchIds = targetIds;
|
|
4545
4548
|
let ambiguous = false;
|
|
4546
4549
|
if (file !== void 0) {
|
|
@@ -4591,11 +4594,17 @@ function findIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
|
4591
4594
|
}
|
|
4592
4595
|
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
4593
4596
|
const allCalls = rows.map(mapCallSiteRow);
|
|
4594
|
-
return {
|
|
4597
|
+
return {
|
|
4598
|
+
calls: allCalls.slice(0, limit),
|
|
4599
|
+
symbolFound: true,
|
|
4600
|
+
ambiguous,
|
|
4601
|
+
totalMatches: allCalls.length
|
|
4602
|
+
};
|
|
4595
4603
|
}
|
|
4596
4604
|
function findOutgoingCallsByName(stmt, symbolName, file, limit) {
|
|
4597
4605
|
const sourceIds = resolveSymbolIds(stmt, symbolName, file);
|
|
4598
|
-
if (sourceIds.length === 0)
|
|
4606
|
+
if (sourceIds.length === 0)
|
|
4607
|
+
return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
|
|
4599
4608
|
const unresolvedCount = chunkedIdScalar(
|
|
4600
4609
|
stmt,
|
|
4601
4610
|
sourceIds,
|
|
@@ -4850,7 +4859,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
4850
4859
|
const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
|
|
4851
4860
|
const packageOf = readPackageLabeller(stmt);
|
|
4852
4861
|
const langOf = (file) => detectLang(file) ?? "other";
|
|
4853
|
-
const pkgFilePaths = allFiles.filter((f) => packageOf(f.file)
|
|
4862
|
+
const pkgFilePaths = allFiles.filter((f) => matchesIndexedPackageFilter(f.file, packageOf(f.file), packageFilter)).map((f) => f.file);
|
|
4854
4863
|
const localFiles = new Set(pkgFilePaths);
|
|
4855
4864
|
if (localFiles.size === 0) return { nodes: [], edges: [] };
|
|
4856
4865
|
const filePlaceholders = [...localFiles].map(() => "?").join(",");
|
|
@@ -4926,9 +4935,12 @@ function getFileGraphWithStatement(stmt, packageFilter) {
|
|
|
4926
4935
|
return { nodes: [...fileNodes.values()], edges };
|
|
4927
4936
|
}
|
|
4928
4937
|
function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
4938
|
+
const indexedFiles = resolveIndexedFiles(stmt, fileFilter);
|
|
4939
|
+
if (indexedFiles.length === 0) return { nodes: [], edges: [] };
|
|
4940
|
+
const filePlaceholders = indexedFiles.map(() => "?").join(",");
|
|
4929
4941
|
const syms = stmt(
|
|
4930
|
-
|
|
4931
|
-
).all(
|
|
4942
|
+
`SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY line, id`
|
|
4943
|
+
).all(...indexedFiles);
|
|
4932
4944
|
if (syms.length === 0) return { nodes: [], edges: [] };
|
|
4933
4945
|
const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
|
|
4934
4946
|
const relatedIds = new Set(syms.map((symbol) => symbol.id));
|
|
@@ -4938,16 +4950,16 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
|
4938
4950
|
SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
4939
4951
|
FROM refs r
|
|
4940
4952
|
JOIN symbols s ON s.id = r.from_id
|
|
4941
|
-
WHERE s.file
|
|
4953
|
+
WHERE s.file IN (${filePlaceholders})
|
|
4942
4954
|
UNION
|
|
4943
4955
|
SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
|
|
4944
4956
|
FROM refs r
|
|
4945
4957
|
JOIN symbols s ON s.id = r.to_id
|
|
4946
|
-
WHERE s.file
|
|
4958
|
+
WHERE s.file IN (${filePlaceholders})
|
|
4947
4959
|
)
|
|
4948
4960
|
WHERE to_id IS NOT NULL
|
|
4949
4961
|
GROUP BY from_id, to_id, call_type`
|
|
4950
|
-
).all(
|
|
4962
|
+
).all(...indexedFiles, ...indexedFiles);
|
|
4951
4963
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
4952
4964
|
for (const r of refRows) {
|
|
4953
4965
|
if (r.to_id == null) continue;
|
|
@@ -4966,40 +4978,194 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
|
|
|
4966
4978
|
).all(...missingIds);
|
|
4967
4979
|
for (const s of extras) symById.set(s.id, s);
|
|
4968
4980
|
}
|
|
4969
|
-
const nodes = buildSymbolGraphNodes(
|
|
4981
|
+
const nodes = buildSymbolGraphNodes(
|
|
4982
|
+
symById,
|
|
4983
|
+
relatedIds,
|
|
4984
|
+
new Set(syms.map((symbol) => symbol.file)),
|
|
4985
|
+
readPackageLabeller(stmt)
|
|
4986
|
+
);
|
|
4970
4987
|
return { nodes, edges };
|
|
4971
4988
|
}
|
|
4972
4989
|
|
|
4973
|
-
// src/codebase-index/
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4990
|
+
// src/codebase-index/vector-search.ts
|
|
4991
|
+
var RRF_K = 60;
|
|
4992
|
+
var VECTOR_DIMENSIONS = 384;
|
|
4993
|
+
var NGRAM_SIZE = 3;
|
|
4994
|
+
function embedText(text) {
|
|
4995
|
+
const vec = new Float32Array(VECTOR_DIMENSIONS);
|
|
4996
|
+
const normalized = text.toLowerCase().trim();
|
|
4997
|
+
if (normalized.length < NGRAM_SIZE) {
|
|
4998
|
+
const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
|
|
4999
|
+
for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
|
|
5000
|
+
const ngram = padded.slice(i, i + NGRAM_SIZE);
|
|
5001
|
+
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
5002
|
+
vec[bucket] += 1;
|
|
5003
|
+
}
|
|
5004
|
+
} else {
|
|
5005
|
+
for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
|
|
5006
|
+
const ngram = normalized.slice(i, i + NGRAM_SIZE);
|
|
5007
|
+
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
5008
|
+
vec[bucket] += 1;
|
|
4988
5009
|
}
|
|
4989
|
-
if (!owner && ref.callType === "import") owner = ordered[0];
|
|
4990
|
-
if (!owner || owner.id <= 0) continue;
|
|
4991
|
-
const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
|
|
4992
|
-
if (seen.has(key)) continue;
|
|
4993
|
-
seen.add(key);
|
|
4994
|
-
assigned.push({ ...ref, fromId: owner.id });
|
|
4995
5010
|
}
|
|
4996
|
-
|
|
5011
|
+
let norm = 0;
|
|
5012
|
+
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
5013
|
+
norm += vec[i] * vec[i];
|
|
5014
|
+
}
|
|
5015
|
+
norm = Math.sqrt(norm);
|
|
5016
|
+
if (norm > 0) {
|
|
5017
|
+
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
5018
|
+
vec[i] /= norm;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
return vec;
|
|
4997
5022
|
}
|
|
4998
|
-
function
|
|
4999
|
-
|
|
5023
|
+
function hashNgram(str) {
|
|
5024
|
+
let hash = 2166136261;
|
|
5025
|
+
for (let i = 0; i < str.length; i++) {
|
|
5026
|
+
hash ^= str.charCodeAt(i);
|
|
5027
|
+
hash = Math.imul(hash, 16777619);
|
|
5028
|
+
}
|
|
5029
|
+
return hash >>> 0;
|
|
5000
5030
|
}
|
|
5001
|
-
|
|
5002
|
-
|
|
5031
|
+
function cosineSimilarity(a, b) {
|
|
5032
|
+
let dot = 0;
|
|
5033
|
+
const len = Math.min(a.length, b.length);
|
|
5034
|
+
for (let i = 0; i < len; i++) {
|
|
5035
|
+
dot += a[i] * b[i];
|
|
5036
|
+
}
|
|
5037
|
+
return dot;
|
|
5038
|
+
}
|
|
5039
|
+
function encodeVector(vec) {
|
|
5040
|
+
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
5041
|
+
}
|
|
5042
|
+
function decodeVector(buf) {
|
|
5043
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
5044
|
+
const copy = new Float32Array(buf.byteLength / 4);
|
|
5045
|
+
for (let i = 0; i < copy.length; i++) {
|
|
5046
|
+
copy[i] = view.getFloat32(i * 4, true);
|
|
5047
|
+
}
|
|
5048
|
+
return copy;
|
|
5049
|
+
}
|
|
5050
|
+
function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
|
|
5051
|
+
const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
|
|
5052
|
+
const scored = [];
|
|
5053
|
+
for (const id of allIds) {
|
|
5054
|
+
const bm25Rank = bm25Ranks.get(id);
|
|
5055
|
+
const vecRank = vectorRanks.get(id);
|
|
5056
|
+
let score = 0;
|
|
5057
|
+
if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
|
|
5058
|
+
if (vecRank !== void 0) score += 1 / (k + vecRank);
|
|
5059
|
+
scored.push([id, score]);
|
|
5060
|
+
}
|
|
5061
|
+
scored.sort((a, b) => b[1] - a[1]);
|
|
5062
|
+
return scored;
|
|
5063
|
+
}
|
|
5064
|
+
|
|
5065
|
+
// src/codebase-index/writer-mutations.ts
|
|
5066
|
+
function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
|
|
5067
|
+
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
5068
|
+
return [];
|
|
5069
|
+
}
|
|
5070
|
+
const affectedNames = /* @__PURE__ */ new Set();
|
|
5071
|
+
for (const entry of entries) {
|
|
5072
|
+
for (const symbol of entry.symbols) affectedNames.add(symbol.name);
|
|
5073
|
+
for (const ref of entry.refs) affectedNames.add(ref.toName);
|
|
5074
|
+
}
|
|
5075
|
+
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
5076
|
+
const placeholders = options.deleteForFiles.map(() => "?").join(",");
|
|
5077
|
+
for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
|
|
5078
|
+
affectedNames.add(name);
|
|
5079
|
+
}
|
|
5080
|
+
if (ftsAvailable) {
|
|
5081
|
+
stmtFn(
|
|
5082
|
+
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5083
|
+
).run(...options.deleteForFiles);
|
|
5084
|
+
}
|
|
5085
|
+
if (vectorsAvailable) {
|
|
5086
|
+
stmtFn(
|
|
5087
|
+
`DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5088
|
+
).run(...options.deleteForFiles);
|
|
5089
|
+
}
|
|
5090
|
+
stmtFn(
|
|
5091
|
+
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5092
|
+
).run(...options.deleteForFiles);
|
|
5093
|
+
stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
|
|
5094
|
+
}
|
|
5095
|
+
const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
5096
|
+
let nextId = allocateSymbolIds(totalSymbols);
|
|
5097
|
+
const allInserted = [];
|
|
5098
|
+
const refsToInsert = [];
|
|
5099
|
+
const bulkSyms = [];
|
|
5100
|
+
const ftsRows = [];
|
|
5101
|
+
const vectorRows = [];
|
|
5102
|
+
for (const entry of entries) {
|
|
5103
|
+
const insertedForEntry = [];
|
|
5104
|
+
for (const s of entry.symbols) {
|
|
5105
|
+
const id = nextId++;
|
|
5106
|
+
bulkSyms.push({
|
|
5107
|
+
id,
|
|
5108
|
+
lang: s.lang,
|
|
5109
|
+
kind: s.kind,
|
|
5110
|
+
name: s.name,
|
|
5111
|
+
file: s.file,
|
|
5112
|
+
line: s.line,
|
|
5113
|
+
col: s.col,
|
|
5114
|
+
signature: s.signature,
|
|
5115
|
+
docComment: s.docComment,
|
|
5116
|
+
scope: s.scope,
|
|
5117
|
+
text: s.text
|
|
5118
|
+
});
|
|
5119
|
+
if (ftsAvailable) {
|
|
5120
|
+
ftsRows.push({
|
|
5121
|
+
id,
|
|
5122
|
+
text: buildIndexableText(s.name, s.signature, s.docComment)
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
vectorRows.push({
|
|
5126
|
+
id,
|
|
5127
|
+
vector: encodeVector(
|
|
5128
|
+
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
5129
|
+
)
|
|
5130
|
+
});
|
|
5131
|
+
const inserted = { ...s, id };
|
|
5132
|
+
allInserted.push(inserted);
|
|
5133
|
+
insertedForEntry.push(inserted);
|
|
5134
|
+
}
|
|
5135
|
+
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
5136
|
+
}
|
|
5137
|
+
bulkInsertSymbolsWithStatement((sql) => stmtFn(sql), maxSqlVars, bulkSyms);
|
|
5138
|
+
bulkInsertFtsWithStatement((sql) => stmtFn(sql), maxSqlVars, ftsAvailable, ftsRows);
|
|
5139
|
+
if (vectorsAvailable) {
|
|
5140
|
+
bulkInsertVectorsWithStatement((sql) => stmtFn(sql), maxSqlVars, vectorRows);
|
|
5141
|
+
}
|
|
5142
|
+
bulkInsertRefsWithStatement((sql) => stmtFn(sql), maxSqlVars, refsToInsert);
|
|
5143
|
+
const upsertStmt = stmtFn(
|
|
5144
|
+
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
5145
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
5146
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
5147
|
+
lang = excluded.lang,
|
|
5148
|
+
mtime_ms = excluded.mtime_ms,
|
|
5149
|
+
content_hash = excluded.content_hash,
|
|
5150
|
+
symbol_count = excluded.symbol_count,
|
|
5151
|
+
last_indexed = excluded.last_indexed`
|
|
5152
|
+
);
|
|
5153
|
+
const now = Date.now();
|
|
5154
|
+
for (const entry of entries) {
|
|
5155
|
+
upsertStmt.run(
|
|
5156
|
+
entry.file,
|
|
5157
|
+
entry.lang,
|
|
5158
|
+
entry.mtimeMs,
|
|
5159
|
+
entry.contentHash ?? "",
|
|
5160
|
+
entry.symbolCount,
|
|
5161
|
+
now
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
5164
|
+
resolveRefsForNamesUnsafe2(affectedNames);
|
|
5165
|
+
return allInserted;
|
|
5166
|
+
}
|
|
5167
|
+
|
|
5168
|
+
// src/codebase-index/writer-pragmas.ts
|
|
5003
5169
|
import { sqliteCachePragmas } from "@wrongstack/core/utils";
|
|
5004
5170
|
function applyIndexStorePragmas(db) {
|
|
5005
5171
|
try {
|
|
@@ -5111,6 +5277,237 @@ var SYMBOL_VECTORS_TABLE_SQL = `
|
|
|
5111
5277
|
);
|
|
5112
5278
|
`;
|
|
5113
5279
|
|
|
5280
|
+
// src/codebase-index/writer-refs.ts
|
|
5281
|
+
var FAMILY_MATCH_SQL = `(
|
|
5282
|
+
sym.lang = refs.lang
|
|
5283
|
+
OR EXISTS (
|
|
5284
|
+
SELECT 1 FROM lang_family lf1
|
|
5285
|
+
JOIN lang_family lf2 ON lf1.family = lf2.family
|
|
5286
|
+
WHERE lf1.lang = sym.lang AND lf2.lang = refs.lang
|
|
5287
|
+
)
|
|
5288
|
+
OR ? IN (
|
|
5289
|
+
SELECT family FROM lang_family WHERE lang = refs.lang
|
|
5290
|
+
)
|
|
5291
|
+
)`;
|
|
5292
|
+
function getNamespaceDeclarationsWithStatement(stmtFn) {
|
|
5293
|
+
return stmtFn(
|
|
5294
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
5295
|
+
).all();
|
|
5296
|
+
}
|
|
5297
|
+
function getFilePackagesWithStatement(stmtFn) {
|
|
5298
|
+
const rows = stmtFn("SELECT file, package FROM files WHERE package != ''").all();
|
|
5299
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
5300
|
+
}
|
|
5301
|
+
function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
|
|
5302
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
5303
|
+
FROM refs r
|
|
5304
|
+
JOIN symbols s ON s.id = r.from_id
|
|
5305
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
5306
|
+
if (!onlyFiles?.length) {
|
|
5307
|
+
return stmtFn(base).all();
|
|
5308
|
+
}
|
|
5309
|
+
const out = [];
|
|
5310
|
+
for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
|
|
5311
|
+
const chunk = onlyFiles.slice(i, i + maxSqlVars);
|
|
5312
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
5313
|
+
out.push(
|
|
5314
|
+
...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
5315
|
+
);
|
|
5316
|
+
}
|
|
5317
|
+
return out;
|
|
5318
|
+
}
|
|
5319
|
+
function getAllResolvedRefsWithStatement(stmtFn) {
|
|
5320
|
+
return stmtFn(
|
|
5321
|
+
"SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
|
|
5322
|
+
).all();
|
|
5323
|
+
}
|
|
5324
|
+
function getAllImportRefsWithStatement(stmtFn) {
|
|
5325
|
+
return stmtFn(
|
|
5326
|
+
`SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
|
|
5327
|
+
r.call_type AS callType, r.line
|
|
5328
|
+
FROM refs r
|
|
5329
|
+
LEFT JOIN symbols s ON r.from_id = s.id
|
|
5330
|
+
WHERE r.call_type = 'import'
|
|
5331
|
+
ORDER BY r.line`
|
|
5332
|
+
).all();
|
|
5333
|
+
}
|
|
5334
|
+
function resolveRefsWithStatement(stmtFn) {
|
|
5335
|
+
try {
|
|
5336
|
+
const result = stmtFn(
|
|
5337
|
+
`UPDATE refs
|
|
5338
|
+
SET to_id = s.id
|
|
5339
|
+
FROM (
|
|
5340
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5341
|
+
FROM symbols sym
|
|
5342
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5343
|
+
GROUP BY sym.name, lf.family
|
|
5344
|
+
UNION ALL
|
|
5345
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5346
|
+
FROM symbols sym
|
|
5347
|
+
GROUP BY sym.name
|
|
5348
|
+
) AS s,
|
|
5349
|
+
lang_family AS rf
|
|
5350
|
+
WHERE refs.to_id IS NULL
|
|
5351
|
+
AND refs.to_name IS NOT NULL
|
|
5352
|
+
AND rf.lang = refs.lang
|
|
5353
|
+
AND s.name = refs.to_name
|
|
5354
|
+
AND s.family = rf.family`
|
|
5355
|
+
).run();
|
|
5356
|
+
return result.changes ?? 0;
|
|
5357
|
+
} catch {
|
|
5358
|
+
const result = stmtFn(
|
|
5359
|
+
`UPDATE refs SET to_id = (
|
|
5360
|
+
SELECT sym.id FROM symbols sym
|
|
5361
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5362
|
+
ORDER BY sym.id LIMIT 1
|
|
5363
|
+
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
5364
|
+
AND EXISTS (
|
|
5365
|
+
SELECT 1 FROM symbols sym
|
|
5366
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5367
|
+
)`
|
|
5368
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
5369
|
+
return result.changes ?? 0;
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVars, resolutions) {
|
|
5373
|
+
if (resolutions.length === 0) return 0;
|
|
5374
|
+
return runWithRetry(() => {
|
|
5375
|
+
db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
5376
|
+
db.exec(
|
|
5377
|
+
`CREATE TEMP TABLE import_resolution (
|
|
5378
|
+
from_file TEXT NOT NULL,
|
|
5379
|
+
lang TEXT NOT NULL,
|
|
5380
|
+
module TEXT NOT NULL,
|
|
5381
|
+
to_file TEXT NOT NULL
|
|
5382
|
+
)`
|
|
5383
|
+
);
|
|
5384
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
|
|
5385
|
+
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
5386
|
+
const chunk = resolutions.slice(i, i + chunkSize);
|
|
5387
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
5388
|
+
const binds = [];
|
|
5389
|
+
for (const entry of chunk) {
|
|
5390
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
5391
|
+
}
|
|
5392
|
+
stmtFn(
|
|
5393
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
5394
|
+
VALUES ${placeholders}`
|
|
5395
|
+
).run(...binds);
|
|
5396
|
+
}
|
|
5397
|
+
db.exec(
|
|
5398
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
5399
|
+
ON import_resolution(module, lang, from_file)`
|
|
5400
|
+
);
|
|
5401
|
+
const result = stmtFn(
|
|
5402
|
+
`UPDATE refs
|
|
5403
|
+
SET to_file = (
|
|
5404
|
+
SELECT ir.to_file
|
|
5405
|
+
FROM temp.import_resolution ir
|
|
5406
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
5407
|
+
WHERE ir.module = refs.module
|
|
5408
|
+
AND ir.lang = refs.lang
|
|
5409
|
+
AND ir.from_file = s.file
|
|
5410
|
+
LIMIT 1
|
|
5411
|
+
)
|
|
5412
|
+
WHERE refs.call_type = 'import'
|
|
5413
|
+
AND refs.module IS NOT NULL
|
|
5414
|
+
AND EXISTS (
|
|
5415
|
+
SELECT 1
|
|
5416
|
+
FROM temp.import_resolution ir
|
|
5417
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
5418
|
+
WHERE ir.module = refs.module
|
|
5419
|
+
AND ir.lang = refs.lang
|
|
5420
|
+
AND ir.from_file = s.file
|
|
5421
|
+
)`
|
|
5422
|
+
).run();
|
|
5423
|
+
db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
5424
|
+
return result.changes ?? 0;
|
|
5425
|
+
});
|
|
5426
|
+
}
|
|
5427
|
+
function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
|
|
5428
|
+
const list = [...names].filter((name) => name.length > 0);
|
|
5429
|
+
if (list.length === 0) return 0;
|
|
5430
|
+
let total = 0;
|
|
5431
|
+
for (let i = 0; i < list.length; i += maxSqlVars) {
|
|
5432
|
+
const chunk = list.slice(i, i + maxSqlVars);
|
|
5433
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
5434
|
+
try {
|
|
5435
|
+
const result = stmtFn(
|
|
5436
|
+
`UPDATE refs
|
|
5437
|
+
SET to_id = s.id
|
|
5438
|
+
FROM (
|
|
5439
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5440
|
+
FROM symbols sym
|
|
5441
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5442
|
+
WHERE sym.name IN (${placeholders})
|
|
5443
|
+
GROUP BY sym.name, lf.family
|
|
5444
|
+
UNION ALL
|
|
5445
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5446
|
+
FROM symbols sym
|
|
5447
|
+
WHERE sym.name IN (${placeholders})
|
|
5448
|
+
GROUP BY sym.name
|
|
5449
|
+
) AS s,
|
|
5450
|
+
lang_family AS rf
|
|
5451
|
+
WHERE refs.to_name IN (${placeholders})
|
|
5452
|
+
AND rf.lang = refs.lang
|
|
5453
|
+
AND s.name = refs.to_name
|
|
5454
|
+
AND s.family = rf.family`
|
|
5455
|
+
).run(...chunk, ...chunk, ...chunk);
|
|
5456
|
+
total += result.changes ?? 0;
|
|
5457
|
+
} catch {
|
|
5458
|
+
const result = stmtFn(
|
|
5459
|
+
`UPDATE refs SET to_id = (
|
|
5460
|
+
SELECT sym.id FROM symbols sym
|
|
5461
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5462
|
+
ORDER BY sym.id LIMIT 1
|
|
5463
|
+
) WHERE refs.to_name IN (${placeholders})
|
|
5464
|
+
AND EXISTS (
|
|
5465
|
+
SELECT 1 FROM symbols sym
|
|
5466
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5467
|
+
)`
|
|
5468
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk, LANG_FAMILY_WILDCARD);
|
|
5469
|
+
total += result.changes ?? 0;
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
return total;
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
// src/codebase-index/writer-search.ts
|
|
5476
|
+
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
5477
|
+
|
|
5478
|
+
// src/codebase-index/lsp-kind.ts
|
|
5479
|
+
function lspKindToInternalKind(k) {
|
|
5480
|
+
switch (k) {
|
|
5481
|
+
case 5 /* Class */:
|
|
5482
|
+
return "class";
|
|
5483
|
+
case 6 /* Method */:
|
|
5484
|
+
return "method";
|
|
5485
|
+
case 7 /* Property */:
|
|
5486
|
+
case 8 /* Field */:
|
|
5487
|
+
return "property";
|
|
5488
|
+
case 9 /* Constructor */:
|
|
5489
|
+
return "class";
|
|
5490
|
+
case 10 /* Enum */:
|
|
5491
|
+
return "enum";
|
|
5492
|
+
case 11 /* Interface */:
|
|
5493
|
+
return "interface";
|
|
5494
|
+
case 12 /* Function */:
|
|
5495
|
+
return "function";
|
|
5496
|
+
case 13 /* Variable */:
|
|
5497
|
+
return "var";
|
|
5498
|
+
case 14 /* Constant */:
|
|
5499
|
+
return "const";
|
|
5500
|
+
case 22 /* EnumMember */:
|
|
5501
|
+
return "enum";
|
|
5502
|
+
case 26 /* TypeParameter */:
|
|
5503
|
+
return "type";
|
|
5504
|
+
case 3 /* Namespace */:
|
|
5505
|
+
return "namespace";
|
|
5506
|
+
default:
|
|
5507
|
+
return null;
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5114
5511
|
// src/codebase-index/writer-search-helpers.ts
|
|
5115
5512
|
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
5116
5513
|
function normalizeSearchLimit(limit) {
|
|
@@ -5164,6 +5561,173 @@ function mapWriterSearchRow(row, lspKind, score = 0, snippet = "") {
|
|
|
5164
5561
|
};
|
|
5165
5562
|
}
|
|
5166
5563
|
|
|
5564
|
+
// src/codebase-index/writer-search.ts
|
|
5565
|
+
function searchWithStatement(stmtFn, query, filter, opts) {
|
|
5566
|
+
const built = buildWriterSearchWhere(query, filter);
|
|
5567
|
+
if (built === null) return [];
|
|
5568
|
+
const { where, values } = built;
|
|
5569
|
+
const limit = normalizeSearchLimit(opts?.limit);
|
|
5570
|
+
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
5571
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
5572
|
+
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
5573
|
+
const rows = stmtFn(sql).all(...binds);
|
|
5574
|
+
return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
|
|
5575
|
+
}
|
|
5576
|
+
function countSearchWithStatement(stmtFn, query, filter) {
|
|
5577
|
+
const built = buildWriterSearchWhere(query, filter);
|
|
5578
|
+
if (built === null) return 0;
|
|
5579
|
+
const row = stmtFn(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
|
|
5580
|
+
...built.values
|
|
5581
|
+
);
|
|
5582
|
+
return Number(row?.n ?? 0);
|
|
5583
|
+
}
|
|
5584
|
+
function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvailable, getOrBuildBm25, query, filter, limit) {
|
|
5585
|
+
const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
5586
|
+
const safeLimit = Math.max(1, Math.min(rawLimit, 100));
|
|
5587
|
+
const tokens = tokenise(query);
|
|
5588
|
+
if (tokens.length === 0 || !ftsAvailable) {
|
|
5589
|
+
return searchRankedFallbackWithStatement(
|
|
5590
|
+
stmtFn,
|
|
5591
|
+
searchFn,
|
|
5592
|
+
getOrBuildBm25,
|
|
5593
|
+
query,
|
|
5594
|
+
filter,
|
|
5595
|
+
safeLimit
|
|
5596
|
+
);
|
|
5597
|
+
}
|
|
5598
|
+
let effectiveKind = filter?.kind;
|
|
5599
|
+
if (filter?.lspKind !== void 0) {
|
|
5600
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
5601
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
5602
|
+
effectiveKind = mapped;
|
|
5603
|
+
}
|
|
5604
|
+
const longTokens = tokens.filter((t) => t.length >= 3);
|
|
5605
|
+
const shortTokens = tokens.filter((t) => t.length < 3);
|
|
5606
|
+
if (longTokens.length === 0) {
|
|
5607
|
+
return searchRankedFallbackWithStatement(
|
|
5608
|
+
stmtFn,
|
|
5609
|
+
searchFn,
|
|
5610
|
+
getOrBuildBm25,
|
|
5611
|
+
query,
|
|
5612
|
+
filter,
|
|
5613
|
+
safeLimit
|
|
5614
|
+
);
|
|
5615
|
+
}
|
|
5616
|
+
const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
5617
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
5618
|
+
const values = [match];
|
|
5619
|
+
for (const shortTok of shortTokens) {
|
|
5620
|
+
conditions.push("s.text LIKE ? ESCAPE '\\'");
|
|
5621
|
+
values.push(`%${escapeLike(shortTok)}%`);
|
|
5622
|
+
}
|
|
5623
|
+
if (effectiveKind) {
|
|
5624
|
+
conditions.push("s.kind = ?");
|
|
5625
|
+
values.push(effectiveKind);
|
|
5626
|
+
}
|
|
5627
|
+
if (filter?.lang) {
|
|
5628
|
+
conditions.push("s.lang = ?");
|
|
5629
|
+
values.push(filter.lang);
|
|
5630
|
+
}
|
|
5631
|
+
if (filter?.file) {
|
|
5632
|
+
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
5633
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
5634
|
+
}
|
|
5635
|
+
const where = conditions.join(" AND ");
|
|
5636
|
+
const countRows = stmtFn(
|
|
5637
|
+
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
5638
|
+
).all(...values);
|
|
5639
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
5640
|
+
if (total === 0) return { results: [], total: 0 };
|
|
5641
|
+
const bm25Rows = stmtFn(
|
|
5642
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
5643
|
+
-bm25(symbols_fts) AS score,
|
|
5644
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
5645
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
5646
|
+
WHERE ${where}
|
|
5647
|
+
ORDER BY
|
|
5648
|
+
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
5649
|
+
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
5650
|
+
ELSE 2 END,
|
|
5651
|
+
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
5652
|
+
LIMIT ?`
|
|
5653
|
+
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
5654
|
+
if (vectorsAvailable && bm25Rows.length > 0) {
|
|
5655
|
+
const queryVec = embedText(query);
|
|
5656
|
+
const candidateIds = bm25Rows.map((r) => r.id);
|
|
5657
|
+
const placeholders = candidateIds.map(() => "?").join(",");
|
|
5658
|
+
const vecRows = stmtFn(
|
|
5659
|
+
`SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
|
|
5660
|
+
).all(...candidateIds);
|
|
5661
|
+
const vecScores = vecRows.map((r) => ({
|
|
5662
|
+
id: r.symbol_id,
|
|
5663
|
+
sim: cosineSimilarity(queryVec, decodeVector(r.vector))
|
|
5664
|
+
})).sort((a, b) => b.sim - a.sim);
|
|
5665
|
+
const bm25Rank = /* @__PURE__ */ new Map();
|
|
5666
|
+
bm25Rows.forEach((r, i) => {
|
|
5667
|
+
bm25Rank.set(r.id, i);
|
|
5668
|
+
});
|
|
5669
|
+
const vecRank = /* @__PURE__ */ new Map();
|
|
5670
|
+
vecScores.forEach((r, i) => {
|
|
5671
|
+
vecRank.set(r.id, i);
|
|
5672
|
+
});
|
|
5673
|
+
const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
|
|
5674
|
+
const fusedScore = new Map(fused);
|
|
5675
|
+
const sorted = [...bm25Rows].sort(
|
|
5676
|
+
(a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
|
|
5677
|
+
);
|
|
5678
|
+
return {
|
|
5679
|
+
results: sorted.map(
|
|
5680
|
+
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
5681
|
+
),
|
|
5682
|
+
total
|
|
5683
|
+
};
|
|
5684
|
+
}
|
|
5685
|
+
return {
|
|
5686
|
+
results: bm25Rows.map(
|
|
5687
|
+
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
5688
|
+
),
|
|
5689
|
+
total
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
function searchRankedFallbackWithStatement(stmtFn, searchFn, getOrBuildBm25, query, filter, limit) {
|
|
5693
|
+
if (!query.trim()) {
|
|
5694
|
+
const total2 = countSearchWithStatement(stmtFn, query, filter);
|
|
5695
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
5696
|
+
return { results: searchFn(query, filter, { limit }), total: total2 };
|
|
5697
|
+
}
|
|
5698
|
+
const total = countSearchWithStatement(stmtFn, query, filter);
|
|
5699
|
+
if (total === 0) return { results: [], total: 0 };
|
|
5700
|
+
const candidates = searchFn(query, filter, {
|
|
5701
|
+
limit: SEARCH_CANDIDATE_SCAN_CAP
|
|
5702
|
+
});
|
|
5703
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
5704
|
+
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
5705
|
+
const bm25 = getOrBuildBm25();
|
|
5706
|
+
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
5707
|
+
const q = query.trim().toLowerCase();
|
|
5708
|
+
const rank = (id) => {
|
|
5709
|
+
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
5710
|
+
if (name === q) return 0;
|
|
5711
|
+
if (name.startsWith(q)) return 1;
|
|
5712
|
+
return 2;
|
|
5713
|
+
};
|
|
5714
|
+
scored.sort((a, b) => {
|
|
5715
|
+
const rankDiff = rank(a.id) - rank(b.id);
|
|
5716
|
+
if (rankDiff !== 0) return rankDiff;
|
|
5717
|
+
const scoreDiff = b.score - a.score;
|
|
5718
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
5719
|
+
const left = expectDefined4(candidateById.get(a.id));
|
|
5720
|
+
const right = expectDefined4(candidateById.get(b.id));
|
|
5721
|
+
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
5722
|
+
});
|
|
5723
|
+
const qTokens = tokenise(query);
|
|
5724
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
5725
|
+
const c = expectDefined4(candidateById.get(id));
|
|
5726
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
5727
|
+
});
|
|
5728
|
+
return { results, total };
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5167
5731
|
// src/codebase-index/writer-store-pool.ts
|
|
5168
5732
|
var DEFAULT_MAX_WARM_STORES = 2;
|
|
5169
5733
|
var StorePool = class {
|
|
@@ -5255,68 +5819,14 @@ var DB_FILE2 = "index.db";
|
|
|
5255
5819
|
var MAX_STATEMENT_CACHE = 128;
|
|
5256
5820
|
var IndexStore = class _IndexStore {
|
|
5257
5821
|
db;
|
|
5258
|
-
/**
|
|
5259
|
-
* True while an index run owns one outer SQLite transaction. Individual
|
|
5260
|
-
* writer methods normally protect themselves with BEGIN/COMMIT, but during
|
|
5261
|
-
* a refresh they join this transaction so readers observe either the last
|
|
5262
|
-
* completed index or the next completed index, never an in-between batch.
|
|
5263
|
-
*/
|
|
5264
5822
|
atomicIndexUpdateActive = false;
|
|
5265
5823
|
writeSavepointSequence = 0;
|
|
5266
|
-
/** Absolute path to this project's index directory. */
|
|
5267
5824
|
indexDir;
|
|
5268
|
-
/**
|
|
5269
|
-
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
5270
|
-
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
5271
|
-
*/
|
|
5272
5825
|
ftsAvailable = false;
|
|
5273
|
-
/**
|
|
5274
|
-
* Phase 3: true when the `symbol_vectors` table was created successfully.
|
|
5275
|
-
* When false, hybrid search skips the vector pass and falls back to FTS5
|
|
5276
|
-
* (or LIKE) only.
|
|
5277
|
-
*/
|
|
5278
5826
|
vectorsAvailable = false;
|
|
5279
|
-
/**
|
|
5280
|
-
* Cache of prepared statements keyed by their SQL text. `DatabaseSync`
|
|
5281
|
-
* compiles SQL on every `.prepare()` call; for the fixed-SQL methods
|
|
5282
|
-
* (upsertFile, getFileMeta, deleteFile, insertRefs, …) that runs thousands
|
|
5283
|
-
* of times during a full reindex. `StatementSync` objects are reusable
|
|
5284
|
-
* across calls on the same connection, so we compile each distinct SQL once
|
|
5285
|
-
* and reuse it. Cleared in {@link close} when the connection is torn down.
|
|
5286
|
-
*/
|
|
5287
5827
|
stmtCache = /* @__PURE__ */ new Map();
|
|
5288
|
-
/**
|
|
5289
|
-
* Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
|
|
5290
|
-
* Built lazily on the first `searchRankedFallback` call and invalidated
|
|
5291
|
-
* (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
|
|
5292
|
-
* IDF over the full corpus is also more correct than the old per-query
|
|
5293
|
-
* candidate-subset IDF.
|
|
5294
|
-
*
|
|
5295
|
-
* Cache-lifecycle invariants (single source of truth lives at the
|
|
5296
|
-
* `invalidateBm25()` helper — see its docblock for the "every mutation
|
|
5297
|
-
* MUST call this" contract):
|
|
5298
|
-
* - declaration: this field + `bm25Dirty` (here)
|
|
5299
|
-
* - invalidation: `invalidateBm25()` flips the flag and nulls the cache
|
|
5300
|
-
* - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
|
|
5301
|
-
* - teardown: `close()` resets the flag and nulls the cache
|
|
5302
|
-
*/
|
|
5303
5828
|
bm25Cache = null;
|
|
5304
|
-
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
5305
|
-
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
5306
5829
|
bm25Dirty = true;
|
|
5307
|
-
/**
|
|
5308
|
-
* Prepare-once helper: compile `sql` on first use, reuse thereafter.
|
|
5309
|
-
*
|
|
5310
|
-
* Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
|
|
5311
|
-
* the fallback search builder emits one `text LIKE ?` clause per query token
|
|
5312
|
-
* — so the SQL varies with the token count and a stream of differently-sized
|
|
5313
|
-
* queries grew the cache without limit. Sage's store already bounds its
|
|
5314
|
-
* equivalent at 128 (WS-096).
|
|
5315
|
-
*
|
|
5316
|
-
* Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
|
|
5317
|
-
* insertRefs, …) at the young end, so a burst of one-off search SQL evicts
|
|
5318
|
-
* itself rather than the reindex hot path.
|
|
5319
|
-
*/
|
|
5320
5830
|
stmt(sql) {
|
|
5321
5831
|
const cached = this.stmtCache.get(sql);
|
|
5322
5832
|
if (cached !== void 0) {
|
|
@@ -5343,7 +5853,6 @@ var IndexStore = class _IndexStore {
|
|
|
5343
5853
|
runWithRetry(fn) {
|
|
5344
5854
|
return runSqliteWithRetry(fn);
|
|
5345
5855
|
}
|
|
5346
|
-
/** Run a complete index mutation as one WAL-visible publication. */
|
|
5347
5856
|
async runAtomicIndexUpdate(job) {
|
|
5348
5857
|
if (this.atomicIndexUpdateActive) return job();
|
|
5349
5858
|
this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
|
|
@@ -5362,11 +5871,6 @@ var IndexStore = class _IndexStore {
|
|
|
5362
5871
|
this.atomicIndexUpdateActive = false;
|
|
5363
5872
|
}
|
|
5364
5873
|
}
|
|
5365
|
-
/**
|
|
5366
|
-
* Begin a method-local transaction. Inside an atomic index publication a
|
|
5367
|
-
* SAVEPOINT preserves the old per-batch rollback boundary, which is needed
|
|
5368
|
-
* when commitBatch falls back to per-file writes after one batch fails.
|
|
5369
|
-
*/
|
|
5370
5874
|
beginWriteTransaction() {
|
|
5371
5875
|
if (this.atomicIndexUpdateActive) {
|
|
5372
5876
|
const savepoint = `index_write_${++this.writeSavepointSequence}`;
|
|
@@ -5388,35 +5892,11 @@ var IndexStore = class _IndexStore {
|
|
|
5388
5892
|
this.db.exec("ROLLBACK");
|
|
5389
5893
|
}
|
|
5390
5894
|
}
|
|
5391
|
-
/**
|
|
5392
|
-
* Mirror the in-process language→family map into SQLite.
|
|
5393
|
-
*
|
|
5394
|
-
* Rewritten on every open rather than only on schema bumps: the mapping is
|
|
5395
|
-
* static lookup data, so a code-side change (a new language, a language
|
|
5396
|
-
* moving families) must take effect without forcing a full reindex.
|
|
5397
|
-
*/
|
|
5398
5895
|
seedLangFamilies() {
|
|
5399
5896
|
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
5400
5897
|
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
5401
5898
|
insert.run("", LANG_FAMILY_WILDCARD);
|
|
5402
5899
|
}
|
|
5403
|
-
/**
|
|
5404
|
-
* Add any column the current schema expects but the on-disk table lacks.
|
|
5405
|
-
*
|
|
5406
|
-
* `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
|
|
5407
|
-
* and the version check above only rebuilds on a version *mismatch*. That
|
|
5408
|
-
* leaves a real gap: several wstack processes share this database, and while
|
|
5409
|
-
* a version upgrade is rolling out one of them may still be running the
|
|
5410
|
-
* previous build. That older process sees the newer version number, drops the
|
|
5411
|
-
* tables, and recreates them from *its* DDL — without the newer columns —
|
|
5412
|
-
* while the metadata row still reads the new version. Every later query for
|
|
5413
|
-
* one of those columns then fails with `no such column`, and no amount of
|
|
5414
|
-
* reindexing fixes it, because the version numbers already agree.
|
|
5415
|
-
*
|
|
5416
|
-
* Repairing column-by-column makes the schema self-healing from any of those
|
|
5417
|
-
* states. Table and column names are compile-time literals from this module,
|
|
5418
|
-
* never user input.
|
|
5419
|
-
*/
|
|
5420
5900
|
repairMissingColumns() {
|
|
5421
5901
|
const expected = [
|
|
5422
5902
|
{
|
|
@@ -5521,27 +6001,8 @@ var IndexStore = class _IndexStore {
|
|
|
5521
6001
|
}
|
|
5522
6002
|
this.ensureNextSymbolIdSeeded();
|
|
5523
6003
|
}
|
|
5524
|
-
// ─── ID allocation & bulk helpers ────────────────────────────────────────────
|
|
5525
6004
|
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
5526
|
-
/** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
|
|
5527
6005
|
static MAX_SQL_VARS = 900;
|
|
5528
|
-
/**
|
|
5529
|
-
* Correlated predicate: the ref in `refs` and the candidate symbol aliased
|
|
5530
|
-
* `sym` belong to the same language family — or the ref carries no language,
|
|
5531
|
-
* in which case the wildcard bind matches everything.
|
|
5532
|
-
*
|
|
5533
|
-
* Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
|
|
5534
|
-
*/
|
|
5535
|
-
static FAMILY_MATCH_SQL = `(
|
|
5536
|
-
(SELECT family FROM lang_family WHERE lang = refs.lang) = ?
|
|
5537
|
-
OR (SELECT family FROM lang_family WHERE lang = sym.lang)
|
|
5538
|
-
= (SELECT family FROM lang_family WHERE lang = refs.lang)
|
|
5539
|
-
)`;
|
|
5540
|
-
/**
|
|
5541
|
-
* Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
|
|
5542
|
-
* transaction on open; the first concurrent writer under BEGIN IMMEDIATE
|
|
5543
|
-
* re-reads and advances the counter atomically.
|
|
5544
|
-
*/
|
|
5545
6006
|
ensureNextSymbolIdSeeded() {
|
|
5546
6007
|
const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
|
|
5547
6008
|
_IndexStore.NEXT_SYMBOL_ID_KEY
|
|
@@ -5554,10 +6015,6 @@ var IndexStore = class _IndexStore {
|
|
|
5554
6015
|
String(next)
|
|
5555
6016
|
);
|
|
5556
6017
|
}
|
|
5557
|
-
/**
|
|
5558
|
-
* Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
|
|
5559
|
-
* so concurrent indexers cannot hand out overlapping ranges.
|
|
5560
|
-
*/
|
|
5561
6018
|
allocateSymbolIds(count) {
|
|
5562
6019
|
if (count <= 0) return this.getMaxSymbolId() + 1;
|
|
5563
6020
|
this.ensureNextSymbolIdSeeded();
|
|
@@ -5571,16 +6028,8 @@ var IndexStore = class _IndexStore {
|
|
|
5571
6028
|
);
|
|
5572
6029
|
return start;
|
|
5573
6030
|
}
|
|
5574
|
-
/**
|
|
5575
|
-
* Disconnect inbound refs before their target symbols are replaced and
|
|
5576
|
-
* return the affected names for scoped re-resolution.
|
|
5577
|
-
*
|
|
5578
|
-
* This also repairs a long-standing dangling-id edge case: `refs.to_id` has
|
|
5579
|
-
* no physical FK, so deleting a symbol previously left callers pointing at a
|
|
5580
|
-
* non-existent row.
|
|
5581
|
-
*/
|
|
5582
6031
|
invalidateIncomingRefsForFiles(files) {
|
|
5583
|
-
if (files.length === 0) return
|
|
6032
|
+
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
5584
6033
|
const placeholders = files.map(() => "?").join(",");
|
|
5585
6034
|
const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
|
|
5586
6035
|
...files
|
|
@@ -5589,36 +6038,11 @@ var IndexStore = class _IndexStore {
|
|
|
5589
6038
|
`UPDATE refs SET to_id = NULL
|
|
5590
6039
|
WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5591
6040
|
).run(...files);
|
|
5592
|
-
return names;
|
|
6041
|
+
return new Set(names);
|
|
5593
6042
|
}
|
|
5594
|
-
/** Resolve only refs whose target names may have changed. */
|
|
5595
6043
|
resolveRefsForNamesUnsafe(names) {
|
|
5596
|
-
|
|
5597
|
-
let changes = 0;
|
|
5598
|
-
for (let start = 0; start < unique.length; start += _IndexStore.MAX_SQL_VARS) {
|
|
5599
|
-
const chunk = unique.slice(start, start + _IndexStore.MAX_SQL_VARS);
|
|
5600
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
5601
|
-
const result = this.stmt(
|
|
5602
|
-
`UPDATE refs
|
|
5603
|
-
SET to_id = (
|
|
5604
|
-
SELECT MIN(sym.id) FROM symbols sym
|
|
5605
|
-
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5606
|
-
)
|
|
5607
|
-
WHERE to_name IN (${placeholders})`
|
|
5608
|
-
).run(LANG_FAMILY_WILDCARD, ...chunk);
|
|
5609
|
-
changes += result.changes ?? 0;
|
|
5610
|
-
}
|
|
5611
|
-
return changes;
|
|
6044
|
+
return resolveRefsForNamesUnsafe((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, names);
|
|
5612
6045
|
}
|
|
5613
|
-
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
5614
|
-
/**
|
|
5615
|
-
* Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
|
|
5616
|
-
* `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
|
|
5617
|
-
* (O(1)); multi-row INSERT amortizes bind overhead for large files.
|
|
5618
|
-
*
|
|
5619
|
-
* @returns The symbols array with `id` fields populated so the caller can
|
|
5620
|
-
* use them for refs without re-reading from the DB.
|
|
5621
|
-
*/
|
|
5622
6046
|
insertSymbols(symbols) {
|
|
5623
6047
|
this.invalidateBm25();
|
|
5624
6048
|
return this.runWithRetry(() => {
|
|
@@ -5647,12 +6071,6 @@ var IndexStore = class _IndexStore {
|
|
|
5647
6071
|
if (this.ftsAvailable) {
|
|
5648
6072
|
ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
|
|
5649
6073
|
}
|
|
5650
|
-
vectorRows.push({
|
|
5651
|
-
id,
|
|
5652
|
-
vector: encodeVector(
|
|
5653
|
-
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
5654
|
-
)
|
|
5655
|
-
});
|
|
5656
6074
|
result.push({ ...s, id });
|
|
5657
6075
|
}
|
|
5658
6076
|
bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
|
|
@@ -5702,11 +6120,6 @@ var IndexStore = class _IndexStore {
|
|
|
5702
6120
|
}
|
|
5703
6121
|
});
|
|
5704
6122
|
}
|
|
5705
|
-
/**
|
|
5706
|
-
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
5707
|
-
* when a source file disappears between index runs — previously this only
|
|
5708
|
-
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
5709
|
-
*/
|
|
5710
6123
|
deleteFile(file) {
|
|
5711
6124
|
this.invalidateBm25();
|
|
5712
6125
|
this.runWithRetry(() => {
|
|
@@ -5736,7 +6149,6 @@ var IndexStore = class _IndexStore {
|
|
|
5736
6149
|
}
|
|
5737
6150
|
});
|
|
5738
6151
|
}
|
|
5739
|
-
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
5740
6152
|
upsertFile(meta) {
|
|
5741
6153
|
this.runWithRetry(() => {
|
|
5742
6154
|
this.stmt(
|
|
@@ -5764,8 +6176,6 @@ var IndexStore = class _IndexStore {
|
|
|
5764
6176
|
getAllFileMetas() {
|
|
5765
6177
|
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
5766
6178
|
}
|
|
5767
|
-
// ─── Project structure & module resolution ──────────────────────────────────
|
|
5768
|
-
/** Store the Code Atlas grouping label for each indexed file. */
|
|
5769
6179
|
setFilePackages(entries) {
|
|
5770
6180
|
if (entries.size === 0) return;
|
|
5771
6181
|
this.runWithRetry(() => {
|
|
@@ -5773,272 +6183,50 @@ var IndexStore = class _IndexStore {
|
|
|
5773
6183
|
for (const [file, label] of entries) update.run(label, file);
|
|
5774
6184
|
});
|
|
5775
6185
|
}
|
|
5776
|
-
/**
|
|
5777
|
-
* Every indexed `namespace`/`module` declaration, for ecosystems whose import
|
|
5778
|
-
* specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
|
|
5779
|
-
* Ordered so the resolver's choice among duplicate declarations is stable.
|
|
5780
|
-
*/
|
|
5781
6186
|
getNamespaceDeclarations() {
|
|
5782
|
-
return this.stmt(
|
|
5783
|
-
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
5784
|
-
).all();
|
|
6187
|
+
return getNamespaceDeclarationsWithStatement((sql) => this.stmt(sql));
|
|
5785
6188
|
}
|
|
5786
|
-
/** `file → package` for every indexed file that has a label. */
|
|
5787
6189
|
getFilePackages() {
|
|
5788
|
-
|
|
5789
|
-
return new Map(rows.map((row) => [row.file, row.package]));
|
|
6190
|
+
return getFilePackagesWithStatement((sql) => this.stmt(sql));
|
|
5790
6191
|
}
|
|
5791
|
-
/**
|
|
5792
|
-
* Distinct `(fromFile, lang, module)` triples needing module resolution.
|
|
5793
|
-
*
|
|
5794
|
-
* Distinct rather than per-ref because resolution depends only on these three
|
|
5795
|
-
* values: a file importing the same module twenty times resolves it once.
|
|
5796
|
-
*/
|
|
5797
6192
|
getUnresolvedImports(onlyFiles) {
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
return this.stmt(base).all();
|
|
5804
|
-
}
|
|
5805
|
-
const out = [];
|
|
5806
|
-
for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
|
|
5807
|
-
const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
|
|
5808
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
5809
|
-
out.push(
|
|
5810
|
-
...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
5811
|
-
);
|
|
5812
|
-
}
|
|
5813
|
-
return out;
|
|
6193
|
+
return getUnresolvedImportsWithStatement(
|
|
6194
|
+
(sql) => this.stmt(sql),
|
|
6195
|
+
_IndexStore.MAX_SQL_VARS,
|
|
6196
|
+
onlyFiles
|
|
6197
|
+
);
|
|
5814
6198
|
}
|
|
5815
|
-
/**
|
|
5816
|
-
* Write resolved import targets back onto `refs.to_file`.
|
|
5817
|
-
*
|
|
5818
|
-
* Applied through a temp table and a single UPDATE: one statement per
|
|
5819
|
-
* resolution would mean thousands of round-trips on a first index.
|
|
5820
|
-
*/
|
|
5821
6199
|
applyImportResolutions(resolutions) {
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
this.
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
lang TEXT NOT NULL,
|
|
5829
|
-
module TEXT NOT NULL,
|
|
5830
|
-
to_file TEXT NOT NULL
|
|
5831
|
-
)`
|
|
5832
|
-
);
|
|
5833
|
-
const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
|
|
5834
|
-
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
5835
|
-
const chunk = resolutions.slice(i, i + chunkSize);
|
|
5836
|
-
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
5837
|
-
const binds = [];
|
|
5838
|
-
for (const entry of chunk) {
|
|
5839
|
-
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
5840
|
-
}
|
|
5841
|
-
this.stmt(
|
|
5842
|
-
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
5843
|
-
VALUES ${placeholders}`
|
|
5844
|
-
).run(...binds);
|
|
5845
|
-
}
|
|
5846
|
-
this.db.exec(
|
|
5847
|
-
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
5848
|
-
ON import_resolution(module, lang, from_file)`
|
|
5849
|
-
);
|
|
5850
|
-
const result = this.stmt(
|
|
5851
|
-
`UPDATE refs
|
|
5852
|
-
SET to_file = (
|
|
5853
|
-
SELECT ir.to_file
|
|
5854
|
-
FROM temp.import_resolution ir
|
|
5855
|
-
JOIN symbols s ON s.id = refs.from_id
|
|
5856
|
-
WHERE ir.module = refs.module
|
|
5857
|
-
AND ir.lang = refs.lang
|
|
5858
|
-
AND ir.from_file = s.file
|
|
5859
|
-
LIMIT 1
|
|
5860
|
-
)
|
|
5861
|
-
WHERE refs.call_type = 'import'
|
|
5862
|
-
AND refs.module IS NOT NULL
|
|
5863
|
-
AND EXISTS (
|
|
5864
|
-
SELECT 1
|
|
5865
|
-
FROM temp.import_resolution ir
|
|
5866
|
-
JOIN symbols s ON s.id = refs.from_id
|
|
5867
|
-
WHERE ir.module = refs.module
|
|
5868
|
-
AND ir.lang = refs.lang
|
|
5869
|
-
AND ir.from_file = s.file
|
|
5870
|
-
)`
|
|
5871
|
-
).run();
|
|
5872
|
-
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
5873
|
-
return result.changes ?? 0;
|
|
5874
|
-
});
|
|
5875
|
-
}
|
|
5876
|
-
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
5877
|
-
search(query, filter, opts) {
|
|
5878
|
-
const built = this.buildSearchWhere(query, filter);
|
|
5879
|
-
if (built === null) return [];
|
|
5880
|
-
const { where, values } = built;
|
|
5881
|
-
const limit = normalizeSearchLimit(opts?.limit);
|
|
5882
|
-
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
5883
|
-
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
5884
|
-
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
5885
|
-
const rows = this.stmt(sql).all(
|
|
5886
|
-
...binds
|
|
6200
|
+
return applyImportResolutionsWithStatement(
|
|
6201
|
+
this.db,
|
|
6202
|
+
(sql) => this.stmt(sql),
|
|
6203
|
+
this.runWithRetry.bind(this),
|
|
6204
|
+
_IndexStore.MAX_SQL_VARS,
|
|
6205
|
+
resolutions
|
|
5887
6206
|
);
|
|
5888
|
-
return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
|
|
5889
6207
|
}
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
return buildWriterSearchWhere(query, filter);
|
|
6208
|
+
search(query, filter, opts) {
|
|
6209
|
+
return searchWithStatement((sql) => this.stmt(sql), query, filter, opts);
|
|
5893
6210
|
}
|
|
5894
6211
|
countSearch(query, filter) {
|
|
5895
|
-
|
|
5896
|
-
if (built === null) return 0;
|
|
5897
|
-
const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
|
|
5898
|
-
...built.values
|
|
5899
|
-
);
|
|
5900
|
-
return Number(row?.n ?? 0);
|
|
6212
|
+
return countSearchWithStatement((sql) => this.stmt(sql), query, filter);
|
|
5901
6213
|
}
|
|
5902
|
-
/**
|
|
5903
|
-
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
5904
|
-
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
5905
|
-
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
5906
|
-
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
5907
|
-
*
|
|
5908
|
-
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
5909
|
-
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
5910
|
-
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
5911
|
-
*/
|
|
5912
6214
|
searchRanked(query, filter, limit) {
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
effectiveKind = mapped;
|
|
5924
|
-
}
|
|
5925
|
-
const longTokens = tokens.filter((t) => t.length >= 3);
|
|
5926
|
-
const shortTokens = tokens.filter((t) => t.length < 3);
|
|
5927
|
-
if (longTokens.length === 0) {
|
|
5928
|
-
return this.searchRankedFallback(query, filter, safeLimit);
|
|
5929
|
-
}
|
|
5930
|
-
const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
5931
|
-
const conditions = ["symbols_fts MATCH ?"];
|
|
5932
|
-
const values = [match];
|
|
5933
|
-
for (const shortTok of shortTokens) {
|
|
5934
|
-
conditions.push("s.text LIKE ? ESCAPE '\\'");
|
|
5935
|
-
values.push(`%${escapeLike(shortTok)}%`);
|
|
5936
|
-
}
|
|
5937
|
-
if (effectiveKind) {
|
|
5938
|
-
conditions.push("s.kind = ?");
|
|
5939
|
-
values.push(effectiveKind);
|
|
5940
|
-
}
|
|
5941
|
-
if (filter?.lang) {
|
|
5942
|
-
conditions.push("s.lang = ?");
|
|
5943
|
-
values.push(filter.lang);
|
|
5944
|
-
}
|
|
5945
|
-
if (filter?.file) {
|
|
5946
|
-
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
5947
|
-
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
5948
|
-
}
|
|
5949
|
-
const where = conditions.join(" AND ");
|
|
5950
|
-
const countRows = this.stmt(
|
|
5951
|
-
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
5952
|
-
).all(...values);
|
|
5953
|
-
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
5954
|
-
if (total === 0) return { results: [], total: 0 };
|
|
5955
|
-
const bm25Rows = this.stmt(
|
|
5956
|
-
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
5957
|
-
-bm25(symbols_fts) AS score,
|
|
5958
|
-
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
5959
|
-
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
5960
|
-
WHERE ${where}
|
|
5961
|
-
ORDER BY
|
|
5962
|
-
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
5963
|
-
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
5964
|
-
ELSE 2 END,
|
|
5965
|
-
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
5966
|
-
LIMIT ?`
|
|
5967
|
-
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
5968
|
-
if (this.vectorsAvailable && bm25Rows.length > 0) {
|
|
5969
|
-
const queryVec = embedText(query);
|
|
5970
|
-
const candidateIds = bm25Rows.map((r) => r.id);
|
|
5971
|
-
const placeholders = candidateIds.map(() => "?").join(",");
|
|
5972
|
-
const vecRows = this.stmt(
|
|
5973
|
-
`SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
|
|
5974
|
-
).all(...candidateIds);
|
|
5975
|
-
const vecScores = vecRows.map((r) => ({
|
|
5976
|
-
id: r.symbol_id,
|
|
5977
|
-
sim: cosineSimilarity(queryVec, decodeVector(r.vector))
|
|
5978
|
-
})).sort((a, b) => b.sim - a.sim);
|
|
5979
|
-
const bm25Rank = /* @__PURE__ */ new Map();
|
|
5980
|
-
bm25Rows.forEach((r, i) => {
|
|
5981
|
-
bm25Rank.set(r.id, i);
|
|
5982
|
-
});
|
|
5983
|
-
const vecRank = /* @__PURE__ */ new Map();
|
|
5984
|
-
vecScores.forEach((r, i) => {
|
|
5985
|
-
vecRank.set(r.id, i);
|
|
5986
|
-
});
|
|
5987
|
-
const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
|
|
5988
|
-
const fusedScore = new Map(fused);
|
|
5989
|
-
const sorted = [...bm25Rows].sort(
|
|
5990
|
-
(a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
|
|
5991
|
-
);
|
|
5992
|
-
return {
|
|
5993
|
-
results: sorted.map(
|
|
5994
|
-
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
5995
|
-
),
|
|
5996
|
-
total
|
|
5997
|
-
};
|
|
5998
|
-
}
|
|
5999
|
-
return {
|
|
6000
|
-
results: bm25Rows.map(
|
|
6001
|
-
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
6002
|
-
),
|
|
6003
|
-
total
|
|
6004
|
-
};
|
|
6215
|
+
return searchRankedWithStatement(
|
|
6216
|
+
(sql) => this.stmt(sql),
|
|
6217
|
+
this.search.bind(this),
|
|
6218
|
+
this.ftsAvailable,
|
|
6219
|
+
this.vectorsAvailable,
|
|
6220
|
+
this.getOrBuildBm25.bind(this),
|
|
6221
|
+
query,
|
|
6222
|
+
filter,
|
|
6223
|
+
limit
|
|
6224
|
+
);
|
|
6005
6225
|
}
|
|
6006
|
-
/**
|
|
6007
|
-
* Invalidate the cached BM25 index.
|
|
6008
|
-
*
|
|
6009
|
-
* **Contract: every method that mutates `symbols` MUST call this before
|
|
6010
|
-
* returning.** (`refs` mutations do not affect the BM25 fallback because
|
|
6011
|
-
* the corpus is built from `symbols.text` via `getAllIndexable()` and the
|
|
6012
|
-
* BM25 score is filtered by the LIKE-selected candidate set in
|
|
6013
|
-
* `searchRankedFallback`.) Today the call sites are `repairDrift`,
|
|
6014
|
-
* `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
|
|
6015
|
-
* `commitBatch`. A future mutation that adds a new write path (e.g.
|
|
6016
|
-
* `renameFile`, `updateSignature`) MUST also call this — otherwise the
|
|
6017
|
-
* FTS5-unavailable fallback will serve stale search results. The
|
|
6018
|
-
* `close()` reset at L1820-1821 tears the cache down on store shutdown,
|
|
6019
|
-
* which is the only legitimate place that flips the flag outside this
|
|
6020
|
-
* helper.
|
|
6021
|
-
*
|
|
6022
|
-
* Called *before* `runWithRetry` on purpose: if the write fails all
|
|
6023
|
-
* retries the flag stays set, forcing a rebuild on the next search rather
|
|
6024
|
-
* than trusting a cache that may not reflect the intended mutation.
|
|
6025
|
-
* Do not move this inside the retry closure.
|
|
6026
|
-
*/
|
|
6027
6226
|
invalidateBm25() {
|
|
6028
6227
|
this.bm25Dirty = true;
|
|
6029
6228
|
this.bm25Cache = null;
|
|
6030
6229
|
}
|
|
6031
|
-
/**
|
|
6032
|
-
* Return the cached full-corpus BM25 index, rebuilding it only when the
|
|
6033
|
-
* symbols table has been mutated since the last build. The full-corpus IDF
|
|
6034
|
-
* is more correct than the old per-query candidate-subset IDF, and the
|
|
6035
|
-
* amortized build cost drops from O(symbols × tokens) per search to once
|
|
6036
|
-
* per write batch.
|
|
6037
|
-
*
|
|
6038
|
-
* Note: the first call after a long idle (or on a freshly opened store)
|
|
6039
|
-
* pays the full corpus rebuild synchronously on the search path. For a
|
|
6040
|
-
* 5 500+ symbol corpus this is a visible one-time latency spike.
|
|
6041
|
-
*/
|
|
6042
6230
|
getOrBuildBm25() {
|
|
6043
6231
|
if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
|
|
6044
6232
|
const docs = this.getAllIndexable();
|
|
@@ -6046,57 +6234,12 @@ var IndexStore = class _IndexStore {
|
|
|
6046
6234
|
this.bm25Dirty = false;
|
|
6047
6235
|
return this.bm25Cache;
|
|
6048
6236
|
}
|
|
6049
|
-
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
6050
|
-
searchRankedFallback(query, filter, limit) {
|
|
6051
|
-
if (!query.trim()) {
|
|
6052
|
-
const total2 = this.countSearch(query, filter);
|
|
6053
|
-
if (total2 === 0) return { results: [], total: 0 };
|
|
6054
|
-
return { results: this.search(query, filter, { limit }), total: total2 };
|
|
6055
|
-
}
|
|
6056
|
-
const total = this.countSearch(query, filter);
|
|
6057
|
-
if (total === 0) return { results: [], total: 0 };
|
|
6058
|
-
const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
|
|
6059
|
-
if (candidates.length === 0) return { results: [], total: 0 };
|
|
6060
|
-
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
6061
|
-
const bm25 = this.getOrBuildBm25();
|
|
6062
|
-
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
6063
|
-
const q = query.trim().toLowerCase();
|
|
6064
|
-
const rank = (id) => {
|
|
6065
|
-
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
6066
|
-
if (name === q) return 0;
|
|
6067
|
-
if (name.startsWith(q)) return 1;
|
|
6068
|
-
return 2;
|
|
6069
|
-
};
|
|
6070
|
-
scored.sort((a, b) => {
|
|
6071
|
-
const rankDiff = rank(a.id) - rank(b.id);
|
|
6072
|
-
if (rankDiff !== 0) return rankDiff;
|
|
6073
|
-
const scoreDiff = b.score - a.score;
|
|
6074
|
-
if (scoreDiff !== 0) return scoreDiff;
|
|
6075
|
-
const left = expectDefined4(candidateById.get(a.id));
|
|
6076
|
-
const right = expectDefined4(candidateById.get(b.id));
|
|
6077
|
-
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
6078
|
-
});
|
|
6079
|
-
const qTokens = tokenise(query);
|
|
6080
|
-
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
6081
|
-
const c = expectDefined4(candidateById.get(id));
|
|
6082
|
-
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
6083
|
-
});
|
|
6084
|
-
return { results, total };
|
|
6085
|
-
}
|
|
6086
6237
|
getAllIndexable() {
|
|
6087
6238
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
6088
6239
|
}
|
|
6089
|
-
/**
|
|
6090
|
-
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
6091
|
-
* allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
|
|
6092
|
-
* changed file's rows, so the row count drops below the max id and a
|
|
6093
|
-
* count-based id would collide with a surviving row (UNIQUE constraint on
|
|
6094
|
-
* `symbols.id`). Ids may have gaps — that is fine.
|
|
6095
|
-
*/
|
|
6096
6240
|
getMaxSymbolId() {
|
|
6097
6241
|
return getMaxSymbolIdWithStatement((sql) => this.stmt(sql));
|
|
6098
6242
|
}
|
|
6099
|
-
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
6100
6243
|
getStats() {
|
|
6101
6244
|
return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
|
|
6102
6245
|
}
|
|
@@ -6139,11 +6282,6 @@ var IndexStore = class _IndexStore {
|
|
|
6139
6282
|
}
|
|
6140
6283
|
});
|
|
6141
6284
|
}
|
|
6142
|
-
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
6143
|
-
/**
|
|
6144
|
-
* Insert cross-references for a given source symbol id.
|
|
6145
|
-
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
6146
|
-
*/
|
|
6147
6285
|
insertRefs(fromId, refs) {
|
|
6148
6286
|
this.runWithRetry(() => {
|
|
6149
6287
|
this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
@@ -6155,167 +6293,36 @@ var IndexStore = class _IndexStore {
|
|
|
6155
6293
|
);
|
|
6156
6294
|
});
|
|
6157
6295
|
}
|
|
6158
|
-
/**
|
|
6159
|
-
* Bulk-insert refs for many source symbols in a single transaction.
|
|
6160
|
-
*
|
|
6161
|
-
* Unlike {@link insertRefs} this does NOT delete per source id — the caller
|
|
6162
|
-
* (the indexer) has already cleared stale refs for the file via
|
|
6163
|
-
* {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
|
|
6164
|
-
* repeated once per symbol. One transaction for the whole file instead of one
|
|
6165
|
-
* per symbol turns an O(symbols) transaction count into O(1).
|
|
6166
|
-
*
|
|
6167
|
-
* Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
|
|
6168
|
-
*/
|
|
6169
6296
|
insertRefsBatch(refs) {
|
|
6170
6297
|
if (refs.length === 0) return;
|
|
6171
6298
|
this.runWithRetry(() => {
|
|
6172
6299
|
bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refs);
|
|
6173
6300
|
});
|
|
6174
6301
|
}
|
|
6175
|
-
/**
|
|
6176
|
-
* Commit a batch of file-level symbol/refs/upserts in a single transaction.
|
|
6177
|
-
*
|
|
6178
|
-
* Used by the indexer to amortize SQLite commit overhead across many files.
|
|
6179
|
-
* Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
|
|
6180
|
-
* for symbols, plus per-file deletes and an upsertFile call), so a 20-file
|
|
6181
|
-
* parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
|
|
6182
|
-
* this entry point we do exactly one BEGIN/COMMIT per parallel batch.
|
|
6183
|
-
*
|
|
6184
|
-
* Each entry must already be a fully-parsed FileSymbols (symbols + refs).
|
|
6185
|
-
* The caller is responsible for the per-file prefix accounting
|
|
6186
|
-
* (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
|
|
6187
|
-
* the caller clear stale symbols/refs for any files being re-indexed before
|
|
6188
|
-
* the inserts run (required to keep refs → symbols FK invariants).
|
|
6189
|
-
*
|
|
6190
|
-
* Returns the symbols back with their assigned `id` (same shape as
|
|
6191
|
-
* {@link insertSymbols}) so callers can build final per-file results.
|
|
6192
|
-
*/
|
|
6193
6302
|
commitBatch(entries, options = {}) {
|
|
6194
|
-
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
6195
|
-
return [];
|
|
6196
|
-
}
|
|
6197
6303
|
this.invalidateBm25();
|
|
6198
6304
|
return this.runWithRetry(() => {
|
|
6199
6305
|
const ownsTransaction = this.beginWriteTransaction();
|
|
6200
6306
|
try {
|
|
6201
|
-
const
|
|
6202
|
-
for (const entry of entries) {
|
|
6203
|
-
for (const symbol of entry.symbols) affectedNames.add(symbol.name);
|
|
6204
|
-
for (const ref of entry.refs) affectedNames.add(ref.toName);
|
|
6205
|
-
}
|
|
6206
|
-
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
6207
|
-
const placeholders = options.deleteForFiles.map(() => "?").join(",");
|
|
6208
|
-
for (const name of this.invalidateIncomingRefsForFiles(options.deleteForFiles)) {
|
|
6209
|
-
affectedNames.add(name);
|
|
6210
|
-
}
|
|
6211
|
-
if (this.ftsAvailable) {
|
|
6212
|
-
this.stmt(
|
|
6213
|
-
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
6214
|
-
).run(...options.deleteForFiles);
|
|
6215
|
-
}
|
|
6216
|
-
if (this.vectorsAvailable) {
|
|
6217
|
-
this.stmt(
|
|
6218
|
-
`DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
6219
|
-
).run(...options.deleteForFiles);
|
|
6220
|
-
}
|
|
6221
|
-
this.stmt(
|
|
6222
|
-
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
6223
|
-
).run(...options.deleteForFiles);
|
|
6224
|
-
this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(
|
|
6225
|
-
...options.deleteForFiles
|
|
6226
|
-
);
|
|
6227
|
-
}
|
|
6228
|
-
const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
6229
|
-
let nextId = this.allocateSymbolIds(totalSymbols);
|
|
6230
|
-
const allInserted = [];
|
|
6231
|
-
const refsToInsert = [];
|
|
6232
|
-
const bulkSyms = [];
|
|
6233
|
-
const ftsRows = [];
|
|
6234
|
-
const vectorRows = [];
|
|
6235
|
-
for (const entry of entries) {
|
|
6236
|
-
const insertedForEntry = [];
|
|
6237
|
-
for (const s of entry.symbols) {
|
|
6238
|
-
const id = nextId++;
|
|
6239
|
-
bulkSyms.push({
|
|
6240
|
-
id,
|
|
6241
|
-
lang: s.lang,
|
|
6242
|
-
kind: s.kind,
|
|
6243
|
-
name: s.name,
|
|
6244
|
-
file: s.file,
|
|
6245
|
-
line: s.line,
|
|
6246
|
-
col: s.col,
|
|
6247
|
-
signature: s.signature,
|
|
6248
|
-
docComment: s.docComment,
|
|
6249
|
-
scope: s.scope,
|
|
6250
|
-
text: s.text
|
|
6251
|
-
});
|
|
6252
|
-
if (this.ftsAvailable) {
|
|
6253
|
-
ftsRows.push({
|
|
6254
|
-
id,
|
|
6255
|
-
text: buildIndexableText(s.name, s.signature, s.docComment)
|
|
6256
|
-
});
|
|
6257
|
-
}
|
|
6258
|
-
vectorRows.push({
|
|
6259
|
-
id,
|
|
6260
|
-
vector: encodeVector(
|
|
6261
|
-
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
6262
|
-
)
|
|
6263
|
-
});
|
|
6264
|
-
const inserted = { ...s, id };
|
|
6265
|
-
allInserted.push(inserted);
|
|
6266
|
-
insertedForEntry.push(inserted);
|
|
6267
|
-
}
|
|
6268
|
-
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
6269
|
-
}
|
|
6270
|
-
bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulkSyms);
|
|
6271
|
-
bulkInsertFtsWithStatement(
|
|
6307
|
+
const result = commitBatchWithStatement(
|
|
6272
6308
|
(sql) => this.stmt(sql),
|
|
6273
6309
|
_IndexStore.MAX_SQL_VARS,
|
|
6274
6310
|
this.ftsAvailable,
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
vectorRows
|
|
6282
|
-
);
|
|
6283
|
-
}
|
|
6284
|
-
bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refsToInsert);
|
|
6285
|
-
const upsertStmt = this.stmt(
|
|
6286
|
-
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
6287
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
6288
|
-
ON CONFLICT(file) DO UPDATE SET
|
|
6289
|
-
lang = excluded.lang,
|
|
6290
|
-
mtime_ms = excluded.mtime_ms,
|
|
6291
|
-
content_hash = excluded.content_hash,
|
|
6292
|
-
symbol_count = excluded.symbol_count,
|
|
6293
|
-
last_indexed = excluded.last_indexed`
|
|
6311
|
+
this.vectorsAvailable,
|
|
6312
|
+
this.allocateSymbolIds.bind(this),
|
|
6313
|
+
this.invalidateIncomingRefsForFiles.bind(this),
|
|
6314
|
+
this.resolveRefsForNamesUnsafe.bind(this),
|
|
6315
|
+
entries,
|
|
6316
|
+
options
|
|
6294
6317
|
);
|
|
6295
|
-
const now = Date.now();
|
|
6296
|
-
for (const entry of entries) {
|
|
6297
|
-
upsertStmt.run(
|
|
6298
|
-
entry.file,
|
|
6299
|
-
entry.lang,
|
|
6300
|
-
entry.mtimeMs,
|
|
6301
|
-
entry.contentHash ?? "",
|
|
6302
|
-
entry.symbolCount,
|
|
6303
|
-
now
|
|
6304
|
-
);
|
|
6305
|
-
}
|
|
6306
|
-
this.resolveRefsForNamesUnsafe(affectedNames);
|
|
6307
6318
|
this.commitWriteTransaction(ownsTransaction);
|
|
6308
|
-
return
|
|
6319
|
+
return result;
|
|
6309
6320
|
} catch (err) {
|
|
6310
6321
|
this.rollbackWriteTransaction(ownsTransaction);
|
|
6311
6322
|
throw err;
|
|
6312
6323
|
}
|
|
6313
6324
|
});
|
|
6314
6325
|
}
|
|
6315
|
-
/**
|
|
6316
|
-
* Delete all refs whose source symbols are in a given file.
|
|
6317
|
-
* Used when re-indexing a file to clear stale refs.
|
|
6318
|
-
*/
|
|
6319
6326
|
deleteRefsForFile(file) {
|
|
6320
6327
|
this.runWithRetry(() => {
|
|
6321
6328
|
this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
|
|
@@ -6323,64 +6330,12 @@ var IndexStore = class _IndexStore {
|
|
|
6323
6330
|
);
|
|
6324
6331
|
});
|
|
6325
6332
|
}
|
|
6326
|
-
/**
|
|
6327
|
-
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
6328
|
-
* Call this after all symbols have been inserted to fill in cross-references.
|
|
6329
|
-
*
|
|
6330
|
-
* A match additionally requires the referencing ref and the target symbol to
|
|
6331
|
-
* be in the same {@link LangFamily}. Without that guard a name match is a
|
|
6332
|
-
* cross-language accident waiting to happen — `main`, `New`, `Parse` and
|
|
6333
|
-
* `Config` are declared in most languages at once, and each collision draws a
|
|
6334
|
-
* Code Atlas edge between files that never reference each other. Refs stored
|
|
6335
|
-
* without a language keep the old global behaviour via the `'*'` wildcard row.
|
|
6336
|
-
*/
|
|
6337
6333
|
resolveRefs() {
|
|
6338
|
-
return this.runWithRetry(() =>
|
|
6339
|
-
try {
|
|
6340
|
-
const result = this.stmt(
|
|
6341
|
-
`UPDATE refs
|
|
6342
|
-
SET to_id = s.id
|
|
6343
|
-
FROM (
|
|
6344
|
-
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
6345
|
-
FROM symbols sym
|
|
6346
|
-
JOIN lang_family lf ON lf.lang = sym.lang
|
|
6347
|
-
GROUP BY sym.name, lf.family
|
|
6348
|
-
UNION ALL
|
|
6349
|
-
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
6350
|
-
FROM symbols sym
|
|
6351
|
-
GROUP BY sym.name
|
|
6352
|
-
) AS s,
|
|
6353
|
-
lang_family AS rf
|
|
6354
|
-
WHERE refs.to_id IS NULL
|
|
6355
|
-
AND refs.to_name IS NOT NULL
|
|
6356
|
-
AND rf.lang = refs.lang
|
|
6357
|
-
AND s.name = refs.to_name
|
|
6358
|
-
AND s.family = rf.family`
|
|
6359
|
-
).run();
|
|
6360
|
-
return result.changes ?? 0;
|
|
6361
|
-
} catch {
|
|
6362
|
-
const result = this.stmt(
|
|
6363
|
-
`UPDATE refs SET to_id = (
|
|
6364
|
-
SELECT sym.id FROM symbols sym
|
|
6365
|
-
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
6366
|
-
ORDER BY sym.id LIMIT 1
|
|
6367
|
-
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
6368
|
-
AND EXISTS (
|
|
6369
|
-
SELECT 1 FROM symbols sym
|
|
6370
|
-
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
6371
|
-
)`
|
|
6372
|
-
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
6373
|
-
return result.changes ?? 0;
|
|
6374
|
-
}
|
|
6375
|
-
});
|
|
6334
|
+
return this.runWithRetry(() => resolveRefsWithStatement((sql) => this.stmt(sql)));
|
|
6376
6335
|
}
|
|
6377
6336
|
resolveRefsForNames(names) {
|
|
6378
6337
|
return this.runWithRetry(() => this.resolveRefsForNamesUnsafe(names));
|
|
6379
6338
|
}
|
|
6380
|
-
/**
|
|
6381
|
-
* Clear symbols/refs for a file and mark it as indexed with zero symbols.
|
|
6382
|
-
* Used by the indexer for empty-parse results so three writes share one txn.
|
|
6383
|
-
*/
|
|
6384
6339
|
replaceEmptyFile(meta) {
|
|
6385
6340
|
this.invalidateBm25();
|
|
6386
6341
|
this.runWithRetry(() => {
|
|
@@ -6426,20 +6381,12 @@ var IndexStore = class _IndexStore {
|
|
|
6426
6381
|
}
|
|
6427
6382
|
});
|
|
6428
6383
|
}
|
|
6429
|
-
/** Best-effort query planner refresh after a large reindex. */
|
|
6430
6384
|
optimize() {
|
|
6431
6385
|
try {
|
|
6432
6386
|
this.db.exec("PRAGMA optimize");
|
|
6433
6387
|
} catch {
|
|
6434
6388
|
}
|
|
6435
6389
|
}
|
|
6436
|
-
/**
|
|
6437
|
-
* Reclaim page churn left by repeated force rebuilds.
|
|
6438
|
-
*
|
|
6439
|
-
* SQLite's DROP/CREATE path makes rebuilds fast but leaves pages on the
|
|
6440
|
-
* freelist. Compact only large, materially sparse databases and only when the
|
|
6441
|
-
* caller is already on a full-index maintenance path.
|
|
6442
|
-
*/
|
|
6443
6390
|
compactIfNeeded(options = {}) {
|
|
6444
6391
|
const minBytes = options.minBytes ?? 256 * 1024 * 1024;
|
|
6445
6392
|
const minFreeRatio = options.minFreeRatio ?? 0.35;
|
|
@@ -6466,115 +6413,44 @@ var IndexStore = class _IndexStore {
|
|
|
6466
6413
|
return false;
|
|
6467
6414
|
}
|
|
6468
6415
|
}
|
|
6469
|
-
/**
|
|
6470
|
-
* Find all symbols that reference the named target symbol (incoming callers).
|
|
6471
|
-
* Accepts a name instead of an id so the agent doesn't need a prior lookup.
|
|
6472
|
-
*/
|
|
6473
6416
|
findIncomingCallsByName(symbolName, file, limit = 100) {
|
|
6474
6417
|
return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6475
6418
|
}
|
|
6476
|
-
/**
|
|
6477
|
-
* Find all symbols that the named source symbol references (outgoing callees).
|
|
6478
|
-
* Accepts a name instead of an id so the agent doesn't need a prior lookup.
|
|
6479
|
-
*/
|
|
6480
6419
|
findOutgoingCallsByName(symbolName, file, limit = 100) {
|
|
6481
6420
|
return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6482
6421
|
}
|
|
6483
|
-
/**
|
|
6484
|
-
* Transitive incoming-call tree: all symbols that transitively call the
|
|
6485
|
-
* target, to an unbounded depth (cycle-safe via SQL UNION deduplication).
|
|
6486
|
-
* Used by `codebase-incoming-calls` when the caller wants the full call
|
|
6487
|
-
* chain rather than just direct callers.
|
|
6488
|
-
*/
|
|
6489
6422
|
findTransitiveIncomingCallsByName(symbolName, file, limit = 200) {
|
|
6490
6423
|
return findTransitiveIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6491
6424
|
}
|
|
6492
|
-
/**
|
|
6493
|
-
* Transitive outgoing-call tree: all symbols the target transitively calls.
|
|
6494
|
-
* Used by `codebase-outgoing-calls` when the caller wants the full
|
|
6495
|
-
* dependency chain rather than just direct callees.
|
|
6496
|
-
*/
|
|
6497
6425
|
findTransitiveOutgoingCallsByName(symbolName, file, limit = 200) {
|
|
6498
6426
|
return findTransitiveOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6499
6427
|
}
|
|
6500
|
-
/**
|
|
6501
|
-
* Compute the set of symbol IDs reachable from the given seed IDs using a
|
|
6502
|
-
* native SQLite recursive CTE. Used by dead-code detection to replace the
|
|
6503
|
-
* in-memory BFS.
|
|
6504
|
-
*/
|
|
6505
6428
|
findReachableSymbolIds(seedIds) {
|
|
6506
6429
|
return findReachableSymbolIds((sql) => this.stmt(sql), seedIds);
|
|
6507
6430
|
}
|
|
6508
|
-
/**
|
|
6509
|
-
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
6510
|
-
*/
|
|
6511
6431
|
findRefsTo(symbolId) {
|
|
6512
6432
|
return findRefsToWithStatement((sql) => this.stmt(sql), symbolId);
|
|
6513
6433
|
}
|
|
6514
|
-
/**
|
|
6515
|
-
* Find all references FROM a given symbol (what does this symbol call/use?).
|
|
6516
|
-
*/
|
|
6517
6434
|
findRefsFrom(symbolId) {
|
|
6518
6435
|
return findRefsFromWithStatement((sql) => this.stmt(sql), symbolId);
|
|
6519
6436
|
}
|
|
6520
|
-
// ─── CodeMap graph aggregation ──────────────────────────────────────────────
|
|
6521
|
-
/**
|
|
6522
|
-
* Package-level graph: each workspace package is a node; edges are derived
|
|
6523
|
-
* from cross-package symbol references (a symbol in package A references a
|
|
6524
|
-
* symbol resolved in package B). Node metadata includes symbol/file counts.
|
|
6525
|
-
*/
|
|
6526
6437
|
getPackageGraph() {
|
|
6527
6438
|
return getPackageGraphWithStatement((sql) => this.stmt(sql));
|
|
6528
6439
|
}
|
|
6529
|
-
/**
|
|
6530
|
-
* File-level graph for a single package: each file is a node; edges are
|
|
6531
|
-
* derived from cross-file symbol references within the package.
|
|
6532
|
-
*/
|
|
6533
6440
|
getFileGraph(packageFilter) {
|
|
6534
6441
|
return getFileGraphWithStatement((sql) => this.stmt(sql), packageFilter);
|
|
6535
6442
|
}
|
|
6536
|
-
/**
|
|
6537
|
-
* Symbol-level graph for a single file: each symbol is a node; edges are
|
|
6538
|
-
* derived from intra-file and cross-file symbol references (who calls whom).
|
|
6539
|
-
*/
|
|
6540
6443
|
getSymbolGraph(fileFilter) {
|
|
6541
6444
|
return getSymbolGraphWithStatement((sql) => this.stmt(sql), fileFilter);
|
|
6542
6445
|
}
|
|
6543
|
-
/**
|
|
6544
|
-
* Returns every symbol in the index. Used by dead-code analysis to
|
|
6545
|
-
* build the full symbol universe for the reachability scan.
|
|
6546
|
-
*/
|
|
6547
6446
|
getAllSymbols() {
|
|
6548
6447
|
return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
|
|
6549
6448
|
}
|
|
6550
|
-
/**
|
|
6551
|
-
* Returns every resolved reference (to_id IS NOT NULL). Used by
|
|
6552
|
-
* dead-code analysis to build the consumer-ship graph. Refs whose
|
|
6553
|
-
* target symbol id is null (unresolved imports) are excluded.
|
|
6554
|
-
*/
|
|
6555
6449
|
getAllResolvedRefs() {
|
|
6556
|
-
return this.stmt(
|
|
6557
|
-
"SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
|
|
6558
|
-
).all();
|
|
6450
|
+
return getAllResolvedRefsWithStatement((sql) => this.stmt(sql));
|
|
6559
6451
|
}
|
|
6560
|
-
/**
|
|
6561
|
-
* Returns ALL import refs (including unresolved) with their source-file
|
|
6562
|
-
* path and resolved target id. Used by the dead-code scan's file-level
|
|
6563
|
-
* graph traversal to handle barrel-only entry points where no symbol
|
|
6564
|
-
* carries the ref.
|
|
6565
|
-
*
|
|
6566
|
-
* Refs whose `from_id` doesn't match a known symbol (e.g. pure-barrel
|
|
6567
|
-
* files with no declarations) will have `sourceFile === null`.
|
|
6568
|
-
*/
|
|
6569
6452
|
getAllImportRefs() {
|
|
6570
|
-
return this.stmt(
|
|
6571
|
-
`SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
|
|
6572
|
-
r.call_type AS callType, r.line
|
|
6573
|
-
FROM refs r
|
|
6574
|
-
LEFT JOIN symbols s ON r.from_id = s.id
|
|
6575
|
-
WHERE r.call_type = 'import'
|
|
6576
|
-
ORDER BY r.line`
|
|
6577
|
-
).all();
|
|
6453
|
+
return getAllImportRefsWithStatement((sql) => this.stmt(sql));
|
|
6578
6454
|
}
|
|
6579
6455
|
close() {
|
|
6580
6456
|
this.stmtCache.clear();
|
|
@@ -7448,7 +7324,9 @@ var stopMemoryWatchdog = startSharedHeapWatchdog({
|
|
|
7448
7324
|
var lastProgressBroadcastAt = 0;
|
|
7449
7325
|
var externalWatcher;
|
|
7450
7326
|
var DEFAULT_EXTERNAL_DEBOUNCE_MS = 400;
|
|
7327
|
+
var DEFAULT_EXTERNAL_COALESCE_WINDOW_MS = 50;
|
|
7451
7328
|
var externalDebounceMs = DEFAULT_EXTERNAL_DEBOUNCE_MS;
|
|
7329
|
+
var externalCoalesceWindowMs = DEFAULT_EXTERNAL_COALESCE_WINDOW_MS;
|
|
7452
7330
|
var externalDebounceTimers = /* @__PURE__ */ new Map();
|
|
7453
7331
|
var externalReadyFiles = /* @__PURE__ */ new Set();
|
|
7454
7332
|
var externalReadyFlush;
|
|
@@ -7720,9 +7598,14 @@ async function handleMessage(state, message) {
|
|
|
7720
7598
|
if (message.type === "configure") {
|
|
7721
7599
|
const previousWatchExternal = state.watchExternal;
|
|
7722
7600
|
const previousDebounceMs = state.debounceMs;
|
|
7601
|
+
const previousCoalesceWindowMs = state.coalesceWindowMs;
|
|
7723
7602
|
try {
|
|
7724
7603
|
state.watchExternal = message.watchExternal;
|
|
7725
7604
|
state.debounceMs = Math.max(0, message.debounceMs);
|
|
7605
|
+
state.coalesceWindowMs = Math.max(
|
|
7606
|
+
0,
|
|
7607
|
+
message.coalesceWindowMs ?? DEFAULT_EXTERNAL_COALESCE_WINDOW_MS
|
|
7608
|
+
);
|
|
7726
7609
|
reconcileExternalWatcher();
|
|
7727
7610
|
send(state, {
|
|
7728
7611
|
type: "response",
|
|
@@ -7736,6 +7619,7 @@ async function handleMessage(state, message) {
|
|
|
7736
7619
|
} catch (error) {
|
|
7737
7620
|
state.watchExternal = previousWatchExternal;
|
|
7738
7621
|
state.debounceMs = previousDebounceMs;
|
|
7622
|
+
state.coalesceWindowMs = previousCoalesceWindowMs;
|
|
7739
7623
|
try {
|
|
7740
7624
|
reconcileExternalWatcher();
|
|
7741
7625
|
} catch {
|
|
@@ -7791,25 +7675,24 @@ function enqueueExternalFile(file) {
|
|
|
7791
7675
|
const timer = setTimeout(() => {
|
|
7792
7676
|
externalDebounceTimers.delete(file);
|
|
7793
7677
|
externalReadyFiles.add(file);
|
|
7794
|
-
if (
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
});
|
|
7678
|
+
if (externalReadyFlush) clearTimeout(externalReadyFlush);
|
|
7679
|
+
externalReadyFlush = setTimeout(() => {
|
|
7680
|
+
externalReadyFlush = void 0;
|
|
7681
|
+
const files = [...externalReadyFiles].sort();
|
|
7682
|
+
externalReadyFiles.clear();
|
|
7683
|
+
void withIndexWrite(
|
|
7684
|
+
(onProgress) => indexService(
|
|
7685
|
+
{
|
|
7686
|
+
projectRoot,
|
|
7687
|
+
indexDir,
|
|
7688
|
+
files
|
|
7689
|
+
},
|
|
7690
|
+
{ onProgress }
|
|
7691
|
+
)
|
|
7692
|
+
).catch(() => {
|
|
7810
7693
|
});
|
|
7811
|
-
|
|
7812
|
-
|
|
7694
|
+
}, externalCoalesceWindowMs);
|
|
7695
|
+
externalReadyFlush.unref?.();
|
|
7813
7696
|
}, externalDebounceMs);
|
|
7814
7697
|
timer.unref?.();
|
|
7815
7698
|
externalDebounceTimers.set(file, timer);
|
|
@@ -7841,7 +7724,7 @@ function stopExternalWatcher() {
|
|
|
7841
7724
|
externalWatcher = void 0;
|
|
7842
7725
|
for (const timer of externalDebounceTimers.values()) clearTimeout(timer);
|
|
7843
7726
|
externalDebounceTimers.clear();
|
|
7844
|
-
if (externalReadyFlush)
|
|
7727
|
+
if (externalReadyFlush) clearTimeout(externalReadyFlush);
|
|
7845
7728
|
externalReadyFlush = void 0;
|
|
7846
7729
|
externalReadyFiles.clear();
|
|
7847
7730
|
}
|
|
@@ -7849,10 +7732,12 @@ function reconcileExternalWatcher() {
|
|
|
7849
7732
|
const owners = [...clients].filter((client) => client.watchExternal);
|
|
7850
7733
|
if (owners.length === 0) {
|
|
7851
7734
|
externalDebounceMs = DEFAULT_EXTERNAL_DEBOUNCE_MS;
|
|
7735
|
+
externalCoalesceWindowMs = DEFAULT_EXTERNAL_COALESCE_WINDOW_MS;
|
|
7852
7736
|
stopExternalWatcher();
|
|
7853
7737
|
return;
|
|
7854
7738
|
}
|
|
7855
7739
|
externalDebounceMs = Math.min(...owners.map((client) => client.debounceMs));
|
|
7740
|
+
externalCoalesceWindowMs = Math.min(...owners.map((client) => client.coalesceWindowMs));
|
|
7856
7741
|
ensureExternalWatcher();
|
|
7857
7742
|
}
|
|
7858
7743
|
function consume(state, chunk) {
|
|
@@ -7919,6 +7804,7 @@ var server = net.createServer((socket) => {
|
|
|
7919
7804
|
cancel: /* @__PURE__ */ new Map(),
|
|
7920
7805
|
watchExternal: false,
|
|
7921
7806
|
debounceMs: DEFAULT_EXTERNAL_DEBOUNCE_MS,
|
|
7807
|
+
coalesceWindowMs: DEFAULT_EXTERNAL_COALESCE_WINDOW_MS,
|
|
7922
7808
|
lastSeenAt: Date.now()
|
|
7923
7809
|
};
|
|
7924
7810
|
clients.add(state);
|