@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
@@ -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 { DEFAULT_WALK_IGNORE_DIRS, 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,61 +1345,77 @@ 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, COUNT(*) AS n
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'
1393
+ GROUP BY r.call_type, sf.file, st.file`
1149
1394
  ).all();
1150
1395
  const edgeMap = /* @__PURE__ */ new Map();
1151
1396
  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;
1397
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
1398
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
1399
+ if (fromPkg === toPkg) continue;
1156
1400
  const key = `${fromPkg}\0${toPkg}`;
1157
1401
  let e = edgeMap.get(key);
1158
1402
  if (!e) {
1159
1403
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
1160
1404
  edgeMap.set(key, e);
1161
1405
  }
1162
- e.weight++;
1163
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1406
+ const n = Number(r.n) || 0;
1407
+ e.weight += n;
1408
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
1164
1409
  }
1165
- const importRows = this.db.prepare(
1166
- `SELECT r.from_id, r.to_name
1410
+ const importRows = this.stmt(
1411
+ `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
1167
1412
  FROM refs r
1168
- WHERE r.call_type = 'import'`
1413
+ JOIN symbols s ON s.id = r.from_id
1414
+ WHERE r.call_type = 'import'
1415
+ GROUP BY r.to_name, s.file`
1169
1416
  ).all();
1170
1417
  for (const r of importRows) {
1171
- const fromPkg = symbolToPkg.get(r.from_id);
1418
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
1172
1419
  const toPkg = _IndexStore.packageFromImport(r.to_name);
1173
1420
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1174
1421
  const key = `${fromPkg}\0${toPkg}`;
@@ -1177,8 +1424,9 @@ var IndexStore = class _IndexStore {
1177
1424
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1178
1425
  edgeMap.set(key, edge);
1179
1426
  }
1180
- edge.weight++;
1181
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1427
+ const n = Number(r.n) || 0;
1428
+ edge.weight += n;
1429
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
1182
1430
  }
1183
1431
  const edges = [];
1184
1432
  for (const [key, e] of edgeMap) {
@@ -1205,12 +1453,12 @@ var IndexStore = class _IndexStore {
1205
1453
  * derived from cross-file symbol references within the package.
1206
1454
  */
1207
1455
  getFileGraph(packageFilter) {
1208
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
1456
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
1209
1457
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
1210
1458
  const localFiles = new Set(pkgFilePaths);
1211
1459
  if (localFiles.size === 0) return { nodes: [], edges: [] };
1212
1460
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
1213
- const pkgSyms = this.db.prepare(
1461
+ const pkgSyms = this.stmt(
1214
1462
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
1215
1463
  ).all(...pkgFilePaths);
1216
1464
  const fileNodes = /* @__PURE__ */ new Map();
@@ -1242,12 +1490,13 @@ var IndexStore = class _IndexStore {
1242
1490
  ensureFileNode(file);
1243
1491
  }
1244
1492
  const indexedFiles = new Set(allFiles.map((f) => f.file));
1245
- const refRows = this.db.prepare(
1246
- `SELECT r.from_id, r.to_id, r.call_type
1493
+ const refRows = this.stmt(
1494
+ `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
1247
1495
  FROM refs r
1248
1496
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
1249
1497
  OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
1250
- AND r.to_id IS NOT NULL`
1498
+ AND r.to_id IS NOT NULL
1499
+ GROUP BY r.from_id, r.to_id, r.call_type`
1251
1500
  ).all(...pkgFilePaths, ...pkgFilePaths);
1252
1501
  const knownSymIds = new Set(pkgSyms.map((s) => s.id));
1253
1502
  const crossRefIds = /* @__PURE__ */ new Set();
@@ -1257,7 +1506,7 @@ var IndexStore = class _IndexStore {
1257
1506
  }
1258
1507
  if (crossRefIds.size > 0) {
1259
1508
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
1260
- const extras = this.db.prepare(
1509
+ const extras = this.stmt(
1261
1510
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
1262
1511
  ).all(...crossRefIds);
1263
1512
  for (const x of extras) {
@@ -1282,14 +1531,16 @@ var IndexStore = class _IndexStore {
1282
1531
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
1283
1532
  edgeMap.set(key, e);
1284
1533
  }
1285
- e.weight++;
1286
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1534
+ const n = Number(r.n) || 0;
1535
+ e.weight += n;
1536
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
1287
1537
  }
1288
- const importRows = this.db.prepare(
1289
- `SELECT r.from_id, r.to_name
1538
+ const importRows = this.stmt(
1539
+ `SELECT r.from_id, r.to_name, COUNT(*) AS n
1290
1540
  FROM refs r
1291
1541
  WHERE r.call_type = 'import'
1292
- AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
1542
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
1543
+ GROUP BY r.from_id, r.to_name`
1293
1544
  ).all(...pkgFilePaths);
1294
1545
  for (const r of importRows) {
1295
1546
  const fromFile = symToFile.get(r.from_id);
@@ -1304,8 +1555,9 @@ var IndexStore = class _IndexStore {
1304
1555
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1305
1556
  edgeMap.set(key, edge);
1306
1557
  }
1307
- edge.weight++;
1308
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1558
+ const n = Number(r.n) || 0;
1559
+ edge.weight += n;
1560
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
1309
1561
  }
1310
1562
  const edges = [];
1311
1563
  for (const [key, e] of edgeMap) {
@@ -1332,7 +1584,7 @@ var IndexStore = class _IndexStore {
1332
1584
  * derived from intra-file and cross-file symbol references (who calls whom).
1333
1585
  */
1334
1586
  getSymbolGraph(fileFilter) {
1335
- const syms = this.db.prepare(
1587
+ const syms = this.stmt(
1336
1588
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
1337
1589
  ).all(fileFilter);
1338
1590
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -1352,17 +1604,25 @@ var IndexStore = class _IndexStore {
1352
1604
  scope: s.scope,
1353
1605
  external: s.file !== fileFilter
1354
1606
  });
1355
- const symIds = new Set(syms.map((s) => s.id));
1356
- const refRows = this.db.prepare(
1357
- `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1358
- 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 = ?)`
1607
+ const refRows = this.stmt(
1608
+ `SELECT from_id, to_id, call_type, COUNT(*) AS n
1609
+ FROM (
1610
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1611
+ FROM refs r
1612
+ JOIN symbols s ON s.id = r.from_id
1613
+ WHERE s.file = ?
1614
+ UNION
1615
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1616
+ FROM refs r
1617
+ JOIN symbols s ON s.id = r.to_id
1618
+ WHERE s.file = ?
1619
+ )
1620
+ WHERE to_id IS NOT NULL
1621
+ GROUP BY from_id, to_id, call_type`
1361
1622
  ).all(fileFilter, fileFilter);
1362
1623
  const edgeMap = /* @__PURE__ */ new Map();
1363
1624
  for (const r of refRows) {
1364
- if (r.to_id === null) continue;
1365
- if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
1625
+ if (r.to_id == null) continue;
1366
1626
  relatedIds.add(r.from_id);
1367
1627
  relatedIds.add(r.to_id);
1368
1628
  const key = `${r.from_id}\0${r.to_id}`;
@@ -1371,8 +1631,9 @@ var IndexStore = class _IndexStore {
1371
1631
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
1372
1632
  edgeMap.set(key, e);
1373
1633
  }
1374
- e.weight++;
1375
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1634
+ const n = Number(r.n) || 0;
1635
+ e.weight += n;
1636
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
1376
1637
  }
1377
1638
  const edges = [];
1378
1639
  for (const [key, e] of edgeMap) {
@@ -1396,7 +1657,7 @@ var IndexStore = class _IndexStore {
1396
1657
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
1397
1658
  if (missingIds.length > 0) {
1398
1659
  const placeholders = missingIds.map(() => "?").join(",");
1399
- const extras = this.db.prepare(
1660
+ const extras = this.stmt(
1400
1661
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
1401
1662
  ).all(...missingIds);
1402
1663
  for (const s of extras) symById.set(s.id, s);
@@ -1410,6 +1671,8 @@ var IndexStore = class _IndexStore {
1410
1671
  }
1411
1672
  close() {
1412
1673
  this.stmtCache.clear();
1674
+ this.bm25Dirty = true;
1675
+ this.bm25Cache = null;
1413
1676
  try {
1414
1677
  this.db.close();
1415
1678
  } catch {
@@ -1419,6 +1682,110 @@ var IndexStore = class _IndexStore {
1419
1682
 
1420
1683
  // src/codebase-index/ts-parser.ts
1421
1684
  import * as ts from "@typescript/typescript6";
1685
+
1686
+ // src/codebase-index/languages.ts
1687
+ import * as path2 from "node:path";
1688
+ var EXT_TO_LANG = {
1689
+ // TypeScript / JavaScript (first-class TS compiler API)
1690
+ ".ts": "ts",
1691
+ ".mts": "ts",
1692
+ ".cts": "ts",
1693
+ ".tsx": "tsx",
1694
+ ".js": "js",
1695
+ ".mjs": "js",
1696
+ ".cjs": "js",
1697
+ ".jsx": "jsx",
1698
+ // First-class native/spawn parsers (+ regex fallback)
1699
+ ".go": "go",
1700
+ ".py": "py",
1701
+ ".pyi": "py",
1702
+ ".pyw": "py",
1703
+ ".rs": "rs",
1704
+ ".json": "json",
1705
+ ".jsonc": "json",
1706
+ ".yaml": "yaml",
1707
+ ".yml": "yaml",
1708
+ // C family
1709
+ ".c": "c",
1710
+ ".h": "c",
1711
+ ".cc": "cpp",
1712
+ ".cpp": "cpp",
1713
+ ".cxx": "cpp",
1714
+ ".hh": "cpp",
1715
+ ".hpp": "cpp",
1716
+ ".hxx": "cpp",
1717
+ // JVM / .NET
1718
+ ".java": "java",
1719
+ ".cs": "csharp",
1720
+ ".kt": "kotlin",
1721
+ ".kts": "kotlin",
1722
+ ".scala": "scala",
1723
+ ".sc": "scala",
1724
+ // Scripting
1725
+ ".php": "php",
1726
+ ".rb": "ruby",
1727
+ ".swift": "swift",
1728
+ ".dart": "dart",
1729
+ ".lua": "lua",
1730
+ ".r": "r",
1731
+ ".R": "r",
1732
+ ".pl": "other",
1733
+ ".pm": "other",
1734
+ // Systems / functional
1735
+ ".zig": "zig",
1736
+ ".ex": "elixir",
1737
+ ".exs": "elixir",
1738
+ ".hs": "haskell",
1739
+ ".lhs": "haskell",
1740
+ // Shell / data / docs / web
1741
+ ".sh": "shell",
1742
+ ".bash": "shell",
1743
+ ".zsh": "shell",
1744
+ ".ps1": "shell",
1745
+ ".sql": "sql",
1746
+ ".md": "md",
1747
+ ".mdx": "md",
1748
+ ".toml": "toml",
1749
+ ".html": "html",
1750
+ ".htm": "html",
1751
+ ".css": "css",
1752
+ ".scss": "css",
1753
+ ".less": "css",
1754
+ ".vue": "vue",
1755
+ ".svelte": "svelte",
1756
+ ".proto": "proto",
1757
+ ".graphql": "graphql",
1758
+ ".gql": "graphql"
1759
+ };
1760
+ var INDEXABLE_EXTENSIONS = Object.freeze(
1761
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
1762
+ );
1763
+ var SPECIAL_FILENAMES = {
1764
+ makefile: "other",
1765
+ gnumakefile: "other",
1766
+ dockerfile: "other",
1767
+ "docker-compose.yml": "yaml",
1768
+ "docker-compose.yaml": "yaml",
1769
+ "cmakelists.txt": "other",
1770
+ gemfile: "ruby",
1771
+ rakefile: "ruby",
1772
+ procfile: "other",
1773
+ justfile: "other"
1774
+ };
1775
+ function detectLang(file) {
1776
+ const base = path2.basename(file);
1777
+ const lowerBase = base.toLowerCase();
1778
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
1779
+ return "ts";
1780
+ }
1781
+ const special = SPECIAL_FILENAMES[lowerBase];
1782
+ if (special) return special;
1783
+ const ext = path2.extname(base).toLowerCase();
1784
+ if (!ext) return null;
1785
+ return EXT_TO_LANG[ext] ?? null;
1786
+ }
1787
+
1788
+ // src/codebase-index/ts-parser.ts
1422
1789
  var KIND_MAP = {
1423
1790
  [ts.SyntaxKind.ClassDeclaration]: "class",
1424
1791
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -1445,32 +1812,6 @@ function kindOf(node) {
1445
1812
  if (ts.isModuleDeclaration(node)) return "namespace";
1446
1813
  return KIND_MAP[node.kind] ?? null;
1447
1814
  }
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
1815
  function getSignature(printer, node, sourceFile) {
1475
1816
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1476
1817
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -1593,30 +1934,25 @@ function deduplicateRefs(refs) {
1593
1934
  return true;
1594
1935
  });
1595
1936
  }
1596
- function detectLang(file) {
1597
- const idx = file.lastIndexOf(".");
1598
- if (idx < 0) return null;
1599
- return extToLang(file.slice(idx));
1600
- }
1601
1937
 
1602
1938
  // src/codebase-index/go-parser.ts
1603
1939
  import { spawn } from "node:child_process";
1604
1940
  import * as os from "node:os";
1605
- import * as path3 from "node:path";
1941
+ import * as path4 from "node:path";
1606
1942
  import * as fs3 from "node:fs/promises";
1607
1943
 
1608
1944
  // src/_win32-resolve.ts
1609
1945
  import * as fs2 from "node:fs";
1610
- import * as path2 from "node:path";
1946
+ import * as path3 from "node:path";
1611
1947
  function resolveWin32Command(cmd) {
1612
1948
  if (process.platform !== "win32") return cmd;
1613
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
1949
+ if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
1614
1950
  return cmd;
1615
1951
  }
1616
1952
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
1617
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
1953
+ const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
1618
1954
  for (const dir of pathDirs) {
1619
- const base = path2.join(dir, cmd);
1955
+ const base = path3.join(dir, cmd);
1620
1956
  for (const ext of pathext) {
1621
1957
  const full = `${base}${ext}`;
1622
1958
  try {
@@ -1629,11 +1965,22 @@ function resolveWin32Command(cmd) {
1629
1965
  return cmd;
1630
1966
  }
1631
1967
 
1968
+ // src/codebase-index/spawn-gate.ts
1969
+ var chain = Promise.resolve();
1970
+ function withSpawnGate(fn) {
1971
+ const run = chain.then(fn, fn);
1972
+ chain = run.then(
1973
+ () => void 0,
1974
+ () => void 0
1975
+ );
1976
+ return run;
1977
+ }
1978
+
1632
1979
  // src/codebase-index/go-parser.ts
1633
1980
  async function parseSymbols2(opts) {
1634
1981
  const { file, content, lang } = opts;
1635
1982
  try {
1636
- const parsed = await syncGoParse(file, content, lang);
1983
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
1637
1984
  if (parsed.symbols.length > 0) {
1638
1985
  return parsed;
1639
1986
  }
@@ -1929,11 +2276,16 @@ func formatType(t ast.Expr) string {
1929
2276
  }
1930
2277
  }
1931
2278
  `;
2279
+ var _cachedGoScriptPath = null;
1932
2280
  async function syncGoParse(filePath, content, lang) {
1933
- const tmpDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "ws-go-parse-"));
1934
2281
  try {
1935
- const scriptPath = path3.join(tmpDir, "parse.go");
1936
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2282
+ let scriptPath = _cachedGoScriptPath;
2283
+ if (!scriptPath) {
2284
+ const tmpDir = await fs3.mkdtemp(path4.join(os.tmpdir(), "ws-go-parse-"));
2285
+ scriptPath = path4.join(tmpDir, "parse.go");
2286
+ await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
2287
+ _cachedGoScriptPath = scriptPath;
2288
+ }
1937
2289
  const goBinary = resolveWin32Command("go");
1938
2290
  const goResult = await new Promise(
1939
2291
  (resolve2, reject) => {
@@ -1990,23 +2342,311 @@ async function syncGoParse(filePath, content, lang) {
1990
2342
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
1991
2343
  } catch {
1992
2344
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1993
- } finally {
1994
- await fs3.rm(tmpDir, { recursive: true, force: true });
1995
2345
  }
1996
2346
  }
1997
2347
 
1998
2348
  // src/codebase-index/py-parser.ts
1999
- import { spawn as spawn2, spawnSync } from "node:child_process";
2349
+ import { spawn as spawn2 } from "node:child_process";
2000
2350
  import * as fs4 from "node:fs/promises";
2001
2351
  import * as os2 from "node:os";
2002
- import * as path4 from "node:path";
2352
+ import * as path5 from "node:path";
2353
+
2354
+ // src/codebase-index/generic-parser.ts
2355
+ var C_LIKE = [
2356
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
2357
+ {
2358
+ 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,
2359
+ kind: "function"
2360
+ },
2361
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
2362
+ ];
2363
+ var LANG_PATTERNS = {
2364
+ py: [
2365
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
2366
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2367
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
2368
+ ],
2369
+ go: [
2370
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
2371
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
2372
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
2373
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
2374
+ ],
2375
+ rs: [
2376
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
2377
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
2378
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
2379
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
2380
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
2381
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
2382
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
2383
+ ],
2384
+ c: C_LIKE,
2385
+ cpp: C_LIKE,
2386
+ java: [
2387
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
2388
+ {
2389
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
2390
+ kind: "method"
2391
+ }
2392
+ ],
2393
+ csharp: [
2394
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
2395
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
2396
+ {
2397
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
2398
+ kind: "method"
2399
+ }
2400
+ ],
2401
+ php: [
2402
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
2403
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
2404
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
2405
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
2406
+ ],
2407
+ ruby: [
2408
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
2409
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2410
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
2411
+ ],
2412
+ swift: [
2413
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
2414
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
2415
+ ],
2416
+ kotlin: [
2417
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
2418
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
2419
+ ],
2420
+ scala: [
2421
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
2422
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
2423
+ ],
2424
+ shell: [
2425
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
2426
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
2427
+ ],
2428
+ sql: [
2429
+ { 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" }
2430
+ ],
2431
+ md: [
2432
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
2433
+ ],
2434
+ toml: [
2435
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
2436
+ ],
2437
+ html: [
2438
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
2439
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
2440
+ ],
2441
+ css: [
2442
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
2443
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
2444
+ ],
2445
+ vue: [
2446
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
2447
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
2448
+ ],
2449
+ svelte: [
2450
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
2451
+ ],
2452
+ dart: [
2453
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
2454
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
2455
+ ],
2456
+ lua: [
2457
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
2458
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
2459
+ ],
2460
+ r: [
2461
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
2462
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
2463
+ ],
2464
+ proto: [
2465
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
2466
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
2467
+ ],
2468
+ graphql: [
2469
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
2470
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
2471
+ ],
2472
+ zig: [
2473
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
2474
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
2475
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
2476
+ ],
2477
+ elixir: [
2478
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
2479
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
2480
+ ],
2481
+ haskell: [
2482
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
2483
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
2484
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
2485
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
2486
+ ],
2487
+ other: [
2488
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
2489
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
2490
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
2491
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
2492
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
2493
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
2494
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
2495
+ ]
2496
+ };
2497
+ var KEYWORDS = /* @__PURE__ */ new Set([
2498
+ "if",
2499
+ "else",
2500
+ "for",
2501
+ "while",
2502
+ "switch",
2503
+ "case",
2504
+ "return",
2505
+ "break",
2506
+ "continue",
2507
+ "new",
2508
+ "delete",
2509
+ "typeof",
2510
+ "instanceof",
2511
+ "void",
2512
+ "null",
2513
+ "true",
2514
+ "false",
2515
+ "this",
2516
+ "super",
2517
+ "import",
2518
+ "export",
2519
+ "from",
2520
+ "as",
2521
+ "default",
2522
+ "public",
2523
+ "private",
2524
+ "protected",
2525
+ "static",
2526
+ "final",
2527
+ "class",
2528
+ "struct",
2529
+ "enum",
2530
+ "interface",
2531
+ "function",
2532
+ "def",
2533
+ "fn",
2534
+ "func",
2535
+ "var",
2536
+ "let",
2537
+ "const",
2538
+ "package",
2539
+ "namespace",
2540
+ "module",
2541
+ "using",
2542
+ "include",
2543
+ "require",
2544
+ "select",
2545
+ "from",
2546
+ "where",
2547
+ "and",
2548
+ "or",
2549
+ "not",
2550
+ "in",
2551
+ "is",
2552
+ "try",
2553
+ "catch",
2554
+ "finally",
2555
+ "throw",
2556
+ "async",
2557
+ "await",
2558
+ "yield"
2559
+ ]);
2560
+ function patternsFor(lang) {
2561
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
2562
+ }
2563
+ function looksBinary(content) {
2564
+ if (content.includes("\0")) return true;
2565
+ const sample = content.slice(0, 2048);
2566
+ if (sample.length === 0) return false;
2567
+ let bad = 0;
2568
+ for (let i = 0; i < sample.length; i++) {
2569
+ const c = sample.charCodeAt(i);
2570
+ if (c === 9 || c === 10 || c === 13) continue;
2571
+ if (c < 32 || c === 127) bad++;
2572
+ }
2573
+ return bad / sample.length > 0.1;
2574
+ }
2575
+ function lineColAt(content, index) {
2576
+ let line = 1;
2577
+ let lastNl = -1;
2578
+ for (let i = 0; i < index && i < content.length; i++) {
2579
+ if (content.charCodeAt(i) === 10) {
2580
+ line++;
2581
+ lastNl = i;
2582
+ }
2583
+ }
2584
+ return { line, col: index - lastNl };
2585
+ }
2586
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
2587
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
2588
+ function parseGeneric(opts) {
2589
+ const { file, lang } = opts;
2590
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
2591
+ const mtimeMs = Date.now();
2592
+ if (!opts.content || looksBinary(opts.content)) {
2593
+ return { file, lang, symbols: [], mtimeMs };
2594
+ }
2595
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
2596
+ const patterns = patternsFor(lang);
2597
+ const symbols = [];
2598
+ const seen = /* @__PURE__ */ new Set();
2599
+ for (const pattern of patterns) {
2600
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
2601
+ re.lastIndex = 0;
2602
+ for (const match of content.matchAll(re)) {
2603
+ if (symbols.length >= maxSymbols) break;
2604
+ let name = (match[1] ?? match[2] ?? "").trim();
2605
+ if (lang === "md" && match[2]) name = match[2].trim();
2606
+ if (!name || name.length > 200) continue;
2607
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
2608
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
2609
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
2610
+ continue;
2611
+ }
2612
+ const { line, col } = lineColAt(content, match.index);
2613
+ const key = `${name}\0${line}\0${pattern.kind}`;
2614
+ if (seen.has(key)) continue;
2615
+ seen.add(key);
2616
+ const nl = content.indexOf("\n", match.index);
2617
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
2618
+ const signature = (lineText || name).trim().slice(0, 500);
2619
+ symbols.push({
2620
+ id: 0,
2621
+ lang,
2622
+ kind: lang === "md" ? "namespace" : pattern.kind,
2623
+ name: name.slice(0, 200),
2624
+ file,
2625
+ line,
2626
+ col,
2627
+ signature,
2628
+ docComment: "",
2629
+ scope: "",
2630
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
2631
+ });
2632
+ }
2633
+ if (symbols.length >= maxSymbols) break;
2634
+ }
2635
+ return { file, lang, symbols, mtimeMs };
2636
+ }
2003
2637
  async function parseSymbols3(opts) {
2638
+ return parseGeneric(opts);
2639
+ }
2640
+
2641
+ // src/codebase-index/py-parser.ts
2642
+ async function parseSymbols4(opts) {
2004
2643
  const { file, content, lang } = opts;
2005
2644
  try {
2006
- return await syncPyParse(file, content, lang);
2645
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
2646
+ if (native !== null) return native;
2007
2647
  } catch {
2008
- return { file, lang, symbols: [], mtimeMs: Date.now() };
2009
2648
  }
2649
+ return parseGeneric({ file, content, lang: lang === "py" ? "py" : lang });
2010
2650
  }
2011
2651
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
2012
2652
 
@@ -2211,20 +2851,37 @@ visitor.visit(tree)
2211
2851
 
2212
2852
  print(json.dumps([s.to_dict() for s in syms]))
2213
2853
  `;
2214
- function resolvePython() {
2854
+ async function resolvePython() {
2215
2855
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
2216
2856
  for (const name of candidates) {
2217
2857
  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;
2858
+ if (!await commandIsAvailable(resolved)) continue;
2224
2859
  return resolved;
2225
2860
  }
2226
2861
  return null;
2227
2862
  }
2863
+ function commandIsAvailable(command) {
2864
+ return new Promise((resolve2) => {
2865
+ let settled = false;
2866
+ const proc = spawn2(command, ["--version"], {
2867
+ stdio: "ignore",
2868
+ windowsHide: true
2869
+ });
2870
+ const finish = (available) => {
2871
+ if (settled) return;
2872
+ settled = true;
2873
+ clearTimeout(timer);
2874
+ resolve2(available);
2875
+ };
2876
+ const timer = setTimeout(() => {
2877
+ proc.kill("SIGKILL");
2878
+ finish(false);
2879
+ }, 5e3);
2880
+ timer.unref?.();
2881
+ proc.once("error", () => finish(false));
2882
+ proc.once("close", (code) => finish(code === 0));
2883
+ });
2884
+ }
2228
2885
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2229
2886
  return new Promise((resolve2, reject) => {
2230
2887
  let settled = false;
@@ -2260,21 +2917,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2260
2917
  });
2261
2918
  }
2262
2919
  var _cachedScriptPath = null;
2263
- var _cachedPyBinary = null;
2920
+ var cachedPyBinary;
2264
2921
  async function syncPyParse(filePath, content, lang) {
2265
2922
  try {
2266
2923
  if (!_cachedScriptPath) {
2267
- const tmpDir = path4.join(os2.tmpdir(), "ws-py-parse");
2924
+ const tmpDir = path5.join(os2.tmpdir(), "ws-py-parse");
2268
2925
  await fs4.mkdir(tmpDir, { recursive: true });
2269
- _cachedScriptPath = path4.join(tmpDir, "parse.py");
2926
+ _cachedScriptPath = path5.join(tmpDir, "parse.py");
2270
2927
  await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
2271
2928
  }
2272
- if (!_cachedPyBinary) {
2273
- _cachedPyBinary = resolvePython();
2274
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2275
- }
2929
+ cachedPyBinary ??= resolvePython();
2930
+ const pyBinary = await cachedPyBinary;
2931
+ if (!pyBinary) return null;
2276
2932
  const { code, stdout } = await spawnPyParser(
2277
- _cachedPyBinary,
2933
+ pyBinary,
2278
2934
  _cachedScriptPath,
2279
2935
  filePath,
2280
2936
  content
@@ -2298,30 +2954,39 @@ async function syncPyParse(filePath, content, lang) {
2298
2954
  }));
2299
2955
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
2300
2956
  } catch {
2301
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2957
+ return null;
2302
2958
  }
2303
2959
  }
2304
2960
 
2305
2961
  // src/codebase-index/rs-parser.ts
2306
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
2307
- import { execFileSync, spawn as spawn3 } from "node:child_process";
2962
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
2963
+ import { execFile, spawn as spawn3 } from "node:child_process";
2308
2964
  import * as fs5 from "node:fs/promises";
2309
- import * as path5 from "node:path";
2310
- async function parseSymbols4(opts) {
2965
+ import * as path6 from "node:path";
2966
+ async function parseSymbols5(opts) {
2311
2967
  const { file, content, lang } = opts;
2312
- const nativeAvailable = checkNativeParser();
2968
+ const nativeAvailable = await checkNativeParser();
2313
2969
  if (nativeAvailable) {
2314
- const result = await tryNativeParse(file, content);
2970
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
2315
2971
  if (result) return result;
2316
2972
  }
2317
2973
  return regexParse({ file, content, lang });
2318
2974
  }
2975
+ var nativeParserAvailability;
2976
+ function probe(command, args) {
2977
+ return new Promise((resolve2, reject) => {
2978
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
2979
+ if (error) reject(error);
2980
+ else resolve2();
2981
+ });
2982
+ });
2983
+ }
2319
2984
  function checkNativeParser() {
2320
- try {
2321
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
2322
- const toolsDir = path5.join(process.cwd(), "tools");
2985
+ nativeParserAvailability ??= (async () => {
2323
2986
  try {
2324
- execFileSync(
2987
+ await probe("rustc", ["--version"]);
2988
+ const toolsDir = path6.join(process.cwd(), "tools");
2989
+ await probe(
2325
2990
  "cargo",
2326
2991
  [
2327
2992
  "metadata",
@@ -2329,23 +2994,21 @@ function checkNativeParser() {
2329
2994
  "--format-version",
2330
2995
  "1",
2331
2996
  "--manifest-path",
2332
- path5.join(toolsDir, "Cargo.toml")
2333
- ],
2334
- { stdio: "pipe", windowsHide: true }
2997
+ path6.join(toolsDir, "Cargo.toml")
2998
+ ]
2335
2999
  );
2336
3000
  return true;
2337
3001
  } catch {
2338
3002
  return false;
2339
3003
  }
2340
- } catch {
2341
- return false;
2342
- }
3004
+ })();
3005
+ return nativeParserAvailability;
2343
3006
  }
2344
3007
  async function tryNativeParse(file, content) {
2345
3008
  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");
3009
+ const toolsDir = path6.join(process.cwd(), "tools");
3010
+ const crateDir = path6.join(toolsDir, "syn-parser");
3011
+ const tmpFile = path6.join(crateDir, "src", "input.rs");
2349
3012
  await fs5.writeFile(tmpFile, content, "utf8");
2350
3013
  const cargoBinary = resolveWin32Command("cargo");
2351
3014
  const result = await new Promise(
@@ -2353,7 +3016,7 @@ async function tryNativeParse(file, content) {
2353
3016
  let settled = false;
2354
3017
  const proc = spawn3(
2355
3018
  cargoBinary,
2356
- ["run", "--manifest-path", path5.join(toolsDir, "Cargo.toml")],
3019
+ ["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
2357
3020
  {
2358
3021
  cwd: process.cwd(),
2359
3022
  stdio: ["pipe", "pipe", "pipe"],
@@ -2467,9 +3130,9 @@ function regexParse(opts) {
2467
3130
  }
2468
3131
 
2469
3132
  // 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) {
3133
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
3134
+ import * as path7 from "node:path";
3135
+ function parseSymbols6(opts) {
2473
3136
  const { file, content, lang } = opts;
2474
3137
  try {
2475
3138
  return regexParse2({ file, content, lang });
@@ -2480,9 +3143,9 @@ function parseSymbols5(opts) {
2480
3143
  function regexParse2(opts) {
2481
3144
  const { file, content, lang } = opts;
2482
3145
  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";
3146
+ const basename4 = path7.basename(file).toLowerCase();
3147
+ const isPackageJson = basename4 === "package.json";
3148
+ const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
2486
3149
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2487
3150
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2488
3151
  const lines = content.split("\n");
@@ -2506,11 +3169,11 @@ function regexParse2(opts) {
2506
3169
  const line = lineFromOffset(offset);
2507
3170
  symbols.push(
2508
3171
  makeSymbol({
2509
- name: path6.basename(file),
3172
+ name: path7.basename(file),
2510
3173
  kind: "object",
2511
3174
  line,
2512
3175
  col: 0,
2513
- signature: `"${path6.basename(file)}" = { ... }`,
3176
+ signature: `"${path7.basename(file)}" = { ... }`,
2514
3177
  file,
2515
3178
  lang
2516
3179
  })
@@ -2672,8 +3335,8 @@ function makeSymbol(opts) {
2672
3335
  }
2673
3336
 
2674
3337
  // src/codebase-index/yaml-parser.ts
2675
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core";
2676
- function parseSymbols6(opts) {
3338
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
3339
+ function parseSymbols7(opts) {
2677
3340
  const { file, content, lang } = opts;
2678
3341
  try {
2679
3342
  return regexParse3({ file, content, lang });
@@ -2822,8 +3485,8 @@ function makeSymbol2(opts) {
2822
3485
 
2823
3486
  // src/codebase-index/gitignore.ts
2824
3487
  import * as fs6 from "node:fs/promises";
2825
- import * as path7 from "node:path";
2826
- import { compileGlob } from "@wrongstack/core";
3488
+ import * as path8 from "node:path";
3489
+ import { compileGlob } from "@wrongstack/core/utils";
2827
3490
  function globBody(glob) {
2828
3491
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
2829
3492
  }
@@ -2868,7 +3531,7 @@ function compileGitignore(lines) {
2868
3531
  async function loadGitignoreMatcher(projectRoot) {
2869
3532
  let lines = [];
2870
3533
  try {
2871
- const raw = await fs6.readFile(path7.join(projectRoot, ".gitignore"), "utf8");
3534
+ const raw = await fs6.readFile(path8.join(projectRoot, ".gitignore"), "utf8");
2872
3535
  lines = raw.split("\n");
2873
3536
  } catch {
2874
3537
  }
@@ -2877,7 +3540,9 @@ async function loadGitignoreMatcher(projectRoot) {
2877
3540
 
2878
3541
  // src/codebase-index/indexer.ts
2879
3542
  var YIELD_EVERY_N = 50;
2880
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
3543
+ function resolveParallelBatch() {
3544
+ return indexParallelBatchSize(availableParallelism());
3545
+ }
2881
3546
  function yieldEventLoop() {
2882
3547
  return new Promise((resolve2) => setImmediate(resolve2));
2883
3548
  }
@@ -2889,22 +3554,12 @@ function throwIfAborted(signal) {
2889
3554
  function isAbortError(err) {
2890
3555
  return err instanceof DOMException && err.name === "AbortError";
2891
3556
  }
2892
- var DEFAULT_IGNORE = [
2893
- "node_modules",
2894
- ".git",
2895
- "dist",
2896
- "build",
2897
- ".next",
2898
- "coverage",
2899
- ".turbo",
2900
- "__snapshots__",
2901
- ".nyc_output"
2902
- ];
3557
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
2903
3558
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2904
3559
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2905
3560
  function isWithinProject(projectRoot, file) {
2906
- const rel = path8.relative(projectRoot, file);
2907
- return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
3561
+ const rel = path9.relative(projectRoot, file);
3562
+ return rel !== "" && !rel.startsWith(`..${path9.sep}`) && rel !== ".." && !path9.isAbsolute(rel);
2908
3563
  }
2909
3564
  function isMissingPathError(err) {
2910
3565
  const code = err?.code;
@@ -2915,18 +3570,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2915
3570
  const errors = [];
2916
3571
  let complete = true;
2917
3572
  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
- ];
3573
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
2930
3574
  let dirCount = 0;
2931
3575
  const walk = async (dir) => {
2932
3576
  throwIfAborted(signal);
@@ -2945,19 +3589,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2945
3589
  dirCount++;
2946
3590
  for (const e of entries) {
2947
3591
  if (ignoreSet.has(e.name)) continue;
2948
- const full = path8.join(dir, e.name);
2949
- const rel = path8.relative(projectRoot, full).replace(/\\/g, "/");
3592
+ const full = path9.join(dir, e.name);
3593
+ const rel = path9.relative(projectRoot, full).replace(/\\/g, "/");
2950
3594
  if (e.isDirectory()) {
2951
3595
  if (isGitIgnored(rel, true)) continue;
2952
3596
  await walk(full);
2953
3597
  } else if (e.isFile()) {
2954
3598
  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
- }
3599
+ const ext = path9.extname(e.name).toLowerCase();
3600
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
3601
+ results.push(full);
2961
3602
  }
2962
3603
  }
2963
3604
  }
@@ -2966,24 +3607,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2966
3607
  return { files: results, complete, errors };
2967
3608
  }
2968
3609
  async function parseFile(file, content, lang) {
2969
- switch (lang) {
3610
+ const symbolLang = lang;
3611
+ switch (symbolLang) {
2970
3612
  case "ts":
2971
3613
  case "tsx":
2972
3614
  case "js":
2973
3615
  case "jsx":
2974
- return parseSymbols({ file, content, lang });
3616
+ return parseSymbols({ file, content, lang: symbolLang });
2975
3617
  case "go":
2976
3618
  return parseSymbols2({ file, content, lang: "go" });
2977
3619
  case "py":
2978
- return parseSymbols3({ file, content, lang: "py" });
3620
+ return parseSymbols4({ file, content, lang: "py" });
2979
3621
  case "rs":
2980
- return parseSymbols4({ file, content, lang: "rs" });
3622
+ return parseSymbols5({ file, content, lang: "rs" });
2981
3623
  case "json":
2982
- return parseSymbols5({ file, content, lang: "json" });
3624
+ return parseSymbols6({ file, content, lang: "json" });
2983
3625
  case "yaml":
2984
- return parseSymbols6({ file, content, lang: "yaml" });
3626
+ return parseSymbols7({ file, content, lang: "yaml" });
2985
3627
  default:
2986
- return { file, lang, symbols: [], mtimeMs: Date.now() };
3628
+ return parseSymbols3({ file, content, lang: symbolLang });
2987
3629
  }
2988
3630
  }
2989
3631
  function assignRefsToSymbols2(refs, symbols) {
@@ -3031,10 +3673,10 @@ async function runIndexerWithStore(store, opts) {
3031
3673
  let discoveredFiles = null;
3032
3674
  let discoveryComplete = true;
3033
3675
  if (opts.files && opts.files.length > 0) {
3034
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3676
+ files = opts.files.map((f) => path9.resolve(projectRoot, f)).filter((f) => {
3035
3677
  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);
3678
+ const rel = path9.relative(projectRoot, f).replace(/\\/g, "/");
3679
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path9.basename(f)) && !isGitIgnored(rel, false);
3038
3680
  });
3039
3681
  } else {
3040
3682
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -3055,15 +3697,19 @@ async function runIndexerWithStore(store, opts) {
3055
3697
  if (!force) {
3056
3698
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
3057
3699
  }
3700
+ const parallelBatch = resolveParallelBatch();
3058
3701
  let filesSinceLastYield = 0;
3059
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
3060
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
3702
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
3703
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
3061
3704
  const batchFiles = files.slice(batchStart, batchEnd);
3062
3705
  opts.onProgress?.(batchEnd, files.length);
3063
3706
  filesSinceLastYield += batchFiles.length;
3064
3707
  if (filesSinceLastYield >= YIELD_EVERY_N) {
3065
3708
  filesSinceLastYield = 0;
3066
3709
  await yieldEventLoop();
3710
+ if (isFrugalPerf()) {
3711
+ await new Promise((r) => setTimeout(r, 8));
3712
+ }
3067
3713
  throwIfAborted(signal);
3068
3714
  }
3069
3715
  const statOpts = signal ? { signal } : {};
@@ -3167,9 +3813,7 @@ async function runIndexerWithStore(store, opts) {
3167
3813
  continue;
3168
3814
  }
3169
3815
  if (parsed.symbols.length === 0) {
3170
- store.deleteRefsForFile(file);
3171
- store.deleteSymbolsForFile(file);
3172
- store.upsertFile({
3816
+ store.replaceEmptyFile({
3173
3817
  file,
3174
3818
  lang,
3175
3819
  mtimeMs: Math.floor(stat2.mtimeMs),
@@ -3238,6 +3882,7 @@ async function runIndexerWithStore(store, opts) {
3238
3882
  }
3239
3883
  store.resolveRefs();
3240
3884
  store.setMetadata("relation_graph_version", relationGraphVersion);
3885
+ store.optimize();
3241
3886
  const durationMs = Date.now() - startMs;
3242
3887
  store.setLastIndexed(Date.now());
3243
3888
  return {