@wrongstack/tools 0.286.0 → 0.289.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 (120) hide show
  1. package/README.md +2 -2
  2. package/dist/_edit-match.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_redact-command.d.ts.map +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/audit.js +61 -14
  7. package/dist/audit.js.map +3 -3
  8. package/dist/bash.d.ts +1 -1
  9. package/dist/bash.d.ts.map +1 -1
  10. package/dist/bash.js +97 -19
  11. package/dist/bash.js.map +3 -3
  12. package/dist/batch-tool-use.d.ts +1 -1
  13. package/dist/batch-tool-use.d.ts.map +1 -1
  14. package/dist/batch-tool-use.js +26 -8
  15. package/dist/batch-tool-use.js.map +2 -2
  16. package/dist/browser/index.js +1 -1
  17. package/dist/browser/index.js.map +1 -1
  18. package/dist/browser/types.d.ts +1 -1
  19. package/dist/browser/types.d.ts.map +1 -1
  20. package/dist/builtin.d.ts +12 -10
  21. package/dist/builtin.d.ts.map +1 -1
  22. package/dist/builtin.js +2107 -864
  23. package/dist/builtin.js.map +4 -4
  24. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  25. package/dist/codebase-index/bm25.d.ts +1 -2
  26. package/dist/codebase-index/bm25.d.ts.map +1 -1
  27. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  28. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  29. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  30. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  31. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  32. package/dist/codebase-index/index-service.d.ts +11 -1
  33. package/dist/codebase-index/index-service.d.ts.map +1 -1
  34. package/dist/codebase-index/index.d.ts +2 -1
  35. package/dist/codebase-index/index.d.ts.map +1 -1
  36. package/dist/codebase-index/index.js +934 -283
  37. package/dist/codebase-index/index.js.map +4 -4
  38. package/dist/codebase-index/indexer.d.ts.map +1 -1
  39. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  40. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  41. package/dist/codebase-index/schema.d.ts +43 -1
  42. package/dist/codebase-index/schema.d.ts.map +1 -1
  43. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  45. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  46. package/dist/codebase-index/worker.js +793 -241
  47. package/dist/codebase-index/worker.js.map +4 -4
  48. package/dist/codebase-index/writer.d.ts +27 -1
  49. package/dist/codebase-index/writer.d.ts.map +1 -1
  50. package/dist/e2e.js +477 -0
  51. package/dist/e2e.js.map +7 -0
  52. package/dist/edit.d.ts.map +1 -1
  53. package/dist/edit.js +36 -12
  54. package/dist/edit.js.map +3 -3
  55. package/dist/exec.d.ts +1 -1
  56. package/dist/exec.d.ts.map +1 -1
  57. package/dist/exec.js +60 -5
  58. package/dist/exec.js.map +3 -3
  59. package/dist/fetch.js +2 -2
  60. package/dist/fetch.js.map +2 -2
  61. package/dist/format.js +61 -14
  62. package/dist/format.js.map +3 -3
  63. package/dist/glob.js +2 -2
  64. package/dist/glob.js.map +1 -1
  65. package/dist/grep.d.ts.map +1 -1
  66. package/dist/grep.js +2 -2
  67. package/dist/grep.js.map +2 -2
  68. package/dist/index.d.ts +3 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2535 -865
  71. package/dist/index.js.map +4 -4
  72. package/dist/install.js +61 -14
  73. package/dist/install.js.map +3 -3
  74. package/dist/kanban.d.ts +16 -2
  75. package/dist/kanban.d.ts.map +1 -1
  76. package/dist/kanban.js +795 -532
  77. package/dist/kanban.js.map +4 -4
  78. package/dist/languages/detect.d.ts.map +1 -1
  79. package/dist/languages/index.js +61 -14
  80. package/dist/languages/index.js.map +3 -3
  81. package/dist/languages/types.d.ts +7 -0
  82. package/dist/languages/types.d.ts.map +1 -1
  83. package/dist/lint.js +61 -14
  84. package/dist/lint.js.map +3 -3
  85. package/dist/next-steps.d.ts +6 -2
  86. package/dist/next-steps.d.ts.map +1 -1
  87. package/dist/next-steps.js +3 -3
  88. package/dist/next-steps.js.map +2 -2
  89. package/dist/outdated.js +61 -14
  90. package/dist/outdated.js.map +3 -3
  91. package/dist/pack.js +2104 -861
  92. package/dist/pack.js.map +4 -4
  93. package/dist/plan.d.ts.map +1 -1
  94. package/dist/plan.js +158 -4
  95. package/dist/plan.js.map +4 -4
  96. package/dist/process-registry.js +12 -5
  97. package/dist/process-registry.js.map +2 -2
  98. package/dist/search.js +2 -2
  99. package/dist/search.js.map +2 -2
  100. package/dist/session-kanban.d.ts +47 -0
  101. package/dist/session-kanban.d.ts.map +1 -0
  102. package/dist/session-kanban.js +626 -0
  103. package/dist/session-kanban.js.map +7 -0
  104. package/dist/task.d.ts.map +1 -1
  105. package/dist/task.js +167 -4
  106. package/dist/task.js.map +4 -4
  107. package/dist/test.js +61 -14
  108. package/dist/test.js.map +3 -3
  109. package/dist/todo.d.ts.map +1 -1
  110. package/dist/todo.js +159 -7
  111. package/dist/todo.js.map +4 -4
  112. package/dist/tool-icon-map.d.ts.map +1 -1
  113. package/dist/tool-icons.d.ts.map +1 -1
  114. package/dist/tool-icons.js +7 -1
  115. package/dist/tool-icons.js.map +2 -2
  116. package/dist/tree.js +2 -2
  117. package/dist/tree.js.map +1 -1
  118. package/dist/typecheck.js +61 -14
  119. package/dist/typecheck.js.map +3 -3
  120. package/package.json +11 -3
@@ -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
  }
@@ -299,7 +323,7 @@ function sleepSync(ms) {
299
323
  } catch {
300
324
  }
301
325
  }
302
- var IndexStore = class {
326
+ var IndexStore = class _IndexStore {
303
327
  db;
304
328
  /** Absolute path to this project's index directory. */
305
329
  indexDir;
@@ -351,10 +375,7 @@ var IndexStore = class {
351
375
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
352
376
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
353
377
  }
354
- const delay = Math.min(
355
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
356
- LOCK_RETRY_MAX_DELAY_MS
357
- );
378
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
358
379
  sleepSync(delay);
359
380
  }
360
381
  }
@@ -436,8 +457,24 @@ var IndexStore = class {
436
457
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
437
458
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
438
459
  try {
439
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
460
+ this.db.exec(
461
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
462
+ );
440
463
  this.ftsAvailable = true;
464
+ const symbolCount = Number(
465
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
466
+ );
467
+ const ftsCount = Number(
468
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
469
+ );
470
+ if (symbolCount !== ftsCount) {
471
+ this.db.exec("DELETE FROM symbols_fts");
472
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
473
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
474
+ for (const row of rows) {
475
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
476
+ }
477
+ }
441
478
  } catch {
442
479
  this.ftsAvailable = false;
443
480
  }
@@ -495,7 +532,9 @@ var IndexStore = class {
495
532
  deleteSymbolsForFile(file) {
496
533
  this.runWithRetry(() => {
497
534
  if (this.ftsAvailable) {
498
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
535
+ this.stmt(
536
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
537
+ ).run(file);
499
538
  }
500
539
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
501
540
  });
@@ -507,9 +546,21 @@ var IndexStore = class {
507
546
  */
508
547
  deleteFile(file) {
509
548
  this.runWithRetry(() => {
510
- this.deleteRefsForFile(file);
511
- this.deleteSymbolsForFile(file);
512
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
549
+ this.db.exec("BEGIN IMMEDIATE");
550
+ try {
551
+ if (this.ftsAvailable) {
552
+ this.db.prepare(
553
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
554
+ ).run(file);
555
+ }
556
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
557
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
558
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
559
+ this.db.exec("COMMIT");
560
+ } catch (err) {
561
+ this.db.exec("ROLLBACK");
562
+ throw err;
563
+ }
513
564
  });
514
565
  }
515
566
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -532,14 +583,22 @@ var IndexStore = class {
532
583
  ).all(file);
533
584
  if (!rows.length) return null;
534
585
  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 };
586
+ return {
587
+ file: r.file,
588
+ lang: r.lang,
589
+ mtimeMs: r.mtime_ms,
590
+ symbolCount: r.symbol_count,
591
+ lastIndexed: r.last_indexed
592
+ };
536
593
  }
537
594
  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
- );
595
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
596
+ file: r.file,
597
+ lang: r.lang,
598
+ mtimeMs: r.mtime_ms,
599
+ symbolCount: r.symbol_count,
600
+ lastIndexed: r.last_indexed
601
+ }));
543
602
  }
544
603
  // ─── Search ──────────────────────────────────────────────────────────────────
545
604
  search(query, filter) {
@@ -563,8 +622,8 @@ var IndexStore = class {
563
622
  values.push(filter.lang);
564
623
  }
565
624
  if (filter?.file) {
566
- conditions.push("file LIKE ?");
567
- values.push(`%${filter.file}%`);
625
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
626
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
568
627
  }
569
628
  if (query.trim()) {
570
629
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -602,9 +661,11 @@ var IndexStore = class {
602
661
  * "users", camelCase-split text makes "complex" find "complexOperation").
603
662
  */
604
663
  searchRanked(query, filter, limit) {
664
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
665
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
605
666
  const tokens = tokenise(query);
606
667
  if (tokens.length === 0 || !this.ftsAvailable) {
607
- return this.searchRankedFallback(query, filter, limit);
668
+ return this.searchRankedFallback(query, filter, safeLimit);
608
669
  }
609
670
  let effectiveKind = filter?.kind;
610
671
  if (filter?.lspKind !== void 0) {
@@ -624,11 +685,13 @@ var IndexStore = class {
624
685
  values.push(filter.lang);
625
686
  }
626
687
  if (filter?.file) {
627
- conditions.push("s.file LIKE ?");
628
- values.push(`%${filter.file}%`);
688
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
689
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
629
690
  }
630
691
  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);
692
+ const countRows = this.db.prepare(
693
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
694
+ ).all(...values);
632
695
  const total = countRows[0] ? Number(countRows[0].n) : 0;
633
696
  if (total === 0) return { results: [], total: 0 };
634
697
  const rows = this.db.prepare(
@@ -637,9 +700,13 @@ var IndexStore = class {
637
700
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
638
701
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
639
702
  WHERE ${where}
640
- ORDER BY bm25(symbols_fts)
703
+ ORDER BY
704
+ CASE WHEN lower(s.name) = lower(?) THEN 0
705
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
706
+ ELSE 2 END,
707
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
641
708
  LIMIT ?`
642
- ).all(...values, limit);
709
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
643
710
  return {
644
711
  results: rows.map((r) => ({
645
712
  id: r.id,
@@ -669,10 +736,28 @@ var IndexStore = class {
669
736
  }
670
737
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
671
738
  const bm25 = buildBm25Index(
672
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
739
+ candidates.map((c) => ({
740
+ id: c.id,
741
+ text: buildIndexableText(c.name, c.signature, c.docComment)
742
+ }))
673
743
  );
674
744
  const scored = bm25.score(query, (id) => candidateById.has(id));
675
- scored.sort((a, b) => b.score - a.score);
745
+ const q = query.trim().toLowerCase();
746
+ const rank = (id) => {
747
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
748
+ if (name === q) return 0;
749
+ if (name.startsWith(q)) return 1;
750
+ return 2;
751
+ };
752
+ scored.sort((a, b) => {
753
+ const rankDiff = rank(a.id) - rank(b.id);
754
+ if (rankDiff !== 0) return rankDiff;
755
+ const scoreDiff = b.score - a.score;
756
+ if (scoreDiff !== 0) return scoreDiff;
757
+ const left = expectDefined(candidateById.get(a.id));
758
+ const right = expectDefined(candidateById.get(b.id));
759
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
760
+ });
676
761
  const qTokens = tokenise(query);
677
762
  const results = scored.slice(0, limit).map(({ id, score }) => {
678
763
  const c = expectDefined(candidateById.get(id));
@@ -681,9 +766,7 @@ var IndexStore = class {
681
766
  return { results, total: candidates.length };
682
767
  }
683
768
  getAllIndexable() {
684
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
685
- ({ id, text }) => ({ id, text })
686
- );
769
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
687
770
  }
688
771
  /**
689
772
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -699,22 +782,16 @@ var IndexStore = class {
699
782
  // ─── Stats ───────────────────────────────────────────────────────────────────
700
783
  getStats() {
701
784
  const sizeBytes = this.sizeBytes();
702
- const lastRows = this.db.prepare(
703
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
704
- ).all();
785
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
705
786
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
706
787
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
707
788
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
708
789
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
709
790
  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();
791
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
713
792
  const byLang = {};
714
793
  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();
794
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
718
795
  const byKind = {};
719
796
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
720
797
  return {
@@ -730,17 +807,31 @@ var IndexStore = class {
730
807
  }
731
808
  setLastIndexed(ts2) {
732
809
  this.runWithRetry(() => {
733
- this.db.prepare(
734
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
735
- ).run(String(ts2));
810
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
811
+ });
812
+ }
813
+ getMetadata(key) {
814
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
815
+ return rows[0]?.value;
816
+ }
817
+ setMetadata(key, value) {
818
+ this.runWithRetry(() => {
819
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
736
820
  });
737
821
  }
738
822
  clearAll() {
739
823
  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");
824
+ this.db.exec("BEGIN IMMEDIATE");
825
+ try {
826
+ this.db.exec("DELETE FROM refs");
827
+ this.db.exec("DELETE FROM symbols");
828
+ this.db.exec("DELETE FROM files");
829
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
830
+ this.db.exec("COMMIT");
831
+ } catch (err) {
832
+ this.db.exec("ROLLBACK");
833
+ throw err;
834
+ }
744
835
  });
745
836
  }
746
837
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -831,6 +922,7 @@ var IndexStore = class {
831
922
  const allInserted = [];
832
923
  const refsToInsert = [];
833
924
  for (const entry of entries) {
925
+ const insertedForEntry = [];
834
926
  for (const s of entry.symbols) {
835
927
  const id = nextId++;
836
928
  symStmt.run(
@@ -848,9 +940,11 @@ var IndexStore = class {
848
940
  s.file
849
941
  );
850
942
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
851
- allInserted.push({ ...s, id });
943
+ const inserted = { ...s, id };
944
+ allInserted.push(inserted);
945
+ insertedForEntry.push(inserted);
852
946
  }
853
- for (const r of entry.refs) refsToInsert.push(r);
947
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
854
948
  }
855
949
  if (refsToInsert.length > 0) {
856
950
  const refStmt = this.db.prepare(
@@ -888,9 +982,7 @@ var IndexStore = class {
888
982
  */
889
983
  deleteRefsForFile(file) {
890
984
  this.runWithRetry(() => {
891
- this.db.prepare(
892
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
893
- ).run(file);
985
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
894
986
  });
895
987
  }
896
988
  /**
@@ -931,9 +1023,7 @@ var IndexStore = class {
931
1023
  * Find all references FROM a given symbol (what does this symbol call/use?).
932
1024
  */
933
1025
  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) => ({
1026
+ 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
1027
  id: r.id,
938
1028
  fromId: r.from_id,
939
1029
  toName: r.to_name,
@@ -950,6 +1040,325 @@ var IndexStore = class {
950
1040
  return 0;
951
1041
  }
952
1042
  }
1043
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
1044
+ /**
1045
+ * Derive a monorepo package name from an absolute file path.
1046
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
1047
+ * Returns `undefined` for files outside any recognisable package structure.
1048
+ */
1049
+ static derivePackage(filePath) {
1050
+ const f = filePath.replace(/\\/g, "/");
1051
+ const pkgsIdx = f.indexOf("/packages/");
1052
+ if (pkgsIdx !== -1) {
1053
+ const rest = f.slice(pkgsIdx + "/packages/".length);
1054
+ const seg = rest.split("/")[0];
1055
+ return seg ? `@wrongstack/${seg}` : void 0;
1056
+ }
1057
+ const appsIdx = f.indexOf("/apps/");
1058
+ if (appsIdx !== -1) {
1059
+ const rest = f.slice(appsIdx + "/apps/".length);
1060
+ const seg = rest.split("/")[0];
1061
+ return seg ? `app:${seg}` : void 0;
1062
+ }
1063
+ return void 0;
1064
+ }
1065
+ static packageFromImport(moduleName) {
1066
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
1067
+ const parts = moduleName.split("/");
1068
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
1069
+ }
1070
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
1071
+ if (!moduleName.startsWith(".")) return void 0;
1072
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
1073
+ const absolute = path.posix.normalize(
1074
+ path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
1075
+ );
1076
+ const extension = path.posix.extname(absolute);
1077
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
1078
+ const candidates = [
1079
+ absolute,
1080
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
1081
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
1082
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
1083
+ ];
1084
+ const indexedByPortablePath = new Map(
1085
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
1086
+ );
1087
+ for (const candidate of candidates) {
1088
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
1089
+ if (indexed) return indexed;
1090
+ }
1091
+ return void 0;
1092
+ }
1093
+ /**
1094
+ * Package-level graph: each workspace package is a node; edges are derived
1095
+ * from cross-package symbol references (a symbol in package A references a
1096
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
1097
+ */
1098
+ getPackageGraph() {
1099
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1100
+ const pkgNodes = /* @__PURE__ */ new Map();
1101
+ const fileToPkg = /* @__PURE__ */ new Map();
1102
+ const symbolToPkg = /* @__PURE__ */ new Map();
1103
+ for (const s of symbols) {
1104
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
1105
+ fileToPkg.set(s.file, pkg);
1106
+ symbolToPkg.set(s.id, pkg);
1107
+ const node = pkgNodes.get(pkg);
1108
+ if (node) {
1109
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
1110
+ } else {
1111
+ pkgNodes.set(pkg, {
1112
+ id: `pkg:${pkg}`,
1113
+ label: pkg,
1114
+ kind: "package",
1115
+ package: pkg,
1116
+ symbolCount: 1,
1117
+ fileCount: 0
1118
+ });
1119
+ }
1120
+ }
1121
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1122
+ for (const { file } of files) {
1123
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1124
+ const node = pkgNodes.get(pkg);
1125
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1126
+ }
1127
+ const refRows = this.db.prepare(
1128
+ `SELECT r.from_id, r.to_id, r.call_type
1129
+ FROM refs r
1130
+ WHERE r.to_id IS NOT NULL`
1131
+ ).all();
1132
+ const edgeMap = /* @__PURE__ */ new Map();
1133
+ for (const r of refRows) {
1134
+ if (r.call_type === "import") continue;
1135
+ const fromPkg = symbolToPkg.get(r.from_id);
1136
+ const toPkg = symbolToPkg.get(r.to_id);
1137
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
1138
+ const key = `${fromPkg}\0${toPkg}`;
1139
+ let e = edgeMap.get(key);
1140
+ if (!e) {
1141
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1142
+ edgeMap.set(key, e);
1143
+ }
1144
+ e.weight++;
1145
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1146
+ }
1147
+ const importRows = this.db.prepare(
1148
+ `SELECT r.from_id, r.to_name
1149
+ FROM refs r
1150
+ WHERE r.call_type = 'import'`
1151
+ ).all();
1152
+ for (const r of importRows) {
1153
+ const fromPkg = symbolToPkg.get(r.from_id);
1154
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
1155
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1156
+ const key = `${fromPkg}\0${toPkg}`;
1157
+ let edge = edgeMap.get(key);
1158
+ if (!edge) {
1159
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1160
+ edgeMap.set(key, edge);
1161
+ }
1162
+ edge.weight++;
1163
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1164
+ }
1165
+ const edges = [];
1166
+ for (const [key, e] of edgeMap) {
1167
+ const [source, target] = key.split("\0");
1168
+ let bestType = "call";
1169
+ let bestCount = 0;
1170
+ for (const [t, c] of e.types) {
1171
+ if (c > bestCount) {
1172
+ bestType = t;
1173
+ bestCount = c;
1174
+ }
1175
+ }
1176
+ edges.push({
1177
+ source: `pkg:${source}`,
1178
+ target: `pkg:${target}`,
1179
+ weight: e.weight,
1180
+ refType: bestType
1181
+ });
1182
+ }
1183
+ return { nodes: [...pkgNodes.values()], edges };
1184
+ }
1185
+ /**
1186
+ * File-level graph for a single package: each file is a node; edges are
1187
+ * derived from cross-file symbol references within the package.
1188
+ */
1189
+ getFileGraph(packageFilter) {
1190
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1191
+ const pkgSyms = allSymbols.filter(
1192
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
1193
+ );
1194
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
1195
+ const fileNodes = /* @__PURE__ */ new Map();
1196
+ const symToFile = /* @__PURE__ */ new Map();
1197
+ const fileStats = /* @__PURE__ */ new Map();
1198
+ for (const s of allSymbols) {
1199
+ symToFile.set(s.id, s.file);
1200
+ const current = fileStats.get(s.file);
1201
+ fileStats.set(s.file, {
1202
+ count: (current?.count ?? 0) + 1,
1203
+ lang: current?.lang ?? s.lang
1204
+ });
1205
+ }
1206
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
1207
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
1208
+ const ensureFileNode = (file) => {
1209
+ if (fileNodes.has(file)) return;
1210
+ const stats = fileStats.get(file);
1211
+ fileNodes.set(file, {
1212
+ id: `file:${file}`,
1213
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
1214
+ kind: "file",
1215
+ package: _IndexStore.derivePackage(file) ?? "(root)",
1216
+ file,
1217
+ symbolCount: stats?.count ?? 0,
1218
+ lang: stats?.lang,
1219
+ external: !localFiles.has(file)
1220
+ });
1221
+ };
1222
+ for (const file of localFiles) {
1223
+ ensureFileNode(file);
1224
+ }
1225
+ const refRows = this.db.prepare(
1226
+ `SELECT r.from_id, r.to_id, r.call_type
1227
+ FROM refs r
1228
+ WHERE r.to_id IS NOT NULL`
1229
+ ).all();
1230
+ const edgeMap = /* @__PURE__ */ new Map();
1231
+ for (const r of refRows) {
1232
+ if (r.call_type === "import") continue;
1233
+ const fromFile = symToFile.get(r.from_id);
1234
+ const toFile = symToFile.get(r.to_id);
1235
+ if (!fromFile || !toFile || fromFile === toFile) continue;
1236
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
1237
+ ensureFileNode(fromFile);
1238
+ ensureFileNode(toFile);
1239
+ const key = `${fromFile}\0${toFile}`;
1240
+ let e = edgeMap.get(key);
1241
+ if (!e) {
1242
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1243
+ edgeMap.set(key, e);
1244
+ }
1245
+ e.weight++;
1246
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1247
+ }
1248
+ const importRows = this.db.prepare(
1249
+ `SELECT r.from_id, r.to_name
1250
+ FROM refs r
1251
+ WHERE r.call_type = 'import'`
1252
+ ).all();
1253
+ for (const r of importRows) {
1254
+ const fromFile = symToFile.get(r.from_id);
1255
+ if (!fromFile || !localFiles.has(fromFile)) continue;
1256
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
1257
+ if (!toFile || fromFile === toFile) continue;
1258
+ ensureFileNode(fromFile);
1259
+ ensureFileNode(toFile);
1260
+ const key = `${fromFile}\0${toFile}`;
1261
+ let edge = edgeMap.get(key);
1262
+ if (!edge) {
1263
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1264
+ edgeMap.set(key, edge);
1265
+ }
1266
+ edge.weight++;
1267
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1268
+ }
1269
+ const edges = [];
1270
+ for (const [key, e] of edgeMap) {
1271
+ const [source, target] = key.split("\0");
1272
+ let bestType = "call";
1273
+ let bestCount = 0;
1274
+ for (const [t, c] of e.types) {
1275
+ if (c > bestCount) {
1276
+ bestType = t;
1277
+ bestCount = c;
1278
+ }
1279
+ }
1280
+ edges.push({
1281
+ source: `file:${source}`,
1282
+ target: `file:${target}`,
1283
+ weight: e.weight,
1284
+ refType: bestType
1285
+ });
1286
+ }
1287
+ return { nodes: [...fileNodes.values()], edges };
1288
+ }
1289
+ /**
1290
+ * Symbol-level graph for a single file: each symbol is a node; edges are
1291
+ * derived from intra-file and cross-file symbol references (who calls whom).
1292
+ */
1293
+ getSymbolGraph(fileFilter) {
1294
+ const allSymbols = this.db.prepare(
1295
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
1296
+ ).all();
1297
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
1298
+ if (syms.length === 0) return { nodes: [], edges: [] };
1299
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
1300
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
1301
+ const toGraphNode = (s) => ({
1302
+ id: `sym:${s.id}`,
1303
+ label: s.name,
1304
+ kind: "symbol",
1305
+ symbolId: s.id,
1306
+ symbolKind: s.kind,
1307
+ file: s.file,
1308
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
1309
+ lang: s.lang,
1310
+ line: s.line,
1311
+ signature: s.signature,
1312
+ scope: s.scope,
1313
+ external: s.file !== fileFilter
1314
+ });
1315
+ const symIds = new Set(syms.map((s) => s.id));
1316
+ const refRows = this.db.prepare(
1317
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1318
+ FROM refs r
1319
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
1320
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
1321
+ ).all(fileFilter, fileFilter);
1322
+ const edgeMap = /* @__PURE__ */ new Map();
1323
+ for (const r of refRows) {
1324
+ if (r.to_id === null) continue;
1325
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
1326
+ relatedIds.add(r.from_id);
1327
+ relatedIds.add(r.to_id);
1328
+ const key = `${r.from_id}\0${r.to_id}`;
1329
+ let e = edgeMap.get(key);
1330
+ if (!e) {
1331
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1332
+ edgeMap.set(key, e);
1333
+ }
1334
+ e.weight++;
1335
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1336
+ }
1337
+ const edges = [];
1338
+ for (const [key, e] of edgeMap) {
1339
+ const [fromId, toId] = key.split("\0").map(Number);
1340
+ let bestType = "call";
1341
+ let bestCount = 0;
1342
+ for (const [t, c] of e.types) {
1343
+ if (c > bestCount) {
1344
+ bestType = t;
1345
+ bestCount = c;
1346
+ }
1347
+ }
1348
+ edges.push({
1349
+ source: `sym:${fromId}`,
1350
+ target: `sym:${toId}`,
1351
+ weight: e.weight,
1352
+ refType: bestType
1353
+ });
1354
+ }
1355
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
1356
+ const aExternal = a.file === fileFilter ? 0 : 1;
1357
+ const bExternal = b.file === fileFilter ? 0 : 1;
1358
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
1359
+ }).map(toGraphNode);
1360
+ return { nodes, edges };
1361
+ }
953
1362
  close() {
954
1363
  this.stmtCache.clear();
955
1364
  try {
@@ -960,14 +1369,14 @@ var IndexStore = class {
960
1369
  };
961
1370
 
962
1371
  // src/codebase-index/background-indexer.ts
963
- import * as fs7 from "node:fs";
1372
+ import * as fs8 from "node:fs";
964
1373
  import { fileURLToPath } from "node:url";
965
1374
  import { Worker } from "node:worker_threads";
966
1375
 
967
1376
  // src/codebase-index/indexer.ts
968
1377
  import { expectDefined as expectDefined5 } from "@wrongstack/core";
969
- import * as fs6 from "node:fs/promises";
970
- import * as path7 from "node:path";
1378
+ import * as fs7 from "node:fs/promises";
1379
+ import * as path8 from "node:path";
971
1380
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
972
1381
 
973
1382
  // src/codebase-index/ts-parser.ts
@@ -1044,6 +1453,14 @@ function getJsDoc(node, sourceFile) {
1044
1453
  }
1045
1454
  return "";
1046
1455
  }
1456
+ function hasFunctionLikeAncestor(node) {
1457
+ let current = node.parent;
1458
+ while (current) {
1459
+ if (ts.isFunctionLike(current)) return true;
1460
+ current = current.parent;
1461
+ }
1462
+ return false;
1463
+ }
1047
1464
  function buildScope(node) {
1048
1465
  const parts = [];
1049
1466
  let current = node.parent;
@@ -1071,6 +1488,10 @@ function parseSymbols(opts) {
1071
1488
  function visit(node) {
1072
1489
  const kind = kindOf(node);
1073
1490
  if (kind) {
1491
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
1492
+ ts.forEachChild(node, visit);
1493
+ return;
1494
+ }
1074
1495
  const nameNode = node.name;
1075
1496
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
1076
1497
  const name = nameNode.text;
@@ -1160,8 +1581,34 @@ function detectLang(file) {
1160
1581
  // src/codebase-index/go-parser.ts
1161
1582
  import { spawn } from "node:child_process";
1162
1583
  import * as os from "node:os";
1584
+ import * as path3 from "node:path";
1585
+ import * as fs3 from "node:fs/promises";
1586
+
1587
+ // src/_win32-resolve.ts
1588
+ import * as fs2 from "node:fs";
1163
1589
  import * as path2 from "node:path";
1164
- import * as fs2 from "node:fs/promises";
1590
+ function resolveWin32Command(cmd) {
1591
+ if (process.platform !== "win32") return cmd;
1592
+ if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
1593
+ return cmd;
1594
+ }
1595
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
1596
+ const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
1597
+ for (const dir of pathDirs) {
1598
+ const base = path2.join(dir, cmd);
1599
+ for (const ext of pathext) {
1600
+ const full = `${base}${ext}`;
1601
+ try {
1602
+ fs2.accessSync(full, fs2.constants.X_OK);
1603
+ return full;
1604
+ } catch {
1605
+ }
1606
+ }
1607
+ }
1608
+ return cmd;
1609
+ }
1610
+
1611
+ // src/codebase-index/go-parser.ts
1165
1612
  async function parseSymbols2(opts) {
1166
1613
  const { file, content, lang } = opts;
1167
1614
  try {
@@ -1462,31 +1909,46 @@ func formatType(t ast.Expr) string {
1462
1909
  }
1463
1910
  `;
1464
1911
  async function syncGoParse(filePath, content, lang) {
1465
- const tmpDir = await fs2.mkdtemp(path2.join(os.tmpdir(), "ws-go-parse-"));
1912
+ const tmpDir = await fs3.mkdtemp(path3.join(os.tmpdir(), "ws-go-parse-"));
1466
1913
  try {
1467
- const scriptPath = path2.join(tmpDir, "parse.go");
1468
- await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
1469
- const proc = spawn("go", ["run", scriptPath], {
1470
- stdio: ["pipe", "pipe", "pipe"],
1471
- windowsHide: true
1472
- });
1473
- let stdout = "";
1474
- proc.stdout?.on("data", (chunk) => {
1475
- stdout += chunk.toString();
1476
- });
1477
- proc.stdin?.write(content);
1478
- proc.stdin?.end();
1479
- const { code } = await Promise.race([
1480
- new Promise((resolve2) => {
1481
- proc.on("close", (c) => resolve2({ code: c }));
1482
- }),
1483
- new Promise(
1484
- (_, reject) => setTimeout(() => {
1914
+ const scriptPath = path3.join(tmpDir, "parse.go");
1915
+ await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
1916
+ const goBinary = resolveWin32Command("go");
1917
+ const goResult = await new Promise(
1918
+ (resolve2, reject) => {
1919
+ let settled = false;
1920
+ const proc = spawn(goBinary, ["run", scriptPath], {
1921
+ stdio: ["pipe", "pipe", "pipe"],
1922
+ windowsHide: true
1923
+ });
1924
+ proc.on("error", (err) => {
1925
+ if (settled) return;
1926
+ settled = true;
1927
+ reject(err);
1928
+ });
1929
+ let stdout2 = "";
1930
+ proc.stdout?.on("data", (chunk) => {
1931
+ stdout2 += chunk.toString();
1932
+ });
1933
+ proc.stderr?.resume();
1934
+ proc.stdin?.write(content);
1935
+ proc.stdin?.end();
1936
+ const timer = setTimeout(() => {
1937
+ if (settled) return;
1938
+ settled = true;
1485
1939
  proc.kill("SIGKILL");
1486
1940
  reject(new Error("timeout"));
1487
- }, 15e3)
1488
- )
1489
- ]).catch(() => ({ code: -1 }));
1941
+ }, 15e3);
1942
+ timer.unref?.();
1943
+ proc.on("close", (code2) => {
1944
+ if (settled) return;
1945
+ settled = true;
1946
+ clearTimeout(timer);
1947
+ resolve2({ code: code2, stdout: stdout2 });
1948
+ });
1949
+ }
1950
+ );
1951
+ const { code, stdout } = goResult;
1490
1952
  if (code !== 0 || !stdout.trim()) {
1491
1953
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1492
1954
  }
@@ -1508,15 +1970,15 @@ async function syncGoParse(filePath, content, lang) {
1508
1970
  } catch {
1509
1971
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1510
1972
  } finally {
1511
- await fs2.rm(tmpDir, { recursive: true, force: true });
1973
+ await fs3.rm(tmpDir, { recursive: true, force: true });
1512
1974
  }
1513
1975
  }
1514
1976
 
1515
1977
  // src/codebase-index/py-parser.ts
1516
- import { spawn as spawn2 } from "node:child_process";
1517
- import * as fs3 from "node:fs/promises";
1978
+ import { spawn as spawn2, spawnSync } from "node:child_process";
1979
+ import * as fs4 from "node:fs/promises";
1518
1980
  import * as os2 from "node:os";
1519
- import * as path3 from "node:path";
1981
+ import * as path4 from "node:path";
1520
1982
  async function parseSymbols3(opts) {
1521
1983
  const { file, content, lang } = opts;
1522
1984
  try {
@@ -1728,36 +2190,74 @@ visitor.visit(tree)
1728
2190
 
1729
2191
  print(json.dumps([s.to_dict() for s in syms]))
1730
2192
  `;
1731
- var _cachedScriptPath = null;
1732
- async function syncPyParse(filePath, content, lang) {
1733
- try {
1734
- if (!_cachedScriptPath) {
1735
- const tmpDir = path3.join(os2.tmpdir(), "ws-py-parse");
1736
- await fs3.mkdir(tmpDir, { recursive: true });
1737
- _cachedScriptPath = path3.join(tmpDir, "parse.py");
1738
- await fs3.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1739
- }
1740
- const proc = spawn2("python", [_cachedScriptPath, filePath], {
2193
+ function resolvePython() {
2194
+ const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
2195
+ for (const name of candidates) {
2196
+ const resolved = resolveWin32Command(name);
2197
+ const result = spawnSync(resolved, ["--version"], {
2198
+ stdio: "pipe",
2199
+ timeout: 5e3
2200
+ });
2201
+ if (result.error) continue;
2202
+ if (result.status !== 0) continue;
2203
+ return resolved;
2204
+ }
2205
+ return null;
2206
+ }
2207
+ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
2208
+ return new Promise((resolve2, reject) => {
2209
+ let settled = false;
2210
+ const proc = spawn2(pyBinary, [scriptPath, filePath], {
1741
2211
  stdio: ["pipe", "pipe", "pipe"],
1742
2212
  windowsHide: true
1743
2213
  });
2214
+ proc.on("error", (err) => {
2215
+ if (settled) return;
2216
+ settled = true;
2217
+ reject(err);
2218
+ });
1744
2219
  proc.stdin?.write(content);
1745
2220
  proc.stdin?.end();
1746
2221
  let stdout = "";
1747
2222
  proc.stdout?.on("data", (chunk) => {
1748
2223
  stdout += chunk.toString();
1749
2224
  });
1750
- const { code } = await Promise.race([
1751
- new Promise((resolve2) => {
1752
- proc.on("close", (c) => resolve2({ code: c }));
1753
- }),
1754
- new Promise(
1755
- (_, reject) => setTimeout(() => {
1756
- proc.kill("SIGKILL");
1757
- reject(new Error("timeout"));
1758
- }, 15e3)
1759
- )
1760
- ]).catch(() => ({ code: -1 }));
2225
+ proc.stderr?.resume();
2226
+ const timer = setTimeout(() => {
2227
+ if (settled) return;
2228
+ settled = true;
2229
+ proc.kill("SIGKILL");
2230
+ reject(new Error("timeout"));
2231
+ }, 15e3);
2232
+ timer.unref?.();
2233
+ proc.on("close", (code) => {
2234
+ if (settled) return;
2235
+ settled = true;
2236
+ clearTimeout(timer);
2237
+ resolve2({ code, stdout });
2238
+ });
2239
+ });
2240
+ }
2241
+ var _cachedScriptPath = null;
2242
+ var _cachedPyBinary = null;
2243
+ async function syncPyParse(filePath, content, lang) {
2244
+ try {
2245
+ if (!_cachedScriptPath) {
2246
+ const tmpDir = path4.join(os2.tmpdir(), "ws-py-parse");
2247
+ await fs4.mkdir(tmpDir, { recursive: true });
2248
+ _cachedScriptPath = path4.join(tmpDir, "parse.py");
2249
+ await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
2250
+ }
2251
+ if (!_cachedPyBinary) {
2252
+ _cachedPyBinary = resolvePython();
2253
+ if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
2254
+ }
2255
+ const { code, stdout } = await spawnPyParser(
2256
+ _cachedPyBinary,
2257
+ _cachedScriptPath,
2258
+ filePath,
2259
+ content
2260
+ );
1761
2261
  if (code !== 0 || !stdout.trim()) {
1762
2262
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1763
2263
  }
@@ -1784,8 +2284,8 @@ async function syncPyParse(filePath, content, lang) {
1784
2284
  // src/codebase-index/rs-parser.ts
1785
2285
  import { expectDefined as expectDefined2 } from "@wrongstack/core";
1786
2286
  import { execFileSync, spawn as spawn3 } from "node:child_process";
1787
- import * as fs4 from "node:fs/promises";
1788
- import * as path4 from "node:path";
2287
+ import * as fs5 from "node:fs/promises";
2288
+ import * as path5 from "node:path";
1789
2289
  async function parseSymbols4(opts) {
1790
2290
  const { file, content, lang } = opts;
1791
2291
  const nativeAvailable = checkNativeParser();
@@ -1798,7 +2298,7 @@ async function parseSymbols4(opts) {
1798
2298
  function checkNativeParser() {
1799
2299
  try {
1800
2300
  execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
1801
- const toolsDir = path4.join(process.cwd(), "tools");
2301
+ const toolsDir = path5.join(process.cwd(), "tools");
1802
2302
  try {
1803
2303
  execFileSync(
1804
2304
  "cargo",
@@ -1808,7 +2308,7 @@ function checkNativeParser() {
1808
2308
  "--format-version",
1809
2309
  "1",
1810
2310
  "--manifest-path",
1811
- path4.join(toolsDir, "Cargo.toml")
2311
+ path5.join(toolsDir, "Cargo.toml")
1812
2312
  ],
1813
2313
  { stdio: "pipe", windowsHide: true }
1814
2314
  );
@@ -1822,34 +2322,49 @@ function checkNativeParser() {
1822
2322
  }
1823
2323
  async function tryNativeParse(file, content) {
1824
2324
  try {
1825
- const toolsDir = path4.join(process.cwd(), "tools");
1826
- const crateDir = path4.join(toolsDir, "syn-parser");
1827
- const tmpFile = path4.join(crateDir, "src", "input.rs");
1828
- await fs4.writeFile(tmpFile, content, "utf8");
1829
- const proc = spawn3(
1830
- "cargo",
1831
- ["run", "--manifest-path", path4.join(toolsDir, "Cargo.toml")],
1832
- {
1833
- cwd: process.cwd(),
1834
- stdio: ["pipe", "pipe", "pipe"],
1835
- windowsHide: true
1836
- }
1837
- );
1838
- let stdout = "";
1839
- proc.stdout?.on("data", (chunk) => {
1840
- stdout += chunk.toString();
1841
- });
1842
- const { code } = await Promise.race([
1843
- new Promise((resolve2) => {
1844
- proc.on("close", (c) => resolve2({ code: c }));
1845
- }),
1846
- new Promise(
1847
- (_, reject) => setTimeout(() => {
2325
+ const toolsDir = path5.join(process.cwd(), "tools");
2326
+ const crateDir = path5.join(toolsDir, "syn-parser");
2327
+ const tmpFile = path5.join(crateDir, "src", "input.rs");
2328
+ await fs5.writeFile(tmpFile, content, "utf8");
2329
+ const cargoBinary = resolveWin32Command("cargo");
2330
+ const result = await new Promise(
2331
+ (resolve2, reject) => {
2332
+ let settled = false;
2333
+ const proc = spawn3(
2334
+ cargoBinary,
2335
+ ["run", "--manifest-path", path5.join(toolsDir, "Cargo.toml")],
2336
+ {
2337
+ cwd: process.cwd(),
2338
+ stdio: ["pipe", "pipe", "pipe"],
2339
+ windowsHide: true
2340
+ }
2341
+ );
2342
+ proc.on("error", (err) => {
2343
+ if (settled) return;
2344
+ settled = true;
2345
+ reject(err);
2346
+ });
2347
+ let stdout2 = "";
2348
+ proc.stdout?.on("data", (chunk) => {
2349
+ stdout2 += chunk.toString();
2350
+ });
2351
+ proc.stderr?.resume();
2352
+ const timer = setTimeout(() => {
2353
+ if (settled) return;
2354
+ settled = true;
1848
2355
  proc.kill("SIGKILL");
1849
2356
  reject(new Error("timeout"));
1850
- }, 15e3)
1851
- )
1852
- ]).catch(() => ({ code: -1 }));
2357
+ }, 15e3);
2358
+ timer.unref?.();
2359
+ proc.on("close", (c) => {
2360
+ if (settled) return;
2361
+ settled = true;
2362
+ clearTimeout(timer);
2363
+ resolve2({ code: c, stdout: stdout2 });
2364
+ });
2365
+ }
2366
+ );
2367
+ const { code, stdout } = result;
1853
2368
  if (code === 0 && stdout.trim()) {
1854
2369
  const symbols = JSON.parse(stdout.trim());
1855
2370
  return {
@@ -1932,7 +2447,7 @@ function regexParse(opts) {
1932
2447
 
1933
2448
  // src/codebase-index/json-parser.ts
1934
2449
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
1935
- import * as path5 from "node:path";
2450
+ import * as path6 from "node:path";
1936
2451
  function parseSymbols5(opts) {
1937
2452
  const { file, content, lang } = opts;
1938
2453
  try {
@@ -1944,9 +2459,9 @@ function parseSymbols5(opts) {
1944
2459
  function regexParse2(opts) {
1945
2460
  const { file, content, lang } = opts;
1946
2461
  const symbols = [];
1947
- const basename2 = path5.basename(file).toLowerCase();
1948
- const isPackageJson = basename2 === "package.json";
1949
- const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2462
+ const basename3 = path6.basename(file).toLowerCase();
2463
+ const isPackageJson = basename3 === "package.json";
2464
+ const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
1950
2465
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
1951
2466
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
1952
2467
  const lines = content.split("\n");
@@ -1970,11 +2485,11 @@ function regexParse2(opts) {
1970
2485
  const line = lineFromOffset(offset);
1971
2486
  symbols.push(
1972
2487
  makeSymbol({
1973
- name: path5.basename(file),
2488
+ name: path6.basename(file),
1974
2489
  kind: "object",
1975
2490
  line,
1976
2491
  col: 0,
1977
- signature: `"${path5.basename(file)}" = { ... }`,
2492
+ signature: `"${path6.basename(file)}" = { ... }`,
1978
2493
  file,
1979
2494
  lang
1980
2495
  })
@@ -2285,8 +2800,8 @@ function makeSymbol2(opts) {
2285
2800
  }
2286
2801
 
2287
2802
  // src/codebase-index/gitignore.ts
2288
- import * as fs5 from "node:fs/promises";
2289
- import * as path6 from "node:path";
2803
+ import * as fs6 from "node:fs/promises";
2804
+ import * as path7 from "node:path";
2290
2805
  import { compileGlob } from "@wrongstack/core";
2291
2806
  function globBody(glob) {
2292
2807
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -2332,7 +2847,7 @@ function compileGitignore(lines) {
2332
2847
  async function loadGitignoreMatcher(projectRoot) {
2333
2848
  let lines = [];
2334
2849
  try {
2335
- const raw = await fs5.readFile(path6.join(projectRoot, ".gitignore"), "utf8");
2850
+ const raw = await fs6.readFile(path7.join(projectRoot, ".gitignore"), "utf8");
2336
2851
  lines = raw.split("\n");
2337
2852
  } catch {
2338
2853
  }
@@ -2348,9 +2863,7 @@ function yieldEventLoop() {
2348
2863
  function throwIfAborted(signal) {
2349
2864
  if (!signal?.aborted) return;
2350
2865
  if (signal.reason instanceof Error) throw signal.reason;
2351
- throw new Error(
2352
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
2353
- );
2866
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
2354
2867
  }
2355
2868
  function isAbortError(err) {
2356
2869
  return err instanceof DOMException && err.name === "AbortError";
@@ -2366,8 +2879,20 @@ var DEFAULT_IGNORE = [
2366
2879
  "__snapshots__",
2367
2880
  ".nyc_output"
2368
2881
  ];
2882
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2883
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2884
+ function isWithinProject(projectRoot, file) {
2885
+ const rel = path8.relative(projectRoot, file);
2886
+ return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
2887
+ }
2888
+ function isMissingPathError(err) {
2889
+ const code = err?.code;
2890
+ return code === "ENOENT" || code === "ENOTDIR";
2891
+ }
2369
2892
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2370
2893
  const results = [];
2894
+ const errors = [];
2895
+ let complete = true;
2371
2896
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2372
2897
  const globs = [
2373
2898
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -2390,21 +2915,23 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2390
2915
  }
2391
2916
  let entries;
2392
2917
  try {
2393
- entries = await fs6.readdir(dir, { withFileTypes: true });
2394
- } catch {
2918
+ entries = await fs7.readdir(dir, { withFileTypes: true });
2919
+ } catch (err) {
2920
+ complete = false;
2921
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
2395
2922
  return;
2396
2923
  }
2397
2924
  dirCount++;
2398
2925
  for (const e of entries) {
2399
2926
  if (ignoreSet.has(e.name)) continue;
2400
- const full = path7.join(dir, e.name);
2401
- const rel = path7.relative(projectRoot, full).replace(/\\/g, "/");
2927
+ const full = path8.join(dir, e.name);
2928
+ const rel = path8.relative(projectRoot, full).replace(/\\/g, "/");
2402
2929
  if (e.isDirectory()) {
2403
2930
  if (isGitIgnored(rel, true)) continue;
2404
2931
  await walk(full);
2405
2932
  } else if (e.isFile()) {
2406
- if (isGitIgnored(rel, false)) continue;
2407
- const ext = path7.extname(e.name);
2933
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2934
+ const ext = path8.extname(e.name).toLowerCase();
2408
2935
  for (const { ext: extName, pat } of globs) {
2409
2936
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2410
2937
  results.push(full);
@@ -2415,7 +2942,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2415
2942
  }
2416
2943
  };
2417
2944
  await walk(projectRoot);
2418
- return results;
2945
+ return { files: results, complete, errors };
2419
2946
  }
2420
2947
  async function parseFile(file, content, lang) {
2421
2948
  switch (lang) {
@@ -2438,6 +2965,26 @@ async function parseFile(file, content, lang) {
2438
2965
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2439
2966
  }
2440
2967
  }
2968
+ function assignRefsToSymbols2(refs, symbols) {
2969
+ if (refs.length === 0 || symbols.length === 0) return [];
2970
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
2971
+ const seen = /* @__PURE__ */ new Set();
2972
+ const assigned = [];
2973
+ for (const ref of refs) {
2974
+ let owner;
2975
+ for (const symbol of ordered) {
2976
+ if (symbol.line > ref.line) break;
2977
+ owner = symbol;
2978
+ }
2979
+ if (!owner && ref.callType === "import") owner = ordered[0];
2980
+ if (!owner || owner.id <= 0) continue;
2981
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
2982
+ if (seen.has(key)) continue;
2983
+ seen.add(key);
2984
+ assigned.push({ ...ref, fromId: owner.id });
2985
+ }
2986
+ return assigned;
2987
+ }
2441
2988
  async function runIndexer(_ctx, opts) {
2442
2989
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
2443
2990
  try {
@@ -2450,7 +2997,9 @@ async function runIndexer(_ctx, opts) {
2450
2997
  }
2451
2998
  }
2452
2999
  async function runIndexerWithStore(store, opts) {
2453
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
3000
+ const { projectRoot, langs, ignore = [], signal } = opts;
3001
+ const relationGraphVersion = "2";
3002
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
2454
3003
  const startMs = Date.now();
2455
3004
  const errors = [];
2456
3005
  const langStats = {};
@@ -2459,10 +3008,18 @@ async function runIndexerWithStore(store, opts) {
2459
3008
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2460
3009
  let files;
2461
3010
  let discoveredFiles = null;
3011
+ let discoveryComplete = true;
2462
3012
  if (opts.files && opts.files.length > 0) {
2463
- files = opts.files.map((f) => path7.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path7.relative(projectRoot, f).replace(/\\/g, "/"), false));
3013
+ files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
3014
+ if (!isWithinProject(projectRoot, f)) return false;
3015
+ const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
3016
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
3017
+ });
2464
3018
  } else {
2465
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3019
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3020
+ files = discovery.files;
3021
+ errors.push(...discovery.errors);
3022
+ discoveryComplete = discovery.complete;
2466
3023
  discoveredFiles = new Set(files);
2467
3024
  }
2468
3025
  if (langs && langs.length > 0) {
@@ -2487,36 +3044,66 @@ async function runIndexerWithStore(store, opts) {
2487
3044
  }
2488
3045
  const statOpts = signal ? { signal } : {};
2489
3046
  const statReadParse = await Promise.allSettled(
2490
- batchFiles.map(async (file) => {
2491
- let stat2;
2492
- try {
2493
- stat2 = await fs6.stat(file, statOpts);
2494
- } catch (e) {
2495
- if (isAbortError(e)) throw e;
2496
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
2497
- }
2498
- if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
2499
- const lang = detectLang(file);
2500
- if (!lang) return { file, stat: stat2, lang: "", parsed: null };
2501
- const meta = existingMeta.get(file);
2502
- if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
2503
- return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
2504
- }
2505
- let content;
2506
- try {
2507
- content = await fs6.readFile(file, { encoding: "utf8", signal });
2508
- } catch (e) {
2509
- if (isAbortError(e)) throw e;
2510
- return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
2511
- }
2512
- let parsed;
2513
- try {
2514
- parsed = await parseFile(file, content, lang);
2515
- } catch (e) {
2516
- return { file, stat: stat2, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
3047
+ batchFiles.map(
3048
+ async (file) => {
3049
+ let stat2;
3050
+ try {
3051
+ stat2 = await fs7.stat(file, statOpts);
3052
+ } catch (e) {
3053
+ if (isAbortError(e)) throw e;
3054
+ return {
3055
+ file,
3056
+ stat: null,
3057
+ lang: "",
3058
+ parsed: null,
3059
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
3060
+ missing: isMissingPathError(e)
3061
+ };
3062
+ }
3063
+ if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
3064
+ const lang = detectLang(file);
3065
+ if (!lang) return { file, stat: stat2, lang: "", parsed: null };
3066
+ if (stat2.size > MAX_INDEX_FILE_BYTES) {
3067
+ return {
3068
+ file,
3069
+ stat: stat2,
3070
+ lang,
3071
+ parsed: null,
3072
+ error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
3073
+ };
3074
+ }
3075
+ const meta = existingMeta.get(file);
3076
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
3077
+ return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
3078
+ }
3079
+ let content;
3080
+ try {
3081
+ content = await fs7.readFile(file, { encoding: "utf8", signal });
3082
+ } catch (e) {
3083
+ if (isAbortError(e)) throw e;
3084
+ return {
3085
+ file,
3086
+ stat: stat2,
3087
+ lang,
3088
+ parsed: null,
3089
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
3090
+ };
3091
+ }
3092
+ let parsed;
3093
+ try {
3094
+ parsed = await parseFile(file, content, lang);
3095
+ } catch (e) {
3096
+ return {
3097
+ file,
3098
+ stat: stat2,
3099
+ lang,
3100
+ parsed: null,
3101
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
3102
+ };
3103
+ }
3104
+ return { file, stat: stat2, lang, parsed, content };
2517
3105
  }
2518
- return { file, stat: stat2, lang, parsed, content };
2519
- })
3106
+ )
2520
3107
  );
2521
3108
  const batchEntries = [];
2522
3109
  const deleteForFiles = [];
@@ -2531,8 +3118,8 @@ async function runIndexerWithStore(store, opts) {
2531
3118
  }
2532
3119
  const result = settled.value;
2533
3120
  if (result.error) {
2534
- if (result.stat) store.deleteFile(file);
2535
- if (result.error.includes("error:")) errors.push(result.error);
3121
+ if (result.missing) store.deleteFile(file);
3122
+ errors.push(`${file}: ${result.error}`);
2536
3123
  continue;
2537
3124
  }
2538
3125
  const { stat: stat2, lang, parsed } = result;
@@ -2556,6 +3143,8 @@ async function runIndexerWithStore(store, opts) {
2556
3143
  continue;
2557
3144
  }
2558
3145
  if (parsed.symbols.length === 0) {
3146
+ store.deleteRefsForFile(file);
3147
+ store.deleteSymbolsForFile(file);
2559
3148
  store.upsertFile({
2560
3149
  file,
2561
3150
  lang,
@@ -2566,15 +3155,11 @@ async function runIndexerWithStore(store, opts) {
2566
3155
  filesIndexed++;
2567
3156
  continue;
2568
3157
  }
2569
- const refs = [];
2570
- if (parsed.refs && parsed.refs.length > 0) {
2571
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
2572
- }
2573
3158
  batchEntries.push({
2574
3159
  file,
2575
3160
  lang,
2576
3161
  symbols: parsed.symbols,
2577
- refs,
3162
+ refs: parsed.refs ?? [],
2578
3163
  mtimeMs: Math.floor(stat2.mtimeMs),
2579
3164
  symbolCount: parsed.symbols.length
2580
3165
  });
@@ -2582,34 +3167,12 @@ async function runIndexerWithStore(store, opts) {
2582
3167
  }
2583
3168
  if (batchEntries.length > 0) {
2584
3169
  try {
2585
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
2586
- let cursor = 0;
3170
+ store.commitBatch(batchEntries, { deleteForFiles });
2587
3171
  for (const entry of batchEntries) {
2588
3172
  const count = entry.symbols.length;
2589
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
2590
- cursor += count;
2591
3173
  symbolsIndexed += count;
2592
3174
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
2593
3175
  filesIndexed++;
2594
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2595
- const refsByLine = /* @__PURE__ */ new Map();
2596
- for (let i = 0; i < symbolsWithIds.length; i++) {
2597
- const sym = symbolsWithIds[i];
2598
- let arr = refsByLine.get(sym.line);
2599
- if (!arr) {
2600
- arr = [];
2601
- refsByLine.set(sym.line, arr);
2602
- }
2603
- arr.push(i);
2604
- }
2605
- for (const ref of entry.refs) {
2606
- const indices = refsByLine.get(ref.line);
2607
- if (indices && indices.length > 0) {
2608
- const idx = indices.shift();
2609
- ref.fromId = symbolsWithIds[idx].id;
2610
- }
2611
- }
2612
- }
2613
3176
  }
2614
3177
  } catch (err) {
2615
3178
  const message = err instanceof Error ? err.message : String(err);
@@ -2623,23 +3186,7 @@ async function runIndexerWithStore(store, opts) {
2623
3186
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
2624
3187
  filesIndexed++;
2625
3188
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2626
- const refsByLine = /* @__PURE__ */ new Map();
2627
- for (const sym of symbolsWithIds) {
2628
- let arr = refsByLine.get(sym.line);
2629
- if (!arr) {
2630
- arr = [];
2631
- refsByLine.set(sym.line, arr);
2632
- }
2633
- arr.push(sym);
2634
- }
2635
- const fallbackBatch = [];
2636
- for (const ref of entry.refs) {
2637
- const syms = refsByLine.get(ref.line);
2638
- if (syms && syms.length > 0) {
2639
- const sym = syms.shift();
2640
- fallbackBatch.push({ ...ref, fromId: sym.id });
2641
- }
2642
- }
3189
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
2643
3190
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
2644
3191
  }
2645
3192
  store.upsertFile({
@@ -2650,19 +3197,23 @@ async function runIndexerWithStore(store, opts) {
2650
3197
  lastIndexed: Date.now()
2651
3198
  });
2652
3199
  } catch (innerErr) {
2653
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
3200
+ errors.push(
3201
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
3202
+ );
2654
3203
  }
2655
3204
  }
2656
3205
  }
2657
3206
  }
2658
3207
  }
2659
- if (discoveredFiles) {
3208
+ if (discoveredFiles && discoveryComplete) {
2660
3209
  for (const [file_] of existingMeta) {
2661
3210
  if (!discoveredFiles.has(file_)) {
2662
3211
  store.deleteFile(file_);
2663
3212
  }
2664
3213
  }
2665
3214
  }
3215
+ store.resolveRefs();
3216
+ store.setMetadata("relation_graph_version", relationGraphVersion);
2666
3217
  const durationMs = Date.now() - startMs;
2667
3218
  store.setLastIndexed(Date.now());
2668
3219
  return {
@@ -2722,11 +3273,35 @@ function statsService(args) {
2722
3273
  store.close();
2723
3274
  }
2724
3275
  }
3276
+ function packageGraphService(args) {
3277
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3278
+ try {
3279
+ return store.getPackageGraph();
3280
+ } finally {
3281
+ store.close();
3282
+ }
3283
+ }
3284
+ function fileGraphService(args) {
3285
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3286
+ try {
3287
+ return store.getFileGraph(args.packageFilter);
3288
+ } finally {
3289
+ store.close();
3290
+ }
3291
+ }
3292
+ function symbolGraphService(args) {
3293
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
3294
+ try {
3295
+ return store.getSymbolGraph(args.fileFilter);
3296
+ } finally {
3297
+ store.close();
3298
+ }
3299
+ }
2725
3300
 
2726
3301
  // src/codebase-index/background-indexer.ts
2727
3302
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
2728
3303
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
2729
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
3304
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
2730
3305
  var _ready = false;
2731
3306
  var _indexing = false;
2732
3307
  var _currentFile = 0;
@@ -2773,7 +3348,7 @@ function resolveWorkerUrl() {
2773
3348
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
2774
3349
  try {
2775
3350
  const url = new URL(rel, import.meta.url);
2776
- if (url.protocol === "file:" && fs7.existsSync(fileURLToPath(url))) return url;
3351
+ if (url.protocol === "file:" && fs8.existsSync(fileURLToPath(url))) return url;
2777
3352
  } catch {
2778
3353
  }
2779
3354
  }
@@ -2804,14 +3379,22 @@ function ensureWorker() {
2804
3379
  if (!entry) return;
2805
3380
  pending.delete(msg.id);
2806
3381
  if (msg.ok) entry.resolve(msg.result);
2807
- else entry.reject(new Error(msg.error));
3382
+ else {
3383
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
3384
+ if (msg.errorName && msg.errorName !== "Error") {
3385
+ error.name = msg.errorName;
3386
+ }
3387
+ entry.reject(error);
3388
+ }
2808
3389
  });
2809
3390
  w.on("error", (err) => {
3391
+ if (worker !== w) return;
2810
3392
  worker = null;
2811
3393
  failAllPending(err);
2812
3394
  });
2813
3395
  w.on("exit", () => {
2814
- if (worker === w) worker = null;
3396
+ if (worker !== w) return;
3397
+ worker = null;
2815
3398
  failAllPending(new Error("codebase-index worker exited"));
2816
3399
  });
2817
3400
  worker = w;
@@ -2836,6 +3419,11 @@ function shutdownCodebaseIndexHost() {
2836
3419
  function callIndexOp(op, args, opts) {
2837
3420
  const w = ensureWorker();
2838
3421
  if (!w) return callInline(op, args, opts);
3422
+ if (opts.signal?.aborted) {
3423
+ return Promise.reject(
3424
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
3425
+ );
3426
+ }
2839
3427
  return new Promise((resolve2, reject) => {
2840
3428
  const id = nextRpcId++;
2841
3429
  const timer = setTimeout(() => {
@@ -2850,8 +3438,7 @@ function callIndexOp(op, args, opts) {
2850
3438
  const onAbort = () => {
2851
3439
  w.postMessage({ type: "cancel", id });
2852
3440
  };
2853
- if (opts.signal?.aborted) onAbort();
2854
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
3441
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2855
3442
  const cleanup = () => {
2856
3443
  clearTimeout(timer);
2857
3444
  opts.signal?.removeEventListener("abort", onAbort);
@@ -3039,7 +3626,7 @@ var codebaseIndexTool = {
3039
3626
  category: "Project",
3040
3627
  icon: "index",
3041
3628
  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.",
3042
- 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.",
3629
+ 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.",
3043
3630
  permission: "confirm",
3044
3631
  mutating: true,
3045
3632
  capabilities: ["fs.write.outside-project"],
@@ -3095,12 +3682,14 @@ var codebaseSearchTool = {
3095
3682
  name: "codebase-search",
3096
3683
  category: "Project",
3097
3684
  icon: "index",
3098
- 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).",
3099
- 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.",
3685
+ 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).",
3686
+ 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.",
3100
3687
  permission: "auto",
3101
3688
  mutating: false,
3102
3689
  capabilities: ["fs.read"],
3103
- timeoutMs: 1e4,
3690
+ // The index host has its own 30s read watchdog. Leave enough headroom for
3691
+ // worker teardown and structured timeout reporting.
3692
+ timeoutMs: 35e3,
3104
3693
  inputSchema: {
3105
3694
  type: "object",
3106
3695
  properties: {
@@ -3110,11 +3699,34 @@ var codebaseSearchTool = {
3110
3699
  },
3111
3700
  kind: {
3112
3701
  type: "string",
3113
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
3702
+ enum: [
3703
+ "class",
3704
+ "interface",
3705
+ "enum",
3706
+ "type",
3707
+ "function",
3708
+ "method",
3709
+ "var",
3710
+ "const",
3711
+ "let",
3712
+ "property",
3713
+ "parameter",
3714
+ "namespace",
3715
+ "object",
3716
+ "literal",
3717
+ "schema",
3718
+ "struct",
3719
+ "trait",
3720
+ "impl",
3721
+ "static",
3722
+ "mod"
3723
+ ],
3724
+ description: "Filter by indexed symbol kind"
3114
3725
  },
3115
3726
  lang: {
3116
3727
  type: "string",
3117
- description: "Filter by language: ts, tsx, js, jsx"
3728
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
3729
+ description: "Filter by indexed language"
3118
3730
  },
3119
3731
  lspKind: {
3120
3732
  type: "integer",
@@ -3139,20 +3751,12 @@ var codebaseSearchTool = {
3139
3751
  },
3140
3752
  async execute(input, ctx, execOpts) {
3141
3753
  const state = getIndexState();
3142
- if (!state.ready) {
3754
+ if (state.indexing && !state.ready) {
3143
3755
  return {
3144
3756
  results: [],
3145
3757
  total: 0,
3146
3758
  query: input.query,
3147
- 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."
3148
- };
3149
- }
3150
- if (state.indexing) {
3151
- return {
3152
- results: [],
3153
- total: 0,
3154
- query: input.query,
3155
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
3759
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
3156
3760
  };
3157
3761
  }
3158
3762
  if (state.lastError) {
@@ -3165,21 +3769,37 @@ var codebaseSearchTool = {
3165
3769
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
3166
3770
  };
3167
3771
  }
3168
- const limit = Math.min(input.limit ?? 20, 100);
3772
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
3169
3773
  const { results, total } = await searchCodebaseIndex(
3170
3774
  {
3171
3775
  projectRoot: ctx.projectRoot,
3172
3776
  indexDir: codebaseIndexDirOverride(ctx),
3173
3777
  query: input.query,
3174
- kind: input.kind,
3175
- lang: input.lang,
3778
+ kind: input.kind?.toLowerCase(),
3779
+ lang: input.lang?.toLowerCase(),
3176
3780
  file: input.file,
3177
3781
  lspKind: input.lspKind,
3178
3782
  limit
3179
3783
  },
3180
3784
  { signal: execOpts?.signal }
3181
3785
  );
3182
- return { results, total, query: input.query };
3786
+ let hasPersistedIndex = state.ready || total > 0;
3787
+ if (!hasPersistedIndex) {
3788
+ try {
3789
+ const stats = await codebaseIndexStats(
3790
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3791
+ { signal: execOpts?.signal }
3792
+ );
3793
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
3794
+ } catch {
3795
+ }
3796
+ }
3797
+ return {
3798
+ results,
3799
+ total,
3800
+ query: input.query,
3801
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
3802
+ };
3183
3803
  }
3184
3804
  };
3185
3805
 
@@ -3188,12 +3808,15 @@ var codebaseStatsTool = {
3188
3808
  name: "codebase-stats",
3189
3809
  category: "Project",
3190
3810
  icon: "index",
3191
- 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.",
3192
- 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.",
3811
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
3812
+ 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.",
3193
3813
  permission: "auto",
3194
3814
  mutating: false,
3195
3815
  capabilities: ["fs.read"],
3196
- timeoutMs: 5e3,
3816
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
3817
+ // longer so the host can return a structured timeout instead of being
3818
+ // pre-empted by the generic tool executor.
3819
+ timeoutMs: 35e3,
3197
3820
  inputSchema: {
3198
3821
  type: "object",
3199
3822
  properties: {},
@@ -3201,7 +3824,8 @@ var codebaseStatsTool = {
3201
3824
  },
3202
3825
  async execute(_input, ctx, execOpts) {
3203
3826
  const idxState = getIndexState();
3204
- if (!idxState.ready) {
3827
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
3828
+ if (idxState.indexing) {
3205
3829
  return {
3206
3830
  totalSymbols: 0,
3207
3831
  totalFiles: 0,
@@ -3209,22 +3833,47 @@ var codebaseStatsTool = {
3209
3833
  byKind: {},
3210
3834
  lastIndexed: null,
3211
3835
  sizeBytes: 0,
3212
- indexPath: "",
3836
+ indexPath,
3213
3837
  version: SCHEMA_VERSION,
3214
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
3838
+ statsAvailable: false,
3839
+ indexing: {
3840
+ currentFile: idxState.currentFile,
3841
+ totalFiles: idxState.totalFiles
3842
+ },
3843
+ 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.`
3215
3844
  };
3216
3845
  }
3217
- const stats = await codebaseIndexStats(
3218
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3219
- { signal: execOpts?.signal }
3220
- );
3221
- if (idxState.indexing) {
3846
+ let stats;
3847
+ try {
3848
+ stats = await codebaseIndexStats(
3849
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
3850
+ { signal: execOpts?.signal }
3851
+ );
3852
+ } catch (err) {
3853
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
3854
+ throw err;
3855
+ }
3222
3856
  return {
3223
- ...stats,
3224
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3857
+ totalSymbols: 0,
3858
+ totalFiles: 0,
3859
+ byLang: {},
3860
+ byKind: {},
3861
+ lastIndexed: null,
3862
+ sizeBytes: 0,
3863
+ indexPath,
3864
+ version: SCHEMA_VERSION,
3865
+ statsAvailable: false,
3866
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
3225
3867
  };
3226
3868
  }
3227
3869
  const circuit = idxState.circuit;
3870
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
3871
+ let indexStatus;
3872
+ if (circuit.state === "open") {
3873
+ 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.");
3874
+ } else if (!hasPersistedIndex) {
3875
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
3876
+ }
3228
3877
  return {
3229
3878
  totalSymbols: stats.totalSymbols,
3230
3879
  totalFiles: stats.totalFiles,
@@ -3234,9 +3883,8 @@ var codebaseStatsTool = {
3234
3883
  sizeBytes: stats.sizeBytes,
3235
3884
  indexPath: stats.indexPath,
3236
3885
  version: stats.version,
3237
- ...circuit.state === "open" ? {
3238
- 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.`
3239
- } : {}
3886
+ statsAvailable: true,
3887
+ ...indexStatus ? { indexStatus } : {}
3240
3888
  };
3241
3889
  }
3242
3890
  };
@@ -3255,6 +3903,7 @@ export {
3255
3903
  codebaseSearchTool,
3256
3904
  codebaseStatsTool,
3257
3905
  enqueueReindex,
3906
+ fileGraphService,
3258
3907
  getIndexState,
3259
3908
  indexCircuitBreaker,
3260
3909
  internalKindToLspKind,
@@ -3263,12 +3912,14 @@ export {
3263
3912
  isIndexing,
3264
3913
  lspKindToInternalKind,
3265
3914
  onIndexStateChange,
3915
+ packageGraphService,
3266
3916
  resetIndexCircuitBreaker,
3267
3917
  resolveIndexDir,
3268
3918
  runIndexer,
3269
3919
  runStartupIndex,
3270
3920
  searchCodebaseIndex,
3271
3921
  shutdownCodebaseIndexHost,
3922
+ symbolGraphService,
3272
3923
  tokenise
3273
3924
  };
3274
3925
  //# sourceMappingURL=index.js.map