@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
@@ -92,7 +92,7 @@ import * as path from "node:path";
92
92
  import { resolveWstackPaths } from "@wrongstack/core";
93
93
 
94
94
  // src/codebase-index/schema.ts
95
- var SCHEMA_VERSION = 2;
95
+ var SCHEMA_VERSION = 3;
96
96
 
97
97
  // src/codebase-index/lsp-kind.ts
98
98
  function lspKindToInternalKind(k) {
@@ -164,7 +164,7 @@ function tokenise(text) {
164
164
  return sanitised.toLowerCase().split(" ").filter(Boolean);
165
165
  }
166
166
  function splitName(name) {
167
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
167
+ 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();
168
168
  }
169
169
  function buildIndexableText(name, signature, docComment) {
170
170
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -190,14 +190,12 @@ function buildBm25Index(docs) {
190
190
  return new Bm25Index(documents, df, N, avgLen);
191
191
  }
192
192
  var Bm25Index = class {
193
- constructor(documents, df, N, avgLen) {
193
+ constructor(documents, _df, N, avgLen) {
194
194
  this.documents = documents;
195
- this.df = df;
196
195
  this.N = N;
197
196
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
198
197
  }
199
198
  documents;
200
- df;
201
199
  N;
202
200
  safeAvgLen;
203
201
  score(query, filter) {
@@ -210,10 +208,13 @@ var Bm25Index = class {
210
208
  for (const qTerm of qTokens) {
211
209
  let tf = 0;
212
210
  for (const t of doc.tokens) {
213
- if (t === qTerm) tf++;
211
+ if (t.startsWith(qTerm)) tf++;
214
212
  }
215
213
  if (tf === 0) continue;
216
- const dfVal = this.df[qTerm] ?? 0;
214
+ let dfVal = 0;
215
+ for (const candidate of this.documents) {
216
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
217
+ }
217
218
  if (dfVal === 0) continue;
218
219
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
219
220
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -246,6 +247,29 @@ var Bm25Index = class {
246
247
 
247
248
  // src/codebase-index/writer.ts
248
249
  var DB_FILE = "index.db";
250
+ function escapeLike(value) {
251
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
252
+ }
253
+ function assignRefsToSymbols(refs, symbols) {
254
+ if (refs.length === 0 || symbols.length === 0) return [];
255
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
256
+ const seen = /* @__PURE__ */ new Set();
257
+ const assigned = [];
258
+ for (const ref of refs) {
259
+ let owner;
260
+ for (const symbol of ordered) {
261
+ if (symbol.line > ref.line) break;
262
+ owner = symbol;
263
+ }
264
+ if (!owner && ref.callType === "import") owner = ordered[0];
265
+ if (!owner || owner.id <= 0) continue;
266
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
267
+ if (seen.has(key)) continue;
268
+ seen.add(key);
269
+ assigned.push({ ...ref, fromId: owner.id });
270
+ }
271
+ return assigned;
272
+ }
249
273
  function resolveIndexDir(projectRoot, override) {
250
274
  return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
251
275
  }
@@ -253,6 +277,53 @@ function codebaseIndexDirOverride(ctx) {
253
277
  const v = ctx.meta?.["codebaseIndexDir"];
254
278
  return typeof v === "string" ? v : void 0;
255
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();
256
327
  var warningSilenced = false;
257
328
  function silenceSqliteExperimentalWarning() {
258
329
  if (warningSilenced) return;
@@ -299,7 +370,7 @@ function sleepSync(ms) {
299
370
  } catch {
300
371
  }
301
372
  }
302
- var IndexStore = class {
373
+ var IndexStore = class _IndexStore {
303
374
  db;
304
375
  /** Absolute path to this project's index directory. */
305
376
  indexDir;
@@ -351,10 +422,7 @@ var IndexStore = class {
351
422
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
352
423
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
353
424
  }
354
- const delay = Math.min(
355
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
356
- LOCK_RETRY_MAX_DELAY_MS
357
- );
425
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
358
426
  sleepSync(delay);
359
427
  }
360
428
  }
@@ -436,8 +504,24 @@ var IndexStore = class {
436
504
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
437
505
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
438
506
  try {
439
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
507
+ this.db.exec(
508
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
509
+ );
440
510
  this.ftsAvailable = true;
511
+ const symbolCount = Number(
512
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
513
+ );
514
+ const ftsCount = Number(
515
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
516
+ );
517
+ if (symbolCount !== ftsCount) {
518
+ this.db.exec("DELETE FROM symbols_fts");
519
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
520
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
521
+ for (const row of rows) {
522
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
523
+ }
524
+ }
441
525
  } catch {
442
526
  this.ftsAvailable = false;
443
527
  }
@@ -457,7 +541,7 @@ var IndexStore = class {
457
541
  return this.runWithRetry(() => {
458
542
  this.db.exec("BEGIN IMMEDIATE");
459
543
  try {
460
- 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();
461
545
  let nextId = (maxRows[0]?.m ?? 0) + 1;
462
546
  const stmt = this.db.prepare(
463
547
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -495,7 +579,9 @@ var IndexStore = class {
495
579
  deleteSymbolsForFile(file) {
496
580
  this.runWithRetry(() => {
497
581
  if (this.ftsAvailable) {
498
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
582
+ this.stmt(
583
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
584
+ ).run(file);
499
585
  }
500
586
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
501
587
  });
@@ -507,9 +593,21 @@ var IndexStore = class {
507
593
  */
508
594
  deleteFile(file) {
509
595
  this.runWithRetry(() => {
510
- this.deleteRefsForFile(file);
511
- this.deleteSymbolsForFile(file);
512
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
596
+ this.db.exec("BEGIN IMMEDIATE");
597
+ try {
598
+ if (this.ftsAvailable) {
599
+ this.db.prepare(
600
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
601
+ ).run(file);
602
+ }
603
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
604
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
605
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
606
+ this.db.exec("COMMIT");
607
+ } catch (err) {
608
+ this.db.exec("ROLLBACK");
609
+ throw err;
610
+ }
513
611
  });
514
612
  }
515
613
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -532,14 +630,22 @@ var IndexStore = class {
532
630
  ).all(file);
533
631
  if (!rows.length) return null;
534
632
  const r = expectDefined(rows[0]);
535
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
633
+ return {
634
+ file: r.file,
635
+ lang: r.lang,
636
+ mtimeMs: r.mtime_ms,
637
+ symbolCount: r.symbol_count,
638
+ lastIndexed: r.last_indexed
639
+ };
536
640
  }
537
641
  getAllFileMetas() {
538
- return this.db.prepare(
539
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
540
- ).all().map(
541
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
542
- );
642
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
643
+ file: r.file,
644
+ lang: r.lang,
645
+ mtimeMs: r.mtime_ms,
646
+ symbolCount: r.symbol_count,
647
+ lastIndexed: r.last_indexed
648
+ }));
543
649
  }
544
650
  // ─── Search ──────────────────────────────────────────────────────────────────
545
651
  search(query, filter) {
@@ -563,8 +669,8 @@ var IndexStore = class {
563
669
  values.push(filter.lang);
564
670
  }
565
671
  if (filter?.file) {
566
- conditions.push("file LIKE ?");
567
- values.push(`%${filter.file}%`);
672
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
673
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
568
674
  }
569
675
  if (query.trim()) {
570
676
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -602,9 +708,11 @@ var IndexStore = class {
602
708
  * "users", camelCase-split text makes "complex" find "complexOperation").
603
709
  */
604
710
  searchRanked(query, filter, limit) {
711
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
712
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
605
713
  const tokens = tokenise(query);
606
714
  if (tokens.length === 0 || !this.ftsAvailable) {
607
- return this.searchRankedFallback(query, filter, limit);
715
+ return this.searchRankedFallback(query, filter, safeLimit);
608
716
  }
609
717
  let effectiveKind = filter?.kind;
610
718
  if (filter?.lspKind !== void 0) {
@@ -624,11 +732,13 @@ var IndexStore = class {
624
732
  values.push(filter.lang);
625
733
  }
626
734
  if (filter?.file) {
627
- conditions.push("s.file LIKE ?");
628
- values.push(`%${filter.file}%`);
735
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
736
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
629
737
  }
630
738
  const where = conditions.join(" AND ");
631
- 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);
739
+ const countRows = this.db.prepare(
740
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
741
+ ).all(...values);
632
742
  const total = countRows[0] ? Number(countRows[0].n) : 0;
633
743
  if (total === 0) return { results: [], total: 0 };
634
744
  const rows = this.db.prepare(
@@ -637,9 +747,13 @@ var IndexStore = class {
637
747
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
638
748
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
639
749
  WHERE ${where}
640
- ORDER BY bm25(symbols_fts)
750
+ ORDER BY
751
+ CASE WHEN lower(s.name) = lower(?) THEN 0
752
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
753
+ ELSE 2 END,
754
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
641
755
  LIMIT ?`
642
- ).all(...values, limit);
756
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
643
757
  return {
644
758
  results: rows.map((r) => ({
645
759
  id: r.id,
@@ -669,10 +783,28 @@ var IndexStore = class {
669
783
  }
670
784
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
671
785
  const bm25 = buildBm25Index(
672
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
786
+ candidates.map((c) => ({
787
+ id: c.id,
788
+ text: buildIndexableText(c.name, c.signature, c.docComment)
789
+ }))
673
790
  );
674
791
  const scored = bm25.score(query, (id) => candidateById.has(id));
675
- scored.sort((a, b) => b.score - a.score);
792
+ const q = query.trim().toLowerCase();
793
+ const rank = (id) => {
794
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
795
+ if (name === q) return 0;
796
+ if (name.startsWith(q)) return 1;
797
+ return 2;
798
+ };
799
+ scored.sort((a, b) => {
800
+ const rankDiff = rank(a.id) - rank(b.id);
801
+ if (rankDiff !== 0) return rankDiff;
802
+ const scoreDiff = b.score - a.score;
803
+ if (scoreDiff !== 0) return scoreDiff;
804
+ const left = expectDefined(candidateById.get(a.id));
805
+ const right = expectDefined(candidateById.get(b.id));
806
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
807
+ });
676
808
  const qTokens = tokenise(query);
677
809
  const results = scored.slice(0, limit).map(({ id, score }) => {
678
810
  const c = expectDefined(candidateById.get(id));
@@ -681,9 +813,7 @@ var IndexStore = class {
681
813
  return { results, total: candidates.length };
682
814
  }
683
815
  getAllIndexable() {
684
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
685
- ({ id, text }) => ({ id, text })
686
- );
816
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
687
817
  }
688
818
  /**
689
819
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -693,28 +823,22 @@ var IndexStore = class {
693
823
  * `symbols.id`). Ids may have gaps — that is fine.
694
824
  */
695
825
  getMaxSymbolId() {
696
- 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();
697
827
  return rows[0]?.m ?? 0;
698
828
  }
699
829
  // ─── Stats ───────────────────────────────────────────────────────────────────
700
830
  getStats() {
701
831
  const sizeBytes = this.sizeBytes();
702
- const lastRows = this.db.prepare(
703
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
704
- ).all();
832
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
705
833
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
706
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
834
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
707
835
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
708
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
836
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
709
837
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
710
- const langRows = this.db.prepare(
711
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
712
- ).all();
838
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
713
839
  const byLang = {};
714
840
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
715
- const kindRows = this.db.prepare(
716
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
717
- ).all();
841
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
718
842
  const byKind = {};
719
843
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
720
844
  return {
@@ -730,17 +854,34 @@ var IndexStore = class {
730
854
  }
731
855
  setLastIndexed(ts2) {
732
856
  this.runWithRetry(() => {
733
- this.db.prepare(
734
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
735
- ).run(String(ts2));
857
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
858
+ });
859
+ }
860
+ getMetadata(key) {
861
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
862
+ return rows[0]?.value;
863
+ }
864
+ setMetadata(key, value) {
865
+ this.runWithRetry(() => {
866
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
736
867
  });
737
868
  }
738
869
  clearAll() {
739
870
  this.runWithRetry(() => {
740
- this.db.exec("DELETE FROM symbols");
741
- this.db.exec("DELETE FROM files");
742
- this.db.exec("DELETE FROM refs");
743
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
871
+ this.db.exec("BEGIN IMMEDIATE");
872
+ try {
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");
878
+ this.db.exec("COMMIT");
879
+ this.stmtCache.clear();
880
+ this.initSchema();
881
+ } catch (err) {
882
+ this.db.exec("ROLLBACK");
883
+ throw err;
884
+ }
744
885
  });
745
886
  }
746
887
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -821,7 +962,7 @@ var IndexStore = class {
821
962
  ).run(...options.deleteForFiles);
822
963
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
823
964
  }
824
- 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();
825
966
  let nextId = (maxRows[0]?.m ?? 0) + 1;
826
967
  const symStmt = this.db.prepare(
827
968
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -831,6 +972,7 @@ var IndexStore = class {
831
972
  const allInserted = [];
832
973
  const refsToInsert = [];
833
974
  for (const entry of entries) {
975
+ const insertedForEntry = [];
834
976
  for (const s of entry.symbols) {
835
977
  const id = nextId++;
836
978
  symStmt.run(
@@ -848,9 +990,11 @@ var IndexStore = class {
848
990
  s.file
849
991
  );
850
992
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
851
- allInserted.push({ ...s, id });
993
+ const inserted = { ...s, id };
994
+ allInserted.push(inserted);
995
+ insertedForEntry.push(inserted);
852
996
  }
853
- for (const r of entry.refs) refsToInsert.push(r);
997
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
854
998
  }
855
999
  if (refsToInsert.length > 0) {
856
1000
  const refStmt = this.db.prepare(
@@ -888,9 +1032,7 @@ var IndexStore = class {
888
1032
  */
889
1033
  deleteRefsForFile(file) {
890
1034
  this.runWithRetry(() => {
891
- this.db.prepare(
892
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
893
- ).run(file);
1035
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
894
1036
  });
895
1037
  }
896
1038
  /**
@@ -931,9 +1073,7 @@ var IndexStore = class {
931
1073
  * Find all references FROM a given symbol (what does this symbol call/use?).
932
1074
  */
933
1075
  findRefsFrom(symbolId) {
934
- return this.db.prepare(
935
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
936
- ).all(symbolId).map((r) => ({
1076
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
937
1077
  id: r.id,
938
1078
  fromId: r.from_id,
939
1079
  toName: r.to_name,
@@ -950,6 +1090,356 @@ var IndexStore = class {
950
1090
  return 0;
951
1091
  }
952
1092
  }
1093
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
1094
+ /**
1095
+ * Derive a monorepo package name from an absolute file path.
1096
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
1097
+ * Returns `undefined` for files outside any recognisable package structure.
1098
+ */
1099
+ static derivePackage(filePath) {
1100
+ const f = filePath.replace(/\\/g, "/");
1101
+ const pkgsIdx = f.indexOf("/packages/");
1102
+ if (pkgsIdx !== -1) {
1103
+ const rest = f.slice(pkgsIdx + "/packages/".length);
1104
+ const seg = rest.split("/")[0];
1105
+ return seg ? `@wrongstack/${seg}` : void 0;
1106
+ }
1107
+ const appsIdx = f.indexOf("/apps/");
1108
+ if (appsIdx !== -1) {
1109
+ const rest = f.slice(appsIdx + "/apps/".length);
1110
+ const seg = rest.split("/")[0];
1111
+ return seg ? `app:${seg}` : void 0;
1112
+ }
1113
+ return void 0;
1114
+ }
1115
+ static packageFromImport(moduleName) {
1116
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
1117
+ const parts = moduleName.split("/");
1118
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
1119
+ }
1120
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
1121
+ if (!moduleName.startsWith(".")) return void 0;
1122
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
1123
+ const absolute = path.posix.normalize(
1124
+ path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
1125
+ );
1126
+ const extension = path.posix.extname(absolute);
1127
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
1128
+ const candidates = [
1129
+ absolute,
1130
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
1131
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
1132
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
1133
+ ];
1134
+ const indexedByPortablePath = new Map(
1135
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
1136
+ );
1137
+ for (const candidate of candidates) {
1138
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
1139
+ if (indexed) return indexed;
1140
+ }
1141
+ return void 0;
1142
+ }
1143
+ /**
1144
+ * Package-level graph: each workspace package is a node; edges are derived
1145
+ * from cross-package symbol references (a symbol in package A references a
1146
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
1147
+ */
1148
+ getPackageGraph() {
1149
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
1150
+ const pkgNodes = /* @__PURE__ */ new Map();
1151
+ const fileToPkg = /* @__PURE__ */ new Map();
1152
+ const symbolToPkg = /* @__PURE__ */ new Map();
1153
+ for (const s of symbols) {
1154
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
1155
+ fileToPkg.set(s.file, pkg);
1156
+ symbolToPkg.set(s.id, pkg);
1157
+ const node = pkgNodes.get(pkg);
1158
+ if (node) {
1159
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
1160
+ } else {
1161
+ pkgNodes.set(pkg, {
1162
+ id: `pkg:${pkg}`,
1163
+ label: pkg,
1164
+ kind: "package",
1165
+ package: pkg,
1166
+ symbolCount: 1,
1167
+ fileCount: 0
1168
+ });
1169
+ }
1170
+ }
1171
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1172
+ for (const { file } of files) {
1173
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1174
+ const node = pkgNodes.get(pkg);
1175
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1176
+ }
1177
+ const refRows = this.db.prepare(
1178
+ `SELECT r.from_id, r.to_id, r.call_type
1179
+ FROM refs r
1180
+ WHERE r.to_id IS NOT NULL`
1181
+ ).all();
1182
+ const edgeMap = /* @__PURE__ */ new Map();
1183
+ for (const r of refRows) {
1184
+ if (r.call_type === "import") continue;
1185
+ const fromPkg = symbolToPkg.get(r.from_id);
1186
+ const toPkg = symbolToPkg.get(r.to_id);
1187
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
1188
+ const key = `${fromPkg}\0${toPkg}`;
1189
+ let e = edgeMap.get(key);
1190
+ if (!e) {
1191
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1192
+ edgeMap.set(key, e);
1193
+ }
1194
+ e.weight++;
1195
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1196
+ }
1197
+ const importRows = this.db.prepare(
1198
+ `SELECT r.from_id, r.to_name
1199
+ FROM refs r
1200
+ WHERE r.call_type = 'import'`
1201
+ ).all();
1202
+ for (const r of importRows) {
1203
+ const fromPkg = symbolToPkg.get(r.from_id);
1204
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
1205
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1206
+ const key = `${fromPkg}\0${toPkg}`;
1207
+ let edge = edgeMap.get(key);
1208
+ if (!edge) {
1209
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1210
+ edgeMap.set(key, edge);
1211
+ }
1212
+ edge.weight++;
1213
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1214
+ }
1215
+ const edges = [];
1216
+ for (const [key, e] of edgeMap) {
1217
+ const [source, target] = key.split("\0");
1218
+ let bestType = "call";
1219
+ let bestCount = 0;
1220
+ for (const [t, c] of e.types) {
1221
+ if (c > bestCount) {
1222
+ bestType = t;
1223
+ bestCount = c;
1224
+ }
1225
+ }
1226
+ edges.push({
1227
+ source: `pkg:${source}`,
1228
+ target: `pkg:${target}`,
1229
+ weight: e.weight,
1230
+ refType: bestType
1231
+ });
1232
+ }
1233
+ return { nodes: [...pkgNodes.values()], edges };
1234
+ }
1235
+ /**
1236
+ * File-level graph for a single package: each file is a node; edges are
1237
+ * derived from cross-file symbol references within the package.
1238
+ */
1239
+ getFileGraph(packageFilter) {
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);
1248
+ const fileNodes = /* @__PURE__ */ new Map();
1249
+ const symToFile = /* @__PURE__ */ new Map();
1250
+ const fileStats = /* @__PURE__ */ new Map();
1251
+ for (const s of pkgSyms) {
1252
+ symToFile.set(s.id, s.file);
1253
+ const current = fileStats.get(s.file);
1254
+ fileStats.set(s.file, {
1255
+ count: (current?.count ?? 0) + 1,
1256
+ lang: current?.lang ?? s.lang
1257
+ });
1258
+ }
1259
+ const ensureFileNode = (file) => {
1260
+ if (fileNodes.has(file)) return;
1261
+ const stats = fileStats.get(file);
1262
+ fileNodes.set(file, {
1263
+ id: `file:${file}`,
1264
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
1265
+ kind: "file",
1266
+ package: _IndexStore.derivePackage(file) ?? "(root)",
1267
+ file,
1268
+ symbolCount: stats?.count ?? 0,
1269
+ lang: stats?.lang,
1270
+ external: !localFiles.has(file)
1271
+ });
1272
+ };
1273
+ for (const file of localFiles) {
1274
+ ensureFileNode(file);
1275
+ }
1276
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
1277
+ const refRows = this.db.prepare(
1278
+ `SELECT r.from_id, r.to_id, r.call_type
1279
+ FROM refs r
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
+ }
1302
+ const edgeMap = /* @__PURE__ */ new Map();
1303
+ for (const r of refRows) {
1304
+ if (r.call_type === "import") continue;
1305
+ const fromFile = symToFile.get(r.from_id);
1306
+ const toFile = symToFile.get(r.to_id);
1307
+ if (!fromFile || !toFile || fromFile === toFile) continue;
1308
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
1309
+ ensureFileNode(fromFile);
1310
+ ensureFileNode(toFile);
1311
+ const key = `${fromFile}\0${toFile}`;
1312
+ let e = edgeMap.get(key);
1313
+ if (!e) {
1314
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1315
+ edgeMap.set(key, e);
1316
+ }
1317
+ e.weight++;
1318
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1319
+ }
1320
+ const importRows = this.db.prepare(
1321
+ `SELECT r.from_id, r.to_name
1322
+ FROM refs r
1323
+ WHERE r.call_type = 'import'
1324
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
1325
+ ).all(...pkgFilePaths);
1326
+ for (const r of importRows) {
1327
+ const fromFile = symToFile.get(r.from_id);
1328
+ if (!fromFile || !localFiles.has(fromFile)) continue;
1329
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
1330
+ if (!toFile || fromFile === toFile) continue;
1331
+ ensureFileNode(fromFile);
1332
+ ensureFileNode(toFile);
1333
+ const key = `${fromFile}\0${toFile}`;
1334
+ let edge = edgeMap.get(key);
1335
+ if (!edge) {
1336
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1337
+ edgeMap.set(key, edge);
1338
+ }
1339
+ edge.weight++;
1340
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1341
+ }
1342
+ const edges = [];
1343
+ for (const [key, e] of edgeMap) {
1344
+ const [source, target] = key.split("\0");
1345
+ let bestType = "call";
1346
+ let bestCount = 0;
1347
+ for (const [t, c] of e.types) {
1348
+ if (c > bestCount) {
1349
+ bestType = t;
1350
+ bestCount = c;
1351
+ }
1352
+ }
1353
+ edges.push({
1354
+ source: `file:${source}`,
1355
+ target: `file:${target}`,
1356
+ weight: e.weight,
1357
+ refType: bestType
1358
+ });
1359
+ }
1360
+ return { nodes: [...fileNodes.values()], edges };
1361
+ }
1362
+ /**
1363
+ * Symbol-level graph for a single file: each symbol is a node; edges are
1364
+ * derived from intra-file and cross-file symbol references (who calls whom).
1365
+ */
1366
+ getSymbolGraph(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);
1370
+ if (syms.length === 0) return { nodes: [], edges: [] };
1371
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
1372
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
1373
+ const toGraphNode = (s) => ({
1374
+ id: `sym:${s.id}`,
1375
+ label: s.name,
1376
+ kind: "symbol",
1377
+ symbolId: s.id,
1378
+ symbolKind: s.kind,
1379
+ file: s.file,
1380
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
1381
+ lang: s.lang,
1382
+ line: s.line,
1383
+ signature: s.signature,
1384
+ scope: s.scope,
1385
+ external: s.file !== fileFilter
1386
+ });
1387
+ const symIds = new Set(syms.map((s) => s.id));
1388
+ const refRows = this.db.prepare(
1389
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1390
+ FROM refs r
1391
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
1392
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
1393
+ ).all(fileFilter, fileFilter);
1394
+ const edgeMap = /* @__PURE__ */ new Map();
1395
+ for (const r of refRows) {
1396
+ if (r.to_id === null) continue;
1397
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
1398
+ relatedIds.add(r.from_id);
1399
+ relatedIds.add(r.to_id);
1400
+ const key = `${r.from_id}\0${r.to_id}`;
1401
+ let e = edgeMap.get(key);
1402
+ if (!e) {
1403
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1404
+ edgeMap.set(key, e);
1405
+ }
1406
+ e.weight++;
1407
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1408
+ }
1409
+ const edges = [];
1410
+ for (const [key, e] of edgeMap) {
1411
+ const [fromId, toId] = key.split("\0").map(Number);
1412
+ let bestType = "call";
1413
+ let bestCount = 0;
1414
+ for (const [t, c] of e.types) {
1415
+ if (c > bestCount) {
1416
+ bestType = t;
1417
+ bestCount = c;
1418
+ }
1419
+ }
1420
+ edges.push({
1421
+ source: `sym:${fromId}`,
1422
+ target: `sym:${toId}`,
1423
+ weight: e.weight,
1424
+ refType: bestType
1425
+ });
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
+ }
1436
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
1437
+ const aExternal = a.file === fileFilter ? 0 : 1;
1438
+ const bExternal = b.file === fileFilter ? 0 : 1;
1439
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
1440
+ }).map(toGraphNode);
1441
+ return { nodes, edges };
1442
+ }
953
1443
  close() {
954
1444
  this.stmtCache.clear();
955
1445
  try {
@@ -968,6 +1458,7 @@ import { Worker } from "node:worker_threads";
968
1458
  import { expectDefined as expectDefined5 } from "@wrongstack/core";
969
1459
  import * as fs7 from "node:fs/promises";
970
1460
  import * as path8 from "node:path";
1461
+ import { availableParallelism } from "node:os";
971
1462
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
972
1463
 
973
1464
  // src/codebase-index/ts-parser.ts
@@ -1024,8 +1515,7 @@ function extToLang(ext) {
1024
1515
  return null;
1025
1516
  }
1026
1517
  }
1027
- function getSignature(node, sourceFile) {
1028
- const printer = ts.createPrinter({});
1518
+ function getSignature(printer, node, sourceFile) {
1029
1519
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
1030
1520
  return raw.replace(/\s+/g, " ").slice(0, 500);
1031
1521
  }
@@ -1044,20 +1534,14 @@ function getJsDoc(node, sourceFile) {
1044
1534
  }
1045
1535
  return "";
1046
1536
  }
1047
- function buildScope(node) {
1048
- const parts = [];
1049
- let current = node.parent;
1050
- while (current) {
1051
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
1052
- parts.unshift(current.name?.text ?? "Anon");
1053
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
1054
- if (current.name && ts.isIdentifier(current.name)) {
1055
- parts.unshift(current.name.text);
1056
- }
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);
1057
1543
  }
1058
- current = current.parent;
1059
1544
  }
1060
- return parts.join(".");
1061
1545
  }
1062
1546
  function parseSymbols(opts) {
1063
1547
  const { file, content, lang } = opts;
@@ -1068,41 +1552,39 @@ function parseSymbols(opts) {
1068
1552
  return { file, lang, symbols: [], mtimeMs: Date.now() };
1069
1553
  }
1070
1554
  const symbols = [];
1071
- function visit(node) {
1555
+ const refs = [];
1556
+ const printer = ts.createPrinter({});
1557
+ function visit(node, funcDepth, scopeParts) {
1072
1558
  const kind = kindOf(node);
1073
1559
  if (kind) {
1074
- const nameNode = node.name;
1075
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
1076
- const name = nameNode.text;
1077
- const pos = nameNode.getStart(sourceFile);
1078
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
1079
- const scope = buildScope(node);
1080
- const signature = getSignature(node, sourceFile);
1081
- const docComment = getJsDoc(node, sourceFile);
1082
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
1083
- symbols.push({
1084
- id: 0,
1085
- lang,
1086
- kind,
1087
- name,
1088
- file,
1089
- line: line + 1,
1090
- col: character,
1091
- signature,
1092
- docComment,
1093
- scope,
1094
- text
1095
- });
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
+ });
1586
+ }
1096
1587
  }
1097
- ts.forEachChild(node, visit);
1098
- }
1099
- visit(sourceFile);
1100
- const refs = extractRefs(sourceFile);
1101
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
1102
- }
1103
- function extractRefs(sourceFile) {
1104
- const refs = [];
1105
- function visit(node) {
1106
1588
  const pos = node.getStart(sourceFile);
1107
1589
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
1108
1590
  const lineNum = line + 1;
@@ -1127,10 +1609,14 @@ function extractRefs(sourceFile) {
1127
1609
  const moduleName = getModuleName(node);
1128
1610
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
1129
1611
  }
1130
- 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;
1131
1617
  }
1132
- visit(sourceFile);
1133
- return deduplicateRefs(refs);
1618
+ visit(sourceFile, 0, []);
1619
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
1134
1620
  }
1135
1621
  function getTypeName(name) {
1136
1622
  if (ts.isIdentifier(name)) return name.text;
@@ -2038,9 +2524,9 @@ function parseSymbols5(opts) {
2038
2524
  function regexParse2(opts) {
2039
2525
  const { file, content, lang } = opts;
2040
2526
  const symbols = [];
2041
- const basename2 = path6.basename(file).toLowerCase();
2042
- const isPackageJson = basename2 === "package.json";
2043
- const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2527
+ const basename3 = path6.basename(file).toLowerCase();
2528
+ const isPackageJson = basename3 === "package.json";
2529
+ const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
2044
2530
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2045
2531
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2046
2532
  const lines = content.split("\n");
@@ -2435,16 +2921,14 @@ async function loadGitignoreMatcher(projectRoot) {
2435
2921
 
2436
2922
  // src/codebase-index/indexer.ts
2437
2923
  var YIELD_EVERY_N = 50;
2438
- var PARALLEL_BATCH = 20;
2924
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
2439
2925
  function yieldEventLoop() {
2440
2926
  return new Promise((resolve2) => setImmediate(resolve2));
2441
2927
  }
2442
2928
  function throwIfAborted(signal) {
2443
2929
  if (!signal?.aborted) return;
2444
2930
  if (signal.reason instanceof Error) throw signal.reason;
2445
- throw new Error(
2446
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
2447
- );
2931
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
2448
2932
  }
2449
2933
  function isAbortError(err) {
2450
2934
  return err instanceof DOMException && err.name === "AbortError";
@@ -2460,8 +2944,20 @@ var DEFAULT_IGNORE = [
2460
2944
  "__snapshots__",
2461
2945
  ".nyc_output"
2462
2946
  ];
2947
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2948
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2949
+ function isWithinProject(projectRoot, file) {
2950
+ const rel = path8.relative(projectRoot, file);
2951
+ return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
2952
+ }
2953
+ function isMissingPathError(err) {
2954
+ const code = err?.code;
2955
+ return code === "ENOENT" || code === "ENOTDIR";
2956
+ }
2463
2957
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2464
2958
  const results = [];
2959
+ const errors = [];
2960
+ let complete = true;
2465
2961
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2466
2962
  const globs = [
2467
2963
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -2485,7 +2981,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2485
2981
  let entries;
2486
2982
  try {
2487
2983
  entries = await fs7.readdir(dir, { withFileTypes: true });
2488
- } catch {
2984
+ } catch (err) {
2985
+ complete = false;
2986
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
2489
2987
  return;
2490
2988
  }
2491
2989
  dirCount++;
@@ -2497,8 +2995,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2497
2995
  if (isGitIgnored(rel, true)) continue;
2498
2996
  await walk(full);
2499
2997
  } else if (e.isFile()) {
2500
- if (isGitIgnored(rel, false)) continue;
2501
- const ext = path8.extname(e.name);
2998
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2999
+ const ext = path8.extname(e.name).toLowerCase();
2502
3000
  for (const { ext: extName, pat } of globs) {
2503
3001
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2504
3002
  results.push(full);
@@ -2509,7 +3007,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2509
3007
  }
2510
3008
  };
2511
3009
  await walk(projectRoot);
2512
- return results;
3010
+ return { files: results, complete, errors };
2513
3011
  }
2514
3012
  async function parseFile(file, content, lang) {
2515
3013
  switch (lang) {
@@ -2532,6 +3030,26 @@ async function parseFile(file, content, lang) {
2532
3030
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2533
3031
  }
2534
3032
  }
3033
+ function assignRefsToSymbols2(refs, symbols) {
3034
+ if (refs.length === 0 || symbols.length === 0) return [];
3035
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
3036
+ const seen = /* @__PURE__ */ new Set();
3037
+ const assigned = [];
3038
+ for (const ref of refs) {
3039
+ let owner;
3040
+ for (const symbol of ordered) {
3041
+ if (symbol.line > ref.line) break;
3042
+ owner = symbol;
3043
+ }
3044
+ if (!owner && ref.callType === "import") owner = ordered[0];
3045
+ if (!owner || owner.id <= 0) continue;
3046
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
3047
+ if (seen.has(key)) continue;
3048
+ seen.add(key);
3049
+ assigned.push({ ...ref, fromId: owner.id });
3050
+ }
3051
+ return assigned;
3052
+ }
2535
3053
  async function runIndexer(_ctx, opts) {
2536
3054
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
2537
3055
  try {
@@ -2544,7 +3062,9 @@ async function runIndexer(_ctx, opts) {
2544
3062
  }
2545
3063
  }
2546
3064
  async function runIndexerWithStore(store, opts) {
2547
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
3065
+ const { projectRoot, langs, ignore = [], signal } = opts;
3066
+ const relationGraphVersion = "2";
3067
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
2548
3068
  const startMs = Date.now();
2549
3069
  const errors = [];
2550
3070
  const langStats = {};
@@ -2553,10 +3073,18 @@ async function runIndexerWithStore(store, opts) {
2553
3073
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2554
3074
  let files;
2555
3075
  let discoveredFiles = null;
3076
+ let discoveryComplete = true;
2556
3077
  if (opts.files && opts.files.length > 0) {
2557
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path8.relative(projectRoot, f).replace(/\\/g, "/"), false));
3078
+ files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3079
+ if (!isWithinProject(projectRoot, f)) return false;
3080
+ const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
3081
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
3082
+ });
2558
3083
  } else {
2559
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3084
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3085
+ files = discovery.files;
3086
+ errors.push(...discovery.errors);
3087
+ discoveryComplete = discovery.complete;
2560
3088
  discoveredFiles = new Set(files);
2561
3089
  }
2562
3090
  if (langs && langs.length > 0) {
@@ -2571,46 +3099,79 @@ async function runIndexerWithStore(store, opts) {
2571
3099
  if (!force) {
2572
3100
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
2573
3101
  }
3102
+ let filesSinceLastYield = 0;
2574
3103
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
2575
3104
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
2576
3105
  const batchFiles = files.slice(batchStart, batchEnd);
2577
3106
  opts.onProgress?.(batchEnd, files.length);
2578
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
3107
+ filesSinceLastYield += batchFiles.length;
3108
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
3109
+ filesSinceLastYield = 0;
2579
3110
  await yieldEventLoop();
2580
3111
  throwIfAborted(signal);
2581
3112
  }
2582
3113
  const statOpts = signal ? { signal } : {};
2583
3114
  const statReadParse = await Promise.allSettled(
2584
- batchFiles.map(async (file) => {
2585
- let stat2;
2586
- try {
2587
- stat2 = await fs7.stat(file, statOpts);
2588
- } catch (e) {
2589
- if (isAbortError(e)) throw e;
2590
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
2591
- }
2592
- if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
2593
- const lang = detectLang(file);
2594
- if (!lang) return { file, stat: stat2, lang: "", parsed: null };
2595
- const meta = existingMeta.get(file);
2596
- if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
2597
- return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
2598
- }
2599
- let content;
2600
- try {
2601
- content = await fs7.readFile(file, { encoding: "utf8", signal });
2602
- } catch (e) {
2603
- if (isAbortError(e)) throw e;
2604
- return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
2605
- }
2606
- let parsed;
2607
- try {
2608
- parsed = await parseFile(file, content, lang);
2609
- } catch (e) {
2610
- return { file, stat: stat2, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
3115
+ batchFiles.map(
3116
+ async (file) => {
3117
+ let stat2;
3118
+ try {
3119
+ stat2 = await fs7.stat(file, statOpts);
3120
+ } catch (e) {
3121
+ if (isAbortError(e)) throw e;
3122
+ return {
3123
+ file,
3124
+ stat: null,
3125
+ lang: "",
3126
+ parsed: null,
3127
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
3128
+ missing: isMissingPathError(e)
3129
+ };
3130
+ }
3131
+ if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
3132
+ const lang = detectLang(file);
3133
+ if (!lang) return { file, stat: stat2, lang: "", parsed: null };
3134
+ if (stat2.size > MAX_INDEX_FILE_BYTES) {
3135
+ return {
3136
+ file,
3137
+ stat: stat2,
3138
+ lang,
3139
+ parsed: null,
3140
+ error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
3141
+ };
3142
+ }
3143
+ const meta = existingMeta.get(file);
3144
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
3145
+ return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
3146
+ }
3147
+ let content;
3148
+ try {
3149
+ content = await fs7.readFile(file, { encoding: "utf8", signal });
3150
+ } catch (e) {
3151
+ if (isAbortError(e)) throw e;
3152
+ return {
3153
+ file,
3154
+ stat: stat2,
3155
+ lang,
3156
+ parsed: null,
3157
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
3158
+ };
3159
+ }
3160
+ let parsed;
3161
+ try {
3162
+ parsed = await parseFile(file, content, lang);
3163
+ } catch (e) {
3164
+ return {
3165
+ file,
3166
+ stat: stat2,
3167
+ lang,
3168
+ parsed: null,
3169
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
3170
+ };
3171
+ }
3172
+ return { file, stat: stat2, lang, parsed, content };
2611
3173
  }
2612
- return { file, stat: stat2, lang, parsed, content };
2613
- })
3174
+ )
2614
3175
  );
2615
3176
  const batchEntries = [];
2616
3177
  const deleteForFiles = [];
@@ -2625,8 +3186,8 @@ async function runIndexerWithStore(store, opts) {
2625
3186
  }
2626
3187
  const result = settled.value;
2627
3188
  if (result.error) {
2628
- if (result.stat) store.deleteFile(file);
2629
- if (result.error.includes("error:")) errors.push(result.error);
3189
+ if (result.missing) store.deleteFile(file);
3190
+ errors.push(`${file}: ${result.error}`);
2630
3191
  continue;
2631
3192
  }
2632
3193
  const { stat: stat2, lang, parsed } = result;
@@ -2650,6 +3211,8 @@ async function runIndexerWithStore(store, opts) {
2650
3211
  continue;
2651
3212
  }
2652
3213
  if (parsed.symbols.length === 0) {
3214
+ store.deleteRefsForFile(file);
3215
+ store.deleteSymbolsForFile(file);
2653
3216
  store.upsertFile({
2654
3217
  file,
2655
3218
  lang,
@@ -2660,15 +3223,11 @@ async function runIndexerWithStore(store, opts) {
2660
3223
  filesIndexed++;
2661
3224
  continue;
2662
3225
  }
2663
- const refs = [];
2664
- if (parsed.refs && parsed.refs.length > 0) {
2665
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
2666
- }
2667
3226
  batchEntries.push({
2668
3227
  file,
2669
3228
  lang,
2670
3229
  symbols: parsed.symbols,
2671
- refs,
3230
+ refs: parsed.refs ?? [],
2672
3231
  mtimeMs: Math.floor(stat2.mtimeMs),
2673
3232
  symbolCount: parsed.symbols.length
2674
3233
  });
@@ -2676,34 +3235,12 @@ async function runIndexerWithStore(store, opts) {
2676
3235
  }
2677
3236
  if (batchEntries.length > 0) {
2678
3237
  try {
2679
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
2680
- let cursor = 0;
3238
+ store.commitBatch(batchEntries, { deleteForFiles });
2681
3239
  for (const entry of batchEntries) {
2682
3240
  const count = entry.symbols.length;
2683
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
2684
- cursor += count;
2685
3241
  symbolsIndexed += count;
2686
3242
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
2687
3243
  filesIndexed++;
2688
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2689
- const refsByLine = /* @__PURE__ */ new Map();
2690
- for (let i = 0; i < symbolsWithIds.length; i++) {
2691
- const sym = symbolsWithIds[i];
2692
- let arr = refsByLine.get(sym.line);
2693
- if (!arr) {
2694
- arr = [];
2695
- refsByLine.set(sym.line, arr);
2696
- }
2697
- arr.push(i);
2698
- }
2699
- for (const ref of entry.refs) {
2700
- const indices = refsByLine.get(ref.line);
2701
- if (indices && indices.length > 0) {
2702
- const idx = indices.shift();
2703
- ref.fromId = symbolsWithIds[idx].id;
2704
- }
2705
- }
2706
- }
2707
3244
  }
2708
3245
  } catch (err) {
2709
3246
  const message = err instanceof Error ? err.message : String(err);
@@ -2717,23 +3254,7 @@ async function runIndexerWithStore(store, opts) {
2717
3254
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
2718
3255
  filesIndexed++;
2719
3256
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2720
- const refsByLine = /* @__PURE__ */ new Map();
2721
- for (const sym of symbolsWithIds) {
2722
- let arr = refsByLine.get(sym.line);
2723
- if (!arr) {
2724
- arr = [];
2725
- refsByLine.set(sym.line, arr);
2726
- }
2727
- arr.push(sym);
2728
- }
2729
- const fallbackBatch = [];
2730
- for (const ref of entry.refs) {
2731
- const syms = refsByLine.get(ref.line);
2732
- if (syms && syms.length > 0) {
2733
- const sym = syms.shift();
2734
- fallbackBatch.push({ ...ref, fromId: sym.id });
2735
- }
2736
- }
3257
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
2737
3258
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
2738
3259
  }
2739
3260
  store.upsertFile({
@@ -2744,19 +3265,23 @@ async function runIndexerWithStore(store, opts) {
2744
3265
  lastIndexed: Date.now()
2745
3266
  });
2746
3267
  } catch (innerErr) {
2747
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
3268
+ errors.push(
3269
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
3270
+ );
2748
3271
  }
2749
3272
  }
2750
3273
  }
2751
3274
  }
2752
3275
  }
2753
- if (discoveredFiles) {
3276
+ if (discoveredFiles && discoveryComplete) {
2754
3277
  for (const [file_] of existingMeta) {
2755
3278
  if (!discoveredFiles.has(file_)) {
2756
3279
  store.deleteFile(file_);
2757
3280
  }
2758
3281
  }
2759
3282
  }
3283
+ store.resolveRefs();
3284
+ store.setMetadata("relation_graph_version", relationGraphVersion);
2760
3285
  const durationMs = Date.now() - startMs;
2761
3286
  store.setLastIndexed(Date.now());
2762
3287
  return {
@@ -2792,7 +3317,7 @@ async function indexService(args, hooks = {}) {
2792
3317
  });
2793
3318
  }
2794
3319
  function searchService(args) {
2795
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3320
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
2796
3321
  try {
2797
3322
  return store.searchRanked(
2798
3323
  args.query,
@@ -2805,22 +3330,46 @@ function searchService(args) {
2805
3330
  args.limit
2806
3331
  );
2807
3332
  } finally {
2808
- store.close();
3333
+ indexStorePool.release(store);
2809
3334
  }
2810
3335
  }
2811
3336
  function statsService(args) {
2812
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3337
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
2813
3338
  try {
2814
3339
  return store.getStats();
2815
3340
  } finally {
2816
- store.close();
3341
+ indexStorePool.release(store);
3342
+ }
3343
+ }
3344
+ function packageGraphService(args) {
3345
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3346
+ try {
3347
+ return store.getPackageGraph();
3348
+ } finally {
3349
+ indexStorePool.release(store);
3350
+ }
3351
+ }
3352
+ function fileGraphService(args) {
3353
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3354
+ try {
3355
+ return store.getFileGraph(args.packageFilter);
3356
+ } finally {
3357
+ indexStorePool.release(store);
3358
+ }
3359
+ }
3360
+ function symbolGraphService(args) {
3361
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
3362
+ try {
3363
+ return store.getSymbolGraph(args.fileFilter);
3364
+ } finally {
3365
+ indexStorePool.release(store);
2817
3366
  }
2818
3367
  }
2819
3368
 
2820
3369
  // src/codebase-index/background-indexer.ts
2821
3370
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
2822
3371
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
2823
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
3372
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
2824
3373
  var _ready = false;
2825
3374
  var _indexing = false;
2826
3375
  var _currentFile = 0;
@@ -2898,14 +3447,22 @@ function ensureWorker() {
2898
3447
  if (!entry) return;
2899
3448
  pending.delete(msg.id);
2900
3449
  if (msg.ok) entry.resolve(msg.result);
2901
- else entry.reject(new Error(msg.error));
3450
+ else {
3451
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
3452
+ if (msg.errorName && msg.errorName !== "Error") {
3453
+ error.name = msg.errorName;
3454
+ }
3455
+ entry.reject(error);
3456
+ }
2902
3457
  });
2903
3458
  w.on("error", (err) => {
3459
+ if (worker !== w) return;
2904
3460
  worker = null;
2905
3461
  failAllPending(err);
2906
3462
  });
2907
3463
  w.on("exit", () => {
2908
- if (worker === w) worker = null;
3464
+ if (worker !== w) return;
3465
+ worker = null;
2909
3466
  failAllPending(new Error("codebase-index worker exited"));
2910
3467
  });
2911
3468
  worker = w;
@@ -2922,14 +3479,28 @@ function terminateWorker(reason) {
2922
3479
  if (w) void w.terminate().catch(() => {
2923
3480
  });
2924
3481
  }
2925
- function shutdownCodebaseIndexHost() {
3482
+ async function shutdownCodebaseIndexHost() {
2926
3483
  cancelPendingReindexes();
2927
- 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"));
2928
3488
  workerUnavailable = false;
3489
+ if (w) {
3490
+ try {
3491
+ await w.terminate();
3492
+ } catch {
3493
+ }
3494
+ }
2929
3495
  }
2930
3496
  function callIndexOp(op, args, opts) {
2931
3497
  const w = ensureWorker();
2932
3498
  if (!w) return callInline(op, args, opts);
3499
+ if (opts.signal?.aborted) {
3500
+ return Promise.reject(
3501
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
3502
+ );
3503
+ }
2933
3504
  return new Promise((resolve2, reject) => {
2934
3505
  const id = nextRpcId++;
2935
3506
  const timer = setTimeout(() => {
@@ -2944,8 +3515,7 @@ function callIndexOp(op, args, opts) {
2944
3515
  const onAbort = () => {
2945
3516
  w.postMessage({ type: "cancel", id });
2946
3517
  };
2947
- if (opts.signal?.aborted) onAbort();
2948
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
3518
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2949
3519
  const cleanup = () => {
2950
3520
  clearTimeout(timer);
2951
3521
  opts.signal?.removeEventListener("abort", onAbort);
@@ -3133,7 +3703,7 @@ var codebaseIndexTool = {
3133
3703
  category: "Project",
3134
3704
  icon: "index",
3135
3705
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
3136
- usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
3706
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
3137
3707
  permission: "confirm",
3138
3708
  mutating: true,
3139
3709
  capabilities: ["fs.write.outside-project"],
@@ -3189,12 +3759,14 @@ var codebaseSearchTool = {
3189
3759
  name: "codebase-search",
3190
3760
  category: "Project",
3191
3761
  icon: "index",
3192
- description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
3193
- usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
3762
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
3763
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
3194
3764
  permission: "auto",
3195
3765
  mutating: false,
3196
3766
  capabilities: ["fs.read"],
3197
- timeoutMs: 1e4,
3767
+ // The index host has its own 30s read watchdog. Leave enough headroom for
3768
+ // worker teardown and structured timeout reporting.
3769
+ timeoutMs: 35e3,
3198
3770
  inputSchema: {
3199
3771
  type: "object",
3200
3772
  properties: {
@@ -3204,11 +3776,34 @@ var codebaseSearchTool = {
3204
3776
  },
3205
3777
  kind: {
3206
3778
  type: "string",
3207
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
3779
+ enum: [
3780
+ "class",
3781
+ "interface",
3782
+ "enum",
3783
+ "type",
3784
+ "function",
3785
+ "method",
3786
+ "var",
3787
+ "const",
3788
+ "let",
3789
+ "property",
3790
+ "parameter",
3791
+ "namespace",
3792
+ "object",
3793
+ "literal",
3794
+ "schema",
3795
+ "struct",
3796
+ "trait",
3797
+ "impl",
3798
+ "static",
3799
+ "mod"
3800
+ ],
3801
+ description: "Filter by indexed symbol kind"
3208
3802
  },
3209
3803
  lang: {
3210
3804
  type: "string",
3211
- description: "Filter by language: ts, tsx, js, jsx"
3805
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
3806
+ description: "Filter by indexed language"
3212
3807
  },
3213
3808
  lspKind: {
3214
3809
  type: "integer",
@@ -3233,20 +3828,12 @@ var codebaseSearchTool = {
3233
3828
  },
3234
3829
  async execute(input, ctx, execOpts) {
3235
3830
  const state = getIndexState();
3236
- if (!state.ready) {
3237
- return {
3238
- results: [],
3239
- total: 0,
3240
- query: input.query,
3241
- indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
3242
- };
3243
- }
3244
- if (state.indexing) {
3831
+ if (state.indexing && !state.ready) {
3245
3832
  return {
3246
3833
  results: [],
3247
3834
  total: 0,
3248
3835
  query: input.query,
3249
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
3836
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
3250
3837
  };
3251
3838
  }
3252
3839
  if (state.lastError) {
@@ -3259,21 +3846,37 @@ var codebaseSearchTool = {
3259
3846
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
3260
3847
  };
3261
3848
  }
3262
- const limit = Math.min(input.limit ?? 20, 100);
3849
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
3263
3850
  const { results, total } = await searchCodebaseIndex(
3264
3851
  {
3265
3852
  projectRoot: ctx.projectRoot,
3266
3853
  indexDir: codebaseIndexDirOverride(ctx),
3267
3854
  query: input.query,
3268
- kind: input.kind,
3269
- lang: input.lang,
3855
+ kind: input.kind?.toLowerCase(),
3856
+ lang: input.lang?.toLowerCase(),
3270
3857
  file: input.file,
3271
3858
  lspKind: input.lspKind,
3272
3859
  limit
3273
3860
  },
3274
3861
  { signal: execOpts?.signal }
3275
3862
  );
3276
- return { results, total, query: input.query };
3863
+ let hasPersistedIndex = state.ready || total > 0;
3864
+ if (!hasPersistedIndex) {
3865
+ try {
3866
+ const stats = await codebaseIndexStats(
3867
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3868
+ { signal: execOpts?.signal }
3869
+ );
3870
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
3871
+ } catch {
3872
+ }
3873
+ }
3874
+ return {
3875
+ results,
3876
+ total,
3877
+ query: input.query,
3878
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
3879
+ };
3277
3880
  }
3278
3881
  };
3279
3882
 
@@ -3282,12 +3885,15 @@ var codebaseStatsTool = {
3282
3885
  name: "codebase-stats",
3283
3886
  category: "Project",
3284
3887
  icon: "index",
3285
- description: "Return health and statistics about the current symbol index (total symbols, files, language/kind breakdown, size, last update). Useful to decide whether to re-index.",
3286
- usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
3888
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
3889
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
3287
3890
  permission: "auto",
3288
3891
  mutating: false,
3289
3892
  capabilities: ["fs.read"],
3290
- timeoutMs: 5e3,
3893
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
3894
+ // longer so the host can return a structured timeout instead of being
3895
+ // pre-empted by the generic tool executor.
3896
+ timeoutMs: 35e3,
3291
3897
  inputSchema: {
3292
3898
  type: "object",
3293
3899
  properties: {},
@@ -3295,7 +3901,8 @@ var codebaseStatsTool = {
3295
3901
  },
3296
3902
  async execute(_input, ctx, execOpts) {
3297
3903
  const idxState = getIndexState();
3298
- if (!idxState.ready) {
3904
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
3905
+ if (idxState.indexing) {
3299
3906
  return {
3300
3907
  totalSymbols: 0,
3301
3908
  totalFiles: 0,
@@ -3303,22 +3910,47 @@ var codebaseStatsTool = {
3303
3910
  byKind: {},
3304
3911
  lastIndexed: null,
3305
3912
  sizeBytes: 0,
3306
- indexPath: "",
3913
+ indexPath,
3307
3914
  version: SCHEMA_VERSION,
3308
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
3915
+ statsAvailable: false,
3916
+ indexing: {
3917
+ currentFile: idxState.currentFile,
3918
+ totalFiles: idxState.totalFiles
3919
+ },
3920
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
3309
3921
  };
3310
3922
  }
3311
- const stats = await codebaseIndexStats(
3312
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3313
- { signal: execOpts?.signal }
3314
- );
3315
- if (idxState.indexing) {
3923
+ let stats;
3924
+ try {
3925
+ stats = await codebaseIndexStats(
3926
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3927
+ { signal: execOpts?.signal }
3928
+ );
3929
+ } catch (err) {
3930
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
3931
+ throw err;
3932
+ }
3316
3933
  return {
3317
- ...stats,
3318
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3934
+ totalSymbols: 0,
3935
+ totalFiles: 0,
3936
+ byLang: {},
3937
+ byKind: {},
3938
+ lastIndexed: null,
3939
+ sizeBytes: 0,
3940
+ indexPath,
3941
+ version: SCHEMA_VERSION,
3942
+ statsAvailable: false,
3943
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
3319
3944
  };
3320
3945
  }
3321
3946
  const circuit = idxState.circuit;
3947
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
3948
+ let indexStatus;
3949
+ if (circuit.state === "open") {
3950
+ indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
3951
+ } else if (!hasPersistedIndex) {
3952
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
3953
+ }
3322
3954
  return {
3323
3955
  totalSymbols: stats.totalSymbols,
3324
3956
  totalFiles: stats.totalFiles,
@@ -3328,9 +3960,8 @@ var codebaseStatsTool = {
3328
3960
  sizeBytes: stats.sizeBytes,
3329
3961
  indexPath: stats.indexPath,
3330
3962
  version: stats.version,
3331
- ...circuit.state === "open" ? {
3332
- indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
3333
- } : {}
3963
+ statsAvailable: true,
3964
+ ...indexStatus ? { indexStatus } : {}
3334
3965
  };
3335
3966
  }
3336
3967
  };
@@ -3349,6 +3980,7 @@ export {
3349
3980
  codebaseSearchTool,
3350
3981
  codebaseStatsTool,
3351
3982
  enqueueReindex,
3983
+ fileGraphService,
3352
3984
  getIndexState,
3353
3985
  indexCircuitBreaker,
3354
3986
  internalKindToLspKind,
@@ -3357,12 +3989,14 @@ export {
3357
3989
  isIndexing,
3358
3990
  lspKindToInternalKind,
3359
3991
  onIndexStateChange,
3992
+ packageGraphService,
3360
3993
  resetIndexCircuitBreaker,
3361
3994
  resolveIndexDir,
3362
3995
  runIndexer,
3363
3996
  runStartupIndex,
3364
3997
  searchCodebaseIndex,
3365
3998
  shutdownCodebaseIndexHost,
3999
+ symbolGraphService,
3366
4000
  tokenise
3367
4001
  };
3368
4002
  //# sourceMappingURL=index.js.map