@wrongstack/tools 0.289.0 → 0.291.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ export declare function onIndexStateChange(listener: IndexStateListener): () =>
58
58
  * Tear down the index host (worker + pending debounces). Call on process
59
59
  * shutdown; safe to call when nothing is running.
60
60
  */
61
- export declare function shutdownCodebaseIndexHost(): void;
61
+ export declare function shutdownCodebaseIndexHost(): Promise<void>;
62
62
  /** True when the file's extension maps to a language the indexer can parse. */
63
63
  export declare function isIndexableFile(filePath: string): boolean;
64
64
  export declare function runStartupIndex(opts: {
@@ -1 +1 @@
1
- {"version":3,"file":"background-indexer.d.ts","sourceRoot":"","sources":["../../src/codebase-index/background-indexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,EAEL,KAAK,eAAe,EAIrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,KAAK,EAKV,YAAY,EACZ,cAAc,EACd,WAAW,EAEZ,MAAM,sBAAsB,CAAC;AAkC9B,iFAAiF;AACjF,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,qDAAqD;AACrD,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wFAAwF;AACxF,wBAAgB,aAAa,IAAI;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uFAAuF;IACvF,OAAO,EAAE,eAAe,CAAC;CAC1B,CASA;AAED;;;;GAIG;AACH,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,KAAK,IAAI,CAAC;AAG5E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAK3E;AA8GD;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAIhD;AA4JD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAmBD,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7B,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACjC,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,GAAG,OAAO,CAAC,WAAW,CAAC,CA2DvB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAChD,GAAG,IAAI,CAmCP;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,YAAY,EAClB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAO,GAC9E,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED,0EAA0E;AAC1E,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,WAAW,EACjB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAO,GAC9E,OAAO,CAAC,UAAU,CAAC,CAKrB;AAID;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAchD"}
1
+ {"version":3,"file":"background-indexer.d.ts","sourceRoot":"","sources":["../../src/codebase-index/background-indexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,EAEL,KAAK,eAAe,EAIrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG3D,OAAO,KAAK,EAKV,YAAY,EACZ,cAAc,EACd,WAAW,EAEZ,MAAM,sBAAsB,CAAC;AAkC9B,iFAAiF;AACjF,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,qDAAqD;AACrD,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wFAAwF;AACxF,wBAAgB,aAAa,IAAI;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uFAAuF;IACvF,OAAO,EAAE,eAAe,CAAC;CAC1B,CASA;AAED;;;;GAIG;AACH,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,KAAK,IAAI,CAAC;AAG5E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAK3E;AA8GD;;;GAGG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB/D;AA4JD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAmBD,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7B,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IACjC,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,GAAG,OAAO,CAAC,WAAW,CAAC,CA2DvB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAChD,GAAG,IAAI,CAmCP;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,YAAY,EAClB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAO,GAC9E,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED,0EAA0E;AAC1E,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,WAAW,EACjB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAO,GAC9E,OAAO,CAAC,UAAU,CAAC,CAKrB;AAID;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAchD"}
@@ -277,6 +277,53 @@ function codebaseIndexDirOverride(ctx) {
277
277
  const v = ctx.meta?.["codebaseIndexDir"];
278
278
  return typeof v === "string" ? v : void 0;
279
279
  }
280
+ var StorePool = class {
281
+ stores = /* @__PURE__ */ new Map();
282
+ key(projectRoot, indexDir) {
283
+ return `${projectRoot}\0${indexDir ?? ""}`;
284
+ }
285
+ /** Borrow a store. Creates it on first access for this key. */
286
+ acquire(projectRoot, opts) {
287
+ const k = this.key(projectRoot, opts?.indexDir);
288
+ let store = this.stores.get(k);
289
+ if (!store) {
290
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
291
+ this.stores.set(k, store);
292
+ }
293
+ return store;
294
+ }
295
+ /** Return the store to the pool. The connection stays warm for subsequent
296
+ * operations on the same (projectRoot, indexDir). */
297
+ release(_store) {
298
+ }
299
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
300
+ closeAll() {
301
+ for (const store of this.stores.values()) {
302
+ try {
303
+ store.close();
304
+ } catch {
305
+ }
306
+ }
307
+ this.stores.clear();
308
+ }
309
+ /** Remove one store from the pool. Used by tests that need isolation. */
310
+ evict(projectRoot, indexDir) {
311
+ const k = this.key(projectRoot, indexDir);
312
+ const store = this.stores.get(k);
313
+ if (store) {
314
+ try {
315
+ store.close();
316
+ } catch {
317
+ }
318
+ this.stores.delete(k);
319
+ }
320
+ }
321
+ /** True when the pool holds a connection for the given key. */
322
+ has(projectRoot, indexDir) {
323
+ return this.stores.has(this.key(projectRoot, indexDir));
324
+ }
325
+ };
326
+ var indexStorePool = new StorePool();
280
327
  var warningSilenced = false;
281
328
  function silenceSqliteExperimentalWarning() {
282
329
  if (warningSilenced) return;
@@ -494,7 +541,7 @@ var IndexStore = class _IndexStore {
494
541
  return this.runWithRetry(() => {
495
542
  this.db.exec("BEGIN IMMEDIATE");
496
543
  try {
497
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
544
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
498
545
  let nextId = (maxRows[0]?.m ?? 0) + 1;
499
546
  const stmt = this.db.prepare(
500
547
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -766,7 +813,7 @@ var IndexStore = class _IndexStore {
766
813
  return { results, total: candidates.length };
767
814
  }
768
815
  getAllIndexable() {
769
- return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
816
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
770
817
  }
771
818
  /**
772
819
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -776,7 +823,7 @@ var IndexStore = class _IndexStore {
776
823
  * `symbols.id`). Ids may have gaps — that is fine.
777
824
  */
778
825
  getMaxSymbolId() {
779
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
826
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
780
827
  return rows[0]?.m ?? 0;
781
828
  }
782
829
  // ─── Stats ───────────────────────────────────────────────────────────────────
@@ -784,14 +831,14 @@ var IndexStore = class _IndexStore {
784
831
  const sizeBytes = this.sizeBytes();
785
832
  const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
786
833
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
787
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
834
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
788
835
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
789
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
836
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
790
837
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
791
- const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
838
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
792
839
  const byLang = {};
793
840
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
794
- const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
841
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
795
842
  const byKind = {};
796
843
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
797
844
  return {
@@ -823,11 +870,14 @@ var IndexStore = class _IndexStore {
823
870
  this.runWithRetry(() => {
824
871
  this.db.exec("BEGIN IMMEDIATE");
825
872
  try {
826
- this.db.exec("DELETE FROM refs");
827
- this.db.exec("DELETE FROM symbols");
828
- this.db.exec("DELETE FROM files");
829
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
873
+ this.db.exec("DROP TABLE IF EXISTS refs");
874
+ this.db.exec("DROP TABLE IF EXISTS symbols");
875
+ this.db.exec("DROP TABLE IF EXISTS files");
876
+ this.db.exec("DROP TABLE IF EXISTS metadata");
877
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
830
878
  this.db.exec("COMMIT");
879
+ this.stmtCache.clear();
880
+ this.initSchema();
831
881
  } catch (err) {
832
882
  this.db.exec("ROLLBACK");
833
883
  throw err;
@@ -912,7 +962,7 @@ var IndexStore = class _IndexStore {
912
962
  ).run(...options.deleteForFiles);
913
963
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
914
964
  }
915
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
965
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
916
966
  let nextId = (maxRows[0]?.m ?? 0) + 1;
917
967
  const symStmt = this.db.prepare(
918
968
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -1096,7 +1146,7 @@ var IndexStore = class _IndexStore {
1096
1146
  * symbol resolved in package B). Node metadata includes symbol/file counts.
1097
1147
  */
1098
1148
  getPackageGraph() {
1099
- const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1149
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
1100
1150
  const pkgNodes = /* @__PURE__ */ new Map();
1101
1151
  const fileToPkg = /* @__PURE__ */ new Map();
1102
1152
  const symbolToPkg = /* @__PURE__ */ new Map();
@@ -1187,15 +1237,18 @@ var IndexStore = class _IndexStore {
1187
1237
  * derived from cross-file symbol references within the package.
1188
1238
  */
1189
1239
  getFileGraph(packageFilter) {
1190
- const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1191
- const pkgSyms = allSymbols.filter(
1192
- (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
1193
- );
1194
- if (pkgSyms.length === 0) return { nodes: [], edges: [] };
1240
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
1241
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
1242
+ const localFiles = new Set(pkgFilePaths);
1243
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
1244
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
1245
+ const pkgSyms = this.db.prepare(
1246
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
1247
+ ).all(...pkgFilePaths);
1195
1248
  const fileNodes = /* @__PURE__ */ new Map();
1196
1249
  const symToFile = /* @__PURE__ */ new Map();
1197
1250
  const fileStats = /* @__PURE__ */ new Map();
1198
- for (const s of allSymbols) {
1251
+ for (const s of pkgSyms) {
1199
1252
  symToFile.set(s.id, s.file);
1200
1253
  const current = fileStats.get(s.file);
1201
1254
  fileStats.set(s.file, {
@@ -1203,8 +1256,6 @@ var IndexStore = class _IndexStore {
1203
1256
  lang: current?.lang ?? s.lang
1204
1257
  });
1205
1258
  }
1206
- const localFiles = new Set(pkgSyms.map((s) => s.file));
1207
- const indexedFiles = new Set(allSymbols.map((s) => s.file));
1208
1259
  const ensureFileNode = (file) => {
1209
1260
  if (fileNodes.has(file)) return;
1210
1261
  const stats = fileStats.get(file);
@@ -1222,11 +1273,32 @@ var IndexStore = class _IndexStore {
1222
1273
  for (const file of localFiles) {
1223
1274
  ensureFileNode(file);
1224
1275
  }
1276
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
1225
1277
  const refRows = this.db.prepare(
1226
1278
  `SELECT r.from_id, r.to_id, r.call_type
1227
1279
  FROM refs r
1228
- WHERE r.to_id IS NOT NULL`
1229
- ).all();
1280
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
1281
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
1282
+ AND r.to_id IS NOT NULL`
1283
+ ).all(...pkgFilePaths, ...pkgFilePaths);
1284
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
1285
+ const crossRefIds = /* @__PURE__ */ new Set();
1286
+ for (const r of refRows) {
1287
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
1288
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
1289
+ }
1290
+ if (crossRefIds.size > 0) {
1291
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
1292
+ const extras = this.db.prepare(
1293
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
1294
+ ).all(...crossRefIds);
1295
+ for (const x of extras) {
1296
+ symToFile.set(x.id, x.file);
1297
+ if (!fileStats.has(x.file)) {
1298
+ fileStats.set(x.file, { count: 0, lang: "ts" });
1299
+ }
1300
+ }
1301
+ }
1230
1302
  const edgeMap = /* @__PURE__ */ new Map();
1231
1303
  for (const r of refRows) {
1232
1304
  if (r.call_type === "import") continue;
@@ -1248,8 +1320,9 @@ var IndexStore = class _IndexStore {
1248
1320
  const importRows = this.db.prepare(
1249
1321
  `SELECT r.from_id, r.to_name
1250
1322
  FROM refs r
1251
- WHERE r.call_type = 'import'`
1252
- ).all();
1323
+ WHERE r.call_type = 'import'
1324
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
1325
+ ).all(...pkgFilePaths);
1253
1326
  for (const r of importRows) {
1254
1327
  const fromFile = symToFile.get(r.from_id);
1255
1328
  if (!fromFile || !localFiles.has(fromFile)) continue;
@@ -1291,12 +1364,11 @@ var IndexStore = class _IndexStore {
1291
1364
  * derived from intra-file and cross-file symbol references (who calls whom).
1292
1365
  */
1293
1366
  getSymbolGraph(fileFilter) {
1294
- const allSymbols = this.db.prepare(
1295
- "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
1296
- ).all();
1297
- const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
1367
+ const syms = this.db.prepare(
1368
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
1369
+ ).all(fileFilter);
1298
1370
  if (syms.length === 0) return { nodes: [], edges: [] };
1299
- const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
1371
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
1300
1372
  const relatedIds = new Set(syms.map((symbol) => symbol.id));
1301
1373
  const toGraphNode = (s) => ({
1302
1374
  id: `sym:${s.id}`,
@@ -1352,6 +1424,15 @@ var IndexStore = class _IndexStore {
1352
1424
  refType: bestType
1353
1425
  });
1354
1426
  }
1427
+ const loadedIds = new Set(syms.map((s) => s.id));
1428
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
1429
+ if (missingIds.length > 0) {
1430
+ const placeholders = missingIds.map(() => "?").join(",");
1431
+ const extras = this.db.prepare(
1432
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
1433
+ ).all(...missingIds);
1434
+ for (const s of extras) symById.set(s.id, s);
1435
+ }
1355
1436
  const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
1356
1437
  const aExternal = a.file === fileFilter ? 0 : 1;
1357
1438
  const bExternal = b.file === fileFilter ? 0 : 1;
@@ -1377,6 +1458,7 @@ import { Worker } from "node:worker_threads";
1377
1458
  import { expectDefined as expectDefined5 } from "@wrongstack/core";
1378
1459
  import * as fs7 from "node:fs/promises";
1379
1460
  import * as path8 from "node:path";
1461
+ import { availableParallelism } from "node:os";
1380
1462
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
1381
1463
 
1382
1464
  // src/codebase-index/ts-parser.ts
@@ -1433,8 +1515,7 @@ function extToLang(ext) {
1433
1515
  return null;
1434
1516
  }
1435
1517
  }
1436
- function getSignature(node, sourceFile) {
1437
- const printer = ts.createPrinter({});
1518
+ function getSignature(printer, node, sourceFile) {
1438
1519
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1439
1520
  return raw.replace(/\s+/g, " ").slice(0, 500);
1440
1521
  }
@@ -1453,28 +1534,14 @@ function getJsDoc(node, sourceFile) {
1453
1534
  }
1454
1535
  return "";
1455
1536
  }
1456
- function hasFunctionLikeAncestor(node) {
1457
- let current = node.parent;
1458
- while (current) {
1459
- if (ts.isFunctionLike(current)) return true;
1460
- current = current.parent;
1461
- }
1462
- return false;
1463
- }
1464
- function buildScope(node) {
1465
- const parts = [];
1466
- let current = node.parent;
1467
- while (current) {
1468
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
1469
- parts.unshift(current.name?.text ?? "Anon");
1470
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
1471
- if (current.name && ts.isIdentifier(current.name)) {
1472
- parts.unshift(current.name.text);
1473
- }
1537
+ function pushScopeName(node, parts) {
1538
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
1539
+ parts.push(node.name?.text ?? "Anon");
1540
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
1541
+ if (node.name && ts.isIdentifier(node.name)) {
1542
+ parts.push(node.name.text);
1474
1543
  }
1475
- current = current.parent;
1476
1544
  }
1477
- return parts.join(".");
1478
1545
  }
1479
1546
  function parseSymbols(opts) {
1480
1547
  const { file, content, lang } = opts;
@@ -1485,45 +1552,39 @@ function parseSymbols(opts) {
1485
1552
  return { file, lang, symbols: [], mtimeMs: Date.now() };
1486
1553
  }
1487
1554
  const symbols = [];
1488
- function visit(node) {
1555
+ const refs = [];
1556
+ const printer = ts.createPrinter({});
1557
+ function visit(node, funcDepth, scopeParts) {
1489
1558
  const kind = kindOf(node);
1490
1559
  if (kind) {
1491
- if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
1492
- ts.forEachChild(node, visit);
1493
- return;
1560
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
1561
+ } else {
1562
+ const nameNode = node.name;
1563
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
1564
+ return;
1565
+ }
1566
+ const name = nameNode.text;
1567
+ const pos2 = nameNode.getStart(sourceFile);
1568
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
1569
+ const scope = scopeParts.join(".");
1570
+ const signature = getSignature(printer, node, sourceFile);
1571
+ const docComment = getJsDoc(node, sourceFile);
1572
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
1573
+ symbols.push({
1574
+ id: 0,
1575
+ lang,
1576
+ kind,
1577
+ name,
1578
+ file,
1579
+ line: line2 + 1,
1580
+ col: character,
1581
+ signature,
1582
+ docComment,
1583
+ scope,
1584
+ text
1585
+ });
1494
1586
  }
1495
- const nameNode = node.name;
1496
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
1497
- const name = nameNode.text;
1498
- const pos = nameNode.getStart(sourceFile);
1499
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
1500
- const scope = buildScope(node);
1501
- const signature = getSignature(node, sourceFile);
1502
- const docComment = getJsDoc(node, sourceFile);
1503
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
1504
- symbols.push({
1505
- id: 0,
1506
- lang,
1507
- kind,
1508
- name,
1509
- file,
1510
- line: line + 1,
1511
- col: character,
1512
- signature,
1513
- docComment,
1514
- scope,
1515
- text
1516
- });
1517
1587
  }
1518
- ts.forEachChild(node, visit);
1519
- }
1520
- visit(sourceFile);
1521
- const refs = extractRefs(sourceFile);
1522
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
1523
- }
1524
- function extractRefs(sourceFile) {
1525
- const refs = [];
1526
- function visit(node) {
1527
1588
  const pos = node.getStart(sourceFile);
1528
1589
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
1529
1590
  const lineNum = line + 1;
@@ -1548,10 +1609,14 @@ function extractRefs(sourceFile) {
1548
1609
  const moduleName = getModuleName(node);
1549
1610
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
1550
1611
  }
1551
- ts.forEachChild(node, visit);
1612
+ const scopeIdx = scopeParts.length;
1613
+ pushScopeName(node, scopeParts);
1614
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
1615
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
1616
+ scopeParts.length = scopeIdx;
1552
1617
  }
1553
- visit(sourceFile);
1554
- return deduplicateRefs(refs);
1618
+ visit(sourceFile, 0, []);
1619
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
1555
1620
  }
1556
1621
  function getTypeName(name) {
1557
1622
  if (ts.isIdentifier(name)) return name.text;
@@ -2856,7 +2921,7 @@ async function loadGitignoreMatcher(projectRoot) {
2856
2921
 
2857
2922
  // src/codebase-index/indexer.ts
2858
2923
  var YIELD_EVERY_N = 50;
2859
- var PARALLEL_BATCH = 20;
2924
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
2860
2925
  function yieldEventLoop() {
2861
2926
  return new Promise((resolve2) => setImmediate(resolve2));
2862
2927
  }
@@ -3034,11 +3099,14 @@ async function runIndexerWithStore(store, opts) {
3034
3099
  if (!force) {
3035
3100
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
3036
3101
  }
3102
+ let filesSinceLastYield = 0;
3037
3103
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
3038
3104
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
3039
3105
  const batchFiles = files.slice(batchStart, batchEnd);
3040
3106
  opts.onProgress?.(batchEnd, files.length);
3041
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
3107
+ filesSinceLastYield += batchFiles.length;
3108
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
3109
+ filesSinceLastYield = 0;
3042
3110
  await yieldEventLoop();
3043
3111
  throwIfAborted(signal);
3044
3112
  }
@@ -3249,7 +3317,7 @@ async function indexService(args, hooks = {}) {
3249
3317
  });
3250
3318
  }
3251
3319
  function searchService(args) {
3252
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3320
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3253
3321
  try {
3254
3322
  return store.searchRanked(
3255
3323
  args.query,
@@ -3262,39 +3330,39 @@ function searchService(args) {
3262
3330
  args.limit
3263
3331
  );
3264
3332
  } finally {
3265
- store.close();
3333
+ indexStorePool.release(store);
3266
3334
  }
3267
3335
  }
3268
3336
  function statsService(args) {
3269
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3337
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3270
3338
  try {
3271
3339
  return store.getStats();
3272
3340
  } finally {
3273
- store.close();
3341
+ indexStorePool.release(store);
3274
3342
  }
3275
3343
  }
3276
3344
  function packageGraphService(args) {
3277
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3345
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3278
3346
  try {
3279
3347
  return store.getPackageGraph();
3280
3348
  } finally {
3281
- store.close();
3349
+ indexStorePool.release(store);
3282
3350
  }
3283
3351
  }
3284
3352
  function fileGraphService(args) {
3285
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3353
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3286
3354
  try {
3287
3355
  return store.getFileGraph(args.packageFilter);
3288
3356
  } finally {
3289
- store.close();
3357
+ indexStorePool.release(store);
3290
3358
  }
3291
3359
  }
3292
3360
  function symbolGraphService(args) {
3293
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3361
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3294
3362
  try {
3295
3363
  return store.getSymbolGraph(args.fileFilter);
3296
3364
  } finally {
3297
- store.close();
3365
+ indexStorePool.release(store);
3298
3366
  }
3299
3367
  }
3300
3368
 
@@ -3411,10 +3479,19 @@ function terminateWorker(reason) {
3411
3479
  if (w) void w.terminate().catch(() => {
3412
3480
  });
3413
3481
  }
3414
- function shutdownCodebaseIndexHost() {
3482
+ async function shutdownCodebaseIndexHost() {
3415
3483
  cancelPendingReindexes();
3416
- terminateWorker(new Error("codebase-index host shut down"));
3484
+ indexStorePool.closeAll();
3485
+ const w = worker;
3486
+ worker = null;
3487
+ failAllPending(new Error("codebase-index host shut down"));
3417
3488
  workerUnavailable = false;
3489
+ if (w) {
3490
+ try {
3491
+ await w.terminate();
3492
+ } catch {
3493
+ }
3494
+ }
3418
3495
  }
3419
3496
  function callIndexOp(op, args, opts) {
3420
3497
  const w = ensureWorker();