@wrongstack/tools 0.306.4 → 0.307.0
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 +11302 -9305
- 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 +40 -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 +4141 -1479
- 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 +751 -909
- 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 +723 -891
- 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 +8558 -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 +11386 -9416
- 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 +11295 -9305
- package/dist/patch.js +8301 -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 +751 -910
- package/dist/replace.js +8450 -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 +11302 -9305
- package/dist/typecheck.js +14 -8
- package/dist/write.js +8374 -153
- package/package.json +7 -7
package/dist/read.js
CHANGED
|
@@ -562,17 +562,44 @@ function fallbackParse(filePath, content, lang) {
|
|
|
562
562
|
const col = line.length - trimmed.length + 1;
|
|
563
563
|
const fn = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/.exec(trimmed);
|
|
564
564
|
if (fn?.[1]) {
|
|
565
|
-
addFallbackSymbol(symbols, {
|
|
565
|
+
addFallbackSymbol(symbols, {
|
|
566
|
+
filePath,
|
|
567
|
+
lang,
|
|
568
|
+
kind: trimmed.startsWith("func (") ? "method" : "function",
|
|
569
|
+
name: fn[1],
|
|
570
|
+
line: idx + 1,
|
|
571
|
+
col,
|
|
572
|
+
signature: trimmed,
|
|
573
|
+
scope: packageName ? `${packageName}.${fn[1]}` : fn[1]
|
|
574
|
+
});
|
|
566
575
|
continue;
|
|
567
576
|
}
|
|
568
577
|
const typeDecl = /^type\s+([A-Za-z_]\w*)\b/.exec(trimmed);
|
|
569
578
|
if (typeDecl?.[1]) {
|
|
570
|
-
addFallbackSymbol(symbols, {
|
|
579
|
+
addFallbackSymbol(symbols, {
|
|
580
|
+
filePath,
|
|
581
|
+
lang,
|
|
582
|
+
kind: "type",
|
|
583
|
+
name: typeDecl[1],
|
|
584
|
+
line: idx + 1,
|
|
585
|
+
col,
|
|
586
|
+
signature: trimmed,
|
|
587
|
+
scope: packageName
|
|
588
|
+
});
|
|
571
589
|
continue;
|
|
572
590
|
}
|
|
573
591
|
const valueDecl = /^(const|var)\s+([A-Za-z_]\w*)\b/.exec(trimmed);
|
|
574
592
|
if (valueDecl?.[1] && valueDecl[2]) {
|
|
575
|
-
addFallbackSymbol(symbols, {
|
|
593
|
+
addFallbackSymbol(symbols, {
|
|
594
|
+
filePath,
|
|
595
|
+
lang,
|
|
596
|
+
kind: valueDecl[1],
|
|
597
|
+
name: valueDecl[2],
|
|
598
|
+
line: idx + 1,
|
|
599
|
+
col,
|
|
600
|
+
signature: trimmed,
|
|
601
|
+
scope: packageName
|
|
602
|
+
});
|
|
576
603
|
}
|
|
577
604
|
}
|
|
578
605
|
return { file: filePath, lang, symbols, mtimeMs: Date.now() };
|
|
@@ -1023,7 +1050,10 @@ function parseGeneric(opts) {
|
|
|
1023
1050
|
const seen = /* @__PURE__ */ new Set();
|
|
1024
1051
|
const nlOffsets = newlineOffsets2(content);
|
|
1025
1052
|
for (const pattern of patterns) {
|
|
1026
|
-
const re = new RegExp(
|
|
1053
|
+
const re = new RegExp(
|
|
1054
|
+
pattern.re.source,
|
|
1055
|
+
pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`
|
|
1056
|
+
);
|
|
1027
1057
|
re.lastIndex = 0;
|
|
1028
1058
|
for (const match of content.matchAll(re)) {
|
|
1029
1059
|
if (symbols.length >= maxSymbols) break;
|
|
@@ -1130,7 +1160,10 @@ var init_generic_parser = __esm({
|
|
|
1130
1160
|
],
|
|
1131
1161
|
kotlin: [
|
|
1132
1162
|
{ re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
|
|
1133
|
-
{
|
|
1163
|
+
{
|
|
1164
|
+
re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g,
|
|
1165
|
+
kind: "class"
|
|
1166
|
+
}
|
|
1134
1167
|
],
|
|
1135
1168
|
scala: [
|
|
1136
1169
|
{ re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
|
|
@@ -1141,14 +1174,13 @@ var init_generic_parser = __esm({
|
|
|
1141
1174
|
{ re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
|
|
1142
1175
|
],
|
|
1143
1176
|
sql: [
|
|
1144
|
-
{
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
],
|
|
1149
|
-
toml: [
|
|
1150
|
-
{ re: /^\[([^\]]+)\]/gm, kind: "namespace" }
|
|
1177
|
+
{
|
|
1178
|
+
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,
|
|
1179
|
+
kind: "type"
|
|
1180
|
+
}
|
|
1151
1181
|
],
|
|
1182
|
+
md: [{ re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }],
|
|
1183
|
+
toml: [{ re: /^\[([^\]]+)\]/gm, kind: "namespace" }],
|
|
1152
1184
|
html: [
|
|
1153
1185
|
{ re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
|
|
1154
1186
|
{ re: /<(?:script|template|style)\b/gi, kind: "namespace" }
|
|
@@ -1158,11 +1190,17 @@ var init_generic_parser = __esm({
|
|
|
1158
1190
|
{ re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
|
|
1159
1191
|
],
|
|
1160
1192
|
vue: [
|
|
1161
|
-
{
|
|
1193
|
+
{
|
|
1194
|
+
re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g,
|
|
1195
|
+
kind: "function"
|
|
1196
|
+
},
|
|
1162
1197
|
{ re: /<(?:script|template|style)\b/gi, kind: "namespace" }
|
|
1163
1198
|
],
|
|
1164
1199
|
svelte: [
|
|
1165
|
-
{
|
|
1200
|
+
{
|
|
1201
|
+
re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g,
|
|
1202
|
+
kind: "function"
|
|
1203
|
+
}
|
|
1166
1204
|
],
|
|
1167
1205
|
dart: [
|
|
1168
1206
|
{ re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
|
|
@@ -1376,12 +1414,7 @@ async function syncPyParse(filePath, content, lang) {
|
|
|
1376
1414
|
cachedPyBinary ??= resolvePython();
|
|
1377
1415
|
const pyBinary = await cachedPyBinary;
|
|
1378
1416
|
if (!pyBinary) return null;
|
|
1379
|
-
const { code, stdout } = await spawnPyParser(
|
|
1380
|
-
pyBinary,
|
|
1381
|
-
_cachedScriptPath,
|
|
1382
|
-
filePath,
|
|
1383
|
-
content
|
|
1384
|
-
);
|
|
1417
|
+
const { code, stdout } = await spawnPyParser(pyBinary, _cachedScriptPath, filePath, content);
|
|
1385
1418
|
if (code !== 0 || !stdout.trim()) {
|
|
1386
1419
|
return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
|
|
1387
1420
|
}
|
|
@@ -2575,7 +2608,8 @@ __export(tree_sitter_parser_exports, {
|
|
|
2575
2608
|
getGrammarWasmPath: () => getGrammarWasmPath,
|
|
2576
2609
|
isTreeSitterSupported: () => isTreeSitterSupported,
|
|
2577
2610
|
loadTreeSitterLanguage: () => loadTreeSitterLanguage,
|
|
2578
|
-
parseSymbols: () => parseSymbols8
|
|
2611
|
+
parseSymbols: () => parseSymbols8,
|
|
2612
|
+
parseTreeSitterAst: () => parseTreeSitterAst
|
|
2579
2613
|
});
|
|
2580
2614
|
import * as path10 from "node:path";
|
|
2581
2615
|
import { fileURLToPath } from "node:url";
|
|
@@ -2667,6 +2701,26 @@ async function __smokeRootType(opts) {
|
|
|
2667
2701
|
parser.delete();
|
|
2668
2702
|
}
|
|
2669
2703
|
}
|
|
2704
|
+
async function parseTreeSitterAst(opts) {
|
|
2705
|
+
const grammar = resolveGrammarName(opts.lang) ?? (opts.lang === "go" ? "go" : opts.lang === "py" ? "python" : opts.lang === "rs" ? "rust" : void 0);
|
|
2706
|
+
if (!grammar) return null;
|
|
2707
|
+
try {
|
|
2708
|
+
const { Parser, Language, init } = await getRuntime();
|
|
2709
|
+
await init();
|
|
2710
|
+
const wasmPath = path10.join(WASM_DIR, grammar, `tree-sitter-${grammar}.wasm`);
|
|
2711
|
+
const languageObj = await Language.load(wasmPath);
|
|
2712
|
+
const parser = new Parser();
|
|
2713
|
+
parser.setLanguage(languageObj);
|
|
2714
|
+
const tree = parser.parse(opts.content);
|
|
2715
|
+
if (!tree) {
|
|
2716
|
+
parser.delete();
|
|
2717
|
+
return null;
|
|
2718
|
+
}
|
|
2719
|
+
return { tree, parser };
|
|
2720
|
+
} catch {
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2670
2724
|
var WASM_DIR, RUNTIME_WASM, LANG_TO_GRAMMAR, GO_OPT_IN, PY_OPT_IN, RS_OPT_IN, runtimePromise, languageCache;
|
|
2671
2725
|
var init_tree_sitter_parser = __esm({
|
|
2672
2726
|
"src/codebase-index/tree-sitter-parser.ts"() {
|
|
@@ -2699,7 +2753,7 @@ var init_tree_sitter_parser = __esm({
|
|
|
2699
2753
|
});
|
|
2700
2754
|
|
|
2701
2755
|
// src/read.ts
|
|
2702
|
-
import * as
|
|
2756
|
+
import * as fs14 from "node:fs/promises";
|
|
2703
2757
|
import { FsError, ToolValidationError } from "@wrongstack/core/types";
|
|
2704
2758
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
2705
2759
|
|
|
@@ -2775,8 +2829,8 @@ function isBinaryBuffer(buf) {
|
|
|
2775
2829
|
}
|
|
2776
2830
|
|
|
2777
2831
|
// src/codebase-index/background-indexer.ts
|
|
2778
|
-
import * as
|
|
2779
|
-
import { fileURLToPath as
|
|
2832
|
+
import * as fs13 from "node:fs";
|
|
2833
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
2780
2834
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
2781
2835
|
|
|
2782
2836
|
// src/codebase-index/circuit-breaker.ts
|
|
@@ -3622,10 +3676,7 @@ var LANG_IMPORTS = {
|
|
|
3622
3676
|
// quoted path. A stray match elsewhere resolves to no file and is dropped.
|
|
3623
3677
|
{ re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
|
|
3624
3678
|
],
|
|
3625
|
-
py: [
|
|
3626
|
-
{ re: /^[ \t]*import\s+([\w.]+)/gm },
|
|
3627
|
-
{ re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
|
|
3628
|
-
],
|
|
3679
|
+
py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
|
|
3629
3680
|
rs: [
|
|
3630
3681
|
// use a::b::C; | use a::b::{C, D}; → the path before any brace
|
|
3631
3682
|
{ re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
|
|
@@ -3962,10 +4013,7 @@ function defaultWorkerCount() {
|
|
|
3962
4013
|
return Math.max(1, Math.min(4, cores - 1));
|
|
3963
4014
|
}
|
|
3964
4015
|
function resolveWorkerScriptUrl() {
|
|
3965
|
-
for (const rel of [
|
|
3966
|
-
"./parser-worker-script.js",
|
|
3967
|
-
"./codebase-index/parser-worker-script.js"
|
|
3968
|
-
]) {
|
|
4016
|
+
for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
|
|
3969
4017
|
try {
|
|
3970
4018
|
const url = new URL(rel, import.meta.url);
|
|
3971
4019
|
if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
|
|
@@ -3981,7 +4029,6 @@ function getParserPool() {
|
|
|
3981
4029
|
}
|
|
3982
4030
|
|
|
3983
4031
|
// src/codebase-index/writer.ts
|
|
3984
|
-
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
3985
4032
|
import * as fs8 from "node:fs";
|
|
3986
4033
|
import * as path12 from "node:path";
|
|
3987
4034
|
|
|
@@ -4077,39 +4124,6 @@ var Bm25Index = class {
|
|
|
4077
4124
|
// src/codebase-index/writer.ts
|
|
4078
4125
|
init_languages();
|
|
4079
4126
|
|
|
4080
|
-
// src/codebase-index/lsp-kind.ts
|
|
4081
|
-
function lspKindToInternalKind(k) {
|
|
4082
|
-
switch (k) {
|
|
4083
|
-
case 5 /* Class */:
|
|
4084
|
-
return "class";
|
|
4085
|
-
case 6 /* Method */:
|
|
4086
|
-
return "method";
|
|
4087
|
-
case 7 /* Property */:
|
|
4088
|
-
case 8 /* Field */:
|
|
4089
|
-
return "property";
|
|
4090
|
-
case 9 /* Constructor */:
|
|
4091
|
-
return "class";
|
|
4092
|
-
case 10 /* Enum */:
|
|
4093
|
-
return "enum";
|
|
4094
|
-
case 11 /* Interface */:
|
|
4095
|
-
return "interface";
|
|
4096
|
-
case 12 /* Function */:
|
|
4097
|
-
return "function";
|
|
4098
|
-
case 13 /* Variable */:
|
|
4099
|
-
return "var";
|
|
4100
|
-
case 14 /* Constant */:
|
|
4101
|
-
return "const";
|
|
4102
|
-
case 22 /* EnumMember */:
|
|
4103
|
-
return "enum";
|
|
4104
|
-
case 26 /* TypeParameter */:
|
|
4105
|
-
return "type";
|
|
4106
|
-
case 3 /* Namespace */:
|
|
4107
|
-
return "namespace";
|
|
4108
|
-
default:
|
|
4109
|
-
return null;
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
|
|
4113
4127
|
// src/codebase-index/schema.ts
|
|
4114
4128
|
var SCHEMA_VERSION = 4;
|
|
4115
4129
|
|
|
@@ -4180,91 +4194,14 @@ function runSqliteWithRetry(fn) {
|
|
|
4180
4194
|
throw lastError;
|
|
4181
4195
|
}
|
|
4182
4196
|
|
|
4183
|
-
// src/codebase-index/vector-search.ts
|
|
4184
|
-
var RRF_K = 60;
|
|
4185
|
-
var VECTOR_DIMENSIONS = 384;
|
|
4186
|
-
var NGRAM_SIZE = 3;
|
|
4187
|
-
function embedText(text) {
|
|
4188
|
-
const vec = new Float32Array(VECTOR_DIMENSIONS);
|
|
4189
|
-
const normalized = text.toLowerCase().trim();
|
|
4190
|
-
if (normalized.length < NGRAM_SIZE) {
|
|
4191
|
-
const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
|
|
4192
|
-
for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
|
|
4193
|
-
const ngram = padded.slice(i, i + NGRAM_SIZE);
|
|
4194
|
-
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
4195
|
-
vec[bucket] += 1;
|
|
4196
|
-
}
|
|
4197
|
-
} else {
|
|
4198
|
-
for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
|
|
4199
|
-
const ngram = normalized.slice(i, i + NGRAM_SIZE);
|
|
4200
|
-
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
4201
|
-
vec[bucket] += 1;
|
|
4202
|
-
}
|
|
4203
|
-
}
|
|
4204
|
-
let norm = 0;
|
|
4205
|
-
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
4206
|
-
norm += vec[i] * vec[i];
|
|
4207
|
-
}
|
|
4208
|
-
norm = Math.sqrt(norm);
|
|
4209
|
-
if (norm > 0) {
|
|
4210
|
-
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
4211
|
-
vec[i] /= norm;
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
return vec;
|
|
4215
|
-
}
|
|
4216
|
-
function hashNgram(str) {
|
|
4217
|
-
let hash = 2166136261;
|
|
4218
|
-
for (let i = 0; i < str.length; i++) {
|
|
4219
|
-
hash ^= str.charCodeAt(i);
|
|
4220
|
-
hash = Math.imul(hash, 16777619);
|
|
4221
|
-
}
|
|
4222
|
-
return hash >>> 0;
|
|
4223
|
-
}
|
|
4224
|
-
function cosineSimilarity(a, b) {
|
|
4225
|
-
let dot = 0;
|
|
4226
|
-
const len = Math.min(a.length, b.length);
|
|
4227
|
-
for (let i = 0; i < len; i++) {
|
|
4228
|
-
dot += a[i] * b[i];
|
|
4229
|
-
}
|
|
4230
|
-
return dot;
|
|
4231
|
-
}
|
|
4232
|
-
function encodeVector(vec) {
|
|
4233
|
-
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
4234
|
-
}
|
|
4235
|
-
function decodeVector(buf) {
|
|
4236
|
-
const view = new DataView(
|
|
4237
|
-
buf.buffer,
|
|
4238
|
-
buf.byteOffset,
|
|
4239
|
-
buf.byteLength
|
|
4240
|
-
);
|
|
4241
|
-
const copy = new Float32Array(buf.byteLength / 4);
|
|
4242
|
-
for (let i = 0; i < copy.length; i++) {
|
|
4243
|
-
copy[i] = view.getFloat32(i * 4, true);
|
|
4244
|
-
}
|
|
4245
|
-
return copy;
|
|
4246
|
-
}
|
|
4247
|
-
function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
|
|
4248
|
-
const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
|
|
4249
|
-
const scored = [];
|
|
4250
|
-
for (const id of allIds) {
|
|
4251
|
-
const bm25Rank = bm25Ranks.get(id);
|
|
4252
|
-
const vecRank = vectorRanks.get(id);
|
|
4253
|
-
let score = 0;
|
|
4254
|
-
if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
|
|
4255
|
-
if (vecRank !== void 0) score += 1 / (k + vecRank);
|
|
4256
|
-
scored.push([id, score]);
|
|
4257
|
-
}
|
|
4258
|
-
scored.sort((a, b) => b[1] - a[1]);
|
|
4259
|
-
return scored;
|
|
4260
|
-
}
|
|
4261
|
-
|
|
4262
4197
|
// src/codebase-index/writer-admin.ts
|
|
4263
4198
|
import * as fs7 from "node:fs";
|
|
4264
4199
|
import * as path11 from "node:path";
|
|
4265
4200
|
var DB_FILE = "index.db";
|
|
4266
4201
|
function getAllIndexableWithStatement(stmt) {
|
|
4267
|
-
return stmt("SELECT id, text FROM symbols").all().map(
|
|
4202
|
+
return stmt("SELECT id, text FROM symbols").all().map(
|
|
4203
|
+
({ id, text }) => ({ id, text })
|
|
4204
|
+
);
|
|
4268
4205
|
}
|
|
4269
4206
|
function getMaxSymbolIdWithStatement(stmt) {
|
|
4270
4207
|
const rows = stmt("SELECT MAX(id) AS m FROM symbols").all();
|
|
@@ -4604,7 +4541,8 @@ function resolveSymbolIds(stmt, symbolName, file) {
|
|
|
4604
4541
|
}
|
|
4605
4542
|
function findIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
4606
4543
|
const targetIds = resolveSymbolIds(stmt, symbolName, file);
|
|
4607
|
-
if (targetIds.length === 0)
|
|
4544
|
+
if (targetIds.length === 0)
|
|
4545
|
+
return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
|
|
4608
4546
|
let matchIds = targetIds;
|
|
4609
4547
|
let ambiguous = false;
|
|
4610
4548
|
if (file !== void 0) {
|
|
@@ -4655,11 +4593,17 @@ function findIncomingCallsByName(stmt, symbolName, file, limit) {
|
|
|
4655
4593
|
}
|
|
4656
4594
|
rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
|
|
4657
4595
|
const allCalls = rows.map(mapCallSiteRow);
|
|
4658
|
-
return {
|
|
4596
|
+
return {
|
|
4597
|
+
calls: allCalls.slice(0, limit),
|
|
4598
|
+
symbolFound: true,
|
|
4599
|
+
ambiguous,
|
|
4600
|
+
totalMatches: allCalls.length
|
|
4601
|
+
};
|
|
4659
4602
|
}
|
|
4660
4603
|
function findOutgoingCallsByName(stmt, symbolName, file, limit) {
|
|
4661
4604
|
const sourceIds = resolveSymbolIds(stmt, symbolName, file);
|
|
4662
|
-
if (sourceIds.length === 0)
|
|
4605
|
+
if (sourceIds.length === 0)
|
|
4606
|
+
return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
|
|
4663
4607
|
const unresolvedCount = chunkedIdScalar(
|
|
4664
4608
|
stmt,
|
|
4665
4609
|
sourceIds,
|
|
@@ -5067,6 +5011,184 @@ function codebaseIndexDirOverride(ctx) {
|
|
|
5067
5011
|
return typeof v === "string" ? v : void 0;
|
|
5068
5012
|
}
|
|
5069
5013
|
|
|
5014
|
+
// src/codebase-index/vector-search.ts
|
|
5015
|
+
var RRF_K = 60;
|
|
5016
|
+
var VECTOR_DIMENSIONS = 384;
|
|
5017
|
+
var NGRAM_SIZE = 3;
|
|
5018
|
+
function embedText(text) {
|
|
5019
|
+
const vec = new Float32Array(VECTOR_DIMENSIONS);
|
|
5020
|
+
const normalized = text.toLowerCase().trim();
|
|
5021
|
+
if (normalized.length < NGRAM_SIZE) {
|
|
5022
|
+
const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
|
|
5023
|
+
for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
|
|
5024
|
+
const ngram = padded.slice(i, i + NGRAM_SIZE);
|
|
5025
|
+
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
5026
|
+
vec[bucket] += 1;
|
|
5027
|
+
}
|
|
5028
|
+
} else {
|
|
5029
|
+
for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
|
|
5030
|
+
const ngram = normalized.slice(i, i + NGRAM_SIZE);
|
|
5031
|
+
const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
|
|
5032
|
+
vec[bucket] += 1;
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
let norm = 0;
|
|
5036
|
+
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
5037
|
+
norm += vec[i] * vec[i];
|
|
5038
|
+
}
|
|
5039
|
+
norm = Math.sqrt(norm);
|
|
5040
|
+
if (norm > 0) {
|
|
5041
|
+
for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
|
|
5042
|
+
vec[i] /= norm;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
return vec;
|
|
5046
|
+
}
|
|
5047
|
+
function hashNgram(str) {
|
|
5048
|
+
let hash = 2166136261;
|
|
5049
|
+
for (let i = 0; i < str.length; i++) {
|
|
5050
|
+
hash ^= str.charCodeAt(i);
|
|
5051
|
+
hash = Math.imul(hash, 16777619);
|
|
5052
|
+
}
|
|
5053
|
+
return hash >>> 0;
|
|
5054
|
+
}
|
|
5055
|
+
function cosineSimilarity(a, b) {
|
|
5056
|
+
let dot = 0;
|
|
5057
|
+
const len = Math.min(a.length, b.length);
|
|
5058
|
+
for (let i = 0; i < len; i++) {
|
|
5059
|
+
dot += a[i] * b[i];
|
|
5060
|
+
}
|
|
5061
|
+
return dot;
|
|
5062
|
+
}
|
|
5063
|
+
function encodeVector(vec) {
|
|
5064
|
+
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
5065
|
+
}
|
|
5066
|
+
function decodeVector(buf) {
|
|
5067
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
5068
|
+
const copy = new Float32Array(buf.byteLength / 4);
|
|
5069
|
+
for (let i = 0; i < copy.length; i++) {
|
|
5070
|
+
copy[i] = view.getFloat32(i * 4, true);
|
|
5071
|
+
}
|
|
5072
|
+
return copy;
|
|
5073
|
+
}
|
|
5074
|
+
function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
|
|
5075
|
+
const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
|
|
5076
|
+
const scored = [];
|
|
5077
|
+
for (const id of allIds) {
|
|
5078
|
+
const bm25Rank = bm25Ranks.get(id);
|
|
5079
|
+
const vecRank = vectorRanks.get(id);
|
|
5080
|
+
let score = 0;
|
|
5081
|
+
if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
|
|
5082
|
+
if (vecRank !== void 0) score += 1 / (k + vecRank);
|
|
5083
|
+
scored.push([id, score]);
|
|
5084
|
+
}
|
|
5085
|
+
scored.sort((a, b) => b[1] - a[1]);
|
|
5086
|
+
return scored;
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
// src/codebase-index/writer-mutations.ts
|
|
5090
|
+
function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
|
|
5091
|
+
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
5092
|
+
return [];
|
|
5093
|
+
}
|
|
5094
|
+
const affectedNames = /* @__PURE__ */ new Set();
|
|
5095
|
+
for (const entry of entries) {
|
|
5096
|
+
for (const symbol of entry.symbols) affectedNames.add(symbol.name);
|
|
5097
|
+
for (const ref of entry.refs) affectedNames.add(ref.toName);
|
|
5098
|
+
}
|
|
5099
|
+
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
5100
|
+
const placeholders = options.deleteForFiles.map(() => "?").join(",");
|
|
5101
|
+
for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
|
|
5102
|
+
affectedNames.add(name);
|
|
5103
|
+
}
|
|
5104
|
+
if (ftsAvailable) {
|
|
5105
|
+
stmtFn(
|
|
5106
|
+
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5107
|
+
).run(...options.deleteForFiles);
|
|
5108
|
+
}
|
|
5109
|
+
if (vectorsAvailable) {
|
|
5110
|
+
stmtFn(
|
|
5111
|
+
`DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5112
|
+
).run(...options.deleteForFiles);
|
|
5113
|
+
}
|
|
5114
|
+
stmtFn(
|
|
5115
|
+
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5116
|
+
).run(...options.deleteForFiles);
|
|
5117
|
+
stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
|
|
5118
|
+
}
|
|
5119
|
+
const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
5120
|
+
let nextId = allocateSymbolIds(totalSymbols);
|
|
5121
|
+
const allInserted = [];
|
|
5122
|
+
const refsToInsert = [];
|
|
5123
|
+
const bulkSyms = [];
|
|
5124
|
+
const ftsRows = [];
|
|
5125
|
+
const vectorRows = [];
|
|
5126
|
+
for (const entry of entries) {
|
|
5127
|
+
const insertedForEntry = [];
|
|
5128
|
+
for (const s of entry.symbols) {
|
|
5129
|
+
const id = nextId++;
|
|
5130
|
+
bulkSyms.push({
|
|
5131
|
+
id,
|
|
5132
|
+
lang: s.lang,
|
|
5133
|
+
kind: s.kind,
|
|
5134
|
+
name: s.name,
|
|
5135
|
+
file: s.file,
|
|
5136
|
+
line: s.line,
|
|
5137
|
+
col: s.col,
|
|
5138
|
+
signature: s.signature,
|
|
5139
|
+
docComment: s.docComment,
|
|
5140
|
+
scope: s.scope,
|
|
5141
|
+
text: s.text
|
|
5142
|
+
});
|
|
5143
|
+
if (ftsAvailable) {
|
|
5144
|
+
ftsRows.push({
|
|
5145
|
+
id,
|
|
5146
|
+
text: buildIndexableText(s.name, s.signature, s.docComment)
|
|
5147
|
+
});
|
|
5148
|
+
}
|
|
5149
|
+
vectorRows.push({
|
|
5150
|
+
id,
|
|
5151
|
+
vector: encodeVector(
|
|
5152
|
+
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
5153
|
+
)
|
|
5154
|
+
});
|
|
5155
|
+
const inserted = { ...s, id };
|
|
5156
|
+
allInserted.push(inserted);
|
|
5157
|
+
insertedForEntry.push(inserted);
|
|
5158
|
+
}
|
|
5159
|
+
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
5160
|
+
}
|
|
5161
|
+
bulkInsertSymbolsWithStatement((sql) => stmtFn(sql), maxSqlVars, bulkSyms);
|
|
5162
|
+
bulkInsertFtsWithStatement((sql) => stmtFn(sql), maxSqlVars, ftsAvailable, ftsRows);
|
|
5163
|
+
if (vectorsAvailable) {
|
|
5164
|
+
bulkInsertVectorsWithStatement((sql) => stmtFn(sql), maxSqlVars, vectorRows);
|
|
5165
|
+
}
|
|
5166
|
+
bulkInsertRefsWithStatement((sql) => stmtFn(sql), maxSqlVars, refsToInsert);
|
|
5167
|
+
const upsertStmt = stmtFn(
|
|
5168
|
+
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
5169
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
5170
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
5171
|
+
lang = excluded.lang,
|
|
5172
|
+
mtime_ms = excluded.mtime_ms,
|
|
5173
|
+
content_hash = excluded.content_hash,
|
|
5174
|
+
symbol_count = excluded.symbol_count,
|
|
5175
|
+
last_indexed = excluded.last_indexed`
|
|
5176
|
+
);
|
|
5177
|
+
const now = Date.now();
|
|
5178
|
+
for (const entry of entries) {
|
|
5179
|
+
upsertStmt.run(
|
|
5180
|
+
entry.file,
|
|
5181
|
+
entry.lang,
|
|
5182
|
+
entry.mtimeMs,
|
|
5183
|
+
entry.contentHash ?? "",
|
|
5184
|
+
entry.symbolCount,
|
|
5185
|
+
now
|
|
5186
|
+
);
|
|
5187
|
+
}
|
|
5188
|
+
resolveRefsForNamesUnsafe2(affectedNames);
|
|
5189
|
+
return allInserted;
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5070
5192
|
// src/codebase-index/writer-pragmas.ts
|
|
5071
5193
|
import { sqliteCachePragmas } from "@wrongstack/core/utils";
|
|
5072
5194
|
function applyIndexStorePragmas(db) {
|
|
@@ -5179,22 +5301,253 @@ var SYMBOL_VECTORS_TABLE_SQL = `
|
|
|
5179
5301
|
);
|
|
5180
5302
|
`;
|
|
5181
5303
|
|
|
5182
|
-
// src/codebase-index/writer-
|
|
5183
|
-
var
|
|
5184
|
-
|
|
5185
|
-
|
|
5304
|
+
// src/codebase-index/writer-refs.ts
|
|
5305
|
+
var FAMILY_MATCH_SQL = `(
|
|
5306
|
+
sym.lang = refs.lang
|
|
5307
|
+
OR EXISTS (
|
|
5308
|
+
SELECT 1 FROM lang_family lf1
|
|
5309
|
+
JOIN lang_family lf2 ON lf1.family = lf2.family
|
|
5310
|
+
WHERE lf1.lang = sym.lang AND lf2.lang = refs.lang
|
|
5311
|
+
)
|
|
5312
|
+
OR ? IN (
|
|
5313
|
+
SELECT family FROM lang_family WHERE lang = refs.lang
|
|
5314
|
+
)
|
|
5315
|
+
)`;
|
|
5316
|
+
function getNamespaceDeclarationsWithStatement(stmtFn) {
|
|
5317
|
+
return stmtFn(
|
|
5318
|
+
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
5319
|
+
).all();
|
|
5186
5320
|
}
|
|
5187
|
-
function
|
|
5188
|
-
const
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5321
|
+
function getFilePackagesWithStatement(stmtFn) {
|
|
5322
|
+
const rows = stmtFn("SELECT file, package FROM files WHERE package != ''").all();
|
|
5323
|
+
return new Map(rows.map((row) => [row.file, row.package]));
|
|
5324
|
+
}
|
|
5325
|
+
function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
|
|
5326
|
+
const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
|
|
5327
|
+
FROM refs r
|
|
5328
|
+
JOIN symbols s ON s.id = r.from_id
|
|
5329
|
+
WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
|
|
5330
|
+
if (!onlyFiles?.length) {
|
|
5331
|
+
return stmtFn(base).all();
|
|
5332
|
+
}
|
|
5333
|
+
const out = [];
|
|
5334
|
+
for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
|
|
5335
|
+
const chunk = onlyFiles.slice(i, i + maxSqlVars);
|
|
5336
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
5337
|
+
out.push(
|
|
5338
|
+
...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
5339
|
+
);
|
|
5340
|
+
}
|
|
5341
|
+
return out;
|
|
5342
|
+
}
|
|
5343
|
+
function getAllResolvedRefsWithStatement(stmtFn) {
|
|
5344
|
+
return stmtFn(
|
|
5345
|
+
"SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
|
|
5346
|
+
).all();
|
|
5347
|
+
}
|
|
5348
|
+
function getAllImportRefsWithStatement(stmtFn) {
|
|
5349
|
+
return stmtFn(
|
|
5350
|
+
`SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
|
|
5351
|
+
r.call_type AS callType, r.line
|
|
5352
|
+
FROM refs r
|
|
5353
|
+
LEFT JOIN symbols s ON r.from_id = s.id
|
|
5354
|
+
WHERE r.call_type = 'import'
|
|
5355
|
+
ORDER BY r.line`
|
|
5356
|
+
).all();
|
|
5357
|
+
}
|
|
5358
|
+
function resolveRefsWithStatement(stmtFn) {
|
|
5359
|
+
try {
|
|
5360
|
+
const result = stmtFn(
|
|
5361
|
+
`UPDATE refs
|
|
5362
|
+
SET to_id = s.id
|
|
5363
|
+
FROM (
|
|
5364
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5365
|
+
FROM symbols sym
|
|
5366
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5367
|
+
GROUP BY sym.name, lf.family
|
|
5368
|
+
UNION ALL
|
|
5369
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5370
|
+
FROM symbols sym
|
|
5371
|
+
GROUP BY sym.name
|
|
5372
|
+
) AS s,
|
|
5373
|
+
lang_family AS rf
|
|
5374
|
+
WHERE refs.to_id IS NULL
|
|
5375
|
+
AND refs.to_name IS NOT NULL
|
|
5376
|
+
AND rf.lang = refs.lang
|
|
5377
|
+
AND s.name = refs.to_name
|
|
5378
|
+
AND s.family = rf.family`
|
|
5379
|
+
).run();
|
|
5380
|
+
return result.changes ?? 0;
|
|
5381
|
+
} catch {
|
|
5382
|
+
const result = stmtFn(
|
|
5383
|
+
`UPDATE refs SET to_id = (
|
|
5384
|
+
SELECT sym.id FROM symbols sym
|
|
5385
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5386
|
+
ORDER BY sym.id LIMIT 1
|
|
5387
|
+
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
5388
|
+
AND EXISTS (
|
|
5389
|
+
SELECT 1 FROM symbols sym
|
|
5390
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5391
|
+
)`
|
|
5392
|
+
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
5393
|
+
return result.changes ?? 0;
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVars, resolutions) {
|
|
5397
|
+
if (resolutions.length === 0) return 0;
|
|
5398
|
+
return runWithRetry(() => {
|
|
5399
|
+
db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
5400
|
+
db.exec(
|
|
5401
|
+
`CREATE TEMP TABLE import_resolution (
|
|
5402
|
+
from_file TEXT NOT NULL,
|
|
5403
|
+
lang TEXT NOT NULL,
|
|
5404
|
+
module TEXT NOT NULL,
|
|
5405
|
+
to_file TEXT NOT NULL
|
|
5406
|
+
)`
|
|
5407
|
+
);
|
|
5408
|
+
const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
|
|
5409
|
+
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
5410
|
+
const chunk = resolutions.slice(i, i + chunkSize);
|
|
5411
|
+
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
5412
|
+
const binds = [];
|
|
5413
|
+
for (const entry of chunk) {
|
|
5414
|
+
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
5415
|
+
}
|
|
5416
|
+
stmtFn(
|
|
5417
|
+
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
5418
|
+
VALUES ${placeholders}`
|
|
5419
|
+
).run(...binds);
|
|
5420
|
+
}
|
|
5421
|
+
db.exec(
|
|
5422
|
+
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
5423
|
+
ON import_resolution(module, lang, from_file)`
|
|
5424
|
+
);
|
|
5425
|
+
const result = stmtFn(
|
|
5426
|
+
`UPDATE refs
|
|
5427
|
+
SET to_file = (
|
|
5428
|
+
SELECT ir.to_file
|
|
5429
|
+
FROM temp.import_resolution ir
|
|
5430
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
5431
|
+
WHERE ir.module = refs.module
|
|
5432
|
+
AND ir.lang = refs.lang
|
|
5433
|
+
AND ir.from_file = s.file
|
|
5434
|
+
LIMIT 1
|
|
5435
|
+
)
|
|
5436
|
+
WHERE refs.call_type = 'import'
|
|
5437
|
+
AND refs.module IS NOT NULL
|
|
5438
|
+
AND EXISTS (
|
|
5439
|
+
SELECT 1
|
|
5440
|
+
FROM temp.import_resolution ir
|
|
5441
|
+
JOIN symbols s ON s.id = refs.from_id
|
|
5442
|
+
WHERE ir.module = refs.module
|
|
5443
|
+
AND ir.lang = refs.lang
|
|
5444
|
+
AND ir.from_file = s.file
|
|
5445
|
+
)`
|
|
5446
|
+
).run();
|
|
5447
|
+
db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
5448
|
+
return result.changes ?? 0;
|
|
5449
|
+
});
|
|
5450
|
+
}
|
|
5451
|
+
function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
|
|
5452
|
+
const list = [...names].filter((name) => name.length > 0);
|
|
5453
|
+
if (list.length === 0) return 0;
|
|
5454
|
+
let total = 0;
|
|
5455
|
+
for (let i = 0; i < list.length; i += maxSqlVars) {
|
|
5456
|
+
const chunk = list.slice(i, i + maxSqlVars);
|
|
5457
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
5458
|
+
try {
|
|
5459
|
+
const result = stmtFn(
|
|
5460
|
+
`UPDATE refs
|
|
5461
|
+
SET to_id = s.id
|
|
5462
|
+
FROM (
|
|
5463
|
+
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
5464
|
+
FROM symbols sym
|
|
5465
|
+
JOIN lang_family lf ON lf.lang = sym.lang
|
|
5466
|
+
WHERE sym.name IN (${placeholders})
|
|
5467
|
+
GROUP BY sym.name, lf.family
|
|
5468
|
+
UNION ALL
|
|
5469
|
+
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
5470
|
+
FROM symbols sym
|
|
5471
|
+
WHERE sym.name IN (${placeholders})
|
|
5472
|
+
GROUP BY sym.name
|
|
5473
|
+
) AS s,
|
|
5474
|
+
lang_family AS rf
|
|
5475
|
+
WHERE refs.to_name IN (${placeholders})
|
|
5476
|
+
AND rf.lang = refs.lang
|
|
5477
|
+
AND s.name = refs.to_name
|
|
5478
|
+
AND s.family = rf.family`
|
|
5479
|
+
).run(...chunk, ...chunk, ...chunk);
|
|
5480
|
+
total += result.changes ?? 0;
|
|
5481
|
+
} catch {
|
|
5482
|
+
const result = stmtFn(
|
|
5483
|
+
`UPDATE refs SET to_id = (
|
|
5484
|
+
SELECT sym.id FROM symbols sym
|
|
5485
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5486
|
+
ORDER BY sym.id LIMIT 1
|
|
5487
|
+
) WHERE refs.to_name IN (${placeholders})
|
|
5488
|
+
AND EXISTS (
|
|
5489
|
+
SELECT 1 FROM symbols sym
|
|
5490
|
+
WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
|
|
5491
|
+
)`
|
|
5492
|
+
).run(LANG_FAMILY_WILDCARD, ...chunk, LANG_FAMILY_WILDCARD);
|
|
5493
|
+
total += result.changes ?? 0;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
return total;
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
// src/codebase-index/writer-search.ts
|
|
5500
|
+
import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
|
|
5501
|
+
|
|
5502
|
+
// src/codebase-index/lsp-kind.ts
|
|
5503
|
+
function lspKindToInternalKind(k) {
|
|
5504
|
+
switch (k) {
|
|
5505
|
+
case 5 /* Class */:
|
|
5506
|
+
return "class";
|
|
5507
|
+
case 6 /* Method */:
|
|
5508
|
+
return "method";
|
|
5509
|
+
case 7 /* Property */:
|
|
5510
|
+
case 8 /* Field */:
|
|
5511
|
+
return "property";
|
|
5512
|
+
case 9 /* Constructor */:
|
|
5513
|
+
return "class";
|
|
5514
|
+
case 10 /* Enum */:
|
|
5515
|
+
return "enum";
|
|
5516
|
+
case 11 /* Interface */:
|
|
5517
|
+
return "interface";
|
|
5518
|
+
case 12 /* Function */:
|
|
5519
|
+
return "function";
|
|
5520
|
+
case 13 /* Variable */:
|
|
5521
|
+
return "var";
|
|
5522
|
+
case 14 /* Constant */:
|
|
5523
|
+
return "const";
|
|
5524
|
+
case 22 /* EnumMember */:
|
|
5525
|
+
return "enum";
|
|
5526
|
+
case 26 /* TypeParameter */:
|
|
5527
|
+
return "type";
|
|
5528
|
+
case 3 /* Namespace */:
|
|
5529
|
+
return "namespace";
|
|
5530
|
+
default:
|
|
5531
|
+
return null;
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
// src/codebase-index/writer-search-helpers.ts
|
|
5536
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
5537
|
+
function normalizeSearchLimit(limit) {
|
|
5538
|
+
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
5539
|
+
}
|
|
5540
|
+
function buildWriterSearchWhere(query, filter) {
|
|
5541
|
+
const conditions = [];
|
|
5542
|
+
const values = [];
|
|
5543
|
+
let effectiveKind = filter?.kind;
|
|
5544
|
+
if (filter?.lspKind !== void 0) {
|
|
5545
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
5546
|
+
if (mapped !== null) {
|
|
5547
|
+
effectiveKind = mapped;
|
|
5548
|
+
} else {
|
|
5549
|
+
return null;
|
|
5550
|
+
}
|
|
5198
5551
|
}
|
|
5199
5552
|
if (effectiveKind) {
|
|
5200
5553
|
conditions.push("kind = ?");
|
|
@@ -5232,6 +5585,173 @@ function mapWriterSearchRow(row, lspKind, score = 0, snippet = "") {
|
|
|
5232
5585
|
};
|
|
5233
5586
|
}
|
|
5234
5587
|
|
|
5588
|
+
// src/codebase-index/writer-search.ts
|
|
5589
|
+
function searchWithStatement(stmtFn, query, filter, opts) {
|
|
5590
|
+
const built = buildWriterSearchWhere(query, filter);
|
|
5591
|
+
if (built === null) return [];
|
|
5592
|
+
const { where, values } = built;
|
|
5593
|
+
const limit = normalizeSearchLimit(opts?.limit);
|
|
5594
|
+
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
5595
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
5596
|
+
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
5597
|
+
const rows = stmtFn(sql).all(...binds);
|
|
5598
|
+
return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
|
|
5599
|
+
}
|
|
5600
|
+
function countSearchWithStatement(stmtFn, query, filter) {
|
|
5601
|
+
const built = buildWriterSearchWhere(query, filter);
|
|
5602
|
+
if (built === null) return 0;
|
|
5603
|
+
const row = stmtFn(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
|
|
5604
|
+
...built.values
|
|
5605
|
+
);
|
|
5606
|
+
return Number(row?.n ?? 0);
|
|
5607
|
+
}
|
|
5608
|
+
function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvailable, getOrBuildBm25, query, filter, limit) {
|
|
5609
|
+
const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
|
|
5610
|
+
const safeLimit = Math.max(1, Math.min(rawLimit, 100));
|
|
5611
|
+
const tokens = tokenise(query);
|
|
5612
|
+
if (tokens.length === 0 || !ftsAvailable) {
|
|
5613
|
+
return searchRankedFallbackWithStatement(
|
|
5614
|
+
stmtFn,
|
|
5615
|
+
searchFn,
|
|
5616
|
+
getOrBuildBm25,
|
|
5617
|
+
query,
|
|
5618
|
+
filter,
|
|
5619
|
+
safeLimit
|
|
5620
|
+
);
|
|
5621
|
+
}
|
|
5622
|
+
let effectiveKind = filter?.kind;
|
|
5623
|
+
if (filter?.lspKind !== void 0) {
|
|
5624
|
+
const mapped = lspKindToInternalKind(filter.lspKind);
|
|
5625
|
+
if (mapped === null) return { results: [], total: 0 };
|
|
5626
|
+
effectiveKind = mapped;
|
|
5627
|
+
}
|
|
5628
|
+
const longTokens = tokens.filter((t) => t.length >= 3);
|
|
5629
|
+
const shortTokens = tokens.filter((t) => t.length < 3);
|
|
5630
|
+
if (longTokens.length === 0) {
|
|
5631
|
+
return searchRankedFallbackWithStatement(
|
|
5632
|
+
stmtFn,
|
|
5633
|
+
searchFn,
|
|
5634
|
+
getOrBuildBm25,
|
|
5635
|
+
query,
|
|
5636
|
+
filter,
|
|
5637
|
+
safeLimit
|
|
5638
|
+
);
|
|
5639
|
+
}
|
|
5640
|
+
const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
5641
|
+
const conditions = ["symbols_fts MATCH ?"];
|
|
5642
|
+
const values = [match];
|
|
5643
|
+
for (const shortTok of shortTokens) {
|
|
5644
|
+
conditions.push("s.text LIKE ? ESCAPE '\\'");
|
|
5645
|
+
values.push(`%${escapeLike(shortTok)}%`);
|
|
5646
|
+
}
|
|
5647
|
+
if (effectiveKind) {
|
|
5648
|
+
conditions.push("s.kind = ?");
|
|
5649
|
+
values.push(effectiveKind);
|
|
5650
|
+
}
|
|
5651
|
+
if (filter?.lang) {
|
|
5652
|
+
conditions.push("s.lang = ?");
|
|
5653
|
+
values.push(filter.lang);
|
|
5654
|
+
}
|
|
5655
|
+
if (filter?.file) {
|
|
5656
|
+
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
5657
|
+
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
5658
|
+
}
|
|
5659
|
+
const where = conditions.join(" AND ");
|
|
5660
|
+
const countRows = stmtFn(
|
|
5661
|
+
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
5662
|
+
).all(...values);
|
|
5663
|
+
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
5664
|
+
if (total === 0) return { results: [], total: 0 };
|
|
5665
|
+
const bm25Rows = stmtFn(
|
|
5666
|
+
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
5667
|
+
-bm25(symbols_fts) AS score,
|
|
5668
|
+
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
5669
|
+
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
5670
|
+
WHERE ${where}
|
|
5671
|
+
ORDER BY
|
|
5672
|
+
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
5673
|
+
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
5674
|
+
ELSE 2 END,
|
|
5675
|
+
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
5676
|
+
LIMIT ?`
|
|
5677
|
+
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
5678
|
+
if (vectorsAvailable && bm25Rows.length > 0) {
|
|
5679
|
+
const queryVec = embedText(query);
|
|
5680
|
+
const candidateIds = bm25Rows.map((r) => r.id);
|
|
5681
|
+
const placeholders = candidateIds.map(() => "?").join(",");
|
|
5682
|
+
const vecRows = stmtFn(
|
|
5683
|
+
`SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
|
|
5684
|
+
).all(...candidateIds);
|
|
5685
|
+
const vecScores = vecRows.map((r) => ({
|
|
5686
|
+
id: r.symbol_id,
|
|
5687
|
+
sim: cosineSimilarity(queryVec, decodeVector(r.vector))
|
|
5688
|
+
})).sort((a, b) => b.sim - a.sim);
|
|
5689
|
+
const bm25Rank = /* @__PURE__ */ new Map();
|
|
5690
|
+
bm25Rows.forEach((r, i) => {
|
|
5691
|
+
bm25Rank.set(r.id, i);
|
|
5692
|
+
});
|
|
5693
|
+
const vecRank = /* @__PURE__ */ new Map();
|
|
5694
|
+
vecScores.forEach((r, i) => {
|
|
5695
|
+
vecRank.set(r.id, i);
|
|
5696
|
+
});
|
|
5697
|
+
const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
|
|
5698
|
+
const fusedScore = new Map(fused);
|
|
5699
|
+
const sorted = [...bm25Rows].sort(
|
|
5700
|
+
(a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
|
|
5701
|
+
);
|
|
5702
|
+
return {
|
|
5703
|
+
results: sorted.map(
|
|
5704
|
+
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
5705
|
+
),
|
|
5706
|
+
total
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
return {
|
|
5710
|
+
results: bm25Rows.map(
|
|
5711
|
+
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
5712
|
+
),
|
|
5713
|
+
total
|
|
5714
|
+
};
|
|
5715
|
+
}
|
|
5716
|
+
function searchRankedFallbackWithStatement(stmtFn, searchFn, getOrBuildBm25, query, filter, limit) {
|
|
5717
|
+
if (!query.trim()) {
|
|
5718
|
+
const total2 = countSearchWithStatement(stmtFn, query, filter);
|
|
5719
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
5720
|
+
return { results: searchFn(query, filter, { limit }), total: total2 };
|
|
5721
|
+
}
|
|
5722
|
+
const total = countSearchWithStatement(stmtFn, query, filter);
|
|
5723
|
+
if (total === 0) return { results: [], total: 0 };
|
|
5724
|
+
const candidates = searchFn(query, filter, {
|
|
5725
|
+
limit: SEARCH_CANDIDATE_SCAN_CAP
|
|
5726
|
+
});
|
|
5727
|
+
if (candidates.length === 0) return { results: [], total: 0 };
|
|
5728
|
+
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
5729
|
+
const bm25 = getOrBuildBm25();
|
|
5730
|
+
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
5731
|
+
const q = query.trim().toLowerCase();
|
|
5732
|
+
const rank = (id) => {
|
|
5733
|
+
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
5734
|
+
if (name === q) return 0;
|
|
5735
|
+
if (name.startsWith(q)) return 1;
|
|
5736
|
+
return 2;
|
|
5737
|
+
};
|
|
5738
|
+
scored.sort((a, b) => {
|
|
5739
|
+
const rankDiff = rank(a.id) - rank(b.id);
|
|
5740
|
+
if (rankDiff !== 0) return rankDiff;
|
|
5741
|
+
const scoreDiff = b.score - a.score;
|
|
5742
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
5743
|
+
const left = expectDefined4(candidateById.get(a.id));
|
|
5744
|
+
const right = expectDefined4(candidateById.get(b.id));
|
|
5745
|
+
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
5746
|
+
});
|
|
5747
|
+
const qTokens = tokenise(query);
|
|
5748
|
+
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
5749
|
+
const c = expectDefined4(candidateById.get(id));
|
|
5750
|
+
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
5751
|
+
});
|
|
5752
|
+
return { results, total };
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5235
5755
|
// src/codebase-index/writer-store-pool.ts
|
|
5236
5756
|
var DEFAULT_MAX_WARM_STORES = 2;
|
|
5237
5757
|
var StorePool = class {
|
|
@@ -5323,68 +5843,14 @@ var DB_FILE2 = "index.db";
|
|
|
5323
5843
|
var MAX_STATEMENT_CACHE = 128;
|
|
5324
5844
|
var IndexStore = class _IndexStore {
|
|
5325
5845
|
db;
|
|
5326
|
-
/**
|
|
5327
|
-
* True while an index run owns one outer SQLite transaction. Individual
|
|
5328
|
-
* writer methods normally protect themselves with BEGIN/COMMIT, but during
|
|
5329
|
-
* a refresh they join this transaction so readers observe either the last
|
|
5330
|
-
* completed index or the next completed index, never an in-between batch.
|
|
5331
|
-
*/
|
|
5332
5846
|
atomicIndexUpdateActive = false;
|
|
5333
5847
|
writeSavepointSequence = 0;
|
|
5334
|
-
/** Absolute path to this project's index directory. */
|
|
5335
5848
|
indexDir;
|
|
5336
|
-
/**
|
|
5337
|
-
* True when the SQLite build provides FTS5 (Node's bundled SQLite does).
|
|
5338
|
-
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
5339
|
-
*/
|
|
5340
5849
|
ftsAvailable = false;
|
|
5341
|
-
/**
|
|
5342
|
-
* Phase 3: true when the `symbol_vectors` table was created successfully.
|
|
5343
|
-
* When false, hybrid search skips the vector pass and falls back to FTS5
|
|
5344
|
-
* (or LIKE) only.
|
|
5345
|
-
*/
|
|
5346
5850
|
vectorsAvailable = false;
|
|
5347
|
-
/**
|
|
5348
|
-
* Cache of prepared statements keyed by their SQL text. `DatabaseSync`
|
|
5349
|
-
* compiles SQL on every `.prepare()` call; for the fixed-SQL methods
|
|
5350
|
-
* (upsertFile, getFileMeta, deleteFile, insertRefs, …) that runs thousands
|
|
5351
|
-
* of times during a full reindex. `StatementSync` objects are reusable
|
|
5352
|
-
* across calls on the same connection, so we compile each distinct SQL once
|
|
5353
|
-
* and reuse it. Cleared in {@link close} when the connection is torn down.
|
|
5354
|
-
*/
|
|
5355
5851
|
stmtCache = /* @__PURE__ */ new Map();
|
|
5356
|
-
/**
|
|
5357
|
-
* Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
|
|
5358
|
-
* Built lazily on the first `searchRankedFallback` call and invalidated
|
|
5359
|
-
* (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
|
|
5360
|
-
* IDF over the full corpus is also more correct than the old per-query
|
|
5361
|
-
* candidate-subset IDF.
|
|
5362
|
-
*
|
|
5363
|
-
* Cache-lifecycle invariants (single source of truth lives at the
|
|
5364
|
-
* `invalidateBm25()` helper — see its docblock for the "every mutation
|
|
5365
|
-
* MUST call this" contract):
|
|
5366
|
-
* - declaration: this field + `bm25Dirty` (here)
|
|
5367
|
-
* - invalidation: `invalidateBm25()` flips the flag and nulls the cache
|
|
5368
|
-
* - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
|
|
5369
|
-
* - teardown: `close()` resets the flag and nulls the cache
|
|
5370
|
-
*/
|
|
5371
5852
|
bm25Cache = null;
|
|
5372
|
-
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
5373
|
-
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
5374
5853
|
bm25Dirty = true;
|
|
5375
|
-
/**
|
|
5376
|
-
* Prepare-once helper: compile `sql` on first use, reuse thereafter.
|
|
5377
|
-
*
|
|
5378
|
-
* Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
|
|
5379
|
-
* the fallback search builder emits one `text LIKE ?` clause per query token
|
|
5380
|
-
* — so the SQL varies with the token count and a stream of differently-sized
|
|
5381
|
-
* queries grew the cache without limit. Sage's store already bounds its
|
|
5382
|
-
* equivalent at 128 (WS-096).
|
|
5383
|
-
*
|
|
5384
|
-
* Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
|
|
5385
|
-
* insertRefs, …) at the young end, so a burst of one-off search SQL evicts
|
|
5386
|
-
* itself rather than the reindex hot path.
|
|
5387
|
-
*/
|
|
5388
5854
|
stmt(sql) {
|
|
5389
5855
|
const cached = this.stmtCache.get(sql);
|
|
5390
5856
|
if (cached !== void 0) {
|
|
@@ -5411,7 +5877,6 @@ var IndexStore = class _IndexStore {
|
|
|
5411
5877
|
runWithRetry(fn) {
|
|
5412
5878
|
return runSqliteWithRetry(fn);
|
|
5413
5879
|
}
|
|
5414
|
-
/** Run a complete index mutation as one WAL-visible publication. */
|
|
5415
5880
|
async runAtomicIndexUpdate(job) {
|
|
5416
5881
|
if (this.atomicIndexUpdateActive) return job();
|
|
5417
5882
|
this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
|
|
@@ -5430,11 +5895,6 @@ var IndexStore = class _IndexStore {
|
|
|
5430
5895
|
this.atomicIndexUpdateActive = false;
|
|
5431
5896
|
}
|
|
5432
5897
|
}
|
|
5433
|
-
/**
|
|
5434
|
-
* Begin a method-local transaction. Inside an atomic index publication a
|
|
5435
|
-
* SAVEPOINT preserves the old per-batch rollback boundary, which is needed
|
|
5436
|
-
* when commitBatch falls back to per-file writes after one batch fails.
|
|
5437
|
-
*/
|
|
5438
5898
|
beginWriteTransaction() {
|
|
5439
5899
|
if (this.atomicIndexUpdateActive) {
|
|
5440
5900
|
const savepoint = `index_write_${++this.writeSavepointSequence}`;
|
|
@@ -5456,35 +5916,11 @@ var IndexStore = class _IndexStore {
|
|
|
5456
5916
|
this.db.exec("ROLLBACK");
|
|
5457
5917
|
}
|
|
5458
5918
|
}
|
|
5459
|
-
/**
|
|
5460
|
-
* Mirror the in-process language→family map into SQLite.
|
|
5461
|
-
*
|
|
5462
|
-
* Rewritten on every open rather than only on schema bumps: the mapping is
|
|
5463
|
-
* static lookup data, so a code-side change (a new language, a language
|
|
5464
|
-
* moving families) must take effect without forcing a full reindex.
|
|
5465
|
-
*/
|
|
5466
5919
|
seedLangFamilies() {
|
|
5467
5920
|
const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
|
|
5468
5921
|
for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
|
|
5469
5922
|
insert.run("", LANG_FAMILY_WILDCARD);
|
|
5470
5923
|
}
|
|
5471
|
-
/**
|
|
5472
|
-
* Add any column the current schema expects but the on-disk table lacks.
|
|
5473
|
-
*
|
|
5474
|
-
* `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
|
|
5475
|
-
* and the version check above only rebuilds on a version *mismatch*. That
|
|
5476
|
-
* leaves a real gap: several wstack processes share this database, and while
|
|
5477
|
-
* a version upgrade is rolling out one of them may still be running the
|
|
5478
|
-
* previous build. That older process sees the newer version number, drops the
|
|
5479
|
-
* tables, and recreates them from *its* DDL — without the newer columns —
|
|
5480
|
-
* while the metadata row still reads the new version. Every later query for
|
|
5481
|
-
* one of those columns then fails with `no such column`, and no amount of
|
|
5482
|
-
* reindexing fixes it, because the version numbers already agree.
|
|
5483
|
-
*
|
|
5484
|
-
* Repairing column-by-column makes the schema self-healing from any of those
|
|
5485
|
-
* states. Table and column names are compile-time literals from this module,
|
|
5486
|
-
* never user input.
|
|
5487
|
-
*/
|
|
5488
5924
|
repairMissingColumns() {
|
|
5489
5925
|
const expected = [
|
|
5490
5926
|
{
|
|
@@ -5589,27 +6025,8 @@ var IndexStore = class _IndexStore {
|
|
|
5589
6025
|
}
|
|
5590
6026
|
this.ensureNextSymbolIdSeeded();
|
|
5591
6027
|
}
|
|
5592
|
-
// ─── ID allocation & bulk helpers ────────────────────────────────────────────
|
|
5593
6028
|
static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
|
|
5594
|
-
/** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
|
|
5595
6029
|
static MAX_SQL_VARS = 900;
|
|
5596
|
-
/**
|
|
5597
|
-
* Correlated predicate: the ref in `refs` and the candidate symbol aliased
|
|
5598
|
-
* `sym` belong to the same language family — or the ref carries no language,
|
|
5599
|
-
* in which case the wildcard bind matches everything.
|
|
5600
|
-
*
|
|
5601
|
-
* Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
|
|
5602
|
-
*/
|
|
5603
|
-
static FAMILY_MATCH_SQL = `(
|
|
5604
|
-
(SELECT family FROM lang_family WHERE lang = refs.lang) = ?
|
|
5605
|
-
OR (SELECT family FROM lang_family WHERE lang = sym.lang)
|
|
5606
|
-
= (SELECT family FROM lang_family WHERE lang = refs.lang)
|
|
5607
|
-
)`;
|
|
5608
|
-
/**
|
|
5609
|
-
* Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
|
|
5610
|
-
* transaction on open; the first concurrent writer under BEGIN IMMEDIATE
|
|
5611
|
-
* re-reads and advances the counter atomically.
|
|
5612
|
-
*/
|
|
5613
6030
|
ensureNextSymbolIdSeeded() {
|
|
5614
6031
|
const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
|
|
5615
6032
|
_IndexStore.NEXT_SYMBOL_ID_KEY
|
|
@@ -5622,10 +6039,6 @@ var IndexStore = class _IndexStore {
|
|
|
5622
6039
|
String(next)
|
|
5623
6040
|
);
|
|
5624
6041
|
}
|
|
5625
|
-
/**
|
|
5626
|
-
* Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
|
|
5627
|
-
* so concurrent indexers cannot hand out overlapping ranges.
|
|
5628
|
-
*/
|
|
5629
6042
|
allocateSymbolIds(count) {
|
|
5630
6043
|
if (count <= 0) return this.getMaxSymbolId() + 1;
|
|
5631
6044
|
this.ensureNextSymbolIdSeeded();
|
|
@@ -5639,16 +6052,8 @@ var IndexStore = class _IndexStore {
|
|
|
5639
6052
|
);
|
|
5640
6053
|
return start;
|
|
5641
6054
|
}
|
|
5642
|
-
/**
|
|
5643
|
-
* Disconnect inbound refs before their target symbols are replaced and
|
|
5644
|
-
* return the affected names for scoped re-resolution.
|
|
5645
|
-
*
|
|
5646
|
-
* This also repairs a long-standing dangling-id edge case: `refs.to_id` has
|
|
5647
|
-
* no physical FK, so deleting a symbol previously left callers pointing at a
|
|
5648
|
-
* non-existent row.
|
|
5649
|
-
*/
|
|
5650
6055
|
invalidateIncomingRefsForFiles(files) {
|
|
5651
|
-
if (files.length === 0) return
|
|
6056
|
+
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
5652
6057
|
const placeholders = files.map(() => "?").join(",");
|
|
5653
6058
|
const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
|
|
5654
6059
|
...files
|
|
@@ -5657,36 +6062,11 @@ var IndexStore = class _IndexStore {
|
|
|
5657
6062
|
`UPDATE refs SET to_id = NULL
|
|
5658
6063
|
WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
5659
6064
|
).run(...files);
|
|
5660
|
-
return names;
|
|
6065
|
+
return new Set(names);
|
|
5661
6066
|
}
|
|
5662
|
-
/** Resolve only refs whose target names may have changed. */
|
|
5663
6067
|
resolveRefsForNamesUnsafe(names) {
|
|
5664
|
-
|
|
5665
|
-
let changes = 0;
|
|
5666
|
-
for (let start = 0; start < unique.length; start += _IndexStore.MAX_SQL_VARS) {
|
|
5667
|
-
const chunk = unique.slice(start, start + _IndexStore.MAX_SQL_VARS);
|
|
5668
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
5669
|
-
const result = this.stmt(
|
|
5670
|
-
`UPDATE refs
|
|
5671
|
-
SET to_id = (
|
|
5672
|
-
SELECT MIN(sym.id) FROM symbols sym
|
|
5673
|
-
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
5674
|
-
)
|
|
5675
|
-
WHERE to_name IN (${placeholders})`
|
|
5676
|
-
).run(LANG_FAMILY_WILDCARD, ...chunk);
|
|
5677
|
-
changes += result.changes ?? 0;
|
|
5678
|
-
}
|
|
5679
|
-
return changes;
|
|
6068
|
+
return resolveRefsForNamesUnsafe((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, names);
|
|
5680
6069
|
}
|
|
5681
|
-
// ─── Symbol CRUD ─────────────────────────────────────────────────────────────
|
|
5682
|
-
/**
|
|
5683
|
-
* Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
|
|
5684
|
-
* `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
|
|
5685
|
-
* (O(1)); multi-row INSERT amortizes bind overhead for large files.
|
|
5686
|
-
*
|
|
5687
|
-
* @returns The symbols array with `id` fields populated so the caller can
|
|
5688
|
-
* use them for refs without re-reading from the DB.
|
|
5689
|
-
*/
|
|
5690
6070
|
insertSymbols(symbols) {
|
|
5691
6071
|
this.invalidateBm25();
|
|
5692
6072
|
return this.runWithRetry(() => {
|
|
@@ -5715,12 +6095,6 @@ var IndexStore = class _IndexStore {
|
|
|
5715
6095
|
if (this.ftsAvailable) {
|
|
5716
6096
|
ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
|
|
5717
6097
|
}
|
|
5718
|
-
vectorRows.push({
|
|
5719
|
-
id,
|
|
5720
|
-
vector: encodeVector(
|
|
5721
|
-
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
5722
|
-
)
|
|
5723
|
-
});
|
|
5724
6098
|
result.push({ ...s, id });
|
|
5725
6099
|
}
|
|
5726
6100
|
bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
|
|
@@ -5770,11 +6144,6 @@ var IndexStore = class _IndexStore {
|
|
|
5770
6144
|
}
|
|
5771
6145
|
});
|
|
5772
6146
|
}
|
|
5773
|
-
/**
|
|
5774
|
-
* Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
|
|
5775
|
-
* when a source file disappears between index runs — previously this only
|
|
5776
|
-
* dropped the `files` row, leaving its symbols orphaned but still searchable.
|
|
5777
|
-
*/
|
|
5778
6147
|
deleteFile(file) {
|
|
5779
6148
|
this.invalidateBm25();
|
|
5780
6149
|
this.runWithRetry(() => {
|
|
@@ -5804,7 +6173,6 @@ var IndexStore = class _IndexStore {
|
|
|
5804
6173
|
}
|
|
5805
6174
|
});
|
|
5806
6175
|
}
|
|
5807
|
-
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
5808
6176
|
upsertFile(meta) {
|
|
5809
6177
|
this.runWithRetry(() => {
|
|
5810
6178
|
this.stmt(
|
|
@@ -5832,8 +6200,6 @@ var IndexStore = class _IndexStore {
|
|
|
5832
6200
|
getAllFileMetas() {
|
|
5833
6201
|
return getAllFileMetasWithStatement((sql) => this.stmt(sql));
|
|
5834
6202
|
}
|
|
5835
|
-
// ─── Project structure & module resolution ──────────────────────────────────
|
|
5836
|
-
/** Store the Code Atlas grouping label for each indexed file. */
|
|
5837
6203
|
setFilePackages(entries) {
|
|
5838
6204
|
if (entries.size === 0) return;
|
|
5839
6205
|
this.runWithRetry(() => {
|
|
@@ -5841,272 +6207,50 @@ var IndexStore = class _IndexStore {
|
|
|
5841
6207
|
for (const [file, label] of entries) update.run(label, file);
|
|
5842
6208
|
});
|
|
5843
6209
|
}
|
|
5844
|
-
/**
|
|
5845
|
-
* Every indexed `namespace`/`module` declaration, for ecosystems whose import
|
|
5846
|
-
* specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
|
|
5847
|
-
* Ordered so the resolver's choice among duplicate declarations is stable.
|
|
5848
|
-
*/
|
|
5849
6210
|
getNamespaceDeclarations() {
|
|
5850
|
-
return this.stmt(
|
|
5851
|
-
`SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
|
|
5852
|
-
).all();
|
|
6211
|
+
return getNamespaceDeclarationsWithStatement((sql) => this.stmt(sql));
|
|
5853
6212
|
}
|
|
5854
|
-
/** `file → package` for every indexed file that has a label. */
|
|
5855
6213
|
getFilePackages() {
|
|
5856
|
-
|
|
5857
|
-
return new Map(rows.map((row) => [row.file, row.package]));
|
|
6214
|
+
return getFilePackagesWithStatement((sql) => this.stmt(sql));
|
|
5858
6215
|
}
|
|
5859
|
-
/**
|
|
5860
|
-
* Distinct `(fromFile, lang, module)` triples needing module resolution.
|
|
5861
|
-
*
|
|
5862
|
-
* Distinct rather than per-ref because resolution depends only on these three
|
|
5863
|
-
* values: a file importing the same module twenty times resolves it once.
|
|
5864
|
-
*/
|
|
5865
6216
|
getUnresolvedImports(onlyFiles) {
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
return this.stmt(base).all();
|
|
5872
|
-
}
|
|
5873
|
-
const out = [];
|
|
5874
|
-
for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
|
|
5875
|
-
const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
|
|
5876
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
5877
|
-
out.push(
|
|
5878
|
-
...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
|
|
5879
|
-
);
|
|
5880
|
-
}
|
|
5881
|
-
return out;
|
|
6217
|
+
return getUnresolvedImportsWithStatement(
|
|
6218
|
+
(sql) => this.stmt(sql),
|
|
6219
|
+
_IndexStore.MAX_SQL_VARS,
|
|
6220
|
+
onlyFiles
|
|
6221
|
+
);
|
|
5882
6222
|
}
|
|
5883
|
-
/**
|
|
5884
|
-
* Write resolved import targets back onto `refs.to_file`.
|
|
5885
|
-
*
|
|
5886
|
-
* Applied through a temp table and a single UPDATE: one statement per
|
|
5887
|
-
* resolution would mean thousands of round-trips on a first index.
|
|
5888
|
-
*/
|
|
5889
6223
|
applyImportResolutions(resolutions) {
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
this.
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
lang TEXT NOT NULL,
|
|
5897
|
-
module TEXT NOT NULL,
|
|
5898
|
-
to_file TEXT NOT NULL
|
|
5899
|
-
)`
|
|
5900
|
-
);
|
|
5901
|
-
const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
|
|
5902
|
-
for (let i = 0; i < resolutions.length; i += chunkSize) {
|
|
5903
|
-
const chunk = resolutions.slice(i, i + chunkSize);
|
|
5904
|
-
const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
|
|
5905
|
-
const binds = [];
|
|
5906
|
-
for (const entry of chunk) {
|
|
5907
|
-
binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
|
|
5908
|
-
}
|
|
5909
|
-
this.stmt(
|
|
5910
|
-
`INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
|
|
5911
|
-
VALUES ${placeholders}`
|
|
5912
|
-
).run(...binds);
|
|
5913
|
-
}
|
|
5914
|
-
this.db.exec(
|
|
5915
|
-
`CREATE INDEX IF NOT EXISTS temp.idx_ir
|
|
5916
|
-
ON import_resolution(module, lang, from_file)`
|
|
5917
|
-
);
|
|
5918
|
-
const result = this.stmt(
|
|
5919
|
-
`UPDATE refs
|
|
5920
|
-
SET to_file = (
|
|
5921
|
-
SELECT ir.to_file
|
|
5922
|
-
FROM temp.import_resolution ir
|
|
5923
|
-
JOIN symbols s ON s.id = refs.from_id
|
|
5924
|
-
WHERE ir.module = refs.module
|
|
5925
|
-
AND ir.lang = refs.lang
|
|
5926
|
-
AND ir.from_file = s.file
|
|
5927
|
-
LIMIT 1
|
|
5928
|
-
)
|
|
5929
|
-
WHERE refs.call_type = 'import'
|
|
5930
|
-
AND refs.module IS NOT NULL
|
|
5931
|
-
AND EXISTS (
|
|
5932
|
-
SELECT 1
|
|
5933
|
-
FROM temp.import_resolution ir
|
|
5934
|
-
JOIN symbols s ON s.id = refs.from_id
|
|
5935
|
-
WHERE ir.module = refs.module
|
|
5936
|
-
AND ir.lang = refs.lang
|
|
5937
|
-
AND ir.from_file = s.file
|
|
5938
|
-
)`
|
|
5939
|
-
).run();
|
|
5940
|
-
this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
|
|
5941
|
-
return result.changes ?? 0;
|
|
5942
|
-
});
|
|
5943
|
-
}
|
|
5944
|
-
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
5945
|
-
search(query, filter, opts) {
|
|
5946
|
-
const built = this.buildSearchWhere(query, filter);
|
|
5947
|
-
if (built === null) return [];
|
|
5948
|
-
const { where, values } = built;
|
|
5949
|
-
const limit = normalizeSearchLimit(opts?.limit);
|
|
5950
|
-
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
5951
|
-
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
5952
|
-
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
5953
|
-
const rows = this.stmt(sql).all(
|
|
5954
|
-
...binds
|
|
6224
|
+
return applyImportResolutionsWithStatement(
|
|
6225
|
+
this.db,
|
|
6226
|
+
(sql) => this.stmt(sql),
|
|
6227
|
+
this.runWithRetry.bind(this),
|
|
6228
|
+
_IndexStore.MAX_SQL_VARS,
|
|
6229
|
+
resolutions
|
|
5955
6230
|
);
|
|
5956
|
-
return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
|
|
5957
6231
|
}
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
return buildWriterSearchWhere(query, filter);
|
|
6232
|
+
search(query, filter, opts) {
|
|
6233
|
+
return searchWithStatement((sql) => this.stmt(sql), query, filter, opts);
|
|
5961
6234
|
}
|
|
5962
6235
|
countSearch(query, filter) {
|
|
5963
|
-
|
|
5964
|
-
if (built === null) return 0;
|
|
5965
|
-
const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
|
|
5966
|
-
...built.values
|
|
5967
|
-
);
|
|
5968
|
-
return Number(row?.n ?? 0);
|
|
6236
|
+
return countSearchWithStatement((sql) => this.stmt(sql), query, filter);
|
|
5969
6237
|
}
|
|
5970
|
-
/**
|
|
5971
|
-
* Ranked search — the one-stop query the codebase-search tool and plug-lsp
|
|
5972
|
-
* use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
|
|
5973
|
-
* `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
|
|
5974
|
-
* legacy LIKE scan + in-process BM25 (identical semantics, slower).
|
|
5975
|
-
*
|
|
5976
|
-
* Tokens are matched as prefixes (`"tok"*`), mirroring the old
|
|
5977
|
-
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
5978
|
-
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
5979
|
-
*/
|
|
5980
6238
|
searchRanked(query, filter, limit) {
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
effectiveKind = mapped;
|
|
5992
|
-
}
|
|
5993
|
-
const longTokens = tokens.filter((t) => t.length >= 3);
|
|
5994
|
-
const shortTokens = tokens.filter((t) => t.length < 3);
|
|
5995
|
-
if (longTokens.length === 0) {
|
|
5996
|
-
return this.searchRankedFallback(query, filter, safeLimit);
|
|
5997
|
-
}
|
|
5998
|
-
const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
|
|
5999
|
-
const conditions = ["symbols_fts MATCH ?"];
|
|
6000
|
-
const values = [match];
|
|
6001
|
-
for (const shortTok of shortTokens) {
|
|
6002
|
-
conditions.push("s.text LIKE ? ESCAPE '\\'");
|
|
6003
|
-
values.push(`%${escapeLike(shortTok)}%`);
|
|
6004
|
-
}
|
|
6005
|
-
if (effectiveKind) {
|
|
6006
|
-
conditions.push("s.kind = ?");
|
|
6007
|
-
values.push(effectiveKind);
|
|
6008
|
-
}
|
|
6009
|
-
if (filter?.lang) {
|
|
6010
|
-
conditions.push("s.lang = ?");
|
|
6011
|
-
values.push(filter.lang);
|
|
6012
|
-
}
|
|
6013
|
-
if (filter?.file) {
|
|
6014
|
-
conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
|
|
6015
|
-
values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
|
|
6016
|
-
}
|
|
6017
|
-
const where = conditions.join(" AND ");
|
|
6018
|
-
const countRows = this.stmt(
|
|
6019
|
-
`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
|
|
6020
|
-
).all(...values);
|
|
6021
|
-
const total = countRows[0] ? Number(countRows[0].n) : 0;
|
|
6022
|
-
if (total === 0) return { results: [], total: 0 };
|
|
6023
|
-
const bm25Rows = this.stmt(
|
|
6024
|
-
`SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
|
|
6025
|
-
-bm25(symbols_fts) AS score,
|
|
6026
|
-
snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
|
|
6027
|
-
FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
|
|
6028
|
-
WHERE ${where}
|
|
6029
|
-
ORDER BY
|
|
6030
|
-
CASE WHEN lower(s.name) = lower(?) THEN 0
|
|
6031
|
-
WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
|
|
6032
|
-
ELSE 2 END,
|
|
6033
|
-
bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
|
|
6034
|
-
LIMIT ?`
|
|
6035
|
-
).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
|
|
6036
|
-
if (this.vectorsAvailable && bm25Rows.length > 0) {
|
|
6037
|
-
const queryVec = embedText(query);
|
|
6038
|
-
const candidateIds = bm25Rows.map((r) => r.id);
|
|
6039
|
-
const placeholders = candidateIds.map(() => "?").join(",");
|
|
6040
|
-
const vecRows = this.stmt(
|
|
6041
|
-
`SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
|
|
6042
|
-
).all(...candidateIds);
|
|
6043
|
-
const vecScores = vecRows.map((r) => ({
|
|
6044
|
-
id: r.symbol_id,
|
|
6045
|
-
sim: cosineSimilarity(queryVec, decodeVector(r.vector))
|
|
6046
|
-
})).sort((a, b) => b.sim - a.sim);
|
|
6047
|
-
const bm25Rank = /* @__PURE__ */ new Map();
|
|
6048
|
-
bm25Rows.forEach((r, i) => {
|
|
6049
|
-
bm25Rank.set(r.id, i);
|
|
6050
|
-
});
|
|
6051
|
-
const vecRank = /* @__PURE__ */ new Map();
|
|
6052
|
-
vecScores.forEach((r, i) => {
|
|
6053
|
-
vecRank.set(r.id, i);
|
|
6054
|
-
});
|
|
6055
|
-
const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
|
|
6056
|
-
const fusedScore = new Map(fused);
|
|
6057
|
-
const sorted = [...bm25Rows].sort(
|
|
6058
|
-
(a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
|
|
6059
|
-
);
|
|
6060
|
-
return {
|
|
6061
|
-
results: sorted.map(
|
|
6062
|
-
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
6063
|
-
),
|
|
6064
|
-
total
|
|
6065
|
-
};
|
|
6066
|
-
}
|
|
6067
|
-
return {
|
|
6068
|
-
results: bm25Rows.map(
|
|
6069
|
-
(row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
|
|
6070
|
-
),
|
|
6071
|
-
total
|
|
6072
|
-
};
|
|
6239
|
+
return searchRankedWithStatement(
|
|
6240
|
+
(sql) => this.stmt(sql),
|
|
6241
|
+
this.search.bind(this),
|
|
6242
|
+
this.ftsAvailable,
|
|
6243
|
+
this.vectorsAvailable,
|
|
6244
|
+
this.getOrBuildBm25.bind(this),
|
|
6245
|
+
query,
|
|
6246
|
+
filter,
|
|
6247
|
+
limit
|
|
6248
|
+
);
|
|
6073
6249
|
}
|
|
6074
|
-
/**
|
|
6075
|
-
* Invalidate the cached BM25 index.
|
|
6076
|
-
*
|
|
6077
|
-
* **Contract: every method that mutates `symbols` MUST call this before
|
|
6078
|
-
* returning.** (`refs` mutations do not affect the BM25 fallback because
|
|
6079
|
-
* the corpus is built from `symbols.text` via `getAllIndexable()` and the
|
|
6080
|
-
* BM25 score is filtered by the LIKE-selected candidate set in
|
|
6081
|
-
* `searchRankedFallback`.) Today the call sites are `repairDrift`,
|
|
6082
|
-
* `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
|
|
6083
|
-
* `commitBatch`. A future mutation that adds a new write path (e.g.
|
|
6084
|
-
* `renameFile`, `updateSignature`) MUST also call this — otherwise the
|
|
6085
|
-
* FTS5-unavailable fallback will serve stale search results. The
|
|
6086
|
-
* `close()` reset at L1820-1821 tears the cache down on store shutdown,
|
|
6087
|
-
* which is the only legitimate place that flips the flag outside this
|
|
6088
|
-
* helper.
|
|
6089
|
-
*
|
|
6090
|
-
* Called *before* `runWithRetry` on purpose: if the write fails all
|
|
6091
|
-
* retries the flag stays set, forcing a rebuild on the next search rather
|
|
6092
|
-
* than trusting a cache that may not reflect the intended mutation.
|
|
6093
|
-
* Do not move this inside the retry closure.
|
|
6094
|
-
*/
|
|
6095
6250
|
invalidateBm25() {
|
|
6096
6251
|
this.bm25Dirty = true;
|
|
6097
6252
|
this.bm25Cache = null;
|
|
6098
6253
|
}
|
|
6099
|
-
/**
|
|
6100
|
-
* Return the cached full-corpus BM25 index, rebuilding it only when the
|
|
6101
|
-
* symbols table has been mutated since the last build. The full-corpus IDF
|
|
6102
|
-
* is more correct than the old per-query candidate-subset IDF, and the
|
|
6103
|
-
* amortized build cost drops from O(symbols × tokens) per search to once
|
|
6104
|
-
* per write batch.
|
|
6105
|
-
*
|
|
6106
|
-
* Note: the first call after a long idle (or on a freshly opened store)
|
|
6107
|
-
* pays the full corpus rebuild synchronously on the search path. For a
|
|
6108
|
-
* 5 500+ symbol corpus this is a visible one-time latency spike.
|
|
6109
|
-
*/
|
|
6110
6254
|
getOrBuildBm25() {
|
|
6111
6255
|
if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
|
|
6112
6256
|
const docs = this.getAllIndexable();
|
|
@@ -6114,57 +6258,12 @@ var IndexStore = class _IndexStore {
|
|
|
6114
6258
|
this.bm25Dirty = false;
|
|
6115
6259
|
return this.bm25Cache;
|
|
6116
6260
|
}
|
|
6117
|
-
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
6118
|
-
searchRankedFallback(query, filter, limit) {
|
|
6119
|
-
if (!query.trim()) {
|
|
6120
|
-
const total2 = this.countSearch(query, filter);
|
|
6121
|
-
if (total2 === 0) return { results: [], total: 0 };
|
|
6122
|
-
return { results: this.search(query, filter, { limit }), total: total2 };
|
|
6123
|
-
}
|
|
6124
|
-
const total = this.countSearch(query, filter);
|
|
6125
|
-
if (total === 0) return { results: [], total: 0 };
|
|
6126
|
-
const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
|
|
6127
|
-
if (candidates.length === 0) return { results: [], total: 0 };
|
|
6128
|
-
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
6129
|
-
const bm25 = this.getOrBuildBm25();
|
|
6130
|
-
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
6131
|
-
const q = query.trim().toLowerCase();
|
|
6132
|
-
const rank = (id) => {
|
|
6133
|
-
const name = candidateById.get(id)?.name.toLowerCase() ?? "";
|
|
6134
|
-
if (name === q) return 0;
|
|
6135
|
-
if (name.startsWith(q)) return 1;
|
|
6136
|
-
return 2;
|
|
6137
|
-
};
|
|
6138
|
-
scored.sort((a, b) => {
|
|
6139
|
-
const rankDiff = rank(a.id) - rank(b.id);
|
|
6140
|
-
if (rankDiff !== 0) return rankDiff;
|
|
6141
|
-
const scoreDiff = b.score - a.score;
|
|
6142
|
-
if (scoreDiff !== 0) return scoreDiff;
|
|
6143
|
-
const left = expectDefined4(candidateById.get(a.id));
|
|
6144
|
-
const right = expectDefined4(candidateById.get(b.id));
|
|
6145
|
-
return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
|
|
6146
|
-
});
|
|
6147
|
-
const qTokens = tokenise(query);
|
|
6148
|
-
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
6149
|
-
const c = expectDefined4(candidateById.get(id));
|
|
6150
|
-
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
6151
|
-
});
|
|
6152
|
-
return { results, total };
|
|
6153
|
-
}
|
|
6154
6261
|
getAllIndexable() {
|
|
6155
6262
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
6156
6263
|
}
|
|
6157
|
-
/**
|
|
6158
|
-
* Largest symbol id currently in the table (0 when empty). New ids must be
|
|
6159
|
-
* allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
|
|
6160
|
-
* changed file's rows, so the row count drops below the max id and a
|
|
6161
|
-
* count-based id would collide with a surviving row (UNIQUE constraint on
|
|
6162
|
-
* `symbols.id`). Ids may have gaps — that is fine.
|
|
6163
|
-
*/
|
|
6164
6264
|
getMaxSymbolId() {
|
|
6165
6265
|
return getMaxSymbolIdWithStatement((sql) => this.stmt(sql));
|
|
6166
6266
|
}
|
|
6167
|
-
// ─── Stats ───────────────────────────────────────────────────────────────────
|
|
6168
6267
|
getStats() {
|
|
6169
6268
|
return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
|
|
6170
6269
|
}
|
|
@@ -6207,11 +6306,6 @@ var IndexStore = class _IndexStore {
|
|
|
6207
6306
|
}
|
|
6208
6307
|
});
|
|
6209
6308
|
}
|
|
6210
|
-
// ─── Ref CRUD ────────────────────────────────────────────────────────────────
|
|
6211
|
-
/**
|
|
6212
|
-
* Insert cross-references for a given source symbol id.
|
|
6213
|
-
* Replaces any existing refs from the same source (idempotent on re-index).
|
|
6214
|
-
*/
|
|
6215
6309
|
insertRefs(fromId, refs) {
|
|
6216
6310
|
this.runWithRetry(() => {
|
|
6217
6311
|
this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
@@ -6223,167 +6317,36 @@ var IndexStore = class _IndexStore {
|
|
|
6223
6317
|
);
|
|
6224
6318
|
});
|
|
6225
6319
|
}
|
|
6226
|
-
/**
|
|
6227
|
-
* Bulk-insert refs for many source symbols in a single transaction.
|
|
6228
|
-
*
|
|
6229
|
-
* Unlike {@link insertRefs} this does NOT delete per source id — the caller
|
|
6230
|
-
* (the indexer) has already cleared stale refs for the file via
|
|
6231
|
-
* {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
|
|
6232
|
-
* repeated once per symbol. One transaction for the whole file instead of one
|
|
6233
|
-
* per symbol turns an O(symbols) transaction count into O(1).
|
|
6234
|
-
*
|
|
6235
|
-
* Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
|
|
6236
|
-
*/
|
|
6237
6320
|
insertRefsBatch(refs) {
|
|
6238
6321
|
if (refs.length === 0) return;
|
|
6239
6322
|
this.runWithRetry(() => {
|
|
6240
6323
|
bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refs);
|
|
6241
6324
|
});
|
|
6242
6325
|
}
|
|
6243
|
-
/**
|
|
6244
|
-
* Commit a batch of file-level symbol/refs/upserts in a single transaction.
|
|
6245
|
-
*
|
|
6246
|
-
* Used by the indexer to amortize SQLite commit overhead across many files.
|
|
6247
|
-
* Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
|
|
6248
|
-
* for symbols, plus per-file deletes and an upsertFile call), so a 20-file
|
|
6249
|
-
* parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
|
|
6250
|
-
* this entry point we do exactly one BEGIN/COMMIT per parallel batch.
|
|
6251
|
-
*
|
|
6252
|
-
* Each entry must already be a fully-parsed FileSymbols (symbols + refs).
|
|
6253
|
-
* The caller is responsible for the per-file prefix accounting
|
|
6254
|
-
* (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
|
|
6255
|
-
* the caller clear stale symbols/refs for any files being re-indexed before
|
|
6256
|
-
* the inserts run (required to keep refs → symbols FK invariants).
|
|
6257
|
-
*
|
|
6258
|
-
* Returns the symbols back with their assigned `id` (same shape as
|
|
6259
|
-
* {@link insertSymbols}) so callers can build final per-file results.
|
|
6260
|
-
*/
|
|
6261
6326
|
commitBatch(entries, options = {}) {
|
|
6262
|
-
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
6263
|
-
return [];
|
|
6264
|
-
}
|
|
6265
6327
|
this.invalidateBm25();
|
|
6266
6328
|
return this.runWithRetry(() => {
|
|
6267
6329
|
const ownsTransaction = this.beginWriteTransaction();
|
|
6268
6330
|
try {
|
|
6269
|
-
const
|
|
6270
|
-
for (const entry of entries) {
|
|
6271
|
-
for (const symbol of entry.symbols) affectedNames.add(symbol.name);
|
|
6272
|
-
for (const ref of entry.refs) affectedNames.add(ref.toName);
|
|
6273
|
-
}
|
|
6274
|
-
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
6275
|
-
const placeholders = options.deleteForFiles.map(() => "?").join(",");
|
|
6276
|
-
for (const name of this.invalidateIncomingRefsForFiles(options.deleteForFiles)) {
|
|
6277
|
-
affectedNames.add(name);
|
|
6278
|
-
}
|
|
6279
|
-
if (this.ftsAvailable) {
|
|
6280
|
-
this.stmt(
|
|
6281
|
-
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
6282
|
-
).run(...options.deleteForFiles);
|
|
6283
|
-
}
|
|
6284
|
-
if (this.vectorsAvailable) {
|
|
6285
|
-
this.stmt(
|
|
6286
|
-
`DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
6287
|
-
).run(...options.deleteForFiles);
|
|
6288
|
-
}
|
|
6289
|
-
this.stmt(
|
|
6290
|
-
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
6291
|
-
).run(...options.deleteForFiles);
|
|
6292
|
-
this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(
|
|
6293
|
-
...options.deleteForFiles
|
|
6294
|
-
);
|
|
6295
|
-
}
|
|
6296
|
-
const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
6297
|
-
let nextId = this.allocateSymbolIds(totalSymbols);
|
|
6298
|
-
const allInserted = [];
|
|
6299
|
-
const refsToInsert = [];
|
|
6300
|
-
const bulkSyms = [];
|
|
6301
|
-
const ftsRows = [];
|
|
6302
|
-
const vectorRows = [];
|
|
6303
|
-
for (const entry of entries) {
|
|
6304
|
-
const insertedForEntry = [];
|
|
6305
|
-
for (const s of entry.symbols) {
|
|
6306
|
-
const id = nextId++;
|
|
6307
|
-
bulkSyms.push({
|
|
6308
|
-
id,
|
|
6309
|
-
lang: s.lang,
|
|
6310
|
-
kind: s.kind,
|
|
6311
|
-
name: s.name,
|
|
6312
|
-
file: s.file,
|
|
6313
|
-
line: s.line,
|
|
6314
|
-
col: s.col,
|
|
6315
|
-
signature: s.signature,
|
|
6316
|
-
docComment: s.docComment,
|
|
6317
|
-
scope: s.scope,
|
|
6318
|
-
text: s.text
|
|
6319
|
-
});
|
|
6320
|
-
if (this.ftsAvailable) {
|
|
6321
|
-
ftsRows.push({
|
|
6322
|
-
id,
|
|
6323
|
-
text: buildIndexableText(s.name, s.signature, s.docComment)
|
|
6324
|
-
});
|
|
6325
|
-
}
|
|
6326
|
-
vectorRows.push({
|
|
6327
|
-
id,
|
|
6328
|
-
vector: encodeVector(
|
|
6329
|
-
embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
|
|
6330
|
-
)
|
|
6331
|
-
});
|
|
6332
|
-
const inserted = { ...s, id };
|
|
6333
|
-
allInserted.push(inserted);
|
|
6334
|
-
insertedForEntry.push(inserted);
|
|
6335
|
-
}
|
|
6336
|
-
refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
|
|
6337
|
-
}
|
|
6338
|
-
bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulkSyms);
|
|
6339
|
-
bulkInsertFtsWithStatement(
|
|
6331
|
+
const result = commitBatchWithStatement(
|
|
6340
6332
|
(sql) => this.stmt(sql),
|
|
6341
6333
|
_IndexStore.MAX_SQL_VARS,
|
|
6342
6334
|
this.ftsAvailable,
|
|
6343
|
-
|
|
6335
|
+
this.vectorsAvailable,
|
|
6336
|
+
this.allocateSymbolIds.bind(this),
|
|
6337
|
+
this.invalidateIncomingRefsForFiles.bind(this),
|
|
6338
|
+
this.resolveRefsForNamesUnsafe.bind(this),
|
|
6339
|
+
entries,
|
|
6340
|
+
options
|
|
6344
6341
|
);
|
|
6345
|
-
if (this.vectorsAvailable) {
|
|
6346
|
-
bulkInsertVectorsWithStatement(
|
|
6347
|
-
(sql) => this.stmt(sql),
|
|
6348
|
-
_IndexStore.MAX_SQL_VARS,
|
|
6349
|
-
vectorRows
|
|
6350
|
-
);
|
|
6351
|
-
}
|
|
6352
|
-
bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refsToInsert);
|
|
6353
|
-
const upsertStmt = this.stmt(
|
|
6354
|
-
`INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
|
|
6355
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
6356
|
-
ON CONFLICT(file) DO UPDATE SET
|
|
6357
|
-
lang = excluded.lang,
|
|
6358
|
-
mtime_ms = excluded.mtime_ms,
|
|
6359
|
-
content_hash = excluded.content_hash,
|
|
6360
|
-
symbol_count = excluded.symbol_count,
|
|
6361
|
-
last_indexed = excluded.last_indexed`
|
|
6362
|
-
);
|
|
6363
|
-
const now = Date.now();
|
|
6364
|
-
for (const entry of entries) {
|
|
6365
|
-
upsertStmt.run(
|
|
6366
|
-
entry.file,
|
|
6367
|
-
entry.lang,
|
|
6368
|
-
entry.mtimeMs,
|
|
6369
|
-
entry.contentHash ?? "",
|
|
6370
|
-
entry.symbolCount,
|
|
6371
|
-
now
|
|
6372
|
-
);
|
|
6373
|
-
}
|
|
6374
|
-
this.resolveRefsForNamesUnsafe(affectedNames);
|
|
6375
6342
|
this.commitWriteTransaction(ownsTransaction);
|
|
6376
|
-
return
|
|
6343
|
+
return result;
|
|
6377
6344
|
} catch (err) {
|
|
6378
6345
|
this.rollbackWriteTransaction(ownsTransaction);
|
|
6379
6346
|
throw err;
|
|
6380
6347
|
}
|
|
6381
6348
|
});
|
|
6382
6349
|
}
|
|
6383
|
-
/**
|
|
6384
|
-
* Delete all refs whose source symbols are in a given file.
|
|
6385
|
-
* Used when re-indexing a file to clear stale refs.
|
|
6386
|
-
*/
|
|
6387
6350
|
deleteRefsForFile(file) {
|
|
6388
6351
|
this.runWithRetry(() => {
|
|
6389
6352
|
this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
|
|
@@ -6391,64 +6354,12 @@ var IndexStore = class _IndexStore {
|
|
|
6391
6354
|
);
|
|
6392
6355
|
});
|
|
6393
6356
|
}
|
|
6394
|
-
/**
|
|
6395
|
-
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
6396
|
-
* Call this after all symbols have been inserted to fill in cross-references.
|
|
6397
|
-
*
|
|
6398
|
-
* A match additionally requires the referencing ref and the target symbol to
|
|
6399
|
-
* be in the same {@link LangFamily}. Without that guard a name match is a
|
|
6400
|
-
* cross-language accident waiting to happen — `main`, `New`, `Parse` and
|
|
6401
|
-
* `Config` are declared in most languages at once, and each collision draws a
|
|
6402
|
-
* Code Atlas edge between files that never reference each other. Refs stored
|
|
6403
|
-
* without a language keep the old global behaviour via the `'*'` wildcard row.
|
|
6404
|
-
*/
|
|
6405
6357
|
resolveRefs() {
|
|
6406
|
-
return this.runWithRetry(() =>
|
|
6407
|
-
try {
|
|
6408
|
-
const result = this.stmt(
|
|
6409
|
-
`UPDATE refs
|
|
6410
|
-
SET to_id = s.id
|
|
6411
|
-
FROM (
|
|
6412
|
-
SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
|
|
6413
|
-
FROM symbols sym
|
|
6414
|
-
JOIN lang_family lf ON lf.lang = sym.lang
|
|
6415
|
-
GROUP BY sym.name, lf.family
|
|
6416
|
-
UNION ALL
|
|
6417
|
-
SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
|
|
6418
|
-
FROM symbols sym
|
|
6419
|
-
GROUP BY sym.name
|
|
6420
|
-
) AS s,
|
|
6421
|
-
lang_family AS rf
|
|
6422
|
-
WHERE refs.to_id IS NULL
|
|
6423
|
-
AND refs.to_name IS NOT NULL
|
|
6424
|
-
AND rf.lang = refs.lang
|
|
6425
|
-
AND s.name = refs.to_name
|
|
6426
|
-
AND s.family = rf.family`
|
|
6427
|
-
).run();
|
|
6428
|
-
return result.changes ?? 0;
|
|
6429
|
-
} catch {
|
|
6430
|
-
const result = this.stmt(
|
|
6431
|
-
`UPDATE refs SET to_id = (
|
|
6432
|
-
SELECT sym.id FROM symbols sym
|
|
6433
|
-
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
6434
|
-
ORDER BY sym.id LIMIT 1
|
|
6435
|
-
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
6436
|
-
AND EXISTS (
|
|
6437
|
-
SELECT 1 FROM symbols sym
|
|
6438
|
-
WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
|
|
6439
|
-
)`
|
|
6440
|
-
).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
|
|
6441
|
-
return result.changes ?? 0;
|
|
6442
|
-
}
|
|
6443
|
-
});
|
|
6358
|
+
return this.runWithRetry(() => resolveRefsWithStatement((sql) => this.stmt(sql)));
|
|
6444
6359
|
}
|
|
6445
6360
|
resolveRefsForNames(names) {
|
|
6446
6361
|
return this.runWithRetry(() => this.resolveRefsForNamesUnsafe(names));
|
|
6447
6362
|
}
|
|
6448
|
-
/**
|
|
6449
|
-
* Clear symbols/refs for a file and mark it as indexed with zero symbols.
|
|
6450
|
-
* Used by the indexer for empty-parse results so three writes share one txn.
|
|
6451
|
-
*/
|
|
6452
6363
|
replaceEmptyFile(meta) {
|
|
6453
6364
|
this.invalidateBm25();
|
|
6454
6365
|
this.runWithRetry(() => {
|
|
@@ -6494,20 +6405,12 @@ var IndexStore = class _IndexStore {
|
|
|
6494
6405
|
}
|
|
6495
6406
|
});
|
|
6496
6407
|
}
|
|
6497
|
-
/** Best-effort query planner refresh after a large reindex. */
|
|
6498
6408
|
optimize() {
|
|
6499
6409
|
try {
|
|
6500
6410
|
this.db.exec("PRAGMA optimize");
|
|
6501
6411
|
} catch {
|
|
6502
6412
|
}
|
|
6503
6413
|
}
|
|
6504
|
-
/**
|
|
6505
|
-
* Reclaim page churn left by repeated force rebuilds.
|
|
6506
|
-
*
|
|
6507
|
-
* SQLite's DROP/CREATE path makes rebuilds fast but leaves pages on the
|
|
6508
|
-
* freelist. Compact only large, materially sparse databases and only when the
|
|
6509
|
-
* caller is already on a full-index maintenance path.
|
|
6510
|
-
*/
|
|
6511
6414
|
compactIfNeeded(options = {}) {
|
|
6512
6415
|
const minBytes = options.minBytes ?? 256 * 1024 * 1024;
|
|
6513
6416
|
const minFreeRatio = options.minFreeRatio ?? 0.35;
|
|
@@ -6534,115 +6437,44 @@ var IndexStore = class _IndexStore {
|
|
|
6534
6437
|
return false;
|
|
6535
6438
|
}
|
|
6536
6439
|
}
|
|
6537
|
-
/**
|
|
6538
|
-
* Find all symbols that reference the named target symbol (incoming callers).
|
|
6539
|
-
* Accepts a name instead of an id so the agent doesn't need a prior lookup.
|
|
6540
|
-
*/
|
|
6541
6440
|
findIncomingCallsByName(symbolName, file, limit = 100) {
|
|
6542
6441
|
return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6543
6442
|
}
|
|
6544
|
-
/**
|
|
6545
|
-
* Find all symbols that the named source symbol references (outgoing callees).
|
|
6546
|
-
* Accepts a name instead of an id so the agent doesn't need a prior lookup.
|
|
6547
|
-
*/
|
|
6548
6443
|
findOutgoingCallsByName(symbolName, file, limit = 100) {
|
|
6549
6444
|
return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6550
6445
|
}
|
|
6551
|
-
/**
|
|
6552
|
-
* Transitive incoming-call tree: all symbols that transitively call the
|
|
6553
|
-
* target, to an unbounded depth (cycle-safe via SQL UNION deduplication).
|
|
6554
|
-
* Used by `codebase-incoming-calls` when the caller wants the full call
|
|
6555
|
-
* chain rather than just direct callers.
|
|
6556
|
-
*/
|
|
6557
6446
|
findTransitiveIncomingCallsByName(symbolName, file, limit = 200) {
|
|
6558
6447
|
return findTransitiveIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6559
6448
|
}
|
|
6560
|
-
/**
|
|
6561
|
-
* Transitive outgoing-call tree: all symbols the target transitively calls.
|
|
6562
|
-
* Used by `codebase-outgoing-calls` when the caller wants the full
|
|
6563
|
-
* dependency chain rather than just direct callees.
|
|
6564
|
-
*/
|
|
6565
6449
|
findTransitiveOutgoingCallsByName(symbolName, file, limit = 200) {
|
|
6566
6450
|
return findTransitiveOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
|
|
6567
6451
|
}
|
|
6568
|
-
/**
|
|
6569
|
-
* Compute the set of symbol IDs reachable from the given seed IDs using a
|
|
6570
|
-
* native SQLite recursive CTE. Used by dead-code detection to replace the
|
|
6571
|
-
* in-memory BFS.
|
|
6572
|
-
*/
|
|
6573
6452
|
findReachableSymbolIds(seedIds) {
|
|
6574
6453
|
return findReachableSymbolIds((sql) => this.stmt(sql), seedIds);
|
|
6575
6454
|
}
|
|
6576
|
-
/**
|
|
6577
|
-
* Find all references TO a given symbol (who calls / uses this symbol?).
|
|
6578
|
-
*/
|
|
6579
6455
|
findRefsTo(symbolId) {
|
|
6580
6456
|
return findRefsToWithStatement((sql) => this.stmt(sql), symbolId);
|
|
6581
6457
|
}
|
|
6582
|
-
/**
|
|
6583
|
-
* Find all references FROM a given symbol (what does this symbol call/use?).
|
|
6584
|
-
*/
|
|
6585
6458
|
findRefsFrom(symbolId) {
|
|
6586
6459
|
return findRefsFromWithStatement((sql) => this.stmt(sql), symbolId);
|
|
6587
6460
|
}
|
|
6588
|
-
// ─── CodeMap graph aggregation ──────────────────────────────────────────────
|
|
6589
|
-
/**
|
|
6590
|
-
* Package-level graph: each workspace package is a node; edges are derived
|
|
6591
|
-
* from cross-package symbol references (a symbol in package A references a
|
|
6592
|
-
* symbol resolved in package B). Node metadata includes symbol/file counts.
|
|
6593
|
-
*/
|
|
6594
6461
|
getPackageGraph() {
|
|
6595
6462
|
return getPackageGraphWithStatement((sql) => this.stmt(sql));
|
|
6596
6463
|
}
|
|
6597
|
-
/**
|
|
6598
|
-
* File-level graph for a single package: each file is a node; edges are
|
|
6599
|
-
* derived from cross-file symbol references within the package.
|
|
6600
|
-
*/
|
|
6601
6464
|
getFileGraph(packageFilter) {
|
|
6602
6465
|
return getFileGraphWithStatement((sql) => this.stmt(sql), packageFilter);
|
|
6603
6466
|
}
|
|
6604
|
-
/**
|
|
6605
|
-
* Symbol-level graph for a single file: each symbol is a node; edges are
|
|
6606
|
-
* derived from intra-file and cross-file symbol references (who calls whom).
|
|
6607
|
-
*/
|
|
6608
6467
|
getSymbolGraph(fileFilter) {
|
|
6609
6468
|
return getSymbolGraphWithStatement((sql) => this.stmt(sql), fileFilter);
|
|
6610
6469
|
}
|
|
6611
|
-
/**
|
|
6612
|
-
* Returns every symbol in the index. Used by dead-code analysis to
|
|
6613
|
-
* build the full symbol universe for the reachability scan.
|
|
6614
|
-
*/
|
|
6615
6470
|
getAllSymbols() {
|
|
6616
6471
|
return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
|
|
6617
6472
|
}
|
|
6618
|
-
/**
|
|
6619
|
-
* Returns every resolved reference (to_id IS NOT NULL). Used by
|
|
6620
|
-
* dead-code analysis to build the consumer-ship graph. Refs whose
|
|
6621
|
-
* target symbol id is null (unresolved imports) are excluded.
|
|
6622
|
-
*/
|
|
6623
6473
|
getAllResolvedRefs() {
|
|
6624
|
-
return this.stmt(
|
|
6625
|
-
"SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
|
|
6626
|
-
).all();
|
|
6474
|
+
return getAllResolvedRefsWithStatement((sql) => this.stmt(sql));
|
|
6627
6475
|
}
|
|
6628
|
-
/**
|
|
6629
|
-
* Returns ALL import refs (including unresolved) with their source-file
|
|
6630
|
-
* path and resolved target id. Used by the dead-code scan's file-level
|
|
6631
|
-
* graph traversal to handle barrel-only entry points where no symbol
|
|
6632
|
-
* carries the ref.
|
|
6633
|
-
*
|
|
6634
|
-
* Refs whose `from_id` doesn't match a known symbol (e.g. pure-barrel
|
|
6635
|
-
* files with no declarations) will have `sourceFile === null`.
|
|
6636
|
-
*/
|
|
6637
6476
|
getAllImportRefs() {
|
|
6638
|
-
return this.stmt(
|
|
6639
|
-
`SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
|
|
6640
|
-
r.call_type AS callType, r.line
|
|
6641
|
-
FROM refs r
|
|
6642
|
-
LEFT JOIN symbols s ON r.from_id = s.id
|
|
6643
|
-
WHERE r.call_type = 'import'
|
|
6644
|
-
ORDER BY r.line`
|
|
6645
|
-
).all();
|
|
6477
|
+
return getAllImportRefsWithStatement((sql) => this.stmt(sql));
|
|
6646
6478
|
}
|
|
6647
6479
|
close() {
|
|
6648
6480
|
this.stmtCache.clear();
|
|
@@ -7342,11 +7174,10 @@ function outgoingCallsService(args) {
|
|
|
7342
7174
|
|
|
7343
7175
|
// src/codebase-index/project-server-client.ts
|
|
7344
7176
|
import { spawn as spawn3 } from "node:child_process";
|
|
7345
|
-
import * as
|
|
7177
|
+
import * as fs12 from "node:fs";
|
|
7346
7178
|
import * as net from "node:net";
|
|
7347
7179
|
import { StringDecoder } from "node:string_decoder";
|
|
7348
|
-
import { fileURLToPath as
|
|
7349
|
-
import { checkUnixSocketPath } from "@wrongstack/core/utils";
|
|
7180
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7350
7181
|
|
|
7351
7182
|
// src/codebase-index/binary-frame.ts
|
|
7352
7183
|
import { decode, encode } from "@msgpack/msgpack";
|
|
@@ -7420,7 +7251,10 @@ function encodeProjectServerMessage(message) {
|
|
|
7420
7251
|
`;
|
|
7421
7252
|
}
|
|
7422
7253
|
|
|
7423
|
-
// src/codebase-index/project-server-client.ts
|
|
7254
|
+
// src/codebase-index/project-server-client-state.ts
|
|
7255
|
+
import * as fs11 from "node:fs";
|
|
7256
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7257
|
+
import { checkUnixSocketPath } from "@wrongstack/core/utils";
|
|
7424
7258
|
var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
|
|
7425
7259
|
var SERVER_START_TIMEOUT_MS = 1e4;
|
|
7426
7260
|
var SERVER_CONTROL_TIMEOUT_MS = 5e3;
|
|
@@ -7440,6 +7274,9 @@ var latestConnectionState = {
|
|
|
7440
7274
|
status: "offline",
|
|
7441
7275
|
connected: false
|
|
7442
7276
|
};
|
|
7277
|
+
function setLatestConnectionState(state) {
|
|
7278
|
+
latestConnectionState = state;
|
|
7279
|
+
}
|
|
7443
7280
|
function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
|
|
7444
7281
|
if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
|
|
7445
7282
|
return { kind: "inline-requested" };
|
|
@@ -7535,6 +7372,8 @@ function delay(ms) {
|
|
|
7535
7372
|
function cancellationError(signal) {
|
|
7536
7373
|
return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
|
|
7537
7374
|
}
|
|
7375
|
+
|
|
7376
|
+
// src/codebase-index/project-server-client.ts
|
|
7538
7377
|
var ProjectServerConnection = class {
|
|
7539
7378
|
constructor(projectRoot, indexDir, endpoint) {
|
|
7540
7379
|
this.projectRoot = projectRoot;
|
|
@@ -7665,11 +7504,11 @@ var ProjectServerConnection = class {
|
|
|
7665
7504
|
this.close();
|
|
7666
7505
|
}
|
|
7667
7506
|
}
|
|
7668
|
-
async configure(watchExternal, debounceMs) {
|
|
7507
|
+
async configure(watchExternal, debounceMs, coalesceWindowMs) {
|
|
7669
7508
|
await this.ensureConnected(true);
|
|
7670
7509
|
const startedAt = Date.now();
|
|
7671
7510
|
const result = await this.request(
|
|
7672
|
-
{ type: "configure", watchExternal, debounceMs },
|
|
7511
|
+
{ type: "configure", watchExternal, debounceMs, coalesceWindowMs },
|
|
7673
7512
|
{ timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
|
|
7674
7513
|
);
|
|
7675
7514
|
if (isProjectIndexServerHealth(result.health)) {
|
|
@@ -7713,7 +7552,7 @@ var ProjectServerConnection = class {
|
|
|
7713
7552
|
currentAuthToken() {
|
|
7714
7553
|
if (this.authToken === void 0) {
|
|
7715
7554
|
try {
|
|
7716
|
-
const raw =
|
|
7555
|
+
const raw = fs12.readFileSync(
|
|
7717
7556
|
projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
|
|
7718
7557
|
"utf8"
|
|
7719
7558
|
);
|
|
@@ -8029,11 +7868,11 @@ var ProjectServerConnection = class {
|
|
|
8029
7868
|
if (!url) throw new Error("built codebase-index project server is unavailable");
|
|
8030
7869
|
if (process.platform !== "win32") {
|
|
8031
7870
|
try {
|
|
8032
|
-
|
|
7871
|
+
fs12.rmSync(this.endpoint, { force: true });
|
|
8033
7872
|
} catch {
|
|
8034
7873
|
}
|
|
8035
7874
|
}
|
|
8036
|
-
const args = [
|
|
7875
|
+
const args = [fileURLToPath5(url), "--project-root", this.projectRoot];
|
|
8037
7876
|
if (this.indexDir) args.push("--index-dir", this.indexDir);
|
|
8038
7877
|
const child = spawn3(process.execPath, args, {
|
|
8039
7878
|
detached: true,
|
|
@@ -8053,8 +7892,8 @@ var ProjectServerConnection = class {
|
|
|
8053
7892
|
process.kill(pid);
|
|
8054
7893
|
const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
|
|
8055
7894
|
try {
|
|
8056
|
-
const metadata = JSON.parse(
|
|
8057
|
-
if (metadata.pid === pid)
|
|
7895
|
+
const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
|
|
7896
|
+
if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
|
|
8058
7897
|
} catch {
|
|
8059
7898
|
}
|
|
8060
7899
|
return true;
|
|
@@ -8071,10 +7910,12 @@ function forgetConnection(endpoint, connection) {
|
|
|
8071
7910
|
connection.close();
|
|
8072
7911
|
connectionStates.delete(endpoint);
|
|
8073
7912
|
if (latestConnectionState.endpoint !== endpoint) return;
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
7913
|
+
setLatestConnectionState(
|
|
7914
|
+
[...connectionStates.values()].at(-1) ?? {
|
|
7915
|
+
status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
|
|
7916
|
+
connected: false
|
|
7917
|
+
}
|
|
7918
|
+
);
|
|
8078
7919
|
}
|
|
8079
7920
|
function trimConnectionCache(protectedConnection) {
|
|
8080
7921
|
if (connections.size <= MAX_CACHED_CONNECTIONS) return;
|
|
@@ -8153,7 +7994,7 @@ function resolveWorkerUrl() {
|
|
|
8153
7994
|
for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
|
|
8154
7995
|
try {
|
|
8155
7996
|
const url = new URL(rel, import.meta.url);
|
|
8156
|
-
if (url.protocol === "file:" &&
|
|
7997
|
+
if (url.protocol === "file:" && fs13.existsSync(fileURLToPath6(url))) return url;
|
|
8157
7998
|
} catch {
|
|
8158
7999
|
}
|
|
8159
8000
|
}
|
|
@@ -8406,7 +8247,7 @@ var readTool = {
|
|
|
8406
8247
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
8407
8248
|
let stat3;
|
|
8408
8249
|
try {
|
|
8409
|
-
stat3 = await
|
|
8250
|
+
stat3 = await fs14.stat(absPath);
|
|
8410
8251
|
} catch (err) {
|
|
8411
8252
|
const code = err.code;
|
|
8412
8253
|
if (code === "ENOENT") {
|
|
@@ -8458,7 +8299,7 @@ var readTool = {
|
|
|
8458
8299
|
...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
|
|
8459
8300
|
};
|
|
8460
8301
|
}
|
|
8461
|
-
const buf = await
|
|
8302
|
+
const buf = await fs14.readFile(absPath);
|
|
8462
8303
|
if (isBinaryBuffer(buf)) {
|
|
8463
8304
|
throw new FsError({
|
|
8464
8305
|
message: `read: "${input.path}" appears to be binary`,
|