@wrongstack/tools 0.287.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 (93) 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/bash.d.ts +1 -1
  7. package/dist/bash.d.ts.map +1 -1
  8. package/dist/bash.js +97 -19
  9. package/dist/bash.js.map +3 -3
  10. package/dist/builtin.d.ts +12 -10
  11. package/dist/builtin.d.ts.map +1 -1
  12. package/dist/builtin.js +1620 -803
  13. package/dist/builtin.js.map +3 -3
  14. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  15. package/dist/codebase-index/bm25.d.ts +1 -2
  16. package/dist/codebase-index/bm25.d.ts.map +1 -1
  17. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  18. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  19. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  20. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  21. package/dist/codebase-index/index-service.d.ts +11 -1
  22. package/dist/codebase-index/index-service.d.ts.map +1 -1
  23. package/dist/codebase-index/index.d.ts +2 -1
  24. package/dist/codebase-index/index.d.ts.map +1 -1
  25. package/dist/codebase-index/index.js +749 -192
  26. package/dist/codebase-index/index.js.map +3 -3
  27. package/dist/codebase-index/indexer.d.ts.map +1 -1
  28. package/dist/codebase-index/schema.d.ts +43 -1
  29. package/dist/codebase-index/schema.d.ts.map +1 -1
  30. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  31. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  32. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  33. package/dist/codebase-index/worker.js +610 -152
  34. package/dist/codebase-index/worker.js.map +3 -3
  35. package/dist/codebase-index/writer.d.ts +27 -1
  36. package/dist/codebase-index/writer.d.ts.map +1 -1
  37. package/dist/e2e.js +477 -0
  38. package/dist/e2e.js.map +7 -0
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.d.ts.map +1 -1
  41. package/dist/exec.js +60 -5
  42. package/dist/exec.js.map +3 -3
  43. package/dist/fetch.js +2 -2
  44. package/dist/fetch.js.map +2 -2
  45. package/dist/format.js +61 -14
  46. package/dist/format.js.map +3 -3
  47. package/dist/glob.js +2 -2
  48. package/dist/glob.js.map +1 -1
  49. package/dist/grep.d.ts.map +1 -1
  50. package/dist/grep.js +2 -2
  51. package/dist/grep.js.map +2 -2
  52. package/dist/index.d.ts +3 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1786 -809
  55. package/dist/index.js.map +3 -3
  56. package/dist/install.js +61 -14
  57. package/dist/install.js.map +3 -3
  58. package/dist/kanban.d.ts +11 -2
  59. package/dist/kanban.d.ts.map +1 -1
  60. package/dist/kanban.js +695 -571
  61. package/dist/kanban.js.map +3 -3
  62. package/dist/languages/detect.d.ts.map +1 -1
  63. package/dist/languages/index.js +61 -14
  64. package/dist/languages/index.js.map +3 -3
  65. package/dist/languages/types.d.ts +7 -0
  66. package/dist/languages/types.d.ts.map +1 -1
  67. package/dist/lint.js +61 -14
  68. package/dist/lint.js.map +3 -3
  69. package/dist/outdated.js +61 -14
  70. package/dist/outdated.js.map +3 -3
  71. package/dist/pack.js +1617 -800
  72. package/dist/pack.js.map +3 -3
  73. package/dist/plan.js +4 -0
  74. package/dist/plan.js.map +2 -2
  75. package/dist/process-registry.js +12 -5
  76. package/dist/process-registry.js.map +2 -2
  77. package/dist/search.js +2 -2
  78. package/dist/search.js.map +2 -2
  79. package/dist/session-kanban.d.ts +9 -0
  80. package/dist/session-kanban.d.ts.map +1 -1
  81. package/dist/session-kanban.js +137 -0
  82. package/dist/session-kanban.js.map +2 -2
  83. package/dist/task.js +4 -0
  84. package/dist/task.js.map +2 -2
  85. package/dist/test.js +61 -14
  86. package/dist/test.js.map +3 -3
  87. package/dist/todo.js +4 -0
  88. package/dist/todo.js.map +2 -2
  89. package/dist/tree.js +2 -2
  90. package/dist/tree.js.map +1 -1
  91. package/dist/typecheck.js +61 -14
  92. package/dist/typecheck.js.map +3 -3
  93. package/package.json +7 -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) {
@@ -135,7 +135,7 @@ function tokenise(text) {
135
135
  return sanitised.toLowerCase().split(" ").filter(Boolean);
136
136
  }
137
137
  function splitName(name) {
138
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
138
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
139
139
  }
140
140
  function buildIndexableText(name, signature, docComment) {
141
141
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -161,14 +161,12 @@ function buildBm25Index(docs) {
161
161
  return new Bm25Index(documents, df, N, avgLen);
162
162
  }
163
163
  var Bm25Index = class {
164
- constructor(documents, df, N, avgLen) {
164
+ constructor(documents, _df, N, avgLen) {
165
165
  this.documents = documents;
166
- this.df = df;
167
166
  this.N = N;
168
167
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
169
168
  }
170
169
  documents;
171
- df;
172
170
  N;
173
171
  safeAvgLen;
174
172
  score(query, filter) {
@@ -181,10 +179,13 @@ var Bm25Index = class {
181
179
  for (const qTerm of qTokens) {
182
180
  let tf = 0;
183
181
  for (const t of doc.tokens) {
184
- if (t === qTerm) tf++;
182
+ if (t.startsWith(qTerm)) tf++;
185
183
  }
186
184
  if (tf === 0) continue;
187
- const dfVal = this.df[qTerm] ?? 0;
185
+ let dfVal = 0;
186
+ for (const candidate of this.documents) {
187
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
188
+ }
188
189
  if (dfVal === 0) continue;
189
190
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
190
191
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -217,6 +218,29 @@ var Bm25Index = class {
217
218
 
218
219
  // src/codebase-index/writer.ts
219
220
  var DB_FILE = "index.db";
221
+ function escapeLike(value) {
222
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
223
+ }
224
+ function assignRefsToSymbols(refs, symbols) {
225
+ if (refs.length === 0 || symbols.length === 0) return [];
226
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
227
+ const seen = /* @__PURE__ */ new Set();
228
+ const assigned = [];
229
+ for (const ref of refs) {
230
+ let owner;
231
+ for (const symbol of ordered) {
232
+ if (symbol.line > ref.line) break;
233
+ owner = symbol;
234
+ }
235
+ if (!owner && ref.callType === "import") owner = ordered[0];
236
+ if (!owner || owner.id <= 0) continue;
237
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
238
+ if (seen.has(key)) continue;
239
+ seen.add(key);
240
+ assigned.push({ ...ref, fromId: owner.id });
241
+ }
242
+ return assigned;
243
+ }
220
244
  function resolveIndexDir(projectRoot, override) {
221
245
  return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
222
246
  }
@@ -266,7 +290,7 @@ function sleepSync(ms) {
266
290
  } catch {
267
291
  }
268
292
  }
269
- var IndexStore = class {
293
+ var IndexStore = class _IndexStore {
270
294
  db;
271
295
  /** Absolute path to this project's index directory. */
272
296
  indexDir;
@@ -318,10 +342,7 @@ var IndexStore = class {
318
342
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
319
343
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
320
344
  }
321
- const delay = Math.min(
322
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
323
- LOCK_RETRY_MAX_DELAY_MS
324
- );
345
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
325
346
  sleepSync(delay);
326
347
  }
327
348
  }
@@ -403,8 +424,24 @@ var IndexStore = class {
403
424
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
404
425
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
405
426
  try {
406
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
427
+ this.db.exec(
428
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
429
+ );
407
430
  this.ftsAvailable = true;
431
+ const symbolCount = Number(
432
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
433
+ );
434
+ const ftsCount = Number(
435
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
436
+ );
437
+ if (symbolCount !== ftsCount) {
438
+ this.db.exec("DELETE FROM symbols_fts");
439
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
440
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
441
+ for (const row of rows) {
442
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
443
+ }
444
+ }
408
445
  } catch {
409
446
  this.ftsAvailable = false;
410
447
  }
@@ -462,7 +499,9 @@ var IndexStore = class {
462
499
  deleteSymbolsForFile(file) {
463
500
  this.runWithRetry(() => {
464
501
  if (this.ftsAvailable) {
465
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
502
+ this.stmt(
503
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
504
+ ).run(file);
466
505
  }
467
506
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
468
507
  });
@@ -474,9 +513,21 @@ var IndexStore = class {
474
513
  */
475
514
  deleteFile(file) {
476
515
  this.runWithRetry(() => {
477
- this.deleteRefsForFile(file);
478
- this.deleteSymbolsForFile(file);
479
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
516
+ this.db.exec("BEGIN IMMEDIATE");
517
+ try {
518
+ if (this.ftsAvailable) {
519
+ this.db.prepare(
520
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
521
+ ).run(file);
522
+ }
523
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
524
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
525
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
526
+ this.db.exec("COMMIT");
527
+ } catch (err) {
528
+ this.db.exec("ROLLBACK");
529
+ throw err;
530
+ }
480
531
  });
481
532
  }
482
533
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -499,14 +550,22 @@ var IndexStore = class {
499
550
  ).all(file);
500
551
  if (!rows.length) return null;
501
552
  const r = expectDefined(rows[0]);
502
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
553
+ return {
554
+ file: r.file,
555
+ lang: r.lang,
556
+ mtimeMs: r.mtime_ms,
557
+ symbolCount: r.symbol_count,
558
+ lastIndexed: r.last_indexed
559
+ };
503
560
  }
504
561
  getAllFileMetas() {
505
- return this.db.prepare(
506
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
507
- ).all().map(
508
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
509
- );
562
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
563
+ file: r.file,
564
+ lang: r.lang,
565
+ mtimeMs: r.mtime_ms,
566
+ symbolCount: r.symbol_count,
567
+ lastIndexed: r.last_indexed
568
+ }));
510
569
  }
511
570
  // ─── Search ──────────────────────────────────────────────────────────────────
512
571
  search(query, filter) {
@@ -530,8 +589,8 @@ var IndexStore = class {
530
589
  values.push(filter.lang);
531
590
  }
532
591
  if (filter?.file) {
533
- conditions.push("file LIKE ?");
534
- values.push(`%${filter.file}%`);
592
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
593
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
535
594
  }
536
595
  if (query.trim()) {
537
596
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -569,9 +628,11 @@ var IndexStore = class {
569
628
  * "users", camelCase-split text makes "complex" find "complexOperation").
570
629
  */
571
630
  searchRanked(query, filter, limit) {
631
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
632
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
572
633
  const tokens = tokenise(query);
573
634
  if (tokens.length === 0 || !this.ftsAvailable) {
574
- return this.searchRankedFallback(query, filter, limit);
635
+ return this.searchRankedFallback(query, filter, safeLimit);
575
636
  }
576
637
  let effectiveKind = filter?.kind;
577
638
  if (filter?.lspKind !== void 0) {
@@ -591,11 +652,13 @@ var IndexStore = class {
591
652
  values.push(filter.lang);
592
653
  }
593
654
  if (filter?.file) {
594
- conditions.push("s.file LIKE ?");
595
- values.push(`%${filter.file}%`);
655
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
656
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
596
657
  }
597
658
  const where = conditions.join(" AND ");
598
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
659
+ const countRows = this.db.prepare(
660
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
661
+ ).all(...values);
599
662
  const total = countRows[0] ? Number(countRows[0].n) : 0;
600
663
  if (total === 0) return { results: [], total: 0 };
601
664
  const rows = this.db.prepare(
@@ -604,9 +667,13 @@ var IndexStore = class {
604
667
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
605
668
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
606
669
  WHERE ${where}
607
- ORDER BY bm25(symbols_fts)
670
+ ORDER BY
671
+ CASE WHEN lower(s.name) = lower(?) THEN 0
672
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
673
+ ELSE 2 END,
674
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
608
675
  LIMIT ?`
609
- ).all(...values, limit);
676
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
610
677
  return {
611
678
  results: rows.map((r) => ({
612
679
  id: r.id,
@@ -636,10 +703,28 @@ var IndexStore = class {
636
703
  }
637
704
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
638
705
  const bm25 = buildBm25Index(
639
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
706
+ candidates.map((c) => ({
707
+ id: c.id,
708
+ text: buildIndexableText(c.name, c.signature, c.docComment)
709
+ }))
640
710
  );
641
711
  const scored = bm25.score(query, (id) => candidateById.has(id));
642
- scored.sort((a, b) => b.score - a.score);
712
+ const q = query.trim().toLowerCase();
713
+ const rank = (id) => {
714
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
715
+ if (name === q) return 0;
716
+ if (name.startsWith(q)) return 1;
717
+ return 2;
718
+ };
719
+ scored.sort((a, b) => {
720
+ const rankDiff = rank(a.id) - rank(b.id);
721
+ if (rankDiff !== 0) return rankDiff;
722
+ const scoreDiff = b.score - a.score;
723
+ if (scoreDiff !== 0) return scoreDiff;
724
+ const left = expectDefined(candidateById.get(a.id));
725
+ const right = expectDefined(candidateById.get(b.id));
726
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
727
+ });
643
728
  const qTokens = tokenise(query);
644
729
  const results = scored.slice(0, limit).map(({ id, score }) => {
645
730
  const c = expectDefined(candidateById.get(id));
@@ -648,9 +733,7 @@ var IndexStore = class {
648
733
  return { results, total: candidates.length };
649
734
  }
650
735
  getAllIndexable() {
651
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
652
- ({ id, text }) => ({ id, text })
653
- );
736
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
654
737
  }
655
738
  /**
656
739
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -666,22 +749,16 @@ var IndexStore = class {
666
749
  // ─── Stats ───────────────────────────────────────────────────────────────────
667
750
  getStats() {
668
751
  const sizeBytes = this.sizeBytes();
669
- const lastRows = this.db.prepare(
670
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
671
- ).all();
752
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
672
753
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
673
754
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
674
755
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
675
756
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
676
757
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
677
- const langRows = this.db.prepare(
678
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
679
- ).all();
758
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
680
759
  const byLang = {};
681
760
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
682
- const kindRows = this.db.prepare(
683
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
684
- ).all();
761
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
685
762
  const byKind = {};
686
763
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
687
764
  return {
@@ -697,17 +774,31 @@ var IndexStore = class {
697
774
  }
698
775
  setLastIndexed(ts2) {
699
776
  this.runWithRetry(() => {
700
- this.db.prepare(
701
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
702
- ).run(String(ts2));
777
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
778
+ });
779
+ }
780
+ getMetadata(key) {
781
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
782
+ return rows[0]?.value;
783
+ }
784
+ setMetadata(key, value) {
785
+ this.runWithRetry(() => {
786
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
703
787
  });
704
788
  }
705
789
  clearAll() {
706
790
  this.runWithRetry(() => {
707
- this.db.exec("DELETE FROM symbols");
708
- this.db.exec("DELETE FROM files");
709
- this.db.exec("DELETE FROM refs");
710
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
791
+ this.db.exec("BEGIN IMMEDIATE");
792
+ try {
793
+ this.db.exec("DELETE FROM refs");
794
+ this.db.exec("DELETE FROM symbols");
795
+ this.db.exec("DELETE FROM files");
796
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
797
+ this.db.exec("COMMIT");
798
+ } catch (err) {
799
+ this.db.exec("ROLLBACK");
800
+ throw err;
801
+ }
711
802
  });
712
803
  }
713
804
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -798,6 +889,7 @@ var IndexStore = class {
798
889
  const allInserted = [];
799
890
  const refsToInsert = [];
800
891
  for (const entry of entries) {
892
+ const insertedForEntry = [];
801
893
  for (const s of entry.symbols) {
802
894
  const id = nextId++;
803
895
  symStmt.run(
@@ -815,9 +907,11 @@ var IndexStore = class {
815
907
  s.file
816
908
  );
817
909
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
818
- allInserted.push({ ...s, id });
910
+ const inserted = { ...s, id };
911
+ allInserted.push(inserted);
912
+ insertedForEntry.push(inserted);
819
913
  }
820
- for (const r of entry.refs) refsToInsert.push(r);
914
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
821
915
  }
822
916
  if (refsToInsert.length > 0) {
823
917
  const refStmt = this.db.prepare(
@@ -855,9 +949,7 @@ var IndexStore = class {
855
949
  */
856
950
  deleteRefsForFile(file) {
857
951
  this.runWithRetry(() => {
858
- this.db.prepare(
859
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
860
- ).run(file);
952
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
861
953
  });
862
954
  }
863
955
  /**
@@ -898,9 +990,7 @@ var IndexStore = class {
898
990
  * Find all references FROM a given symbol (what does this symbol call/use?).
899
991
  */
900
992
  findRefsFrom(symbolId) {
901
- return this.db.prepare(
902
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
903
- ).all(symbolId).map((r) => ({
993
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
904
994
  id: r.id,
905
995
  fromId: r.from_id,
906
996
  toName: r.to_name,
@@ -917,6 +1007,325 @@ var IndexStore = class {
917
1007
  return 0;
918
1008
  }
919
1009
  }
1010
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
1011
+ /**
1012
+ * Derive a monorepo package name from an absolute file path.
1013
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
1014
+ * Returns `undefined` for files outside any recognisable package structure.
1015
+ */
1016
+ static derivePackage(filePath) {
1017
+ const f = filePath.replace(/\\/g, "/");
1018
+ const pkgsIdx = f.indexOf("/packages/");
1019
+ if (pkgsIdx !== -1) {
1020
+ const rest = f.slice(pkgsIdx + "/packages/".length);
1021
+ const seg = rest.split("/")[0];
1022
+ return seg ? `@wrongstack/${seg}` : void 0;
1023
+ }
1024
+ const appsIdx = f.indexOf("/apps/");
1025
+ if (appsIdx !== -1) {
1026
+ const rest = f.slice(appsIdx + "/apps/".length);
1027
+ const seg = rest.split("/")[0];
1028
+ return seg ? `app:${seg}` : void 0;
1029
+ }
1030
+ return void 0;
1031
+ }
1032
+ static packageFromImport(moduleName) {
1033
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
1034
+ const parts = moduleName.split("/");
1035
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
1036
+ }
1037
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
1038
+ if (!moduleName.startsWith(".")) return void 0;
1039
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
1040
+ const absolute = path.posix.normalize(
1041
+ path.posix.join(path.posix.dirname(normalizedFrom), moduleName)
1042
+ );
1043
+ const extension = path.posix.extname(absolute);
1044
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
1045
+ const candidates = [
1046
+ absolute,
1047
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
1048
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(absolute, `index${ext}`)),
1049
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path.posix.join(base, `index${ext}`))
1050
+ ];
1051
+ const indexedByPortablePath = new Map(
1052
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
1053
+ );
1054
+ for (const candidate of candidates) {
1055
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
1056
+ if (indexed) return indexed;
1057
+ }
1058
+ return void 0;
1059
+ }
1060
+ /**
1061
+ * Package-level graph: each workspace package is a node; edges are derived
1062
+ * from cross-package symbol references (a symbol in package A references a
1063
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
1064
+ */
1065
+ getPackageGraph() {
1066
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1067
+ const pkgNodes = /* @__PURE__ */ new Map();
1068
+ const fileToPkg = /* @__PURE__ */ new Map();
1069
+ const symbolToPkg = /* @__PURE__ */ new Map();
1070
+ for (const s of symbols) {
1071
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
1072
+ fileToPkg.set(s.file, pkg);
1073
+ symbolToPkg.set(s.id, pkg);
1074
+ const node = pkgNodes.get(pkg);
1075
+ if (node) {
1076
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
1077
+ } else {
1078
+ pkgNodes.set(pkg, {
1079
+ id: `pkg:${pkg}`,
1080
+ label: pkg,
1081
+ kind: "package",
1082
+ package: pkg,
1083
+ symbolCount: 1,
1084
+ fileCount: 0
1085
+ });
1086
+ }
1087
+ }
1088
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
1089
+ for (const { file } of files) {
1090
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
1091
+ const node = pkgNodes.get(pkg);
1092
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
1093
+ }
1094
+ const refRows = this.db.prepare(
1095
+ `SELECT r.from_id, r.to_id, r.call_type
1096
+ FROM refs r
1097
+ WHERE r.to_id IS NOT NULL`
1098
+ ).all();
1099
+ const edgeMap = /* @__PURE__ */ new Map();
1100
+ for (const r of refRows) {
1101
+ if (r.call_type === "import") continue;
1102
+ const fromPkg = symbolToPkg.get(r.from_id);
1103
+ const toPkg = symbolToPkg.get(r.to_id);
1104
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
1105
+ const key = `${fromPkg}\0${toPkg}`;
1106
+ let e = edgeMap.get(key);
1107
+ if (!e) {
1108
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1109
+ edgeMap.set(key, e);
1110
+ }
1111
+ e.weight++;
1112
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1113
+ }
1114
+ const importRows = this.db.prepare(
1115
+ `SELECT r.from_id, r.to_name
1116
+ FROM refs r
1117
+ WHERE r.call_type = 'import'`
1118
+ ).all();
1119
+ for (const r of importRows) {
1120
+ const fromPkg = symbolToPkg.get(r.from_id);
1121
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
1122
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
1123
+ const key = `${fromPkg}\0${toPkg}`;
1124
+ let edge = edgeMap.get(key);
1125
+ if (!edge) {
1126
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1127
+ edgeMap.set(key, edge);
1128
+ }
1129
+ edge.weight++;
1130
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1131
+ }
1132
+ const edges = [];
1133
+ for (const [key, e] of edgeMap) {
1134
+ const [source, target] = key.split("\0");
1135
+ let bestType = "call";
1136
+ let bestCount = 0;
1137
+ for (const [t, c] of e.types) {
1138
+ if (c > bestCount) {
1139
+ bestType = t;
1140
+ bestCount = c;
1141
+ }
1142
+ }
1143
+ edges.push({
1144
+ source: `pkg:${source}`,
1145
+ target: `pkg:${target}`,
1146
+ weight: e.weight,
1147
+ refType: bestType
1148
+ });
1149
+ }
1150
+ return { nodes: [...pkgNodes.values()], edges };
1151
+ }
1152
+ /**
1153
+ * File-level graph for a single package: each file is a node; edges are
1154
+ * derived from cross-file symbol references within the package.
1155
+ */
1156
+ getFileGraph(packageFilter) {
1157
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
1158
+ const pkgSyms = allSymbols.filter(
1159
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
1160
+ );
1161
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
1162
+ const fileNodes = /* @__PURE__ */ new Map();
1163
+ const symToFile = /* @__PURE__ */ new Map();
1164
+ const fileStats = /* @__PURE__ */ new Map();
1165
+ for (const s of allSymbols) {
1166
+ symToFile.set(s.id, s.file);
1167
+ const current = fileStats.get(s.file);
1168
+ fileStats.set(s.file, {
1169
+ count: (current?.count ?? 0) + 1,
1170
+ lang: current?.lang ?? s.lang
1171
+ });
1172
+ }
1173
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
1174
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
1175
+ const ensureFileNode = (file) => {
1176
+ if (fileNodes.has(file)) return;
1177
+ const stats = fileStats.get(file);
1178
+ fileNodes.set(file, {
1179
+ id: `file:${file}`,
1180
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
1181
+ kind: "file",
1182
+ package: _IndexStore.derivePackage(file) ?? "(root)",
1183
+ file,
1184
+ symbolCount: stats?.count ?? 0,
1185
+ lang: stats?.lang,
1186
+ external: !localFiles.has(file)
1187
+ });
1188
+ };
1189
+ for (const file of localFiles) {
1190
+ ensureFileNode(file);
1191
+ }
1192
+ const refRows = this.db.prepare(
1193
+ `SELECT r.from_id, r.to_id, r.call_type
1194
+ FROM refs r
1195
+ WHERE r.to_id IS NOT NULL`
1196
+ ).all();
1197
+ const edgeMap = /* @__PURE__ */ new Map();
1198
+ for (const r of refRows) {
1199
+ if (r.call_type === "import") continue;
1200
+ const fromFile = symToFile.get(r.from_id);
1201
+ const toFile = symToFile.get(r.to_id);
1202
+ if (!fromFile || !toFile || fromFile === toFile) continue;
1203
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
1204
+ ensureFileNode(fromFile);
1205
+ ensureFileNode(toFile);
1206
+ const key = `${fromFile}\0${toFile}`;
1207
+ let e = edgeMap.get(key);
1208
+ if (!e) {
1209
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1210
+ edgeMap.set(key, e);
1211
+ }
1212
+ e.weight++;
1213
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1214
+ }
1215
+ const importRows = this.db.prepare(
1216
+ `SELECT r.from_id, r.to_name
1217
+ FROM refs r
1218
+ WHERE r.call_type = 'import'`
1219
+ ).all();
1220
+ for (const r of importRows) {
1221
+ const fromFile = symToFile.get(r.from_id);
1222
+ if (!fromFile || !localFiles.has(fromFile)) continue;
1223
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
1224
+ if (!toFile || fromFile === toFile) continue;
1225
+ ensureFileNode(fromFile);
1226
+ ensureFileNode(toFile);
1227
+ const key = `${fromFile}\0${toFile}`;
1228
+ let edge = edgeMap.get(key);
1229
+ if (!edge) {
1230
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
1231
+ edgeMap.set(key, edge);
1232
+ }
1233
+ edge.weight++;
1234
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
1235
+ }
1236
+ const edges = [];
1237
+ for (const [key, e] of edgeMap) {
1238
+ const [source, target] = key.split("\0");
1239
+ let bestType = "call";
1240
+ let bestCount = 0;
1241
+ for (const [t, c] of e.types) {
1242
+ if (c > bestCount) {
1243
+ bestType = t;
1244
+ bestCount = c;
1245
+ }
1246
+ }
1247
+ edges.push({
1248
+ source: `file:${source}`,
1249
+ target: `file:${target}`,
1250
+ weight: e.weight,
1251
+ refType: bestType
1252
+ });
1253
+ }
1254
+ return { nodes: [...fileNodes.values()], edges };
1255
+ }
1256
+ /**
1257
+ * Symbol-level graph for a single file: each symbol is a node; edges are
1258
+ * derived from intra-file and cross-file symbol references (who calls whom).
1259
+ */
1260
+ getSymbolGraph(fileFilter) {
1261
+ const allSymbols = this.db.prepare(
1262
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
1263
+ ).all();
1264
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
1265
+ if (syms.length === 0) return { nodes: [], edges: [] };
1266
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
1267
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
1268
+ const toGraphNode = (s) => ({
1269
+ id: `sym:${s.id}`,
1270
+ label: s.name,
1271
+ kind: "symbol",
1272
+ symbolId: s.id,
1273
+ symbolKind: s.kind,
1274
+ file: s.file,
1275
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
1276
+ lang: s.lang,
1277
+ line: s.line,
1278
+ signature: s.signature,
1279
+ scope: s.scope,
1280
+ external: s.file !== fileFilter
1281
+ });
1282
+ const symIds = new Set(syms.map((s) => s.id));
1283
+ const refRows = this.db.prepare(
1284
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
1285
+ FROM refs r
1286
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
1287
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
1288
+ ).all(fileFilter, fileFilter);
1289
+ const edgeMap = /* @__PURE__ */ new Map();
1290
+ for (const r of refRows) {
1291
+ if (r.to_id === null) continue;
1292
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
1293
+ relatedIds.add(r.from_id);
1294
+ relatedIds.add(r.to_id);
1295
+ const key = `${r.from_id}\0${r.to_id}`;
1296
+ let e = edgeMap.get(key);
1297
+ if (!e) {
1298
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
1299
+ edgeMap.set(key, e);
1300
+ }
1301
+ e.weight++;
1302
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
1303
+ }
1304
+ const edges = [];
1305
+ for (const [key, e] of edgeMap) {
1306
+ const [fromId, toId] = key.split("\0").map(Number);
1307
+ let bestType = "call";
1308
+ let bestCount = 0;
1309
+ for (const [t, c] of e.types) {
1310
+ if (c > bestCount) {
1311
+ bestType = t;
1312
+ bestCount = c;
1313
+ }
1314
+ }
1315
+ edges.push({
1316
+ source: `sym:${fromId}`,
1317
+ target: `sym:${toId}`,
1318
+ weight: e.weight,
1319
+ refType: bestType
1320
+ });
1321
+ }
1322
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
1323
+ const aExternal = a.file === fileFilter ? 0 : 1;
1324
+ const bExternal = b.file === fileFilter ? 0 : 1;
1325
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
1326
+ }).map(toGraphNode);
1327
+ return { nodes, edges };
1328
+ }
920
1329
  close() {
921
1330
  this.stmtCache.clear();
922
1331
  try {
@@ -1000,6 +1409,14 @@ function getJsDoc(node, sourceFile) {
1000
1409
  }
1001
1410
  return "";
1002
1411
  }
1412
+ function hasFunctionLikeAncestor(node) {
1413
+ let current = node.parent;
1414
+ while (current) {
1415
+ if (ts.isFunctionLike(current)) return true;
1416
+ current = current.parent;
1417
+ }
1418
+ return false;
1419
+ }
1003
1420
  function buildScope(node) {
1004
1421
  const parts = [];
1005
1422
  let current = node.parent;
@@ -1027,6 +1444,10 @@ function parseSymbols(opts) {
1027
1444
  function visit(node) {
1028
1445
  const kind = kindOf(node);
1029
1446
  if (kind) {
1447
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
1448
+ ts.forEachChild(node, visit);
1449
+ return;
1450
+ }
1030
1451
  const nameNode = node.name;
1031
1452
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
1032
1453
  const name = nameNode.text;
@@ -1994,9 +2415,9 @@ function parseSymbols5(opts) {
1994
2415
  function regexParse2(opts) {
1995
2416
  const { file, content, lang } = opts;
1996
2417
  const symbols = [];
1997
- const basename2 = path6.basename(file).toLowerCase();
1998
- const isPackageJson = basename2 === "package.json";
1999
- const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2418
+ const basename3 = path6.basename(file).toLowerCase();
2419
+ const isPackageJson = basename3 === "package.json";
2420
+ const isTsconfig = basename3 === "tsconfig.json" || basename3 === "tsconfig.build.json";
2000
2421
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
2001
2422
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
2002
2423
  const lines = content.split("\n");
@@ -2398,9 +2819,7 @@ function yieldEventLoop() {
2398
2819
  function throwIfAborted(signal) {
2399
2820
  if (!signal?.aborted) return;
2400
2821
  if (signal.reason instanceof Error) throw signal.reason;
2401
- throw new Error(
2402
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
2403
- );
2822
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
2404
2823
  }
2405
2824
  function isAbortError(err) {
2406
2825
  return err instanceof DOMException && err.name === "AbortError";
@@ -2416,8 +2835,20 @@ var DEFAULT_IGNORE = [
2416
2835
  "__snapshots__",
2417
2836
  ".nyc_output"
2418
2837
  ];
2838
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
2839
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
2840
+ function isWithinProject(projectRoot, file) {
2841
+ const rel = path8.relative(projectRoot, file);
2842
+ return rel !== "" && !rel.startsWith(`..${path8.sep}`) && rel !== ".." && !path8.isAbsolute(rel);
2843
+ }
2844
+ function isMissingPathError(err) {
2845
+ const code = err?.code;
2846
+ return code === "ENOENT" || code === "ENOTDIR";
2847
+ }
2419
2848
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2420
2849
  const results = [];
2850
+ const errors = [];
2851
+ let complete = true;
2421
2852
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2422
2853
  const globs = [
2423
2854
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -2441,7 +2872,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2441
2872
  let entries;
2442
2873
  try {
2443
2874
  entries = await fs7.readdir(dir, { withFileTypes: true });
2444
- } catch {
2875
+ } catch (err) {
2876
+ complete = false;
2877
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
2445
2878
  return;
2446
2879
  }
2447
2880
  dirCount++;
@@ -2453,8 +2886,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2453
2886
  if (isGitIgnored(rel, true)) continue;
2454
2887
  await walk(full);
2455
2888
  } else if (e.isFile()) {
2456
- if (isGitIgnored(rel, false)) continue;
2457
- const ext = path8.extname(e.name);
2889
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
2890
+ const ext = path8.extname(e.name).toLowerCase();
2458
2891
  for (const { ext: extName, pat } of globs) {
2459
2892
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
2460
2893
  results.push(full);
@@ -2465,7 +2898,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2465
2898
  }
2466
2899
  };
2467
2900
  await walk(projectRoot);
2468
- return results;
2901
+ return { files: results, complete, errors };
2469
2902
  }
2470
2903
  async function parseFile(file, content, lang) {
2471
2904
  switch (lang) {
@@ -2488,6 +2921,26 @@ async function parseFile(file, content, lang) {
2488
2921
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2489
2922
  }
2490
2923
  }
2924
+ function assignRefsToSymbols2(refs, symbols) {
2925
+ if (refs.length === 0 || symbols.length === 0) return [];
2926
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
2927
+ const seen = /* @__PURE__ */ new Set();
2928
+ const assigned = [];
2929
+ for (const ref of refs) {
2930
+ let owner;
2931
+ for (const symbol of ordered) {
2932
+ if (symbol.line > ref.line) break;
2933
+ owner = symbol;
2934
+ }
2935
+ if (!owner && ref.callType === "import") owner = ordered[0];
2936
+ if (!owner || owner.id <= 0) continue;
2937
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
2938
+ if (seen.has(key)) continue;
2939
+ seen.add(key);
2940
+ assigned.push({ ...ref, fromId: owner.id });
2941
+ }
2942
+ return assigned;
2943
+ }
2491
2944
  async function runIndexer(_ctx, opts) {
2492
2945
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
2493
2946
  try {
@@ -2500,7 +2953,9 @@ async function runIndexer(_ctx, opts) {
2500
2953
  }
2501
2954
  }
2502
2955
  async function runIndexerWithStore(store, opts) {
2503
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
2956
+ const { projectRoot, langs, ignore = [], signal } = opts;
2957
+ const relationGraphVersion = "2";
2958
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
2504
2959
  const startMs = Date.now();
2505
2960
  const errors = [];
2506
2961
  const langStats = {};
@@ -2509,10 +2964,18 @@ async function runIndexerWithStore(store, opts) {
2509
2964
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2510
2965
  let files;
2511
2966
  let discoveredFiles = null;
2967
+ let discoveryComplete = true;
2512
2968
  if (opts.files && opts.files.length > 0) {
2513
- files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path8.relative(projectRoot, f).replace(/\\/g, "/"), false));
2969
+ files = opts.files.map((f) => path8.resolve(projectRoot, f)).filter((f) => {
2970
+ if (!isWithinProject(projectRoot, f)) return false;
2971
+ const rel = path8.relative(projectRoot, f).replace(/\\/g, "/");
2972
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path8.basename(f)) && !isGitIgnored(rel, false);
2973
+ });
2514
2974
  } else {
2515
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
2975
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
2976
+ files = discovery.files;
2977
+ errors.push(...discovery.errors);
2978
+ discoveryComplete = discovery.complete;
2516
2979
  discoveredFiles = new Set(files);
2517
2980
  }
2518
2981
  if (langs && langs.length > 0) {
@@ -2537,36 +3000,66 @@ async function runIndexerWithStore(store, opts) {
2537
3000
  }
2538
3001
  const statOpts = signal ? { signal } : {};
2539
3002
  const statReadParse = await Promise.allSettled(
2540
- batchFiles.map(async (file) => {
2541
- let stat2;
2542
- try {
2543
- stat2 = await fs7.stat(file, statOpts);
2544
- } catch (e) {
2545
- if (isAbortError(e)) throw e;
2546
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
2547
- }
2548
- if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
2549
- const lang = detectLang(file);
2550
- if (!lang) return { file, stat: stat2, lang: "", parsed: null };
2551
- const meta = existingMeta.get(file);
2552
- if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
2553
- return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
2554
- }
2555
- let content;
2556
- try {
2557
- content = await fs7.readFile(file, { encoding: "utf8", signal });
2558
- } catch (e) {
2559
- if (isAbortError(e)) throw e;
2560
- return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
2561
- }
2562
- let parsed;
2563
- try {
2564
- parsed = await parseFile(file, content, lang);
2565
- } catch (e) {
2566
- return { file, stat: stat2, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
3003
+ batchFiles.map(
3004
+ async (file) => {
3005
+ let stat2;
3006
+ try {
3007
+ stat2 = await fs7.stat(file, statOpts);
3008
+ } catch (e) {
3009
+ if (isAbortError(e)) throw e;
3010
+ return {
3011
+ file,
3012
+ stat: null,
3013
+ lang: "",
3014
+ parsed: null,
3015
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
3016
+ missing: isMissingPathError(e)
3017
+ };
3018
+ }
3019
+ if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
3020
+ const lang = detectLang(file);
3021
+ if (!lang) return { file, stat: stat2, lang: "", parsed: null };
3022
+ if (stat2.size > MAX_INDEX_FILE_BYTES) {
3023
+ return {
3024
+ file,
3025
+ stat: stat2,
3026
+ lang,
3027
+ parsed: null,
3028
+ error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
3029
+ };
3030
+ }
3031
+ const meta = existingMeta.get(file);
3032
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
3033
+ return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
3034
+ }
3035
+ let content;
3036
+ try {
3037
+ content = await fs7.readFile(file, { encoding: "utf8", signal });
3038
+ } catch (e) {
3039
+ if (isAbortError(e)) throw e;
3040
+ return {
3041
+ file,
3042
+ stat: stat2,
3043
+ lang,
3044
+ parsed: null,
3045
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
3046
+ };
3047
+ }
3048
+ let parsed;
3049
+ try {
3050
+ parsed = await parseFile(file, content, lang);
3051
+ } catch (e) {
3052
+ return {
3053
+ file,
3054
+ stat: stat2,
3055
+ lang,
3056
+ parsed: null,
3057
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
3058
+ };
3059
+ }
3060
+ return { file, stat: stat2, lang, parsed, content };
2567
3061
  }
2568
- return { file, stat: stat2, lang, parsed, content };
2569
- })
3062
+ )
2570
3063
  );
2571
3064
  const batchEntries = [];
2572
3065
  const deleteForFiles = [];
@@ -2581,8 +3074,8 @@ async function runIndexerWithStore(store, opts) {
2581
3074
  }
2582
3075
  const result = settled.value;
2583
3076
  if (result.error) {
2584
- if (result.stat) store.deleteFile(file);
2585
- if (result.error.includes("error:")) errors.push(result.error);
3077
+ if (result.missing) store.deleteFile(file);
3078
+ errors.push(`${file}: ${result.error}`);
2586
3079
  continue;
2587
3080
  }
2588
3081
  const { stat: stat2, lang, parsed } = result;
@@ -2606,6 +3099,8 @@ async function runIndexerWithStore(store, opts) {
2606
3099
  continue;
2607
3100
  }
2608
3101
  if (parsed.symbols.length === 0) {
3102
+ store.deleteRefsForFile(file);
3103
+ store.deleteSymbolsForFile(file);
2609
3104
  store.upsertFile({
2610
3105
  file,
2611
3106
  lang,
@@ -2616,15 +3111,11 @@ async function runIndexerWithStore(store, opts) {
2616
3111
  filesIndexed++;
2617
3112
  continue;
2618
3113
  }
2619
- const refs = [];
2620
- if (parsed.refs && parsed.refs.length > 0) {
2621
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
2622
- }
2623
3114
  batchEntries.push({
2624
3115
  file,
2625
3116
  lang,
2626
3117
  symbols: parsed.symbols,
2627
- refs,
3118
+ refs: parsed.refs ?? [],
2628
3119
  mtimeMs: Math.floor(stat2.mtimeMs),
2629
3120
  symbolCount: parsed.symbols.length
2630
3121
  });
@@ -2632,34 +3123,12 @@ async function runIndexerWithStore(store, opts) {
2632
3123
  }
2633
3124
  if (batchEntries.length > 0) {
2634
3125
  try {
2635
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
2636
- let cursor = 0;
3126
+ store.commitBatch(batchEntries, { deleteForFiles });
2637
3127
  for (const entry of batchEntries) {
2638
3128
  const count = entry.symbols.length;
2639
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
2640
- cursor += count;
2641
3129
  symbolsIndexed += count;
2642
3130
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
2643
3131
  filesIndexed++;
2644
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2645
- const refsByLine = /* @__PURE__ */ new Map();
2646
- for (let i = 0; i < symbolsWithIds.length; i++) {
2647
- const sym = symbolsWithIds[i];
2648
- let arr = refsByLine.get(sym.line);
2649
- if (!arr) {
2650
- arr = [];
2651
- refsByLine.set(sym.line, arr);
2652
- }
2653
- arr.push(i);
2654
- }
2655
- for (const ref of entry.refs) {
2656
- const indices = refsByLine.get(ref.line);
2657
- if (indices && indices.length > 0) {
2658
- const idx = indices.shift();
2659
- ref.fromId = symbolsWithIds[idx].id;
2660
- }
2661
- }
2662
- }
2663
3132
  }
2664
3133
  } catch (err) {
2665
3134
  const message = err instanceof Error ? err.message : String(err);
@@ -2673,23 +3142,7 @@ async function runIndexerWithStore(store, opts) {
2673
3142
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
2674
3143
  filesIndexed++;
2675
3144
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
2676
- const refsByLine = /* @__PURE__ */ new Map();
2677
- for (const sym of symbolsWithIds) {
2678
- let arr = refsByLine.get(sym.line);
2679
- if (!arr) {
2680
- arr = [];
2681
- refsByLine.set(sym.line, arr);
2682
- }
2683
- arr.push(sym);
2684
- }
2685
- const fallbackBatch = [];
2686
- for (const ref of entry.refs) {
2687
- const syms = refsByLine.get(ref.line);
2688
- if (syms && syms.length > 0) {
2689
- const sym = syms.shift();
2690
- fallbackBatch.push({ ...ref, fromId: sym.id });
2691
- }
2692
- }
3145
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
2693
3146
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
2694
3147
  }
2695
3148
  store.upsertFile({
@@ -2700,19 +3153,23 @@ async function runIndexerWithStore(store, opts) {
2700
3153
  lastIndexed: Date.now()
2701
3154
  });
2702
3155
  } catch (innerErr) {
2703
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
3156
+ errors.push(
3157
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
3158
+ );
2704
3159
  }
2705
3160
  }
2706
3161
  }
2707
3162
  }
2708
3163
  }
2709
- if (discoveredFiles) {
3164
+ if (discoveredFiles && discoveryComplete) {
2710
3165
  for (const [file_] of existingMeta) {
2711
3166
  if (!discoveredFiles.has(file_)) {
2712
3167
  store.deleteFile(file_);
2713
3168
  }
2714
3169
  }
2715
3170
  }
3171
+ store.resolveRefs();
3172
+ store.setMetadata("relation_graph_version", relationGraphVersion);
2716
3173
  const durationMs = Date.now() - startMs;
2717
3174
  store.setLastIndexed(Date.now());
2718
3175
  return {
@@ -2815,7 +3272,8 @@ port.on("message", (msg) => {
2815
3272
  type: "response",
2816
3273
  id: msg.id,
2817
3274
  ok: false,
2818
- error: err instanceof Error ? err.message : String(err)
3275
+ error: err instanceof Error ? err.message : String(err),
3276
+ errorName: err instanceof Error ? err.name : void 0
2819
3277
  });
2820
3278
  } catch {
2821
3279
  }