@wrongstack/tools 0.293.0 → 0.295.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) 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/bash.d.ts +1 -1
  13. package/dist/bash.d.ts.map +1 -1
  14. package/dist/bash.js +66 -52
  15. package/dist/bash.js.map +3 -3
  16. package/dist/batch-tool-use.d.ts +1 -1
  17. package/dist/batch-tool-use.d.ts.map +1 -1
  18. package/dist/batch-tool-use.js +1 -1
  19. package/dist/batch-tool-use.js.map +1 -1
  20. package/dist/browser/index.js +10 -1
  21. package/dist/browser/index.js.map +2 -2
  22. package/dist/browser/manager.d.ts +2 -0
  23. package/dist/browser/manager.d.ts.map +1 -1
  24. package/dist/browser/tools.d.ts +1 -1
  25. package/dist/browser/tools.d.ts.map +1 -1
  26. package/dist/builtin.d.ts +1 -1
  27. package/dist/builtin.d.ts.map +1 -1
  28. package/dist/builtin.js +1394 -553
  29. package/dist/builtin.js.map +4 -4
  30. package/dist/codebase-index/background-indexer.d.ts +2 -2
  31. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  32. package/dist/codebase-index/bm25.d.ts +3 -1
  33. package/dist/codebase-index/bm25.d.ts.map +1 -1
  34. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  35. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  36. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  37. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/generic-parser.d.ts +34 -0
  41. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  42. package/dist/codebase-index/go-parser.d.ts +1 -1
  43. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/index.d.ts +4 -2
  45. package/dist/codebase-index/index.d.ts.map +1 -1
  46. package/dist/codebase-index/index.js +1036 -341
  47. package/dist/codebase-index/index.js.map +4 -4
  48. package/dist/codebase-index/indexer.d.ts +6 -1
  49. package/dist/codebase-index/indexer.d.ts.map +1 -1
  50. package/dist/codebase-index/json-parser.d.ts +1 -1
  51. package/dist/codebase-index/languages.d.ts +25 -0
  52. package/dist/codebase-index/languages.d.ts.map +1 -0
  53. package/dist/codebase-index/py-parser.d.ts +9 -1
  54. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  55. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  56. package/dist/codebase-index/rs-parser.d.ts +1 -1
  57. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  58. package/dist/codebase-index/schema.d.ts +9 -2
  59. package/dist/codebase-index/schema.d.ts.map +1 -1
  60. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  61. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  62. package/dist/codebase-index/ts-parser.d.ts +2 -2
  63. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  64. package/dist/codebase-index/worker.js +961 -317
  65. package/dist/codebase-index/worker.js.map +4 -4
  66. package/dist/codebase-index/writer.d.ts +83 -4
  67. package/dist/codebase-index/writer.d.ts.map +1 -1
  68. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  69. package/dist/design.d.ts +5 -2
  70. package/dist/design.d.ts.map +1 -1
  71. package/dist/design.js +36 -3
  72. package/dist/design.js.map +2 -2
  73. package/dist/diff.d.ts +1 -1
  74. package/dist/diff.d.ts.map +1 -1
  75. package/dist/diff.js +2 -2
  76. package/dist/diff.js.map +1 -1
  77. package/dist/document.d.ts +1 -1
  78. package/dist/document.d.ts.map +1 -1
  79. package/dist/document.js +1 -1
  80. package/dist/document.js.map +1 -1
  81. package/dist/e2e.d.ts +1 -1
  82. package/dist/e2e.d.ts.map +1 -1
  83. package/dist/e2e.js +1 -1
  84. package/dist/e2e.js.map +1 -1
  85. package/dist/edit.d.ts +1 -1
  86. package/dist/edit.d.ts.map +1 -1
  87. package/dist/edit.js +3 -3
  88. package/dist/edit.js.map +2 -2
  89. package/dist/exec.d.ts +1 -1
  90. package/dist/exec.d.ts.map +1 -1
  91. package/dist/exec.js +60 -21
  92. package/dist/exec.js.map +3 -3
  93. package/dist/fetch.d.ts +1 -1
  94. package/dist/fetch.d.ts.map +1 -1
  95. package/dist/fetch.js +4 -5
  96. package/dist/fetch.js.map +2 -2
  97. package/dist/format.d.ts +1 -1
  98. package/dist/format.d.ts.map +1 -1
  99. package/dist/format.js +81 -16
  100. package/dist/format.js.map +2 -2
  101. package/dist/git.d.ts +1 -1
  102. package/dist/git.d.ts.map +1 -1
  103. package/dist/git.js +3 -2
  104. package/dist/git.js.map +2 -2
  105. package/dist/glob.d.ts +1 -1
  106. package/dist/glob.d.ts.map +1 -1
  107. package/dist/glob.js +2 -2
  108. package/dist/glob.js.map +1 -1
  109. package/dist/grep.d.ts +1 -1
  110. package/dist/grep.d.ts.map +1 -1
  111. package/dist/grep.js +4 -3
  112. package/dist/grep.js.map +2 -2
  113. package/dist/index.d.ts +4 -4
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1657 -705
  116. package/dist/index.js.map +4 -4
  117. package/dist/install.d.ts +1 -1
  118. package/dist/install.d.ts.map +1 -1
  119. package/dist/install.js +85 -17
  120. package/dist/install.js.map +2 -2
  121. package/dist/json.d.ts +1 -1
  122. package/dist/json.d.ts.map +1 -1
  123. package/dist/json.js +2 -2
  124. package/dist/json.js.map +2 -2
  125. package/dist/kanban.d.ts +4 -3
  126. package/dist/kanban.d.ts.map +1 -1
  127. package/dist/kanban.js +94 -27
  128. package/dist/kanban.js.map +2 -2
  129. package/dist/languages/execute-tool.d.ts +1 -1
  130. package/dist/languages/execute-tool.d.ts.map +1 -1
  131. package/dist/languages/execute.d.ts +1 -1
  132. package/dist/languages/execute.d.ts.map +1 -1
  133. package/dist/languages/index.d.ts +1 -1
  134. package/dist/languages/index.d.ts.map +1 -1
  135. package/dist/languages/index.js +99 -17
  136. package/dist/languages/index.js.map +2 -2
  137. package/dist/languages/package-tool.d.ts +3 -4
  138. package/dist/languages/package-tool.d.ts.map +1 -1
  139. package/dist/languages/plan.d.ts.map +1 -1
  140. package/dist/languages/profiles/additional.d.ts.map +1 -1
  141. package/dist/languages/profiles/primary.d.ts.map +1 -1
  142. package/dist/languages/tool.d.ts +1 -1
  143. package/dist/languages/tool.d.ts.map +1 -1
  144. package/dist/lint.d.ts +1 -1
  145. package/dist/lint.d.ts.map +1 -1
  146. package/dist/lint.js +81 -16
  147. package/dist/lint.js.map +2 -2
  148. package/dist/logs.d.ts +1 -1
  149. package/dist/logs.d.ts.map +1 -1
  150. package/dist/logs.js +2 -2
  151. package/dist/logs.js.map +1 -1
  152. package/dist/memory.d.ts +1 -1
  153. package/dist/memory.d.ts.map +1 -1
  154. package/dist/memory.js +1 -1
  155. package/dist/memory.js.map +1 -1
  156. package/dist/mode.d.ts +1 -1
  157. package/dist/mode.d.ts.map +1 -1
  158. package/dist/mode.js.map +1 -1
  159. package/dist/outdated.d.ts +1 -1
  160. package/dist/outdated.d.ts.map +1 -1
  161. package/dist/outdated.js +101 -19
  162. package/dist/outdated.js.map +2 -2
  163. package/dist/pack.d.ts +1 -1
  164. package/dist/pack.js +1394 -553
  165. package/dist/pack.js.map +4 -4
  166. package/dist/patch.d.ts +1 -1
  167. package/dist/patch.d.ts.map +1 -1
  168. package/dist/patch.js +2 -2
  169. package/dist/patch.js.map +1 -1
  170. package/dist/plan.d.ts +1 -1
  171. package/dist/plan.d.ts.map +1 -1
  172. package/dist/plan.js +7 -7
  173. package/dist/plan.js.map +2 -2
  174. package/dist/process-guardian.d.ts +7 -0
  175. package/dist/process-guardian.d.ts.map +1 -1
  176. package/dist/process-registry-persistent.d.ts +5 -0
  177. package/dist/process-registry-persistent.d.ts.map +1 -1
  178. package/dist/process-registry.d.ts +9 -1
  179. package/dist/process-registry.d.ts.map +1 -1
  180. package/dist/process-registry.js +18 -3
  181. package/dist/process-registry.js.map +2 -2
  182. package/dist/ps-slash.js +54 -15
  183. package/dist/ps-slash.js.map +2 -2
  184. package/dist/read.d.ts +1 -1
  185. package/dist/read.d.ts.map +1 -1
  186. package/dist/read.js +3 -2
  187. package/dist/read.js.map +2 -2
  188. package/dist/replace.d.ts +1 -1
  189. package/dist/replace.d.ts.map +1 -1
  190. package/dist/replace.js +4 -4
  191. package/dist/replace.js.map +2 -2
  192. package/dist/scaffold.d.ts +1 -1
  193. package/dist/scaffold.d.ts.map +1 -1
  194. package/dist/scaffold.js +2 -2
  195. package/dist/scaffold.js.map +1 -1
  196. package/dist/search.d.ts +1 -1
  197. package/dist/search.d.ts.map +1 -1
  198. package/dist/search.js +4 -4
  199. package/dist/search.js.map +2 -2
  200. package/dist/session-kanban.d.ts +4 -1
  201. package/dist/session-kanban.d.ts.map +1 -1
  202. package/dist/session-kanban.js +23 -15
  203. package/dist/session-kanban.js.map +2 -2
  204. package/dist/set-working-dir.d.ts +1 -1
  205. package/dist/set-working-dir.d.ts.map +1 -1
  206. package/dist/skill.d.ts +1 -1
  207. package/dist/skill.d.ts.map +1 -1
  208. package/dist/skill.js +2 -5
  209. package/dist/skill.js.map +2 -2
  210. package/dist/spawn-background.d.ts.map +1 -1
  211. package/dist/task.d.ts +3 -2
  212. package/dist/task.d.ts.map +1 -1
  213. package/dist/task.js +6 -8
  214. package/dist/task.js.map +2 -2
  215. package/dist/test.d.ts +1 -1
  216. package/dist/test.d.ts.map +1 -1
  217. package/dist/test.js +81 -16
  218. package/dist/test.js.map +2 -2
  219. package/dist/todo.d.ts +2 -1
  220. package/dist/todo.d.ts.map +1 -1
  221. package/dist/todo.js +4 -4
  222. package/dist/todo.js.map +2 -2
  223. package/dist/tool-help.d.ts +1 -1
  224. package/dist/tool-help.d.ts.map +1 -1
  225. package/dist/tool-help.js.map +1 -1
  226. package/dist/tool-search.d.ts +1 -1
  227. package/dist/tool-search.d.ts.map +1 -1
  228. package/dist/tool-search.js.map +1 -1
  229. package/dist/tool-tier.d.ts +16 -0
  230. package/dist/tool-tier.d.ts.map +1 -0
  231. package/dist/tool-tier.js +22233 -0
  232. package/dist/tool-tier.js.map +7 -0
  233. package/dist/tool-use.d.ts +1 -1
  234. package/dist/tool-use.d.ts.map +1 -1
  235. package/dist/tool-use.js.map +1 -1
  236. package/dist/tree.d.ts +1 -1
  237. package/dist/tree.d.ts.map +1 -1
  238. package/dist/tree.js +2 -2
  239. package/dist/tree.js.map +1 -1
  240. package/dist/typecheck.d.ts +1 -1
  241. package/dist/typecheck.d.ts.map +1 -1
  242. package/dist/typecheck.js +81 -16
  243. package/dist/typecheck.js.map +2 -2
  244. package/dist/write.d.ts +1 -1
  245. package/dist/write.d.ts.map +1 -1
  246. package/dist/write.js +3 -2
  247. package/dist/write.js.map +2 -2
  248. package/package.json +7 -3
@@ -2,14 +2,14 @@
2
2
  import { parentPort } from "node:worker_threads";
3
3
 
4
4
  // src/codebase-index/indexer.ts
5
- import { expectDefined as expectDefined5 } from "@wrongstack/core";
5
+ import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
6
6
  import * as fs7 from "node:fs/promises";
7
- import * as path8 from "node:path";
7
+ import * as path9 from "node:path";
8
8
  import { availableParallelism } from "node:os";
9
- import { compileGlob as compileGlob2 } from "@wrongstack/core";
9
+ import { indexParallelBatchSize, isFrugalPerf } from "@wrongstack/core/utils";
10
10
 
11
11
  // src/codebase-index/writer.ts
12
- import { expectDefined } from "@wrongstack/core";
12
+ import { expectDefined } from "@wrongstack/core/utils";
13
13
 
14
14
  // src/codebase-index/circuit-breaker.ts
15
15
  var LockError = class extends Error {
@@ -90,7 +90,7 @@ import { toErrorMessage } from "@wrongstack/core/utils";
90
90
  import { createRequire } from "node:module";
91
91
  import * as fs from "node:fs";
92
92
  import * as path from "node:path";
93
- import { resolveWstackPaths } from "@wrongstack/core";
93
+ import { resolveWstackPaths, sqliteCachePragmas } from "@wrongstack/core/utils";
94
94
 
95
95
  // src/codebase-index/schema.ts
96
96
  var SCHEMA_VERSION = 3;
@@ -146,23 +146,13 @@ function buildBm25Index(docs) {
146
146
  const tokens = tokenise(d.text);
147
147
  return { id: d.id, tokens, raw: d.text, len: tokens.length };
148
148
  });
149
- const df = {};
150
- for (const doc of documents) {
151
- const seen = /* @__PURE__ */ new Set();
152
- for (const t of doc.tokens) {
153
- if (!seen.has(t)) {
154
- df[t] = (df[t] ?? 0) + 1;
155
- seen.add(t);
156
- }
157
- }
158
- }
159
149
  const N = documents.length;
160
150
  const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
161
151
  const avgLen = N === 0 ? 0 : totalLen / N;
162
- return new Bm25Index(documents, df, N, avgLen);
152
+ return new Bm25Index(documents, N, avgLen);
163
153
  }
164
154
  var Bm25Index = class {
165
- constructor(documents, _df, N, avgLen) {
155
+ constructor(documents, N, avgLen) {
166
156
  this.documents = documents;
167
157
  this.N = N;
168
158
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
@@ -170,9 +160,19 @@ var Bm25Index = class {
170
160
  documents;
171
161
  N;
172
162
  safeAvgLen;
163
+ /** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
164
+ _byId;
173
165
  score(query, filter) {
174
166
  const qTokens = tokenise(query);
175
167
  if (qTokens.length === 0) return [];
168
+ const dfByTerm = /* @__PURE__ */ new Map();
169
+ for (const qTerm of qTokens) {
170
+ let dfVal = 0;
171
+ for (const candidate of this.documents) {
172
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
173
+ }
174
+ dfByTerm.set(qTerm, dfVal);
175
+ }
176
176
  const results = [];
177
177
  for (const doc of this.documents) {
178
178
  if (filter && !filter(doc.id)) continue;
@@ -183,10 +183,7 @@ var Bm25Index = class {
183
183
  if (t.startsWith(qTerm)) tf++;
184
184
  }
185
185
  if (tf === 0) continue;
186
- let dfVal = 0;
187
- for (const candidate of this.documents) {
188
- if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
189
- }
186
+ const dfVal = dfByTerm.get(qTerm) ?? 0;
190
187
  if (dfVal === 0) continue;
191
188
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
192
189
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -198,7 +195,10 @@ var Bm25Index = class {
198
195
  return results;
199
196
  }
200
197
  getDoc(id) {
201
- return this.documents.find((d) => d.id === id);
198
+ if (!this._byId) {
199
+ this._byId = new Map(this.documents.map((d) => [d.id, d]));
200
+ }
201
+ return this._byId.get(id);
202
202
  }
203
203
  extractSnippet(docId, queryTokens, radius = 40) {
204
204
  const doc = this.getDoc(docId);
@@ -356,6 +356,25 @@ var IndexStore = class _IndexStore {
356
356
  * and reuse it. Cleared in {@link close} when the connection is torn down.
357
357
  */
358
358
  stmtCache = /* @__PURE__ */ new Map();
359
+ /**
360
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
361
+ * Built lazily on the first `searchRankedFallback` call and invalidated
362
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
363
+ * IDF over the full corpus is also more correct than the old per-query
364
+ * candidate-subset IDF.
365
+ *
366
+ * Cache-lifecycle invariants (single source of truth lives at the
367
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
368
+ * MUST call this" contract):
369
+ * - declaration: this field + `bm25Dirty` (here)
370
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
371
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
372
+ * - teardown: `close()` resets the flag and nulls the cache
373
+ */
374
+ bm25Cache = null;
375
+ // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
376
+ // an empty or pre-existing corpus makes a stale IDF table meaningless.
377
+ bm25Dirty = true;
359
378
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
360
379
  stmt(sql) {
361
380
  let s = this.stmtCache.get(sql);
@@ -403,7 +422,15 @@ var IndexStore = class _IndexStore {
403
422
  this.db = new Database(path.join(this.indexDir, DB_FILE));
404
423
  try {
405
424
  this.db.exec("PRAGMA journal_mode = WAL");
406
- this.db.exec("PRAGMA busy_timeout = 5000");
425
+ this.db.exec("PRAGMA synchronous = NORMAL");
426
+ this.db.exec("PRAGMA busy_timeout = 15000");
427
+ this.db.exec("PRAGMA temp_store = MEMORY");
428
+ const cache = sqliteCachePragmas();
429
+ this.db.exec(`PRAGMA cache_size = -${cache.cacheSizeKiB}`);
430
+ this.db.exec(`PRAGMA mmap_size = ${cache.mmapBytes}`);
431
+ this.db.exec("PRAGMA foreign_keys = ON");
432
+ this.db.exec("PRAGMA journal_size_limit = 67108864");
433
+ this.db.exec("PRAGMA wal_autocheckpoint = 1000");
407
434
  } catch {
408
435
  }
409
436
  this.initSchema();
@@ -415,7 +442,7 @@ var IndexStore = class _IndexStore {
415
442
  value TEXT NOT NULL
416
443
  );
417
444
  `);
418
- const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
445
+ const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
419
446
  const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
420
447
  if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
421
448
  this.db.exec(`
@@ -424,9 +451,9 @@ var IndexStore = class _IndexStore {
424
451
  DROP TABLE IF EXISTS refs;
425
452
  `);
426
453
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
427
- this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
454
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
428
455
  } else if (storedVersion === null) {
429
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
456
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
430
457
  }
431
458
  this.db.exec(`
432
459
  CREATE TABLE IF NOT EXISTS files (
@@ -457,6 +484,7 @@ var IndexStore = class _IndexStore {
457
484
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
458
485
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
459
486
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
487
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)");
460
488
  this.db.exec(`
461
489
  CREATE TABLE IF NOT EXISTS refs (
462
490
  id INTEGER PRIMARY KEY,
@@ -477,65 +505,167 @@ var IndexStore = class _IndexStore {
477
505
  );
478
506
  this.ftsAvailable = true;
479
507
  const symbolCount = Number(
480
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
508
+ this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
481
509
  );
482
510
  const ftsCount = Number(
483
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
511
+ this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
484
512
  );
485
513
  if (symbolCount !== ftsCount) {
486
514
  this.db.exec("DELETE FROM symbols_fts");
487
- const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
488
- const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
489
- for (const row of rows) {
490
- insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
491
- }
515
+ const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
516
+ this.bulkInsertFts(
517
+ rows.map((row) => ({
518
+ id: row.id,
519
+ text: buildIndexableText(row.name, row.signature, row.doc_comment)
520
+ }))
521
+ );
522
+ this.invalidateBm25();
492
523
  }
493
524
  } catch {
494
525
  this.ftsAvailable = false;
495
526
  }
527
+ this.ensureNextSymbolIdSeeded();
528
+ }
529
+ // ─── ID allocation & bulk helpers ────────────────────────────────────────────
530
+ static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
531
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
532
+ static MAX_SQL_VARS = 900;
533
+ /**
534
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
535
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
536
+ * re-reads and advances the counter atomically.
537
+ */
538
+ ensureNextSymbolIdSeeded() {
539
+ const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
540
+ _IndexStore.NEXT_SYMBOL_ID_KEY
541
+ );
542
+ if (existing?.value !== void 0) return;
543
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
544
+ const next = (maxRows[0]?.m ?? 0) + 1;
545
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
546
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
547
+ String(next)
548
+ );
549
+ }
550
+ /**
551
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
552
+ * so concurrent indexers cannot hand out overlapping ranges.
553
+ */
554
+ allocateSymbolIds(count) {
555
+ if (count <= 0) return this.getMaxSymbolId() + 1;
556
+ this.ensureNextSymbolIdSeeded();
557
+ const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
558
+ _IndexStore.NEXT_SYMBOL_ID_KEY
559
+ );
560
+ const start = Math.max(1, Number(row?.value ?? 1) || 1);
561
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
562
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
563
+ String(start + count)
564
+ );
565
+ return start;
566
+ }
567
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
568
+ bulkInsertSymbols(rows) {
569
+ if (rows.length === 0) return;
570
+ const cols = 12;
571
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
572
+ for (let i = 0; i < rows.length; i += chunkSize) {
573
+ const chunk = rows.slice(i, i + chunkSize);
574
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
575
+ const stmt = this.stmt(
576
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
577
+ VALUES ${placeholders}`
578
+ );
579
+ const binds = [];
580
+ for (const r of chunk) {
581
+ binds.push(
582
+ r.id,
583
+ r.lang,
584
+ r.kind,
585
+ r.name,
586
+ r.file,
587
+ r.line,
588
+ r.col,
589
+ r.signature,
590
+ r.docComment,
591
+ r.scope,
592
+ r.text,
593
+ r.file
594
+ );
595
+ }
596
+ stmt.run(...binds);
597
+ }
598
+ }
599
+ bulkInsertFts(rows) {
600
+ if (!this.ftsAvailable || rows.length === 0) return;
601
+ const cols = 2;
602
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
603
+ for (let i = 0; i < rows.length; i += chunkSize) {
604
+ const chunk = rows.slice(i, i + chunkSize);
605
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
606
+ const stmt = this.stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
607
+ const binds = [];
608
+ for (const r of chunk) binds.push(r.id, r.text);
609
+ stmt.run(...binds);
610
+ }
611
+ }
612
+ bulkInsertRefs(refs) {
613
+ if (refs.length === 0) return;
614
+ const cols = 5;
615
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
616
+ for (let i = 0; i < refs.length; i += chunkSize) {
617
+ const chunk = refs.slice(i, i + chunkSize);
618
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
619
+ const stmt = this.stmt(
620
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
621
+ );
622
+ const binds = [];
623
+ for (const ref of chunk) {
624
+ binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
625
+ }
626
+ stmt.run(...binds);
627
+ }
496
628
  }
497
629
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
498
630
  /**
499
631
  * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
500
- * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
501
- * the same transaction, preventing UNIQUE constraint violations when two
502
- * processes index concurrently (each would see a different `MAX(id)` and
503
- * neither can insert with the other's IDs).
632
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
633
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
504
634
  *
505
635
  * @returns The symbols array with `id` fields populated so the caller can
506
636
  * use them for refs without re-reading from the DB.
507
637
  */
508
638
  insertSymbols(symbols) {
639
+ this.invalidateBm25();
509
640
  return this.runWithRetry(() => {
510
641
  this.db.exec("BEGIN IMMEDIATE");
511
642
  try {
512
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
513
- let nextId = (maxRows[0]?.m ?? 0) + 1;
514
- const stmt = this.db.prepare(
515
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
516
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
517
- );
518
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
643
+ let nextId = this.allocateSymbolIds(symbols.length);
519
644
  const result = [];
645
+ const bulk = [];
646
+ const ftsRows = [];
520
647
  for (const s of symbols) {
521
648
  const id = nextId++;
522
- stmt.run(
649
+ bulk.push({
523
650
  id,
524
- s.lang,
525
- s.kind,
526
- s.name,
527
- s.file,
528
- s.line,
529
- s.col,
530
- s.signature,
531
- s.docComment,
532
- s.scope,
533
- s.text,
534
- s.file
535
- );
536
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
651
+ lang: s.lang,
652
+ kind: s.kind,
653
+ name: s.name,
654
+ file: s.file,
655
+ line: s.line,
656
+ col: s.col,
657
+ signature: s.signature,
658
+ docComment: s.docComment,
659
+ scope: s.scope,
660
+ text: s.text
661
+ });
662
+ if (this.ftsAvailable) {
663
+ ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
664
+ }
537
665
  result.push({ ...s, id });
538
666
  }
667
+ this.bulkInsertSymbols(bulk);
668
+ this.bulkInsertFts(ftsRows);
539
669
  this.db.exec("COMMIT");
540
670
  return result;
541
671
  } catch (err) {
@@ -545,6 +675,7 @@ var IndexStore = class _IndexStore {
545
675
  });
546
676
  }
547
677
  deleteSymbolsForFile(file) {
678
+ this.invalidateBm25();
548
679
  this.runWithRetry(() => {
549
680
  if (this.ftsAvailable) {
550
681
  this.stmt(
@@ -560,17 +691,20 @@ var IndexStore = class _IndexStore {
560
691
  * dropped the `files` row, leaving its symbols orphaned but still searchable.
561
692
  */
562
693
  deleteFile(file) {
694
+ this.invalidateBm25();
563
695
  this.runWithRetry(() => {
564
696
  this.db.exec("BEGIN IMMEDIATE");
565
697
  try {
566
698
  if (this.ftsAvailable) {
567
- this.db.prepare(
699
+ this.stmt(
568
700
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
569
701
  ).run(file);
570
702
  }
571
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
572
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
573
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
703
+ this.stmt(
704
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
705
+ ).run(file);
706
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
707
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
574
708
  this.db.exec("COMMIT");
575
709
  } catch (err) {
576
710
  this.db.exec("ROLLBACK");
@@ -607,7 +741,7 @@ var IndexStore = class _IndexStore {
607
741
  };
608
742
  }
609
743
  getAllFileMetas() {
610
- return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
744
+ return this.stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
611
745
  file: r.file,
612
746
  lang: r.lang,
613
747
  mtimeMs: r.mtime_ms,
@@ -616,7 +750,32 @@ var IndexStore = class _IndexStore {
616
750
  }));
617
751
  }
618
752
  // ─── Search ──────────────────────────────────────────────────────────────────
619
- search(query, filter) {
753
+ search(query, filter, opts) {
754
+ const built = this.buildSearchWhere(query, filter);
755
+ if (built === null) return [];
756
+ const { where, values } = built;
757
+ const limit = typeof opts?.limit === "number" && Number.isFinite(opts.limit) ? Math.max(0, Math.trunc(opts.limit)) : void 0;
758
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
759
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
760
+ const binds = limit !== void 0 ? [...values, limit] : values;
761
+ const rows = this.stmt(sql).all(...binds);
762
+ return rows.map((r) => ({
763
+ id: r.id,
764
+ lang: r.lang,
765
+ kind: r.kind,
766
+ name: r.name,
767
+ file: r.file,
768
+ line: r.line,
769
+ col: r.col,
770
+ signature: r.signature,
771
+ docComment: r.doc_comment,
772
+ score: 0,
773
+ snippet: "",
774
+ lspKind: filter?.lspKind
775
+ }));
776
+ }
777
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
778
+ buildSearchWhere(query, filter) {
620
779
  const conditions = [];
621
780
  const values = [];
622
781
  let effectiveKind = filter?.kind;
@@ -625,7 +784,7 @@ var IndexStore = class _IndexStore {
625
784
  if (mapped !== null) {
626
785
  effectiveKind = mapped;
627
786
  } else {
628
- return [];
787
+ return null;
629
788
  }
630
789
  }
631
790
  if (effectiveKind) {
@@ -647,23 +806,15 @@ var IndexStore = class _IndexStore {
647
806
  for (const t of tokens) values.push(`%${t}%`);
648
807
  }
649
808
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
650
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}`;
651
- const stmt = this.db.prepare(sql);
652
- const rows = stmt.all(...values);
653
- return rows.map((r) => ({
654
- id: r.id,
655
- lang: r.lang,
656
- kind: r.kind,
657
- name: r.name,
658
- file: r.file,
659
- line: r.line,
660
- col: r.col,
661
- signature: r.signature,
662
- docComment: r.doc_comment,
663
- score: 0,
664
- snippet: "",
665
- lspKind: filter?.lspKind
666
- }));
809
+ return { where, values };
810
+ }
811
+ countSearch(query, filter) {
812
+ const built = this.buildSearchWhere(query, filter);
813
+ if (built === null) return 0;
814
+ const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
815
+ ...built.values
816
+ );
817
+ return Number(row?.n ?? 0);
667
818
  }
668
819
  /**
669
820
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
@@ -704,12 +855,12 @@ var IndexStore = class _IndexStore {
704
855
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
705
856
  }
706
857
  const where = conditions.join(" AND ");
707
- const countRows = this.db.prepare(
858
+ const countRows = this.stmt(
708
859
  `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
709
860
  ).all(...values);
710
861
  const total = countRows[0] ? Number(countRows[0].n) : 0;
711
862
  if (total === 0) return { results: [], total: 0 };
712
- const rows = this.db.prepare(
863
+ const rows = this.stmt(
713
864
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
714
865
  -bm25(symbols_fts) AS score,
715
866
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -742,20 +893,60 @@ var IndexStore = class _IndexStore {
742
893
  total
743
894
  };
744
895
  }
896
+ /**
897
+ * Invalidate the cached BM25 index.
898
+ *
899
+ * **Contract: every method that mutates `symbols` MUST call this before
900
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
901
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
902
+ * BM25 score is filtered by the LIKE-selected candidate set in
903
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
904
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
905
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
906
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
907
+ * FTS5-unavailable fallback will serve stale search results. The
908
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
909
+ * which is the only legitimate place that flips the flag outside this
910
+ * helper.
911
+ *
912
+ * Called *before* `runWithRetry` on purpose: if the write fails all
913
+ * retries the flag stays set, forcing a rebuild on the next search rather
914
+ * than trusting a cache that may not reflect the intended mutation.
915
+ * Do not move this inside the retry closure.
916
+ */
917
+ invalidateBm25() {
918
+ this.bm25Dirty = true;
919
+ this.bm25Cache = null;
920
+ }
921
+ /**
922
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
923
+ * symbols table has been mutated since the last build. The full-corpus IDF
924
+ * is more correct than the old per-query candidate-subset IDF, and the
925
+ * amortized build cost drops from O(symbols × tokens) per search to once
926
+ * per write batch.
927
+ *
928
+ * Note: the first call after a long idle (or on a freshly opened store)
929
+ * pays the full corpus rebuild synchronously on the search path. For a
930
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
931
+ */
932
+ getOrBuildBm25() {
933
+ if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
934
+ const docs = this.getAllIndexable();
935
+ this.bm25Cache = buildBm25Index(docs);
936
+ this.bm25Dirty = false;
937
+ return this.bm25Cache;
938
+ }
745
939
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
746
940
  searchRankedFallback(query, filter, limit) {
747
- const candidates = this.search(query, filter);
748
- if (candidates.length === 0) return { results: [], total: 0 };
749
941
  if (!query.trim()) {
750
- return { results: candidates.slice(0, limit), total: candidates.length };
942
+ const total = this.countSearch(query, filter);
943
+ if (total === 0) return { results: [], total: 0 };
944
+ return { results: this.search(query, filter, { limit }), total };
751
945
  }
946
+ const candidates = this.search(query, filter);
947
+ if (candidates.length === 0) return { results: [], total: 0 };
752
948
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
753
- const bm25 = buildBm25Index(
754
- candidates.map((c) => ({
755
- id: c.id,
756
- text: buildIndexableText(c.name, c.signature, c.docComment)
757
- }))
758
- );
949
+ const bm25 = this.getOrBuildBm25();
759
950
  const scored = bm25.score(query, (id) => candidateById.has(id));
760
951
  const q = query.trim().toLowerCase();
761
952
  const rank = (id) => {
@@ -797,7 +988,7 @@ var IndexStore = class _IndexStore {
797
988
  // ─── Stats ───────────────────────────────────────────────────────────────────
798
989
  getStats() {
799
990
  const sizeBytes = this.sizeBytes();
800
- const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
991
+ const lastRows = this.stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
801
992
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
802
993
  const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
803
994
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
@@ -822,19 +1013,20 @@ var IndexStore = class _IndexStore {
822
1013
  }
823
1014
  setLastIndexed(ts2) {
824
1015
  this.runWithRetry(() => {
825
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
1016
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
826
1017
  });
827
1018
  }
828
1019
  getMetadata(key) {
829
- const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
1020
+ const rows = this.stmt("SELECT value FROM metadata WHERE key = ?").all(key);
830
1021
  return rows[0]?.value;
831
1022
  }
832
1023
  setMetadata(key, value) {
833
1024
  this.runWithRetry(() => {
834
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
1025
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
835
1026
  });
836
1027
  }
837
1028
  clearAll() {
1029
+ this.invalidateBm25();
838
1030
  this.runWithRetry(() => {
839
1031
  this.db.exec("BEGIN IMMEDIATE");
840
1032
  try {
@@ -861,13 +1053,7 @@ var IndexStore = class _IndexStore {
861
1053
  this.runWithRetry(() => {
862
1054
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
863
1055
  if (refs.length === 0) return;
864
- const stmt = this.db.prepare(
865
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
866
- VALUES (?, ?, ?, ?, ?)`
867
- );
868
- for (const ref of refs) {
869
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
870
- }
1056
+ this.bulkInsertRefs(refs.map((ref) => ({ ...ref, fromId })));
871
1057
  });
872
1058
  }
873
1059
  /**
@@ -884,13 +1070,7 @@ var IndexStore = class _IndexStore {
884
1070
  insertRefsBatch(refs) {
885
1071
  if (refs.length === 0) return;
886
1072
  this.runWithRetry(() => {
887
- const stmt = this.db.prepare(
888
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
889
- VALUES (?, ?, ?, ?, ?)`
890
- );
891
- for (const ref of refs) {
892
- stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
893
- }
1073
+ this.bulkInsertRefs(refs);
894
1074
  });
895
1075
  }
896
1076
  /**
@@ -915,65 +1095,61 @@ var IndexStore = class _IndexStore {
915
1095
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
916
1096
  return [];
917
1097
  }
1098
+ this.invalidateBm25();
918
1099
  return this.runWithRetry(() => {
919
1100
  this.db.exec("BEGIN IMMEDIATE");
920
1101
  try {
921
1102
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
922
1103
  const placeholders = options.deleteForFiles.map(() => "?").join(",");
923
1104
  if (this.ftsAvailable) {
924
- this.db.prepare(
1105
+ this.stmt(
925
1106
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
926
1107
  ).run(...options.deleteForFiles);
927
1108
  }
928
- this.db.prepare(
1109
+ this.stmt(
929
1110
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
930
1111
  ).run(...options.deleteForFiles);
931
- this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
1112
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
932
1113
  }
933
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
934
- let nextId = (maxRows[0]?.m ?? 0) + 1;
935
- const symStmt = this.db.prepare(
936
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
937
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
938
- );
939
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
1114
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
1115
+ let nextId = this.allocateSymbolIds(totalSymbols);
940
1116
  const allInserted = [];
941
1117
  const refsToInsert = [];
1118
+ const bulkSyms = [];
1119
+ const ftsRows = [];
942
1120
  for (const entry of entries) {
943
1121
  const insertedForEntry = [];
944
1122
  for (const s of entry.symbols) {
945
1123
  const id = nextId++;
946
- symStmt.run(
1124
+ bulkSyms.push({
947
1125
  id,
948
- s.lang,
949
- s.kind,
950
- s.name,
951
- s.file,
952
- s.line,
953
- s.col,
954
- s.signature,
955
- s.docComment,
956
- s.scope,
957
- s.text,
958
- s.file
959
- );
960
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
1126
+ lang: s.lang,
1127
+ kind: s.kind,
1128
+ name: s.name,
1129
+ file: s.file,
1130
+ line: s.line,
1131
+ col: s.col,
1132
+ signature: s.signature,
1133
+ docComment: s.docComment,
1134
+ scope: s.scope,
1135
+ text: s.text
1136
+ });
1137
+ if (this.ftsAvailable) {
1138
+ ftsRows.push({
1139
+ id,
1140
+ text: buildIndexableText(s.name, s.signature, s.docComment)
1141
+ });
1142
+ }
961
1143
  const inserted = { ...s, id };
962
1144
  allInserted.push(inserted);
963
1145
  insertedForEntry.push(inserted);
964
1146
  }
965
1147
  refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
966
1148
  }
967
- if (refsToInsert.length > 0) {
968
- const refStmt = this.db.prepare(
969
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
970
- VALUES (?, ?, ?, ?, ?)`
971
- );
972
- for (const ref of refsToInsert) {
973
- refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
974
- }
975
- }
976
- const upsertStmt = this.db.prepare(
1149
+ this.bulkInsertSymbols(bulkSyms);
1150
+ this.bulkInsertFts(ftsRows);
1151
+ this.bulkInsertRefs(refsToInsert);
1152
+ const upsertStmt = this.stmt(
977
1153
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
978
1154
  VALUES (?, ?, ?, ?, ?)
979
1155
  ON CONFLICT(file) DO UPDATE SET
@@ -1000,7 +1176,7 @@ var IndexStore = class _IndexStore {
1000
1176
  */
1001
1177
  deleteRefsForFile(file) {
1002
1178
  this.runWithRetry(() => {
1003
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
1179
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
1004
1180
  });
1005
1181
  }
1006
1182
  /**
@@ -1013,20 +1189,75 @@ var IndexStore = class _IndexStore {
1013
1189
  */
1014
1190
  resolveRefs() {
1015
1191
  return this.runWithRetry(() => {
1016
- const result = this.db.prepare(
1017
- `UPDATE refs SET to_id = (
1018
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
1019
- ) WHERE to_id IS NULL AND to_name IS NOT NULL
1020
- AND to_name IN (SELECT name FROM symbols)`
1021
- ).run();
1022
- return result.changes ?? 0;
1192
+ try {
1193
+ const result = this.stmt(
1194
+ `UPDATE refs
1195
+ SET to_id = s.id
1196
+ FROM (
1197
+ SELECT name, MIN(id) AS id FROM symbols GROUP BY name
1198
+ ) AS s
1199
+ WHERE refs.to_id IS NULL
1200
+ AND refs.to_name IS NOT NULL
1201
+ AND refs.to_name = s.name`
1202
+ ).run();
1203
+ return result.changes ?? 0;
1204
+ } catch {
1205
+ const result = this.stmt(
1206
+ `UPDATE refs SET to_id = (
1207
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
1208
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
1209
+ AND to_name IN (SELECT name FROM symbols)`
1210
+ ).run();
1211
+ return result.changes ?? 0;
1212
+ }
1023
1213
  });
1024
1214
  }
1215
+ /**
1216
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
1217
+ * Used by the indexer for empty-parse results so three writes share one txn.
1218
+ */
1219
+ replaceEmptyFile(meta) {
1220
+ this.invalidateBm25();
1221
+ this.runWithRetry(() => {
1222
+ this.db.exec("BEGIN IMMEDIATE");
1223
+ try {
1224
+ if (this.ftsAvailable) {
1225
+ this.stmt(
1226
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
1227
+ ).run(meta.file);
1228
+ }
1229
+ this.stmt(
1230
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
1231
+ ).run(meta.file);
1232
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
1233
+ this.stmt(
1234
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
1235
+ VALUES (?, ?, ?, ?, ?)
1236
+ ON CONFLICT(file) DO UPDATE SET
1237
+ lang = excluded.lang,
1238
+ mtime_ms = excluded.mtime_ms,
1239
+ symbol_count = excluded.symbol_count,
1240
+ last_indexed = excluded.last_indexed`
1241
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
1242
+ this.db.exec("COMMIT");
1243
+ } catch (err) {
1244
+ this.db.exec("ROLLBACK");
1245
+ throw err;
1246
+ }
1247
+ });
1248
+ }
1249
+ /** Best-effort query planner refresh after a large reindex. */
1250
+ optimize() {
1251
+ try {
1252
+ this.db.exec("PRAGMA optimize");
1253
+ } catch {
1254
+ }
1255
+ }
1025
1256
  /**
1026
1257
  * Find all references TO a given symbol (who calls / uses this symbol?).
1027
1258
  */
1028
1259
  findRefsTo(symbolId) {
1029
- return this.db.prepare(
1260
+ return this.stmt(
1030
1261
  "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 = ?)"
1031
1262
  ).all(symbolId, symbolId).map((r) => ({
1032
1263
  id: r.id,
@@ -1041,7 +1272,7 @@ var IndexStore = class _IndexStore {
1041
1272
  * Find all references FROM a given symbol (what does this symbol call/use?).
1042
1273
  */
1043
1274
  findRefsFrom(symbolId) {
1044
- return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
1275
+ return this.stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
1045
1276
  id: r.id,
1046
1277
  fromId: r.from_id,
1047
1278
  toName: r.to_name,
@@ -1114,45 +1345,57 @@ var IndexStore = class _IndexStore {
1114
1345
  * symbol resolved in package B). Node metadata includes symbol/file counts.
1115
1346
  */
1116
1347
  getPackageGraph() {
1117
- const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
1348
+ const fileCounts = this.stmt(
1349
+ "SELECT file, COUNT(*) AS n FROM symbols GROUP BY file"
1350
+ ).all();
1118
1351
  const pkgNodes = /* @__PURE__ */ new Map();
1119
1352
  const fileToPkg = /* @__PURE__ */ new Map();
1120
- const symbolToPkg = /* @__PURE__ */ new Map();
1121
- for (const s of symbols) {
1122
- const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
1123
- fileToPkg.set(s.file, pkg);
1124
- symbolToPkg.set(s.id, pkg);
1353
+ for (const { file, n } of fileCounts) {
1354
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1355
+ fileToPkg.set(file, pkg);
1125
1356
  const node = pkgNodes.get(pkg);
1126
1357
  if (node) {
1127
- node.symbolCount = (node.symbolCount ?? 0) + 1;
1358
+ node.symbolCount = (node.symbolCount ?? 0) + n;
1128
1359
  } else {
1129
1360
  pkgNodes.set(pkg, {
1130
1361
  id: `pkg:${pkg}`,
1131
1362
  label: pkg,
1132
1363
  kind: "package",
1133
1364
  package: pkg,
1134
- symbolCount: 1,
1365
+ symbolCount: n,
1135
1366
  fileCount: 0
1136
1367
  });
1137
1368
  }
1138
1369
  }
1139
- const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1370
+ const files = this.stmt("SELECT DISTINCT file FROM files").all();
1140
1371
  for (const { file } of files) {
1141
1372
  const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1373
+ fileToPkg.set(file, pkg);
1142
1374
  const node = pkgNodes.get(pkg);
1143
1375
  if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1376
+ else {
1377
+ pkgNodes.set(pkg, {
1378
+ id: `pkg:${pkg}`,
1379
+ label: pkg,
1380
+ kind: "package",
1381
+ package: pkg,
1382
+ symbolCount: 0,
1383
+ fileCount: 1
1384
+ });
1385
+ }
1144
1386
  }
1145
- const refRows = this.db.prepare(
1146
- `SELECT r.from_id, r.to_id, r.call_type
1387
+ const refRows = this.stmt(
1388
+ `SELECT r.call_type, sf.file AS from_file, st.file AS to_file
1147
1389
  FROM refs r
1148
- WHERE r.to_id IS NOT NULL`
1390
+ JOIN symbols sf ON sf.id = r.from_id
1391
+ JOIN symbols st ON st.id = r.to_id
1392
+ WHERE r.to_id IS NOT NULL AND r.call_type != 'import'`
1149
1393
  ).all();
1150
1394
  const edgeMap = /* @__PURE__ */ new Map();
1151
1395
  for (const r of refRows) {
1152
- if (r.call_type === "import") continue;
1153
- const fromPkg = symbolToPkg.get(r.from_id);
1154
- const toPkg = symbolToPkg.get(r.to_id);
1155
- if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
1396
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
1397
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
1398
+ if (fromPkg === toPkg) continue;
1156
1399
  const key = `${fromPkg}\0${toPkg}`;
1157
1400
  let e = edgeMap.get(key);
1158
1401
  if (!e) {
@@ -1162,13 +1405,14 @@ var IndexStore = class _IndexStore {
1162
1405
  e.weight++;
1163
1406
  e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1164
1407
  }
1165
- const importRows = this.db.prepare(
1166
- `SELECT r.from_id, r.to_name
1408
+ const importRows = this.stmt(
1409
+ `SELECT r.to_name, s.file AS from_file
1167
1410
  FROM refs r
1411
+ JOIN symbols s ON s.id = r.from_id
1168
1412
  WHERE r.call_type = 'import'`
1169
1413
  ).all();
1170
1414
  for (const r of importRows) {
1171
- const fromPkg = symbolToPkg.get(r.from_id);
1415
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
1172
1416
  const toPkg = _IndexStore.packageFromImport(r.to_name);
1173
1417
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1174
1418
  const key = `${fromPkg}\0${toPkg}`;
@@ -1205,12 +1449,12 @@ var IndexStore = class _IndexStore {
1205
1449
  * derived from cross-file symbol references within the package.
1206
1450
  */
1207
1451
  getFileGraph(packageFilter) {
1208
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
1452
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
1209
1453
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
1210
1454
  const localFiles = new Set(pkgFilePaths);
1211
1455
  if (localFiles.size === 0) return { nodes: [], edges: [] };
1212
1456
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
1213
- const pkgSyms = this.db.prepare(
1457
+ const pkgSyms = this.stmt(
1214
1458
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
1215
1459
  ).all(...pkgFilePaths);
1216
1460
  const fileNodes = /* @__PURE__ */ new Map();
@@ -1242,7 +1486,7 @@ var IndexStore = class _IndexStore {
1242
1486
  ensureFileNode(file);
1243
1487
  }
1244
1488
  const indexedFiles = new Set(allFiles.map((f) => f.file));
1245
- const refRows = this.db.prepare(
1489
+ const refRows = this.stmt(
1246
1490
  `SELECT r.from_id, r.to_id, r.call_type
1247
1491
  FROM refs r
1248
1492
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
@@ -1257,7 +1501,7 @@ var IndexStore = class _IndexStore {
1257
1501
  }
1258
1502
  if (crossRefIds.size > 0) {
1259
1503
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
1260
- const extras = this.db.prepare(
1504
+ const extras = this.stmt(
1261
1505
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
1262
1506
  ).all(...crossRefIds);
1263
1507
  for (const x of extras) {
@@ -1285,7 +1529,7 @@ var IndexStore = class _IndexStore {
1285
1529
  e.weight++;
1286
1530
  e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1287
1531
  }
1288
- const importRows = this.db.prepare(
1532
+ const importRows = this.stmt(
1289
1533
  `SELECT r.from_id, r.to_name
1290
1534
  FROM refs r
1291
1535
  WHERE r.call_type = 'import'
@@ -1332,7 +1576,7 @@ var IndexStore = class _IndexStore {
1332
1576
  * derived from intra-file and cross-file symbol references (who calls whom).
1333
1577
  */
1334
1578
  getSymbolGraph(fileFilter) {
1335
- const syms = this.db.prepare(
1579
+ const syms = this.stmt(
1336
1580
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
1337
1581
  ).all(fileFilter);
1338
1582
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -1353,11 +1597,16 @@ var IndexStore = class _IndexStore {
1353
1597
  external: s.file !== fileFilter
1354
1598
  });
1355
1599
  const symIds = new Set(syms.map((s) => s.id));
1356
- const refRows = this.db.prepare(
1600
+ const refRows = this.stmt(
1357
1601
  `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1358
1602
  FROM refs r
1359
- WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
1360
- OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
1603
+ JOIN symbols s ON s.id = r.from_id
1604
+ WHERE s.file = ?
1605
+ UNION
1606
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1607
+ FROM refs r
1608
+ JOIN symbols s ON s.id = r.to_id
1609
+ WHERE s.file = ?`
1361
1610
  ).all(fileFilter, fileFilter);
1362
1611
  const edgeMap = /* @__PURE__ */ new Map();
1363
1612
  for (const r of refRows) {
@@ -1396,7 +1645,7 @@ var IndexStore = class _IndexStore {
1396
1645
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
1397
1646
  if (missingIds.length > 0) {
1398
1647
  const placeholders = missingIds.map(() => "?").join(",");
1399
- const extras = this.db.prepare(
1648
+ const extras = this.stmt(
1400
1649
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
1401
1650
  ).all(...missingIds);
1402
1651
  for (const s of extras) symById.set(s.id, s);
@@ -1410,6 +1659,8 @@ var IndexStore = class _IndexStore {
1410
1659
  }
1411
1660
  close() {
1412
1661
  this.stmtCache.clear();
1662
+ this.bm25Dirty = true;
1663
+ this.bm25Cache = null;
1413
1664
  try {
1414
1665
  this.db.close();
1415
1666
  } catch {
@@ -1419,6 +1670,110 @@ var IndexStore = class _IndexStore {
1419
1670
 
1420
1671
  // src/codebase-index/ts-parser.ts
1421
1672
  import * as ts from "@typescript/typescript6";
1673
+
1674
+ // src/codebase-index/languages.ts
1675
+ import * as path2 from "node:path";
1676
+ var EXT_TO_LANG = {
1677
+ // TypeScript / JavaScript (first-class TS compiler API)
1678
+ ".ts": "ts",
1679
+ ".mts": "ts",
1680
+ ".cts": "ts",
1681
+ ".tsx": "tsx",
1682
+ ".js": "js",
1683
+ ".mjs": "js",
1684
+ ".cjs": "js",
1685
+ ".jsx": "jsx",
1686
+ // First-class native/spawn parsers (+ regex fallback)
1687
+ ".go": "go",
1688
+ ".py": "py",
1689
+ ".pyi": "py",
1690
+ ".pyw": "py",
1691
+ ".rs": "rs",
1692
+ ".json": "json",
1693
+ ".jsonc": "json",
1694
+ ".yaml": "yaml",
1695
+ ".yml": "yaml",
1696
+ // C family
1697
+ ".c": "c",
1698
+ ".h": "c",
1699
+ ".cc": "cpp",
1700
+ ".cpp": "cpp",
1701
+ ".cxx": "cpp",
1702
+ ".hh": "cpp",
1703
+ ".hpp": "cpp",
1704
+ ".hxx": "cpp",
1705
+ // JVM / .NET
1706
+ ".java": "java",
1707
+ ".cs": "csharp",
1708
+ ".kt": "kotlin",
1709
+ ".kts": "kotlin",
1710
+ ".scala": "scala",
1711
+ ".sc": "scala",
1712
+ // Scripting
1713
+ ".php": "php",
1714
+ ".rb": "ruby",
1715
+ ".swift": "swift",
1716
+ ".dart": "dart",
1717
+ ".lua": "lua",
1718
+ ".r": "r",
1719
+ ".R": "r",
1720
+ ".pl": "other",
1721
+ ".pm": "other",
1722
+ // Systems / functional
1723
+ ".zig": "zig",
1724
+ ".ex": "elixir",
1725
+ ".exs": "elixir",
1726
+ ".hs": "haskell",
1727
+ ".lhs": "haskell",
1728
+ // Shell / data / docs / web
1729
+ ".sh": "shell",
1730
+ ".bash": "shell",
1731
+ ".zsh": "shell",
1732
+ ".ps1": "shell",
1733
+ ".sql": "sql",
1734
+ ".md": "md",
1735
+ ".mdx": "md",
1736
+ ".toml": "toml",
1737
+ ".html": "html",
1738
+ ".htm": "html",
1739
+ ".css": "css",
1740
+ ".scss": "css",
1741
+ ".less": "css",
1742
+ ".vue": "vue",
1743
+ ".svelte": "svelte",
1744
+ ".proto": "proto",
1745
+ ".graphql": "graphql",
1746
+ ".gql": "graphql"
1747
+ };
1748
+ var INDEXABLE_EXTENSIONS = Object.freeze(
1749
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
1750
+ );
1751
+ var SPECIAL_FILENAMES = {
1752
+ makefile: "other",
1753
+ gnumakefile: "other",
1754
+ dockerfile: "other",
1755
+ "docker-compose.yml": "yaml",
1756
+ "docker-compose.yaml": "yaml",
1757
+ "cmakelists.txt": "other",
1758
+ gemfile: "ruby",
1759
+ rakefile: "ruby",
1760
+ procfile: "other",
1761
+ justfile: "other"
1762
+ };
1763
+ function detectLang(file) {
1764
+ const base = path2.basename(file);
1765
+ const lowerBase = base.toLowerCase();
1766
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
1767
+ return "ts";
1768
+ }
1769
+ const special = SPECIAL_FILENAMES[lowerBase];
1770
+ if (special) return special;
1771
+ const ext = path2.extname(base).toLowerCase();
1772
+ if (!ext) return null;
1773
+ return EXT_TO_LANG[ext] ?? null;
1774
+ }
1775
+
1776
+ // src/codebase-index/ts-parser.ts
1422
1777
  var KIND_MAP = {
1423
1778
  [ts.SyntaxKind.ClassDeclaration]: "class",
1424
1779
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -1445,32 +1800,6 @@ function kindOf(node) {
1445
1800
  if (ts.isModuleDeclaration(node)) return "namespace";
1446
1801
  return KIND_MAP[node.kind] ?? null;
1447
1802
  }
1448
- function extToLang(ext) {
1449
- switch (ext) {
1450
- case ".ts":
1451
- return "ts";
1452
- case ".tsx":
1453
- return "tsx";
1454
- case ".js":
1455
- return "js";
1456
- case ".jsx":
1457
- return "jsx";
1458
- case ".go":
1459
- return "go";
1460
- case ".py":
1461
- return "py";
1462
- case ".rs":
1463
- return "rs";
1464
- case ".json":
1465
- return "json";
1466
- case ".yaml":
1467
- return "yaml";
1468
- case ".yml":
1469
- return "yaml";
1470
- default:
1471
- return null;
1472
- }
1473
- }
1474
1803
  function getSignature(printer, node, sourceFile) {
1475
1804
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1476
1805
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -1593,30 +1922,25 @@ function deduplicateRefs(refs) {
1593
1922
  return true;
1594
1923
  });
1595
1924
  }
1596
- function detectLang(file) {
1597
- const idx = file.lastIndexOf(".");
1598
- if (idx < 0) return null;
1599
- return extToLang(file.slice(idx));
1600
- }
1601
1925
 
1602
1926
  // src/codebase-index/go-parser.ts
1603
1927
  import { spawn } from "node:child_process";
1604
1928
  import * as os from "node:os";
1605
- import * as path3 from "node:path";
1929
+ import * as path4 from "node:path";
1606
1930
  import * as fs3 from "node:fs/promises";
1607
1931
 
1608
1932
  // src/_win32-resolve.ts
1609
1933
  import * as fs2 from "node:fs";
1610
- import * as path2 from "node:path";
1934
+ import * as path3 from "node:path";
1611
1935
  function resolveWin32Command(cmd) {
1612
1936
  if (process.platform !== "win32") return cmd;
1613
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
1937
+ if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
1614
1938
  return cmd;
1615
1939
  }
1616
1940
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
1617
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
1941
+ const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
1618
1942
  for (const dir of pathDirs) {
1619
- const base = path2.join(dir, cmd);
1943
+ const base = path3.join(dir, cmd);
1620
1944
  for (const ext of pathext) {
1621
1945
  const full = `${base}${ext}`;
1622
1946
  try {
@@ -1629,11 +1953,22 @@ function resolveWin32Command(cmd) {
1629
1953
  return cmd;
1630
1954
  }
1631
1955
 
1956
+ // src/codebase-index/spawn-gate.ts
1957
+ var chain = Promise.resolve();
1958
+ function withSpawnGate(fn) {
1959
+ const run = chain.then(fn, fn);
1960
+ chain = run.then(
1961
+ () => void 0,
1962
+ () => void 0
1963
+ );
1964
+ return run;
1965
+ }
1966
+
1632
1967
  // src/codebase-index/go-parser.ts
1633
1968
  async function parseSymbols2(opts) {
1634
1969
  const { file, content, lang } = opts;
1635
1970
  try {
1636
- const parsed = await syncGoParse(file, content, lang);
1971
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
1637
1972
  if (parsed.symbols.length > 0) {
1638
1973
  return parsed;
1639
1974
  }
@@ -1929,11 +2264,16 @@ func formatType(t ast.Expr) string {
1929
2264
  }
1930
2265
  }
1931
2266
  `;
2267
+ var _cachedGoScriptPath = null;
1932
2268
  async function syncGoParse(filePath, content, lang) {
1933
- const tmpDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "ws-go-parse-"));
1934
2269
  try {
1935
- const scriptPath = path3.join(tmpDir, "parse.go");
1936
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2270
+ let scriptPath = _cachedGoScriptPath;
2271
+ if (!scriptPath) {
2272
+ const tmpDir = await fs3.mkdtemp(path4.join(os.tmpdir(), "ws-go-parse-"));
2273
+ scriptPath = path4.join(tmpDir, "parse.go");
2274
+ await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2275
+ _cachedGoScriptPath = scriptPath;
2276
+ }
1937
2277
  const goBinary = resolveWin32Command("go");
1938
2278
  const goResult = await new Promise(
1939
2279
  (resolve2, reject) => {
@@ -1990,23 +2330,312 @@ async function syncGoParse(filePath, content, lang) {
1990
2330
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1991
2331
  } catch {
1992
2332
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1993
- } finally {
1994
- await fs3.rm(tmpDir, { recursive: true, force: true });
1995
2333
  }
1996
2334
  }
1997
2335
 
1998
2336
  // src/codebase-index/py-parser.ts
1999
- import { spawn as spawn2, spawnSync } from "node:child_process";
2337
+ import { spawn as spawn2 } from "node:child_process";
2000
2338
  import * as fs4 from "node:fs/promises";
2001
2339
  import * as os2 from "node:os";
2002
- import * as path4 from "node:path";
2340
+ import * as path5 from "node:path";
2341
+
2342
+ // src/codebase-index/generic-parser.ts
2343
+ var C_LIKE = [
2344
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
2345
+ {
2346
+ 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,
2347
+ kind: "function"
2348
+ },
2349
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
2350
+ ];
2351
+ var LANG_PATTERNS = {
2352
+ py: [
2353
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
2354
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2355
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
2356
+ ],
2357
+ go: [
2358
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
2359
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
2360
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
2361
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
2362
+ ],
2363
+ rs: [
2364
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
2365
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
2366
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
2367
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
2368
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
2369
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
2370
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
2371
+ ],
2372
+ c: C_LIKE,
2373
+ cpp: C_LIKE,
2374
+ java: [
2375
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
2376
+ {
2377
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,\[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
2378
+ kind: "method"
2379
+ }
2380
+ ],
2381
+ csharp: [
2382
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
2383
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
2384
+ {
2385
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,\[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
2386
+ kind: "method"
2387
+ }
2388
+ ],
2389
+ php: [
2390
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
2391
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
2392
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
2393
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
2394
+ ],
2395
+ ruby: [
2396
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
2397
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2398
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
2399
+ ],
2400
+ swift: [
2401
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
2402
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
2403
+ ],
2404
+ kotlin: [
2405
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
2406
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
2407
+ ],
2408
+ scala: [
2409
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
2410
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
2411
+ ],
2412
+ shell: [
2413
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
2414
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
2415
+ ],
2416
+ sql: [
2417
+ { 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" }
2418
+ ],
2419
+ md: [
2420
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
2421
+ ],
2422
+ toml: [
2423
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
2424
+ ],
2425
+ html: [
2426
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
2427
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
2428
+ ],
2429
+ css: [
2430
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
2431
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
2432
+ ],
2433
+ vue: [
2434
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
2435
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
2436
+ ],
2437
+ svelte: [
2438
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
2439
+ ],
2440
+ dart: [
2441
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
2442
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
2443
+ ],
2444
+ lua: [
2445
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
2446
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
2447
+ ],
2448
+ r: [
2449
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
2450
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
2451
+ ],
2452
+ proto: [
2453
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
2454
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
2455
+ ],
2456
+ graphql: [
2457
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
2458
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
2459
+ ],
2460
+ zig: [
2461
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
2462
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
2463
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
2464
+ ],
2465
+ elixir: [
2466
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
2467
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
2468
+ ],
2469
+ haskell: [
2470
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
2471
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
2472
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
2473
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
2474
+ ],
2475
+ other: [
2476
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
2477
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2478
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
2479
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
2480
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
2481
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
2482
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
2483
+ ]
2484
+ };
2485
+ var KEYWORDS = /* @__PURE__ */ new Set([
2486
+ "if",
2487
+ "else",
2488
+ "for",
2489
+ "while",
2490
+ "switch",
2491
+ "case",
2492
+ "return",
2493
+ "break",
2494
+ "continue",
2495
+ "new",
2496
+ "delete",
2497
+ "typeof",
2498
+ "instanceof",
2499
+ "void",
2500
+ "null",
2501
+ "true",
2502
+ "false",
2503
+ "this",
2504
+ "super",
2505
+ "import",
2506
+ "export",
2507
+ "from",
2508
+ "as",
2509
+ "default",
2510
+ "public",
2511
+ "private",
2512
+ "protected",
2513
+ "static",
2514
+ "final",
2515
+ "class",
2516
+ "struct",
2517
+ "enum",
2518
+ "interface",
2519
+ "function",
2520
+ "def",
2521
+ "fn",
2522
+ "func",
2523
+ "var",
2524
+ "let",
2525
+ "const",
2526
+ "package",
2527
+ "namespace",
2528
+ "module",
2529
+ "using",
2530
+ "include",
2531
+ "require",
2532
+ "select",
2533
+ "from",
2534
+ "where",
2535
+ "and",
2536
+ "or",
2537
+ "not",
2538
+ "in",
2539
+ "is",
2540
+ "try",
2541
+ "catch",
2542
+ "finally",
2543
+ "throw",
2544
+ "async",
2545
+ "await",
2546
+ "yield"
2547
+ ]);
2548
+ function patternsFor(lang) {
2549
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
2550
+ }
2551
+ function looksBinary(content) {
2552
+ if (content.includes("\0")) return true;
2553
+ const sample = content.slice(0, 2048);
2554
+ if (sample.length === 0) return false;
2555
+ let bad = 0;
2556
+ for (let i = 0; i < sample.length; i++) {
2557
+ const c = sample.charCodeAt(i);
2558
+ if (c === 9 || c === 10 || c === 13) continue;
2559
+ if (c < 32 || c === 127) bad++;
2560
+ }
2561
+ return bad / sample.length > 0.1;
2562
+ }
2563
+ function lineColAt(content, index) {
2564
+ let line = 1;
2565
+ let lastNl = -1;
2566
+ for (let i = 0; i < index && i < content.length; i++) {
2567
+ if (content.charCodeAt(i) === 10) {
2568
+ line++;
2569
+ lastNl = i;
2570
+ }
2571
+ }
2572
+ return { line, col: index - lastNl };
2573
+ }
2574
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
2575
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
2576
+ function parseGeneric(opts) {
2577
+ const { file, lang } = opts;
2578
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
2579
+ const mtimeMs = Date.now();
2580
+ if (!opts.content || looksBinary(opts.content)) {
2581
+ return { file, lang, symbols: [], mtimeMs };
2582
+ }
2583
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
2584
+ const patterns = patternsFor(lang);
2585
+ const symbols = [];
2586
+ const seen = /* @__PURE__ */ new Set();
2587
+ for (const pattern of patterns) {
2588
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
2589
+ re.lastIndex = 0;
2590
+ let match;
2591
+ while ((match = re.exec(content)) !== null) {
2592
+ if (symbols.length >= maxSymbols) break;
2593
+ let name = (match[1] ?? match[2] ?? "").trim();
2594
+ if (lang === "md" && match[2]) name = match[2].trim();
2595
+ if (!name || name.length > 200) continue;
2596
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
2597
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
2598
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
2599
+ continue;
2600
+ }
2601
+ const { line, col } = lineColAt(content, match.index);
2602
+ const key = `${name}\0${line}\0${pattern.kind}`;
2603
+ if (seen.has(key)) continue;
2604
+ seen.add(key);
2605
+ const nl = content.indexOf("\n", match.index);
2606
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
2607
+ const signature = (lineText || name).trim().slice(0, 500);
2608
+ symbols.push({
2609
+ id: 0,
2610
+ lang,
2611
+ kind: lang === "md" ? "namespace" : pattern.kind,
2612
+ name: name.slice(0, 200),
2613
+ file,
2614
+ line,
2615
+ col,
2616
+ signature,
2617
+ docComment: "",
2618
+ scope: "",
2619
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
2620
+ });
2621
+ }
2622
+ if (symbols.length >= maxSymbols) break;
2623
+ }
2624
+ return { file, lang, symbols, mtimeMs };
2625
+ }
2003
2626
  async function parseSymbols3(opts) {
2627
+ return parseGeneric(opts);
2628
+ }
2629
+
2630
+ // src/codebase-index/py-parser.ts
2631
+ async function parseSymbols4(opts) {
2004
2632
  const { file, content, lang } = opts;
2005
2633
  try {
2006
- return await syncPyParse(file, content, lang);
2634
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
2635
+ if (native !== null) return native;
2007
2636
  } catch {
2008
- return { file, lang, symbols: [], mtimeMs: Date.now() };
2009
2637
  }
2638
+ return parseGeneric({ file, content, lang: lang === "py" ? "py" : lang });
2010
2639
  }
2011
2640
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
2012
2641
 
@@ -2211,20 +2840,37 @@ visitor.visit(tree)
2211
2840
 
2212
2841
  print(json.dumps([s.to_dict() for s in syms]))
2213
2842
  `;
2214
- function resolvePython() {
2843
+ async function resolvePython() {
2215
2844
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
2216
2845
  for (const name of candidates) {
2217
2846
  const resolved = resolveWin32Command(name);
2218
- const result = spawnSync(resolved, ["--version"], {
2219
- stdio: "pipe",
2220
- timeout: 5e3
2221
- });
2222
- if (result.error) continue;
2223
- if (result.status !== 0) continue;
2847
+ if (!await commandIsAvailable(resolved)) continue;
2224
2848
  return resolved;
2225
2849
  }
2226
2850
  return null;
2227
2851
  }
2852
+ function commandIsAvailable(command) {
2853
+ return new Promise((resolve2) => {
2854
+ let settled = false;
2855
+ const proc = spawn2(command, ["--version"], {
2856
+ stdio: "ignore",
2857
+ windowsHide: true
2858
+ });
2859
+ const finish = (available) => {
2860
+ if (settled) return;
2861
+ settled = true;
2862
+ clearTimeout(timer);
2863
+ resolve2(available);
2864
+ };
2865
+ const timer = setTimeout(() => {
2866
+ proc.kill("SIGKILL");
2867
+ finish(false);
2868
+ }, 5e3);
2869
+ timer.unref?.();
2870
+ proc.once("error", () => finish(false));
2871
+ proc.once("close", (code) => finish(code === 0));
2872
+ });
2873
+ }
2228
2874
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2229
2875
  return new Promise((resolve2, reject) => {
2230
2876
  let settled = false;
@@ -2260,21 +2906,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2260
2906
  });
2261
2907
  }
2262
2908
  var _cachedScriptPath = null;
2263
- var _cachedPyBinary = null;
2909
+ var cachedPyBinary;
2264
2910
  async function syncPyParse(filePath, content, lang) {
2265
2911
  try {
2266
2912
  if (!_cachedScriptPath) {
2267
- const tmpDir = path4.join(os2.tmpdir(), "ws-py-parse");
2913
+ const tmpDir = path5.join(os2.tmpdir(), "ws-py-parse");
2268
2914
  await fs4.mkdir(tmpDir, { recursive: true });
2269
- _cachedScriptPath = path4.join(tmpDir, "parse.py");
2915
+ _cachedScriptPath = path5.join(tmpDir, "parse.py");
2270
2916
  await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
2271
2917
  }
2272
- if (!_cachedPyBinary) {
2273
- _cachedPyBinary = resolvePython();
2274
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2275
- }
2918
+ cachedPyBinary ??= resolvePython();
2919
+ const pyBinary = await cachedPyBinary;
2920
+ if (!pyBinary) return null;
2276
2921
  const { code, stdout } = await spawnPyParser(
2277
- _cachedPyBinary,
2922
+ pyBinary,
2278
2923
  _cachedScriptPath,
2279
2924
  filePath,
2280
2925
  content
@@ -2298,30 +2943,39 @@ async function syncPyParse(filePath, content, lang) {
2298
2943
  }));
2299
2944
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2300
2945
  } catch {
2301
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2946
+ return null;
2302
2947
  }
2303
2948
  }
2304
2949
 
2305
2950
  // src/codebase-index/rs-parser.ts
2306
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
2307
- import { execFileSync, spawn as spawn3 } from "node:child_process";
2951
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
2952
+ import { execFile, spawn as spawn3 } from "node:child_process";
2308
2953
  import * as fs5 from "node:fs/promises";
2309
- import * as path5 from "node:path";
2310
- async function parseSymbols4(opts) {
2954
+ import * as path6 from "node:path";
2955
+ async function parseSymbols5(opts) {
2311
2956
  const { file, content, lang } = opts;
2312
- const nativeAvailable = checkNativeParser();
2957
+ const nativeAvailable = await checkNativeParser();
2313
2958
  if (nativeAvailable) {
2314
- const result = await tryNativeParse(file, content);
2959
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
2315
2960
  if (result) return result;
2316
2961
  }
2317
2962
  return regexParse({ file, content, lang });
2318
2963
  }
2964
+ var nativeParserAvailability;
2965
+ function probe(command, args) {
2966
+ return new Promise((resolve2, reject) => {
2967
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
2968
+ if (error) reject(error);
2969
+ else resolve2();
2970
+ });
2971
+ });
2972
+ }
2319
2973
  function checkNativeParser() {
2320
- try {
2321
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
2322
- const toolsDir = path5.join(process.cwd(), "tools");
2974
+ nativeParserAvailability ??= (async () => {
2323
2975
  try {
2324
- execFileSync(
2976
+ await probe("rustc", ["--version"]);
2977
+ const toolsDir = path6.join(process.cwd(), "tools");
2978
+ await probe(
2325
2979
  "cargo",
2326
2980
  [
2327
2981
  "metadata",
@@ -2329,23 +2983,21 @@ function checkNativeParser() {
2329
2983
  "--format-version",
2330
2984
  "1",
2331
2985
  "--manifest-path",
2332
- path5.join(toolsDir, "Cargo.toml")
2333
- ],
2334
- { stdio: "pipe", windowsHide: true }
2986
+ path6.join(toolsDir, "Cargo.toml")
2987
+ ]
2335
2988
  );
2336
2989
  return true;
2337
2990
  } catch {
2338
2991
  return false;
2339
2992
  }
2340
- } catch {
2341
- return false;
2342
- }
2993
+ })();
2994
+ return nativeParserAvailability;
2343
2995
  }
2344
2996
  async function tryNativeParse(file, content) {
2345
2997
  try {
2346
- const toolsDir = path5.join(process.cwd(), "tools");
2347
- const crateDir = path5.join(toolsDir, "syn-parser");
2348
- const tmpFile = path5.join(crateDir, "src", "input.rs");
2998
+ const toolsDir = path6.join(process.cwd(), "tools");
2999
+ const crateDir = path6.join(toolsDir, "syn-parser");
3000
+ const tmpFile = path6.join(crateDir, "src", "input.rs");
2349
3001
  await fs5.writeFile(tmpFile, content, "utf8");
2350
3002
  const cargoBinary = resolveWin32Command("cargo");
2351
3003
  const result = await new Promise(
@@ -2353,7 +3005,7 @@ async function tryNativeParse(file, content) {
2353
3005
  let settled = false;
2354
3006
  const proc = spawn3(
2355
3007
  cargoBinary,
2356
- ["run", "--manifest-path", path5.join(toolsDir, "Cargo.toml")],
3008
+ ["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
2357
3009
  {
2358
3010
  cwd: process.cwd(),
2359
3011
  stdio: ["pipe", "pipe", "pipe"],
@@ -2467,9 +3119,9 @@ function regexParse(opts) {
2467
3119
  }
2468
3120
 
2469
3121
  // src/codebase-index/json-parser.ts
2470
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
2471
- import * as path6 from "node:path";
2472
- function parseSymbols5(opts) {
3122
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
3123
+ import * as path7 from "node:path";
3124
+ function parseSymbols6(opts) {
2473
3125
  const { file, content, lang } = opts;
2474
3126
  try {
2475
3127
  return regexParse2({ file, content, lang });
@@ -2480,9 +3132,9 @@ function parseSymbols5(opts) {
2480
3132
  function regexParse2(opts) {
2481
3133
  const { file, content, lang } = opts;
2482
3134
  const symbols = [];
2483
- const basename3 = path6.basename(file).toLowerCase();
2484
- const isPackageJson = basename3 === "package.json";
2485
- const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
3135
+ const basename4 = path7.basename(file).toLowerCase();
3136
+ const isPackageJson = basename4 === "package.json";
3137
+ const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
2486
3138
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2487
3139
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2488
3140
  const lines = content.split("\n");
@@ -2506,11 +3158,11 @@ function regexParse2(opts) {
2506
3158
  const line = lineFromOffset(offset);
2507
3159
  symbols.push(
2508
3160
  makeSymbol({
2509
- name: path6.basename(file),
3161
+ name: path7.basename(file),
2510
3162
  kind: "object",
2511
3163
  line,
2512
3164
  col: 0,
2513
- signature: `"${path6.basename(file)}" = { ... }`,
3165
+ signature: `"${path7.basename(file)}" = { ... }`,
2514
3166
  file,
2515
3167
  lang
2516
3168
  })
@@ -2672,8 +3324,8 @@ function makeSymbol(opts) {
2672
3324
  }
2673
3325
 
2674
3326
  // src/codebase-index/yaml-parser.ts
2675
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core";
2676
- function parseSymbols6(opts) {
3327
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
3328
+ function parseSymbols7(opts) {
2677
3329
  const { file, content, lang } = opts;
2678
3330
  try {
2679
3331
  return regexParse3({ file, content, lang });
@@ -2822,8 +3474,8 @@ function makeSymbol2(opts) {
2822
3474
 
2823
3475
  // src/codebase-index/gitignore.ts
2824
3476
  import * as fs6 from "node:fs/promises";
2825
- import * as path7 from "node:path";
2826
- import { compileGlob } from "@wrongstack/core";
3477
+ import * as path8 from "node:path";
3478
+ import { compileGlob } from "@wrongstack/core/utils";
2827
3479
  function globBody(glob) {
2828
3480
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
2829
3481
  }
@@ -2868,7 +3520,7 @@ function compileGitignore(lines) {
2868
3520
  async function loadGitignoreMatcher(projectRoot) {
2869
3521
  let lines = [];
2870
3522
  try {
2871
- const raw = await fs6.readFile(path7.join(projectRoot, ".gitignore"), "utf8");
3523
+ const raw = await fs6.readFile(path8.join(projectRoot, ".gitignore"), "utf8");
2872
3524
  lines = raw.split("\n");
2873
3525
  } catch {
2874
3526
  }
@@ -2877,7 +3529,9 @@ async function loadGitignoreMatcher(projectRoot) {
2877
3529
 
2878
3530
  // src/codebase-index/indexer.ts
2879
3531
  var YIELD_EVERY_N = 50;
2880
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
3532
+ function resolveParallelBatch() {
3533
+ return indexParallelBatchSize(availableParallelism());
3534
+ }
2881
3535
  function yieldEventLoop() {
2882
3536
  return new Promise((resolve2) => setImmediate(resolve2));
2883
3537
  }
@@ -2903,8 +3557,8 @@ var DEFAULT_IGNORE = [
2903
3557
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2904
3558
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2905
3559
  function isWithinProject(projectRoot, file) {
2906
- const rel = path8.relative(projectRoot, file);
2907
- return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
3560
+ const rel = path9.relative(projectRoot, file);
3561
+ return rel !== "" && !rel.startsWith(`..${path9.sep}`) && rel !== ".." && !path9.isAbsolute(rel);
2908
3562
  }
2909
3563
  function isMissingPathError(err) {
2910
3564
  const code = err?.code;
@@ -2915,18 +3569,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2915
3569
  const errors = [];
2916
3570
  let complete = true;
2917
3571
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2918
- const globs = [
2919
- { ext: ".ts", pat: compileGlob2("**/*.ts") },
2920
- { ext: ".tsx", pat: compileGlob2("**/*.tsx") },
2921
- { ext: ".js", pat: compileGlob2("**/*.js") },
2922
- { ext: ".jsx", pat: compileGlob2("**/*.jsx") },
2923
- { ext: ".go", pat: compileGlob2("**/*.go") },
2924
- { ext: ".py", pat: compileGlob2("**/*.py") },
2925
- { ext: ".rs", pat: compileGlob2("**/*.rs") },
2926
- { ext: ".json", pat: compileGlob2("**/*.json") },
2927
- { ext: ".yaml", pat: compileGlob2("**/*.yaml") },
2928
- { ext: ".yml", pat: compileGlob2("**/*.yml") }
2929
- ];
3572
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
2930
3573
  let dirCount = 0;
2931
3574
  const walk = async (dir) => {
2932
3575
  throwIfAborted(signal);
@@ -2945,19 +3588,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2945
3588
  dirCount++;
2946
3589
  for (const e of entries) {
2947
3590
  if (ignoreSet.has(e.name)) continue;
2948
- const full = path8.join(dir, e.name);
2949
- const rel = path8.relative(projectRoot, full).replace(/\\/g, "/");
3591
+ const full = path9.join(dir, e.name);
3592
+ const rel = path9.relative(projectRoot, full).replace(/\\/g, "/");
2950
3593
  if (e.isDirectory()) {
2951
3594
  if (isGitIgnored(rel, true)) continue;
2952
3595
  await walk(full);
2953
3596
  } else if (e.isFile()) {
2954
3597
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2955
- const ext = path8.extname(e.name).toLowerCase();
2956
- for (const { ext: extName, pat } of globs) {
2957
- if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2958
- results.push(full);
2959
- break;
2960
- }
3598
+ const ext = path9.extname(e.name).toLowerCase();
3599
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
3600
+ results.push(full);
2961
3601
  }
2962
3602
  }
2963
3603
  }
@@ -2966,24 +3606,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2966
3606
  return { files: results, complete, errors };
2967
3607
  }
2968
3608
  async function parseFile(file, content, lang) {
2969
- switch (lang) {
3609
+ const symbolLang = lang;
3610
+ switch (symbolLang) {
2970
3611
  case "ts":
2971
3612
  case "tsx":
2972
3613
  case "js":
2973
3614
  case "jsx":
2974
- return parseSymbols({ file, content, lang });
3615
+ return parseSymbols({ file, content, lang: symbolLang });
2975
3616
  case "go":
2976
3617
  return parseSymbols2({ file, content, lang: "go" });
2977
3618
  case "py":
2978
- return parseSymbols3({ file, content, lang: "py" });
3619
+ return parseSymbols4({ file, content, lang: "py" });
2979
3620
  case "rs":
2980
- return parseSymbols4({ file, content, lang: "rs" });
3621
+ return parseSymbols5({ file, content, lang: "rs" });
2981
3622
  case "json":
2982
- return parseSymbols5({ file, content, lang: "json" });
3623
+ return parseSymbols6({ file, content, lang: "json" });
2983
3624
  case "yaml":
2984
- return parseSymbols6({ file, content, lang: "yaml" });
3625
+ return parseSymbols7({ file, content, lang: "yaml" });
2985
3626
  default:
2986
- return { file, lang, symbols: [], mtimeMs: Date.now() };
3627
+ return parseSymbols3({ file, content, lang: symbolLang });
2987
3628
  }
2988
3629
  }
2989
3630
  function assignRefsToSymbols2(refs, symbols) {
@@ -3031,10 +3672,10 @@ async function runIndexerWithStore(store, opts) {
3031
3672
  let discoveredFiles = null;
3032
3673
  let discoveryComplete = true;
3033
3674
  if (opts.files && opts.files.length > 0) {
3034
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3675
+ files = opts.files.map((f) => path9.resolve(projectRoot, f)).filter((f) => {
3035
3676
  if (!isWithinProject(projectRoot, f)) return false;
3036
- const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
3037
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
3677
+ const rel = path9.relative(projectRoot, f).replace(/\\/g, "/");
3678
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path9.basename(f)) && !isGitIgnored(rel, false);
3038
3679
  });
3039
3680
  } else {
3040
3681
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -3055,15 +3696,19 @@ async function runIndexerWithStore(store, opts) {
3055
3696
  if (!force) {
3056
3697
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
3057
3698
  }
3699
+ const parallelBatch = resolveParallelBatch();
3058
3700
  let filesSinceLastYield = 0;
3059
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
3060
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
3701
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
3702
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
3061
3703
  const batchFiles = files.slice(batchStart, batchEnd);
3062
3704
  opts.onProgress?.(batchEnd, files.length);
3063
3705
  filesSinceLastYield += batchFiles.length;
3064
3706
  if (filesSinceLastYield >= YIELD_EVERY_N) {
3065
3707
  filesSinceLastYield = 0;
3066
3708
  await yieldEventLoop();
3709
+ if (isFrugalPerf()) {
3710
+ await new Promise((r) => setTimeout(r, 8));
3711
+ }
3067
3712
  throwIfAborted(signal);
3068
3713
  }
3069
3714
  const statOpts = signal ? { signal } : {};
@@ -3167,9 +3812,7 @@ async function runIndexerWithStore(store, opts) {
3167
3812
  continue;
3168
3813
  }
3169
3814
  if (parsed.symbols.length === 0) {
3170
- store.deleteRefsForFile(file);
3171
- store.deleteSymbolsForFile(file);
3172
- store.upsertFile({
3815
+ store.replaceEmptyFile({
3173
3816
  file,
3174
3817
  lang,
3175
3818
  mtimeMs: Math.floor(stat2.mtimeMs),
@@ -3238,6 +3881,7 @@ async function runIndexerWithStore(store, opts) {
3238
3881
  }
3239
3882
  store.resolveRefs();
3240
3883
  store.setMetadata("relation_graph_version", relationGraphVersion);
3884
+ store.optimize();
3241
3885
  const durationMs = Date.now() - startMs;
3242
3886
  store.setLastIndexed(Date.now());
3243
3887
  return {