@wrongstack/tools 0.293.0 → 0.295.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 (258) hide show
  1. package/dist/_env.d.ts +1 -1
  2. package/dist/_env.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_spawn-stream.d.ts +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/_util.d.ts +1 -1
  7. package/dist/_util.d.ts.map +1 -1
  8. package/dist/audit.d.ts +1 -1
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +81 -16
  11. package/dist/audit.js.map +2 -2
  12. package/dist/auto-proceed-loop-guard.d.ts +31 -0
  13. package/dist/auto-proceed-loop-guard.d.ts.map +1 -1
  14. package/dist/auto-proceed-loop-guard.js +24 -1
  15. package/dist/auto-proceed-loop-guard.js.map +2 -2
  16. package/dist/bash.d.ts +1 -1
  17. package/dist/bash.d.ts.map +1 -1
  18. package/dist/bash.js +105 -75
  19. package/dist/bash.js.map +3 -3
  20. package/dist/batch-tool-use.d.ts +1 -1
  21. package/dist/batch-tool-use.d.ts.map +1 -1
  22. package/dist/batch-tool-use.js +1 -1
  23. package/dist/batch-tool-use.js.map +1 -1
  24. package/dist/browser/index.js +10 -1
  25. package/dist/browser/index.js.map +2 -2
  26. package/dist/browser/manager.d.ts +2 -0
  27. package/dist/browser/manager.d.ts.map +1 -1
  28. package/dist/browser/tools.d.ts +1 -1
  29. package/dist/browser/tools.d.ts.map +1 -1
  30. package/dist/builtin.d.ts +1 -1
  31. package/dist/builtin.d.ts.map +1 -1
  32. package/dist/builtin.js +1650 -640
  33. package/dist/builtin.js.map +4 -4
  34. package/dist/codebase-index/background-indexer.d.ts +2 -2
  35. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  36. package/dist/codebase-index/bm25.d.ts +3 -1
  37. package/dist/codebase-index/bm25.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  41. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  42. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  43. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  44. package/dist/codebase-index/generic-parser.d.ts +34 -0
  45. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  46. package/dist/codebase-index/go-parser.d.ts +1 -1
  47. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  48. package/dist/codebase-index/index.d.ts +4 -2
  49. package/dist/codebase-index/index.d.ts.map +1 -1
  50. package/dist/codebase-index/index.js +1068 -372
  51. package/dist/codebase-index/index.js.map +4 -4
  52. package/dist/codebase-index/indexer.d.ts +6 -1
  53. package/dist/codebase-index/indexer.d.ts.map +1 -1
  54. package/dist/codebase-index/json-parser.d.ts +1 -1
  55. package/dist/codebase-index/languages.d.ts +25 -0
  56. package/dist/codebase-index/languages.d.ts.map +1 -0
  57. package/dist/codebase-index/py-parser.d.ts +9 -1
  58. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  59. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  60. package/dist/codebase-index/rs-parser.d.ts +1 -1
  61. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  62. package/dist/codebase-index/schema.d.ts +9 -2
  63. package/dist/codebase-index/schema.d.ts.map +1 -1
  64. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  65. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  66. package/dist/codebase-index/ts-parser.d.ts +2 -2
  67. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  68. package/dist/codebase-index/worker.js +993 -348
  69. package/dist/codebase-index/worker.js.map +4 -4
  70. package/dist/codebase-index/writer.d.ts +83 -4
  71. package/dist/codebase-index/writer.d.ts.map +1 -1
  72. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  73. package/dist/design.d.ts +5 -2
  74. package/dist/design.d.ts.map +1 -1
  75. package/dist/design.js +36 -3
  76. package/dist/design.js.map +2 -2
  77. package/dist/diff.d.ts +1 -1
  78. package/dist/diff.d.ts.map +1 -1
  79. package/dist/diff.js +2 -2
  80. package/dist/diff.js.map +1 -1
  81. package/dist/document.d.ts +1 -1
  82. package/dist/document.d.ts.map +1 -1
  83. package/dist/document.js +1 -1
  84. package/dist/document.js.map +1 -1
  85. package/dist/e2e.d.ts +1 -1
  86. package/dist/e2e.d.ts.map +1 -1
  87. package/dist/e2e.js +1 -1
  88. package/dist/e2e.js.map +1 -1
  89. package/dist/edit.d.ts +1 -1
  90. package/dist/edit.d.ts.map +1 -1
  91. package/dist/edit.js +3 -3
  92. package/dist/edit.js.map +2 -2
  93. package/dist/exec.d.ts +1 -1
  94. package/dist/exec.d.ts.map +1 -1
  95. package/dist/exec.js +99 -44
  96. package/dist/exec.js.map +3 -3
  97. package/dist/fetch.d.ts +1 -1
  98. package/dist/fetch.d.ts.map +1 -1
  99. package/dist/fetch.js +4 -5
  100. package/dist/fetch.js.map +2 -2
  101. package/dist/format.d.ts +1 -1
  102. package/dist/format.d.ts.map +1 -1
  103. package/dist/format.js +81 -16
  104. package/dist/format.js.map +2 -2
  105. package/dist/git.d.ts +1 -1
  106. package/dist/git.d.ts.map +1 -1
  107. package/dist/git.js +3 -2
  108. package/dist/git.js.map +2 -2
  109. package/dist/glob.d.ts +1 -1
  110. package/dist/glob.d.ts.map +1 -1
  111. package/dist/glob.js +81 -31
  112. package/dist/glob.js.map +4 -4
  113. package/dist/grep.d.ts +1 -1
  114. package/dist/grep.d.ts.map +1 -1
  115. package/dist/grep.js +90 -22
  116. package/dist/grep.js.map +4 -4
  117. package/dist/index.d.ts +5 -4
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1895 -771
  120. package/dist/index.js.map +4 -4
  121. package/dist/install.d.ts +1 -1
  122. package/dist/install.d.ts.map +1 -1
  123. package/dist/install.js +85 -17
  124. package/dist/install.js.map +2 -2
  125. package/dist/json.d.ts +1 -1
  126. package/dist/json.d.ts.map +1 -1
  127. package/dist/json.js +2 -2
  128. package/dist/json.js.map +2 -2
  129. package/dist/kanban-evidence-bridge.d.ts +21 -0
  130. package/dist/kanban-evidence-bridge.d.ts.map +1 -0
  131. package/dist/kanban.d.ts +20 -3
  132. package/dist/kanban.d.ts.map +1 -1
  133. package/dist/kanban.js +256 -32
  134. package/dist/kanban.js.map +3 -3
  135. package/dist/languages/execute-tool.d.ts +1 -1
  136. package/dist/languages/execute-tool.d.ts.map +1 -1
  137. package/dist/languages/execute.d.ts +1 -1
  138. package/dist/languages/execute.d.ts.map +1 -1
  139. package/dist/languages/index.d.ts +1 -1
  140. package/dist/languages/index.d.ts.map +1 -1
  141. package/dist/languages/index.js +99 -17
  142. package/dist/languages/index.js.map +2 -2
  143. package/dist/languages/package-tool.d.ts +3 -4
  144. package/dist/languages/package-tool.d.ts.map +1 -1
  145. package/dist/languages/plan.d.ts.map +1 -1
  146. package/dist/languages/profiles/additional.d.ts.map +1 -1
  147. package/dist/languages/profiles/primary.d.ts.map +1 -1
  148. package/dist/languages/tool.d.ts +1 -1
  149. package/dist/languages/tool.d.ts.map +1 -1
  150. package/dist/lint.d.ts +1 -1
  151. package/dist/lint.d.ts.map +1 -1
  152. package/dist/lint.js +81 -16
  153. package/dist/lint.js.map +2 -2
  154. package/dist/logs.d.ts +1 -1
  155. package/dist/logs.d.ts.map +1 -1
  156. package/dist/logs.js +2 -2
  157. package/dist/logs.js.map +1 -1
  158. package/dist/memory.d.ts +1 -1
  159. package/dist/memory.d.ts.map +1 -1
  160. package/dist/memory.js +1 -1
  161. package/dist/memory.js.map +1 -1
  162. package/dist/mode.d.ts +1 -1
  163. package/dist/mode.d.ts.map +1 -1
  164. package/dist/mode.js.map +1 -1
  165. package/dist/outdated.d.ts +1 -1
  166. package/dist/outdated.d.ts.map +1 -1
  167. package/dist/outdated.js +101 -19
  168. package/dist/outdated.js.map +2 -2
  169. package/dist/pack.d.ts +1 -1
  170. package/dist/pack.js +1650 -640
  171. package/dist/pack.js.map +4 -4
  172. package/dist/patch.d.ts +1 -1
  173. package/dist/patch.d.ts.map +1 -1
  174. package/dist/patch.js +2 -2
  175. package/dist/patch.js.map +1 -1
  176. package/dist/plan.d.ts +1 -1
  177. package/dist/plan.d.ts.map +1 -1
  178. package/dist/plan.js +7 -7
  179. package/dist/plan.js.map +2 -2
  180. package/dist/process-guardian.d.ts +7 -0
  181. package/dist/process-guardian.d.ts.map +1 -1
  182. package/dist/process-registry-persistent.d.ts +5 -0
  183. package/dist/process-registry-persistent.d.ts.map +1 -1
  184. package/dist/process-registry.d.ts +9 -1
  185. package/dist/process-registry.d.ts.map +1 -1
  186. package/dist/process-registry.js +18 -3
  187. package/dist/process-registry.js.map +2 -2
  188. package/dist/ps-slash.js +93 -38
  189. package/dist/ps-slash.js.map +2 -2
  190. package/dist/read.d.ts +1 -1
  191. package/dist/read.d.ts.map +1 -1
  192. package/dist/read.js +3 -2
  193. package/dist/read.js.map +2 -2
  194. package/dist/replace.d.ts +1 -1
  195. package/dist/replace.d.ts.map +1 -1
  196. package/dist/replace.js +4 -4
  197. package/dist/replace.js.map +2 -2
  198. package/dist/scaffold.d.ts +1 -1
  199. package/dist/scaffold.d.ts.map +1 -1
  200. package/dist/scaffold.js +2 -2
  201. package/dist/scaffold.js.map +1 -1
  202. package/dist/search.d.ts +1 -1
  203. package/dist/search.d.ts.map +1 -1
  204. package/dist/search.js +4 -4
  205. package/dist/search.js.map +2 -2
  206. package/dist/session-kanban.d.ts +4 -1
  207. package/dist/session-kanban.d.ts.map +1 -1
  208. package/dist/session-kanban.js +23 -15
  209. package/dist/session-kanban.js.map +2 -2
  210. package/dist/set-working-dir.d.ts +1 -1
  211. package/dist/set-working-dir.d.ts.map +1 -1
  212. package/dist/skill.d.ts +1 -1
  213. package/dist/skill.d.ts.map +1 -1
  214. package/dist/skill.js +2 -5
  215. package/dist/skill.js.map +2 -2
  216. package/dist/spawn-background.d.ts.map +1 -1
  217. package/dist/task.d.ts +3 -2
  218. package/dist/task.d.ts.map +1 -1
  219. package/dist/task.js +6 -8
  220. package/dist/task.js.map +2 -2
  221. package/dist/test.d.ts +1 -1
  222. package/dist/test.d.ts.map +1 -1
  223. package/dist/test.js +81 -16
  224. package/dist/test.js.map +2 -2
  225. package/dist/todo.d.ts +2 -1
  226. package/dist/todo.d.ts.map +1 -1
  227. package/dist/todo.js +4 -4
  228. package/dist/todo.js.map +2 -2
  229. package/dist/tool-help.d.ts +1 -1
  230. package/dist/tool-help.d.ts.map +1 -1
  231. package/dist/tool-help.js.map +1 -1
  232. package/dist/tool-search.d.ts +1 -1
  233. package/dist/tool-search.d.ts.map +1 -1
  234. package/dist/tool-search.js.map +1 -1
  235. package/dist/tool-tier.d.ts +16 -0
  236. package/dist/tool-tier.d.ts.map +1 -0
  237. package/dist/tool-tier.js +22402 -0
  238. package/dist/tool-tier.js.map +7 -0
  239. package/dist/tool-use.d.ts +1 -1
  240. package/dist/tool-use.d.ts.map +1 -1
  241. package/dist/tool-use.js.map +1 -1
  242. package/dist/tree.d.ts +1 -1
  243. package/dist/tree.d.ts.map +1 -1
  244. package/dist/tree.js +3 -15
  245. package/dist/tree.js.map +2 -2
  246. package/dist/typecheck.d.ts +1 -1
  247. package/dist/typecheck.d.ts.map +1 -1
  248. package/dist/typecheck.js +81 -16
  249. package/dist/typecheck.js.map +2 -2
  250. package/dist/win32.d.ts +10 -0
  251. package/dist/win32.d.ts.map +1 -0
  252. package/dist/win32.js +67 -0
  253. package/dist/win32.js.map +7 -0
  254. package/dist/write.d.ts +1 -1
  255. package/dist/write.d.ts.map +1 -1
  256. package/dist/write.js +3 -2
  257. package/dist/write.js.map +2 -2
  258. package/package.json +11 -3
@@ -1,5 +1,5 @@
1
1
  // src/codebase-index/writer.ts
2
- import { expectDefined } from "@wrongstack/core";
2
+ import { expectDefined } from "@wrongstack/core/utils";
3
3
 
4
4
  // src/codebase-index/circuit-breaker.ts
5
5
  var CircuitOpenError = class extends Error {
@@ -89,7 +89,7 @@ import { toErrorMessage } from "@wrongstack/core/utils";
89
89
  import { createRequire } from "node:module";
90
90
  import * as fs from "node:fs";
91
91
  import * as path from "node:path";
92
- import { resolveWstackPaths } from "@wrongstack/core";
92
+ import { resolveWstackPaths, sqliteCachePragmas } from "@wrongstack/core/utils";
93
93
 
94
94
  // src/codebase-index/schema.ts
95
95
  var SCHEMA_VERSION = 3;
@@ -174,23 +174,13 @@ function buildBm25Index(docs) {
174
174
  const tokens = tokenise(d.text);
175
175
  return { id: d.id, tokens, raw: d.text, len: tokens.length };
176
176
  });
177
- const df = {};
178
- for (const doc of documents) {
179
- const seen = /* @__PURE__ */ new Set();
180
- for (const t of doc.tokens) {
181
- if (!seen.has(t)) {
182
- df[t] = (df[t] ?? 0) + 1;
183
- seen.add(t);
184
- }
185
- }
186
- }
187
177
  const N = documents.length;
188
178
  const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
189
179
  const avgLen = N === 0 ? 0 : totalLen / N;
190
- return new Bm25Index(documents, df, N, avgLen);
180
+ return new Bm25Index(documents, N, avgLen);
191
181
  }
192
182
  var Bm25Index = class {
193
- constructor(documents, _df, N, avgLen) {
183
+ constructor(documents, N, avgLen) {
194
184
  this.documents = documents;
195
185
  this.N = N;
196
186
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
@@ -198,9 +188,19 @@ var Bm25Index = class {
198
188
  documents;
199
189
  N;
200
190
  safeAvgLen;
191
+ /** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
192
+ _byId;
201
193
  score(query, filter) {
202
194
  const qTokens = tokenise(query);
203
195
  if (qTokens.length === 0) return [];
196
+ const dfByTerm = /* @__PURE__ */ new Map();
197
+ for (const qTerm of qTokens) {
198
+ let dfVal = 0;
199
+ for (const candidate of this.documents) {
200
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
201
+ }
202
+ dfByTerm.set(qTerm, dfVal);
203
+ }
204
204
  const results = [];
205
205
  for (const doc of this.documents) {
206
206
  if (filter && !filter(doc.id)) continue;
@@ -211,10 +211,7 @@ var Bm25Index = class {
211
211
  if (t.startsWith(qTerm)) tf++;
212
212
  }
213
213
  if (tf === 0) continue;
214
- let dfVal = 0;
215
- for (const candidate of this.documents) {
216
- if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
217
- }
214
+ const dfVal = dfByTerm.get(qTerm) ?? 0;
218
215
  if (dfVal === 0) continue;
219
216
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
220
217
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -226,7 +223,10 @@ var Bm25Index = class {
226
223
  return results;
227
224
  }
228
225
  getDoc(id) {
229
- return this.documents.find((d) => d.id === id);
226
+ if (!this._byId) {
227
+ this._byId = new Map(this.documents.map((d) => [d.id, d]));
228
+ }
229
+ return this._byId.get(id);
230
230
  }
231
231
  extractSnippet(docId, queryTokens, radius = 40) {
232
232
  const doc = this.getDoc(docId);
@@ -388,6 +388,25 @@ var IndexStore = class _IndexStore {
388
388
  * and reuse it. Cleared in {@link close} when the connection is torn down.
389
389
  */
390
390
  stmtCache = /* @__PURE__ */ new Map();
391
+ /**
392
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
393
+ * Built lazily on the first `searchRankedFallback` call and invalidated
394
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
395
+ * IDF over the full corpus is also more correct than the old per-query
396
+ * candidate-subset IDF.
397
+ *
398
+ * Cache-lifecycle invariants (single source of truth lives at the
399
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
400
+ * MUST call this" contract):
401
+ * - declaration: this field + `bm25Dirty` (here)
402
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
403
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
404
+ * - teardown: `close()` resets the flag and nulls the cache
405
+ */
406
+ bm25Cache = null;
407
+ // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
408
+ // an empty or pre-existing corpus makes a stale IDF table meaningless.
409
+ bm25Dirty = true;
391
410
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
392
411
  stmt(sql) {
393
412
  let s = this.stmtCache.get(sql);
@@ -435,7 +454,15 @@ var IndexStore = class _IndexStore {
435
454
  this.db = new Database(path.join(this.indexDir, DB_FILE));
436
455
  try {
437
456
  this.db.exec("PRAGMA journal_mode = WAL");
438
- this.db.exec("PRAGMA busy_timeout = 5000");
457
+ this.db.exec("PRAGMA synchronous = NORMAL");
458
+ this.db.exec("PRAGMA busy_timeout = 15000");
459
+ this.db.exec("PRAGMA temp_store = MEMORY");
460
+ const cache = sqliteCachePragmas();
461
+ this.db.exec(`PRAGMA cache_size = -${cache.cacheSizeKiB}`);
462
+ this.db.exec(`PRAGMA mmap_size = ${cache.mmapBytes}`);
463
+ this.db.exec("PRAGMA foreign_keys = ON");
464
+ this.db.exec("PRAGMA journal_size_limit = 67108864");
465
+ this.db.exec("PRAGMA wal_autocheckpoint = 1000");
439
466
  } catch {
440
467
  }
441
468
  this.initSchema();
@@ -447,7 +474,7 @@ var IndexStore = class _IndexStore {
447
474
  value TEXT NOT NULL
448
475
  );
449
476
  `);
450
- const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
477
+ const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
451
478
  const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
452
479
  if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
453
480
  this.db.exec(`
@@ -456,9 +483,9 @@ var IndexStore = class _IndexStore {
456
483
  DROP TABLE IF EXISTS refs;
457
484
  `);
458
485
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
459
- this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
486
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
460
487
  } else if (storedVersion === null) {
461
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
488
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
462
489
  }
463
490
  this.db.exec(`
464
491
  CREATE TABLE IF NOT EXISTS files (
@@ -489,6 +516,7 @@ var IndexStore = class _IndexStore {
489
516
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
490
517
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
491
518
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
519
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)");
492
520
  this.db.exec(`
493
521
  CREATE TABLE IF NOT EXISTS refs (
494
522
  id INTEGER PRIMARY KEY,
@@ -509,65 +537,167 @@ var IndexStore = class _IndexStore {
509
537
  );
510
538
  this.ftsAvailable = true;
511
539
  const symbolCount = Number(
512
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
540
+ this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
513
541
  );
514
542
  const ftsCount = Number(
515
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
543
+ this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
516
544
  );
517
545
  if (symbolCount !== ftsCount) {
518
546
  this.db.exec("DELETE FROM symbols_fts");
519
- const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
520
- const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
521
- for (const row of rows) {
522
- insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
523
- }
547
+ const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
548
+ this.bulkInsertFts(
549
+ rows.map((row) => ({
550
+ id: row.id,
551
+ text: buildIndexableText(row.name, row.signature, row.doc_comment)
552
+ }))
553
+ );
554
+ this.invalidateBm25();
524
555
  }
525
556
  } catch {
526
557
  this.ftsAvailable = false;
527
558
  }
559
+ this.ensureNextSymbolIdSeeded();
560
+ }
561
+ // ─── ID allocation & bulk helpers ────────────────────────────────────────────
562
+ static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
563
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
564
+ static MAX_SQL_VARS = 900;
565
+ /**
566
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
567
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
568
+ * re-reads and advances the counter atomically.
569
+ */
570
+ ensureNextSymbolIdSeeded() {
571
+ const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
572
+ _IndexStore.NEXT_SYMBOL_ID_KEY
573
+ );
574
+ if (existing?.value !== void 0) return;
575
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
576
+ const next = (maxRows[0]?.m ?? 0) + 1;
577
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
578
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
579
+ String(next)
580
+ );
581
+ }
582
+ /**
583
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
584
+ * so concurrent indexers cannot hand out overlapping ranges.
585
+ */
586
+ allocateSymbolIds(count) {
587
+ if (count <= 0) return this.getMaxSymbolId() + 1;
588
+ this.ensureNextSymbolIdSeeded();
589
+ const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
590
+ _IndexStore.NEXT_SYMBOL_ID_KEY
591
+ );
592
+ const start = Math.max(1, Number(row?.value ?? 1) || 1);
593
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
594
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
595
+ String(start + count)
596
+ );
597
+ return start;
598
+ }
599
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
600
+ bulkInsertSymbols(rows) {
601
+ if (rows.length === 0) return;
602
+ const cols = 12;
603
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
604
+ for (let i = 0; i < rows.length; i += chunkSize) {
605
+ const chunk = rows.slice(i, i + chunkSize);
606
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
607
+ const stmt = this.stmt(
608
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
609
+ VALUES ${placeholders}`
610
+ );
611
+ const binds = [];
612
+ for (const r of chunk) {
613
+ binds.push(
614
+ r.id,
615
+ r.lang,
616
+ r.kind,
617
+ r.name,
618
+ r.file,
619
+ r.line,
620
+ r.col,
621
+ r.signature,
622
+ r.docComment,
623
+ r.scope,
624
+ r.text,
625
+ r.file
626
+ );
627
+ }
628
+ stmt.run(...binds);
629
+ }
630
+ }
631
+ bulkInsertFts(rows) {
632
+ if (!this.ftsAvailable || rows.length === 0) return;
633
+ const cols = 2;
634
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
635
+ for (let i = 0; i < rows.length; i += chunkSize) {
636
+ const chunk = rows.slice(i, i + chunkSize);
637
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
638
+ const stmt = this.stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
639
+ const binds = [];
640
+ for (const r of chunk) binds.push(r.id, r.text);
641
+ stmt.run(...binds);
642
+ }
643
+ }
644
+ bulkInsertRefs(refs) {
645
+ if (refs.length === 0) return;
646
+ const cols = 5;
647
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
648
+ for (let i = 0; i < refs.length; i += chunkSize) {
649
+ const chunk = refs.slice(i, i + chunkSize);
650
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
651
+ const stmt = this.stmt(
652
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
653
+ );
654
+ const binds = [];
655
+ for (const ref of chunk) {
656
+ binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
657
+ }
658
+ stmt.run(...binds);
659
+ }
528
660
  }
529
661
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
530
662
  /**
531
663
  * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
532
- * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
533
- * the same transaction, preventing UNIQUE constraint violations when two
534
- * processes index concurrently (each would see a different `MAX(id)` and
535
- * neither can insert with the other's IDs).
664
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
665
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
536
666
  *
537
667
  * @returns The symbols array with `id` fields populated so the caller can
538
668
  * use them for refs without re-reading from the DB.
539
669
  */
540
670
  insertSymbols(symbols) {
671
+ this.invalidateBm25();
541
672
  return this.runWithRetry(() => {
542
673
  this.db.exec("BEGIN IMMEDIATE");
543
674
  try {
544
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
545
- let nextId = (maxRows[0]?.m ?? 0) + 1;
546
- const stmt = this.db.prepare(
547
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
548
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
549
- );
550
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
675
+ let nextId = this.allocateSymbolIds(symbols.length);
551
676
  const result = [];
677
+ const bulk = [];
678
+ const ftsRows = [];
552
679
  for (const s of symbols) {
553
680
  const id = nextId++;
554
- stmt.run(
681
+ bulk.push({
555
682
  id,
556
- s.lang,
557
- s.kind,
558
- s.name,
559
- s.file,
560
- s.line,
561
- s.col,
562
- s.signature,
563
- s.docComment,
564
- s.scope,
565
- s.text,
566
- s.file
567
- );
568
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
683
+ lang: s.lang,
684
+ kind: s.kind,
685
+ name: s.name,
686
+ file: s.file,
687
+ line: s.line,
688
+ col: s.col,
689
+ signature: s.signature,
690
+ docComment: s.docComment,
691
+ scope: s.scope,
692
+ text: s.text
693
+ });
694
+ if (this.ftsAvailable) {
695
+ ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
696
+ }
569
697
  result.push({ ...s, id });
570
698
  }
699
+ this.bulkInsertSymbols(bulk);
700
+ this.bulkInsertFts(ftsRows);
571
701
  this.db.exec("COMMIT");
572
702
  return result;
573
703
  } catch (err) {
@@ -577,6 +707,7 @@ var IndexStore = class _IndexStore {
577
707
  });
578
708
  }
579
709
  deleteSymbolsForFile(file) {
710
+ this.invalidateBm25();
580
711
  this.runWithRetry(() => {
581
712
  if (this.ftsAvailable) {
582
713
  this.stmt(
@@ -592,17 +723,20 @@ var IndexStore = class _IndexStore {
592
723
  * dropped the `files` row, leaving its symbols orphaned but still searchable.
593
724
  */
594
725
  deleteFile(file) {
726
+ this.invalidateBm25();
595
727
  this.runWithRetry(() => {
596
728
  this.db.exec("BEGIN IMMEDIATE");
597
729
  try {
598
730
  if (this.ftsAvailable) {
599
- this.db.prepare(
731
+ this.stmt(
600
732
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
601
733
  ).run(file);
602
734
  }
603
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
604
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
605
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
735
+ this.stmt(
736
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
737
+ ).run(file);
738
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
739
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
606
740
  this.db.exec("COMMIT");
607
741
  } catch (err) {
608
742
  this.db.exec("ROLLBACK");
@@ -639,7 +773,7 @@ var IndexStore = class _IndexStore {
639
773
  };
640
774
  }
641
775
  getAllFileMetas() {
642
- return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
776
+ return this.stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
643
777
  file: r.file,
644
778
  lang: r.lang,
645
779
  mtimeMs: r.mtime_ms,
@@ -648,7 +782,32 @@ var IndexStore = class _IndexStore {
648
782
  }));
649
783
  }
650
784
  // ─── Search ──────────────────────────────────────────────────────────────────
651
- search(query, filter) {
785
+ search(query, filter, opts) {
786
+ const built = this.buildSearchWhere(query, filter);
787
+ if (built === null) return [];
788
+ const { where, values } = built;
789
+ const limit = typeof opts?.limit === "number" && Number.isFinite(opts.limit) ? Math.max(0, Math.trunc(opts.limit)) : void 0;
790
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
791
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
792
+ const binds = limit !== void 0 ? [...values, limit] : values;
793
+ const rows = this.stmt(sql).all(...binds);
794
+ return rows.map((r) => ({
795
+ id: r.id,
796
+ lang: r.lang,
797
+ kind: r.kind,
798
+ name: r.name,
799
+ file: r.file,
800
+ line: r.line,
801
+ col: r.col,
802
+ signature: r.signature,
803
+ docComment: r.doc_comment,
804
+ score: 0,
805
+ snippet: "",
806
+ lspKind: filter?.lspKind
807
+ }));
808
+ }
809
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
810
+ buildSearchWhere(query, filter) {
652
811
  const conditions = [];
653
812
  const values = [];
654
813
  let effectiveKind = filter?.kind;
@@ -657,7 +816,7 @@ var IndexStore = class _IndexStore {
657
816
  if (mapped !== null) {
658
817
  effectiveKind = mapped;
659
818
  } else {
660
- return [];
819
+ return null;
661
820
  }
662
821
  }
663
822
  if (effectiveKind) {
@@ -679,23 +838,15 @@ var IndexStore = class _IndexStore {
679
838
  for (const t of tokens) values.push(`%${t}%`);
680
839
  }
681
840
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
682
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}`;
683
- const stmt = this.db.prepare(sql);
684
- const rows = stmt.all(...values);
685
- return rows.map((r) => ({
686
- id: r.id,
687
- lang: r.lang,
688
- kind: r.kind,
689
- name: r.name,
690
- file: r.file,
691
- line: r.line,
692
- col: r.col,
693
- signature: r.signature,
694
- docComment: r.doc_comment,
695
- score: 0,
696
- snippet: "",
697
- lspKind: filter?.lspKind
698
- }));
841
+ return { where, values };
842
+ }
843
+ countSearch(query, filter) {
844
+ const built = this.buildSearchWhere(query, filter);
845
+ if (built === null) return 0;
846
+ const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
847
+ ...built.values
848
+ );
849
+ return Number(row?.n ?? 0);
699
850
  }
700
851
  /**
701
852
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
@@ -736,12 +887,12 @@ var IndexStore = class _IndexStore {
736
887
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
737
888
  }
738
889
  const where = conditions.join(" AND ");
739
- const countRows = this.db.prepare(
890
+ const countRows = this.stmt(
740
891
  `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
741
892
  ).all(...values);
742
893
  const total = countRows[0] ? Number(countRows[0].n) : 0;
743
894
  if (total === 0) return { results: [], total: 0 };
744
- const rows = this.db.prepare(
895
+ const rows = this.stmt(
745
896
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
746
897
  -bm25(symbols_fts) AS score,
747
898
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -774,20 +925,60 @@ var IndexStore = class _IndexStore {
774
925
  total
775
926
  };
776
927
  }
928
+ /**
929
+ * Invalidate the cached BM25 index.
930
+ *
931
+ * **Contract: every method that mutates `symbols` MUST call this before
932
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
933
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
934
+ * BM25 score is filtered by the LIKE-selected candidate set in
935
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
936
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
937
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
938
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
939
+ * FTS5-unavailable fallback will serve stale search results. The
940
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
941
+ * which is the only legitimate place that flips the flag outside this
942
+ * helper.
943
+ *
944
+ * Called *before* `runWithRetry` on purpose: if the write fails all
945
+ * retries the flag stays set, forcing a rebuild on the next search rather
946
+ * than trusting a cache that may not reflect the intended mutation.
947
+ * Do not move this inside the retry closure.
948
+ */
949
+ invalidateBm25() {
950
+ this.bm25Dirty = true;
951
+ this.bm25Cache = null;
952
+ }
953
+ /**
954
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
955
+ * symbols table has been mutated since the last build. The full-corpus IDF
956
+ * is more correct than the old per-query candidate-subset IDF, and the
957
+ * amortized build cost drops from O(symbols × tokens) per search to once
958
+ * per write batch.
959
+ *
960
+ * Note: the first call after a long idle (or on a freshly opened store)
961
+ * pays the full corpus rebuild synchronously on the search path. For a
962
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
963
+ */
964
+ getOrBuildBm25() {
965
+ if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
966
+ const docs = this.getAllIndexable();
967
+ this.bm25Cache = buildBm25Index(docs);
968
+ this.bm25Dirty = false;
969
+ return this.bm25Cache;
970
+ }
777
971
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
778
972
  searchRankedFallback(query, filter, limit) {
779
- const candidates = this.search(query, filter);
780
- if (candidates.length === 0) return { results: [], total: 0 };
781
973
  if (!query.trim()) {
782
- return { results: candidates.slice(0, limit), total: candidates.length };
974
+ const total = this.countSearch(query, filter);
975
+ if (total === 0) return { results: [], total: 0 };
976
+ return { results: this.search(query, filter, { limit }), total };
783
977
  }
978
+ const candidates = this.search(query, filter);
979
+ if (candidates.length === 0) return { results: [], total: 0 };
784
980
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
785
- const bm25 = buildBm25Index(
786
- candidates.map((c) => ({
787
- id: c.id,
788
- text: buildIndexableText(c.name, c.signature, c.docComment)
789
- }))
790
- );
981
+ const bm25 = this.getOrBuildBm25();
791
982
  const scored = bm25.score(query, (id) => candidateById.has(id));
792
983
  const q = query.trim().toLowerCase();
793
984
  const rank = (id) => {
@@ -829,7 +1020,7 @@ var IndexStore = class _IndexStore {
829
1020
  // ─── Stats ───────────────────────────────────────────────────────────────────
830
1021
  getStats() {
831
1022
  const sizeBytes = this.sizeBytes();
832
- const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
1023
+ const lastRows = this.stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
833
1024
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
834
1025
  const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
835
1026
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
@@ -854,19 +1045,20 @@ var IndexStore = class _IndexStore {
854
1045
  }
855
1046
  setLastIndexed(ts2) {
856
1047
  this.runWithRetry(() => {
857
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
1048
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
858
1049
  });
859
1050
  }
860
1051
  getMetadata(key) {
861
- const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
1052
+ const rows = this.stmt("SELECT value FROM metadata WHERE key = ?").all(key);
862
1053
  return rows[0]?.value;
863
1054
  }
864
1055
  setMetadata(key, value) {
865
1056
  this.runWithRetry(() => {
866
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
1057
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
867
1058
  });
868
1059
  }
869
1060
  clearAll() {
1061
+ this.invalidateBm25();
870
1062
  this.runWithRetry(() => {
871
1063
  this.db.exec("BEGIN IMMEDIATE");
872
1064
  try {
@@ -893,13 +1085,7 @@ var IndexStore = class _IndexStore {
893
1085
  this.runWithRetry(() => {
894
1086
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
895
1087
  if (refs.length === 0) return;
896
- const stmt = this.db.prepare(
897
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
898
- VALUES (?, ?, ?, ?, ?)`
899
- );
900
- for (const ref of refs) {
901
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
902
- }
1088
+ this.bulkInsertRefs(refs.map((ref) => ({ ...ref, fromId })));
903
1089
  });
904
1090
  }
905
1091
  /**
@@ -916,13 +1102,7 @@ var IndexStore = class _IndexStore {
916
1102
  insertRefsBatch(refs) {
917
1103
  if (refs.length === 0) return;
918
1104
  this.runWithRetry(() => {
919
- const stmt = this.db.prepare(
920
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
921
- VALUES (?, ?, ?, ?, ?)`
922
- );
923
- for (const ref of refs) {
924
- stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
925
- }
1105
+ this.bulkInsertRefs(refs);
926
1106
  });
927
1107
  }
928
1108
  /**
@@ -947,65 +1127,61 @@ var IndexStore = class _IndexStore {
947
1127
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
948
1128
  return [];
949
1129
  }
1130
+ this.invalidateBm25();
950
1131
  return this.runWithRetry(() => {
951
1132
  this.db.exec("BEGIN IMMEDIATE");
952
1133
  try {
953
1134
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
954
1135
  const placeholders = options.deleteForFiles.map(() => "?").join(",");
955
1136
  if (this.ftsAvailable) {
956
- this.db.prepare(
1137
+ this.stmt(
957
1138
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
958
1139
  ).run(...options.deleteForFiles);
959
1140
  }
960
- this.db.prepare(
1141
+ this.stmt(
961
1142
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
962
1143
  ).run(...options.deleteForFiles);
963
- this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
1144
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
964
1145
  }
965
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
966
- let nextId = (maxRows[0]?.m ?? 0) + 1;
967
- const symStmt = this.db.prepare(
968
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
969
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
970
- );
971
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
1146
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
1147
+ let nextId = this.allocateSymbolIds(totalSymbols);
972
1148
  const allInserted = [];
973
1149
  const refsToInsert = [];
1150
+ const bulkSyms = [];
1151
+ const ftsRows = [];
974
1152
  for (const entry of entries) {
975
1153
  const insertedForEntry = [];
976
1154
  for (const s of entry.symbols) {
977
1155
  const id = nextId++;
978
- symStmt.run(
1156
+ bulkSyms.push({
979
1157
  id,
980
- s.lang,
981
- s.kind,
982
- s.name,
983
- s.file,
984
- s.line,
985
- s.col,
986
- s.signature,
987
- s.docComment,
988
- s.scope,
989
- s.text,
990
- s.file
991
- );
992
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
1158
+ lang: s.lang,
1159
+ kind: s.kind,
1160
+ name: s.name,
1161
+ file: s.file,
1162
+ line: s.line,
1163
+ col: s.col,
1164
+ signature: s.signature,
1165
+ docComment: s.docComment,
1166
+ scope: s.scope,
1167
+ text: s.text
1168
+ });
1169
+ if (this.ftsAvailable) {
1170
+ ftsRows.push({
1171
+ id,
1172
+ text: buildIndexableText(s.name, s.signature, s.docComment)
1173
+ });
1174
+ }
993
1175
  const inserted = { ...s, id };
994
1176
  allInserted.push(inserted);
995
1177
  insertedForEntry.push(inserted);
996
1178
  }
997
1179
  refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
998
1180
  }
999
- if (refsToInsert.length > 0) {
1000
- const refStmt = this.db.prepare(
1001
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
1002
- VALUES (?, ?, ?, ?, ?)`
1003
- );
1004
- for (const ref of refsToInsert) {
1005
- refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
1006
- }
1007
- }
1008
- const upsertStmt = this.db.prepare(
1181
+ this.bulkInsertSymbols(bulkSyms);
1182
+ this.bulkInsertFts(ftsRows);
1183
+ this.bulkInsertRefs(refsToInsert);
1184
+ const upsertStmt = this.stmt(
1009
1185
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
1010
1186
  VALUES (?, ?, ?, ?, ?)
1011
1187
  ON CONFLICT(file) DO UPDATE SET
@@ -1032,7 +1208,7 @@ var IndexStore = class _IndexStore {
1032
1208
  */
1033
1209
  deleteRefsForFile(file) {
1034
1210
  this.runWithRetry(() => {
1035
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
1211
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
1036
1212
  });
1037
1213
  }
1038
1214
  /**
@@ -1045,20 +1221,75 @@ var IndexStore = class _IndexStore {
1045
1221
  */
1046
1222
  resolveRefs() {
1047
1223
  return this.runWithRetry(() => {
1048
- const result = this.db.prepare(
1049
- `UPDATE refs SET to_id = (
1050
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
1051
- ) WHERE to_id IS NULL AND to_name IS NOT NULL
1052
- AND to_name IN (SELECT name FROM symbols)`
1053
- ).run();
1054
- return result.changes ?? 0;
1224
+ try {
1225
+ const result = this.stmt(
1226
+ `UPDATE refs
1227
+ SET to_id = s.id
1228
+ FROM (
1229
+ SELECT name, MIN(id) AS id FROM symbols GROUP BY name
1230
+ ) AS s
1231
+ WHERE refs.to_id IS NULL
1232
+ AND refs.to_name IS NOT NULL
1233
+ AND refs.to_name = s.name`
1234
+ ).run();
1235
+ return result.changes ?? 0;
1236
+ } catch {
1237
+ const result = this.stmt(
1238
+ `UPDATE refs SET to_id = (
1239
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
1240
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
1241
+ AND to_name IN (SELECT name FROM symbols)`
1242
+ ).run();
1243
+ return result.changes ?? 0;
1244
+ }
1245
+ });
1246
+ }
1247
+ /**
1248
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
1249
+ * Used by the indexer for empty-parse results so three writes share one txn.
1250
+ */
1251
+ replaceEmptyFile(meta) {
1252
+ this.invalidateBm25();
1253
+ this.runWithRetry(() => {
1254
+ this.db.exec("BEGIN IMMEDIATE");
1255
+ try {
1256
+ if (this.ftsAvailable) {
1257
+ this.stmt(
1258
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
1259
+ ).run(meta.file);
1260
+ }
1261
+ this.stmt(
1262
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
1263
+ ).run(meta.file);
1264
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
1265
+ this.stmt(
1266
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
1267
+ VALUES (?, ?, ?, ?, ?)
1268
+ ON CONFLICT(file) DO UPDATE SET
1269
+ lang = excluded.lang,
1270
+ mtime_ms = excluded.mtime_ms,
1271
+ symbol_count = excluded.symbol_count,
1272
+ last_indexed = excluded.last_indexed`
1273
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
1274
+ this.db.exec("COMMIT");
1275
+ } catch (err) {
1276
+ this.db.exec("ROLLBACK");
1277
+ throw err;
1278
+ }
1055
1279
  });
1056
1280
  }
1281
+ /** Best-effort query planner refresh after a large reindex. */
1282
+ optimize() {
1283
+ try {
1284
+ this.db.exec("PRAGMA optimize");
1285
+ } catch {
1286
+ }
1287
+ }
1057
1288
  /**
1058
1289
  * Find all references TO a given symbol (who calls / uses this symbol?).
1059
1290
  */
1060
1291
  findRefsTo(symbolId) {
1061
- return this.db.prepare(
1292
+ return this.stmt(
1062
1293
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
1063
1294
  ).all(symbolId, symbolId).map((r) => ({
1064
1295
  id: r.id,
@@ -1073,7 +1304,7 @@ var IndexStore = class _IndexStore {
1073
1304
  * Find all references FROM a given symbol (what does this symbol call/use?).
1074
1305
  */
1075
1306
  findRefsFrom(symbolId) {
1076
- return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
1307
+ return this.stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
1077
1308
  id: r.id,
1078
1309
  fromId: r.from_id,
1079
1310
  toName: r.to_name,
@@ -1146,61 +1377,77 @@ var IndexStore = class _IndexStore {
1146
1377
  * symbol resolved in package B). Node metadata includes symbol/file counts.
1147
1378
  */
1148
1379
  getPackageGraph() {
1149
- const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
1380
+ const fileCounts = this.stmt(
1381
+ "SELECT file, COUNT(*) AS n FROM symbols GROUP BY file"
1382
+ ).all();
1150
1383
  const pkgNodes = /* @__PURE__ */ new Map();
1151
1384
  const fileToPkg = /* @__PURE__ */ new Map();
1152
- const symbolToPkg = /* @__PURE__ */ new Map();
1153
- for (const s of symbols) {
1154
- const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
1155
- fileToPkg.set(s.file, pkg);
1156
- symbolToPkg.set(s.id, pkg);
1385
+ for (const { file, n } of fileCounts) {
1386
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1387
+ fileToPkg.set(file, pkg);
1157
1388
  const node = pkgNodes.get(pkg);
1158
1389
  if (node) {
1159
- node.symbolCount = (node.symbolCount ?? 0) + 1;
1390
+ node.symbolCount = (node.symbolCount ?? 0) + n;
1160
1391
  } else {
1161
1392
  pkgNodes.set(pkg, {
1162
1393
  id: `pkg:${pkg}`,
1163
1394
  label: pkg,
1164
1395
  kind: "package",
1165
1396
  package: pkg,
1166
- symbolCount: 1,
1397
+ symbolCount: n,
1167
1398
  fileCount: 0
1168
1399
  });
1169
1400
  }
1170
1401
  }
1171
- const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1402
+ const files = this.stmt("SELECT DISTINCT file FROM files").all();
1172
1403
  for (const { file } of files) {
1173
1404
  const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1405
+ fileToPkg.set(file, pkg);
1174
1406
  const node = pkgNodes.get(pkg);
1175
1407
  if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1408
+ else {
1409
+ pkgNodes.set(pkg, {
1410
+ id: `pkg:${pkg}`,
1411
+ label: pkg,
1412
+ kind: "package",
1413
+ package: pkg,
1414
+ symbolCount: 0,
1415
+ fileCount: 1
1416
+ });
1417
+ }
1176
1418
  }
1177
- const refRows = this.db.prepare(
1178
- `SELECT r.from_id, r.to_id, r.call_type
1419
+ const refRows = this.stmt(
1420
+ `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
1179
1421
  FROM refs r
1180
- WHERE r.to_id IS NOT NULL`
1422
+ JOIN symbols sf ON sf.id = r.from_id
1423
+ JOIN symbols st ON st.id = r.to_id
1424
+ WHERE r.to_id IS NOT NULL AND r.call_type != 'import'
1425
+ GROUP BY r.call_type, sf.file, st.file`
1181
1426
  ).all();
1182
1427
  const edgeMap = /* @__PURE__ */ new Map();
1183
1428
  for (const r of refRows) {
1184
- if (r.call_type === "import") continue;
1185
- const fromPkg = symbolToPkg.get(r.from_id);
1186
- const toPkg = symbolToPkg.get(r.to_id);
1187
- if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
1429
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
1430
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
1431
+ if (fromPkg === toPkg) continue;
1188
1432
  const key = `${fromPkg}\0${toPkg}`;
1189
1433
  let e = edgeMap.get(key);
1190
1434
  if (!e) {
1191
1435
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
1192
1436
  edgeMap.set(key, e);
1193
1437
  }
1194
- e.weight++;
1195
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1438
+ const n = Number(r.n) || 0;
1439
+ e.weight += n;
1440
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
1196
1441
  }
1197
- const importRows = this.db.prepare(
1198
- `SELECT r.from_id, r.to_name
1442
+ const importRows = this.stmt(
1443
+ `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
1199
1444
  FROM refs r
1200
- WHERE r.call_type = 'import'`
1445
+ JOIN symbols s ON s.id = r.from_id
1446
+ WHERE r.call_type = 'import'
1447
+ GROUP BY r.to_name, s.file`
1201
1448
  ).all();
1202
1449
  for (const r of importRows) {
1203
- const fromPkg = symbolToPkg.get(r.from_id);
1450
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
1204
1451
  const toPkg = _IndexStore.packageFromImport(r.to_name);
1205
1452
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1206
1453
  const key = `${fromPkg}\0${toPkg}`;
@@ -1209,8 +1456,9 @@ var IndexStore = class _IndexStore {
1209
1456
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1210
1457
  edgeMap.set(key, edge);
1211
1458
  }
1212
- edge.weight++;
1213
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1459
+ const n = Number(r.n) || 0;
1460
+ edge.weight += n;
1461
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
1214
1462
  }
1215
1463
  const edges = [];
1216
1464
  for (const [key, e] of edgeMap) {
@@ -1237,12 +1485,12 @@ var IndexStore = class _IndexStore {
1237
1485
  * derived from cross-file symbol references within the package.
1238
1486
  */
1239
1487
  getFileGraph(packageFilter) {
1240
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
1488
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
1241
1489
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
1242
1490
  const localFiles = new Set(pkgFilePaths);
1243
1491
  if (localFiles.size === 0) return { nodes: [], edges: [] };
1244
1492
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
1245
- const pkgSyms = this.db.prepare(
1493
+ const pkgSyms = this.stmt(
1246
1494
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
1247
1495
  ).all(...pkgFilePaths);
1248
1496
  const fileNodes = /* @__PURE__ */ new Map();
@@ -1274,12 +1522,13 @@ var IndexStore = class _IndexStore {
1274
1522
  ensureFileNode(file);
1275
1523
  }
1276
1524
  const indexedFiles = new Set(allFiles.map((f) => f.file));
1277
- const refRows = this.db.prepare(
1278
- `SELECT r.from_id, r.to_id, r.call_type
1525
+ const refRows = this.stmt(
1526
+ `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
1279
1527
  FROM refs r
1280
1528
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
1281
1529
  OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
1282
- AND r.to_id IS NOT NULL`
1530
+ AND r.to_id IS NOT NULL
1531
+ GROUP BY r.from_id, r.to_id, r.call_type`
1283
1532
  ).all(...pkgFilePaths, ...pkgFilePaths);
1284
1533
  const knownSymIds = new Set(pkgSyms.map((s) => s.id));
1285
1534
  const crossRefIds = /* @__PURE__ */ new Set();
@@ -1289,7 +1538,7 @@ var IndexStore = class _IndexStore {
1289
1538
  }
1290
1539
  if (crossRefIds.size > 0) {
1291
1540
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
1292
- const extras = this.db.prepare(
1541
+ const extras = this.stmt(
1293
1542
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
1294
1543
  ).all(...crossRefIds);
1295
1544
  for (const x of extras) {
@@ -1314,14 +1563,16 @@ var IndexStore = class _IndexStore {
1314
1563
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
1315
1564
  edgeMap.set(key, e);
1316
1565
  }
1317
- e.weight++;
1318
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1566
+ const n = Number(r.n) || 0;
1567
+ e.weight += n;
1568
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
1319
1569
  }
1320
- const importRows = this.db.prepare(
1321
- `SELECT r.from_id, r.to_name
1570
+ const importRows = this.stmt(
1571
+ `SELECT r.from_id, r.to_name, COUNT(*) AS n
1322
1572
  FROM refs r
1323
1573
  WHERE r.call_type = 'import'
1324
- AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
1574
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
1575
+ GROUP BY r.from_id, r.to_name`
1325
1576
  ).all(...pkgFilePaths);
1326
1577
  for (const r of importRows) {
1327
1578
  const fromFile = symToFile.get(r.from_id);
@@ -1336,8 +1587,9 @@ var IndexStore = class _IndexStore {
1336
1587
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1337
1588
  edgeMap.set(key, edge);
1338
1589
  }
1339
- edge.weight++;
1340
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1590
+ const n = Number(r.n) || 0;
1591
+ edge.weight += n;
1592
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
1341
1593
  }
1342
1594
  const edges = [];
1343
1595
  for (const [key, e] of edgeMap) {
@@ -1364,7 +1616,7 @@ var IndexStore = class _IndexStore {
1364
1616
  * derived from intra-file and cross-file symbol references (who calls whom).
1365
1617
  */
1366
1618
  getSymbolGraph(fileFilter) {
1367
- const syms = this.db.prepare(
1619
+ const syms = this.stmt(
1368
1620
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
1369
1621
  ).all(fileFilter);
1370
1622
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -1384,17 +1636,25 @@ var IndexStore = class _IndexStore {
1384
1636
  scope: s.scope,
1385
1637
  external: s.file !== fileFilter
1386
1638
  });
1387
- const symIds = new Set(syms.map((s) => s.id));
1388
- const refRows = this.db.prepare(
1389
- `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1390
- FROM refs r
1391
- WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
1392
- OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
1639
+ const refRows = this.stmt(
1640
+ `SELECT from_id, to_id, call_type, COUNT(*) AS n
1641
+ FROM (
1642
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1643
+ FROM refs r
1644
+ JOIN symbols s ON s.id = r.from_id
1645
+ WHERE s.file = ?
1646
+ UNION
1647
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1648
+ FROM refs r
1649
+ JOIN symbols s ON s.id = r.to_id
1650
+ WHERE s.file = ?
1651
+ )
1652
+ WHERE to_id IS NOT NULL
1653
+ GROUP BY from_id, to_id, call_type`
1393
1654
  ).all(fileFilter, fileFilter);
1394
1655
  const edgeMap = /* @__PURE__ */ new Map();
1395
1656
  for (const r of refRows) {
1396
- if (r.to_id === null) continue;
1397
- if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
1657
+ if (r.to_id == null) continue;
1398
1658
  relatedIds.add(r.from_id);
1399
1659
  relatedIds.add(r.to_id);
1400
1660
  const key = `${r.from_id}\0${r.to_id}`;
@@ -1403,8 +1663,9 @@ var IndexStore = class _IndexStore {
1403
1663
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
1404
1664
  edgeMap.set(key, e);
1405
1665
  }
1406
- e.weight++;
1407
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1666
+ const n = Number(r.n) || 0;
1667
+ e.weight += n;
1668
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
1408
1669
  }
1409
1670
  const edges = [];
1410
1671
  for (const [key, e] of edgeMap) {
@@ -1428,7 +1689,7 @@ var IndexStore = class _IndexStore {
1428
1689
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
1429
1690
  if (missingIds.length > 0) {
1430
1691
  const placeholders = missingIds.map(() => "?").join(",");
1431
- const extras = this.db.prepare(
1692
+ const extras = this.stmt(
1432
1693
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
1433
1694
  ).all(...missingIds);
1434
1695
  for (const s of extras) symById.set(s.id, s);
@@ -1442,6 +1703,8 @@ var IndexStore = class _IndexStore {
1442
1703
  }
1443
1704
  close() {
1444
1705
  this.stmtCache.clear();
1706
+ this.bm25Dirty = true;
1707
+ this.bm25Cache = null;
1445
1708
  try {
1446
1709
  this.db.close();
1447
1710
  } catch {
@@ -1455,14 +1718,121 @@ import { fileURLToPath } from "node:url";
1455
1718
  import { Worker } from "node:worker_threads";
1456
1719
 
1457
1720
  // src/codebase-index/indexer.ts
1458
- import { expectDefined as expectDefined5 } from "@wrongstack/core";
1721
+ import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
1459
1722
  import * as fs7 from "node:fs/promises";
1460
- import * as path8 from "node:path";
1723
+ import * as path9 from "node:path";
1461
1724
  import { availableParallelism } from "node:os";
1462
- import { compileGlob as compileGlob2 } from "@wrongstack/core";
1725
+ import { DEFAULT_WALK_IGNORE_DIRS, indexParallelBatchSize, isFrugalPerf } from "@wrongstack/core/utils";
1463
1726
 
1464
1727
  // src/codebase-index/ts-parser.ts
1465
1728
  import * as ts from "@typescript/typescript6";
1729
+
1730
+ // src/codebase-index/languages.ts
1731
+ import * as path2 from "node:path";
1732
+ var EXT_TO_LANG = {
1733
+ // TypeScript / JavaScript (first-class TS compiler API)
1734
+ ".ts": "ts",
1735
+ ".mts": "ts",
1736
+ ".cts": "ts",
1737
+ ".tsx": "tsx",
1738
+ ".js": "js",
1739
+ ".mjs": "js",
1740
+ ".cjs": "js",
1741
+ ".jsx": "jsx",
1742
+ // First-class native/spawn parsers (+ regex fallback)
1743
+ ".go": "go",
1744
+ ".py": "py",
1745
+ ".pyi": "py",
1746
+ ".pyw": "py",
1747
+ ".rs": "rs",
1748
+ ".json": "json",
1749
+ ".jsonc": "json",
1750
+ ".yaml": "yaml",
1751
+ ".yml": "yaml",
1752
+ // C family
1753
+ ".c": "c",
1754
+ ".h": "c",
1755
+ ".cc": "cpp",
1756
+ ".cpp": "cpp",
1757
+ ".cxx": "cpp",
1758
+ ".hh": "cpp",
1759
+ ".hpp": "cpp",
1760
+ ".hxx": "cpp",
1761
+ // JVM / .NET
1762
+ ".java": "java",
1763
+ ".cs": "csharp",
1764
+ ".kt": "kotlin",
1765
+ ".kts": "kotlin",
1766
+ ".scala": "scala",
1767
+ ".sc": "scala",
1768
+ // Scripting
1769
+ ".php": "php",
1770
+ ".rb": "ruby",
1771
+ ".swift": "swift",
1772
+ ".dart": "dart",
1773
+ ".lua": "lua",
1774
+ ".r": "r",
1775
+ ".R": "r",
1776
+ ".pl": "other",
1777
+ ".pm": "other",
1778
+ // Systems / functional
1779
+ ".zig": "zig",
1780
+ ".ex": "elixir",
1781
+ ".exs": "elixir",
1782
+ ".hs": "haskell",
1783
+ ".lhs": "haskell",
1784
+ // Shell / data / docs / web
1785
+ ".sh": "shell",
1786
+ ".bash": "shell",
1787
+ ".zsh": "shell",
1788
+ ".ps1": "shell",
1789
+ ".sql": "sql",
1790
+ ".md": "md",
1791
+ ".mdx": "md",
1792
+ ".toml": "toml",
1793
+ ".html": "html",
1794
+ ".htm": "html",
1795
+ ".css": "css",
1796
+ ".scss": "css",
1797
+ ".less": "css",
1798
+ ".vue": "vue",
1799
+ ".svelte": "svelte",
1800
+ ".proto": "proto",
1801
+ ".graphql": "graphql",
1802
+ ".gql": "graphql"
1803
+ };
1804
+ var INDEXABLE_EXTENSIONS = Object.freeze(
1805
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
1806
+ );
1807
+ var SPECIAL_FILENAMES = {
1808
+ makefile: "other",
1809
+ gnumakefile: "other",
1810
+ dockerfile: "other",
1811
+ "docker-compose.yml": "yaml",
1812
+ "docker-compose.yaml": "yaml",
1813
+ "cmakelists.txt": "other",
1814
+ gemfile: "ruby",
1815
+ rakefile: "ruby",
1816
+ procfile: "other",
1817
+ justfile: "other"
1818
+ };
1819
+ function detectLang(file) {
1820
+ const base = path2.basename(file);
1821
+ const lowerBase = base.toLowerCase();
1822
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
1823
+ return "ts";
1824
+ }
1825
+ const special = SPECIAL_FILENAMES[lowerBase];
1826
+ if (special) return special;
1827
+ const ext = path2.extname(base).toLowerCase();
1828
+ if (!ext) return null;
1829
+ return EXT_TO_LANG[ext] ?? null;
1830
+ }
1831
+ function isIndexablePath(file) {
1832
+ return detectLang(file) !== null;
1833
+ }
1834
+
1835
+ // src/codebase-index/ts-parser.ts
1466
1836
  var KIND_MAP = {
1467
1837
  [ts.SyntaxKind.ClassDeclaration]: "class",
1468
1838
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -1489,32 +1859,6 @@ function kindOf(node) {
1489
1859
  if (ts.isModuleDeclaration(node)) return "namespace";
1490
1860
  return KIND_MAP[node.kind] ?? null;
1491
1861
  }
1492
- function extToLang(ext) {
1493
- switch (ext) {
1494
- case ".ts":
1495
- return "ts";
1496
- case ".tsx":
1497
- return "tsx";
1498
- case ".js":
1499
- return "js";
1500
- case ".jsx":
1501
- return "jsx";
1502
- case ".go":
1503
- return "go";
1504
- case ".py":
1505
- return "py";
1506
- case ".rs":
1507
- return "rs";
1508
- case ".json":
1509
- return "json";
1510
- case ".yaml":
1511
- return "yaml";
1512
- case ".yml":
1513
- return "yaml";
1514
- default:
1515
- return null;
1516
- }
1517
- }
1518
1862
  function getSignature(printer, node, sourceFile) {
1519
1863
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1520
1864
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -1637,30 +1981,25 @@ function deduplicateRefs(refs) {
1637
1981
  return true;
1638
1982
  });
1639
1983
  }
1640
- function detectLang(file) {
1641
- const idx = file.lastIndexOf(".");
1642
- if (idx < 0) return null;
1643
- return extToLang(file.slice(idx));
1644
- }
1645
1984
 
1646
1985
  // src/codebase-index/go-parser.ts
1647
1986
  import { spawn } from "node:child_process";
1648
1987
  import * as os from "node:os";
1649
- import * as path3 from "node:path";
1988
+ import * as path4 from "node:path";
1650
1989
  import * as fs3 from "node:fs/promises";
1651
1990
 
1652
1991
  // src/_win32-resolve.ts
1653
1992
  import * as fs2 from "node:fs";
1654
- import * as path2 from "node:path";
1993
+ import * as path3 from "node:path";
1655
1994
  function resolveWin32Command(cmd) {
1656
1995
  if (process.platform !== "win32") return cmd;
1657
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
1996
+ if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
1658
1997
  return cmd;
1659
1998
  }
1660
1999
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
1661
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
2000
+ const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
1662
2001
  for (const dir of pathDirs) {
1663
- const base = path2.join(dir, cmd);
2002
+ const base = path3.join(dir, cmd);
1664
2003
  for (const ext of pathext) {
1665
2004
  const full = `${base}${ext}`;
1666
2005
  try {
@@ -1673,11 +2012,22 @@ function resolveWin32Command(cmd) {
1673
2012
  return cmd;
1674
2013
  }
1675
2014
 
2015
+ // src/codebase-index/spawn-gate.ts
2016
+ var chain = Promise.resolve();
2017
+ function withSpawnGate(fn) {
2018
+ const run = chain.then(fn, fn);
2019
+ chain = run.then(
2020
+ () => void 0,
2021
+ () => void 0
2022
+ );
2023
+ return run;
2024
+ }
2025
+
1676
2026
  // src/codebase-index/go-parser.ts
1677
2027
  async function parseSymbols2(opts) {
1678
2028
  const { file, content, lang } = opts;
1679
2029
  try {
1680
- const parsed = await syncGoParse(file, content, lang);
2030
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
1681
2031
  if (parsed.symbols.length > 0) {
1682
2032
  return parsed;
1683
2033
  }
@@ -1973,11 +2323,16 @@ func formatType(t ast.Expr) string {
1973
2323
  }
1974
2324
  }
1975
2325
  `;
2326
+ var _cachedGoScriptPath = null;
1976
2327
  async function syncGoParse(filePath, content, lang) {
1977
- const tmpDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "ws-go-parse-"));
1978
2328
  try {
1979
- const scriptPath = path3.join(tmpDir, "parse.go");
1980
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2329
+ let scriptPath = _cachedGoScriptPath;
2330
+ if (!scriptPath) {
2331
+ const tmpDir = await fs3.mkdtemp(path4.join(os.tmpdir(), "ws-go-parse-"));
2332
+ scriptPath = path4.join(tmpDir, "parse.go");
2333
+ await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2334
+ _cachedGoScriptPath = scriptPath;
2335
+ }
1981
2336
  const goBinary = resolveWin32Command("go");
1982
2337
  const goResult = await new Promise(
1983
2338
  (resolve2, reject) => {
@@ -2034,23 +2389,311 @@ async function syncGoParse(filePath, content, lang) {
2034
2389
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2035
2390
  } catch {
2036
2391
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2037
- } finally {
2038
- await fs3.rm(tmpDir, { recursive: true, force: true });
2039
2392
  }
2040
2393
  }
2041
2394
 
2042
2395
  // src/codebase-index/py-parser.ts
2043
- import { spawn as spawn2, spawnSync } from "node:child_process";
2396
+ import { spawn as spawn2 } from "node:child_process";
2044
2397
  import * as fs4 from "node:fs/promises";
2045
2398
  import * as os2 from "node:os";
2046
- import * as path4 from "node:path";
2399
+ import * as path5 from "node:path";
2400
+
2401
+ // src/codebase-index/generic-parser.ts
2402
+ var C_LIKE = [
2403
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
2404
+ {
2405
+ re: /\b(?:public|private|protected|static|final|async|override|virtual|inline|export)?\s*(?:[\w:<>[\]\s*&]+)\s+([A-Za-z_]\w*)\s*\([^;{]*\)\s*(?:const)?\s*[{;]/g,
2406
+ kind: "function"
2407
+ },
2408
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
2409
+ ];
2410
+ var LANG_PATTERNS = {
2411
+ py: [
2412
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
2413
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2414
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
2415
+ ],
2416
+ go: [
2417
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
2418
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
2419
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
2420
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
2421
+ ],
2422
+ rs: [
2423
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
2424
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
2425
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
2426
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
2427
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
2428
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
2429
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
2430
+ ],
2431
+ c: C_LIKE,
2432
+ cpp: C_LIKE,
2433
+ java: [
2434
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
2435
+ {
2436
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
2437
+ kind: "method"
2438
+ }
2439
+ ],
2440
+ csharp: [
2441
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
2442
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
2443
+ {
2444
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
2445
+ kind: "method"
2446
+ }
2447
+ ],
2448
+ php: [
2449
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
2450
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
2451
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
2452
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
2453
+ ],
2454
+ ruby: [
2455
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
2456
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2457
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
2458
+ ],
2459
+ swift: [
2460
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
2461
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
2462
+ ],
2463
+ kotlin: [
2464
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
2465
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
2466
+ ],
2467
+ scala: [
2468
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
2469
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
2470
+ ],
2471
+ shell: [
2472
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
2473
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
2474
+ ],
2475
+ sql: [
2476
+ { 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" }
2477
+ ],
2478
+ md: [
2479
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
2480
+ ],
2481
+ toml: [
2482
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
2483
+ ],
2484
+ html: [
2485
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
2486
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
2487
+ ],
2488
+ css: [
2489
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
2490
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
2491
+ ],
2492
+ vue: [
2493
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
2494
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
2495
+ ],
2496
+ svelte: [
2497
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
2498
+ ],
2499
+ dart: [
2500
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
2501
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
2502
+ ],
2503
+ lua: [
2504
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
2505
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
2506
+ ],
2507
+ r: [
2508
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
2509
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
2510
+ ],
2511
+ proto: [
2512
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
2513
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
2514
+ ],
2515
+ graphql: [
2516
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
2517
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
2518
+ ],
2519
+ zig: [
2520
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
2521
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
2522
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
2523
+ ],
2524
+ elixir: [
2525
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
2526
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
2527
+ ],
2528
+ haskell: [
2529
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
2530
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
2531
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
2532
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
2533
+ ],
2534
+ other: [
2535
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
2536
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2537
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
2538
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
2539
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
2540
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
2541
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
2542
+ ]
2543
+ };
2544
+ var KEYWORDS = /* @__PURE__ */ new Set([
2545
+ "if",
2546
+ "else",
2547
+ "for",
2548
+ "while",
2549
+ "switch",
2550
+ "case",
2551
+ "return",
2552
+ "break",
2553
+ "continue",
2554
+ "new",
2555
+ "delete",
2556
+ "typeof",
2557
+ "instanceof",
2558
+ "void",
2559
+ "null",
2560
+ "true",
2561
+ "false",
2562
+ "this",
2563
+ "super",
2564
+ "import",
2565
+ "export",
2566
+ "from",
2567
+ "as",
2568
+ "default",
2569
+ "public",
2570
+ "private",
2571
+ "protected",
2572
+ "static",
2573
+ "final",
2574
+ "class",
2575
+ "struct",
2576
+ "enum",
2577
+ "interface",
2578
+ "function",
2579
+ "def",
2580
+ "fn",
2581
+ "func",
2582
+ "var",
2583
+ "let",
2584
+ "const",
2585
+ "package",
2586
+ "namespace",
2587
+ "module",
2588
+ "using",
2589
+ "include",
2590
+ "require",
2591
+ "select",
2592
+ "from",
2593
+ "where",
2594
+ "and",
2595
+ "or",
2596
+ "not",
2597
+ "in",
2598
+ "is",
2599
+ "try",
2600
+ "catch",
2601
+ "finally",
2602
+ "throw",
2603
+ "async",
2604
+ "await",
2605
+ "yield"
2606
+ ]);
2607
+ function patternsFor(lang) {
2608
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
2609
+ }
2610
+ function looksBinary(content) {
2611
+ if (content.includes("\0")) return true;
2612
+ const sample = content.slice(0, 2048);
2613
+ if (sample.length === 0) return false;
2614
+ let bad = 0;
2615
+ for (let i = 0; i < sample.length; i++) {
2616
+ const c = sample.charCodeAt(i);
2617
+ if (c === 9 || c === 10 || c === 13) continue;
2618
+ if (c < 32 || c === 127) bad++;
2619
+ }
2620
+ return bad / sample.length > 0.1;
2621
+ }
2622
+ function lineColAt(content, index) {
2623
+ let line = 1;
2624
+ let lastNl = -1;
2625
+ for (let i = 0; i < index && i < content.length; i++) {
2626
+ if (content.charCodeAt(i) === 10) {
2627
+ line++;
2628
+ lastNl = i;
2629
+ }
2630
+ }
2631
+ return { line, col: index - lastNl };
2632
+ }
2633
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
2634
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
2635
+ function parseGeneric(opts) {
2636
+ const { file, lang } = opts;
2637
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
2638
+ const mtimeMs = Date.now();
2639
+ if (!opts.content || looksBinary(opts.content)) {
2640
+ return { file, lang, symbols: [], mtimeMs };
2641
+ }
2642
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
2643
+ const patterns = patternsFor(lang);
2644
+ const symbols = [];
2645
+ const seen = /* @__PURE__ */ new Set();
2646
+ for (const pattern of patterns) {
2647
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
2648
+ re.lastIndex = 0;
2649
+ for (const match of content.matchAll(re)) {
2650
+ if (symbols.length >= maxSymbols) break;
2651
+ let name = (match[1] ?? match[2] ?? "").trim();
2652
+ if (lang === "md" && match[2]) name = match[2].trim();
2653
+ if (!name || name.length > 200) continue;
2654
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
2655
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
2656
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
2657
+ continue;
2658
+ }
2659
+ const { line, col } = lineColAt(content, match.index);
2660
+ const key = `${name}\0${line}\0${pattern.kind}`;
2661
+ if (seen.has(key)) continue;
2662
+ seen.add(key);
2663
+ const nl = content.indexOf("\n", match.index);
2664
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
2665
+ const signature = (lineText || name).trim().slice(0, 500);
2666
+ symbols.push({
2667
+ id: 0,
2668
+ lang,
2669
+ kind: lang === "md" ? "namespace" : pattern.kind,
2670
+ name: name.slice(0, 200),
2671
+ file,
2672
+ line,
2673
+ col,
2674
+ signature,
2675
+ docComment: "",
2676
+ scope: "",
2677
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
2678
+ });
2679
+ }
2680
+ if (symbols.length >= maxSymbols) break;
2681
+ }
2682
+ return { file, lang, symbols, mtimeMs };
2683
+ }
2047
2684
  async function parseSymbols3(opts) {
2685
+ return parseGeneric(opts);
2686
+ }
2687
+
2688
+ // src/codebase-index/py-parser.ts
2689
+ async function parseSymbols4(opts) {
2048
2690
  const { file, content, lang } = opts;
2049
2691
  try {
2050
- return await syncPyParse(file, content, lang);
2692
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
2693
+ if (native !== null) return native;
2051
2694
  } catch {
2052
- return { file, lang, symbols: [], mtimeMs: Date.now() };
2053
2695
  }
2696
+ return parseGeneric({ file, content, lang: lang === "py" ? "py" : lang });
2054
2697
  }
2055
2698
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
2056
2699
 
@@ -2255,20 +2898,37 @@ visitor.visit(tree)
2255
2898
 
2256
2899
  print(json.dumps([s.to_dict() for s in syms]))
2257
2900
  `;
2258
- function resolvePython() {
2901
+ async function resolvePython() {
2259
2902
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
2260
2903
  for (const name of candidates) {
2261
2904
  const resolved = resolveWin32Command(name);
2262
- const result = spawnSync(resolved, ["--version"], {
2263
- stdio: "pipe",
2264
- timeout: 5e3
2265
- });
2266
- if (result.error) continue;
2267
- if (result.status !== 0) continue;
2905
+ if (!await commandIsAvailable(resolved)) continue;
2268
2906
  return resolved;
2269
2907
  }
2270
2908
  return null;
2271
2909
  }
2910
+ function commandIsAvailable(command) {
2911
+ return new Promise((resolve2) => {
2912
+ let settled = false;
2913
+ const proc = spawn2(command, ["--version"], {
2914
+ stdio: "ignore",
2915
+ windowsHide: true
2916
+ });
2917
+ const finish = (available) => {
2918
+ if (settled) return;
2919
+ settled = true;
2920
+ clearTimeout(timer);
2921
+ resolve2(available);
2922
+ };
2923
+ const timer = setTimeout(() => {
2924
+ proc.kill("SIGKILL");
2925
+ finish(false);
2926
+ }, 5e3);
2927
+ timer.unref?.();
2928
+ proc.once("error", () => finish(false));
2929
+ proc.once("close", (code) => finish(code === 0));
2930
+ });
2931
+ }
2272
2932
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2273
2933
  return new Promise((resolve2, reject) => {
2274
2934
  let settled = false;
@@ -2304,21 +2964,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2304
2964
  });
2305
2965
  }
2306
2966
  var _cachedScriptPath = null;
2307
- var _cachedPyBinary = null;
2967
+ var cachedPyBinary;
2308
2968
  async function syncPyParse(filePath, content, lang) {
2309
2969
  try {
2310
2970
  if (!_cachedScriptPath) {
2311
- const tmpDir = path4.join(os2.tmpdir(), "ws-py-parse");
2971
+ const tmpDir = path5.join(os2.tmpdir(), "ws-py-parse");
2312
2972
  await fs4.mkdir(tmpDir, { recursive: true });
2313
- _cachedScriptPath = path4.join(tmpDir, "parse.py");
2973
+ _cachedScriptPath = path5.join(tmpDir, "parse.py");
2314
2974
  await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
2315
2975
  }
2316
- if (!_cachedPyBinary) {
2317
- _cachedPyBinary = resolvePython();
2318
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2319
- }
2976
+ cachedPyBinary ??= resolvePython();
2977
+ const pyBinary = await cachedPyBinary;
2978
+ if (!pyBinary) return null;
2320
2979
  const { code, stdout } = await spawnPyParser(
2321
- _cachedPyBinary,
2980
+ pyBinary,
2322
2981
  _cachedScriptPath,
2323
2982
  filePath,
2324
2983
  content
@@ -2342,30 +3001,39 @@ async function syncPyParse(filePath, content, lang) {
2342
3001
  }));
2343
3002
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2344
3003
  } catch {
2345
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
3004
+ return null;
2346
3005
  }
2347
3006
  }
2348
3007
 
2349
3008
  // src/codebase-index/rs-parser.ts
2350
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
2351
- import { execFileSync, spawn as spawn3 } from "node:child_process";
3009
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
3010
+ import { execFile, spawn as spawn3 } from "node:child_process";
2352
3011
  import * as fs5 from "node:fs/promises";
2353
- import * as path5 from "node:path";
2354
- async function parseSymbols4(opts) {
3012
+ import * as path6 from "node:path";
3013
+ async function parseSymbols5(opts) {
2355
3014
  const { file, content, lang } = opts;
2356
- const nativeAvailable = checkNativeParser();
3015
+ const nativeAvailable = await checkNativeParser();
2357
3016
  if (nativeAvailable) {
2358
- const result = await tryNativeParse(file, content);
3017
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
2359
3018
  if (result) return result;
2360
3019
  }
2361
3020
  return regexParse({ file, content, lang });
2362
3021
  }
3022
+ var nativeParserAvailability;
3023
+ function probe(command, args) {
3024
+ return new Promise((resolve2, reject) => {
3025
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
3026
+ if (error) reject(error);
3027
+ else resolve2();
3028
+ });
3029
+ });
3030
+ }
2363
3031
  function checkNativeParser() {
2364
- try {
2365
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
2366
- const toolsDir = path5.join(process.cwd(), "tools");
3032
+ nativeParserAvailability ??= (async () => {
2367
3033
  try {
2368
- execFileSync(
3034
+ await probe("rustc", ["--version"]);
3035
+ const toolsDir = path6.join(process.cwd(), "tools");
3036
+ await probe(
2369
3037
  "cargo",
2370
3038
  [
2371
3039
  "metadata",
@@ -2373,23 +3041,21 @@ function checkNativeParser() {
2373
3041
  "--format-version",
2374
3042
  "1",
2375
3043
  "--manifest-path",
2376
- path5.join(toolsDir, "Cargo.toml")
2377
- ],
2378
- { stdio: "pipe", windowsHide: true }
3044
+ path6.join(toolsDir, "Cargo.toml")
3045
+ ]
2379
3046
  );
2380
3047
  return true;
2381
3048
  } catch {
2382
3049
  return false;
2383
3050
  }
2384
- } catch {
2385
- return false;
2386
- }
3051
+ })();
3052
+ return nativeParserAvailability;
2387
3053
  }
2388
3054
  async function tryNativeParse(file, content) {
2389
3055
  try {
2390
- const toolsDir = path5.join(process.cwd(), "tools");
2391
- const crateDir = path5.join(toolsDir, "syn-parser");
2392
- const tmpFile = path5.join(crateDir, "src", "input.rs");
3056
+ const toolsDir = path6.join(process.cwd(), "tools");
3057
+ const crateDir = path6.join(toolsDir, "syn-parser");
3058
+ const tmpFile = path6.join(crateDir, "src", "input.rs");
2393
3059
  await fs5.writeFile(tmpFile, content, "utf8");
2394
3060
  const cargoBinary = resolveWin32Command("cargo");
2395
3061
  const result = await new Promise(
@@ -2397,7 +3063,7 @@ async function tryNativeParse(file, content) {
2397
3063
  let settled = false;
2398
3064
  const proc = spawn3(
2399
3065
  cargoBinary,
2400
- ["run", "--manifest-path", path5.join(toolsDir, "Cargo.toml")],
3066
+ ["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
2401
3067
  {
2402
3068
  cwd: process.cwd(),
2403
3069
  stdio: ["pipe", "pipe", "pipe"],
@@ -2511,9 +3177,9 @@ function regexParse(opts) {
2511
3177
  }
2512
3178
 
2513
3179
  // src/codebase-index/json-parser.ts
2514
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
2515
- import * as path6 from "node:path";
2516
- function parseSymbols5(opts) {
3180
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
3181
+ import * as path7 from "node:path";
3182
+ function parseSymbols6(opts) {
2517
3183
  const { file, content, lang } = opts;
2518
3184
  try {
2519
3185
  return regexParse2({ file, content, lang });
@@ -2524,9 +3190,9 @@ function parseSymbols5(opts) {
2524
3190
  function regexParse2(opts) {
2525
3191
  const { file, content, lang } = opts;
2526
3192
  const symbols = [];
2527
- const basename3 = path6.basename(file).toLowerCase();
2528
- const isPackageJson = basename3 === "package.json";
2529
- const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
3193
+ const basename4 = path7.basename(file).toLowerCase();
3194
+ const isPackageJson = basename4 === "package.json";
3195
+ const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
2530
3196
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2531
3197
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2532
3198
  const lines = content.split("\n");
@@ -2550,11 +3216,11 @@ function regexParse2(opts) {
2550
3216
  const line = lineFromOffset(offset);
2551
3217
  symbols.push(
2552
3218
  makeSymbol({
2553
- name: path6.basename(file),
3219
+ name: path7.basename(file),
2554
3220
  kind: "object",
2555
3221
  line,
2556
3222
  col: 0,
2557
- signature: `"${path6.basename(file)}" = { ... }`,
3223
+ signature: `"${path7.basename(file)}" = { ... }`,
2558
3224
  file,
2559
3225
  lang
2560
3226
  })
@@ -2716,8 +3382,8 @@ function makeSymbol(opts) {
2716
3382
  }
2717
3383
 
2718
3384
  // src/codebase-index/yaml-parser.ts
2719
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core";
2720
- function parseSymbols6(opts) {
3385
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
3386
+ function parseSymbols7(opts) {
2721
3387
  const { file, content, lang } = opts;
2722
3388
  try {
2723
3389
  return regexParse3({ file, content, lang });
@@ -2866,8 +3532,8 @@ function makeSymbol2(opts) {
2866
3532
 
2867
3533
  // src/codebase-index/gitignore.ts
2868
3534
  import * as fs6 from "node:fs/promises";
2869
- import * as path7 from "node:path";
2870
- import { compileGlob } from "@wrongstack/core";
3535
+ import * as path8 from "node:path";
3536
+ import { compileGlob } from "@wrongstack/core/utils";
2871
3537
  function globBody(glob) {
2872
3538
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
2873
3539
  }
@@ -2912,7 +3578,7 @@ function compileGitignore(lines) {
2912
3578
  async function loadGitignoreMatcher(projectRoot) {
2913
3579
  let lines = [];
2914
3580
  try {
2915
- const raw = await fs6.readFile(path7.join(projectRoot, ".gitignore"), "utf8");
3581
+ const raw = await fs6.readFile(path8.join(projectRoot, ".gitignore"), "utf8");
2916
3582
  lines = raw.split("\n");
2917
3583
  } catch {
2918
3584
  }
@@ -2921,7 +3587,9 @@ async function loadGitignoreMatcher(projectRoot) {
2921
3587
 
2922
3588
  // src/codebase-index/indexer.ts
2923
3589
  var YIELD_EVERY_N = 50;
2924
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
3590
+ function resolveParallelBatch() {
3591
+ return indexParallelBatchSize(availableParallelism());
3592
+ }
2925
3593
  function yieldEventLoop() {
2926
3594
  return new Promise((resolve2) => setImmediate(resolve2));
2927
3595
  }
@@ -2933,22 +3601,12 @@ function throwIfAborted(signal) {
2933
3601
  function isAbortError(err) {
2934
3602
  return err instanceof DOMException && err.name === "AbortError";
2935
3603
  }
2936
- var DEFAULT_IGNORE = [
2937
- "node_modules",
2938
- ".git",
2939
- "dist",
2940
- "build",
2941
- ".next",
2942
- "coverage",
2943
- ".turbo",
2944
- "__snapshots__",
2945
- ".nyc_output"
2946
- ];
3604
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
2947
3605
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2948
3606
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2949
3607
  function isWithinProject(projectRoot, file) {
2950
- const rel = path8.relative(projectRoot, file);
2951
- return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
3608
+ const rel = path9.relative(projectRoot, file);
3609
+ return rel !== "" && !rel.startsWith(`..${path9.sep}`) && rel !== ".." && !path9.isAbsolute(rel);
2952
3610
  }
2953
3611
  function isMissingPathError(err) {
2954
3612
  const code = err?.code;
@@ -2959,18 +3617,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2959
3617
  const errors = [];
2960
3618
  let complete = true;
2961
3619
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2962
- const globs = [
2963
- { ext: ".ts", pat: compileGlob2("**/*.ts") },
2964
- { ext: ".tsx", pat: compileGlob2("**/*.tsx") },
2965
- { ext: ".js", pat: compileGlob2("**/*.js") },
2966
- { ext: ".jsx", pat: compileGlob2("**/*.jsx") },
2967
- { ext: ".go", pat: compileGlob2("**/*.go") },
2968
- { ext: ".py", pat: compileGlob2("**/*.py") },
2969
- { ext: ".rs", pat: compileGlob2("**/*.rs") },
2970
- { ext: ".json", pat: compileGlob2("**/*.json") },
2971
- { ext: ".yaml", pat: compileGlob2("**/*.yaml") },
2972
- { ext: ".yml", pat: compileGlob2("**/*.yml") }
2973
- ];
3620
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
2974
3621
  let dirCount = 0;
2975
3622
  const walk = async (dir) => {
2976
3623
  throwIfAborted(signal);
@@ -2989,19 +3636,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2989
3636
  dirCount++;
2990
3637
  for (const e of entries) {
2991
3638
  if (ignoreSet.has(e.name)) continue;
2992
- const full = path8.join(dir, e.name);
2993
- const rel = path8.relative(projectRoot, full).replace(/\\/g, "/");
3639
+ const full = path9.join(dir, e.name);
3640
+ const rel = path9.relative(projectRoot, full).replace(/\\/g, "/");
2994
3641
  if (e.isDirectory()) {
2995
3642
  if (isGitIgnored(rel, true)) continue;
2996
3643
  await walk(full);
2997
3644
  } else if (e.isFile()) {
2998
3645
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2999
- const ext = path8.extname(e.name).toLowerCase();
3000
- for (const { ext: extName, pat } of globs) {
3001
- if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
3002
- results.push(full);
3003
- break;
3004
- }
3646
+ const ext = path9.extname(e.name).toLowerCase();
3647
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
3648
+ results.push(full);
3005
3649
  }
3006
3650
  }
3007
3651
  }
@@ -3010,24 +3654,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
3010
3654
  return { files: results, complete, errors };
3011
3655
  }
3012
3656
  async function parseFile(file, content, lang) {
3013
- switch (lang) {
3657
+ const symbolLang = lang;
3658
+ switch (symbolLang) {
3014
3659
  case "ts":
3015
3660
  case "tsx":
3016
3661
  case "js":
3017
3662
  case "jsx":
3018
- return parseSymbols({ file, content, lang });
3663
+ return parseSymbols({ file, content, lang: symbolLang });
3019
3664
  case "go":
3020
3665
  return parseSymbols2({ file, content, lang: "go" });
3021
3666
  case "py":
3022
- return parseSymbols3({ file, content, lang: "py" });
3667
+ return parseSymbols4({ file, content, lang: "py" });
3023
3668
  case "rs":
3024
- return parseSymbols4({ file, content, lang: "rs" });
3669
+ return parseSymbols5({ file, content, lang: "rs" });
3025
3670
  case "json":
3026
- return parseSymbols5({ file, content, lang: "json" });
3671
+ return parseSymbols6({ file, content, lang: "json" });
3027
3672
  case "yaml":
3028
- return parseSymbols6({ file, content, lang: "yaml" });
3673
+ return parseSymbols7({ file, content, lang: "yaml" });
3029
3674
  default:
3030
- return { file, lang, symbols: [], mtimeMs: Date.now() };
3675
+ return parseSymbols3({ file, content, lang: symbolLang });
3031
3676
  }
3032
3677
  }
3033
3678
  function assignRefsToSymbols2(refs, symbols) {
@@ -3075,10 +3720,10 @@ async function runIndexerWithStore(store, opts) {
3075
3720
  let discoveredFiles = null;
3076
3721
  let discoveryComplete = true;
3077
3722
  if (opts.files && opts.files.length > 0) {
3078
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3723
+ files = opts.files.map((f) => path9.resolve(projectRoot, f)).filter((f) => {
3079
3724
  if (!isWithinProject(projectRoot, f)) return false;
3080
- const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
3081
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
3725
+ const rel = path9.relative(projectRoot, f).replace(/\\/g, "/");
3726
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path9.basename(f)) && !isGitIgnored(rel, false);
3082
3727
  });
3083
3728
  } else {
3084
3729
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -3099,15 +3744,19 @@ async function runIndexerWithStore(store, opts) {
3099
3744
  if (!force) {
3100
3745
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
3101
3746
  }
3747
+ const parallelBatch = resolveParallelBatch();
3102
3748
  let filesSinceLastYield = 0;
3103
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
3104
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
3749
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
3750
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
3105
3751
  const batchFiles = files.slice(batchStart, batchEnd);
3106
3752
  opts.onProgress?.(batchEnd, files.length);
3107
3753
  filesSinceLastYield += batchFiles.length;
3108
3754
  if (filesSinceLastYield >= YIELD_EVERY_N) {
3109
3755
  filesSinceLastYield = 0;
3110
3756
  await yieldEventLoop();
3757
+ if (isFrugalPerf()) {
3758
+ await new Promise((r) => setTimeout(r, 8));
3759
+ }
3111
3760
  throwIfAborted(signal);
3112
3761
  }
3113
3762
  const statOpts = signal ? { signal } : {};
@@ -3211,9 +3860,7 @@ async function runIndexerWithStore(store, opts) {
3211
3860
  continue;
3212
3861
  }
3213
3862
  if (parsed.symbols.length === 0) {
3214
- store.deleteRefsForFile(file);
3215
- store.deleteSymbolsForFile(file);
3216
- store.upsertFile({
3863
+ store.replaceEmptyFile({
3217
3864
  file,
3218
3865
  lang,
3219
3866
  mtimeMs: Math.floor(stat2.mtimeMs),
@@ -3282,6 +3929,7 @@ async function runIndexerWithStore(store, opts) {
3282
3929
  }
3283
3930
  store.resolveRefs();
3284
3931
  store.setMetadata("relation_graph_version", relationGraphVersion);
3932
+ store.optimize();
3285
3933
  const durationMs = Date.now() - startMs;
3286
3934
  store.setLastIndexed(Date.now());
3287
3935
  return {
@@ -3572,10 +4220,10 @@ async function callInline(op, args, opts) {
3572
4220
  opts.signal?.removeEventListener("abort", onOuterAbort);
3573
4221
  }
3574
4222
  }
3575
- var chain = Promise.resolve();
4223
+ var chain2 = Promise.resolve();
3576
4224
  function withMutex(job) {
3577
- const run = chain.then(job, job);
3578
- chain = run.then(
4225
+ const run = chain2.then(job, job);
4226
+ chain2 = run.then(
3579
4227
  () => void 0,
3580
4228
  () => void 0
3581
4229
  );
@@ -3589,11 +4237,62 @@ function circuitOpenError() {
3589
4237
  }
3590
4238
  var DEFAULT_DEBOUNCE_MS = 400;
3591
4239
  var debounceTimers = /* @__PURE__ */ new Map();
3592
- function debounceKey(indexDir, file) {
3593
- return `${indexDir ?? ""}|${file}`;
4240
+ var readyReindexBatches = /* @__PURE__ */ new Map();
4241
+ function debounceKey(projectRoot, indexDir, file) {
4242
+ return JSON.stringify([projectRoot, indexDir ?? "", file]);
4243
+ }
4244
+ function reindexBatchKey(projectRoot, indexDir) {
4245
+ return JSON.stringify([projectRoot, indexDir ?? ""]);
4246
+ }
4247
+ function flushReadyReindexBatch(key) {
4248
+ const batch = readyReindexBatches.get(key);
4249
+ if (!batch) return;
4250
+ readyReindexBatches.delete(key);
4251
+ if (!indexCircuitBreaker.allowRequest()) {
4252
+ const error = circuitOpenError();
4253
+ for (const onError of batch.onErrors) onError(error);
4254
+ return;
4255
+ }
4256
+ void withMutex(
4257
+ () => callIndexOp(
4258
+ "index",
4259
+ {
4260
+ projectRoot: batch.projectRoot,
4261
+ files: [...batch.files].sort(),
4262
+ indexDir: batch.indexDir
4263
+ },
4264
+ { timeoutMs: batch.timeoutMs }
4265
+ )
4266
+ ).then(
4267
+ () => indexCircuitBreaker.recordSuccess(),
4268
+ (err) => {
4269
+ indexCircuitBreaker.recordFailure(err);
4270
+ for (const onError of batch.onErrors) onError(err);
4271
+ }
4272
+ );
4273
+ }
4274
+ function addReadyReindex(opts) {
4275
+ const key = reindexBatchKey(opts.projectRoot, opts.indexDir);
4276
+ const existing = readyReindexBatches.get(key);
4277
+ if (existing) {
4278
+ existing.files.add(opts.file);
4279
+ existing.timeoutMs = Math.max(existing.timeoutMs, opts.timeoutMs);
4280
+ if (opts.onError) existing.onErrors.add(opts.onError);
4281
+ return;
4282
+ }
4283
+ const batch = {
4284
+ projectRoot: opts.projectRoot,
4285
+ indexDir: opts.indexDir,
4286
+ files: /* @__PURE__ */ new Set([opts.file]),
4287
+ timeoutMs: opts.timeoutMs,
4288
+ onErrors: new Set(opts.onError ? [opts.onError] : [])
4289
+ };
4290
+ batch.flush = setImmediate(() => flushReadyReindexBatch(key));
4291
+ batch.flush.unref?.();
4292
+ readyReindexBatches.set(key, batch);
3594
4293
  }
3595
4294
  function isIndexableFile(filePath) {
3596
- return detectLang(filePath) !== null;
4295
+ return isIndexablePath(filePath);
3597
4296
  }
3598
4297
  function isRecoverableConstraintError(err) {
3599
4298
  if (err instanceof Error) {
@@ -3653,28 +4352,18 @@ function enqueueReindex(opts) {
3653
4352
  if (files.length === 0) return;
3654
4353
  const ms = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
3655
4354
  for (const file of files) {
3656
- const key = debounceKey(opts.indexDir, file);
4355
+ const key = debounceKey(opts.projectRoot, opts.indexDir, file);
3657
4356
  const existing = debounceTimers.get(key);
3658
4357
  if (existing) clearTimeout(existing);
3659
4358
  const timer = setTimeout(() => {
3660
4359
  debounceTimers.delete(key);
3661
- if (!indexCircuitBreaker.allowRequest()) {
3662
- opts.onError?.(circuitOpenError());
3663
- return;
3664
- }
3665
- void withMutex(
3666
- () => callIndexOp(
3667
- "index",
3668
- { projectRoot: opts.projectRoot, files: [file], indexDir: opts.indexDir },
3669
- { timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS }
3670
- )
3671
- ).then(
3672
- () => indexCircuitBreaker.recordSuccess(),
3673
- (err) => {
3674
- indexCircuitBreaker.recordFailure(err);
3675
- opts.onError?.(err);
3676
- }
3677
- );
4360
+ addReadyReindex({
4361
+ projectRoot: opts.projectRoot,
4362
+ file,
4363
+ indexDir: opts.indexDir,
4364
+ timeoutMs: opts.timeoutMs ?? DEFAULT_INCREMENTAL_TIMEOUT_MS,
4365
+ onError: opts.onError
4366
+ });
3678
4367
  }, ms);
3679
4368
  timer.unref?.();
3680
4369
  debounceTimers.set(key, timer);
@@ -3683,6 +4372,10 @@ function enqueueReindex(opts) {
3683
4372
  function cancelPendingReindexes() {
3684
4373
  for (const t of debounceTimers.values()) clearTimeout(t);
3685
4374
  debounceTimers.clear();
4375
+ for (const batch of readyReindexBatches.values()) {
4376
+ if (batch.flush) clearImmediate(batch.flush);
4377
+ }
4378
+ readyReindexBatches.clear();
3686
4379
  }
3687
4380
  async function searchCodebaseIndex(args, opts = {}) {
3688
4381
  return callIndexOp("search", args, {
@@ -3967,6 +4660,7 @@ var codebaseStatsTool = {
3967
4660
  };
3968
4661
  export {
3969
4662
  CircuitOpenError,
4663
+ INDEXABLE_EXTENSIONS,
3970
4664
  IndexCircuitBreaker,
3971
4665
  IndexStore,
3972
4666
  IndexTimeoutError,
@@ -3979,6 +4673,7 @@ export {
3979
4673
  codebaseIndexTool,
3980
4674
  codebaseSearchTool,
3981
4675
  codebaseStatsTool,
4676
+ detectLang,
3982
4677
  enqueueReindex,
3983
4678
  fileGraphService,
3984
4679
  getIndexState,
@@ -3986,6 +4681,7 @@ export {
3986
4681
  internalKindToLspKind,
3987
4682
  isIndexReady,
3988
4683
  isIndexableFile,
4684
+ isIndexablePath,
3989
4685
  isIndexing,
3990
4686
  lspKindToInternalKind,
3991
4687
  onIndexStateChange,