@wrongstack/tools 0.306.4 → 0.307.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/audit.js +14 -8
  2. package/dist/auto-proceed-loop-guard.js +8 -2
  3. package/dist/bash.js +57 -22
  4. package/dist/builtin.d.ts +6 -6
  5. package/dist/builtin.js +11469 -9304
  6. package/dist/clarify.d.ts +32 -0
  7. package/dist/codebase-index/ast-invariant-engine.d.ts +102 -0
  8. package/dist/codebase-index/ast-symbol-mutator.d.ts +28 -0
  9. package/dist/codebase-index/background-indexer.d.ts +9 -0
  10. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +31 -0
  11. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +42 -0
  12. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +23 -0
  13. package/dist/codebase-index/codebase-repo-map-tool.d.ts +22 -0
  14. package/dist/codebase-index/codebase-skeleton-tool.d.ts +36 -0
  15. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +30 -0
  16. package/dist/codebase-index/index.d.ts +11 -1
  17. package/dist/codebase-index/index.js +4239 -1492
  18. package/dist/codebase-index/project-server-client-state.d.ts +79 -0
  19. package/dist/codebase-index/project-server-client.d.ts +3 -71
  20. package/dist/codebase-index/project-server-protocol.d.ts +1 -0
  21. package/dist/codebase-index/project-server.js +819 -933
  22. package/dist/codebase-index/repo-map.d.ts +20 -0
  23. package/dist/codebase-index/skeleton-extractor.d.ts +64 -0
  24. package/dist/codebase-index/tree-sitter-parser.d.ts +11 -0
  25. package/dist/codebase-index/worker.js +790 -914
  26. package/dist/codebase-index/writer-graph-helpers.d.ts +1 -1
  27. package/dist/codebase-index/writer-helpers.d.ts +12 -0
  28. package/dist/codebase-index/writer-mutations.d.ts +26 -0
  29. package/dist/codebase-index/writer-refs.d.ts +50 -0
  30. package/dist/codebase-index/writer-search.d.ts +30 -0
  31. package/dist/codebase-index/writer.d.ts +2 -320
  32. package/dist/edit.js +8602 -349
  33. package/dist/exec.js +73 -20
  34. package/dist/fetch.js +11 -9
  35. package/dist/format.js +14 -8
  36. package/dist/glob.js +5 -1
  37. package/dist/grep.js +19 -8
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +6178 -4037
  40. package/dist/install.js +14 -8
  41. package/dist/json.js +88 -15
  42. package/dist/kanban-board-actions.d.ts +4 -0
  43. package/dist/kanban-lifecycle-actions.d.ts +4 -0
  44. package/dist/kanban-serializer.d.ts +21 -0
  45. package/dist/kanban.js +985 -1034
  46. package/dist/languages/index.js +14 -8
  47. package/dist/lint.js +14 -8
  48. package/dist/logs.js +13 -3
  49. package/dist/next-steps.d.ts +8 -0
  50. package/dist/next-steps.js +18 -0
  51. package/dist/outdated.js +14 -8
  52. package/dist/pack.js +11461 -9304
  53. package/dist/patch.js +8345 -78
  54. package/dist/plan.js +1231 -1281
  55. package/dist/process-registry.js +14 -8
  56. package/dist/ps-slash.js +65 -30
  57. package/dist/read.js +822 -937
  58. package/dist/replace.js +8494 -224
  59. package/dist/search.js +21 -15
  60. package/dist/security-ast-scan-tool.d.ts +43 -0
  61. package/dist/session-kanban-graph.d.ts +9 -0
  62. package/dist/session-kanban-sync.d.ts +31 -0
  63. package/dist/session-kanban.d.ts +5 -141
  64. package/dist/session-kanban.js +367 -360
  65. package/dist/task.js +1157 -1207
  66. package/dist/test.js +14 -8
  67. package/dist/todo.js +2187 -2237
  68. package/dist/tool-diff.js +6 -1
  69. package/dist/tool-summary.js +4 -2
  70. package/dist/tool-tier.js +11469 -9304
  71. package/dist/typecheck.js +14 -8
  72. package/dist/write.js +8425 -160
  73. 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, { filePath, lang, kind: trimmed.startsWith("func (") ? "method" : "function", name: fn[1], line: idx + 1, col, signature: trimmed, scope: packageName ? `${packageName}.${fn[1]}` : fn[1] });
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, { filePath, lang, kind: "type", name: typeDecl[1], line: idx + 1, col, signature: trimmed, scope: packageName });
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, { filePath, lang, kind: valueDecl[1], name: valueDecl[2], line: idx + 1, col, signature: trimmed, scope: packageName });
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(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
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
- { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
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
- { 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, kind: "type" }
1145
- ],
1146
- md: [
1147
- { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
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
- { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
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
- { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
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 fs13 from "node:fs/promises";
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 fs12 from "node:fs";
2779
- import { fileURLToPath as fileURLToPath5 } from "node:url";
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(({ id, text }) => ({ id, text }));
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();
@@ -4489,10 +4426,16 @@ function buildFileGraphNodeState(pkgSyms, localFiles, packageOf) {
4489
4426
  }
4490
4427
  return { fileNodes, symToFile, fileStats, ensureFileNode };
4491
4428
  }
4492
- function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
4429
+ function buildSymbolGraphNodes(symById, relatedIds, localFiles, packageOf) {
4430
+ const local = new Set(
4431
+ [...typeof localFiles === "string" ? [localFiles] : localFiles].map(
4432
+ (file) => file.replace(/\\/g, "/")
4433
+ )
4434
+ );
4435
+ const isLocal = (file) => local.has(file.replace(/\\/g, "/"));
4493
4436
  return [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
4494
- const aExternal = a.file === fileFilter ? 0 : 1;
4495
- const bExternal = b.file === fileFilter ? 0 : 1;
4437
+ const aExternal = isLocal(a.file) ? 0 : 1;
4438
+ const bExternal = isLocal(b.file) ? 0 : 1;
4496
4439
  return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
4497
4440
  }).map((s) => ({
4498
4441
  id: `sym:${s.id}`,
@@ -4506,7 +4449,7 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter, packageOf) {
4506
4449
  line: s.line,
4507
4450
  signature: s.signature,
4508
4451
  scope: s.scope,
4509
- external: s.file !== fileFilter
4452
+ external: !isLocal(s.file)
4510
4453
  }));
4511
4454
  }
4512
4455
  function addWeightedEdge(edgeMap, source, target, callType, weight) {
@@ -4541,6 +4484,56 @@ function materializeWeightedEdges(edgeMap, idPrefix) {
4541
4484
  return edges;
4542
4485
  }
4543
4486
 
4487
+ // src/codebase-index/writer-helpers.ts
4488
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
4489
+ function escapeLike(value) {
4490
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
4491
+ }
4492
+ function posixIndexPath(file) {
4493
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
4494
+ }
4495
+ function indexedFileMatchSql(column = "file") {
4496
+ return `(${column} = ? OR replace(${column}, '\\', '/') = ? OR replace(${column}, '\\', '/') LIKE ? ESCAPE '\\')`;
4497
+ }
4498
+ function indexedFileMatchArgs(file) {
4499
+ const posix4 = posixIndexPath(file.trim());
4500
+ return [file, posix4, `%/${escapeLike(posix4)}`];
4501
+ }
4502
+ function matchesIndexedPackageFilter(storedFile, packageLabel, filter) {
4503
+ if (packageLabel === filter) return true;
4504
+ const posixFile = posixIndexPath(storedFile);
4505
+ const posixFilter = posixIndexPath(filter.trim());
4506
+ if (!posixFilter) return false;
4507
+ return posixFile === posixFilter || posixFile.endsWith(`/${posixFilter}`) || posixFile.includes(`/${posixFilter}/`);
4508
+ }
4509
+ function assignRefsToSymbols(refs, symbols) {
4510
+ if (refs.length === 0 || symbols.length === 0) return [];
4511
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4512
+ const seen = /* @__PURE__ */ new Set();
4513
+ const assigned = [];
4514
+ for (const ref of refs) {
4515
+ let owner;
4516
+ for (const symbol of ordered) {
4517
+ if (symbol.line > ref.line) break;
4518
+ owner = symbol;
4519
+ }
4520
+ if (!owner && ref.callType === "import") owner = ordered[0];
4521
+ if (!owner || owner.id <= 0) continue;
4522
+ const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
4523
+ if (seen.has(key)) continue;
4524
+ seen.add(key);
4525
+ assigned.push({ ...ref, fromId: owner.id });
4526
+ }
4527
+ return assigned;
4528
+ }
4529
+ function resolveIndexDir(projectRoot, override) {
4530
+ return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
4531
+ }
4532
+ function codebaseIndexDirOverride(ctx) {
4533
+ const v = ctx.meta?.["codebaseIndexDir"];
4534
+ return typeof v === "string" ? v : void 0;
4535
+ }
4536
+
4544
4537
  // src/codebase-index/writer-ref-mapper.ts
4545
4538
  function mapWriterRefRow(row) {
4546
4539
  return {
@@ -4596,15 +4589,29 @@ function mapCallSiteRow(row) {
4596
4589
  line: row.ref_line
4597
4590
  };
4598
4591
  }
4592
+ function resolveIndexedFiles(stmt, file) {
4593
+ const rows = stmt(
4594
+ `SELECT DISTINCT file FROM symbols WHERE ${indexedFileMatchSql("file")} ORDER BY length(file), file`
4595
+ ).all(...indexedFileMatchArgs(file));
4596
+ return rows.map((row) => row.file);
4597
+ }
4599
4598
  function resolveSymbolIds(stmt, symbolName, file) {
4600
- const baseSql = file ? `SELECT id FROM symbols WHERE name = ? AND file = ? ORDER BY id` : `SELECT id FROM symbols WHERE name = ? ORDER BY id`;
4601
- const args = file ? [symbolName, file] : [symbolName];
4602
- const rows = stmt(baseSql).all(...args);
4599
+ if (!file) {
4600
+ const rows2 = stmt("SELECT id FROM symbols WHERE name = ? ORDER BY id").all(symbolName);
4601
+ return rows2.map((r) => r.id);
4602
+ }
4603
+ const indexedFiles = resolveIndexedFiles(stmt, file);
4604
+ if (indexedFiles.length === 0) return [];
4605
+ const placeholders = indexedFiles.map(() => "?").join(",");
4606
+ const rows = stmt(
4607
+ `SELECT id FROM symbols WHERE name = ? AND file IN (${placeholders}) ORDER BY id`
4608
+ ).all(symbolName, ...indexedFiles);
4603
4609
  return rows.map((r) => r.id);
4604
4610
  }
4605
4611
  function findIncomingCallsByName(stmt, symbolName, file, limit) {
4606
4612
  const targetIds = resolveSymbolIds(stmt, symbolName, file);
4607
- if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
4613
+ if (targetIds.length === 0)
4614
+ return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
4608
4615
  let matchIds = targetIds;
4609
4616
  let ambiguous = false;
4610
4617
  if (file !== void 0) {
@@ -4655,11 +4662,17 @@ function findIncomingCallsByName(stmt, symbolName, file, limit) {
4655
4662
  }
4656
4663
  rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
4657
4664
  const allCalls = rows.map(mapCallSiteRow);
4658
- return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
4665
+ return {
4666
+ calls: allCalls.slice(0, limit),
4667
+ symbolFound: true,
4668
+ ambiguous,
4669
+ totalMatches: allCalls.length
4670
+ };
4659
4671
  }
4660
4672
  function findOutgoingCallsByName(stmt, symbolName, file, limit) {
4661
4673
  const sourceIds = resolveSymbolIds(stmt, symbolName, file);
4662
- if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
4674
+ if (sourceIds.length === 0)
4675
+ return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
4663
4676
  const unresolvedCount = chunkedIdScalar(
4664
4677
  stmt,
4665
4678
  sourceIds,
@@ -4914,7 +4927,7 @@ function getFileGraphWithStatement(stmt, packageFilter) {
4914
4927
  const allFiles = stmt("SELECT DISTINCT file FROM symbols").all();
4915
4928
  const packageOf = readPackageLabeller(stmt);
4916
4929
  const langOf = (file) => detectLang(file) ?? "other";
4917
- const pkgFilePaths = allFiles.filter((f) => packageOf(f.file) === packageFilter).map((f) => f.file);
4930
+ const pkgFilePaths = allFiles.filter((f) => matchesIndexedPackageFilter(f.file, packageOf(f.file), packageFilter)).map((f) => f.file);
4918
4931
  const localFiles = new Set(pkgFilePaths);
4919
4932
  if (localFiles.size === 0) return { nodes: [], edges: [] };
4920
4933
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
@@ -4990,9 +5003,12 @@ function getFileGraphWithStatement(stmt, packageFilter) {
4990
5003
  return { nodes: [...fileNodes.values()], edges };
4991
5004
  }
4992
5005
  function getSymbolGraphWithStatement(stmt, fileFilter) {
5006
+ const indexedFiles = resolveIndexedFiles(stmt, fileFilter);
5007
+ if (indexedFiles.length === 0) return { nodes: [], edges: [] };
5008
+ const filePlaceholders = indexedFiles.map(() => "?").join(",");
4993
5009
  const syms = stmt(
4994
- "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
4995
- ).all(fileFilter);
5010
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY line, id`
5011
+ ).all(...indexedFiles);
4996
5012
  if (syms.length === 0) return { nodes: [], edges: [] };
4997
5013
  const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
4998
5014
  const relatedIds = new Set(syms.map((symbol) => symbol.id));
@@ -5002,16 +5018,16 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5002
5018
  SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
5003
5019
  FROM refs r
5004
5020
  JOIN symbols s ON s.id = r.from_id
5005
- WHERE s.file = ?
5021
+ WHERE s.file IN (${filePlaceholders})
5006
5022
  UNION
5007
5023
  SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
5008
5024
  FROM refs r
5009
5025
  JOIN symbols s ON s.id = r.to_id
5010
- WHERE s.file = ?
5026
+ WHERE s.file IN (${filePlaceholders})
5011
5027
  )
5012
5028
  WHERE to_id IS NOT NULL
5013
5029
  GROUP BY from_id, to_id, call_type`
5014
- ).all(fileFilter, fileFilter);
5030
+ ).all(...indexedFiles, ...indexedFiles);
5015
5031
  const edgeMap = /* @__PURE__ */ new Map();
5016
5032
  for (const r of refRows) {
5017
5033
  if (r.to_id == null) continue;
@@ -5030,41 +5046,191 @@ function getSymbolGraphWithStatement(stmt, fileFilter) {
5030
5046
  ).all(...missingIds);
5031
5047
  for (const s of extras) symById.set(s.id, s);
5032
5048
  }
5033
- const nodes = buildSymbolGraphNodes(symById, relatedIds, fileFilter, readPackageLabeller(stmt));
5049
+ const nodes = buildSymbolGraphNodes(
5050
+ symById,
5051
+ relatedIds,
5052
+ new Set(syms.map((symbol) => symbol.file)),
5053
+ readPackageLabeller(stmt)
5054
+ );
5034
5055
  return { nodes, edges };
5035
5056
  }
5036
5057
 
5037
- // src/codebase-index/writer-helpers.ts
5038
- import { resolveWstackPaths } from "@wrongstack/core/utils";
5039
- function escapeLike(value) {
5040
- return value.replace(/[\\%_]/g, (char) => `\\${char}`);
5041
- }
5042
- function assignRefsToSymbols(refs, symbols) {
5043
- if (refs.length === 0 || symbols.length === 0) return [];
5044
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
5045
- const seen = /* @__PURE__ */ new Set();
5046
- const assigned = [];
5047
- for (const ref of refs) {
5048
- let owner;
5049
- for (const symbol of ordered) {
5050
- if (symbol.line > ref.line) break;
5051
- owner = symbol;
5058
+ // src/codebase-index/vector-search.ts
5059
+ var RRF_K = 60;
5060
+ var VECTOR_DIMENSIONS = 384;
5061
+ var NGRAM_SIZE = 3;
5062
+ function embedText(text) {
5063
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
5064
+ const normalized = text.toLowerCase().trim();
5065
+ if (normalized.length < NGRAM_SIZE) {
5066
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5067
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5068
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
5069
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5070
+ vec[bucket] += 1;
5071
+ }
5072
+ } else {
5073
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5074
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
5075
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5076
+ vec[bucket] += 1;
5052
5077
  }
5053
- if (!owner && ref.callType === "import") owner = ordered[0];
5054
- if (!owner || owner.id <= 0) continue;
5055
- const key = `${owner.id}:${ref.toName}:${ref.callType}:${ref.module ?? ""}`;
5056
- if (seen.has(key)) continue;
5057
- seen.add(key);
5058
- assigned.push({ ...ref, fromId: owner.id });
5059
5078
  }
5060
- return assigned;
5061
- }
5062
- function resolveIndexDir(projectRoot, override) {
5063
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
5079
+ let norm = 0;
5080
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5081
+ norm += vec[i] * vec[i];
5082
+ }
5083
+ norm = Math.sqrt(norm);
5084
+ if (norm > 0) {
5085
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5086
+ vec[i] /= norm;
5087
+ }
5088
+ }
5089
+ return vec;
5064
5090
  }
5065
- function codebaseIndexDirOverride(ctx) {
5066
- const v = ctx.meta?.["codebaseIndexDir"];
5067
- return typeof v === "string" ? v : void 0;
5091
+ function hashNgram(str) {
5092
+ let hash = 2166136261;
5093
+ for (let i = 0; i < str.length; i++) {
5094
+ hash ^= str.charCodeAt(i);
5095
+ hash = Math.imul(hash, 16777619);
5096
+ }
5097
+ return hash >>> 0;
5098
+ }
5099
+ function cosineSimilarity(a, b) {
5100
+ let dot = 0;
5101
+ const len = Math.min(a.length, b.length);
5102
+ for (let i = 0; i < len; i++) {
5103
+ dot += a[i] * b[i];
5104
+ }
5105
+ return dot;
5106
+ }
5107
+ function encodeVector(vec) {
5108
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5109
+ }
5110
+ function decodeVector(buf) {
5111
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
5112
+ const copy = new Float32Array(buf.byteLength / 4);
5113
+ for (let i = 0; i < copy.length; i++) {
5114
+ copy[i] = view.getFloat32(i * 4, true);
5115
+ }
5116
+ return copy;
5117
+ }
5118
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5119
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5120
+ const scored = [];
5121
+ for (const id of allIds) {
5122
+ const bm25Rank = bm25Ranks.get(id);
5123
+ const vecRank = vectorRanks.get(id);
5124
+ let score = 0;
5125
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5126
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
5127
+ scored.push([id, score]);
5128
+ }
5129
+ scored.sort((a, b) => b[1] - a[1]);
5130
+ return scored;
5131
+ }
5132
+
5133
+ // src/codebase-index/writer-mutations.ts
5134
+ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
5135
+ if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
5136
+ return [];
5137
+ }
5138
+ const affectedNames = /* @__PURE__ */ new Set();
5139
+ for (const entry of entries) {
5140
+ for (const symbol of entry.symbols) affectedNames.add(symbol.name);
5141
+ for (const ref of entry.refs) affectedNames.add(ref.toName);
5142
+ }
5143
+ if (options.deleteForFiles && options.deleteForFiles.length > 0) {
5144
+ const placeholders = options.deleteForFiles.map(() => "?").join(",");
5145
+ for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
5146
+ affectedNames.add(name);
5147
+ }
5148
+ if (ftsAvailable) {
5149
+ stmtFn(
5150
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5151
+ ).run(...options.deleteForFiles);
5152
+ }
5153
+ if (vectorsAvailable) {
5154
+ stmtFn(
5155
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5156
+ ).run(...options.deleteForFiles);
5157
+ }
5158
+ stmtFn(
5159
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5160
+ ).run(...options.deleteForFiles);
5161
+ stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
5162
+ }
5163
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
5164
+ let nextId = allocateSymbolIds(totalSymbols);
5165
+ const allInserted = [];
5166
+ const refsToInsert = [];
5167
+ const bulkSyms = [];
5168
+ const ftsRows = [];
5169
+ const vectorRows = [];
5170
+ for (const entry of entries) {
5171
+ const insertedForEntry = [];
5172
+ for (const s of entry.symbols) {
5173
+ const id = nextId++;
5174
+ bulkSyms.push({
5175
+ id,
5176
+ lang: s.lang,
5177
+ kind: s.kind,
5178
+ name: s.name,
5179
+ file: s.file,
5180
+ line: s.line,
5181
+ col: s.col,
5182
+ signature: s.signature,
5183
+ docComment: s.docComment,
5184
+ scope: s.scope,
5185
+ text: s.text
5186
+ });
5187
+ if (ftsAvailable) {
5188
+ ftsRows.push({
5189
+ id,
5190
+ text: buildIndexableText(s.name, s.signature, s.docComment)
5191
+ });
5192
+ }
5193
+ vectorRows.push({
5194
+ id,
5195
+ vector: encodeVector(
5196
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5197
+ )
5198
+ });
5199
+ const inserted = { ...s, id };
5200
+ allInserted.push(inserted);
5201
+ insertedForEntry.push(inserted);
5202
+ }
5203
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
5204
+ }
5205
+ bulkInsertSymbolsWithStatement((sql) => stmtFn(sql), maxSqlVars, bulkSyms);
5206
+ bulkInsertFtsWithStatement((sql) => stmtFn(sql), maxSqlVars, ftsAvailable, ftsRows);
5207
+ if (vectorsAvailable) {
5208
+ bulkInsertVectorsWithStatement((sql) => stmtFn(sql), maxSqlVars, vectorRows);
5209
+ }
5210
+ bulkInsertRefsWithStatement((sql) => stmtFn(sql), maxSqlVars, refsToInsert);
5211
+ const upsertStmt = stmtFn(
5212
+ `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
5213
+ VALUES (?, ?, ?, ?, ?, ?)
5214
+ ON CONFLICT(file) DO UPDATE SET
5215
+ lang = excluded.lang,
5216
+ mtime_ms = excluded.mtime_ms,
5217
+ content_hash = excluded.content_hash,
5218
+ symbol_count = excluded.symbol_count,
5219
+ last_indexed = excluded.last_indexed`
5220
+ );
5221
+ const now = Date.now();
5222
+ for (const entry of entries) {
5223
+ upsertStmt.run(
5224
+ entry.file,
5225
+ entry.lang,
5226
+ entry.mtimeMs,
5227
+ entry.contentHash ?? "",
5228
+ entry.symbolCount,
5229
+ now
5230
+ );
5231
+ }
5232
+ resolveRefsForNamesUnsafe2(affectedNames);
5233
+ return allInserted;
5068
5234
  }
5069
5235
 
5070
5236
  // src/codebase-index/writer-pragmas.ts
@@ -5179,6 +5345,237 @@ var SYMBOL_VECTORS_TABLE_SQL = `
5179
5345
  );
5180
5346
  `;
5181
5347
 
5348
+ // src/codebase-index/writer-refs.ts
5349
+ var FAMILY_MATCH_SQL = `(
5350
+ sym.lang = refs.lang
5351
+ OR EXISTS (
5352
+ SELECT 1 FROM lang_family lf1
5353
+ JOIN lang_family lf2 ON lf1.family = lf2.family
5354
+ WHERE lf1.lang = sym.lang AND lf2.lang = refs.lang
5355
+ )
5356
+ OR ? IN (
5357
+ SELECT family FROM lang_family WHERE lang = refs.lang
5358
+ )
5359
+ )`;
5360
+ function getNamespaceDeclarationsWithStatement(stmtFn) {
5361
+ return stmtFn(
5362
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
5363
+ ).all();
5364
+ }
5365
+ function getFilePackagesWithStatement(stmtFn) {
5366
+ const rows = stmtFn("SELECT file, package FROM files WHERE package != ''").all();
5367
+ return new Map(rows.map((row) => [row.file, row.package]));
5368
+ }
5369
+ function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
5370
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
5371
+ FROM refs r
5372
+ JOIN symbols s ON s.id = r.from_id
5373
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
5374
+ if (!onlyFiles?.length) {
5375
+ return stmtFn(base).all();
5376
+ }
5377
+ const out = [];
5378
+ for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
5379
+ const chunk = onlyFiles.slice(i, i + maxSqlVars);
5380
+ const placeholders = chunk.map(() => "?").join(",");
5381
+ out.push(
5382
+ ...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
5383
+ );
5384
+ }
5385
+ return out;
5386
+ }
5387
+ function getAllResolvedRefsWithStatement(stmtFn) {
5388
+ return stmtFn(
5389
+ "SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
5390
+ ).all();
5391
+ }
5392
+ function getAllImportRefsWithStatement(stmtFn) {
5393
+ return stmtFn(
5394
+ `SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
5395
+ r.call_type AS callType, r.line
5396
+ FROM refs r
5397
+ LEFT JOIN symbols s ON r.from_id = s.id
5398
+ WHERE r.call_type = 'import'
5399
+ ORDER BY r.line`
5400
+ ).all();
5401
+ }
5402
+ function resolveRefsWithStatement(stmtFn) {
5403
+ try {
5404
+ const result = stmtFn(
5405
+ `UPDATE refs
5406
+ SET to_id = s.id
5407
+ FROM (
5408
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5409
+ FROM symbols sym
5410
+ JOIN lang_family lf ON lf.lang = sym.lang
5411
+ GROUP BY sym.name, lf.family
5412
+ UNION ALL
5413
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5414
+ FROM symbols sym
5415
+ GROUP BY sym.name
5416
+ ) AS s,
5417
+ lang_family AS rf
5418
+ WHERE refs.to_id IS NULL
5419
+ AND refs.to_name IS NOT NULL
5420
+ AND rf.lang = refs.lang
5421
+ AND s.name = refs.to_name
5422
+ AND s.family = rf.family`
5423
+ ).run();
5424
+ return result.changes ?? 0;
5425
+ } catch {
5426
+ const result = stmtFn(
5427
+ `UPDATE refs SET to_id = (
5428
+ SELECT sym.id FROM symbols sym
5429
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5430
+ ORDER BY sym.id LIMIT 1
5431
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
5432
+ AND EXISTS (
5433
+ SELECT 1 FROM symbols sym
5434
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5435
+ )`
5436
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
5437
+ return result.changes ?? 0;
5438
+ }
5439
+ }
5440
+ function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVars, resolutions) {
5441
+ if (resolutions.length === 0) return 0;
5442
+ return runWithRetry(() => {
5443
+ db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5444
+ db.exec(
5445
+ `CREATE TEMP TABLE import_resolution (
5446
+ from_file TEXT NOT NULL,
5447
+ lang TEXT NOT NULL,
5448
+ module TEXT NOT NULL,
5449
+ to_file TEXT NOT NULL
5450
+ )`
5451
+ );
5452
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
5453
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
5454
+ const chunk = resolutions.slice(i, i + chunkSize);
5455
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5456
+ const binds = [];
5457
+ for (const entry of chunk) {
5458
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5459
+ }
5460
+ stmtFn(
5461
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5462
+ VALUES ${placeholders}`
5463
+ ).run(...binds);
5464
+ }
5465
+ db.exec(
5466
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
5467
+ ON import_resolution(module, lang, from_file)`
5468
+ );
5469
+ const result = stmtFn(
5470
+ `UPDATE refs
5471
+ SET to_file = (
5472
+ SELECT ir.to_file
5473
+ FROM temp.import_resolution ir
5474
+ JOIN symbols s ON s.id = refs.from_id
5475
+ WHERE ir.module = refs.module
5476
+ AND ir.lang = refs.lang
5477
+ AND ir.from_file = s.file
5478
+ LIMIT 1
5479
+ )
5480
+ WHERE refs.call_type = 'import'
5481
+ AND refs.module IS NOT NULL
5482
+ AND EXISTS (
5483
+ SELECT 1
5484
+ FROM temp.import_resolution ir
5485
+ JOIN symbols s ON s.id = refs.from_id
5486
+ WHERE ir.module = refs.module
5487
+ AND ir.lang = refs.lang
5488
+ AND ir.from_file = s.file
5489
+ )`
5490
+ ).run();
5491
+ db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5492
+ return result.changes ?? 0;
5493
+ });
5494
+ }
5495
+ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5496
+ const list = [...names].filter((name) => name.length > 0);
5497
+ if (list.length === 0) return 0;
5498
+ let total = 0;
5499
+ for (let i = 0; i < list.length; i += maxSqlVars) {
5500
+ const chunk = list.slice(i, i + maxSqlVars);
5501
+ const placeholders = chunk.map(() => "?").join(",");
5502
+ try {
5503
+ const result = stmtFn(
5504
+ `UPDATE refs
5505
+ SET to_id = s.id
5506
+ FROM (
5507
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5508
+ FROM symbols sym
5509
+ JOIN lang_family lf ON lf.lang = sym.lang
5510
+ WHERE sym.name IN (${placeholders})
5511
+ GROUP BY sym.name, lf.family
5512
+ UNION ALL
5513
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5514
+ FROM symbols sym
5515
+ WHERE sym.name IN (${placeholders})
5516
+ GROUP BY sym.name
5517
+ ) AS s,
5518
+ lang_family AS rf
5519
+ WHERE refs.to_name IN (${placeholders})
5520
+ AND rf.lang = refs.lang
5521
+ AND s.name = refs.to_name
5522
+ AND s.family = rf.family`
5523
+ ).run(...chunk, ...chunk, ...chunk);
5524
+ total += result.changes ?? 0;
5525
+ } catch {
5526
+ const result = stmtFn(
5527
+ `UPDATE refs SET to_id = (
5528
+ SELECT sym.id FROM symbols sym
5529
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5530
+ ORDER BY sym.id LIMIT 1
5531
+ ) WHERE refs.to_name IN (${placeholders})
5532
+ AND EXISTS (
5533
+ SELECT 1 FROM symbols sym
5534
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5535
+ )`
5536
+ ).run(LANG_FAMILY_WILDCARD, ...chunk, LANG_FAMILY_WILDCARD);
5537
+ total += result.changes ?? 0;
5538
+ }
5539
+ }
5540
+ return total;
5541
+ }
5542
+
5543
+ // src/codebase-index/writer-search.ts
5544
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
5545
+
5546
+ // src/codebase-index/lsp-kind.ts
5547
+ function lspKindToInternalKind(k) {
5548
+ switch (k) {
5549
+ case 5 /* Class */:
5550
+ return "class";
5551
+ case 6 /* Method */:
5552
+ return "method";
5553
+ case 7 /* Property */:
5554
+ case 8 /* Field */:
5555
+ return "property";
5556
+ case 9 /* Constructor */:
5557
+ return "class";
5558
+ case 10 /* Enum */:
5559
+ return "enum";
5560
+ case 11 /* Interface */:
5561
+ return "interface";
5562
+ case 12 /* Function */:
5563
+ return "function";
5564
+ case 13 /* Variable */:
5565
+ return "var";
5566
+ case 14 /* Constant */:
5567
+ return "const";
5568
+ case 22 /* EnumMember */:
5569
+ return "enum";
5570
+ case 26 /* TypeParameter */:
5571
+ return "type";
5572
+ case 3 /* Namespace */:
5573
+ return "namespace";
5574
+ default:
5575
+ return null;
5576
+ }
5577
+ }
5578
+
5182
5579
  // src/codebase-index/writer-search-helpers.ts
5183
5580
  var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
5184
5581
  function normalizeSearchLimit(limit) {
@@ -5232,6 +5629,173 @@ function mapWriterSearchRow(row, lspKind, score = 0, snippet = "") {
5232
5629
  };
5233
5630
  }
5234
5631
 
5632
+ // src/codebase-index/writer-search.ts
5633
+ function searchWithStatement(stmtFn, query, filter, opts) {
5634
+ const built = buildWriterSearchWhere(query, filter);
5635
+ if (built === null) return [];
5636
+ const { where, values } = built;
5637
+ const limit = normalizeSearchLimit(opts?.limit);
5638
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
5639
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
5640
+ const binds = limit !== void 0 ? [...values, limit] : values;
5641
+ const rows = stmtFn(sql).all(...binds);
5642
+ return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
5643
+ }
5644
+ function countSearchWithStatement(stmtFn, query, filter) {
5645
+ const built = buildWriterSearchWhere(query, filter);
5646
+ if (built === null) return 0;
5647
+ const row = stmtFn(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
5648
+ ...built.values
5649
+ );
5650
+ return Number(row?.n ?? 0);
5651
+ }
5652
+ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvailable, getOrBuildBm25, query, filter, limit) {
5653
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
5654
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
5655
+ const tokens = tokenise(query);
5656
+ if (tokens.length === 0 || !ftsAvailable) {
5657
+ return searchRankedFallbackWithStatement(
5658
+ stmtFn,
5659
+ searchFn,
5660
+ getOrBuildBm25,
5661
+ query,
5662
+ filter,
5663
+ safeLimit
5664
+ );
5665
+ }
5666
+ let effectiveKind = filter?.kind;
5667
+ if (filter?.lspKind !== void 0) {
5668
+ const mapped = lspKindToInternalKind(filter.lspKind);
5669
+ if (mapped === null) return { results: [], total: 0 };
5670
+ effectiveKind = mapped;
5671
+ }
5672
+ const longTokens = tokens.filter((t) => t.length >= 3);
5673
+ const shortTokens = tokens.filter((t) => t.length < 3);
5674
+ if (longTokens.length === 0) {
5675
+ return searchRankedFallbackWithStatement(
5676
+ stmtFn,
5677
+ searchFn,
5678
+ getOrBuildBm25,
5679
+ query,
5680
+ filter,
5681
+ safeLimit
5682
+ );
5683
+ }
5684
+ const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
5685
+ const conditions = ["symbols_fts MATCH ?"];
5686
+ const values = [match];
5687
+ for (const shortTok of shortTokens) {
5688
+ conditions.push("s.text LIKE ? ESCAPE '\\'");
5689
+ values.push(`%${escapeLike(shortTok)}%`);
5690
+ }
5691
+ if (effectiveKind) {
5692
+ conditions.push("s.kind = ?");
5693
+ values.push(effectiveKind);
5694
+ }
5695
+ if (filter?.lang) {
5696
+ conditions.push("s.lang = ?");
5697
+ values.push(filter.lang);
5698
+ }
5699
+ if (filter?.file) {
5700
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
5701
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
5702
+ }
5703
+ const where = conditions.join(" AND ");
5704
+ const countRows = stmtFn(
5705
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
5706
+ ).all(...values);
5707
+ const total = countRows[0] ? Number(countRows[0].n) : 0;
5708
+ if (total === 0) return { results: [], total: 0 };
5709
+ const bm25Rows = stmtFn(
5710
+ `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
5711
+ -bm25(symbols_fts) AS score,
5712
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
5713
+ FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
5714
+ WHERE ${where}
5715
+ ORDER BY
5716
+ CASE WHEN lower(s.name) = lower(?) THEN 0
5717
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
5718
+ ELSE 2 END,
5719
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
5720
+ LIMIT ?`
5721
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5722
+ if (vectorsAvailable && bm25Rows.length > 0) {
5723
+ const queryVec = embedText(query);
5724
+ const candidateIds = bm25Rows.map((r) => r.id);
5725
+ const placeholders = candidateIds.map(() => "?").join(",");
5726
+ const vecRows = stmtFn(
5727
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
5728
+ ).all(...candidateIds);
5729
+ const vecScores = vecRows.map((r) => ({
5730
+ id: r.symbol_id,
5731
+ sim: cosineSimilarity(queryVec, decodeVector(r.vector))
5732
+ })).sort((a, b) => b.sim - a.sim);
5733
+ const bm25Rank = /* @__PURE__ */ new Map();
5734
+ bm25Rows.forEach((r, i) => {
5735
+ bm25Rank.set(r.id, i);
5736
+ });
5737
+ const vecRank = /* @__PURE__ */ new Map();
5738
+ vecScores.forEach((r, i) => {
5739
+ vecRank.set(r.id, i);
5740
+ });
5741
+ const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
5742
+ const fusedScore = new Map(fused);
5743
+ const sorted = [...bm25Rows].sort(
5744
+ (a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
5745
+ );
5746
+ return {
5747
+ results: sorted.map(
5748
+ (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5749
+ ),
5750
+ total
5751
+ };
5752
+ }
5753
+ return {
5754
+ results: bm25Rows.map(
5755
+ (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5756
+ ),
5757
+ total
5758
+ };
5759
+ }
5760
+ function searchRankedFallbackWithStatement(stmtFn, searchFn, getOrBuildBm25, query, filter, limit) {
5761
+ if (!query.trim()) {
5762
+ const total2 = countSearchWithStatement(stmtFn, query, filter);
5763
+ if (total2 === 0) return { results: [], total: 0 };
5764
+ return { results: searchFn(query, filter, { limit }), total: total2 };
5765
+ }
5766
+ const total = countSearchWithStatement(stmtFn, query, filter);
5767
+ if (total === 0) return { results: [], total: 0 };
5768
+ const candidates = searchFn(query, filter, {
5769
+ limit: SEARCH_CANDIDATE_SCAN_CAP
5770
+ });
5771
+ if (candidates.length === 0) return { results: [], total: 0 };
5772
+ const candidateById = new Map(candidates.map((c) => [c.id, c]));
5773
+ const bm25 = getOrBuildBm25();
5774
+ const scored = bm25.score(query, (id) => candidateById.has(id));
5775
+ const q = query.trim().toLowerCase();
5776
+ const rank = (id) => {
5777
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
5778
+ if (name === q) return 0;
5779
+ if (name.startsWith(q)) return 1;
5780
+ return 2;
5781
+ };
5782
+ scored.sort((a, b) => {
5783
+ const rankDiff = rank(a.id) - rank(b.id);
5784
+ if (rankDiff !== 0) return rankDiff;
5785
+ const scoreDiff = b.score - a.score;
5786
+ if (scoreDiff !== 0) return scoreDiff;
5787
+ const left = expectDefined4(candidateById.get(a.id));
5788
+ const right = expectDefined4(candidateById.get(b.id));
5789
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
5790
+ });
5791
+ const qTokens = tokenise(query);
5792
+ const results = scored.slice(0, limit).map(({ id, score }) => {
5793
+ const c = expectDefined4(candidateById.get(id));
5794
+ return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
5795
+ });
5796
+ return { results, total };
5797
+ }
5798
+
5235
5799
  // src/codebase-index/writer-store-pool.ts
5236
5800
  var DEFAULT_MAX_WARM_STORES = 2;
5237
5801
  var StorePool = class {
@@ -5323,68 +5887,14 @@ var DB_FILE2 = "index.db";
5323
5887
  var MAX_STATEMENT_CACHE = 128;
5324
5888
  var IndexStore = class _IndexStore {
5325
5889
  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
5890
  atomicIndexUpdateActive = false;
5333
5891
  writeSavepointSequence = 0;
5334
- /** Absolute path to this project's index directory. */
5335
5892
  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
5893
  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
5894
  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
5895
  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
5896
  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
5897
  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
5898
  stmt(sql) {
5389
5899
  const cached = this.stmtCache.get(sql);
5390
5900
  if (cached !== void 0) {
@@ -5411,7 +5921,6 @@ var IndexStore = class _IndexStore {
5411
5921
  runWithRetry(fn) {
5412
5922
  return runSqliteWithRetry(fn);
5413
5923
  }
5414
- /** Run a complete index mutation as one WAL-visible publication. */
5415
5924
  async runAtomicIndexUpdate(job) {
5416
5925
  if (this.atomicIndexUpdateActive) return job();
5417
5926
  this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
@@ -5430,11 +5939,6 @@ var IndexStore = class _IndexStore {
5430
5939
  this.atomicIndexUpdateActive = false;
5431
5940
  }
5432
5941
  }
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
5942
  beginWriteTransaction() {
5439
5943
  if (this.atomicIndexUpdateActive) {
5440
5944
  const savepoint = `index_write_${++this.writeSavepointSequence}`;
@@ -5456,35 +5960,11 @@ var IndexStore = class _IndexStore {
5456
5960
  this.db.exec("ROLLBACK");
5457
5961
  }
5458
5962
  }
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
5963
  seedLangFamilies() {
5467
5964
  const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
5468
5965
  for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
5469
5966
  insert.run("", LANG_FAMILY_WILDCARD);
5470
5967
  }
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
5968
  repairMissingColumns() {
5489
5969
  const expected = [
5490
5970
  {
@@ -5589,27 +6069,8 @@ var IndexStore = class _IndexStore {
5589
6069
  }
5590
6070
  this.ensureNextSymbolIdSeeded();
5591
6071
  }
5592
- // ─── ID allocation & bulk helpers ────────────────────────────────────────────
5593
6072
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
5594
- /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
5595
6073
  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
6074
  ensureNextSymbolIdSeeded() {
5614
6075
  const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
5615
6076
  _IndexStore.NEXT_SYMBOL_ID_KEY
@@ -5622,10 +6083,6 @@ var IndexStore = class _IndexStore {
5622
6083
  String(next)
5623
6084
  );
5624
6085
  }
5625
- /**
5626
- * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
5627
- * so concurrent indexers cannot hand out overlapping ranges.
5628
- */
5629
6086
  allocateSymbolIds(count) {
5630
6087
  if (count <= 0) return this.getMaxSymbolId() + 1;
5631
6088
  this.ensureNextSymbolIdSeeded();
@@ -5639,16 +6096,8 @@ var IndexStore = class _IndexStore {
5639
6096
  );
5640
6097
  return start;
5641
6098
  }
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
6099
  invalidateIncomingRefsForFiles(files) {
5651
- if (files.length === 0) return [];
6100
+ if (files.length === 0) return /* @__PURE__ */ new Set();
5652
6101
  const placeholders = files.map(() => "?").join(",");
5653
6102
  const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
5654
6103
  ...files
@@ -5657,36 +6106,11 @@ var IndexStore = class _IndexStore {
5657
6106
  `UPDATE refs SET to_id = NULL
5658
6107
  WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5659
6108
  ).run(...files);
5660
- return names;
6109
+ return new Set(names);
5661
6110
  }
5662
- /** Resolve only refs whose target names may have changed. */
5663
6111
  resolveRefsForNamesUnsafe(names) {
5664
- const unique = [...new Set(names)].filter(Boolean);
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;
6112
+ return resolveRefsForNamesUnsafe((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, names);
5680
6113
  }
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
6114
  insertSymbols(symbols) {
5691
6115
  this.invalidateBm25();
5692
6116
  return this.runWithRetry(() => {
@@ -5715,12 +6139,6 @@ var IndexStore = class _IndexStore {
5715
6139
  if (this.ftsAvailable) {
5716
6140
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
5717
6141
  }
5718
- vectorRows.push({
5719
- id,
5720
- vector: encodeVector(
5721
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5722
- )
5723
- });
5724
6142
  result.push({ ...s, id });
5725
6143
  }
5726
6144
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -5770,11 +6188,6 @@ var IndexStore = class _IndexStore {
5770
6188
  }
5771
6189
  });
5772
6190
  }
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
6191
  deleteFile(file) {
5779
6192
  this.invalidateBm25();
5780
6193
  this.runWithRetry(() => {
@@ -5804,7 +6217,6 @@ var IndexStore = class _IndexStore {
5804
6217
  }
5805
6218
  });
5806
6219
  }
5807
- // ─── File metadata ──────────────────────────────────────────────────────────
5808
6220
  upsertFile(meta) {
5809
6221
  this.runWithRetry(() => {
5810
6222
  this.stmt(
@@ -5832,8 +6244,6 @@ var IndexStore = class _IndexStore {
5832
6244
  getAllFileMetas() {
5833
6245
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
5834
6246
  }
5835
- // ─── Project structure & module resolution ──────────────────────────────────
5836
- /** Store the Code Atlas grouping label for each indexed file. */
5837
6247
  setFilePackages(entries) {
5838
6248
  if (entries.size === 0) return;
5839
6249
  this.runWithRetry(() => {
@@ -5841,272 +6251,50 @@ var IndexStore = class _IndexStore {
5841
6251
  for (const [file, label] of entries) update.run(label, file);
5842
6252
  });
5843
6253
  }
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
6254
  getNamespaceDeclarations() {
5850
- return this.stmt(
5851
- `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
5852
- ).all();
6255
+ return getNamespaceDeclarationsWithStatement((sql) => this.stmt(sql));
5853
6256
  }
5854
- /** `file → package` for every indexed file that has a label. */
5855
6257
  getFilePackages() {
5856
- const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
5857
- return new Map(rows.map((row) => [row.file, row.package]));
6258
+ return getFilePackagesWithStatement((sql) => this.stmt(sql));
5858
6259
  }
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
6260
  getUnresolvedImports(onlyFiles) {
5866
- const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
5867
- FROM refs r
5868
- JOIN symbols s ON s.id = r.from_id
5869
- WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
5870
- if (!onlyFiles?.length) {
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;
6261
+ return getUnresolvedImportsWithStatement(
6262
+ (sql) => this.stmt(sql),
6263
+ _IndexStore.MAX_SQL_VARS,
6264
+ onlyFiles
6265
+ );
5882
6266
  }
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
6267
  applyImportResolutions(resolutions) {
5890
- if (resolutions.length === 0) return 0;
5891
- return this.runWithRetry(() => {
5892
- this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5893
- this.db.exec(
5894
- `CREATE TEMP TABLE import_resolution (
5895
- from_file TEXT NOT NULL,
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
6268
+ return applyImportResolutionsWithStatement(
6269
+ this.db,
6270
+ (sql) => this.stmt(sql),
6271
+ this.runWithRetry.bind(this),
6272
+ _IndexStore.MAX_SQL_VARS,
6273
+ resolutions
5955
6274
  );
5956
- return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
5957
6275
  }
5958
- /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
5959
- buildSearchWhere(query, filter) {
5960
- return buildWriterSearchWhere(query, filter);
6276
+ search(query, filter, opts) {
6277
+ return searchWithStatement((sql) => this.stmt(sql), query, filter, opts);
5961
6278
  }
5962
6279
  countSearch(query, filter) {
5963
- const built = this.buildSearchWhere(query, filter);
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);
6280
+ return countSearchWithStatement((sql) => this.stmt(sql), query, filter);
5969
6281
  }
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
6282
  searchRanked(query, filter, limit) {
5981
- const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
5982
- const safeLimit = Math.max(1, Math.min(rawLimit, 100));
5983
- const tokens = tokenise(query);
5984
- if (tokens.length === 0 || !this.ftsAvailable) {
5985
- return this.searchRankedFallback(query, filter, safeLimit);
5986
- }
5987
- let effectiveKind = filter?.kind;
5988
- if (filter?.lspKind !== void 0) {
5989
- const mapped = lspKindToInternalKind(filter.lspKind);
5990
- if (mapped === null) return { results: [], total: 0 };
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
- };
6283
+ return searchRankedWithStatement(
6284
+ (sql) => this.stmt(sql),
6285
+ this.search.bind(this),
6286
+ this.ftsAvailable,
6287
+ this.vectorsAvailable,
6288
+ this.getOrBuildBm25.bind(this),
6289
+ query,
6290
+ filter,
6291
+ limit
6292
+ );
6073
6293
  }
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
6294
  invalidateBm25() {
6096
6295
  this.bm25Dirty = true;
6097
6296
  this.bm25Cache = null;
6098
6297
  }
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
6298
  getOrBuildBm25() {
6111
6299
  if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
6112
6300
  const docs = this.getAllIndexable();
@@ -6114,57 +6302,12 @@ var IndexStore = class _IndexStore {
6114
6302
  this.bm25Dirty = false;
6115
6303
  return this.bm25Cache;
6116
6304
  }
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
6305
  getAllIndexable() {
6155
6306
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
6156
6307
  }
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
6308
  getMaxSymbolId() {
6165
6309
  return getMaxSymbolIdWithStatement((sql) => this.stmt(sql));
6166
6310
  }
6167
- // ─── Stats ───────────────────────────────────────────────────────────────────
6168
6311
  getStats() {
6169
6312
  return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
6170
6313
  }
@@ -6207,11 +6350,6 @@ var IndexStore = class _IndexStore {
6207
6350
  }
6208
6351
  });
6209
6352
  }
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
6353
  insertRefs(fromId, refs) {
6216
6354
  this.runWithRetry(() => {
6217
6355
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
@@ -6223,167 +6361,36 @@ var IndexStore = class _IndexStore {
6223
6361
  );
6224
6362
  });
6225
6363
  }
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
6364
  insertRefsBatch(refs) {
6238
6365
  if (refs.length === 0) return;
6239
6366
  this.runWithRetry(() => {
6240
6367
  bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refs);
6241
6368
  });
6242
6369
  }
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
6370
  commitBatch(entries, options = {}) {
6262
- if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
6263
- return [];
6264
- }
6265
6371
  this.invalidateBm25();
6266
6372
  return this.runWithRetry(() => {
6267
6373
  const ownsTransaction = this.beginWriteTransaction();
6268
6374
  try {
6269
- const affectedNames = /* @__PURE__ */ new Set();
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(
6375
+ const result = commitBatchWithStatement(
6340
6376
  (sql) => this.stmt(sql),
6341
6377
  _IndexStore.MAX_SQL_VARS,
6342
6378
  this.ftsAvailable,
6343
- ftsRows
6344
- );
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`
6379
+ this.vectorsAvailable,
6380
+ this.allocateSymbolIds.bind(this),
6381
+ this.invalidateIncomingRefsForFiles.bind(this),
6382
+ this.resolveRefsForNamesUnsafe.bind(this),
6383
+ entries,
6384
+ options
6362
6385
  );
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
6386
  this.commitWriteTransaction(ownsTransaction);
6376
- return allInserted;
6387
+ return result;
6377
6388
  } catch (err) {
6378
6389
  this.rollbackWriteTransaction(ownsTransaction);
6379
6390
  throw err;
6380
6391
  }
6381
6392
  });
6382
6393
  }
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
6394
  deleteRefsForFile(file) {
6388
6395
  this.runWithRetry(() => {
6389
6396
  this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
@@ -6391,64 +6398,12 @@ var IndexStore = class _IndexStore {
6391
6398
  );
6392
6399
  });
6393
6400
  }
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
6401
  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
- });
6402
+ return this.runWithRetry(() => resolveRefsWithStatement((sql) => this.stmt(sql)));
6444
6403
  }
6445
6404
  resolveRefsForNames(names) {
6446
6405
  return this.runWithRetry(() => this.resolveRefsForNamesUnsafe(names));
6447
6406
  }
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
6407
  replaceEmptyFile(meta) {
6453
6408
  this.invalidateBm25();
6454
6409
  this.runWithRetry(() => {
@@ -6494,20 +6449,12 @@ var IndexStore = class _IndexStore {
6494
6449
  }
6495
6450
  });
6496
6451
  }
6497
- /** Best-effort query planner refresh after a large reindex. */
6498
6452
  optimize() {
6499
6453
  try {
6500
6454
  this.db.exec("PRAGMA optimize");
6501
6455
  } catch {
6502
6456
  }
6503
6457
  }
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
6458
  compactIfNeeded(options = {}) {
6512
6459
  const minBytes = options.minBytes ?? 256 * 1024 * 1024;
6513
6460
  const minFreeRatio = options.minFreeRatio ?? 0.35;
@@ -6534,115 +6481,44 @@ var IndexStore = class _IndexStore {
6534
6481
  return false;
6535
6482
  }
6536
6483
  }
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
6484
  findIncomingCallsByName(symbolName, file, limit = 100) {
6542
6485
  return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6543
6486
  }
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
6487
  findOutgoingCallsByName(symbolName, file, limit = 100) {
6549
6488
  return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6550
6489
  }
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
6490
  findTransitiveIncomingCallsByName(symbolName, file, limit = 200) {
6558
6491
  return findTransitiveIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6559
6492
  }
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
6493
  findTransitiveOutgoingCallsByName(symbolName, file, limit = 200) {
6566
6494
  return findTransitiveOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6567
6495
  }
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
6496
  findReachableSymbolIds(seedIds) {
6574
6497
  return findReachableSymbolIds((sql) => this.stmt(sql), seedIds);
6575
6498
  }
6576
- /**
6577
- * Find all references TO a given symbol (who calls / uses this symbol?).
6578
- */
6579
6499
  findRefsTo(symbolId) {
6580
6500
  return findRefsToWithStatement((sql) => this.stmt(sql), symbolId);
6581
6501
  }
6582
- /**
6583
- * Find all references FROM a given symbol (what does this symbol call/use?).
6584
- */
6585
6502
  findRefsFrom(symbolId) {
6586
6503
  return findRefsFromWithStatement((sql) => this.stmt(sql), symbolId);
6587
6504
  }
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
6505
  getPackageGraph() {
6595
6506
  return getPackageGraphWithStatement((sql) => this.stmt(sql));
6596
6507
  }
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
6508
  getFileGraph(packageFilter) {
6602
6509
  return getFileGraphWithStatement((sql) => this.stmt(sql), packageFilter);
6603
6510
  }
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
6511
  getSymbolGraph(fileFilter) {
6609
6512
  return getSymbolGraphWithStatement((sql) => this.stmt(sql), fileFilter);
6610
6513
  }
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
6514
  getAllSymbols() {
6616
6515
  return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
6617
6516
  }
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
6517
  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();
6518
+ return getAllResolvedRefsWithStatement((sql) => this.stmt(sql));
6627
6519
  }
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
6520
  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();
6521
+ return getAllImportRefsWithStatement((sql) => this.stmt(sql));
6646
6522
  }
6647
6523
  close() {
6648
6524
  this.stmtCache.clear();
@@ -7342,11 +7218,10 @@ function outgoingCallsService(args) {
7342
7218
 
7343
7219
  // src/codebase-index/project-server-client.ts
7344
7220
  import { spawn as spawn3 } from "node:child_process";
7345
- import * as fs11 from "node:fs";
7221
+ import * as fs12 from "node:fs";
7346
7222
  import * as net from "node:net";
7347
7223
  import { StringDecoder } from "node:string_decoder";
7348
- import { fileURLToPath as fileURLToPath4 } from "node:url";
7349
- import { checkUnixSocketPath } from "@wrongstack/core/utils";
7224
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
7350
7225
 
7351
7226
  // src/codebase-index/binary-frame.ts
7352
7227
  import { decode, encode } from "@msgpack/msgpack";
@@ -7420,7 +7295,10 @@ function encodeProjectServerMessage(message) {
7420
7295
  `;
7421
7296
  }
7422
7297
 
7423
- // src/codebase-index/project-server-client.ts
7298
+ // src/codebase-index/project-server-client-state.ts
7299
+ import * as fs11 from "node:fs";
7300
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
7301
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
7424
7302
  var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
7425
7303
  var SERVER_START_TIMEOUT_MS = 1e4;
7426
7304
  var SERVER_CONTROL_TIMEOUT_MS = 5e3;
@@ -7440,6 +7318,9 @@ var latestConnectionState = {
7440
7318
  status: "offline",
7441
7319
  connected: false
7442
7320
  };
7321
+ function setLatestConnectionState(state) {
7322
+ latestConnectionState = state;
7323
+ }
7443
7324
  function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
7444
7325
  if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
7445
7326
  return { kind: "inline-requested" };
@@ -7535,6 +7416,8 @@ function delay(ms) {
7535
7416
  function cancellationError(signal) {
7536
7417
  return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
7537
7418
  }
7419
+
7420
+ // src/codebase-index/project-server-client.ts
7538
7421
  var ProjectServerConnection = class {
7539
7422
  constructor(projectRoot, indexDir, endpoint) {
7540
7423
  this.projectRoot = projectRoot;
@@ -7665,11 +7548,11 @@ var ProjectServerConnection = class {
7665
7548
  this.close();
7666
7549
  }
7667
7550
  }
7668
- async configure(watchExternal, debounceMs) {
7551
+ async configure(watchExternal, debounceMs, coalesceWindowMs) {
7669
7552
  await this.ensureConnected(true);
7670
7553
  const startedAt = Date.now();
7671
7554
  const result = await this.request(
7672
- { type: "configure", watchExternal, debounceMs },
7555
+ { type: "configure", watchExternal, debounceMs, coalesceWindowMs },
7673
7556
  { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
7674
7557
  );
7675
7558
  if (isProjectIndexServerHealth(result.health)) {
@@ -7713,7 +7596,7 @@ var ProjectServerConnection = class {
7713
7596
  currentAuthToken() {
7714
7597
  if (this.authToken === void 0) {
7715
7598
  try {
7716
- const raw = fs11.readFileSync(
7599
+ const raw = fs12.readFileSync(
7717
7600
  projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
7718
7601
  "utf8"
7719
7602
  );
@@ -8029,11 +7912,11 @@ var ProjectServerConnection = class {
8029
7912
  if (!url) throw new Error("built codebase-index project server is unavailable");
8030
7913
  if (process.platform !== "win32") {
8031
7914
  try {
8032
- fs11.rmSync(this.endpoint, { force: true });
7915
+ fs12.rmSync(this.endpoint, { force: true });
8033
7916
  } catch {
8034
7917
  }
8035
7918
  }
8036
- const args = [fileURLToPath4(url), "--project-root", this.projectRoot];
7919
+ const args = [fileURLToPath5(url), "--project-root", this.projectRoot];
8037
7920
  if (this.indexDir) args.push("--index-dir", this.indexDir);
8038
7921
  const child = spawn3(process.execPath, args, {
8039
7922
  detached: true,
@@ -8053,8 +7936,8 @@ var ProjectServerConnection = class {
8053
7936
  process.kill(pid);
8054
7937
  const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
8055
7938
  try {
8056
- const metadata = JSON.parse(fs11.readFileSync(metadataPath, "utf8"));
8057
- if (metadata.pid === pid) fs11.rmSync(metadataPath, { force: true });
7939
+ const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
7940
+ if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
8058
7941
  } catch {
8059
7942
  }
8060
7943
  return true;
@@ -8071,10 +7954,12 @@ function forgetConnection(endpoint, connection) {
8071
7954
  connection.close();
8072
7955
  connectionStates.delete(endpoint);
8073
7956
  if (latestConnectionState.endpoint !== endpoint) return;
8074
- latestConnectionState = [...connectionStates.values()].at(-1) ?? {
8075
- status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
8076
- connected: false
8077
- };
7957
+ setLatestConnectionState(
7958
+ [...connectionStates.values()].at(-1) ?? {
7959
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
7960
+ connected: false
7961
+ }
7962
+ );
8078
7963
  }
8079
7964
  function trimConnectionCache(protectedConnection) {
8080
7965
  if (connections.size <= MAX_CACHED_CONNECTIONS) return;
@@ -8153,7 +8038,7 @@ function resolveWorkerUrl() {
8153
8038
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8154
8039
  try {
8155
8040
  const url = new URL(rel, import.meta.url);
8156
- if (url.protocol === "file:" && fs12.existsSync(fileURLToPath5(url))) return url;
8041
+ if (url.protocol === "file:" && fs13.existsSync(fileURLToPath6(url))) return url;
8157
8042
  } catch {
8158
8043
  }
8159
8044
  }
@@ -8406,7 +8291,7 @@ var readTool = {
8406
8291
  const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
8407
8292
  let stat3;
8408
8293
  try {
8409
- stat3 = await fs13.stat(absPath);
8294
+ stat3 = await fs14.stat(absPath);
8410
8295
  } catch (err) {
8411
8296
  const code = err.code;
8412
8297
  if (code === "ENOENT") {
@@ -8458,7 +8343,7 @@ var readTool = {
8458
8343
  ...symResult2?.symbols ? { symbols: symResult2.symbols } : {}
8459
8344
  };
8460
8345
  }
8461
- const buf = await fs13.readFile(absPath);
8346
+ const buf = await fs14.readFile(absPath);
8462
8347
  if (isBinaryBuffer(buf)) {
8463
8348
  throw new FsError({
8464
8349
  message: `read: "${input.path}" appears to be binary`,