@wrongstack/tools 0.287.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.
Files changed (102) hide show
  1. package/dist/_fetch-guard.d.ts.map +1 -1
  2. package/dist/_redact-command.d.ts.map +1 -1
  3. package/dist/_spawn-stream.d.ts.map +1 -1
  4. package/dist/audit.js +61 -14
  5. package/dist/audit.js.map +3 -3
  6. package/dist/auto-proceed-loop-guard.d.ts +128 -0
  7. package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
  8. package/dist/auto-proceed-loop-guard.js +46 -0
  9. package/dist/auto-proceed-loop-guard.js.map +7 -0
  10. package/dist/bash.d.ts +1 -1
  11. package/dist/bash.d.ts.map +1 -1
  12. package/dist/bash.js +97 -19
  13. package/dist/bash.js.map +3 -3
  14. package/dist/builtin.d.ts +12 -10
  15. package/dist/builtin.d.ts.map +1 -1
  16. package/dist/builtin.js +1766 -864
  17. package/dist/builtin.js.map +3 -3
  18. package/dist/codebase-index/background-indexer.d.ts +1 -1
  19. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  20. package/dist/codebase-index/bm25.d.ts +1 -2
  21. package/dist/codebase-index/bm25.d.ts.map +1 -1
  22. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  23. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  24. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  25. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  26. package/dist/codebase-index/index-service.d.ts +11 -1
  27. package/dist/codebase-index/index-service.d.ts.map +1 -1
  28. package/dist/codebase-index/index.d.ts +2 -1
  29. package/dist/codebase-index/index.d.ts.map +1 -1
  30. package/dist/codebase-index/index.js +888 -254
  31. package/dist/codebase-index/index.js.map +3 -3
  32. package/dist/codebase-index/indexer.d.ts.map +1 -1
  33. package/dist/codebase-index/refs-extractor.d.ts +2 -17
  34. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  35. package/dist/codebase-index/schema.d.ts +43 -1
  36. package/dist/codebase-index/schema.d.ts.map +1 -1
  37. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  38. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  39. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  40. package/dist/codebase-index/worker.js +738 -212
  41. package/dist/codebase-index/worker.js.map +3 -3
  42. package/dist/codebase-index/writer.d.ts +60 -1
  43. package/dist/codebase-index/writer.d.ts.map +1 -1
  44. package/dist/e2e.js +477 -0
  45. package/dist/e2e.js.map +7 -0
  46. package/dist/exec.d.ts +1 -1
  47. package/dist/exec.d.ts.map +1 -1
  48. package/dist/exec.js +60 -5
  49. package/dist/exec.js.map +3 -3
  50. package/dist/fetch.js +2 -2
  51. package/dist/fetch.js.map +2 -2
  52. package/dist/format.js +61 -14
  53. package/dist/format.js.map +3 -3
  54. package/dist/glob.js +2 -2
  55. package/dist/glob.js.map +1 -1
  56. package/dist/grep.d.ts.map +1 -1
  57. package/dist/grep.js +2 -2
  58. package/dist/grep.js.map +2 -2
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2001 -873
  62. package/dist/index.js.map +3 -3
  63. package/dist/install.js +61 -14
  64. package/dist/install.js.map +3 -3
  65. package/dist/kanban.d.ts +21 -2
  66. package/dist/kanban.d.ts.map +1 -1
  67. package/dist/kanban.js +713 -572
  68. package/dist/kanban.js.map +3 -3
  69. package/dist/languages/detect.d.ts.map +1 -1
  70. package/dist/languages/index.js +61 -14
  71. package/dist/languages/index.js.map +3 -3
  72. package/dist/languages/types.d.ts +7 -0
  73. package/dist/languages/types.d.ts.map +1 -1
  74. package/dist/lint.js +61 -14
  75. package/dist/lint.js.map +3 -3
  76. package/dist/outdated.js +61 -14
  77. package/dist/outdated.js.map +3 -3
  78. package/dist/pack.js +1763 -861
  79. package/dist/pack.js.map +3 -3
  80. package/dist/plan.js +4 -0
  81. package/dist/plan.js.map +2 -2
  82. package/dist/process-registry.js +12 -5
  83. package/dist/process-registry.js.map +2 -2
  84. package/dist/read.d.ts.map +1 -1
  85. package/dist/read.js.map +2 -2
  86. package/dist/search.js +2 -2
  87. package/dist/search.js.map +2 -2
  88. package/dist/session-kanban.d.ts +9 -0
  89. package/dist/session-kanban.d.ts.map +1 -1
  90. package/dist/session-kanban.js +197 -3
  91. package/dist/session-kanban.js.map +2 -2
  92. package/dist/task.js +4 -0
  93. package/dist/task.js.map +2 -2
  94. package/dist/test.js +61 -14
  95. package/dist/test.js.map +3 -3
  96. package/dist/todo.js +4 -0
  97. package/dist/todo.js.map +2 -2
  98. package/dist/tree.js +2 -2
  99. package/dist/tree.js.map +1 -1
  100. package/dist/typecheck.js +61 -14
  101. package/dist/typecheck.js.map +3 -3
  102. package/package.json +13 -5
@@ -5,6 +5,7 @@ import { parentPort } from "node:worker_threads";
5
5
  import { expectDefined as expectDefined5 } from "@wrongstack/core";
6
6
  import * as fs7 from "node:fs/promises";
7
7
  import * as path8 from "node:path";
8
+ import { availableParallelism } from "node:os";
8
9
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
9
10
 
10
11
  // src/codebase-index/writer.ts
@@ -92,7 +93,7 @@ import * as path from "node:path";
92
93
  import { resolveWstackPaths } from "@wrongstack/core";
93
94
 
94
95
  // src/codebase-index/schema.ts
95
- var SCHEMA_VERSION = 2;
96
+ var SCHEMA_VERSION = 3;
96
97
 
97
98
  // src/codebase-index/lsp-kind.ts
98
99
  function lspKindToInternalKind(k) {
@@ -135,7 +136,7 @@ function tokenise(text) {
135
136
  return sanitised.toLowerCase().split(" ").filter(Boolean);
136
137
  }
137
138
  function splitName(name) {
138
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
139
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
139
140
  }
140
141
  function buildIndexableText(name, signature, docComment) {
141
142
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -161,14 +162,12 @@ function buildBm25Index(docs) {
161
162
  return new Bm25Index(documents, df, N, avgLen);
162
163
  }
163
164
  var Bm25Index = class {
164
- constructor(documents, df, N, avgLen) {
165
+ constructor(documents, _df, N, avgLen) {
165
166
  this.documents = documents;
166
- this.df = df;
167
167
  this.N = N;
168
168
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
169
169
  }
170
170
  documents;
171
- df;
172
171
  N;
173
172
  safeAvgLen;
174
173
  score(query, filter) {
@@ -181,10 +180,13 @@ var Bm25Index = class {
181
180
  for (const qTerm of qTokens) {
182
181
  let tf = 0;
183
182
  for (const t of doc.tokens) {
184
- if (t === qTerm) tf++;
183
+ if (t.startsWith(qTerm)) tf++;
185
184
  }
186
185
  if (tf === 0) continue;
187
- const dfVal = this.df[qTerm] ?? 0;
186
+ let dfVal = 0;
187
+ for (const candidate of this.documents) {
188
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
189
+ }
188
190
  if (dfVal === 0) continue;
189
191
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
190
192
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -217,9 +219,79 @@ var Bm25Index = class {
217
219
 
218
220
  // src/codebase-index/writer.ts
219
221
  var DB_FILE = "index.db";
222
+ function escapeLike(value) {
223
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
224
+ }
225
+ function assignRefsToSymbols(refs, symbols) {
226
+ if (refs.length === 0 || symbols.length === 0) return [];
227
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
228
+ const seen = /* @__PURE__ */ new Set();
229
+ const assigned = [];
230
+ for (const ref of refs) {
231
+ let owner;
232
+ for (const symbol of ordered) {
233
+ if (symbol.line > ref.line) break;
234
+ owner = symbol;
235
+ }
236
+ if (!owner && ref.callType === "import") owner = ordered[0];
237
+ if (!owner || owner.id <= 0) continue;
238
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
239
+ if (seen.has(key)) continue;
240
+ seen.add(key);
241
+ assigned.push({ ...ref, fromId: owner.id });
242
+ }
243
+ return assigned;
244
+ }
220
245
  function resolveIndexDir(projectRoot, override) {
221
246
  return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
222
247
  }
248
+ var StorePool = class {
249
+ stores = /* @__PURE__ */ new Map();
250
+ key(projectRoot, indexDir) {
251
+ return `${projectRoot}\0${indexDir ?? ""}`;
252
+ }
253
+ /** Borrow a store. Creates it on first access for this key. */
254
+ acquire(projectRoot, opts) {
255
+ const k = this.key(projectRoot, opts?.indexDir);
256
+ let store = this.stores.get(k);
257
+ if (!store) {
258
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
259
+ this.stores.set(k, store);
260
+ }
261
+ return store;
262
+ }
263
+ /** Return the store to the pool. The connection stays warm for subsequent
264
+ * operations on the same (projectRoot, indexDir). */
265
+ release(_store) {
266
+ }
267
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
268
+ closeAll() {
269
+ for (const store of this.stores.values()) {
270
+ try {
271
+ store.close();
272
+ } catch {
273
+ }
274
+ }
275
+ this.stores.clear();
276
+ }
277
+ /** Remove one store from the pool. Used by tests that need isolation. */
278
+ evict(projectRoot, indexDir) {
279
+ const k = this.key(projectRoot, indexDir);
280
+ const store = this.stores.get(k);
281
+ if (store) {
282
+ try {
283
+ store.close();
284
+ } catch {
285
+ }
286
+ this.stores.delete(k);
287
+ }
288
+ }
289
+ /** True when the pool holds a connection for the given key. */
290
+ has(projectRoot, indexDir) {
291
+ return this.stores.has(this.key(projectRoot, indexDir));
292
+ }
293
+ };
294
+ var indexStorePool = new StorePool();
223
295
  var warningSilenced = false;
224
296
  function silenceSqliteExperimentalWarning() {
225
297
  if (warningSilenced) return;
@@ -266,7 +338,7 @@ function sleepSync(ms) {
266
338
  } catch {
267
339
  }
268
340
  }
269
- var IndexStore = class {
341
+ var IndexStore = class _IndexStore {
270
342
  db;
271
343
  /** Absolute path to this project's index directory. */
272
344
  indexDir;
@@ -318,10 +390,7 @@ var IndexStore = class {
318
390
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
319
391
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
320
392
  }
321
- const delay = Math.min(
322
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
323
- LOCK_RETRY_MAX_DELAY_MS
324
- );
393
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
325
394
  sleepSync(delay);
326
395
  }
327
396
  }
@@ -403,8 +472,24 @@ var IndexStore = class {
403
472
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
404
473
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
405
474
  try {
406
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
475
+ this.db.exec(
476
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
477
+ );
407
478
  this.ftsAvailable = true;
479
+ const symbolCount = Number(
480
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
481
+ );
482
+ const ftsCount = Number(
483
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
484
+ );
485
+ if (symbolCount !== ftsCount) {
486
+ 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
+ }
492
+ }
408
493
  } catch {
409
494
  this.ftsAvailable = false;
410
495
  }
@@ -424,7 +509,7 @@ var IndexStore = class {
424
509
  return this.runWithRetry(() => {
425
510
  this.db.exec("BEGIN IMMEDIATE");
426
511
  try {
427
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
512
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
428
513
  let nextId = (maxRows[0]?.m ?? 0) + 1;
429
514
  const stmt = this.db.prepare(
430
515
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -462,7 +547,9 @@ var IndexStore = class {
462
547
  deleteSymbolsForFile(file) {
463
548
  this.runWithRetry(() => {
464
549
  if (this.ftsAvailable) {
465
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
550
+ this.stmt(
551
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
552
+ ).run(file);
466
553
  }
467
554
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
468
555
  });
@@ -474,9 +561,21 @@ var IndexStore = class {
474
561
  */
475
562
  deleteFile(file) {
476
563
  this.runWithRetry(() => {
477
- this.deleteRefsForFile(file);
478
- this.deleteSymbolsForFile(file);
479
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
564
+ this.db.exec("BEGIN IMMEDIATE");
565
+ try {
566
+ if (this.ftsAvailable) {
567
+ this.db.prepare(
568
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
569
+ ).run(file);
570
+ }
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);
574
+ this.db.exec("COMMIT");
575
+ } catch (err) {
576
+ this.db.exec("ROLLBACK");
577
+ throw err;
578
+ }
480
579
  });
481
580
  }
482
581
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -499,14 +598,22 @@ var IndexStore = class {
499
598
  ).all(file);
500
599
  if (!rows.length) return null;
501
600
  const r = expectDefined(rows[0]);
502
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
601
+ return {
602
+ file: r.file,
603
+ lang: r.lang,
604
+ mtimeMs: r.mtime_ms,
605
+ symbolCount: r.symbol_count,
606
+ lastIndexed: r.last_indexed
607
+ };
503
608
  }
504
609
  getAllFileMetas() {
505
- return this.db.prepare(
506
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
507
- ).all().map(
508
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
509
- );
610
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
611
+ file: r.file,
612
+ lang: r.lang,
613
+ mtimeMs: r.mtime_ms,
614
+ symbolCount: r.symbol_count,
615
+ lastIndexed: r.last_indexed
616
+ }));
510
617
  }
511
618
  // ─── Search ──────────────────────────────────────────────────────────────────
512
619
  search(query, filter) {
@@ -530,8 +637,8 @@ var IndexStore = class {
530
637
  values.push(filter.lang);
531
638
  }
532
639
  if (filter?.file) {
533
- conditions.push("file LIKE ?");
534
- values.push(`%${filter.file}%`);
640
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
641
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
535
642
  }
536
643
  if (query.trim()) {
537
644
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -569,9 +676,11 @@ var IndexStore = class {
569
676
  * "users", camelCase-split text makes "complex" find "complexOperation").
570
677
  */
571
678
  searchRanked(query, filter, limit) {
679
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
680
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
572
681
  const tokens = tokenise(query);
573
682
  if (tokens.length === 0 || !this.ftsAvailable) {
574
- return this.searchRankedFallback(query, filter, limit);
683
+ return this.searchRankedFallback(query, filter, safeLimit);
575
684
  }
576
685
  let effectiveKind = filter?.kind;
577
686
  if (filter?.lspKind !== void 0) {
@@ -591,11 +700,13 @@ var IndexStore = class {
591
700
  values.push(filter.lang);
592
701
  }
593
702
  if (filter?.file) {
594
- conditions.push("s.file LIKE ?");
595
- values.push(`%${filter.file}%`);
703
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
704
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
596
705
  }
597
706
  const where = conditions.join(" AND ");
598
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
707
+ const countRows = this.db.prepare(
708
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
709
+ ).all(...values);
599
710
  const total = countRows[0] ? Number(countRows[0].n) : 0;
600
711
  if (total === 0) return { results: [], total: 0 };
601
712
  const rows = this.db.prepare(
@@ -604,9 +715,13 @@ var IndexStore = class {
604
715
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
605
716
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
606
717
  WHERE ${where}
607
- ORDER BY bm25(symbols_fts)
718
+ ORDER BY
719
+ CASE WHEN lower(s.name) = lower(?) THEN 0
720
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
721
+ ELSE 2 END,
722
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
608
723
  LIMIT ?`
609
- ).all(...values, limit);
724
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
610
725
  return {
611
726
  results: rows.map((r) => ({
612
727
  id: r.id,
@@ -636,10 +751,28 @@ var IndexStore = class {
636
751
  }
637
752
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
638
753
  const bm25 = buildBm25Index(
639
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
754
+ candidates.map((c) => ({
755
+ id: c.id,
756
+ text: buildIndexableText(c.name, c.signature, c.docComment)
757
+ }))
640
758
  );
641
759
  const scored = bm25.score(query, (id) => candidateById.has(id));
642
- scored.sort((a, b) => b.score - a.score);
760
+ const q = query.trim().toLowerCase();
761
+ const rank = (id) => {
762
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
763
+ if (name === q) return 0;
764
+ if (name.startsWith(q)) return 1;
765
+ return 2;
766
+ };
767
+ scored.sort((a, b) => {
768
+ const rankDiff = rank(a.id) - rank(b.id);
769
+ if (rankDiff !== 0) return rankDiff;
770
+ const scoreDiff = b.score - a.score;
771
+ if (scoreDiff !== 0) return scoreDiff;
772
+ const left = expectDefined(candidateById.get(a.id));
773
+ const right = expectDefined(candidateById.get(b.id));
774
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
775
+ });
643
776
  const qTokens = tokenise(query);
644
777
  const results = scored.slice(0, limit).map(({ id, score }) => {
645
778
  const c = expectDefined(candidateById.get(id));
@@ -648,9 +781,7 @@ var IndexStore = class {
648
781
  return { results, total: candidates.length };
649
782
  }
650
783
  getAllIndexable() {
651
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
652
- ({ id, text }) => ({ id, text })
653
- );
784
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
654
785
  }
655
786
  /**
656
787
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -660,28 +791,22 @@ var IndexStore = class {
660
791
  * `symbols.id`). Ids may have gaps — that is fine.
661
792
  */
662
793
  getMaxSymbolId() {
663
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
794
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
664
795
  return rows[0]?.m ?? 0;
665
796
  }
666
797
  // ─── Stats ───────────────────────────────────────────────────────────────────
667
798
  getStats() {
668
799
  const sizeBytes = this.sizeBytes();
669
- const lastRows = this.db.prepare(
670
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
671
- ).all();
800
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
672
801
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
673
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
802
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
674
803
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
675
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
804
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
676
805
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
677
- const langRows = this.db.prepare(
678
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
679
- ).all();
806
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
680
807
  const byLang = {};
681
808
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
682
- const kindRows = this.db.prepare(
683
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
684
- ).all();
809
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
685
810
  const byKind = {};
686
811
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
687
812
  return {
@@ -697,17 +822,34 @@ var IndexStore = class {
697
822
  }
698
823
  setLastIndexed(ts2) {
699
824
  this.runWithRetry(() => {
700
- this.db.prepare(
701
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
702
- ).run(String(ts2));
825
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
826
+ });
827
+ }
828
+ getMetadata(key) {
829
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
830
+ return rows[0]?.value;
831
+ }
832
+ setMetadata(key, value) {
833
+ this.runWithRetry(() => {
834
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
703
835
  });
704
836
  }
705
837
  clearAll() {
706
838
  this.runWithRetry(() => {
707
- this.db.exec("DELETE FROM symbols");
708
- this.db.exec("DELETE FROM files");
709
- this.db.exec("DELETE FROM refs");
710
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
839
+ this.db.exec("BEGIN IMMEDIATE");
840
+ try {
841
+ this.db.exec("DROP TABLE IF EXISTS refs");
842
+ this.db.exec("DROP TABLE IF EXISTS symbols");
843
+ this.db.exec("DROP TABLE IF EXISTS files");
844
+ this.db.exec("DROP TABLE IF EXISTS metadata");
845
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
846
+ this.db.exec("COMMIT");
847
+ this.stmtCache.clear();
848
+ this.initSchema();
849
+ } catch (err) {
850
+ this.db.exec("ROLLBACK");
851
+ throw err;
852
+ }
711
853
  });
712
854
  }
713
855
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -788,7 +930,7 @@ var IndexStore = class {
788
930
  ).run(...options.deleteForFiles);
789
931
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
790
932
  }
791
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
933
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
792
934
  let nextId = (maxRows[0]?.m ?? 0) + 1;
793
935
  const symStmt = this.db.prepare(
794
936
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -798,6 +940,7 @@ var IndexStore = class {
798
940
  const allInserted = [];
799
941
  const refsToInsert = [];
800
942
  for (const entry of entries) {
943
+ const insertedForEntry = [];
801
944
  for (const s of entry.symbols) {
802
945
  const id = nextId++;
803
946
  symStmt.run(
@@ -815,9 +958,11 @@ var IndexStore = class {
815
958
  s.file
816
959
  );
817
960
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
818
- allInserted.push({ ...s, id });
961
+ const inserted = { ...s, id };
962
+ allInserted.push(inserted);
963
+ insertedForEntry.push(inserted);
819
964
  }
820
- for (const r of entry.refs) refsToInsert.push(r);
965
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
821
966
  }
822
967
  if (refsToInsert.length > 0) {
823
968
  const refStmt = this.db.prepare(
@@ -855,9 +1000,7 @@ var IndexStore = class {
855
1000
  */
856
1001
  deleteRefsForFile(file) {
857
1002
  this.runWithRetry(() => {
858
- this.db.prepare(
859
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
860
- ).run(file);
1003
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
861
1004
  });
862
1005
  }
863
1006
  /**
@@ -898,9 +1041,7 @@ var IndexStore = class {
898
1041
  * Find all references FROM a given symbol (what does this symbol call/use?).
899
1042
  */
900
1043
  findRefsFrom(symbolId) {
901
- return this.db.prepare(
902
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
903
- ).all(symbolId).map((r) => ({
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) => ({
904
1045
  id: r.id,
905
1046
  fromId: r.from_id,
906
1047
  toName: r.to_name,
@@ -917,6 +1058,356 @@ var IndexStore = class {
917
1058
  return 0;
918
1059
  }
919
1060
  }
1061
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
1062
+ /**
1063
+ * Derive a monorepo package name from an absolute file path.
1064
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
1065
+ * Returns `undefined` for files outside any recognisable package structure.
1066
+ */
1067
+ static derivePackage(filePath) {
1068
+ const f = filePath.replace(/\\/g, "/");
1069
+ const pkgsIdx = f.indexOf("/packages/");
1070
+ if (pkgsIdx !== -1) {
1071
+ const rest = f.slice(pkgsIdx + "/packages/".length);
1072
+ const seg = rest.split("/")[0];
1073
+ return seg ? `@wrongstack/${seg}` : void 0;
1074
+ }
1075
+ const appsIdx = f.indexOf("/apps/");
1076
+ if (appsIdx !== -1) {
1077
+ const rest = f.slice(appsIdx + "/apps/".length);
1078
+ const seg = rest.split("/")[0];
1079
+ return seg ? `app:${seg}` : void 0;
1080
+ }
1081
+ return void 0;
1082
+ }
1083
+ static packageFromImport(moduleName) {
1084
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
1085
+ const parts = moduleName.split("/");
1086
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
1087
+ }
1088
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
1089
+ if (!moduleName.startsWith(".")) return void 0;
1090
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
1091
+ const absolute = path.posix.normalize(
1092
+ path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
1093
+ );
1094
+ const extension = path.posix.extname(absolute);
1095
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
1096
+ const candidates = [
1097
+ absolute,
1098
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
1099
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
1100
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
1101
+ ];
1102
+ const indexedByPortablePath = new Map(
1103
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
1104
+ );
1105
+ for (const candidate of candidates) {
1106
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
1107
+ if (indexed) return indexed;
1108
+ }
1109
+ return void 0;
1110
+ }
1111
+ /**
1112
+ * Package-level graph: each workspace package is a node; edges are derived
1113
+ * from cross-package symbol references (a symbol in package A references a
1114
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
1115
+ */
1116
+ getPackageGraph() {
1117
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
1118
+ const pkgNodes = /* @__PURE__ */ new Map();
1119
+ 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);
1125
+ const node = pkgNodes.get(pkg);
1126
+ if (node) {
1127
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
1128
+ } else {
1129
+ pkgNodes.set(pkg, {
1130
+ id: `pkg:${pkg}`,
1131
+ label: pkg,
1132
+ kind: "package",
1133
+ package: pkg,
1134
+ symbolCount: 1,
1135
+ fileCount: 0
1136
+ });
1137
+ }
1138
+ }
1139
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1140
+ for (const { file } of files) {
1141
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1142
+ const node = pkgNodes.get(pkg);
1143
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1144
+ }
1145
+ const refRows = this.db.prepare(
1146
+ `SELECT r.from_id, r.to_id, r.call_type
1147
+ FROM refs r
1148
+ WHERE r.to_id IS NOT NULL`
1149
+ ).all();
1150
+ const edgeMap = /* @__PURE__ */ new Map();
1151
+ 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;
1156
+ const key = `${fromPkg}\0${toPkg}`;
1157
+ let e = edgeMap.get(key);
1158
+ if (!e) {
1159
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1160
+ edgeMap.set(key, e);
1161
+ }
1162
+ e.weight++;
1163
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1164
+ }
1165
+ const importRows = this.db.prepare(
1166
+ `SELECT r.from_id, r.to_name
1167
+ FROM refs r
1168
+ WHERE r.call_type = 'import'`
1169
+ ).all();
1170
+ for (const r of importRows) {
1171
+ const fromPkg = symbolToPkg.get(r.from_id);
1172
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
1173
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1174
+ const key = `${fromPkg}\0${toPkg}`;
1175
+ let edge = edgeMap.get(key);
1176
+ if (!edge) {
1177
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1178
+ edgeMap.set(key, edge);
1179
+ }
1180
+ edge.weight++;
1181
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1182
+ }
1183
+ const edges = [];
1184
+ for (const [key, e] of edgeMap) {
1185
+ const [source, target] = key.split("\0");
1186
+ let bestType = "call";
1187
+ let bestCount = 0;
1188
+ for (const [t, c] of e.types) {
1189
+ if (c > bestCount) {
1190
+ bestType = t;
1191
+ bestCount = c;
1192
+ }
1193
+ }
1194
+ edges.push({
1195
+ source: `pkg:${source}`,
1196
+ target: `pkg:${target}`,
1197
+ weight: e.weight,
1198
+ refType: bestType
1199
+ });
1200
+ }
1201
+ return { nodes: [...pkgNodes.values()], edges };
1202
+ }
1203
+ /**
1204
+ * File-level graph for a single package: each file is a node; edges are
1205
+ * derived from cross-file symbol references within the package.
1206
+ */
1207
+ getFileGraph(packageFilter) {
1208
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
1209
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
1210
+ const localFiles = new Set(pkgFilePaths);
1211
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
1212
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
1213
+ const pkgSyms = this.db.prepare(
1214
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
1215
+ ).all(...pkgFilePaths);
1216
+ const fileNodes = /* @__PURE__ */ new Map();
1217
+ const symToFile = /* @__PURE__ */ new Map();
1218
+ const fileStats = /* @__PURE__ */ new Map();
1219
+ for (const s of pkgSyms) {
1220
+ symToFile.set(s.id, s.file);
1221
+ const current = fileStats.get(s.file);
1222
+ fileStats.set(s.file, {
1223
+ count: (current?.count ?? 0) + 1,
1224
+ lang: current?.lang ?? s.lang
1225
+ });
1226
+ }
1227
+ const ensureFileNode = (file) => {
1228
+ if (fileNodes.has(file)) return;
1229
+ const stats = fileStats.get(file);
1230
+ fileNodes.set(file, {
1231
+ id: `file:${file}`,
1232
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
1233
+ kind: "file",
1234
+ package: _IndexStore.derivePackage(file) ?? "(root)",
1235
+ file,
1236
+ symbolCount: stats?.count ?? 0,
1237
+ lang: stats?.lang,
1238
+ external: !localFiles.has(file)
1239
+ });
1240
+ };
1241
+ for (const file of localFiles) {
1242
+ ensureFileNode(file);
1243
+ }
1244
+ 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
1247
+ FROM refs r
1248
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
1249
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
1250
+ AND r.to_id IS NOT NULL`
1251
+ ).all(...pkgFilePaths, ...pkgFilePaths);
1252
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
1253
+ const crossRefIds = /* @__PURE__ */ new Set();
1254
+ for (const r of refRows) {
1255
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
1256
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
1257
+ }
1258
+ if (crossRefIds.size > 0) {
1259
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
1260
+ const extras = this.db.prepare(
1261
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
1262
+ ).all(...crossRefIds);
1263
+ for (const x of extras) {
1264
+ symToFile.set(x.id, x.file);
1265
+ if (!fileStats.has(x.file)) {
1266
+ fileStats.set(x.file, { count: 0, lang: "ts" });
1267
+ }
1268
+ }
1269
+ }
1270
+ const edgeMap = /* @__PURE__ */ new Map();
1271
+ for (const r of refRows) {
1272
+ if (r.call_type === "import") continue;
1273
+ const fromFile = symToFile.get(r.from_id);
1274
+ const toFile = symToFile.get(r.to_id);
1275
+ if (!fromFile || !toFile || fromFile === toFile) continue;
1276
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
1277
+ ensureFileNode(fromFile);
1278
+ ensureFileNode(toFile);
1279
+ const key = `${fromFile}\0${toFile}`;
1280
+ let e = edgeMap.get(key);
1281
+ if (!e) {
1282
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1283
+ edgeMap.set(key, e);
1284
+ }
1285
+ e.weight++;
1286
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1287
+ }
1288
+ const importRows = this.db.prepare(
1289
+ `SELECT r.from_id, r.to_name
1290
+ FROM refs r
1291
+ WHERE r.call_type = 'import'
1292
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
1293
+ ).all(...pkgFilePaths);
1294
+ for (const r of importRows) {
1295
+ const fromFile = symToFile.get(r.from_id);
1296
+ if (!fromFile || !localFiles.has(fromFile)) continue;
1297
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
1298
+ if (!toFile || fromFile === toFile) continue;
1299
+ ensureFileNode(fromFile);
1300
+ ensureFileNode(toFile);
1301
+ const key = `${fromFile}\0${toFile}`;
1302
+ let edge = edgeMap.get(key);
1303
+ if (!edge) {
1304
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1305
+ edgeMap.set(key, edge);
1306
+ }
1307
+ edge.weight++;
1308
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1309
+ }
1310
+ const edges = [];
1311
+ for (const [key, e] of edgeMap) {
1312
+ const [source, target] = key.split("\0");
1313
+ let bestType = "call";
1314
+ let bestCount = 0;
1315
+ for (const [t, c] of e.types) {
1316
+ if (c > bestCount) {
1317
+ bestType = t;
1318
+ bestCount = c;
1319
+ }
1320
+ }
1321
+ edges.push({
1322
+ source: `file:${source}`,
1323
+ target: `file:${target}`,
1324
+ weight: e.weight,
1325
+ refType: bestType
1326
+ });
1327
+ }
1328
+ return { nodes: [...fileNodes.values()], edges };
1329
+ }
1330
+ /**
1331
+ * Symbol-level graph for a single file: each symbol is a node; edges are
1332
+ * derived from intra-file and cross-file symbol references (who calls whom).
1333
+ */
1334
+ getSymbolGraph(fileFilter) {
1335
+ const syms = this.db.prepare(
1336
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
1337
+ ).all(fileFilter);
1338
+ if (syms.length === 0) return { nodes: [], edges: [] };
1339
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
1340
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
1341
+ const toGraphNode = (s) => ({
1342
+ id: `sym:${s.id}`,
1343
+ label: s.name,
1344
+ kind: "symbol",
1345
+ symbolId: s.id,
1346
+ symbolKind: s.kind,
1347
+ file: s.file,
1348
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
1349
+ lang: s.lang,
1350
+ line: s.line,
1351
+ signature: s.signature,
1352
+ scope: s.scope,
1353
+ external: s.file !== fileFilter
1354
+ });
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 = ?)`
1361
+ ).all(fileFilter, fileFilter);
1362
+ const edgeMap = /* @__PURE__ */ new Map();
1363
+ 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;
1366
+ relatedIds.add(r.from_id);
1367
+ relatedIds.add(r.to_id);
1368
+ const key = `${r.from_id}\0${r.to_id}`;
1369
+ let e = edgeMap.get(key);
1370
+ if (!e) {
1371
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1372
+ edgeMap.set(key, e);
1373
+ }
1374
+ e.weight++;
1375
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1376
+ }
1377
+ const edges = [];
1378
+ for (const [key, e] of edgeMap) {
1379
+ const [fromId, toId] = key.split("\0").map(Number);
1380
+ let bestType = "call";
1381
+ let bestCount = 0;
1382
+ for (const [t, c] of e.types) {
1383
+ if (c > bestCount) {
1384
+ bestType = t;
1385
+ bestCount = c;
1386
+ }
1387
+ }
1388
+ edges.push({
1389
+ source: `sym:${fromId}`,
1390
+ target: `sym:${toId}`,
1391
+ weight: e.weight,
1392
+ refType: bestType
1393
+ });
1394
+ }
1395
+ const loadedIds = new Set(syms.map((s) => s.id));
1396
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
1397
+ if (missingIds.length > 0) {
1398
+ const placeholders = missingIds.map(() => "?").join(",");
1399
+ const extras = this.db.prepare(
1400
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
1401
+ ).all(...missingIds);
1402
+ for (const s of extras) symById.set(s.id, s);
1403
+ }
1404
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
1405
+ const aExternal = a.file === fileFilter ? 0 : 1;
1406
+ const bExternal = b.file === fileFilter ? 0 : 1;
1407
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
1408
+ }).map(toGraphNode);
1409
+ return { nodes, edges };
1410
+ }
920
1411
  close() {
921
1412
  this.stmtCache.clear();
922
1413
  try {
@@ -980,8 +1471,7 @@ function extToLang(ext) {
980
1471
  return null;
981
1472
  }
982
1473
  }
983
- function getSignature(node, sourceFile) {
984
- const printer = ts.createPrinter({});
1474
+ function getSignature(printer, node, sourceFile) {
985
1475
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
986
1476
  return raw.replace(/\s+/g, " ").slice(0, 500);
987
1477
  }
@@ -1000,20 +1490,14 @@ function getJsDoc(node, sourceFile) {
1000
1490
  }
1001
1491
  return "";
1002
1492
  }
1003
- function buildScope(node) {
1004
- const parts = [];
1005
- let current = node.parent;
1006
- while (current) {
1007
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
1008
- parts.unshift(current.name?.text ?? "Anon");
1009
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
1010
- if (current.name && ts.isIdentifier(current.name)) {
1011
- parts.unshift(current.name.text);
1012
- }
1493
+ function pushScopeName(node, parts) {
1494
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
1495
+ parts.push(node.name?.text ?? "Anon");
1496
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
1497
+ if (node.name && ts.isIdentifier(node.name)) {
1498
+ parts.push(node.name.text);
1013
1499
  }
1014
- current = current.parent;
1015
1500
  }
1016
- return parts.join(".");
1017
1501
  }
1018
1502
  function parseSymbols(opts) {
1019
1503
  const { file, content, lang } = opts;
@@ -1024,41 +1508,39 @@ function parseSymbols(opts) {
1024
1508
  return { file, lang, symbols: [], mtimeMs: Date.now() };
1025
1509
  }
1026
1510
  const symbols = [];
1027
- function visit(node) {
1511
+ const refs = [];
1512
+ const printer = ts.createPrinter({});
1513
+ function visit(node, funcDepth, scopeParts) {
1028
1514
  const kind = kindOf(node);
1029
1515
  if (kind) {
1030
- const nameNode = node.name;
1031
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
1032
- const name = nameNode.text;
1033
- const pos = nameNode.getStart(sourceFile);
1034
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
1035
- const scope = buildScope(node);
1036
- const signature = getSignature(node, sourceFile);
1037
- const docComment = getJsDoc(node, sourceFile);
1038
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
1039
- symbols.push({
1040
- id: 0,
1041
- lang,
1042
- kind,
1043
- name,
1044
- file,
1045
- line: line + 1,
1046
- col: character,
1047
- signature,
1048
- docComment,
1049
- scope,
1050
- text
1051
- });
1516
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
1517
+ } else {
1518
+ const nameNode = node.name;
1519
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
1520
+ return;
1521
+ }
1522
+ const name = nameNode.text;
1523
+ const pos2 = nameNode.getStart(sourceFile);
1524
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
1525
+ const scope = scopeParts.join(".");
1526
+ const signature = getSignature(printer, node, sourceFile);
1527
+ const docComment = getJsDoc(node, sourceFile);
1528
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
1529
+ symbols.push({
1530
+ id: 0,
1531
+ lang,
1532
+ kind,
1533
+ name,
1534
+ file,
1535
+ line: line2 + 1,
1536
+ col: character,
1537
+ signature,
1538
+ docComment,
1539
+ scope,
1540
+ text
1541
+ });
1542
+ }
1052
1543
  }
1053
- ts.forEachChild(node, visit);
1054
- }
1055
- visit(sourceFile);
1056
- const refs = extractRefs(sourceFile);
1057
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
1058
- }
1059
- function extractRefs(sourceFile) {
1060
- const refs = [];
1061
- function visit(node) {
1062
1544
  const pos = node.getStart(sourceFile);
1063
1545
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
1064
1546
  const lineNum = line + 1;
@@ -1083,10 +1565,14 @@ function extractRefs(sourceFile) {
1083
1565
  const moduleName = getModuleName(node);
1084
1566
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
1085
1567
  }
1086
- ts.forEachChild(node, visit);
1568
+ const scopeIdx = scopeParts.length;
1569
+ pushScopeName(node, scopeParts);
1570
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
1571
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
1572
+ scopeParts.length = scopeIdx;
1087
1573
  }
1088
- visit(sourceFile);
1089
- return deduplicateRefs(refs);
1574
+ visit(sourceFile, 0, []);
1575
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
1090
1576
  }
1091
1577
  function getTypeName(name) {
1092
1578
  if (ts.isIdentifier(name)) return name.text;
@@ -1994,9 +2480,9 @@ function parseSymbols5(opts) {
1994
2480
  function regexParse2(opts) {
1995
2481
  const { file, content, lang } = opts;
1996
2482
  const symbols = [];
1997
- const basename2 = path6.basename(file).toLowerCase();
1998
- const isPackageJson = basename2 === "package.json";
1999
- const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2483
+ const basename3 = path6.basename(file).toLowerCase();
2484
+ const isPackageJson = basename3 === "package.json";
2485
+ const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
2000
2486
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2001
2487
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2002
2488
  const lines = content.split("\n");
@@ -2391,16 +2877,14 @@ async function loadGitignoreMatcher(projectRoot) {
2391
2877
 
2392
2878
  // src/codebase-index/indexer.ts
2393
2879
  var YIELD_EVERY_N = 50;
2394
- var PARALLEL_BATCH = 20;
2880
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
2395
2881
  function yieldEventLoop() {
2396
2882
  return new Promise((resolve2) => setImmediate(resolve2));
2397
2883
  }
2398
2884
  function throwIfAborted(signal) {
2399
2885
  if (!signal?.aborted) return;
2400
2886
  if (signal.reason instanceof Error) throw signal.reason;
2401
- throw new Error(
2402
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
2403
- );
2887
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
2404
2888
  }
2405
2889
  function isAbortError(err) {
2406
2890
  return err instanceof DOMException && err.name === "AbortError";
@@ -2416,8 +2900,20 @@ var DEFAULT_IGNORE = [
2416
2900
  "__snapshots__",
2417
2901
  ".nyc_output"
2418
2902
  ];
2903
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2904
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2905
+ function isWithinProject(projectRoot, file) {
2906
+ const rel = path8.relative(projectRoot, file);
2907
+ return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
2908
+ }
2909
+ function isMissingPathError(err) {
2910
+ const code = err?.code;
2911
+ return code === "ENOENT" || code === "ENOTDIR";
2912
+ }
2419
2913
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2420
2914
  const results = [];
2915
+ const errors = [];
2916
+ let complete = true;
2421
2917
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2422
2918
  const globs = [
2423
2919
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -2441,7 +2937,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2441
2937
  let entries;
2442
2938
  try {
2443
2939
  entries = await fs7.readdir(dir, { withFileTypes: true });
2444
- } catch {
2940
+ } catch (err) {
2941
+ complete = false;
2942
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
2445
2943
  return;
2446
2944
  }
2447
2945
  dirCount++;
@@ -2453,8 +2951,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2453
2951
  if (isGitIgnored(rel, true)) continue;
2454
2952
  await walk(full);
2455
2953
  } else if (e.isFile()) {
2456
- if (isGitIgnored(rel, false)) continue;
2457
- const ext = path8.extname(e.name);
2954
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2955
+ const ext = path8.extname(e.name).toLowerCase();
2458
2956
  for (const { ext: extName, pat } of globs) {
2459
2957
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2460
2958
  results.push(full);
@@ -2465,7 +2963,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2465
2963
  }
2466
2964
  };
2467
2965
  await walk(projectRoot);
2468
- return results;
2966
+ return { files: results, complete, errors };
2469
2967
  }
2470
2968
  async function parseFile(file, content, lang) {
2471
2969
  switch (lang) {
@@ -2488,6 +2986,26 @@ async function parseFile(file, content, lang) {
2488
2986
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2489
2987
  }
2490
2988
  }
2989
+ function assignRefsToSymbols2(refs, symbols) {
2990
+ if (refs.length === 0 || symbols.length === 0) return [];
2991
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
2992
+ const seen = /* @__PURE__ */ new Set();
2993
+ const assigned = [];
2994
+ for (const ref of refs) {
2995
+ let owner;
2996
+ for (const symbol of ordered) {
2997
+ if (symbol.line > ref.line) break;
2998
+ owner = symbol;
2999
+ }
3000
+ if (!owner && ref.callType === "import") owner = ordered[0];
3001
+ if (!owner || owner.id <= 0) continue;
3002
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
3003
+ if (seen.has(key)) continue;
3004
+ seen.add(key);
3005
+ assigned.push({ ...ref, fromId: owner.id });
3006
+ }
3007
+ return assigned;
3008
+ }
2491
3009
  async function runIndexer(_ctx, opts) {
2492
3010
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
2493
3011
  try {
@@ -2500,7 +3018,9 @@ async function runIndexer(_ctx, opts) {
2500
3018
  }
2501
3019
  }
2502
3020
  async function runIndexerWithStore(store, opts) {
2503
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
3021
+ const { projectRoot, langs, ignore = [], signal } = opts;
3022
+ const relationGraphVersion = "2";
3023
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
2504
3024
  const startMs = Date.now();
2505
3025
  const errors = [];
2506
3026
  const langStats = {};
@@ -2509,10 +3029,18 @@ async function runIndexerWithStore(store, opts) {
2509
3029
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2510
3030
  let files;
2511
3031
  let discoveredFiles = null;
3032
+ let discoveryComplete = true;
2512
3033
  if (opts.files && opts.files.length > 0) {
2513
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path8.relative(projectRoot, f).replace(/\\/g, "/"), false));
3034
+ files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3035
+ 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);
3038
+ });
2514
3039
  } else {
2515
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3040
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3041
+ files = discovery.files;
3042
+ errors.push(...discovery.errors);
3043
+ discoveryComplete = discovery.complete;
2516
3044
  discoveredFiles = new Set(files);
2517
3045
  }
2518
3046
  if (langs && langs.length > 0) {
@@ -2527,46 +3055,79 @@ async function runIndexerWithStore(store, opts) {
2527
3055
  if (!force) {
2528
3056
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
2529
3057
  }
3058
+ let filesSinceLastYield = 0;
2530
3059
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
2531
3060
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
2532
3061
  const batchFiles = files.slice(batchStart, batchEnd);
2533
3062
  opts.onProgress?.(batchEnd, files.length);
2534
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
3063
+ filesSinceLastYield += batchFiles.length;
3064
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
3065
+ filesSinceLastYield = 0;
2535
3066
  await yieldEventLoop();
2536
3067
  throwIfAborted(signal);
2537
3068
  }
2538
3069
  const statOpts = signal ? { signal } : {};
2539
3070
  const statReadParse = await Promise.allSettled(
2540
- batchFiles.map(async (file) => {
2541
- let stat2;
2542
- try {
2543
- stat2 = await fs7.stat(file, statOpts);
2544
- } catch (e) {
2545
- if (isAbortError(e)) throw e;
2546
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
2547
- }
2548
- if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
2549
- const lang = detectLang(file);
2550
- if (!lang) return { file, stat: stat2, lang: "", parsed: null };
2551
- const meta = existingMeta.get(file);
2552
- if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
2553
- return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
2554
- }
2555
- let content;
2556
- try {
2557
- content = await fs7.readFile(file, { encoding: "utf8", signal });
2558
- } catch (e) {
2559
- if (isAbortError(e)) throw e;
2560
- return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
2561
- }
2562
- let parsed;
2563
- try {
2564
- parsed = await parseFile(file, content, lang);
2565
- } catch (e) {
2566
- return { file, stat: stat2, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
3071
+ batchFiles.map(
3072
+ async (file) => {
3073
+ let stat2;
3074
+ try {
3075
+ stat2 = await fs7.stat(file, statOpts);
3076
+ } catch (e) {
3077
+ if (isAbortError(e)) throw e;
3078
+ return {
3079
+ file,
3080
+ stat: null,
3081
+ lang: "",
3082
+ parsed: null,
3083
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
3084
+ missing: isMissingPathError(e)
3085
+ };
3086
+ }
3087
+ if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
3088
+ const lang = detectLang(file);
3089
+ if (!lang) return { file, stat: stat2, lang: "", parsed: null };
3090
+ if (stat2.size > MAX_INDEX_FILE_BYTES) {
3091
+ return {
3092
+ file,
3093
+ stat: stat2,
3094
+ lang,
3095
+ parsed: null,
3096
+ error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
3097
+ };
3098
+ }
3099
+ const meta = existingMeta.get(file);
3100
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
3101
+ return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
3102
+ }
3103
+ let content;
3104
+ try {
3105
+ content = await fs7.readFile(file, { encoding: "utf8", signal });
3106
+ } catch (e) {
3107
+ if (isAbortError(e)) throw e;
3108
+ return {
3109
+ file,
3110
+ stat: stat2,
3111
+ lang,
3112
+ parsed: null,
3113
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
3114
+ };
3115
+ }
3116
+ let parsed;
3117
+ try {
3118
+ parsed = await parseFile(file, content, lang);
3119
+ } catch (e) {
3120
+ return {
3121
+ file,
3122
+ stat: stat2,
3123
+ lang,
3124
+ parsed: null,
3125
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
3126
+ };
3127
+ }
3128
+ return { file, stat: stat2, lang, parsed, content };
2567
3129
  }
2568
- return { file, stat: stat2, lang, parsed, content };
2569
- })
3130
+ )
2570
3131
  );
2571
3132
  const batchEntries = [];
2572
3133
  const deleteForFiles = [];
@@ -2581,8 +3142,8 @@ async function runIndexerWithStore(store, opts) {
2581
3142
  }
2582
3143
  const result = settled.value;
2583
3144
  if (result.error) {
2584
- if (result.stat) store.deleteFile(file);
2585
- if (result.error.includes("error:")) errors.push(result.error);
3145
+ if (result.missing) store.deleteFile(file);
3146
+ errors.push(`${file}: ${result.error}`);
2586
3147
  continue;
2587
3148
  }
2588
3149
  const { stat: stat2, lang, parsed } = result;
@@ -2606,6 +3167,8 @@ async function runIndexerWithStore(store, opts) {
2606
3167
  continue;
2607
3168
  }
2608
3169
  if (parsed.symbols.length === 0) {
3170
+ store.deleteRefsForFile(file);
3171
+ store.deleteSymbolsForFile(file);
2609
3172
  store.upsertFile({
2610
3173
  file,
2611
3174
  lang,
@@ -2616,15 +3179,11 @@ async function runIndexerWithStore(store, opts) {
2616
3179
  filesIndexed++;
2617
3180
  continue;
2618
3181
  }
2619
- const refs = [];
2620
- if (parsed.refs && parsed.refs.length > 0) {
2621
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
2622
- }
2623
3182
  batchEntries.push({
2624
3183
  file,
2625
3184
  lang,
2626
3185
  symbols: parsed.symbols,
2627
- refs,
3186
+ refs: parsed.refs ?? [],
2628
3187
  mtimeMs: Math.floor(stat2.mtimeMs),
2629
3188
  symbolCount: parsed.symbols.length
2630
3189
  });
@@ -2632,34 +3191,12 @@ async function runIndexerWithStore(store, opts) {
2632
3191
  }
2633
3192
  if (batchEntries.length > 0) {
2634
3193
  try {
2635
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
2636
- let cursor = 0;
3194
+ store.commitBatch(batchEntries, { deleteForFiles });
2637
3195
  for (const entry of batchEntries) {
2638
3196
  const count = entry.symbols.length;
2639
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
2640
- cursor += count;
2641
3197
  symbolsIndexed += count;
2642
3198
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
2643
3199
  filesIndexed++;
2644
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2645
- const refsByLine = /* @__PURE__ */ new Map();
2646
- for (let i = 0; i < symbolsWithIds.length; i++) {
2647
- const sym = symbolsWithIds[i];
2648
- let arr = refsByLine.get(sym.line);
2649
- if (!arr) {
2650
- arr = [];
2651
- refsByLine.set(sym.line, arr);
2652
- }
2653
- arr.push(i);
2654
- }
2655
- for (const ref of entry.refs) {
2656
- const indices = refsByLine.get(ref.line);
2657
- if (indices && indices.length > 0) {
2658
- const idx = indices.shift();
2659
- ref.fromId = symbolsWithIds[idx].id;
2660
- }
2661
- }
2662
- }
2663
3200
  }
2664
3201
  } catch (err) {
2665
3202
  const message = err instanceof Error ? err.message : String(err);
@@ -2673,23 +3210,7 @@ async function runIndexerWithStore(store, opts) {
2673
3210
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
2674
3211
  filesIndexed++;
2675
3212
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2676
- const refsByLine = /* @__PURE__ */ new Map();
2677
- for (const sym of symbolsWithIds) {
2678
- let arr = refsByLine.get(sym.line);
2679
- if (!arr) {
2680
- arr = [];
2681
- refsByLine.set(sym.line, arr);
2682
- }
2683
- arr.push(sym);
2684
- }
2685
- const fallbackBatch = [];
2686
- for (const ref of entry.refs) {
2687
- const syms = refsByLine.get(ref.line);
2688
- if (syms && syms.length > 0) {
2689
- const sym = syms.shift();
2690
- fallbackBatch.push({ ...ref, fromId: sym.id });
2691
- }
2692
- }
3213
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
2693
3214
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
2694
3215
  }
2695
3216
  store.upsertFile({
@@ -2700,19 +3221,23 @@ async function runIndexerWithStore(store, opts) {
2700
3221
  lastIndexed: Date.now()
2701
3222
  });
2702
3223
  } catch (innerErr) {
2703
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
3224
+ errors.push(
3225
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
3226
+ );
2704
3227
  }
2705
3228
  }
2706
3229
  }
2707
3230
  }
2708
3231
  }
2709
- if (discoveredFiles) {
3232
+ if (discoveredFiles && discoveryComplete) {
2710
3233
  for (const [file_] of existingMeta) {
2711
3234
  if (!discoveredFiles.has(file_)) {
2712
3235
  store.deleteFile(file_);
2713
3236
  }
2714
3237
  }
2715
3238
  }
3239
+ store.resolveRefs();
3240
+ store.setMetadata("relation_graph_version", relationGraphVersion);
2716
3241
  const durationMs = Date.now() - startMs;
2717
3242
  store.setLastIndexed(Date.now());
2718
3243
  return {
@@ -2748,7 +3273,7 @@ async function indexService(args, hooks = {}) {
2748
3273
  });
2749
3274
  }
2750
3275
  function searchService(args) {
2751
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3276
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
2752
3277
  try {
2753
3278
  return store.searchRanked(
2754
3279
  args.query,
@@ -2761,15 +3286,15 @@ function searchService(args) {
2761
3286
  args.limit
2762
3287
  );
2763
3288
  } finally {
2764
- store.close();
3289
+ indexStorePool.release(store);
2765
3290
  }
2766
3291
  }
2767
3292
  function statsService(args) {
2768
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3293
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
2769
3294
  try {
2770
3295
  return store.getStats();
2771
3296
  } finally {
2772
- store.close();
3297
+ indexStorePool.release(store);
2773
3298
  }
2774
3299
  }
2775
3300
 
@@ -2815,7 +3340,8 @@ port.on("message", (msg) => {
2815
3340
  type: "response",
2816
3341
  id: msg.id,
2817
3342
  ok: false,
2818
- error: err instanceof Error ? err.message : String(err)
3343
+ error: err instanceof Error ? err.message : String(err),
3344
+ errorName: err instanceof Error ? err.name : void 0
2819
3345
  });
2820
3346
  } catch {
2821
3347
  }