@wrongstack/tools 0.306.3 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/audit.js +50 -9
  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 +11315 -9317
  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 +40 -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 +4141 -1479
  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 +751 -909
  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 +723 -891
  26. package/dist/codebase-index/writer-mutations.d.ts +26 -0
  27. package/dist/codebase-index/writer-refs.d.ts +50 -0
  28. package/dist/codebase-index/writer-search.d.ts +30 -0
  29. package/dist/codebase-index/writer.d.ts +2 -320
  30. package/dist/diff.js +3 -2
  31. package/dist/document.js +37 -2
  32. package/dist/e2e.js +3 -2
  33. package/dist/edit.js +8558 -348
  34. package/dist/exec.js +76 -22
  35. package/dist/fetch.js +11 -9
  36. package/dist/format.js +50 -9
  37. package/dist/glob.js +8 -3
  38. package/dist/grep.js +22 -10
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +11411 -9440
  41. package/dist/install.js +50 -9
  42. package/dist/json.js +91 -17
  43. package/dist/kanban-board-actions.d.ts +4 -0
  44. package/dist/kanban-lifecycle-actions.d.ts +4 -0
  45. package/dist/kanban-serializer.d.ts +21 -0
  46. package/dist/kanban.js +985 -1034
  47. package/dist/languages/index.js +17 -10
  48. package/dist/lint.js +50 -9
  49. package/dist/logs.js +17 -6
  50. package/dist/next-steps.d.ts +8 -0
  51. package/dist/next-steps.js +18 -0
  52. package/dist/outdated.js +18 -11
  53. package/dist/pack.js +11308 -9317
  54. package/dist/patch.js +8301 -77
  55. package/dist/plan.js +1231 -1281
  56. package/dist/process-registry.js +14 -8
  57. package/dist/ps-slash.js +65 -30
  58. package/dist/read.js +754 -912
  59. package/dist/replace.js +8456 -195
  60. package/dist/scaffold.js +36 -1
  61. package/dist/search.js +21 -15
  62. package/dist/security-ast-scan-tool.d.ts +43 -0
  63. package/dist/session-kanban-graph.d.ts +9 -0
  64. package/dist/session-kanban-sync.d.ts +31 -0
  65. package/dist/session-kanban.d.ts +5 -141
  66. package/dist/session-kanban.js +367 -360
  67. package/dist/task.js +1157 -1207
  68. package/dist/test.js +50 -9
  69. package/dist/todo.js +2187 -2237
  70. package/dist/tool-diff.js +6 -1
  71. package/dist/tool-summary.js +4 -2
  72. package/dist/tool-tier.js +11315 -9317
  73. package/dist/tree.js +36 -1
  74. package/dist/typecheck.js +51 -10
  75. package/dist/write.js +8374 -152
  76. package/package.json +7 -7
@@ -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 path9 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 = path9.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"() {
@@ -3467,10 +3521,7 @@ var LANG_IMPORTS = {
3467
3521
  // quoted path. A stray match elsewhere resolves to no file and is dropped.
3468
3522
  { re: /^[ \t]*(?:[A-Za-z_.]\w*\s+)?"([^"]+)"\s*$/gm }
3469
3523
  ],
3470
- py: [
3471
- { re: /^[ \t]*import\s+([\w.]+)/gm },
3472
- { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }
3473
- ],
3524
+ py: [{ re: /^[ \t]*import\s+([\w.]+)/gm }, { re: /^[ \t]*from\s+([.\w]+)\s+import\b/gm }],
3474
3525
  rs: [
3475
3526
  // use a::b::C; | use a::b::{C, D}; → the path before any brace
3476
3527
  { re: /^[ \t]*(?:pub\s+)?use\s+([\w:]+?)(?:::\{|\s*;|\s+as\b)/gm },
@@ -3807,10 +3858,7 @@ function defaultWorkerCount() {
3807
3858
  return Math.max(1, Math.min(4, cores - 1));
3808
3859
  }
3809
3860
  function resolveWorkerScriptUrl() {
3810
- for (const rel of [
3811
- "./parser-worker-script.js",
3812
- "./codebase-index/parser-worker-script.js"
3813
- ]) {
3861
+ for (const rel of ["./parser-worker-script.js", "./codebase-index/parser-worker-script.js"]) {
3814
3862
  try {
3815
3863
  const url = new URL(rel, import.meta.url);
3816
3864
  if (url.protocol === "file:" && fs6.existsSync(fileURLToPath2(url))) return url;
@@ -3826,7 +3874,6 @@ function getParserPool() {
3826
3874
  }
3827
3875
 
3828
3876
  // src/codebase-index/writer.ts
3829
- import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
3830
3877
  import * as fs8 from "node:fs";
3831
3878
  import * as path11 from "node:path";
3832
3879
 
@@ -3922,39 +3969,6 @@ var Bm25Index = class {
3922
3969
  // src/codebase-index/writer.ts
3923
3970
  init_languages();
3924
3971
 
3925
- // src/codebase-index/lsp-kind.ts
3926
- function lspKindToInternalKind(k) {
3927
- switch (k) {
3928
- case 5 /* Class */:
3929
- return "class";
3930
- case 6 /* Method */:
3931
- return "method";
3932
- case 7 /* Property */:
3933
- case 8 /* Field */:
3934
- return "property";
3935
- case 9 /* Constructor */:
3936
- return "class";
3937
- case 10 /* Enum */:
3938
- return "enum";
3939
- case 11 /* Interface */:
3940
- return "interface";
3941
- case 12 /* Function */:
3942
- return "function";
3943
- case 13 /* Variable */:
3944
- return "var";
3945
- case 14 /* Constant */:
3946
- return "const";
3947
- case 22 /* EnumMember */:
3948
- return "enum";
3949
- case 26 /* TypeParameter */:
3950
- return "type";
3951
- case 3 /* Namespace */:
3952
- return "namespace";
3953
- default:
3954
- return null;
3955
- }
3956
- }
3957
-
3958
3972
  // src/codebase-index/schema.ts
3959
3973
  var SCHEMA_VERSION = 4;
3960
3974
 
@@ -4101,91 +4115,14 @@ function runSqliteWithRetry(fn) {
4101
4115
  throw lastError;
4102
4116
  }
4103
4117
 
4104
- // src/codebase-index/vector-search.ts
4105
- var RRF_K = 60;
4106
- var VECTOR_DIMENSIONS = 384;
4107
- var NGRAM_SIZE = 3;
4108
- function embedText(text) {
4109
- const vec = new Float32Array(VECTOR_DIMENSIONS);
4110
- const normalized = text.toLowerCase().trim();
4111
- if (normalized.length < NGRAM_SIZE) {
4112
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
4113
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
4114
- const ngram = padded.slice(i, i + NGRAM_SIZE);
4115
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4116
- vec[bucket] += 1;
4117
- }
4118
- } else {
4119
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
4120
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
4121
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4122
- vec[bucket] += 1;
4123
- }
4124
- }
4125
- let norm = 0;
4126
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4127
- norm += vec[i] * vec[i];
4128
- }
4129
- norm = Math.sqrt(norm);
4130
- if (norm > 0) {
4131
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4132
- vec[i] /= norm;
4133
- }
4134
- }
4135
- return vec;
4136
- }
4137
- function hashNgram(str) {
4138
- let hash = 2166136261;
4139
- for (let i = 0; i < str.length; i++) {
4140
- hash ^= str.charCodeAt(i);
4141
- hash = Math.imul(hash, 16777619);
4142
- }
4143
- return hash >>> 0;
4144
- }
4145
- function cosineSimilarity(a, b) {
4146
- let dot = 0;
4147
- const len = Math.min(a.length, b.length);
4148
- for (let i = 0; i < len; i++) {
4149
- dot += a[i] * b[i];
4150
- }
4151
- return dot;
4152
- }
4153
- function encodeVector(vec) {
4154
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
4155
- }
4156
- function decodeVector(buf) {
4157
- const view = new DataView(
4158
- buf.buffer,
4159
- buf.byteOffset,
4160
- buf.byteLength
4161
- );
4162
- const copy = new Float32Array(buf.byteLength / 4);
4163
- for (let i = 0; i < copy.length; i++) {
4164
- copy[i] = view.getFloat32(i * 4, true);
4165
- }
4166
- return copy;
4167
- }
4168
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
4169
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
4170
- const scored = [];
4171
- for (const id of allIds) {
4172
- const bm25Rank = bm25Ranks.get(id);
4173
- const vecRank = vectorRanks.get(id);
4174
- let score = 0;
4175
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
4176
- if (vecRank !== void 0) score += 1 / (k + vecRank);
4177
- scored.push([id, score]);
4178
- }
4179
- scored.sort((a, b) => b[1] - a[1]);
4180
- return scored;
4181
- }
4182
-
4183
4118
  // src/codebase-index/writer-admin.ts
4184
4119
  import * as fs7 from "node:fs";
4185
4120
  import * as path10 from "node:path";
4186
4121
  var DB_FILE = "index.db";
4187
4122
  function getAllIndexableWithStatement(stmt) {
4188
- return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
4123
+ return stmt("SELECT id, text FROM symbols").all().map(
4124
+ ({ id, text }) => ({ id, text })
4125
+ );
4189
4126
  }
4190
4127
  function getMaxSymbolIdWithStatement(stmt) {
4191
4128
  const rows = stmt("SELECT MAX(id) AS m FROM symbols").all();
@@ -4525,7 +4462,8 @@ function resolveSymbolIds(stmt, symbolName, file) {
4525
4462
  }
4526
4463
  function findIncomingCallsByName(stmt, symbolName, file, limit) {
4527
4464
  const targetIds = resolveSymbolIds(stmt, symbolName, file);
4528
- if (targetIds.length === 0) return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
4465
+ if (targetIds.length === 0)
4466
+ return { calls: [], symbolFound: false, ambiguous: false, totalMatches: 0 };
4529
4467
  let matchIds = targetIds;
4530
4468
  let ambiguous = false;
4531
4469
  if (file !== void 0) {
@@ -4576,11 +4514,17 @@ function findIncomingCallsByName(stmt, symbolName, file, limit) {
4576
4514
  }
4577
4515
  rows.sort((a, b) => a.ref_line - b.ref_line || a.sym_id - b.sym_id);
4578
4516
  const allCalls = rows.map(mapCallSiteRow);
4579
- return { calls: allCalls.slice(0, limit), symbolFound: true, ambiguous, totalMatches: allCalls.length };
4517
+ return {
4518
+ calls: allCalls.slice(0, limit),
4519
+ symbolFound: true,
4520
+ ambiguous,
4521
+ totalMatches: allCalls.length
4522
+ };
4580
4523
  }
4581
4524
  function findOutgoingCallsByName(stmt, symbolName, file, limit) {
4582
4525
  const sourceIds = resolveSymbolIds(stmt, symbolName, file);
4583
- if (sourceIds.length === 0) return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
4526
+ if (sourceIds.length === 0)
4527
+ return { calls: [], symbolFound: false, unresolvedCount: 0, totalMatches: 0 };
4584
4528
  const unresolvedCount = chunkedIdScalar(
4585
4529
  stmt,
4586
4530
  sourceIds,
@@ -4984,6 +4928,184 @@ function resolveIndexDir(projectRoot, override) {
4984
4928
  return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
4985
4929
  }
4986
4930
 
4931
+ // src/codebase-index/vector-search.ts
4932
+ var RRF_K = 60;
4933
+ var VECTOR_DIMENSIONS = 384;
4934
+ var NGRAM_SIZE = 3;
4935
+ function embedText(text) {
4936
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
4937
+ const normalized = text.toLowerCase().trim();
4938
+ if (normalized.length < NGRAM_SIZE) {
4939
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
4940
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
4941
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
4942
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4943
+ vec[bucket] += 1;
4944
+ }
4945
+ } else {
4946
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
4947
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
4948
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4949
+ vec[bucket] += 1;
4950
+ }
4951
+ }
4952
+ let norm = 0;
4953
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4954
+ norm += vec[i] * vec[i];
4955
+ }
4956
+ norm = Math.sqrt(norm);
4957
+ if (norm > 0) {
4958
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4959
+ vec[i] /= norm;
4960
+ }
4961
+ }
4962
+ return vec;
4963
+ }
4964
+ function hashNgram(str) {
4965
+ let hash = 2166136261;
4966
+ for (let i = 0; i < str.length; i++) {
4967
+ hash ^= str.charCodeAt(i);
4968
+ hash = Math.imul(hash, 16777619);
4969
+ }
4970
+ return hash >>> 0;
4971
+ }
4972
+ function cosineSimilarity(a, b) {
4973
+ let dot = 0;
4974
+ const len = Math.min(a.length, b.length);
4975
+ for (let i = 0; i < len; i++) {
4976
+ dot += a[i] * b[i];
4977
+ }
4978
+ return dot;
4979
+ }
4980
+ function encodeVector(vec) {
4981
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
4982
+ }
4983
+ function decodeVector(buf) {
4984
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
4985
+ const copy = new Float32Array(buf.byteLength / 4);
4986
+ for (let i = 0; i < copy.length; i++) {
4987
+ copy[i] = view.getFloat32(i * 4, true);
4988
+ }
4989
+ return copy;
4990
+ }
4991
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
4992
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
4993
+ const scored = [];
4994
+ for (const id of allIds) {
4995
+ const bm25Rank = bm25Ranks.get(id);
4996
+ const vecRank = vectorRanks.get(id);
4997
+ let score = 0;
4998
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
4999
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
5000
+ scored.push([id, score]);
5001
+ }
5002
+ scored.sort((a, b) => b[1] - a[1]);
5003
+ return scored;
5004
+ }
5005
+
5006
+ // src/codebase-index/writer-mutations.ts
5007
+ function commitBatchWithStatement(stmtFn, maxSqlVars, ftsAvailable, vectorsAvailable, allocateSymbolIds, invalidateIncomingRefsForFiles, resolveRefsForNamesUnsafe2, entries, options = {}) {
5008
+ if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
5009
+ return [];
5010
+ }
5011
+ const affectedNames = /* @__PURE__ */ new Set();
5012
+ for (const entry of entries) {
5013
+ for (const symbol of entry.symbols) affectedNames.add(symbol.name);
5014
+ for (const ref of entry.refs) affectedNames.add(ref.toName);
5015
+ }
5016
+ if (options.deleteForFiles && options.deleteForFiles.length > 0) {
5017
+ const placeholders = options.deleteForFiles.map(() => "?").join(",");
5018
+ for (const name of invalidateIncomingRefsForFiles(options.deleteForFiles)) {
5019
+ affectedNames.add(name);
5020
+ }
5021
+ if (ftsAvailable) {
5022
+ stmtFn(
5023
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5024
+ ).run(...options.deleteForFiles);
5025
+ }
5026
+ if (vectorsAvailable) {
5027
+ stmtFn(
5028
+ `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5029
+ ).run(...options.deleteForFiles);
5030
+ }
5031
+ stmtFn(
5032
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5033
+ ).run(...options.deleteForFiles);
5034
+ stmtFn(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
5035
+ }
5036
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
5037
+ let nextId = allocateSymbolIds(totalSymbols);
5038
+ const allInserted = [];
5039
+ const refsToInsert = [];
5040
+ const bulkSyms = [];
5041
+ const ftsRows = [];
5042
+ const vectorRows = [];
5043
+ for (const entry of entries) {
5044
+ const insertedForEntry = [];
5045
+ for (const s of entry.symbols) {
5046
+ const id = nextId++;
5047
+ bulkSyms.push({
5048
+ id,
5049
+ lang: s.lang,
5050
+ kind: s.kind,
5051
+ name: s.name,
5052
+ file: s.file,
5053
+ line: s.line,
5054
+ col: s.col,
5055
+ signature: s.signature,
5056
+ docComment: s.docComment,
5057
+ scope: s.scope,
5058
+ text: s.text
5059
+ });
5060
+ if (ftsAvailable) {
5061
+ ftsRows.push({
5062
+ id,
5063
+ text: buildIndexableText(s.name, s.signature, s.docComment)
5064
+ });
5065
+ }
5066
+ vectorRows.push({
5067
+ id,
5068
+ vector: encodeVector(
5069
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5070
+ )
5071
+ });
5072
+ const inserted = { ...s, id };
5073
+ allInserted.push(inserted);
5074
+ insertedForEntry.push(inserted);
5075
+ }
5076
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
5077
+ }
5078
+ bulkInsertSymbolsWithStatement((sql) => stmtFn(sql), maxSqlVars, bulkSyms);
5079
+ bulkInsertFtsWithStatement((sql) => stmtFn(sql), maxSqlVars, ftsAvailable, ftsRows);
5080
+ if (vectorsAvailable) {
5081
+ bulkInsertVectorsWithStatement((sql) => stmtFn(sql), maxSqlVars, vectorRows);
5082
+ }
5083
+ bulkInsertRefsWithStatement((sql) => stmtFn(sql), maxSqlVars, refsToInsert);
5084
+ const upsertStmt = stmtFn(
5085
+ `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
5086
+ VALUES (?, ?, ?, ?, ?, ?)
5087
+ ON CONFLICT(file) DO UPDATE SET
5088
+ lang = excluded.lang,
5089
+ mtime_ms = excluded.mtime_ms,
5090
+ content_hash = excluded.content_hash,
5091
+ symbol_count = excluded.symbol_count,
5092
+ last_indexed = excluded.last_indexed`
5093
+ );
5094
+ const now = Date.now();
5095
+ for (const entry of entries) {
5096
+ upsertStmt.run(
5097
+ entry.file,
5098
+ entry.lang,
5099
+ entry.mtimeMs,
5100
+ entry.contentHash ?? "",
5101
+ entry.symbolCount,
5102
+ now
5103
+ );
5104
+ }
5105
+ resolveRefsForNamesUnsafe2(affectedNames);
5106
+ return allInserted;
5107
+ }
5108
+
4987
5109
  // src/codebase-index/writer-pragmas.ts
4988
5110
  import { sqliteCachePragmas } from "@wrongstack/core/utils";
4989
5111
  function applyIndexStorePragmas(db) {
@@ -5096,25 +5218,256 @@ var SYMBOL_VECTORS_TABLE_SQL = `
5096
5218
  );
5097
5219
  `;
5098
5220
 
5099
- // src/codebase-index/writer-search-helpers.ts
5100
- var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
5101
- function normalizeSearchLimit(limit) {
5102
- return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
5221
+ // src/codebase-index/writer-refs.ts
5222
+ var FAMILY_MATCH_SQL = `(
5223
+ sym.lang = refs.lang
5224
+ OR EXISTS (
5225
+ SELECT 1 FROM lang_family lf1
5226
+ JOIN lang_family lf2 ON lf1.family = lf2.family
5227
+ WHERE lf1.lang = sym.lang AND lf2.lang = refs.lang
5228
+ )
5229
+ OR ? IN (
5230
+ SELECT family FROM lang_family WHERE lang = refs.lang
5231
+ )
5232
+ )`;
5233
+ function getNamespaceDeclarationsWithStatement(stmtFn) {
5234
+ return stmtFn(
5235
+ `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
5236
+ ).all();
5103
5237
  }
5104
- function buildWriterSearchWhere(query, filter) {
5105
- const conditions = [];
5106
- const values = [];
5107
- let effectiveKind = filter?.kind;
5108
- if (filter?.lspKind !== void 0) {
5109
- const mapped = lspKindToInternalKind(filter.lspKind);
5110
- if (mapped !== null) {
5111
- effectiveKind = mapped;
5112
- } else {
5113
- return null;
5114
- }
5238
+ function getFilePackagesWithStatement(stmtFn) {
5239
+ const rows = stmtFn("SELECT file, package FROM files WHERE package != ''").all();
5240
+ return new Map(rows.map((row) => [row.file, row.package]));
5241
+ }
5242
+ function getUnresolvedImportsWithStatement(stmtFn, maxSqlVars, onlyFiles) {
5243
+ const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
5244
+ FROM refs r
5245
+ JOIN symbols s ON s.id = r.from_id
5246
+ WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
5247
+ if (!onlyFiles?.length) {
5248
+ return stmtFn(base).all();
5115
5249
  }
5116
- if (effectiveKind) {
5117
- conditions.push("kind = ?");
5250
+ const out = [];
5251
+ for (let i = 0; i < onlyFiles.length; i += maxSqlVars) {
5252
+ const chunk = onlyFiles.slice(i, i + maxSqlVars);
5253
+ const placeholders = chunk.map(() => "?").join(",");
5254
+ out.push(
5255
+ ...stmtFn(`${base} AND s.file IN (${placeholders})`).all(...chunk)
5256
+ );
5257
+ }
5258
+ return out;
5259
+ }
5260
+ function getAllResolvedRefsWithStatement(stmtFn) {
5261
+ return stmtFn(
5262
+ "SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
5263
+ ).all();
5264
+ }
5265
+ function getAllImportRefsWithStatement(stmtFn) {
5266
+ return stmtFn(
5267
+ `SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
5268
+ r.call_type AS callType, r.line
5269
+ FROM refs r
5270
+ LEFT JOIN symbols s ON r.from_id = s.id
5271
+ WHERE r.call_type = 'import'
5272
+ ORDER BY r.line`
5273
+ ).all();
5274
+ }
5275
+ function resolveRefsWithStatement(stmtFn) {
5276
+ try {
5277
+ const result = stmtFn(
5278
+ `UPDATE refs
5279
+ SET to_id = s.id
5280
+ FROM (
5281
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5282
+ FROM symbols sym
5283
+ JOIN lang_family lf ON lf.lang = sym.lang
5284
+ GROUP BY sym.name, lf.family
5285
+ UNION ALL
5286
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5287
+ FROM symbols sym
5288
+ GROUP BY sym.name
5289
+ ) AS s,
5290
+ lang_family AS rf
5291
+ WHERE refs.to_id IS NULL
5292
+ AND refs.to_name IS NOT NULL
5293
+ AND rf.lang = refs.lang
5294
+ AND s.name = refs.to_name
5295
+ AND s.family = rf.family`
5296
+ ).run();
5297
+ return result.changes ?? 0;
5298
+ } catch {
5299
+ const result = stmtFn(
5300
+ `UPDATE refs SET to_id = (
5301
+ SELECT sym.id FROM symbols sym
5302
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5303
+ ORDER BY sym.id LIMIT 1
5304
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
5305
+ AND EXISTS (
5306
+ SELECT 1 FROM symbols sym
5307
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5308
+ )`
5309
+ ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
5310
+ return result.changes ?? 0;
5311
+ }
5312
+ }
5313
+ function applyImportResolutionsWithStatement(db, stmtFn, runWithRetry, maxSqlVars, resolutions) {
5314
+ if (resolutions.length === 0) return 0;
5315
+ return runWithRetry(() => {
5316
+ db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5317
+ db.exec(
5318
+ `CREATE TEMP TABLE import_resolution (
5319
+ from_file TEXT NOT NULL,
5320
+ lang TEXT NOT NULL,
5321
+ module TEXT NOT NULL,
5322
+ to_file TEXT NOT NULL
5323
+ )`
5324
+ );
5325
+ const chunkSize = Math.max(1, Math.floor(maxSqlVars / 4));
5326
+ for (let i = 0; i < resolutions.length; i += chunkSize) {
5327
+ const chunk = resolutions.slice(i, i + chunkSize);
5328
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5329
+ const binds = [];
5330
+ for (const entry of chunk) {
5331
+ binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5332
+ }
5333
+ stmtFn(
5334
+ `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5335
+ VALUES ${placeholders}`
5336
+ ).run(...binds);
5337
+ }
5338
+ db.exec(
5339
+ `CREATE INDEX IF NOT EXISTS temp.idx_ir
5340
+ ON import_resolution(module, lang, from_file)`
5341
+ );
5342
+ const result = stmtFn(
5343
+ `UPDATE refs
5344
+ SET to_file = (
5345
+ SELECT ir.to_file
5346
+ FROM temp.import_resolution ir
5347
+ JOIN symbols s ON s.id = refs.from_id
5348
+ WHERE ir.module = refs.module
5349
+ AND ir.lang = refs.lang
5350
+ AND ir.from_file = s.file
5351
+ LIMIT 1
5352
+ )
5353
+ WHERE refs.call_type = 'import'
5354
+ AND refs.module IS NOT NULL
5355
+ AND EXISTS (
5356
+ SELECT 1
5357
+ FROM temp.import_resolution ir
5358
+ JOIN symbols s ON s.id = refs.from_id
5359
+ WHERE ir.module = refs.module
5360
+ AND ir.lang = refs.lang
5361
+ AND ir.from_file = s.file
5362
+ )`
5363
+ ).run();
5364
+ db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5365
+ return result.changes ?? 0;
5366
+ });
5367
+ }
5368
+ function resolveRefsForNamesUnsafe(stmtFn, maxSqlVars, names) {
5369
+ const list = [...names].filter((name) => name.length > 0);
5370
+ if (list.length === 0) return 0;
5371
+ let total = 0;
5372
+ for (let i = 0; i < list.length; i += maxSqlVars) {
5373
+ const chunk = list.slice(i, i + maxSqlVars);
5374
+ const placeholders = chunk.map(() => "?").join(",");
5375
+ try {
5376
+ const result = stmtFn(
5377
+ `UPDATE refs
5378
+ SET to_id = s.id
5379
+ FROM (
5380
+ SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
5381
+ FROM symbols sym
5382
+ JOIN lang_family lf ON lf.lang = sym.lang
5383
+ WHERE sym.name IN (${placeholders})
5384
+ GROUP BY sym.name, lf.family
5385
+ UNION ALL
5386
+ SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
5387
+ FROM symbols sym
5388
+ WHERE sym.name IN (${placeholders})
5389
+ GROUP BY sym.name
5390
+ ) AS s,
5391
+ lang_family AS rf
5392
+ WHERE refs.to_name IN (${placeholders})
5393
+ AND rf.lang = refs.lang
5394
+ AND s.name = refs.to_name
5395
+ AND s.family = rf.family`
5396
+ ).run(...chunk, ...chunk, ...chunk);
5397
+ total += result.changes ?? 0;
5398
+ } catch {
5399
+ const result = stmtFn(
5400
+ `UPDATE refs SET to_id = (
5401
+ SELECT sym.id FROM symbols sym
5402
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5403
+ ORDER BY sym.id LIMIT 1
5404
+ ) WHERE refs.to_name IN (${placeholders})
5405
+ AND EXISTS (
5406
+ SELECT 1 FROM symbols sym
5407
+ WHERE sym.name = refs.to_name AND ${FAMILY_MATCH_SQL}
5408
+ )`
5409
+ ).run(LANG_FAMILY_WILDCARD, ...chunk, LANG_FAMILY_WILDCARD);
5410
+ total += result.changes ?? 0;
5411
+ }
5412
+ }
5413
+ return total;
5414
+ }
5415
+
5416
+ // src/codebase-index/writer-search.ts
5417
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
5418
+
5419
+ // src/codebase-index/lsp-kind.ts
5420
+ function lspKindToInternalKind(k) {
5421
+ switch (k) {
5422
+ case 5 /* Class */:
5423
+ return "class";
5424
+ case 6 /* Method */:
5425
+ return "method";
5426
+ case 7 /* Property */:
5427
+ case 8 /* Field */:
5428
+ return "property";
5429
+ case 9 /* Constructor */:
5430
+ return "class";
5431
+ case 10 /* Enum */:
5432
+ return "enum";
5433
+ case 11 /* Interface */:
5434
+ return "interface";
5435
+ case 12 /* Function */:
5436
+ return "function";
5437
+ case 13 /* Variable */:
5438
+ return "var";
5439
+ case 14 /* Constant */:
5440
+ return "const";
5441
+ case 22 /* EnumMember */:
5442
+ return "enum";
5443
+ case 26 /* TypeParameter */:
5444
+ return "type";
5445
+ case 3 /* Namespace */:
5446
+ return "namespace";
5447
+ default:
5448
+ return null;
5449
+ }
5450
+ }
5451
+
5452
+ // src/codebase-index/writer-search-helpers.ts
5453
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
5454
+ function normalizeSearchLimit(limit) {
5455
+ return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
5456
+ }
5457
+ function buildWriterSearchWhere(query, filter) {
5458
+ const conditions = [];
5459
+ const values = [];
5460
+ let effectiveKind = filter?.kind;
5461
+ if (filter?.lspKind !== void 0) {
5462
+ const mapped = lspKindToInternalKind(filter.lspKind);
5463
+ if (mapped !== null) {
5464
+ effectiveKind = mapped;
5465
+ } else {
5466
+ return null;
5467
+ }
5468
+ }
5469
+ if (effectiveKind) {
5470
+ conditions.push("kind = ?");
5118
5471
  values.push(effectiveKind);
5119
5472
  }
5120
5473
  if (filter?.lang) {
@@ -5149,6 +5502,173 @@ function mapWriterSearchRow(row, lspKind, score = 0, snippet = "") {
5149
5502
  };
5150
5503
  }
5151
5504
 
5505
+ // src/codebase-index/writer-search.ts
5506
+ function searchWithStatement(stmtFn, query, filter, opts) {
5507
+ const built = buildWriterSearchWhere(query, filter);
5508
+ if (built === null) return [];
5509
+ const { where, values } = built;
5510
+ const limit = normalizeSearchLimit(opts?.limit);
5511
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
5512
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
5513
+ const binds = limit !== void 0 ? [...values, limit] : values;
5514
+ const rows = stmtFn(sql).all(...binds);
5515
+ return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
5516
+ }
5517
+ function countSearchWithStatement(stmtFn, query, filter) {
5518
+ const built = buildWriterSearchWhere(query, filter);
5519
+ if (built === null) return 0;
5520
+ const row = stmtFn(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
5521
+ ...built.values
5522
+ );
5523
+ return Number(row?.n ?? 0);
5524
+ }
5525
+ function searchRankedWithStatement(stmtFn, searchFn, ftsAvailable, vectorsAvailable, getOrBuildBm25, query, filter, limit) {
5526
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
5527
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
5528
+ const tokens = tokenise(query);
5529
+ if (tokens.length === 0 || !ftsAvailable) {
5530
+ return searchRankedFallbackWithStatement(
5531
+ stmtFn,
5532
+ searchFn,
5533
+ getOrBuildBm25,
5534
+ query,
5535
+ filter,
5536
+ safeLimit
5537
+ );
5538
+ }
5539
+ let effectiveKind = filter?.kind;
5540
+ if (filter?.lspKind !== void 0) {
5541
+ const mapped = lspKindToInternalKind(filter.lspKind);
5542
+ if (mapped === null) return { results: [], total: 0 };
5543
+ effectiveKind = mapped;
5544
+ }
5545
+ const longTokens = tokens.filter((t) => t.length >= 3);
5546
+ const shortTokens = tokens.filter((t) => t.length < 3);
5547
+ if (longTokens.length === 0) {
5548
+ return searchRankedFallbackWithStatement(
5549
+ stmtFn,
5550
+ searchFn,
5551
+ getOrBuildBm25,
5552
+ query,
5553
+ filter,
5554
+ safeLimit
5555
+ );
5556
+ }
5557
+ const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
5558
+ const conditions = ["symbols_fts MATCH ?"];
5559
+ const values = [match];
5560
+ for (const shortTok of shortTokens) {
5561
+ conditions.push("s.text LIKE ? ESCAPE '\\'");
5562
+ values.push(`%${escapeLike(shortTok)}%`);
5563
+ }
5564
+ if (effectiveKind) {
5565
+ conditions.push("s.kind = ?");
5566
+ values.push(effectiveKind);
5567
+ }
5568
+ if (filter?.lang) {
5569
+ conditions.push("s.lang = ?");
5570
+ values.push(filter.lang);
5571
+ }
5572
+ if (filter?.file) {
5573
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
5574
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
5575
+ }
5576
+ const where = conditions.join(" AND ");
5577
+ const countRows = stmtFn(
5578
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
5579
+ ).all(...values);
5580
+ const total = countRows[0] ? Number(countRows[0].n) : 0;
5581
+ if (total === 0) return { results: [], total: 0 };
5582
+ const bm25Rows = stmtFn(
5583
+ `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
5584
+ -bm25(symbols_fts) AS score,
5585
+ snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
5586
+ FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
5587
+ WHERE ${where}
5588
+ ORDER BY
5589
+ CASE WHEN lower(s.name) = lower(?) THEN 0
5590
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
5591
+ ELSE 2 END,
5592
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
5593
+ LIMIT ?`
5594
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5595
+ if (vectorsAvailable && bm25Rows.length > 0) {
5596
+ const queryVec = embedText(query);
5597
+ const candidateIds = bm25Rows.map((r) => r.id);
5598
+ const placeholders = candidateIds.map(() => "?").join(",");
5599
+ const vecRows = stmtFn(
5600
+ `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
5601
+ ).all(...candidateIds);
5602
+ const vecScores = vecRows.map((r) => ({
5603
+ id: r.symbol_id,
5604
+ sim: cosineSimilarity(queryVec, decodeVector(r.vector))
5605
+ })).sort((a, b) => b.sim - a.sim);
5606
+ const bm25Rank = /* @__PURE__ */ new Map();
5607
+ bm25Rows.forEach((r, i) => {
5608
+ bm25Rank.set(r.id, i);
5609
+ });
5610
+ const vecRank = /* @__PURE__ */ new Map();
5611
+ vecScores.forEach((r, i) => {
5612
+ vecRank.set(r.id, i);
5613
+ });
5614
+ const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
5615
+ const fusedScore = new Map(fused);
5616
+ const sorted = [...bm25Rows].sort(
5617
+ (a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
5618
+ );
5619
+ return {
5620
+ results: sorted.map(
5621
+ (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5622
+ ),
5623
+ total
5624
+ };
5625
+ }
5626
+ return {
5627
+ results: bm25Rows.map(
5628
+ (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5629
+ ),
5630
+ total
5631
+ };
5632
+ }
5633
+ function searchRankedFallbackWithStatement(stmtFn, searchFn, getOrBuildBm25, query, filter, limit) {
5634
+ if (!query.trim()) {
5635
+ const total2 = countSearchWithStatement(stmtFn, query, filter);
5636
+ if (total2 === 0) return { results: [], total: 0 };
5637
+ return { results: searchFn(query, filter, { limit }), total: total2 };
5638
+ }
5639
+ const total = countSearchWithStatement(stmtFn, query, filter);
5640
+ if (total === 0) return { results: [], total: 0 };
5641
+ const candidates = searchFn(query, filter, {
5642
+ limit: SEARCH_CANDIDATE_SCAN_CAP
5643
+ });
5644
+ if (candidates.length === 0) return { results: [], total: 0 };
5645
+ const candidateById = new Map(candidates.map((c) => [c.id, c]));
5646
+ const bm25 = getOrBuildBm25();
5647
+ const scored = bm25.score(query, (id) => candidateById.has(id));
5648
+ const q = query.trim().toLowerCase();
5649
+ const rank = (id) => {
5650
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
5651
+ if (name === q) return 0;
5652
+ if (name.startsWith(q)) return 1;
5653
+ return 2;
5654
+ };
5655
+ scored.sort((a, b) => {
5656
+ const rankDiff = rank(a.id) - rank(b.id);
5657
+ if (rankDiff !== 0) return rankDiff;
5658
+ const scoreDiff = b.score - a.score;
5659
+ if (scoreDiff !== 0) return scoreDiff;
5660
+ const left = expectDefined4(candidateById.get(a.id));
5661
+ const right = expectDefined4(candidateById.get(b.id));
5662
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
5663
+ });
5664
+ const qTokens = tokenise(query);
5665
+ const results = scored.slice(0, limit).map(({ id, score }) => {
5666
+ const c = expectDefined4(candidateById.get(id));
5667
+ return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
5668
+ });
5669
+ return { results, total };
5670
+ }
5671
+
5152
5672
  // src/codebase-index/writer-store-pool.ts
5153
5673
  var DEFAULT_MAX_WARM_STORES = 2;
5154
5674
  var StorePool = class {
@@ -5240,68 +5760,14 @@ var DB_FILE2 = "index.db";
5240
5760
  var MAX_STATEMENT_CACHE = 128;
5241
5761
  var IndexStore = class _IndexStore {
5242
5762
  db;
5243
- /**
5244
- * True while an index run owns one outer SQLite transaction. Individual
5245
- * writer methods normally protect themselves with BEGIN/COMMIT, but during
5246
- * a refresh they join this transaction so readers observe either the last
5247
- * completed index or the next completed index, never an in-between batch.
5248
- */
5249
5763
  atomicIndexUpdateActive = false;
5250
5764
  writeSavepointSequence = 0;
5251
- /** Absolute path to this project's index directory. */
5252
5765
  indexDir;
5253
- /**
5254
- * True when the SQLite build provides FTS5 (Node's bundled SQLite does).
5255
- * When false, ranked search falls back to the LIKE + in-process BM25 path.
5256
- */
5257
5766
  ftsAvailable = false;
5258
- /**
5259
- * Phase 3: true when the `symbol_vectors` table was created successfully.
5260
- * When false, hybrid search skips the vector pass and falls back to FTS5
5261
- * (or LIKE) only.
5262
- */
5263
5767
  vectorsAvailable = false;
5264
- /**
5265
- * Cache of prepared statements keyed by their SQL text. `DatabaseSync`
5266
- * compiles SQL on every `.prepare()` call; for the fixed-SQL methods
5267
- * (upsertFile, getFileMeta, deleteFile, insertRefs, …) that runs thousands
5268
- * of times during a full reindex. `StatementSync` objects are reusable
5269
- * across calls on the same connection, so we compile each distinct SQL once
5270
- * and reuse it. Cleared in {@link close} when the connection is torn down.
5271
- */
5272
5768
  stmtCache = /* @__PURE__ */ new Map();
5273
- /**
5274
- * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
5275
- * Built lazily on the first `searchRankedFallback` call and invalidated
5276
- * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
5277
- * IDF over the full corpus is also more correct than the old per-query
5278
- * candidate-subset IDF.
5279
- *
5280
- * Cache-lifecycle invariants (single source of truth lives at the
5281
- * `invalidateBm25()` helper — see its docblock for the "every mutation
5282
- * MUST call this" contract):
5283
- * - declaration: this field + `bm25Dirty` (here)
5284
- * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
5285
- * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
5286
- * - teardown: `close()` resets the flag and nulls the cache
5287
- */
5288
5769
  bm25Cache = null;
5289
- // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
5290
- // an empty or pre-existing corpus makes a stale IDF table meaningless.
5291
5770
  bm25Dirty = true;
5292
- /**
5293
- * Prepare-once helper: compile `sql` on first use, reuse thereafter.
5294
- *
5295
- * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
5296
- * the fallback search builder emits one `text LIKE ?` clause per query token
5297
- * — so the SQL varies with the token count and a stream of differently-sized
5298
- * queries grew the cache without limit. Sage's store already bounds its
5299
- * equivalent at 128 (WS-096).
5300
- *
5301
- * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
5302
- * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
5303
- * itself rather than the reindex hot path.
5304
- */
5305
5771
  stmt(sql) {
5306
5772
  const cached = this.stmtCache.get(sql);
5307
5773
  if (cached !== void 0) {
@@ -5328,7 +5794,6 @@ var IndexStore = class _IndexStore {
5328
5794
  runWithRetry(fn) {
5329
5795
  return runSqliteWithRetry(fn);
5330
5796
  }
5331
- /** Run a complete index mutation as one WAL-visible publication. */
5332
5797
  async runAtomicIndexUpdate(job) {
5333
5798
  if (this.atomicIndexUpdateActive) return job();
5334
5799
  this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
@@ -5347,11 +5812,6 @@ var IndexStore = class _IndexStore {
5347
5812
  this.atomicIndexUpdateActive = false;
5348
5813
  }
5349
5814
  }
5350
- /**
5351
- * Begin a method-local transaction. Inside an atomic index publication a
5352
- * SAVEPOINT preserves the old per-batch rollback boundary, which is needed
5353
- * when commitBatch falls back to per-file writes after one batch fails.
5354
- */
5355
5815
  beginWriteTransaction() {
5356
5816
  if (this.atomicIndexUpdateActive) {
5357
5817
  const savepoint = `index_write_${++this.writeSavepointSequence}`;
@@ -5373,35 +5833,11 @@ var IndexStore = class _IndexStore {
5373
5833
  this.db.exec("ROLLBACK");
5374
5834
  }
5375
5835
  }
5376
- /**
5377
- * Mirror the in-process language→family map into SQLite.
5378
- *
5379
- * Rewritten on every open rather than only on schema bumps: the mapping is
5380
- * static lookup data, so a code-side change (a new language, a language
5381
- * moving families) must take effect without forcing a full reindex.
5382
- */
5383
5836
  seedLangFamilies() {
5384
5837
  const insert = this.stmt("INSERT OR REPLACE INTO lang_family(lang, family) VALUES (?, ?)");
5385
5838
  for (const [lang, family] of LANG_FAMILY_ENTRIES) insert.run(lang, family);
5386
5839
  insert.run("", LANG_FAMILY_WILDCARD);
5387
5840
  }
5388
- /**
5389
- * Add any column the current schema expects but the on-disk table lacks.
5390
- *
5391
- * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
5392
- * and the version check above only rebuilds on a version *mismatch*. That
5393
- * leaves a real gap: several wstack processes share this database, and while
5394
- * a version upgrade is rolling out one of them may still be running the
5395
- * previous build. That older process sees the newer version number, drops the
5396
- * tables, and recreates them from *its* DDL — without the newer columns —
5397
- * while the metadata row still reads the new version. Every later query for
5398
- * one of those columns then fails with `no such column`, and no amount of
5399
- * reindexing fixes it, because the version numbers already agree.
5400
- *
5401
- * Repairing column-by-column makes the schema self-healing from any of those
5402
- * states. Table and column names are compile-time literals from this module,
5403
- * never user input.
5404
- */
5405
5841
  repairMissingColumns() {
5406
5842
  const expected = [
5407
5843
  {
@@ -5506,27 +5942,8 @@ var IndexStore = class _IndexStore {
5506
5942
  }
5507
5943
  this.ensureNextSymbolIdSeeded();
5508
5944
  }
5509
- // ─── ID allocation & bulk helpers ────────────────────────────────────────────
5510
5945
  static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
5511
- /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
5512
5946
  static MAX_SQL_VARS = 900;
5513
- /**
5514
- * Correlated predicate: the ref in `refs` and the candidate symbol aliased
5515
- * `sym` belong to the same language family — or the ref carries no language,
5516
- * in which case the wildcard bind matches everything.
5517
- *
5518
- * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
5519
- */
5520
- static FAMILY_MATCH_SQL = `(
5521
- (SELECT family FROM lang_family WHERE lang = refs.lang) = ?
5522
- OR (SELECT family FROM lang_family WHERE lang = sym.lang)
5523
- = (SELECT family FROM lang_family WHERE lang = refs.lang)
5524
- )`;
5525
- /**
5526
- * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
5527
- * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
5528
- * re-reads and advances the counter atomically.
5529
- */
5530
5947
  ensureNextSymbolIdSeeded() {
5531
5948
  const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
5532
5949
  _IndexStore.NEXT_SYMBOL_ID_KEY
@@ -5539,10 +5956,6 @@ var IndexStore = class _IndexStore {
5539
5956
  String(next)
5540
5957
  );
5541
5958
  }
5542
- /**
5543
- * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
5544
- * so concurrent indexers cannot hand out overlapping ranges.
5545
- */
5546
5959
  allocateSymbolIds(count) {
5547
5960
  if (count <= 0) return this.getMaxSymbolId() + 1;
5548
5961
  this.ensureNextSymbolIdSeeded();
@@ -5556,16 +5969,8 @@ var IndexStore = class _IndexStore {
5556
5969
  );
5557
5970
  return start;
5558
5971
  }
5559
- /**
5560
- * Disconnect inbound refs before their target symbols are replaced and
5561
- * return the affected names for scoped re-resolution.
5562
- *
5563
- * This also repairs a long-standing dangling-id edge case: `refs.to_id` has
5564
- * no physical FK, so deleting a symbol previously left callers pointing at a
5565
- * non-existent row.
5566
- */
5567
5972
  invalidateIncomingRefsForFiles(files) {
5568
- if (files.length === 0) return [];
5973
+ if (files.length === 0) return /* @__PURE__ */ new Set();
5569
5974
  const placeholders = files.map(() => "?").join(",");
5570
5975
  const names = this.stmt(`SELECT DISTINCT name FROM symbols WHERE file IN (${placeholders})`).all(
5571
5976
  ...files
@@ -5574,36 +5979,11 @@ var IndexStore = class _IndexStore {
5574
5979
  `UPDATE refs SET to_id = NULL
5575
5980
  WHERE to_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
5576
5981
  ).run(...files);
5577
- return names;
5982
+ return new Set(names);
5578
5983
  }
5579
- /** Resolve only refs whose target names may have changed. */
5580
5984
  resolveRefsForNamesUnsafe(names) {
5581
- const unique = [...new Set(names)].filter(Boolean);
5582
- let changes = 0;
5583
- for (let start = 0; start < unique.length; start += _IndexStore.MAX_SQL_VARS) {
5584
- const chunk = unique.slice(start, start + _IndexStore.MAX_SQL_VARS);
5585
- const placeholders = chunk.map(() => "?").join(",");
5586
- const result = this.stmt(
5587
- `UPDATE refs
5588
- SET to_id = (
5589
- SELECT MIN(sym.id) FROM symbols sym
5590
- WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
5591
- )
5592
- WHERE to_name IN (${placeholders})`
5593
- ).run(LANG_FAMILY_WILDCARD, ...chunk);
5594
- changes += result.changes ?? 0;
5595
- }
5596
- return changes;
5985
+ return resolveRefsForNamesUnsafe((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, names);
5597
5986
  }
5598
- // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
5599
- /**
5600
- * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
5601
- * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
5602
- * (O(1)); multi-row INSERT amortizes bind overhead for large files.
5603
- *
5604
- * @returns The symbols array with `id` fields populated so the caller can
5605
- * use them for refs without re-reading from the DB.
5606
- */
5607
5987
  insertSymbols(symbols) {
5608
5988
  this.invalidateBm25();
5609
5989
  return this.runWithRetry(() => {
@@ -5632,12 +6012,6 @@ var IndexStore = class _IndexStore {
5632
6012
  if (this.ftsAvailable) {
5633
6013
  ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
5634
6014
  }
5635
- vectorRows.push({
5636
- id,
5637
- vector: encodeVector(
5638
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5639
- )
5640
- });
5641
6015
  result.push({ ...s, id });
5642
6016
  }
5643
6017
  bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulk);
@@ -5687,11 +6061,6 @@ var IndexStore = class _IndexStore {
5687
6061
  }
5688
6062
  });
5689
6063
  }
5690
- /**
5691
- * Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
5692
- * when a source file disappears between index runs — previously this only
5693
- * dropped the `files` row, leaving its symbols orphaned but still searchable.
5694
- */
5695
6064
  deleteFile(file) {
5696
6065
  this.invalidateBm25();
5697
6066
  this.runWithRetry(() => {
@@ -5721,7 +6090,6 @@ var IndexStore = class _IndexStore {
5721
6090
  }
5722
6091
  });
5723
6092
  }
5724
- // ─── File metadata ──────────────────────────────────────────────────────────
5725
6093
  upsertFile(meta) {
5726
6094
  this.runWithRetry(() => {
5727
6095
  this.stmt(
@@ -5749,8 +6117,6 @@ var IndexStore = class _IndexStore {
5749
6117
  getAllFileMetas() {
5750
6118
  return getAllFileMetasWithStatement((sql) => this.stmt(sql));
5751
6119
  }
5752
- // ─── Project structure & module resolution ──────────────────────────────────
5753
- /** Store the Code Atlas grouping label for each indexed file. */
5754
6120
  setFilePackages(entries) {
5755
6121
  if (entries.size === 0) return;
5756
6122
  this.runWithRetry(() => {
@@ -5758,272 +6124,50 @@ var IndexStore = class _IndexStore {
5758
6124
  for (const [file, label] of entries) update.run(label, file);
5759
6125
  });
5760
6126
  }
5761
- /**
5762
- * Every indexed `namespace`/`module` declaration, for ecosystems whose import
5763
- * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
5764
- * Ordered so the resolver's choice among duplicate declarations is stable.
5765
- */
5766
6127
  getNamespaceDeclarations() {
5767
- return this.stmt(
5768
- `SELECT name, file FROM symbols WHERE kind = 'namespace' ORDER BY file, id`
5769
- ).all();
6128
+ return getNamespaceDeclarationsWithStatement((sql) => this.stmt(sql));
5770
6129
  }
5771
- /** `file → package` for every indexed file that has a label. */
5772
6130
  getFilePackages() {
5773
- const rows = this.stmt("SELECT file, package FROM files WHERE package != ''").all();
5774
- return new Map(rows.map((row) => [row.file, row.package]));
6131
+ return getFilePackagesWithStatement((sql) => this.stmt(sql));
5775
6132
  }
5776
- /**
5777
- * Distinct `(fromFile, lang, module)` triples needing module resolution.
5778
- *
5779
- * Distinct rather than per-ref because resolution depends only on these three
5780
- * values: a file importing the same module twenty times resolves it once.
5781
- */
5782
6133
  getUnresolvedImports(onlyFiles) {
5783
- const base = `SELECT DISTINCT s.file AS fromFile, r.lang AS lang, r.module AS module
5784
- FROM refs r
5785
- JOIN symbols s ON s.id = r.from_id
5786
- WHERE r.call_type = 'import' AND r.module IS NOT NULL`;
5787
- if (!onlyFiles?.length) {
5788
- return this.stmt(base).all();
5789
- }
5790
- const out = [];
5791
- for (let i = 0; i < onlyFiles.length; i += _IndexStore.MAX_SQL_VARS) {
5792
- const chunk = onlyFiles.slice(i, i + _IndexStore.MAX_SQL_VARS);
5793
- const placeholders = chunk.map(() => "?").join(",");
5794
- out.push(
5795
- ...this.stmt(`${base} AND s.file IN (${placeholders})`).all(...chunk)
5796
- );
5797
- }
5798
- return out;
6134
+ return getUnresolvedImportsWithStatement(
6135
+ (sql) => this.stmt(sql),
6136
+ _IndexStore.MAX_SQL_VARS,
6137
+ onlyFiles
6138
+ );
5799
6139
  }
5800
- /**
5801
- * Write resolved import targets back onto `refs.to_file`.
5802
- *
5803
- * Applied through a temp table and a single UPDATE: one statement per
5804
- * resolution would mean thousands of round-trips on a first index.
5805
- */
5806
6140
  applyImportResolutions(resolutions) {
5807
- if (resolutions.length === 0) return 0;
5808
- return this.runWithRetry(() => {
5809
- this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5810
- this.db.exec(
5811
- `CREATE TEMP TABLE import_resolution (
5812
- from_file TEXT NOT NULL,
5813
- lang TEXT NOT NULL,
5814
- module TEXT NOT NULL,
5815
- to_file TEXT NOT NULL
5816
- )`
5817
- );
5818
- const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / 4));
5819
- for (let i = 0; i < resolutions.length; i += chunkSize) {
5820
- const chunk = resolutions.slice(i, i + chunkSize);
5821
- const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5822
- const binds = [];
5823
- for (const entry of chunk) {
5824
- binds.push(entry.fromFile, entry.lang, entry.module, entry.toFile);
5825
- }
5826
- this.stmt(
5827
- `INSERT INTO temp.import_resolution(from_file, lang, module, to_file)
5828
- VALUES ${placeholders}`
5829
- ).run(...binds);
5830
- }
5831
- this.db.exec(
5832
- `CREATE INDEX IF NOT EXISTS temp.idx_ir
5833
- ON import_resolution(module, lang, from_file)`
5834
- );
5835
- const result = this.stmt(
5836
- `UPDATE refs
5837
- SET to_file = (
5838
- SELECT ir.to_file
5839
- FROM temp.import_resolution ir
5840
- JOIN symbols s ON s.id = refs.from_id
5841
- WHERE ir.module = refs.module
5842
- AND ir.lang = refs.lang
5843
- AND ir.from_file = s.file
5844
- LIMIT 1
5845
- )
5846
- WHERE refs.call_type = 'import'
5847
- AND refs.module IS NOT NULL
5848
- AND EXISTS (
5849
- SELECT 1
5850
- FROM temp.import_resolution ir
5851
- JOIN symbols s ON s.id = refs.from_id
5852
- WHERE ir.module = refs.module
5853
- AND ir.lang = refs.lang
5854
- AND ir.from_file = s.file
5855
- )`
5856
- ).run();
5857
- this.db.exec("DROP TABLE IF EXISTS temp.import_resolution");
5858
- return result.changes ?? 0;
5859
- });
5860
- }
5861
- // ─── Search ──────────────────────────────────────────────────────────────────
5862
- search(query, filter, opts) {
5863
- const built = this.buildSearchWhere(query, filter);
5864
- if (built === null) return [];
5865
- const { where, values } = built;
5866
- const limit = normalizeSearchLimit(opts?.limit);
5867
- const limitSql = limit !== void 0 ? " LIMIT ?" : "";
5868
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
5869
- const binds = limit !== void 0 ? [...values, limit] : values;
5870
- const rows = this.stmt(sql).all(
5871
- ...binds
6141
+ return applyImportResolutionsWithStatement(
6142
+ this.db,
6143
+ (sql) => this.stmt(sql),
6144
+ this.runWithRetry.bind(this),
6145
+ _IndexStore.MAX_SQL_VARS,
6146
+ resolutions
5872
6147
  );
5873
- return rows.map((row) => mapWriterSearchRow(row, filter?.lspKind));
5874
6148
  }
5875
- /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
5876
- buildSearchWhere(query, filter) {
5877
- return buildWriterSearchWhere(query, filter);
6149
+ search(query, filter, opts) {
6150
+ return searchWithStatement((sql) => this.stmt(sql), query, filter, opts);
5878
6151
  }
5879
6152
  countSearch(query, filter) {
5880
- const built = this.buildSearchWhere(query, filter);
5881
- if (built === null) return 0;
5882
- const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
5883
- ...built.values
5884
- );
5885
- return Number(row?.n ?? 0);
6153
+ return countSearchWithStatement((sql) => this.stmt(sql), query, filter);
5886
6154
  }
5887
- /**
5888
- * Ranked search — the one-stop query the codebase-search tool and plug-lsp
5889
- * use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
5890
- * `bm25()` with a built-in `snippet()`; without FTS5 it falls back to the
5891
- * legacy LIKE scan + in-process BM25 (identical semantics, slower).
5892
- *
5893
- * Tokens are matched as prefixes (`"tok"*`), mirroring the old
5894
- * `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
5895
- * "users", camelCase-split text makes "complex" find "complexOperation").
5896
- */
5897
6155
  searchRanked(query, filter, limit) {
5898
- const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
5899
- const safeLimit = Math.max(1, Math.min(rawLimit, 100));
5900
- const tokens = tokenise(query);
5901
- if (tokens.length === 0 || !this.ftsAvailable) {
5902
- return this.searchRankedFallback(query, filter, safeLimit);
5903
- }
5904
- let effectiveKind = filter?.kind;
5905
- if (filter?.lspKind !== void 0) {
5906
- const mapped = lspKindToInternalKind(filter.lspKind);
5907
- if (mapped === null) return { results: [], total: 0 };
5908
- effectiveKind = mapped;
5909
- }
5910
- const longTokens = tokens.filter((t) => t.length >= 3);
5911
- const shortTokens = tokens.filter((t) => t.length < 3);
5912
- if (longTokens.length === 0) {
5913
- return this.searchRankedFallback(query, filter, safeLimit);
5914
- }
5915
- const match = longTokens.map((t) => `"${t.replaceAll('"', "")}"`).join(" OR ");
5916
- const conditions = ["symbols_fts MATCH ?"];
5917
- const values = [match];
5918
- for (const shortTok of shortTokens) {
5919
- conditions.push("s.text LIKE ? ESCAPE '\\'");
5920
- values.push(`%${escapeLike(shortTok)}%`);
5921
- }
5922
- if (effectiveKind) {
5923
- conditions.push("s.kind = ?");
5924
- values.push(effectiveKind);
5925
- }
5926
- if (filter?.lang) {
5927
- conditions.push("s.lang = ?");
5928
- values.push(filter.lang);
5929
- }
5930
- if (filter?.file) {
5931
- conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
5932
- values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
5933
- }
5934
- const where = conditions.join(" AND ");
5935
- const countRows = this.stmt(
5936
- `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
5937
- ).all(...values);
5938
- const total = countRows[0] ? Number(countRows[0].n) : 0;
5939
- if (total === 0) return { results: [], total: 0 };
5940
- const bm25Rows = this.stmt(
5941
- `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
5942
- -bm25(symbols_fts) AS score,
5943
- snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
5944
- FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
5945
- WHERE ${where}
5946
- ORDER BY
5947
- CASE WHEN lower(s.name) = lower(?) THEN 0
5948
- WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
5949
- ELSE 2 END,
5950
- bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
5951
- LIMIT ?`
5952
- ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
5953
- if (this.vectorsAvailable && bm25Rows.length > 0) {
5954
- const queryVec = embedText(query);
5955
- const candidateIds = bm25Rows.map((r) => r.id);
5956
- const placeholders = candidateIds.map(() => "?").join(",");
5957
- const vecRows = this.stmt(
5958
- `SELECT sv.symbol_id, sv.vector FROM symbol_vectors sv WHERE sv.symbol_id IN (${placeholders})`
5959
- ).all(...candidateIds);
5960
- const vecScores = vecRows.map((r) => ({
5961
- id: r.symbol_id,
5962
- sim: cosineSimilarity(queryVec, decodeVector(r.vector))
5963
- })).sort((a, b) => b.sim - a.sim);
5964
- const bm25Rank = /* @__PURE__ */ new Map();
5965
- bm25Rows.forEach((r, i) => {
5966
- bm25Rank.set(r.id, i);
5967
- });
5968
- const vecRank = /* @__PURE__ */ new Map();
5969
- vecScores.forEach((r, i) => {
5970
- vecRank.set(r.id, i);
5971
- });
5972
- const fused = reciprocalRankFusion(bm25Rank, vecRank, 60);
5973
- const fusedScore = new Map(fused);
5974
- const sorted = [...bm25Rows].sort(
5975
- (a, b) => (fusedScore.get(b.id) ?? 0) - (fusedScore.get(a.id) ?? 0)
5976
- );
5977
- return {
5978
- results: sorted.map(
5979
- (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5980
- ),
5981
- total
5982
- };
5983
- }
5984
- return {
5985
- results: bm25Rows.map(
5986
- (row) => mapWriterSearchRow(row, filter?.lspKind, Math.max(1e-4, row.score), row.snippet)
5987
- ),
5988
- total
5989
- };
6156
+ return searchRankedWithStatement(
6157
+ (sql) => this.stmt(sql),
6158
+ this.search.bind(this),
6159
+ this.ftsAvailable,
6160
+ this.vectorsAvailable,
6161
+ this.getOrBuildBm25.bind(this),
6162
+ query,
6163
+ filter,
6164
+ limit
6165
+ );
5990
6166
  }
5991
- /**
5992
- * Invalidate the cached BM25 index.
5993
- *
5994
- * **Contract: every method that mutates `symbols` MUST call this before
5995
- * returning.** (`refs` mutations do not affect the BM25 fallback because
5996
- * the corpus is built from `symbols.text` via `getAllIndexable()` and the
5997
- * BM25 score is filtered by the LIKE-selected candidate set in
5998
- * `searchRankedFallback`.) Today the call sites are `repairDrift`,
5999
- * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
6000
- * `commitBatch`. A future mutation that adds a new write path (e.g.
6001
- * `renameFile`, `updateSignature`) MUST also call this — otherwise the
6002
- * FTS5-unavailable fallback will serve stale search results. The
6003
- * `close()` reset at L1820-1821 tears the cache down on store shutdown,
6004
- * which is the only legitimate place that flips the flag outside this
6005
- * helper.
6006
- *
6007
- * Called *before* `runWithRetry` on purpose: if the write fails all
6008
- * retries the flag stays set, forcing a rebuild on the next search rather
6009
- * than trusting a cache that may not reflect the intended mutation.
6010
- * Do not move this inside the retry closure.
6011
- */
6012
6167
  invalidateBm25() {
6013
6168
  this.bm25Dirty = true;
6014
6169
  this.bm25Cache = null;
6015
6170
  }
6016
- /**
6017
- * Return the cached full-corpus BM25 index, rebuilding it only when the
6018
- * symbols table has been mutated since the last build. The full-corpus IDF
6019
- * is more correct than the old per-query candidate-subset IDF, and the
6020
- * amortized build cost drops from O(symbols × tokens) per search to once
6021
- * per write batch.
6022
- *
6023
- * Note: the first call after a long idle (or on a freshly opened store)
6024
- * pays the full corpus rebuild synchronously on the search path. For a
6025
- * 5 500+ symbol corpus this is a visible one-time latency spike.
6026
- */
6027
6171
  getOrBuildBm25() {
6028
6172
  if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
6029
6173
  const docs = this.getAllIndexable();
@@ -6031,57 +6175,12 @@ var IndexStore = class _IndexStore {
6031
6175
  this.bm25Dirty = false;
6032
6176
  return this.bm25Cache;
6033
6177
  }
6034
- /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
6035
- searchRankedFallback(query, filter, limit) {
6036
- if (!query.trim()) {
6037
- const total2 = this.countSearch(query, filter);
6038
- if (total2 === 0) return { results: [], total: 0 };
6039
- return { results: this.search(query, filter, { limit }), total: total2 };
6040
- }
6041
- const total = this.countSearch(query, filter);
6042
- if (total === 0) return { results: [], total: 0 };
6043
- const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
6044
- if (candidates.length === 0) return { results: [], total: 0 };
6045
- const candidateById = new Map(candidates.map((c) => [c.id, c]));
6046
- const bm25 = this.getOrBuildBm25();
6047
- const scored = bm25.score(query, (id) => candidateById.has(id));
6048
- const q = query.trim().toLowerCase();
6049
- const rank = (id) => {
6050
- const name = candidateById.get(id)?.name.toLowerCase() ?? "";
6051
- if (name === q) return 0;
6052
- if (name.startsWith(q)) return 1;
6053
- return 2;
6054
- };
6055
- scored.sort((a, b) => {
6056
- const rankDiff = rank(a.id) - rank(b.id);
6057
- if (rankDiff !== 0) return rankDiff;
6058
- const scoreDiff = b.score - a.score;
6059
- if (scoreDiff !== 0) return scoreDiff;
6060
- const left = expectDefined4(candidateById.get(a.id));
6061
- const right = expectDefined4(candidateById.get(b.id));
6062
- return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
6063
- });
6064
- const qTokens = tokenise(query);
6065
- const results = scored.slice(0, limit).map(({ id, score }) => {
6066
- const c = expectDefined4(candidateById.get(id));
6067
- return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
6068
- });
6069
- return { results, total };
6070
- }
6071
6178
  getAllIndexable() {
6072
6179
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
6073
6180
  }
6074
- /**
6075
- * Largest symbol id currently in the table (0 when empty). New ids must be
6076
- * allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
6077
- * changed file's rows, so the row count drops below the max id and a
6078
- * count-based id would collide with a surviving row (UNIQUE constraint on
6079
- * `symbols.id`). Ids may have gaps — that is fine.
6080
- */
6081
6181
  getMaxSymbolId() {
6082
6182
  return getMaxSymbolIdWithStatement((sql) => this.stmt(sql));
6083
6183
  }
6084
- // ─── Stats ───────────────────────────────────────────────────────────────────
6085
6184
  getStats() {
6086
6185
  return getStatsWithStatement((sql) => this.stmt(sql), this.indexDir);
6087
6186
  }
@@ -6124,11 +6223,6 @@ var IndexStore = class _IndexStore {
6124
6223
  }
6125
6224
  });
6126
6225
  }
6127
- // ─── Ref CRUD ────────────────────────────────────────────────────────────────
6128
- /**
6129
- * Insert cross-references for a given source symbol id.
6130
- * Replaces any existing refs from the same source (idempotent on re-index).
6131
- */
6132
6226
  insertRefs(fromId, refs) {
6133
6227
  this.runWithRetry(() => {
6134
6228
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
@@ -6140,167 +6234,36 @@ var IndexStore = class _IndexStore {
6140
6234
  );
6141
6235
  });
6142
6236
  }
6143
- /**
6144
- * Bulk-insert refs for many source symbols in a single transaction.
6145
- *
6146
- * Unlike {@link insertRefs} this does NOT delete per source id — the caller
6147
- * (the indexer) has already cleared stale refs for the file via
6148
- * {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
6149
- * repeated once per symbol. One transaction for the whole file instead of one
6150
- * per symbol turns an O(symbols) transaction count into O(1).
6151
- *
6152
- * Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
6153
- */
6154
6237
  insertRefsBatch(refs) {
6155
6238
  if (refs.length === 0) return;
6156
6239
  this.runWithRetry(() => {
6157
6240
  bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refs);
6158
6241
  });
6159
6242
  }
6160
- /**
6161
- * Commit a batch of file-level symbol/refs/upserts in a single transaction.
6162
- *
6163
- * Used by the indexer to amortize SQLite commit overhead across many files.
6164
- * Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
6165
- * for symbols, plus per-file deletes and an upsertFile call), so a 20-file
6166
- * parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
6167
- * this entry point we do exactly one BEGIN/COMMIT per parallel batch.
6168
- *
6169
- * Each entry must already be a fully-parsed FileSymbols (symbols + refs).
6170
- * The caller is responsible for the per-file prefix accounting
6171
- * (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
6172
- * the caller clear stale symbols/refs for any files being re-indexed before
6173
- * the inserts run (required to keep refs → symbols FK invariants).
6174
- *
6175
- * Returns the symbols back with their assigned `id` (same shape as
6176
- * {@link insertSymbols}) so callers can build final per-file results.
6177
- */
6178
6243
  commitBatch(entries, options = {}) {
6179
- if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
6180
- return [];
6181
- }
6182
6244
  this.invalidateBm25();
6183
6245
  return this.runWithRetry(() => {
6184
6246
  const ownsTransaction = this.beginWriteTransaction();
6185
6247
  try {
6186
- const affectedNames = /* @__PURE__ */ new Set();
6187
- for (const entry of entries) {
6188
- for (const symbol of entry.symbols) affectedNames.add(symbol.name);
6189
- for (const ref of entry.refs) affectedNames.add(ref.toName);
6190
- }
6191
- if (options.deleteForFiles && options.deleteForFiles.length > 0) {
6192
- const placeholders = options.deleteForFiles.map(() => "?").join(",");
6193
- for (const name of this.invalidateIncomingRefsForFiles(options.deleteForFiles)) {
6194
- affectedNames.add(name);
6195
- }
6196
- if (this.ftsAvailable) {
6197
- this.stmt(
6198
- `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6199
- ).run(...options.deleteForFiles);
6200
- }
6201
- if (this.vectorsAvailable) {
6202
- this.stmt(
6203
- `DELETE FROM symbol_vectors WHERE symbol_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6204
- ).run(...options.deleteForFiles);
6205
- }
6206
- this.stmt(
6207
- `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
6208
- ).run(...options.deleteForFiles);
6209
- this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(
6210
- ...options.deleteForFiles
6211
- );
6212
- }
6213
- const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
6214
- let nextId = this.allocateSymbolIds(totalSymbols);
6215
- const allInserted = [];
6216
- const refsToInsert = [];
6217
- const bulkSyms = [];
6218
- const ftsRows = [];
6219
- const vectorRows = [];
6220
- for (const entry of entries) {
6221
- const insertedForEntry = [];
6222
- for (const s of entry.symbols) {
6223
- const id = nextId++;
6224
- bulkSyms.push({
6225
- id,
6226
- lang: s.lang,
6227
- kind: s.kind,
6228
- name: s.name,
6229
- file: s.file,
6230
- line: s.line,
6231
- col: s.col,
6232
- signature: s.signature,
6233
- docComment: s.docComment,
6234
- scope: s.scope,
6235
- text: s.text
6236
- });
6237
- if (this.ftsAvailable) {
6238
- ftsRows.push({
6239
- id,
6240
- text: buildIndexableText(s.name, s.signature, s.docComment)
6241
- });
6242
- }
6243
- vectorRows.push({
6244
- id,
6245
- vector: encodeVector(
6246
- embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6247
- )
6248
- });
6249
- const inserted = { ...s, id };
6250
- allInserted.push(inserted);
6251
- insertedForEntry.push(inserted);
6252
- }
6253
- refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
6254
- }
6255
- bulkInsertSymbolsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, bulkSyms);
6256
- bulkInsertFtsWithStatement(
6248
+ const result = commitBatchWithStatement(
6257
6249
  (sql) => this.stmt(sql),
6258
6250
  _IndexStore.MAX_SQL_VARS,
6259
6251
  this.ftsAvailable,
6260
- ftsRows
6252
+ this.vectorsAvailable,
6253
+ this.allocateSymbolIds.bind(this),
6254
+ this.invalidateIncomingRefsForFiles.bind(this),
6255
+ this.resolveRefsForNamesUnsafe.bind(this),
6256
+ entries,
6257
+ options
6261
6258
  );
6262
- if (this.vectorsAvailable) {
6263
- bulkInsertVectorsWithStatement(
6264
- (sql) => this.stmt(sql),
6265
- _IndexStore.MAX_SQL_VARS,
6266
- vectorRows
6267
- );
6268
- }
6269
- bulkInsertRefsWithStatement((sql) => this.stmt(sql), _IndexStore.MAX_SQL_VARS, refsToInsert);
6270
- const upsertStmt = this.stmt(
6271
- `INSERT INTO files(file, lang, mtime_ms, content_hash, symbol_count, last_indexed)
6272
- VALUES (?, ?, ?, ?, ?, ?)
6273
- ON CONFLICT(file) DO UPDATE SET
6274
- lang = excluded.lang,
6275
- mtime_ms = excluded.mtime_ms,
6276
- content_hash = excluded.content_hash,
6277
- symbol_count = excluded.symbol_count,
6278
- last_indexed = excluded.last_indexed`
6279
- );
6280
- const now = Date.now();
6281
- for (const entry of entries) {
6282
- upsertStmt.run(
6283
- entry.file,
6284
- entry.lang,
6285
- entry.mtimeMs,
6286
- entry.contentHash ?? "",
6287
- entry.symbolCount,
6288
- now
6289
- );
6290
- }
6291
- this.resolveRefsForNamesUnsafe(affectedNames);
6292
6259
  this.commitWriteTransaction(ownsTransaction);
6293
- return allInserted;
6260
+ return result;
6294
6261
  } catch (err) {
6295
6262
  this.rollbackWriteTransaction(ownsTransaction);
6296
6263
  throw err;
6297
6264
  }
6298
6265
  });
6299
6266
  }
6300
- /**
6301
- * Delete all refs whose source symbols are in a given file.
6302
- * Used when re-indexing a file to clear stale refs.
6303
- */
6304
6267
  deleteRefsForFile(file) {
6305
6268
  this.runWithRetry(() => {
6306
6269
  this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(
@@ -6308,64 +6271,12 @@ var IndexStore = class _IndexStore {
6308
6271
  );
6309
6272
  });
6310
6273
  }
6311
- /**
6312
- * Resolve `to_name` → `to_id` for all refs that have a name but no id.
6313
- * Call this after all symbols have been inserted to fill in cross-references.
6314
- *
6315
- * A match additionally requires the referencing ref and the target symbol to
6316
- * be in the same {@link LangFamily}. Without that guard a name match is a
6317
- * cross-language accident waiting to happen — `main`, `New`, `Parse` and
6318
- * `Config` are declared in most languages at once, and each collision draws a
6319
- * Code Atlas edge between files that never reference each other. Refs stored
6320
- * without a language keep the old global behaviour via the `'*'` wildcard row.
6321
- */
6322
6274
  resolveRefs() {
6323
- return this.runWithRetry(() => {
6324
- try {
6325
- const result = this.stmt(
6326
- `UPDATE refs
6327
- SET to_id = s.id
6328
- FROM (
6329
- SELECT sym.name AS name, lf.family AS family, MIN(sym.id) AS id
6330
- FROM symbols sym
6331
- JOIN lang_family lf ON lf.lang = sym.lang
6332
- GROUP BY sym.name, lf.family
6333
- UNION ALL
6334
- SELECT sym.name AS name, '${LANG_FAMILY_WILDCARD}' AS family, MIN(sym.id) AS id
6335
- FROM symbols sym
6336
- GROUP BY sym.name
6337
- ) AS s,
6338
- lang_family AS rf
6339
- WHERE refs.to_id IS NULL
6340
- AND refs.to_name IS NOT NULL
6341
- AND rf.lang = refs.lang
6342
- AND s.name = refs.to_name
6343
- AND s.family = rf.family`
6344
- ).run();
6345
- return result.changes ?? 0;
6346
- } catch {
6347
- const result = this.stmt(
6348
- `UPDATE refs SET to_id = (
6349
- SELECT sym.id FROM symbols sym
6350
- WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
6351
- ORDER BY sym.id LIMIT 1
6352
- ) WHERE to_id IS NULL AND to_name IS NOT NULL
6353
- AND EXISTS (
6354
- SELECT 1 FROM symbols sym
6355
- WHERE sym.name = refs.to_name AND ${_IndexStore.FAMILY_MATCH_SQL}
6356
- )`
6357
- ).run(LANG_FAMILY_WILDCARD, LANG_FAMILY_WILDCARD);
6358
- return result.changes ?? 0;
6359
- }
6360
- });
6275
+ return this.runWithRetry(() => resolveRefsWithStatement((sql) => this.stmt(sql)));
6361
6276
  }
6362
6277
  resolveRefsForNames(names) {
6363
6278
  return this.runWithRetry(() => this.resolveRefsForNamesUnsafe(names));
6364
6279
  }
6365
- /**
6366
- * Clear symbols/refs for a file and mark it as indexed with zero symbols.
6367
- * Used by the indexer for empty-parse results so three writes share one txn.
6368
- */
6369
6280
  replaceEmptyFile(meta) {
6370
6281
  this.invalidateBm25();
6371
6282
  this.runWithRetry(() => {
@@ -6411,20 +6322,12 @@ var IndexStore = class _IndexStore {
6411
6322
  }
6412
6323
  });
6413
6324
  }
6414
- /** Best-effort query planner refresh after a large reindex. */
6415
6325
  optimize() {
6416
6326
  try {
6417
6327
  this.db.exec("PRAGMA optimize");
6418
6328
  } catch {
6419
6329
  }
6420
6330
  }
6421
- /**
6422
- * Reclaim page churn left by repeated force rebuilds.
6423
- *
6424
- * SQLite's DROP/CREATE path makes rebuilds fast but leaves pages on the
6425
- * freelist. Compact only large, materially sparse databases and only when the
6426
- * caller is already on a full-index maintenance path.
6427
- */
6428
6331
  compactIfNeeded(options = {}) {
6429
6332
  const minBytes = options.minBytes ?? 256 * 1024 * 1024;
6430
6333
  const minFreeRatio = options.minFreeRatio ?? 0.35;
@@ -6451,115 +6354,44 @@ var IndexStore = class _IndexStore {
6451
6354
  return false;
6452
6355
  }
6453
6356
  }
6454
- /**
6455
- * Find all symbols that reference the named target symbol (incoming callers).
6456
- * Accepts a name instead of an id so the agent doesn't need a prior lookup.
6457
- */
6458
6357
  findIncomingCallsByName(symbolName, file, limit = 100) {
6459
6358
  return findIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6460
6359
  }
6461
- /**
6462
- * Find all symbols that the named source symbol references (outgoing callees).
6463
- * Accepts a name instead of an id so the agent doesn't need a prior lookup.
6464
- */
6465
6360
  findOutgoingCallsByName(symbolName, file, limit = 100) {
6466
6361
  return findOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6467
6362
  }
6468
- /**
6469
- * Transitive incoming-call tree: all symbols that transitively call the
6470
- * target, to an unbounded depth (cycle-safe via SQL UNION deduplication).
6471
- * Used by `codebase-incoming-calls` when the caller wants the full call
6472
- * chain rather than just direct callers.
6473
- */
6474
6363
  findTransitiveIncomingCallsByName(symbolName, file, limit = 200) {
6475
6364
  return findTransitiveIncomingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6476
6365
  }
6477
- /**
6478
- * Transitive outgoing-call tree: all symbols the target transitively calls.
6479
- * Used by `codebase-outgoing-calls` when the caller wants the full
6480
- * dependency chain rather than just direct callees.
6481
- */
6482
6366
  findTransitiveOutgoingCallsByName(symbolName, file, limit = 200) {
6483
6367
  return findTransitiveOutgoingCallsByName((sql) => this.stmt(sql), symbolName, file, limit);
6484
6368
  }
6485
- /**
6486
- * Compute the set of symbol IDs reachable from the given seed IDs using a
6487
- * native SQLite recursive CTE. Used by dead-code detection to replace the
6488
- * in-memory BFS.
6489
- */
6490
6369
  findReachableSymbolIds(seedIds) {
6491
6370
  return findReachableSymbolIds((sql) => this.stmt(sql), seedIds);
6492
6371
  }
6493
- /**
6494
- * Find all references TO a given symbol (who calls / uses this symbol?).
6495
- */
6496
6372
  findRefsTo(symbolId) {
6497
6373
  return findRefsToWithStatement((sql) => this.stmt(sql), symbolId);
6498
6374
  }
6499
- /**
6500
- * Find all references FROM a given symbol (what does this symbol call/use?).
6501
- */
6502
6375
  findRefsFrom(symbolId) {
6503
6376
  return findRefsFromWithStatement((sql) => this.stmt(sql), symbolId);
6504
6377
  }
6505
- // ─── CodeMap graph aggregation ──────────────────────────────────────────────
6506
- /**
6507
- * Package-level graph: each workspace package is a node; edges are derived
6508
- * from cross-package symbol references (a symbol in package A references a
6509
- * symbol resolved in package B). Node metadata includes symbol/file counts.
6510
- */
6511
6378
  getPackageGraph() {
6512
6379
  return getPackageGraphWithStatement((sql) => this.stmt(sql));
6513
6380
  }
6514
- /**
6515
- * File-level graph for a single package: each file is a node; edges are
6516
- * derived from cross-file symbol references within the package.
6517
- */
6518
6381
  getFileGraph(packageFilter) {
6519
6382
  return getFileGraphWithStatement((sql) => this.stmt(sql), packageFilter);
6520
6383
  }
6521
- /**
6522
- * Symbol-level graph for a single file: each symbol is a node; edges are
6523
- * derived from intra-file and cross-file symbol references (who calls whom).
6524
- */
6525
6384
  getSymbolGraph(fileFilter) {
6526
6385
  return getSymbolGraphWithStatement((sql) => this.stmt(sql), fileFilter);
6527
6386
  }
6528
- /**
6529
- * Returns every symbol in the index. Used by dead-code analysis to
6530
- * build the full symbol universe for the reachability scan.
6531
- */
6532
6387
  getAllSymbols() {
6533
6388
  return this.stmt("SELECT id, name, file, kind, line FROM symbols ORDER BY id").all().map((r) => ({ ...r, kind: r.kind }));
6534
6389
  }
6535
- /**
6536
- * Returns every resolved reference (to_id IS NOT NULL). Used by
6537
- * dead-code analysis to build the consumer-ship graph. Refs whose
6538
- * target symbol id is null (unresolved imports) are excluded.
6539
- */
6540
6390
  getAllResolvedRefs() {
6541
- return this.stmt(
6542
- "SELECT from_id AS fromId, to_id AS toId, call_type AS callType FROM refs WHERE to_id IS NOT NULL"
6543
- ).all();
6391
+ return getAllResolvedRefsWithStatement((sql) => this.stmt(sql));
6544
6392
  }
6545
- /**
6546
- * Returns ALL import refs (including unresolved) with their source-file
6547
- * path and resolved target id. Used by the dead-code scan's file-level
6548
- * graph traversal to handle barrel-only entry points where no symbol
6549
- * carries the ref.
6550
- *
6551
- * Refs whose `from_id` doesn't match a known symbol (e.g. pure-barrel
6552
- * files with no declarations) will have `sourceFile === null`.
6553
- */
6554
6393
  getAllImportRefs() {
6555
- return this.stmt(
6556
- `SELECT s.file AS sourceFile, r.to_name AS toName, r.to_id AS toId,
6557
- r.call_type AS callType, r.line
6558
- FROM refs r
6559
- LEFT JOIN symbols s ON r.from_id = s.id
6560
- WHERE r.call_type = 'import'
6561
- ORDER BY r.line`
6562
- ).all();
6394
+ return getAllImportRefsWithStatement((sql) => this.stmt(sql));
6563
6395
  }
6564
6396
  close() {
6565
6397
  this.stmtCache.clear();