scip-query 0.3.5 → 0.4.1

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 (152) hide show
  1. package/README.md +2 -1
  2. package/dist/{chunk-VMM4SYV4.js → chunk-24LF6IZB.js} +17 -4
  3. package/dist/{chunk-EPWLXXBL.js → chunk-3NJSJ7TE.js} +28 -30
  4. package/dist/{chunk-SMDCNPMK.js → chunk-43A4UCS7.js} +3 -3
  5. package/dist/{chunk-CHDJXYBG.js → chunk-5GCORUNV.js} +3 -3
  6. package/dist/{chunk-OIDHN6GD.js → chunk-6CH23IAS.js} +148 -7
  7. package/dist/chunk-6ECR2FLR.js +60 -0
  8. package/dist/{chunk-C7H5WBTJ.js → chunk-6UZU7DFL.js} +3 -3
  9. package/dist/chunk-7BS4CPJX.js +162 -0
  10. package/dist/{chunk-UGQKAVCD.js → chunk-A6XLXV6W.js} +3 -3
  11. package/dist/chunk-ALUFWH3U.js +2695 -0
  12. package/dist/{chunk-F7XU27LU.js → chunk-CBIWNZZZ.js} +27 -3
  13. package/dist/{chunk-26DOJ63W.js → chunk-DUJNJQPO.js} +14 -3
  14. package/dist/{chunk-GSH2FPKV.js → chunk-EAGKJFDX.js} +3 -3
  15. package/dist/{chunk-VIYSWZCO.js → chunk-ELFGD5EW.js} +32 -4
  16. package/dist/chunk-FVJE4MQL.js +37 -0
  17. package/dist/{chunk-NFS5W3PP.js → chunk-GNAMV3JC.js} +3 -3
  18. package/dist/{chunk-TRESG7OB.js → chunk-J47VSL6I.js} +3 -3
  19. package/dist/chunk-J6QXMYAQ.js +115 -0
  20. package/dist/{chunk-6FKIA6EI.js → chunk-JHVQB4Y5.js} +12 -12
  21. package/dist/{chunk-YY4QGUQ5.js → chunk-JKXHHV4B.js} +2 -2
  22. package/dist/{chunk-EN2Z2CLO.js → chunk-KG4OFQEN.js} +17 -20
  23. package/dist/chunk-KLNKDX6A.js +110 -0
  24. package/dist/{chunk-5OMVSV6E.js → chunk-KYPXKV64.js} +3 -3
  25. package/dist/chunk-NXUIWD6K.js +87 -0
  26. package/dist/{chunk-LFJQVJYJ.js → chunk-OXX3QF24.js} +2 -2
  27. package/dist/{chunk-NG5F43OU.js → chunk-P3VCDYMJ.js} +70 -1
  28. package/dist/{chunk-7HK5ZLOE.js → chunk-PCU455MX.js} +2 -2
  29. package/dist/{chunk-O7Q7FDUJ.js → chunk-POLELLNM.js} +3 -3
  30. package/dist/chunk-PU2254N2.js +115 -0
  31. package/dist/{chunk-WGAD3GNR.js → chunk-QMXSLHZP.js} +5 -5
  32. package/dist/{chunk-YDBXNPYU.js → chunk-R7HPHMRZ.js} +3 -3
  33. package/dist/{chunk-HLUS2HEB.js → chunk-RE7POFGI.js} +5 -4
  34. package/dist/{chunk-VT4JBH6L.js → chunk-RJ5GULL6.js} +2 -2
  35. package/dist/{chunk-7KIMF5PV.js → chunk-RL74LF47.js} +2 -2
  36. package/dist/chunk-SVLUJSY7.js +75 -0
  37. package/dist/chunk-SYQR4QGK.js +38 -0
  38. package/dist/{chunk-QIXNAB5K.js → chunk-TO3L4YNK.js} +1 -2
  39. package/dist/{chunk-P3E6L7KW.js → chunk-TWVXFKJA.js} +84 -4
  40. package/dist/chunk-UJWI5CBB.js +75 -0
  41. package/dist/{chunk-J3JSOSUO.js → chunk-VKBOLNYN.js} +3 -3
  42. package/dist/{chunk-KBOQX573.js → chunk-VY2L4TP6.js} +20 -3
  43. package/dist/{chunk-KKCHYLVI.js → chunk-W46L2BXT.js} +2 -2
  44. package/dist/chunk-XUVPQDXW.js +34 -0
  45. package/dist/{chunk-DH7G3DDV.js → chunk-Z5VSUOEE.js} +2 -2
  46. package/dist/{chunk-GEXE2T6I.js → chunk-ZVZAIIB5.js} +23 -15
  47. package/dist/cli.js +2838 -870
  48. package/dist/{db-viWlyVtv.d.ts → db-C4rPbKkI.d.ts} +6 -1
  49. package/dist/index.d.ts +4 -4
  50. package/dist/index.js +376 -142
  51. package/dist/postinstall.js +9 -3
  52. package/dist/queries/affected.d.ts +1 -1
  53. package/dist/queries/affected.js +3 -3
  54. package/dist/queries/bottlenecks.d.ts +1 -1
  55. package/dist/queries/bottlenecks.js +3 -2
  56. package/dist/queries/by-kind.d.ts +1 -1
  57. package/dist/queries/by-kind.js +3 -2
  58. package/dist/queries/call-graph.d.ts +1 -1
  59. package/dist/queries/call-graph.js +3 -3
  60. package/dist/queries/change-surface.d.ts +1 -1
  61. package/dist/queries/change-surface.js +3 -3
  62. package/dist/queries/code.d.ts +1 -1
  63. package/dist/queries/code.js +3 -3
  64. package/dist/queries/complexity-hotspots.d.ts +1 -1
  65. package/dist/queries/complexity-hotspots.js +3 -3
  66. package/dist/queries/complexity.d.ts +1 -1
  67. package/dist/queries/complexity.js +3 -3
  68. package/dist/queries/convergence.d.ts +1 -1
  69. package/dist/queries/convergence.js +3 -3
  70. package/dist/queries/coupling.d.ts +1 -1
  71. package/dist/queries/coupling.js +3 -3
  72. package/dist/queries/cycles.d.ts +1 -1
  73. package/dist/queries/cycles.js +3 -3
  74. package/dist/queries/dataflow.d.ts +1 -1
  75. package/dist/queries/dataflow.js +3 -3
  76. package/dist/queries/dead.d.ts +1 -1
  77. package/dist/queries/dead.js +4 -4
  78. package/dist/queries/deep-chains.d.ts +1 -1
  79. package/dist/queries/deep-chains.js +3 -3
  80. package/dist/queries/deps.d.ts +1 -1
  81. package/dist/queries/deps.js +3 -3
  82. package/dist/queries/diff-impact.d.ts +1 -1
  83. package/dist/queries/diff-impact.js +2 -2
  84. package/dist/queries/drift.d.ts +1 -1
  85. package/dist/queries/drift.js +3 -3
  86. package/dist/queries/extract-candidates.d.ts +1 -1
  87. package/dist/queries/extract-candidates.js +3 -3
  88. package/dist/queries/fan.d.ts +1 -1
  89. package/dist/queries/fan.js +3 -3
  90. package/dist/queries/files.d.ts +1 -1
  91. package/dist/queries/health.d.ts +1 -1
  92. package/dist/queries/health.js +14 -14
  93. package/dist/queries/hierarchy.d.ts +1 -1
  94. package/dist/queries/hierarchy.js +3 -3
  95. package/dist/queries/hotspots.d.ts +1 -1
  96. package/dist/queries/hotspots.js +3 -2
  97. package/dist/queries/imports.d.ts +1 -1
  98. package/dist/queries/imports.js +3 -3
  99. package/dist/queries/index.d.ts +1 -1
  100. package/dist/queries/index.js +44 -44
  101. package/dist/queries/isolated.d.ts +3 -4
  102. package/dist/queries/isolated.js +4 -4
  103. package/dist/queries/members.d.ts +1 -1
  104. package/dist/queries/members.js +3 -3
  105. package/dist/queries/methods.d.ts +1 -1
  106. package/dist/queries/methods.js +3 -2
  107. package/dist/queries/outline.d.ts +1 -1
  108. package/dist/queries/outline.js +3 -3
  109. package/dist/queries/passthrough-candidates.d.ts +1 -1
  110. package/dist/queries/passthrough-candidates.js +3 -3
  111. package/dist/queries/redundant-reexports.d.ts +1 -1
  112. package/dist/queries/redundant-reexports.js +4 -4
  113. package/dist/queries/refs.d.ts +1 -1
  114. package/dist/queries/refs.js +3 -3
  115. package/dist/queries/similar-chains.d.ts +1 -1
  116. package/dist/queries/similar-chains.js +3 -3
  117. package/dist/queries/similar-files.d.ts +1 -1
  118. package/dist/queries/similar-files.js +3 -3
  119. package/dist/queries/similar-signatures.d.ts +5 -3
  120. package/dist/queries/similar-signatures.js +3 -2
  121. package/dist/queries/similar.d.ts +1 -1
  122. package/dist/queries/similar.js +3 -3
  123. package/dist/queries/slice.d.ts +1 -1
  124. package/dist/queries/slice.js +3 -3
  125. package/dist/queries/stale-abstractions.d.ts +1 -1
  126. package/dist/queries/stale-abstractions.js +3 -3
  127. package/dist/queries/stats.d.ts +1 -1
  128. package/dist/queries/surface.d.ts +1 -1
  129. package/dist/queries/surface.js +3 -3
  130. package/dist/queries/symbols.d.ts +1 -1
  131. package/dist/queries/symbols.js +3 -3
  132. package/dist/queries/system.d.ts +1 -1
  133. package/dist/queries/system.js +3 -3
  134. package/dist/queries/trace.d.ts +1 -1
  135. package/dist/queries/trace.js +3 -3
  136. package/dist/queries/wrapper-candidates.d.ts +1 -1
  137. package/dist/queries/wrapper-candidates.js +3 -3
  138. package/dist/reindex-worker.js +216 -64
  139. package/package.json +5 -1
  140. package/skills/scip-language-playbook/SKILL.md +371 -0
  141. package/dist/chunk-4JCSOF2O.js +0 -97
  142. package/dist/chunk-AXQKUYKF.js +0 -1442
  143. package/dist/chunk-CPVAQJEC.js +0 -46
  144. package/dist/chunk-EOROMIFO.js +0 -41
  145. package/dist/chunk-GU2H5QRN.js +0 -28
  146. package/dist/chunk-LQJUPXQY.js +0 -109
  147. package/dist/chunk-MPGIHELS.js +0 -39
  148. package/dist/chunk-TOIEB3LG.js +0 -78
  149. package/dist/chunk-UQEQ6AHX.js +0 -60
  150. package/dist/chunk-VJJKSGIX.js +0 -121
  151. package/dist/chunk-YGGFLMTM.js +0 -83
  152. package/dist/chunk-ZEUCXQBN.js +0 -71
package/dist/cli.js CHANGED
@@ -3,8 +3,8 @@
3
3
  // src/cli.ts
4
4
  import { program } from "commander";
5
5
  import { createRequire } from "module";
6
- import { existsSync as existsSync8, realpathSync } from "fs";
7
- import { join as join10 } from "path";
6
+ import { existsSync as existsSync10, realpathSync } from "fs";
7
+ import { join as join12 } from "path";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
9
 
10
10
  // src/db.ts
@@ -42,12 +42,23 @@ var ScipDatabase = class {
42
42
  * that need SQL-level filtering, use excludedPathPatterns().
43
43
  */
44
44
  get localSymbolPredicate() {
45
- return `EXISTS (
46
- SELECT 1
47
- FROM defn_enclosing_ranges local_der
48
- JOIN documents local_d ON local_der.document_id = local_d.id
49
- WHERE local_der.symbol_id = gs.id
50
- ${this.pathExclusionsFor("local_d").trimStart()}
45
+ return `(
46
+ EXISTS (
47
+ SELECT 1
48
+ FROM defn_enclosing_ranges local_der
49
+ JOIN documents local_d ON local_der.document_id = local_d.id
50
+ WHERE local_der.symbol_id = gs.id
51
+ ${this.pathExclusionsFor("local_d").trimStart()}
52
+ )
53
+ OR EXISTS (
54
+ SELECT 1
55
+ FROM mentions local_m
56
+ JOIN chunks local_c ON local_m.chunk_id = local_c.id
57
+ JOIN documents local_d ON local_c.document_id = local_d.id
58
+ WHERE local_m.symbol_id = gs.id
59
+ AND local_m.role = 1
60
+ ${this.pathExclusionsFor("local_d").trimStart()}
61
+ )
51
62
  )`;
52
63
  }
53
64
  /**
@@ -275,8 +286,8 @@ function ensureDir(dir) {
275
286
 
276
287
  // src/reindex/index.ts
277
288
  import { execFileSync as execFileSync3 } from "child_process";
278
- import { existsSync as existsSync4 } from "fs";
279
- import { join as join4 } from "path";
289
+ import { existsSync as existsSync6, renameSync } from "fs";
290
+ import { join as join6 } from "path";
280
291
 
281
292
  // src/scip-cli.ts
282
293
  import { execFileSync as execFileSync2 } from "child_process";
@@ -284,7 +295,9 @@ import { platform as platform2, arch } from "os";
284
295
 
285
296
  // src/reindex/install.ts
286
297
  import { execFileSync } from "child_process";
298
+ import { existsSync as existsSync3 } from "fs";
287
299
  import { platform } from "os";
300
+ import { join as join3 } from "path";
288
301
  var IS_WINDOWS = platform() === "win32";
289
302
  function isBinaryAvailable(name) {
290
303
  const cmd = IS_WINDOWS ? "where" : "which";
@@ -313,6 +326,67 @@ function resolveIndexerBinary(config) {
313
326
  function isIndexerInstalled(config) {
314
327
  return resolveIndexerBinary(config) !== null;
315
328
  }
329
+ function resolveProjectLocalIndexerBinary(config, projectRoot) {
330
+ for (const relativePath of config.projectLocalBinaries ?? []) {
331
+ const candidate = join3(projectRoot, relativePath);
332
+ if (existsSync3(candidate)) {
333
+ return candidate;
334
+ }
335
+ }
336
+ return null;
337
+ }
338
+ function resolveIndexerBinaryForProject(config, projectRoot) {
339
+ return resolveProjectLocalIndexerBinary(config, projectRoot) ?? resolveIndexerBinary(config);
340
+ }
341
+ function getIndexerExecutionEnv(config, baseEnv = process.env, binary = config.indexerBinary) {
342
+ if (config.indexerBinary !== "scip-dotnet") {
343
+ return baseEnv;
344
+ }
345
+ if (canRunDotnetIndexer(binary, baseEnv)) {
346
+ return baseEnv;
347
+ }
348
+ const dotnetRoot = resolveWorkingDotnetRoot(binary, baseEnv);
349
+ if (!dotnetRoot) {
350
+ return baseEnv;
351
+ }
352
+ return {
353
+ ...baseEnv,
354
+ DOTNET_ROOT: dotnetRoot
355
+ };
356
+ }
357
+ function getIndexerDependencyStatus(config, projectRoot) {
358
+ const binaryLabel = describeIndexerBinary(config);
359
+ const resolvedBinary = projectRoot ? resolveIndexerBinaryForProject(config, projectRoot) : resolveIndexerBinary(config);
360
+ if (!resolvedBinary) {
361
+ return {
362
+ language: config.language,
363
+ binaryLabel,
364
+ installed: false,
365
+ runnable: false,
366
+ resolvedBinary: null,
367
+ installUrl: config.installUrl
368
+ };
369
+ }
370
+ if (config.indexerBinary !== "scip-dotnet") {
371
+ return {
372
+ language: config.language,
373
+ binaryLabel,
374
+ installed: true,
375
+ runnable: true,
376
+ resolvedBinary
377
+ };
378
+ }
379
+ const runtimeProbe = probeDotnetRuntime(resolvedBinary);
380
+ return {
381
+ language: config.language,
382
+ binaryLabel,
383
+ installed: true,
384
+ runnable: runtimeProbe.runnable,
385
+ resolvedBinary,
386
+ installUrl: config.installUrl,
387
+ note: runtimeProbe.note
388
+ };
389
+ }
316
390
  function tryInstallIndexer(config, onStatus) {
317
391
  const methods2 = config.installMethods;
318
392
  const binaryLabel = describeIndexerBinary(config);
@@ -352,6 +426,64 @@ function tryInstallIndexer(config, onStatus) {
352
426
  }
353
427
  return false;
354
428
  }
429
+ function probeDotnetRuntime(binary) {
430
+ if (canRunDotnetIndexer(binary, process.env)) {
431
+ return { runnable: true };
432
+ }
433
+ const dotnetRoot = resolveWorkingDotnetRoot(binary, process.env);
434
+ if (dotnetRoot) {
435
+ return {
436
+ runnable: true,
437
+ note: `using .NET 9 runtime from ${dotnetRoot}`
438
+ };
439
+ }
440
+ const attemptedRoots = getDotnetRootCandidates(process.env);
441
+ const attemptedNote = attemptedRoots.length > 0 ? `.NET 9 runtime still unavailable after checking ${attemptedRoots.join(", ")}` : "binary is present, but scip-dotnet still needs a .NET 9 runtime";
442
+ return {
443
+ runnable: false,
444
+ note: attemptedNote
445
+ };
446
+ }
447
+ function resolveWorkingDotnetRoot(binary, env) {
448
+ for (const dotnetRoot of getDotnetRootCandidates(env)) {
449
+ if (canRunDotnetIndexer(binary, { ...env, DOTNET_ROOT: dotnetRoot })) {
450
+ return dotnetRoot;
451
+ }
452
+ }
453
+ return null;
454
+ }
455
+ function getDotnetRootCandidates(env) {
456
+ const candidates = [];
457
+ const configured = env["DOTNET_ROOT"];
458
+ if (configured && existsSync3(configured)) {
459
+ candidates.push(configured);
460
+ }
461
+ if (platform() === "darwin" && isBinaryAvailable("brew")) {
462
+ try {
463
+ const prefix = execFileSync("brew", ["--prefix", "dotnet@9"], {
464
+ stdio: "pipe",
465
+ env
466
+ }).toString().trim();
467
+ const candidate = join3(prefix, "libexec");
468
+ if (existsSync3(candidate) && !candidates.includes(candidate)) {
469
+ candidates.push(candidate);
470
+ }
471
+ } catch {
472
+ }
473
+ }
474
+ return candidates;
475
+ }
476
+ function canRunDotnetIndexer(binary, env) {
477
+ try {
478
+ execFileSync(binary, ["--version"], {
479
+ stdio: "pipe",
480
+ env
481
+ });
482
+ return true;
483
+ } catch {
484
+ return false;
485
+ }
486
+ }
355
487
 
356
488
  // src/scip-cli.ts
357
489
  var IS_WINDOWS2 = platform2() === "win32";
@@ -461,46 +593,151 @@ function tryInstallScipCli(onStatus) {
461
593
  }
462
594
 
463
595
  // src/reindex/detect.ts
464
- import { existsSync as existsSync3 } from "fs";
465
- import { join as join3 } from "path";
596
+ import { existsSync as existsSync4, readdirSync } from "fs";
597
+ import { extname, join as join4 } from "path";
598
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
599
+ ".git",
600
+ ".hg",
601
+ ".svn",
602
+ ".idea",
603
+ ".vscode",
604
+ "node_modules",
605
+ "vendor",
606
+ "dist",
607
+ "build",
608
+ "target",
609
+ "bin",
610
+ "obj",
611
+ ".dart_tool",
612
+ ".gradle",
613
+ ".next",
614
+ ".venv",
615
+ "venv",
616
+ "__pycache__"
617
+ ]);
466
618
  var LANGUAGE_MARKERS = [
467
- { language: "typescript", files: ["tsconfig.json", "tsconfig.base.json"] },
468
- { language: "rust", files: ["Cargo.toml"] },
469
- { language: "go", files: ["go.mod"] },
470
- { language: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts"] },
471
- { language: "kotlin", files: ["build.gradle.kts"] },
472
- { language: "scala", files: ["build.sbt"] },
473
- { language: "python", files: ["pyproject.toml", "setup.py", "setup.cfg", "Pipfile"] },
474
- { language: "ruby", files: ["Gemfile"] },
475
- { language: "csharp", files: ["*.csproj", "*.sln"] },
476
- { language: "dart", files: ["pubspec.yaml"] },
477
- { language: "php", files: ["composer.json"] },
478
- { language: "javascript", files: ["package.json"] }
479
- // Last very common, low specificity
619
+ { language: "typescript", files: ["tsconfig.json", "tsconfig.base.json"], extensions: [".ts", ".tsx", ".mts", ".cts"] },
620
+ { language: "rust", files: ["Cargo.toml"], extensions: [".rs"] },
621
+ { language: "go", files: ["go.mod"], extensions: [".go"] },
622
+ { language: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts"], extensions: [".java"] },
623
+ { language: "kotlin", files: ["build.gradle.kts"], extensions: [".kt", ".kts"] },
624
+ { language: "scala", files: ["build.sbt"], extensions: [".scala"] },
625
+ { language: "python", files: ["pyproject.toml", "setup.py", "setup.cfg", "Pipfile"], extensions: [".py", ".pyi"] },
626
+ { language: "ruby", files: ["Gemfile"], extensions: [".rb"] },
627
+ { language: "cpp", files: ["compile_commands.json", "CMakeLists.txt", "Makefile"], extensions: [".cc", ".cpp", ".cxx", ".hpp", ".hh", ".hxx"] },
628
+ { language: "c", files: ["compile_commands.json", "CMakeLists.txt", "Makefile"], extensions: [".c", ".h"] },
629
+ { language: "csharp", globs: ["*.csproj", "*.sln"], extensions: [".cs"] },
630
+ { language: "vb", globs: ["*.vbproj"], extensions: [".vb"] },
631
+ { language: "dart", files: ["pubspec.yaml"], extensions: [".dart"] },
632
+ { language: "php", files: ["composer.json"], extensions: [".php"] },
633
+ { language: "javascript", files: ["package.json"], extensions: [".js", ".jsx", ".mjs", ".cjs"] }
634
+ // Last — very common
480
635
  ];
481
636
  function detectLanguages(projectRoot) {
482
637
  const detected = [];
638
+ const rootEntries = safeReadDir(projectRoot);
639
+ const extensionSet = collectExtensions(projectRoot);
483
640
  for (const marker of LANGUAGE_MARKERS) {
484
- for (const file of marker.files) {
485
- if (file.includes("*")) {
486
- continue;
641
+ if (hasMarkerFile(projectRoot, marker.files)) {
642
+ detected.push(marker.language);
643
+ continue;
644
+ }
645
+ if (matchesRootGlob(rootEntries, marker.globs)) {
646
+ detected.push(marker.language);
647
+ continue;
648
+ }
649
+ if (hasExtension(extensionSet, marker.extensions)) {
650
+ detected.push(marker.language);
651
+ }
652
+ }
653
+ if (detected.includes("typescript")) {
654
+ removeLanguage(detected, "javascript");
655
+ }
656
+ if (detected.includes("cpp") && !extensionSet.has(".c")) {
657
+ removeLanguage(detected, "c");
658
+ }
659
+ return detected;
660
+ }
661
+ function safeReadDir(projectRoot) {
662
+ try {
663
+ return readdirSync(projectRoot);
664
+ } catch {
665
+ return [];
666
+ }
667
+ }
668
+ function hasMarkerFile(projectRoot, files2) {
669
+ if (!files2?.length) {
670
+ return false;
671
+ }
672
+ return files2.some((file) => existsSync4(join4(projectRoot, file)));
673
+ }
674
+ function matchesRootGlob(entries, globs) {
675
+ if (!globs?.length) {
676
+ return false;
677
+ }
678
+ return entries.some((entry) => globs.some((glob) => matchesSimpleGlob(entry, glob)));
679
+ }
680
+ function matchesSimpleGlob(entry, pattern) {
681
+ if (pattern === "*") {
682
+ return true;
683
+ }
684
+ if (!pattern.includes("*")) {
685
+ return entry === pattern;
686
+ }
687
+ const [prefix, suffix] = pattern.split("*");
688
+ return entry.startsWith(prefix ?? "") && entry.endsWith(suffix ?? "");
689
+ }
690
+ function collectExtensions(projectRoot) {
691
+ const found = /* @__PURE__ */ new Set();
692
+ const stack = [projectRoot];
693
+ while (stack.length > 0) {
694
+ const current = stack.pop();
695
+ if (!current) {
696
+ continue;
697
+ }
698
+ let entries;
699
+ try {
700
+ entries = readdirSync(current, { withFileTypes: true });
701
+ } catch {
702
+ continue;
703
+ }
704
+ for (const entry of entries) {
705
+ if (entry.name.startsWith(".") && !entry.name.endsWith("proj") && !entry.name.endsWith("sln")) {
706
+ if (entry.isDirectory()) {
707
+ continue;
708
+ }
487
709
  }
488
- if (existsSync3(join3(projectRoot, file))) {
489
- if (!detected.includes(marker.language)) {
490
- detected.push(marker.language);
710
+ const fullPath = join4(current, entry.name);
711
+ if (entry.isDirectory()) {
712
+ if (!IGNORED_DIRS.has(entry.name)) {
713
+ stack.push(fullPath);
491
714
  }
492
- break;
715
+ continue;
716
+ }
717
+ const extension = extname(entry.name).toLowerCase();
718
+ if (extension) {
719
+ found.add(extension);
493
720
  }
494
721
  }
495
722
  }
496
- if (detected.includes("typescript")) {
497
- const jsIdx = detected.indexOf("javascript");
498
- if (jsIdx !== -1) detected.splice(jsIdx, 1);
723
+ return found;
724
+ }
725
+ function hasExtension(extensionSet, extensions) {
726
+ if (!extensions?.length) {
727
+ return false;
728
+ }
729
+ return extensions.some((extension) => extensionSet.has(extension));
730
+ }
731
+ function removeLanguage(detected, language) {
732
+ const index = detected.indexOf(language);
733
+ if (index !== -1) {
734
+ detected.splice(index, 1);
499
735
  }
500
- return detected;
501
736
  }
502
737
 
503
738
  // src/reindex/indexers.ts
739
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
740
+ import { join as join5 } from "path";
504
741
  var INDEXER_CONFIGS = {
505
742
  typescript: {
506
743
  language: "typescript",
@@ -540,9 +777,7 @@ var INDEXER_CONFIGS = {
540
777
  args: ["index", "--output", outputPath]
541
778
  }),
542
779
  markerFiles: ["pom.xml", "build.gradle"],
543
- installMethods: [
544
- { label: "coursier", prerequisite: "cs", binary: "cs", args: ["install", "scip-java"] }
545
- ],
780
+ installMethods: [],
546
781
  installUrl: "https://github.com/sourcegraph/scip-java/releases"
547
782
  },
548
783
  scala: {
@@ -554,9 +789,7 @@ var INDEXER_CONFIGS = {
554
789
  args: ["index", "--output", outputPath]
555
790
  }),
556
791
  markerFiles: ["build.sbt"],
557
- installMethods: [
558
- { label: "coursier", prerequisite: "cs", binary: "cs", args: ["install", "scip-java"] }
559
- ],
792
+ installMethods: [],
560
793
  installUrl: "https://github.com/sourcegraph/scip-java/releases"
561
794
  },
562
795
  kotlin: {
@@ -568,9 +801,7 @@ var INDEXER_CONFIGS = {
568
801
  args: ["index", "--output", outputPath]
569
802
  }),
570
803
  markerFiles: ["build.gradle.kts"],
571
- installMethods: [
572
- { label: "coursier", prerequisite: "cs", binary: "cs", args: ["install", "scip-java"] }
573
- ],
804
+ installMethods: [],
574
805
  installUrl: "https://github.com/sourcegraph/scip-java/releases"
575
806
  },
576
807
  rust: {
@@ -606,10 +837,11 @@ var INDEXER_CONFIGS = {
606
837
  language: "ruby",
607
838
  indexerBinary: "scip-ruby",
608
839
  checkCommand: "scip-ruby --version",
609
- indexArgs: ({ outputPath }) => ({
610
- binary: "scip-ruby",
611
- args: ["--output", outputPath]
840
+ indexArgs: ({ indexerBinary }) => ({
841
+ binary: indexerBinary,
842
+ args: ["--dir", "."]
612
843
  }),
844
+ defaultOutputPath: "index.scip",
613
845
  markerFiles: ["Gemfile"],
614
846
  installMethods: [],
615
847
  installUrl: "https://github.com/sourcegraph/scip-ruby/releases"
@@ -634,7 +866,7 @@ var INDEXER_CONFIGS = {
634
866
  checkCommand: "scip-clang --version",
635
867
  indexArgs: ({ outputPath }) => ({
636
868
  binary: "scip-clang",
637
- args: ["--output", outputPath]
869
+ args: ["--compdb-path", "compile_commands.json", "--index-output-path", outputPath]
638
870
  }),
639
871
  markerFiles: ["CMakeLists.txt", "Makefile"],
640
872
  installMethods: [],
@@ -646,7 +878,7 @@ var INDEXER_CONFIGS = {
646
878
  checkCommand: "scip-clang --version",
647
879
  indexArgs: ({ outputPath }) => ({
648
880
  binary: "scip-clang",
649
- args: ["--output", outputPath]
881
+ args: ["--compdb-path", "compile_commands.json", "--index-output-path", outputPath]
650
882
  }),
651
883
  markerFiles: ["CMakeLists.txt", "Makefile"],
652
884
  installMethods: [],
@@ -656,11 +888,25 @@ var INDEXER_CONFIGS = {
656
888
  language: "csharp",
657
889
  indexerBinary: "scip-dotnet",
658
890
  checkCommand: "scip-dotnet --version",
659
- indexArgs: ({ outputPath }) => ({
891
+ indexArgs: ({ projectRoot, outputPath }) => ({
660
892
  binary: "scip-dotnet",
661
- args: ["index", "--output", outputPath]
893
+ args: ["index", resolveDotnetProject(projectRoot, [".sln", ".csproj"]) ?? projectRoot, "--output", outputPath, "--working-directory", projectRoot]
894
+ }),
895
+ markerFiles: ["*.csproj", "*.sln"],
896
+ installMethods: [
897
+ { label: "dotnet", prerequisite: "dotnet", binary: "dotnet", args: ["tool", "install", "--global", "scip-dotnet"] }
898
+ ],
899
+ installUrl: "https://github.com/sourcegraph/scip-dotnet/releases"
900
+ },
901
+ vb: {
902
+ language: "vb",
903
+ indexerBinary: "scip-dotnet",
904
+ checkCommand: "scip-dotnet --version",
905
+ indexArgs: ({ projectRoot, outputPath }) => ({
906
+ binary: "scip-dotnet",
907
+ args: ["index", resolveDotnetProject(projectRoot, [".sln", ".vbproj"]) ?? projectRoot, "--output", outputPath, "--working-directory", projectRoot]
662
908
  }),
663
- markerFiles: [],
909
+ markerFiles: ["*.vbproj", "*.sln"],
664
910
  installMethods: [
665
911
  { label: "dotnet", prerequisite: "dotnet", binary: "dotnet", args: ["tool", "install", "--global", "scip-dotnet"] }
666
912
  ],
@@ -670,9 +916,9 @@ var INDEXER_CONFIGS = {
670
916
  language: "dart",
671
917
  indexerBinary: "scip-dart",
672
918
  checkCommand: "scip-dart --version",
673
- indexArgs: ({ outputPath }) => ({
674
- binary: "scip-dart",
675
- args: ["index", "--output", outputPath]
919
+ indexArgs: ({ indexerBinary, outputPath }) => ({
920
+ binary: indexerBinary,
921
+ args: ["--output", outputPath]
676
922
  }),
677
923
  markerFiles: ["pubspec.yaml"],
678
924
  installMethods: [
@@ -683,21 +929,44 @@ var INDEXER_CONFIGS = {
683
929
  php: {
684
930
  language: "php",
685
931
  indexerBinary: "scip-php",
932
+ projectLocalBinaries: ["vendor/davidrjenni/scip-php/bin/scip-php", "vendor/bin/scip-php"],
686
933
  checkCommand: "scip-php --version",
687
- indexArgs: ({ outputPath }) => ({
688
- binary: "scip-php",
689
- args: ["index", "--output", outputPath]
690
- }),
934
+ indexArgs: ({ projectRoot, indexerBinary }) => {
935
+ const localBinary = join5(projectRoot, "vendor", "bin", "scip-php");
936
+ const nestedLocalBinary = join5(projectRoot, "vendor", "davidrjenni", "scip-php", "bin", "scip-php");
937
+ const targetBinary = existsSync5(nestedLocalBinary) ? nestedLocalBinary : existsSync5(localBinary) ? localBinary : indexerBinary;
938
+ return {
939
+ binary: "php",
940
+ args: [
941
+ "-d",
942
+ "error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED",
943
+ targetBinary
944
+ ]
945
+ };
946
+ },
947
+ defaultOutputPath: "index.scip",
691
948
  markerFiles: ["composer.json"],
692
- installMethods: [
693
- { label: "composer", prerequisite: "composer", binary: "composer", args: ["global", "require", "davidrjenni/scip-php"] }
694
- ],
949
+ installMethods: [],
695
950
  installUrl: "https://github.com/davidrjenni/scip-php/releases"
696
951
  }
697
952
  };
698
953
  function getIndexerConfig(language) {
699
954
  return INDEXER_CONFIGS[language];
700
955
  }
956
+ function resolveDotnetProject(projectRoot, suffixes) {
957
+ let entries;
958
+ try {
959
+ entries = readdirSync2(projectRoot);
960
+ } catch {
961
+ return null;
962
+ }
963
+ for (const entry of entries) {
964
+ if (suffixes.some((suffix) => entry.endsWith(suffix))) {
965
+ return join5(projectRoot, entry);
966
+ }
967
+ }
968
+ return null;
969
+ }
701
970
 
702
971
  // src/reindex/index.ts
703
972
  async function reindex(opts) {
@@ -707,8 +976,8 @@ async function reindex(opts) {
707
976
  onStatus = console.log,
708
977
  skipAutoInstall = false
709
978
  } = opts;
710
- const outputScip = opts.outputScip ?? join4(projectRoot, "index.scip");
711
- const outputDb = opts.outputDb ?? join4(projectRoot, "index.db");
979
+ const outputScip = opts.outputScip ?? join6(projectRoot, "index.scip");
980
+ const outputDb = opts.outputDb ?? join6(projectRoot, "index.db");
712
981
  const start = Date.now();
713
982
  const languages = opts.languages ?? detectLanguages(projectRoot);
714
983
  if (languages.length === 0) {
@@ -737,7 +1006,8 @@ async function reindex(opts) {
737
1006
  for (const lang of languages) {
738
1007
  const config = getIndexerConfig(lang);
739
1008
  const binaryLabel = describeIndexerBinary(config);
740
- if (!isIndexerInstalled(config)) {
1009
+ const projectLocalBinary = resolveProjectLocalIndexerBinary(config, projectRoot);
1010
+ if (!projectLocalBinary && !isIndexerInstalled(config)) {
741
1011
  if (skipAutoInstall) {
742
1012
  throw new Error(
743
1013
  `${binaryLabel} is required to index ${lang} but not found on PATH.
@@ -752,7 +1022,7 @@ async function reindex(opts) {
752
1022
  );
753
1023
  }
754
1024
  }
755
- const resolvedBinary = resolveIndexerBinary(config);
1025
+ const resolvedBinary = projectLocalBinary ?? resolveIndexerBinary(config);
756
1026
  if (!resolvedBinary) {
757
1027
  throw new Error(
758
1028
  `${binaryLabel} is required to index ${lang} but was not found on PATH after installation checks.
@@ -760,6 +1030,7 @@ async function reindex(opts) {
760
1030
  );
761
1031
  }
762
1032
  onStatus(`Indexing ${lang} with ${resolvedBinary}...`);
1033
+ const indexerEnv = getIndexerExecutionEnv(config, env, resolvedBinary);
763
1034
  const { binary, args } = config.indexArgs({
764
1035
  projectRoot,
765
1036
  outputPath: outputScip,
@@ -769,7 +1040,7 @@ async function reindex(opts) {
769
1040
  try {
770
1041
  execFileSync3(binary, args, {
771
1042
  cwd: projectRoot,
772
- env,
1043
+ env: indexerEnv,
773
1044
  stdio: "pipe",
774
1045
  maxBuffer: 50 * 1024 * 1024
775
1046
  });
@@ -777,12 +1048,14 @@ async function reindex(opts) {
777
1048
  const msg = err instanceof Error ? err.message : String(err);
778
1049
  throw new Error(
779
1050
  `Failed to index ${lang} with ${resolvedBinary}: ${msg}
780
- Make sure ${binaryLabel} is installed and available on PATH.`
1051
+ Make sure ${binaryLabel} is installed and available on PATH.`,
1052
+ { cause: err }
781
1053
  );
782
1054
  }
1055
+ moveDefaultOutputIfNeeded(config, projectRoot, outputScip);
783
1056
  }
784
1057
  onStatus("Converting to SQLite...");
785
- if (!existsSync4(outputScip)) {
1058
+ if (!existsSync6(outputScip)) {
786
1059
  throw new Error(`SCIP index not found at ${outputScip} after indexing`);
787
1060
  }
788
1061
  try {
@@ -793,17 +1066,26 @@ Make sure ${binaryLabel} is installed and available on PATH.`
793
1066
  });
794
1067
  } catch (err) {
795
1068
  const msg = err instanceof Error ? err.message : String(err);
796
- throw new Error(`Failed to convert SCIP index to SQLite: ${msg}`);
1069
+ throw new Error(`Failed to convert SCIP index to SQLite: ${msg}`, { cause: err });
797
1070
  }
798
1071
  const durationMs = Date.now() - start;
799
1072
  onStatus(`Done in ${(durationMs / 1e3).toFixed(1)}s`);
800
1073
  return { languages, indexPath: outputScip, dbPath: outputDb, durationMs };
801
1074
  }
1075
+ function moveDefaultOutputIfNeeded(config, projectRoot, outputScip) {
1076
+ if (!config.defaultOutputPath) {
1077
+ return;
1078
+ }
1079
+ const defaultOutputPath = join6(projectRoot, config.defaultOutputPath);
1080
+ if (outputScip !== defaultOutputPath && existsSync6(defaultOutputPath)) {
1081
+ renameSync(defaultOutputPath, outputScip);
1082
+ }
1083
+ }
802
1084
 
803
1085
  // src/watch.ts
804
1086
  import { watch } from "fs";
805
- import { existsSync as existsSync5, renameSync } from "fs";
806
- import { join as join5, relative } from "path";
1087
+ import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
1088
+ import { join as join7, relative } from "path";
807
1089
  import { fork } from "child_process";
808
1090
  import ignore2 from "ignore";
809
1091
  var Watcher = class {
@@ -881,7 +1163,7 @@ var Watcher = class {
881
1163
  }
882
1164
  // ── Internal ─────────────────────────────────────────────
883
1165
  handleFileChange(filename) {
884
- const rel = relative(this.projectRoot, join5(this.projectRoot, filename));
1166
+ const rel = relative(this.projectRoot, join7(this.projectRoot, filename));
885
1167
  if (this.gitignoreFilter.isIgnored(rel)) return;
886
1168
  if (this.extraIgnore.ignores(rel)) return;
887
1169
  if (filename.endsWith("index.db") || filename.endsWith("index.scip") || filename.endsWith("index.db.tmp") || filename.endsWith(".scipquery.json")) {
@@ -984,11 +1266,11 @@ var Watcher = class {
984
1266
  child.on("exit", (code2) => {
985
1267
  if (code2 === 0) {
986
1268
  try {
987
- if (existsSync5(tmpDb)) {
988
- renameSync(tmpDb, this.indexPaths.dbPath);
1269
+ if (existsSync7(tmpDb)) {
1270
+ renameSync2(tmpDb, this.indexPaths.dbPath);
989
1271
  }
990
- if (existsSync5(tmpScip)) {
991
- renameSync(tmpScip, this.indexPaths.indexPath);
1272
+ if (existsSync7(tmpScip)) {
1273
+ renameSync2(tmpScip, this.indexPaths.indexPath);
992
1274
  }
993
1275
  resolve4(Date.now() - start);
994
1276
  } catch (err) {
@@ -1042,27 +1324,36 @@ function files(db, pattern) {
1042
1324
  }
1043
1325
 
1044
1326
  // src/query-support.ts
1045
- import { basename } from "path";
1327
+ import { basename as basename2 } from "path";
1046
1328
 
1047
1329
  // src/source-analysis.ts
1048
1330
  import {
1049
- existsSync as existsSync6,
1050
- readFileSync as readFileSync4
1331
+ existsSync as existsSync8,
1332
+ readFileSync as readFileSync3
1051
1333
  } from "fs";
1052
1334
  import {
1335
+ basename,
1053
1336
  dirname as dirname2,
1054
- extname,
1055
- join as join6,
1337
+ extname as extname2,
1338
+ join as join8,
1056
1339
  relative as relative2,
1057
1340
  resolve as resolve2
1058
1341
  } from "path";
1059
1342
  var SOURCE_IMPORT_CACHE = /* @__PURE__ */ new WeakMap();
1343
+ var SOURCE_EXPORT_CACHE = /* @__PURE__ */ new WeakMap();
1060
1344
  var SOURCE_TEXT_CACHE = /* @__PURE__ */ new WeakMap();
1061
1345
  var SOURCE_CALL_CACHE = /* @__PURE__ */ new WeakMap();
1062
1346
  var SOURCE_BINDING_CACHE = /* @__PURE__ */ new WeakMap();
1063
1347
  var INDEXED_PATH_CACHE = /* @__PURE__ */ new WeakMap();
1064
1348
  var SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1065
1349
  var PYTHON_SOURCE_EXTENSIONS = [".py", ".pyi"];
1350
+ var JVM_SOURCE_EXTENSIONS = [".java", ".scala", ".kt", ".kts"];
1351
+ var RUST_SOURCE_EXTENSIONS = [".rs"];
1352
+ var RUBY_SOURCE_EXTENSIONS = [".rb"];
1353
+ var C_LIKE_SOURCE_EXTENSIONS = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh", ".hxx"];
1354
+ var DOTNET_SOURCE_EXTENSIONS = [".cs", ".vb"];
1355
+ var DART_SOURCE_EXTENSIONS = [".dart"];
1356
+ var PHP_SOURCE_EXTENSIONS = [".php"];
1066
1357
  function getSourceImports(db, relativePath) {
1067
1358
  const cache = getCachedMap(SOURCE_IMPORT_CACHE, db);
1068
1359
  const normalized = normalizePath(relativePath);
@@ -1070,13 +1361,30 @@ function getSourceImports(db, relativePath) {
1070
1361
  if (cached) {
1071
1362
  return cached;
1072
1363
  }
1073
- const fullPath = join6(db.config.projectRoot, normalized);
1074
- if (!existsSync6(fullPath)) {
1364
+ const fullPath = join8(db.config.projectRoot, normalized);
1365
+ if (!existsSync8(fullPath)) {
1366
+ cache.set(normalized, []);
1367
+ return [];
1368
+ }
1369
+ const source = readFileSync3(fullPath, "utf-8");
1370
+ const parsed = isPythonSourcePath(normalized) ? parsePythonImports(db, normalized, source) : isJavaScriptSourcePath(normalized) ? parseJavaScriptImports(db, normalized, source) : isJvmSourcePath(normalized) ? parseJvmImports(db, normalized, source) : isRustSourcePath(normalized) ? parseRustImports(db, normalized, source) : isRubySourcePath(normalized) ? parseRubyImports(db, normalized, source) : isCLikeSourcePath(normalized) ? parseCLikeImports(db, normalized, source) : isDotNetSourcePath(normalized) ? parseDotNetImports(db, normalized, source) : isDartSourcePath(normalized) ? parseDartImports(db, normalized, source) : isPhpSourcePath(normalized) ? parsePhpImports(db, normalized, source) : [];
1371
+ cache.set(normalized, parsed);
1372
+ return parsed;
1373
+ }
1374
+ function getSourceExports(db, relativePath) {
1375
+ const cache = getCachedMap(SOURCE_EXPORT_CACHE, db);
1376
+ const normalized = normalizePath(relativePath);
1377
+ const cached = cache.get(normalized);
1378
+ if (cached) {
1379
+ return cached;
1380
+ }
1381
+ const fullPath = join8(db.config.projectRoot, normalized);
1382
+ if (!existsSync8(fullPath)) {
1075
1383
  cache.set(normalized, []);
1076
1384
  return [];
1077
1385
  }
1078
- const source = readFileSync4(fullPath, "utf-8");
1079
- const parsed = isPythonSourcePath(normalized) ? parsePythonImports(db, normalized, source) : parseJavaScriptImports(db, normalized, source);
1386
+ const source = readFileSync3(fullPath, "utf-8");
1387
+ const parsed = isDartSourcePath(normalized) ? parseDartExports(db, normalized, source) : isRustSourcePath(normalized) ? parseRustExports(db, normalized, source) : [];
1080
1388
  cache.set(normalized, parsed);
1081
1389
  return parsed;
1082
1390
  }
@@ -1225,97 +1533,358 @@ function parseJavaScriptImportStatement(db, importerPath, clause, specifier, sta
1225
1533
  };
1226
1534
  });
1227
1535
  }
1228
- function parsePythonCalls(lines, baseLine) {
1229
- const calls = [];
1230
- const controlKeywords = /* @__PURE__ */ new Set([
1231
- "if",
1232
- "for",
1233
- "while",
1234
- "with",
1235
- "except",
1236
- "elif",
1237
- "return",
1238
- "yield",
1239
- "assert",
1240
- "raise",
1241
- "lambda",
1242
- "class",
1243
- "def"
1244
- ]);
1245
- for (let index = 0; index < lines.length; index++) {
1246
- const rawLine = lines[index] ?? "";
1247
- const stripped = stripCommentsAndStrings(rawLine);
1248
- if (!stripped.trim()) continue;
1249
- const attributeMatches = [...stripped.matchAll(/\b([A-Za-z_][\w]*)\s*\.\s*([A-Za-z_][\w]*)\s*\(/g)];
1250
- const attributeRanges = attributeMatches.map((match) => ({
1251
- start: match.index ?? -1,
1252
- end: (match.index ?? -1) + match[0].length
1253
- }));
1254
- for (const match of attributeMatches) {
1255
- const receiverName = match[1];
1256
- const calleeName = match[2];
1257
- if (!receiverName || !calleeName) continue;
1258
- calls.push({
1259
- receiverName,
1260
- calleeName,
1261
- line: baseLine + index
1262
- });
1263
- }
1264
- for (const match of stripped.matchAll(/\b([A-Za-z_][\w]*)\s*\(/g)) {
1265
- const calleeName = match[1];
1266
- const start = match.index ?? -1;
1267
- if (!calleeName || start < 0) continue;
1268
- if (controlKeywords.has(calleeName)) continue;
1269
- if (attributeRanges.some((range) => start >= range.start && start < range.end)) continue;
1270
- const prefix = stripped.slice(0, start).trimEnd();
1271
- if (prefix.endsWith("def") || prefix.endsWith("class") || prefix.endsWith("async def")) {
1272
- continue;
1273
- }
1274
- calls.push({
1275
- receiverName: null,
1276
- calleeName,
1277
- line: baseLine + index
1278
- });
1279
- }
1536
+ function parseJvmImports(db, importerPath, source) {
1537
+ const statements = [];
1538
+ for (const match of source.matchAll(/^[ \t]*import\s+(?:static\s+)?(.+?)\s*;?$/gm)) {
1539
+ const clause = match[1]?.trim();
1540
+ const full = match[0];
1541
+ if (!clause || !full || typeof match.index !== "number") continue;
1542
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1543
+ statements.push(...parseJvmImportClause(db, importerPath, clause, body));
1280
1544
  }
1281
- return calls;
1545
+ return statements;
1282
1546
  }
1283
- function parseJavaScriptCalls(lines, baseLine) {
1284
- const calls = [];
1285
- const controlKeywords = /* @__PURE__ */ new Set([
1286
- "if",
1287
- "for",
1288
- "while",
1289
- "switch",
1290
- "catch",
1291
- "function",
1292
- "class",
1293
- "return",
1294
- "typeof",
1295
- "import"
1296
- ]);
1297
- for (let index = 0; index < lines.length; index++) {
1298
- const stripped = stripCommentsAndStrings(lines[index] ?? "");
1299
- if (!stripped.trim()) continue;
1300
- const attributeMatches = [
1301
- ...stripped.matchAll(/\b([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*\(/g),
1302
- ...stripped.matchAll(/\bthis\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*\(/g)
1303
- ];
1304
- const attributeRanges = attributeMatches.map((match) => ({
1305
- start: match.index ?? -1,
1306
- end: (match.index ?? -1) + match[0].length
1307
- }));
1308
- for (const match of attributeMatches) {
1309
- const receiverName = match[2] ? match[1] : "this";
1310
- const calleeName = match[2] ?? match[1];
1311
- if (!receiverName || !calleeName) continue;
1312
- calls.push({
1313
- receiverName,
1314
- calleeName,
1315
- line: baseLine + index
1316
- });
1317
- }
1318
- for (const match of stripped.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
1547
+ function parseJvmImportClause(db, importerPath, clause, body) {
1548
+ if (clause.includes("{") && clause.includes("}")) {
1549
+ const prefix = clause.slice(0, clause.indexOf("{")).replace(/\.$/, "").trim();
1550
+ const inner = clause.slice(clause.indexOf("{") + 1, clause.lastIndexOf("}")).trim();
1551
+ return splitTopLevel(inner).flatMap((entry) => {
1552
+ const cleaned = entry.trim();
1553
+ if (!cleaned) return [];
1554
+ const [importedPart, aliasPart] = cleaned.includes("=>") ? cleaned.split(/\s*=>\s*/) : cleaned.split(/\s+as\s+/);
1555
+ const importedName = importedPart?.trim();
1556
+ if (!importedName || importedName === "_") return [];
1557
+ const localName = (aliasPart ?? importedName.split(".").pop() ?? importedName).trim();
1558
+ const qualified = importedName === "_" ? prefix : `${prefix}.${importedName}`.replace(/\.\./g, ".");
1559
+ return [buildSimpleImport(db, importerPath, body, qualified, importedName, localName)];
1560
+ });
1561
+ }
1562
+ return [buildSimpleImport(
1563
+ db,
1564
+ importerPath,
1565
+ body,
1566
+ clause,
1567
+ clause.split(".").pop() ?? clause,
1568
+ clause.split(".").pop() ?? clause
1569
+ )];
1570
+ }
1571
+ function parseRustImports(db, importerPath, source) {
1572
+ const statements = [];
1573
+ for (const match of source.matchAll(/^[ \t]*use\s+(.+?)\s*;$/gm)) {
1574
+ const clause = match[1]?.trim();
1575
+ const full = match[0];
1576
+ if (!clause || !full || typeof match.index !== "number") continue;
1577
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1578
+ statements.push(...parseRustUseClause(db, importerPath, clause, body));
1579
+ }
1580
+ return statements;
1581
+ }
1582
+ function parseRustUseClause(db, importerPath, clause, body) {
1583
+ const trimmed = clause.trim();
1584
+ if (trimmed.includes("{") && trimmed.includes("}")) {
1585
+ const prefix = trimmed.slice(0, trimmed.indexOf("{")).replace(/::$/, "").trim();
1586
+ const inner = trimmed.slice(trimmed.indexOf("{") + 1, trimmed.lastIndexOf("}")).trim();
1587
+ return splitTopLevel(inner).flatMap((entry) => {
1588
+ const cleaned = entry.trim();
1589
+ if (!cleaned || cleaned === "self") return [];
1590
+ const [importedPart2, aliasPart2] = cleaned.split(/\s+as\s+/);
1591
+ const importedName2 = importedPart2?.trim();
1592
+ if (!importedName2) return [];
1593
+ const localName2 = (aliasPart2 ?? importedName2.split("::").pop() ?? importedName2).trim();
1594
+ const moduleSpecifier = `${prefix}::${importedName2}`.replace(/::::/g, "::");
1595
+ return [buildSimpleImport(
1596
+ db,
1597
+ importerPath,
1598
+ body,
1599
+ moduleSpecifier,
1600
+ importedName2.split("::").pop() ?? importedName2,
1601
+ localName2,
1602
+ resolveRustImportPath(db, importerPath, prefix)
1603
+ )];
1604
+ });
1605
+ }
1606
+ const [importedPart, aliasPart] = trimmed.split(/\s+as\s+/);
1607
+ const importedName = importedPart?.trim() ?? trimmed;
1608
+ const localName = (aliasPart ?? importedName.split("::").pop() ?? importedName).trim();
1609
+ const resolved = resolveRustImportPath(db, importerPath, importedName) ?? resolveRustImportPath(db, importerPath, importedName.split("::").slice(0, -1).join("::"));
1610
+ return [buildSimpleImport(
1611
+ db,
1612
+ importerPath,
1613
+ body,
1614
+ importedName,
1615
+ importedName.split("::").pop() ?? importedName,
1616
+ localName,
1617
+ resolved
1618
+ )];
1619
+ }
1620
+ function parseRustExports(db, importerPath, source) {
1621
+ const statements = [];
1622
+ for (const match of source.matchAll(/^[ \t]*pub\s+use\s+(.+?)\s*;$/gm)) {
1623
+ const clause = match[1]?.trim();
1624
+ if (!clause) continue;
1625
+ statements.push(...parseRustExportClause(db, importerPath, clause));
1626
+ }
1627
+ return statements;
1628
+ }
1629
+ function parseRustExportClause(db, importerPath, clause) {
1630
+ const trimmed = clause.trim();
1631
+ if (trimmed.includes("{") && trimmed.includes("}")) {
1632
+ const prefix = trimmed.slice(0, trimmed.indexOf("{")).replace(/::$/, "").trim();
1633
+ const inner = trimmed.slice(trimmed.indexOf("{") + 1, trimmed.lastIndexOf("}")).trim();
1634
+ return splitTopLevel(inner).flatMap((entry) => {
1635
+ const cleaned = entry.trim();
1636
+ if (!cleaned || cleaned === "self") return [];
1637
+ const [qualifiedPart] = cleaned.split(/\s+as\s+/);
1638
+ const qualified = `${prefix}::${qualifiedPart?.trim() ?? cleaned}`.replace(/::::/g, "::");
1639
+ return [buildRustExport(db, importerPath, qualified)];
1640
+ });
1641
+ }
1642
+ return [buildRustExport(db, importerPath, trimmed)];
1643
+ }
1644
+ function buildRustExport(db, importerPath, specifier) {
1645
+ return {
1646
+ specifier,
1647
+ sourcePath: resolveRustImportPath(db, importerPath, specifier) ?? resolveRustImportPath(db, importerPath, specifier.split("::").slice(0, -1).join("::"))
1648
+ };
1649
+ }
1650
+ function parseRubyImports(db, importerPath, source) {
1651
+ const statements = [];
1652
+ for (const match of source.matchAll(/^[ \t]*(require_relative|require)\s+["']([^"']+)["']\s*$/gm)) {
1653
+ const kind = match[1];
1654
+ const specifier = match[2];
1655
+ const full = match[0];
1656
+ if (!kind || !specifier || !full || typeof match.index !== "number") continue;
1657
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1658
+ const sourcePath = kind === "require_relative" ? resolveRubyImportPath(db, importerPath, specifier) : null;
1659
+ if (sourcePath) {
1660
+ const localName = rubyConstantName(specifier);
1661
+ statements.push({
1662
+ importedName: localName,
1663
+ localName,
1664
+ sourcePath,
1665
+ kind: "named",
1666
+ used: hasIdentifierUsage(body, localName),
1667
+ usedMembers: []
1668
+ });
1669
+ continue;
1670
+ }
1671
+ statements.push({
1672
+ importedName: specifier,
1673
+ localName: null,
1674
+ sourcePath,
1675
+ kind: "side-effect",
1676
+ used: true,
1677
+ usedMembers: []
1678
+ });
1679
+ }
1680
+ return statements;
1681
+ }
1682
+ function rubyConstantName(specifier) {
1683
+ return basename(specifier).replace(/\.[^.]+$/, "").split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1684
+ }
1685
+ function parseCLikeImports(db, importerPath, source) {
1686
+ const statements = [];
1687
+ for (const match of source.matchAll(/^[ \t]*#include\s+[<"]([^">]+)[">]\s*$/gm)) {
1688
+ const specifier = match[1]?.trim();
1689
+ const full = match[0];
1690
+ if (!specifier || !full || typeof match.index !== "number") continue;
1691
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1692
+ const localName = basename(specifier).replace(/\.[^.]+$/, "");
1693
+ statements.push({
1694
+ importedName: specifier,
1695
+ localName,
1696
+ sourcePath: resolveCLikeImportPath(db, importerPath, specifier),
1697
+ kind: "named",
1698
+ used: hasIdentifierUsage(body, localName),
1699
+ usedMembers: []
1700
+ });
1701
+ }
1702
+ return statements;
1703
+ }
1704
+ function parseDotNetImports(db, importerPath, source) {
1705
+ const statements = [];
1706
+ const lineRegex = isVisualBasicSourcePath(importerPath) ? /^[ \t]*Imports\s+(.+?)\s*$/gm : /^[ \t]*using\s+(.+?)\s*;$/gm;
1707
+ for (const match of source.matchAll(lineRegex)) {
1708
+ const clause = match[1]?.trim();
1709
+ const full = match[0];
1710
+ if (!clause || !full || typeof match.index !== "number") continue;
1711
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1712
+ const [aliasPart, targetPart] = isVisualBasicSourcePath(importerPath) ? clause.split(/\s*=\s*/) : clause.split(/\s*=\s*/);
1713
+ const hasAlias = Boolean(targetPart);
1714
+ const qualified = (hasAlias ? targetPart : aliasPart)?.trim() ?? clause;
1715
+ const importedName = qualified.split(".").pop() ?? qualified;
1716
+ const localName = hasAlias ? aliasPart?.trim() ?? importedName : importedName;
1717
+ statements.push(buildSimpleImport(
1718
+ db,
1719
+ importerPath,
1720
+ body,
1721
+ qualified,
1722
+ importedName,
1723
+ localName,
1724
+ resolveQualifiedImportPath(db, qualified, DOTNET_SOURCE_EXTENSIONS)
1725
+ ));
1726
+ }
1727
+ return statements;
1728
+ }
1729
+ function parseDartImports(db, importerPath, source) {
1730
+ const statements = [];
1731
+ for (const match of source.matchAll(/^[ \t]*import\s+['"]([^'"]+)['"](?:\s+as\s+([A-Za-z_]\w*))?[\s\S]*?;$/gm)) {
1732
+ const specifier = match[1]?.trim();
1733
+ const alias = match[2]?.trim() ?? null;
1734
+ const full = match[0];
1735
+ if (!specifier || !full || typeof match.index !== "number") continue;
1736
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1737
+ statements.push({
1738
+ importedName: specifier,
1739
+ localName: alias,
1740
+ sourcePath: resolveDartImportPath(db, importerPath, specifier),
1741
+ kind: alias ? "namespace" : "side-effect",
1742
+ used: alias ? hasIdentifierUsage(body, alias) : true,
1743
+ usedMembers: alias ? collectNamespaceMembers(body, alias) : []
1744
+ });
1745
+ }
1746
+ return statements;
1747
+ }
1748
+ function parseDartExports(db, importerPath, source) {
1749
+ const statements = [];
1750
+ for (const match of source.matchAll(/^[ \t]*export\s+['"]([^'"]+)['"][\s\S]*?;$/gm)) {
1751
+ const specifier = match[1]?.trim();
1752
+ if (!specifier) continue;
1753
+ statements.push({
1754
+ specifier,
1755
+ sourcePath: resolveDartImportPath(db, importerPath, specifier)
1756
+ });
1757
+ }
1758
+ return statements;
1759
+ }
1760
+ function parsePhpImports(db, importerPath, source) {
1761
+ const statements = [];
1762
+ for (const match of source.matchAll(/^[ \t]*use\s+(.+?)\s*;$/gm)) {
1763
+ const clause = match[1]?.trim();
1764
+ const full = match[0];
1765
+ if (!clause || !full || typeof match.index !== "number") continue;
1766
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1767
+ for (const entry of splitTopLevel(clause)) {
1768
+ const cleaned = entry.trim();
1769
+ if (!cleaned) continue;
1770
+ const [qualifiedPart, aliasPart] = cleaned.split(/\s+as\s+/i);
1771
+ const qualified = qualifiedPart?.trim() ?? cleaned;
1772
+ const importedName = qualified.split("\\").pop() ?? qualified;
1773
+ const localName = (aliasPart ?? importedName).trim();
1774
+ statements.push(buildSimpleImport(
1775
+ db,
1776
+ importerPath,
1777
+ body,
1778
+ qualified,
1779
+ importedName,
1780
+ localName,
1781
+ resolveQualifiedImportPath(db, qualified.replace(/\\/g, "."), PHP_SOURCE_EXTENSIONS)
1782
+ ));
1783
+ }
1784
+ }
1785
+ return statements;
1786
+ }
1787
+ function buildSimpleImport(db, importerPath, body, qualifiedName, importedName, localName, sourcePath) {
1788
+ return {
1789
+ importedName,
1790
+ localName,
1791
+ sourcePath: sourcePath ?? resolveQualifiedImportPath(db, qualifiedName, extensionFamilyFor(importerPath)),
1792
+ kind: "named",
1793
+ used: hasIdentifierUsage(body, localName),
1794
+ usedMembers: []
1795
+ };
1796
+ }
1797
+ function parsePythonCalls(lines, baseLine) {
1798
+ const calls = [];
1799
+ const controlKeywords = /* @__PURE__ */ new Set([
1800
+ "if",
1801
+ "for",
1802
+ "while",
1803
+ "with",
1804
+ "except",
1805
+ "elif",
1806
+ "return",
1807
+ "yield",
1808
+ "assert",
1809
+ "raise",
1810
+ "lambda",
1811
+ "class",
1812
+ "def"
1813
+ ]);
1814
+ for (let index = 0; index < lines.length; index++) {
1815
+ const rawLine = lines[index] ?? "";
1816
+ const stripped = stripCommentsAndStrings(rawLine);
1817
+ if (!stripped.trim()) continue;
1818
+ const attributeMatches = [...stripped.matchAll(/\b([A-Za-z_][\w]*)\s*\.\s*([A-Za-z_][\w]*)\s*\(/g)];
1819
+ const attributeRanges = attributeMatches.map((match) => ({
1820
+ start: match.index ?? -1,
1821
+ end: (match.index ?? -1) + match[0].length
1822
+ }));
1823
+ for (const match of attributeMatches) {
1824
+ const receiverName = match[1];
1825
+ const calleeName = match[2];
1826
+ if (!receiverName || !calleeName) continue;
1827
+ calls.push({
1828
+ receiverName,
1829
+ calleeName,
1830
+ line: baseLine + index
1831
+ });
1832
+ }
1833
+ for (const match of stripped.matchAll(/\b([A-Za-z_][\w]*)\s*\(/g)) {
1834
+ const calleeName = match[1];
1835
+ const start = match.index ?? -1;
1836
+ if (!calleeName || start < 0) continue;
1837
+ if (controlKeywords.has(calleeName)) continue;
1838
+ if (attributeRanges.some((range) => start >= range.start && start < range.end)) continue;
1839
+ const prefix = stripped.slice(0, start).trimEnd();
1840
+ if (prefix.endsWith("def") || prefix.endsWith("class") || prefix.endsWith("async def")) {
1841
+ continue;
1842
+ }
1843
+ calls.push({
1844
+ receiverName: null,
1845
+ calleeName,
1846
+ line: baseLine + index
1847
+ });
1848
+ }
1849
+ }
1850
+ return calls;
1851
+ }
1852
+ function parseJavaScriptCalls(lines, baseLine) {
1853
+ const calls = [];
1854
+ const controlKeywords = /* @__PURE__ */ new Set([
1855
+ "if",
1856
+ "for",
1857
+ "while",
1858
+ "switch",
1859
+ "catch",
1860
+ "function",
1861
+ "class",
1862
+ "return",
1863
+ "typeof",
1864
+ "import"
1865
+ ]);
1866
+ for (let index = 0; index < lines.length; index++) {
1867
+ const stripped = stripCommentsAndStrings(lines[index] ?? "");
1868
+ if (!stripped.trim()) continue;
1869
+ const attributeMatches = [
1870
+ ...stripped.matchAll(/\b([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*\(/g),
1871
+ ...stripped.matchAll(/\bthis\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*\(/g)
1872
+ ];
1873
+ const attributeRanges = attributeMatches.map((match) => ({
1874
+ start: match.index ?? -1,
1875
+ end: (match.index ?? -1) + match[0].length
1876
+ }));
1877
+ for (const match of attributeMatches) {
1878
+ const receiverName = match[2] ? match[1] : "this";
1879
+ const calleeName = match[2] ?? match[1];
1880
+ if (!receiverName || !calleeName) continue;
1881
+ calls.push({
1882
+ receiverName,
1883
+ calleeName,
1884
+ line: baseLine + index
1885
+ });
1886
+ }
1887
+ for (const match of stripped.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
1319
1888
  const calleeName = match[1];
1320
1889
  const start = match.index ?? -1;
1321
1890
  if (!calleeName || start < 0) continue;
@@ -1574,18 +2143,33 @@ function resolveImportPath(db, importerPath, specifier) {
1574
2143
  if (isPythonSourcePath(importerPath)) {
1575
2144
  return resolvePythonImportPath(db, importerPath, specifier);
1576
2145
  }
2146
+ if (isRustSourcePath(importerPath)) {
2147
+ return resolveRustImportPath(db, importerPath, specifier);
2148
+ }
2149
+ if (isRubySourcePath(importerPath)) {
2150
+ return resolveRubyImportPath(db, importerPath, specifier);
2151
+ }
2152
+ if (isCLikeSourcePath(importerPath)) {
2153
+ return resolveCLikeImportPath(db, importerPath, specifier);
2154
+ }
2155
+ if (isJvmSourcePath(importerPath) || isDotNetSourcePath(importerPath) || isPhpSourcePath(importerPath)) {
2156
+ return resolveQualifiedImportPath(db, specifier.replace(/\\/g, "."), extensionFamilyFor(importerPath));
2157
+ }
2158
+ if (isDartSourcePath(importerPath)) {
2159
+ return resolveDartImportPath(db, importerPath, specifier);
2160
+ }
1577
2161
  return resolveJavaScriptImportPath(db, importerPath, specifier);
1578
2162
  }
1579
2163
  function resolveJavaScriptImportPath(db, importerPath, specifier) {
1580
2164
  if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
1581
2165
  return null;
1582
2166
  }
1583
- const importerDir = dirname2(join6(db.config.projectRoot, importerPath));
2167
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
1584
2168
  const absolute = resolve2(importerDir, specifier);
1585
2169
  const indexedPaths = getIndexedPaths(db);
1586
2170
  for (const candidate of candidateImportPaths(absolute)) {
1587
2171
  const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
1588
- if (indexedPaths.has(relativeCandidate) || existsSync6(candidate)) {
2172
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
1589
2173
  return relativeCandidate;
1590
2174
  }
1591
2175
  }
@@ -1599,7 +2183,7 @@ function resolvePythonImportPath(db, importerPath, specifier) {
1599
2183
  if (!match) return null;
1600
2184
  const dots = match[1].length;
1601
2185
  const remainder = match[2].replace(/^\./, "");
1602
- let baseDir = dirname2(join6(db.config.projectRoot, importerPath));
2186
+ let baseDir = dirname2(join8(db.config.projectRoot, importerPath));
1603
2187
  for (let i = 1; i < dots; i++) {
1604
2188
  baseDir = dirname2(baseDir);
1605
2189
  }
@@ -1609,26 +2193,145 @@ function resolvePythonImportPath(db, importerPath, specifier) {
1609
2193
  }
1610
2194
  for (const candidate of pythonCandidateImportPaths(basePath)) {
1611
2195
  const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
1612
- if (indexedPaths.has(relativeCandidate) || existsSync6(candidate)) {
2196
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
2197
+ return relativeCandidate;
2198
+ }
2199
+ }
2200
+ return null;
2201
+ }
2202
+ function resolveRustImportPath(db, importerPath, specifier) {
2203
+ if (!specifier) return null;
2204
+ const normalizedSpecifier = specifier.replace(/\s+as\s+.+$/, "").trim();
2205
+ if (!normalizedSpecifier.startsWith("crate::") && !normalizedSpecifier.startsWith("self::") && !normalizedSpecifier.startsWith("super::")) {
2206
+ return null;
2207
+ }
2208
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2209
+ let basePath;
2210
+ if (normalizedSpecifier.startsWith("crate::")) {
2211
+ basePath = resolve2(db.config.projectRoot, "src", normalizedSpecifier.slice("crate::".length).replace(/::/g, "/"));
2212
+ } else if (normalizedSpecifier.startsWith("self::")) {
2213
+ basePath = resolve2(importerDir, normalizedSpecifier.slice("self::".length).replace(/::/g, "/"));
2214
+ } else {
2215
+ basePath = resolve2(dirname2(importerDir), normalizedSpecifier.slice("super::".length).replace(/::/g, "/"));
2216
+ }
2217
+ for (const candidate of rustCandidateImportPaths(basePath)) {
2218
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2219
+ if (getIndexedPaths(db).has(relativeCandidate) || existsSync8(candidate)) {
2220
+ return relativeCandidate;
2221
+ }
2222
+ }
2223
+ return null;
2224
+ }
2225
+ function resolveRubyImportPath(db, importerPath, specifier) {
2226
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2227
+ const absolute = resolve2(importerDir, specifier);
2228
+ for (const candidate of rubyCandidateImportPaths(absolute)) {
2229
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2230
+ if (getIndexedPaths(db).has(relativeCandidate) || existsSync8(candidate)) {
2231
+ return relativeCandidate;
2232
+ }
2233
+ }
2234
+ return null;
2235
+ }
2236
+ function resolveCLikeImportPath(db, importerPath, specifier) {
2237
+ const indexedPaths = getIndexedPaths(db);
2238
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2239
+ const candidates = [
2240
+ resolve2(importerDir, specifier),
2241
+ resolve2(db.config.projectRoot, specifier),
2242
+ resolve2(db.config.projectRoot, "include", specifier),
2243
+ resolve2(db.config.projectRoot, "src", specifier)
2244
+ ];
2245
+ for (const candidate of candidates) {
2246
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2247
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
2248
+ return relativeCandidate;
2249
+ }
2250
+ }
2251
+ return null;
2252
+ }
2253
+ function resolveQualifiedImportPath(db, specifier, extensions) {
2254
+ const indexedPaths = getIndexedPaths(db);
2255
+ const normalized = specifier.replace(/\\/g, ".").replace(/::/g, ".").replace(/^global::/, "");
2256
+ const pathified = normalized.replace(/\./g, "/");
2257
+ const basenameOnly = normalized.split(".").pop() ?? normalized;
2258
+ for (const ext of extensions) {
2259
+ const exactSuffix = `${pathified}${ext}`;
2260
+ const exact = [...indexedPaths].find((relativePath) => relativePath.endsWith(exactSuffix));
2261
+ if (exact) return exact;
2262
+ }
2263
+ for (const ext of extensions) {
2264
+ const basenameMatch = [...indexedPaths].find((relativePath) => basename(relativePath) === `${basenameOnly}${ext}`);
2265
+ if (basenameMatch) return basenameMatch;
2266
+ }
2267
+ const folderMatches = [...indexedPaths].filter((relativePath) => extensions.includes(extname2(relativePath).toLowerCase())).filter((relativePath) => relativePath.includes(`/${pathified}/`) || relativePath.includes(`/${basenameOnly}/`)).sort((left, right) => left.localeCompare(right));
2268
+ if (folderMatches.length === 1) {
2269
+ return folderMatches[0];
2270
+ }
2271
+ return null;
2272
+ }
2273
+ function resolveDartImportPath(db, importerPath, specifier) {
2274
+ const indexedPaths = getIndexedPaths(db);
2275
+ if (specifier.startsWith("package:")) {
2276
+ const withoutScheme = specifier.slice("package:".length);
2277
+ const slashIndex = withoutScheme.indexOf("/");
2278
+ if (slashIndex < 0) return null;
2279
+ const packageRelative = withoutScheme.slice(slashIndex + 1);
2280
+ const candidate = normalizePath(packageRelative.startsWith("lib/") ? packageRelative : `lib/${packageRelative}`);
2281
+ if (indexedPaths.has(candidate)) return candidate;
2282
+ return null;
2283
+ }
2284
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2285
+ const absolute = resolve2(importerDir, specifier);
2286
+ for (const candidate of dartCandidateImportPaths(absolute)) {
2287
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2288
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
1613
2289
  return relativeCandidate;
1614
2290
  }
1615
2291
  }
1616
2292
  return null;
1617
2293
  }
1618
2294
  function pythonCandidateImportPaths(basePath) {
1619
- const ext = extname(basePath);
2295
+ const ext = extname2(basePath);
1620
2296
  if (PYTHON_SOURCE_EXTENSIONS.includes(ext)) {
1621
2297
  return [basePath];
1622
2298
  }
1623
2299
  return [
1624
2300
  `${basePath}.py`,
1625
2301
  `${basePath}.pyi`,
1626
- join6(basePath, "__init__.py"),
1627
- join6(basePath, "__init__.pyi")
2302
+ join8(basePath, "__init__.py"),
2303
+ join8(basePath, "__init__.pyi")
2304
+ ];
2305
+ }
2306
+ function rustCandidateImportPaths(basePath) {
2307
+ const ext = extname2(basePath);
2308
+ if (RUST_SOURCE_EXTENSIONS.includes(ext)) {
2309
+ return [basePath];
2310
+ }
2311
+ return [
2312
+ `${basePath}.rs`,
2313
+ join8(basePath, "mod.rs")
2314
+ ];
2315
+ }
2316
+ function rubyCandidateImportPaths(basePath) {
2317
+ const ext = extname2(basePath);
2318
+ if (RUBY_SOURCE_EXTENSIONS.includes(ext)) {
2319
+ return [basePath];
2320
+ }
2321
+ return [
2322
+ `${basePath}.rb`,
2323
+ join8(basePath, "index.rb")
1628
2324
  ];
1629
2325
  }
2326
+ function dartCandidateImportPaths(basePath) {
2327
+ const ext = extname2(basePath);
2328
+ if (DART_SOURCE_EXTENSIONS.includes(ext)) {
2329
+ return [basePath];
2330
+ }
2331
+ return [`${basePath}.dart`, basePath];
2332
+ }
1630
2333
  function candidateImportPaths(absolute) {
1631
- const ext = extname(absolute);
2334
+ const ext = extname2(absolute);
1632
2335
  const candidates = /* @__PURE__ */ new Set();
1633
2336
  if (ext) {
1634
2337
  candidates.add(absolute);
@@ -1638,7 +2341,7 @@ function candidateImportPaths(absolute) {
1638
2341
  } else {
1639
2342
  for (const sourceExt of SOURCE_EXTENSIONS) {
1640
2343
  candidates.add(`${absolute}${sourceExt}`);
1641
- candidates.add(join6(absolute, `index${sourceExt}`));
2344
+ candidates.add(join8(absolute, `index${sourceExt}`));
1642
2345
  }
1643
2346
  }
1644
2347
  return [...candidates];
@@ -1671,10 +2374,44 @@ function normalizePath(path2) {
1671
2374
  return path2.replace(/\\/g, "/");
1672
2375
  }
1673
2376
  function isJavaScriptSourcePath(relativePath) {
1674
- return SOURCE_EXTENSIONS.includes(extname(relativePath).toLowerCase());
2377
+ return SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
1675
2378
  }
1676
2379
  function isPythonSourcePath(relativePath) {
1677
- return PYTHON_SOURCE_EXTENSIONS.includes(extname(relativePath).toLowerCase());
2380
+ return PYTHON_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2381
+ }
2382
+ function isJvmSourcePath(relativePath) {
2383
+ return JVM_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2384
+ }
2385
+ function isRustSourcePath(relativePath) {
2386
+ return RUST_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2387
+ }
2388
+ function isRubySourcePath(relativePath) {
2389
+ return RUBY_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2390
+ }
2391
+ function isCLikeSourcePath(relativePath) {
2392
+ return C_LIKE_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2393
+ }
2394
+ function isDotNetSourcePath(relativePath) {
2395
+ return DOTNET_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2396
+ }
2397
+ function isVisualBasicSourcePath(relativePath) {
2398
+ return extname2(relativePath).toLowerCase() === ".vb";
2399
+ }
2400
+ function isDartSourcePath(relativePath) {
2401
+ return DART_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2402
+ }
2403
+ function isPhpSourcePath(relativePath) {
2404
+ return PHP_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2405
+ }
2406
+ function extensionFamilyFor(relativePath) {
2407
+ if (isJvmSourcePath(relativePath)) return JVM_SOURCE_EXTENSIONS;
2408
+ if (isDotNetSourcePath(relativePath)) return DOTNET_SOURCE_EXTENSIONS;
2409
+ if (isPhpSourcePath(relativePath)) return PHP_SOURCE_EXTENSIONS;
2410
+ if (isDartSourcePath(relativePath)) return DART_SOURCE_EXTENSIONS;
2411
+ if (isCLikeSourcePath(relativePath)) return C_LIKE_SOURCE_EXTENSIONS;
2412
+ if (isRustSourcePath(relativePath)) return RUST_SOURCE_EXTENSIONS;
2413
+ if (isRubySourcePath(relativePath)) return RUBY_SOURCE_EXTENSIONS;
2414
+ return SOURCE_EXTENSIONS;
1678
2415
  }
1679
2416
  function getSourceText(db, relativePath) {
1680
2417
  const cache = getCachedMap(SOURCE_TEXT_CACHE, db);
@@ -1683,12 +2420,12 @@ function getSourceText(db, relativePath) {
1683
2420
  if (typeof cached === "string") {
1684
2421
  return cached;
1685
2422
  }
1686
- const fullPath = join6(db.config.projectRoot, normalized);
1687
- if (!existsSync6(fullPath)) {
2423
+ const fullPath = join8(db.config.projectRoot, normalized);
2424
+ if (!existsSync8(fullPath)) {
1688
2425
  cache.set(normalized, "");
1689
2426
  return "";
1690
2427
  }
1691
- const source = readFileSync4(fullPath, "utf-8");
2428
+ const source = readFileSync3(fullPath, "utf-8");
1692
2429
  cache.set(normalized, source);
1693
2430
  return source;
1694
2431
  }
@@ -1790,7 +2527,6 @@ function parseDescriptors(input) {
1790
2527
  const closingTick = input.indexOf("`", i + 1);
1791
2528
  if (closingTick === -1) {
1792
2529
  name = input.slice(i + 1);
1793
- i = input.length;
1794
2530
  descriptors.push({ name, suffix: "term" });
1795
2531
  break;
1796
2532
  }
@@ -1942,18 +2678,43 @@ function buildFileDepGraph(db, scope) {
1942
2678
  ${scopeFilter}`
1943
2679
  );
1944
2680
  const graph = /* @__PURE__ */ new Map();
1945
- for (const edge of edges) {
1946
- if (db.isIgnored(edge.from_file) || db.isIgnored(edge.to_file)) continue;
1947
- if (!graph.has(edge.from_file)) graph.set(edge.from_file, /* @__PURE__ */ new Set());
1948
- graph.get(edge.from_file).add(edge.to_file);
1949
- }
1950
- return graph;
1951
- }
2681
+ const indexedFiles = new Set(
2682
+ db.all(
2683
+ `SELECT relative_path
2684
+ FROM documents
2685
+ WHERE 1 = 1
2686
+ ${db.pathExclusionsFor("documents")}
2687
+ ORDER BY relative_path`
2688
+ ).map((row) => row.relative_path).filter((relativePath) => !db.isIgnored(relativePath))
2689
+ );
2690
+ const addEdge = (fromFile, toFile) => {
2691
+ if (fromFile === toFile) return;
2692
+ if (db.isIgnored(fromFile) || db.isIgnored(toFile)) return;
2693
+ if (!indexedFiles.has(toFile)) return;
2694
+ if (!graph.has(fromFile)) graph.set(fromFile, /* @__PURE__ */ new Set());
2695
+ graph.get(fromFile).add(toFile);
2696
+ };
2697
+ for (const edge of edges) {
2698
+ addEdge(edge.from_file, edge.to_file);
2699
+ }
2700
+ for (const relativePath of indexedFiles) {
2701
+ if (scope && !relativePath.includes(scope)) continue;
2702
+ for (const entry of getSourceImports(db, relativePath)) {
2703
+ if (!entry.sourcePath) continue;
2704
+ addEdge(relativePath, entry.sourcePath);
2705
+ }
2706
+ }
2707
+ return graph;
2708
+ }
1952
2709
  function findFirstSymbolMatch(db, symbolPattern) {
2710
+ const exact = findExactSymbolMatch(db, symbolPattern.trim());
2711
+ if (exact) {
2712
+ return exact;
2713
+ }
1953
2714
  const fileLineMatch = symbolPattern.match(/^(.+):(\d+)-(\d+)$/);
1954
2715
  if (fileLineMatch) {
1955
2716
  const [, filePath, startStr, endStr] = fileLineMatch;
1956
- const row = db.get(
2717
+ let row = db.get(
1957
2718
  `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
1958
2719
  FROM global_symbols gs
1959
2720
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
@@ -1962,25 +2723,41 @@ function findFirstSymbolMatch(db, symbolPattern) {
1962
2723
  AND der.start_line <= ? AND der.end_line >= ?
1963
2724
  ${db.pathExclusionsFor("d")}
1964
2725
  ORDER BY (der.end_line - der.start_line) ASC
1965
- LIMIT 1`,
2726
+ LIMIT 1`,
1966
2727
  `%${filePath}%`,
1967
2728
  parseInt(startStr, 10),
1968
2729
  parseInt(endStr, 10)
1969
2730
  );
2731
+ if (!row) {
2732
+ row = db.get(
2733
+ `SELECT gs.id, gs.symbol, c.document_id, MIN(c.start_line) AS start_line, MAX(c.end_line) AS end_line, d.relative_path
2734
+ FROM global_symbols gs
2735
+ JOIN mentions m ON m.symbol_id = gs.id
2736
+ JOIN chunks c ON m.chunk_id = c.id
2737
+ JOIN documents d ON c.document_id = d.id
2738
+ WHERE m.role = 1
2739
+ AND d.relative_path LIKE ?
2740
+ AND c.start_line <= ? AND c.end_line >= ?
2741
+ ${db.pathExclusionsFor("d")}
2742
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path
2743
+ ORDER BY (MAX(c.end_line) - MIN(c.start_line)) ASC
2744
+ LIMIT 1`,
2745
+ `%${filePath}%`,
2746
+ parseInt(startStr, 10),
2747
+ parseInt(endStr, 10)
2748
+ );
2749
+ }
1970
2750
  if (row && !db.isIgnored(row.relative_path)) {
1971
- return {
1972
- symbolId: row.id,
1973
- symbol: row.symbol,
1974
- documentId: row.document_id,
1975
- startLine: row.start_line,
1976
- endLine: row.end_line,
1977
- relativePath: row.relative_path
1978
- };
2751
+ return hydrateSymbolMatch(db, row);
1979
2752
  }
1980
2753
  }
1981
2754
  const cleaned = normalizeLookupPattern(symbolPattern);
1982
2755
  const tokens = lookupTokens(symbolPattern);
1983
2756
  const candidates = getSymbolLookupCandidates(db, tokens);
2757
+ const direct = findDirectSymbolCandidate(candidates, symbolPattern, cleaned);
2758
+ if (direct && !db.isIgnored(direct.relative_path)) {
2759
+ return hydrateSymbolMatch(db, direct);
2760
+ }
1984
2761
  let best = null;
1985
2762
  for (const row of candidates) {
1986
2763
  if (db.isIgnored(row.relative_path)) continue;
@@ -1991,14 +2768,7 @@ function findFirstSymbolMatch(db, symbolPattern) {
1991
2768
  }
1992
2769
  }
1993
2770
  if (best) {
1994
- return {
1995
- symbolId: best.row.id,
1996
- symbol: best.row.symbol,
1997
- documentId: best.row.document_id,
1998
- startLine: best.row.start_line,
1999
- endLine: best.row.end_line,
2000
- relativePath: best.row.relative_path
2001
- };
2771
+ return hydrateSymbolMatch(db, best.row);
2002
2772
  }
2003
2773
  return null;
2004
2774
  }
@@ -2017,14 +2787,7 @@ function findExactSymbolMatch(db, symbol) {
2017
2787
  if (!row || db.isIgnored(row.relative_path)) {
2018
2788
  return null;
2019
2789
  }
2020
- return {
2021
- symbolId: row.id,
2022
- symbol: row.symbol,
2023
- documentId: row.document_id,
2024
- startLine: row.start_line,
2025
- endLine: row.end_line,
2026
- relativePath: row.relative_path
2027
- };
2790
+ return hydrateSymbolMatch(db, row);
2028
2791
  }
2029
2792
  function resolveIndexedFile(db, filePattern) {
2030
2793
  return resolveDocumentCandidates(db, filePattern, { allowMultiple: false })[0]?.relativePath ?? null;
@@ -2048,28 +2811,63 @@ function getSymbolLookupCandidates(db, tokens) {
2048
2811
  const like = `%${token}%`;
2049
2812
  return [like, like, like];
2050
2813
  });
2051
- return db.all(
2814
+ const primary = db.all(
2052
2815
  `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path, gs.display_name
2053
2816
  FROM global_symbols gs
2054
2817
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2055
2818
  JOIN documents d ON der.document_id = d.id
2056
2819
  WHERE ${tokenClauses.join("\n AND ")}
2057
2820
  ${db.pathExclusionsFor("d")}
2821
+ LIMIT 200`,
2822
+ ...params
2823
+ );
2824
+ if (primary.length > 0) {
2825
+ return primary;
2826
+ }
2827
+ return db.all(
2828
+ `SELECT
2829
+ gs.id,
2830
+ gs.symbol,
2831
+ c.document_id,
2832
+ MIN(c.start_line) AS start_line,
2833
+ MAX(c.end_line) AS end_line,
2834
+ d.relative_path,
2835
+ gs.display_name
2836
+ FROM global_symbols gs
2837
+ JOIN mentions m ON m.symbol_id = gs.id
2838
+ JOIN chunks c ON m.chunk_id = c.id
2839
+ JOIN documents d ON c.document_id = d.id
2840
+ WHERE m.role = 1
2841
+ AND ${tokenClauses.join("\n AND ")}
2842
+ ${db.pathExclusionsFor("d")}
2843
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path, gs.display_name
2058
2844
  LIMIT 200`,
2059
2845
  ...params
2060
2846
  );
2061
2847
  }
2062
2848
  function scoreSymbolCandidate(row, originalPattern, cleanedPattern, tokens) {
2849
+ const originalCase = originalPattern.trim();
2850
+ const cleanedCase = cleanedPattern;
2851
+ const noParensCase = cleanedCase.replace(/\(\)$/, "");
2063
2852
  const original = originalPattern.toLowerCase();
2064
2853
  const cleaned = cleanedPattern.toLowerCase();
2065
2854
  const noParens = cleaned.replace(/\(\)$/, "");
2855
+ const rawCase = row.symbol;
2856
+ const shortCase = shortenSymbol(row.symbol);
2857
+ const leafCase = leafName(row.symbol);
2858
+ const displayCase = row.display_name ?? "";
2066
2859
  const raw = row.symbol.toLowerCase();
2067
- const short = shortenSymbol(row.symbol).toLowerCase();
2068
- const leaf = leafName(row.symbol).toLowerCase();
2069
- const display = (row.display_name ?? "").toLowerCase();
2860
+ const short = shortCase.toLowerCase();
2861
+ const leaf = leafCase.toLowerCase();
2862
+ const display = displayCase.toLowerCase();
2070
2863
  const path2 = row.relative_path.toLowerCase();
2071
2864
  const looksPathLike = /[/:.]/.test(cleanedPattern);
2072
2865
  let score = 0;
2866
+ if (rawCase === originalCase || rawCase === cleanedCase) score += 1150;
2867
+ if (shortCase === originalCase || shortCase === cleanedCase) score += 1100;
2868
+ if (displayCase === noParensCase) score += 980;
2869
+ if (leafCase === noParensCase) score += 960;
2870
+ if (`${leafCase}()` === originalCase || `${leafCase}()` === cleanedCase) score += 955;
2073
2871
  if (raw === original || raw === cleaned) score += 1e3;
2074
2872
  if (short === original || short === cleaned) score += 950;
2075
2873
  if (path2 === original || path2 === cleaned) score += 925;
@@ -2098,48 +2896,42 @@ function scoreSymbolCandidate(row, originalPattern, cleanedPattern, tokens) {
2098
2896
  return score;
2099
2897
  }
2100
2898
  function getCalleeRowsForSymbol(db, symbol, opts = {}) {
2101
- const rows = db.all(
2102
- `SELECT DISTINCT
2103
- callee_gs.symbol AS symbol,
2104
- callee_d.relative_path AS file,
2105
- c.id AS chunk_id
2106
- FROM mentions m
2107
- JOIN chunks c ON m.chunk_id = c.id
2108
- JOIN global_symbols callee_gs ON m.symbol_id = callee_gs.id
2109
- JOIN defn_enclosing_ranges callee_der ON callee_gs.id = callee_der.symbol_id
2110
- JOIN documents callee_d ON callee_der.document_id = callee_d.id
2111
- WHERE c.document_id = ?
2112
- AND c.start_line >= ?
2113
- AND c.end_line <= ?
2114
- AND m.role != 1
2115
- AND callee_gs.id != ?
2116
- ${db.symbolNoiseFor("callee_gs")}
2117
- ${db.pathExclusionsFor("callee_d")}
2118
- ORDER BY callee_d.relative_path
2119
- ${opts.limit ? "LIMIT ?" : ""}`,
2899
+ const mentions = db.all(
2900
+ `SELECT DISTINCT m.symbol_id, c.id AS chunk_id
2901
+ FROM mentions m
2902
+ JOIN chunks c ON m.chunk_id = c.id
2903
+ WHERE c.document_id = ?
2904
+ AND c.start_line >= ?
2905
+ AND c.end_line <= ?
2906
+ AND m.role != 1
2907
+ AND m.symbol_id != ?
2908
+ ${opts.limit ? "LIMIT ?" : ""}`,
2120
2909
  ...calleeQueryParams(symbol, opts.limit)
2121
2910
  );
2122
- const primary = rows.filter((row) => !db.isIgnored(row.file)).map((row) => ({
2123
- symbol: row.symbol,
2124
- file: row.file,
2125
- chunkId: row.chunk_id
2126
- }));
2127
- const sourceFallback = getSourceBackedCalleeRows(db, symbol, opts.limit);
2128
- if (sourceFallback.length === 0) {
2129
- return primary;
2911
+ const primary = [];
2912
+ const directSeen = /* @__PURE__ */ new Set();
2913
+ for (const mention of mentions) {
2914
+ const callee = getFullSymbolMatch(db, {
2915
+ symbolId: mention.symbol_id,
2916
+ documentId: symbol.documentId,
2917
+ startLine: symbol.startLine,
2918
+ endLine: symbol.endLine
2919
+ });
2920
+ if (!callee || db.isIgnored(callee.relativePath)) continue;
2921
+ const key = `${callee.symbol}|${callee.relativePath}|${mention.chunk_id}`;
2922
+ if (directSeen.has(key)) continue;
2923
+ directSeen.add(key);
2924
+ primary.push({
2925
+ symbol: callee.symbol,
2926
+ file: callee.relativePath,
2927
+ chunkId: mention.chunk_id
2928
+ });
2130
2929
  }
2131
- if (primary.length === 0) {
2930
+ const sourceFallback = getSourceBackedCalleeRows(db, symbol, opts.limit);
2931
+ if (sourceFallback.length > 0) {
2132
2932
  return applyLimit(sourceFallback, opts.limit);
2133
2933
  }
2134
- const seen = new Set(primary.map((row) => `${row.symbol}|${row.file}`));
2135
- const merged = [...primary];
2136
- for (const row of sourceFallback) {
2137
- const key = `${row.symbol}|${row.file}`;
2138
- if (seen.has(key)) continue;
2139
- seen.add(key);
2140
- merged.push(row);
2141
- }
2142
- return applyLimit(merged, opts.limit);
2934
+ return primary;
2143
2935
  }
2144
2936
  function getCallerRowsForSymbol(db, symbol, opts = {}) {
2145
2937
  const match = getFullSymbolMatch(db, symbol);
@@ -2147,35 +2939,38 @@ function getCallerRowsForSymbol(db, symbol, opts = {}) {
2147
2939
  return [];
2148
2940
  }
2149
2941
  const primary = db.all(
2150
- `SELECT DISTINCT caller_gs.symbol AS caller_symbol, caller_d.relative_path AS caller_file
2151
- FROM mentions m
2152
- JOIN chunks c ON m.chunk_id = c.id
2153
- JOIN documents ref_d ON c.document_id = ref_d.id
2154
- JOIN defn_enclosing_ranges caller_der
2155
- ON caller_der.document_id = ref_d.id
2156
- AND c.start_line >= caller_der.start_line
2157
- AND c.end_line <= caller_der.end_line
2158
- JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id
2159
- JOIN documents caller_d ON caller_der.document_id = caller_d.id
2160
- WHERE m.symbol_id = ?
2161
- AND m.role != 1
2162
- AND caller_gs.id != ?
2163
- ${db.symbolNoiseFor("caller_gs")}
2164
- ${db.pathExclusionsFor("caller_d")}
2165
- ORDER BY caller_d.relative_path
2166
- ${opts.limit ? "LIMIT ?" : ""}`,
2167
- ...callerQueryParams(match, opts.limit)
2168
- ).filter((row) => !db.isIgnored(row.caller_file)).map((row) => ({
2169
- symbol: row.caller_symbol,
2170
- file: row.caller_file
2171
- }));
2172
- const sourceFallback = getPythonSourceCallerRows(db, match, opts.limit);
2942
+ `SELECT DISTINCT ref_d.relative_path AS caller_file, c.start_line AS line
2943
+ FROM mentions m
2944
+ JOIN chunks c ON m.chunk_id = c.id
2945
+ JOIN documents ref_d ON c.document_id = ref_d.id
2946
+ WHERE m.symbol_id = ?
2947
+ AND m.role != 1
2948
+ ${db.pathExclusionsFor("ref_d")}
2949
+ ORDER BY ref_d.relative_path
2950
+ ${opts.limit ? "LIMIT ?" : ""}`,
2951
+ ...referenceQueryParams(match, opts.limit)
2952
+ ).filter((row) => !db.isIgnored(row.caller_file)).flatMap((row) => {
2953
+ const enclosing = findEnclosingDefinition(getDefinitionsForFile(db, row.caller_file), row.line);
2954
+ if (!enclosing || enclosing.symbolId === match.symbolId) {
2955
+ return [];
2956
+ }
2957
+ return [{
2958
+ symbol: enclosing.symbol,
2959
+ file: row.caller_file
2960
+ }];
2961
+ });
2962
+ const sourceFallback = dedupeCallerRows([
2963
+ ...getPythonSourceCallerRows(db, match, opts.limit),
2964
+ ...getGenericSourceCallerRows(db, match, opts.limit)
2965
+ ]);
2173
2966
  if (sourceFallback.length === 0) {
2174
- return primary;
2967
+ return dedupeCallerRows(primary);
2175
2968
  }
2176
- const merged = sourceFallback.length > 0 ? [...sourceFallback] : [];
2969
+ const merged = [...sourceFallback];
2970
+ const fallbackFiles = new Set(sourceFallback.map((row) => row.file));
2177
2971
  const seen = new Set(merged.map((row) => `${row.symbol}|${row.file}`));
2178
2972
  for (const row of primary) {
2973
+ if (fallbackFiles.has(row.file)) continue;
2179
2974
  const key = `${row.symbol}|${row.file}`;
2180
2975
  if (seen.has(key)) continue;
2181
2976
  if (isFunctionLikeSymbol(row.symbol) || merged.length === 0) {
@@ -2185,6 +2980,26 @@ function getCallerRowsForSymbol(db, symbol, opts = {}) {
2185
2980
  }
2186
2981
  return applyLimit(merged, opts.limit);
2187
2982
  }
2983
+ function getGenericSourceCallerRows(db, symbol, limit) {
2984
+ return applyLimit(
2985
+ getSourceReferenceSites(db, symbol).filter((site) => site.enclosingSymbol && site.enclosingSymbol !== symbol.symbol).map((site) => ({
2986
+ symbol: site.enclosingSymbol,
2987
+ file: site.file
2988
+ })),
2989
+ limit
2990
+ );
2991
+ }
2992
+ function dedupeCallerRows(rows) {
2993
+ const seen = /* @__PURE__ */ new Set();
2994
+ const unique = [];
2995
+ for (const row of rows) {
2996
+ const key = `${row.symbol}|${row.file}`;
2997
+ if (seen.has(key)) continue;
2998
+ seen.add(key);
2999
+ unique.push(row);
3000
+ }
3001
+ return unique;
3002
+ }
2188
3003
  function getSourceReferenceSites(db, symbol) {
2189
3004
  const match = getFullSymbolMatch(db, symbol);
2190
3005
  if (!match) {
@@ -2206,21 +3021,9 @@ function getSourceReferenceSites(db, symbol) {
2206
3021
  for (const document of documents) {
2207
3022
  if (db.isIgnored(document.relative_path)) continue;
2208
3023
  const lines = findIdentifierLines(db, document.relative_path, identifier, document.relative_path === match.relativePath ? { excludeStartLine: match.startLine, excludeEndLine: match.endLine } : {});
3024
+ const definitions = getDefinitionsForFile(db, document.relative_path);
2209
3025
  for (const line of lines) {
2210
- const enclosing = db.get(
2211
- `SELECT gs.symbol
2212
- FROM defn_enclosing_ranges der
2213
- JOIN global_symbols gs ON der.symbol_id = gs.id
2214
- JOIN documents d ON der.document_id = d.id
2215
- WHERE d.relative_path = ?
2216
- AND der.start_line <= ?
2217
- AND der.end_line >= ?
2218
- ORDER BY (der.end_line - der.start_line) ASC
2219
- LIMIT 1`,
2220
- document.relative_path,
2221
- line,
2222
- line
2223
- );
3026
+ const enclosing = findEnclosingDefinition(definitions, line);
2224
3027
  const key = `${document.relative_path}|${line}|${enclosing?.symbol ?? ""}`;
2225
3028
  if (seen.has(key)) continue;
2226
3029
  seen.add(key);
@@ -2245,13 +3048,23 @@ function calleeQueryParams(symbol, limit) {
2245
3048
  }
2246
3049
  return params;
2247
3050
  }
2248
- function callerQueryParams(symbol, limit) {
2249
- const params = [symbol.symbolId, symbol.symbolId];
3051
+ function referenceQueryParams(symbol, limit) {
3052
+ const params = [symbol.symbolId];
2250
3053
  if (typeof limit === "number") {
2251
3054
  params.push(limit);
2252
3055
  }
2253
3056
  return params;
2254
3057
  }
3058
+ function findEnclosingDefinition(definitions, line) {
3059
+ let best = null;
3060
+ for (const definition of definitions) {
3061
+ if (definition.startLine > line || definition.endLine < line) continue;
3062
+ if (!best || definition.endLine - definition.startLine < best.endLine - best.startLine) {
3063
+ best = definition;
3064
+ }
3065
+ }
3066
+ return best;
3067
+ }
2255
3068
  function getPythonSourceCalleeRows(db, symbol, limit) {
2256
3069
  const match = getFullSymbolMatch(db, symbol);
2257
3070
  if (!match || !isPythonDocument(db, match.relativePath)) {
@@ -2342,6 +3155,94 @@ function getJavaScriptSourceCalleeRows(db, symbol, limit) {
2342
3155
  }
2343
3156
  return applyLimit(rows, limit);
2344
3157
  }
3158
+ function getJavaSourceCalleeRows(db, symbol, limit) {
3159
+ const match = getFullSymbolMatch(db, symbol);
3160
+ if (!match || !isJavaDocument(db, match.relativePath)) {
3161
+ return [];
3162
+ }
3163
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3164
+ const bindings = parseJavaFieldBindings(getSourceText(db, match.relativePath));
3165
+ return resolveSimpleSourceCallees(db, match, calls, bindings, limit);
3166
+ }
3167
+ function getKotlinSourceCalleeRows(db, symbol, limit) {
3168
+ const match = getFullSymbolMatch(db, symbol);
3169
+ if (!match || !isKotlinDocument(db, match.relativePath)) {
3170
+ return [];
3171
+ }
3172
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3173
+ const bindings = parseKotlinFieldBindings(getSourceText(db, match.relativePath));
3174
+ return resolveSimpleSourceCallees(db, match, calls, bindings, limit);
3175
+ }
3176
+ function getScalaSourceCalleeRows(db, symbol, limit) {
3177
+ const match = getFullSymbolMatch(db, symbol);
3178
+ if (!match || !isScalaDocument(db, match.relativePath)) {
3179
+ return [];
3180
+ }
3181
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3182
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3183
+ }
3184
+ function getCSharpSourceCalleeRows(db, symbol, limit) {
3185
+ const match = getFullSymbolMatch(db, symbol);
3186
+ if (!match || !isCSharpDocument(db, match.relativePath)) {
3187
+ return [];
3188
+ }
3189
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3190
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3191
+ }
3192
+ function getVisualBasicSourceCalleeRows(db, symbol, limit) {
3193
+ const match = getFullSymbolMatch(db, symbol);
3194
+ if (!match || !isVisualBasicDocument(db, match.relativePath)) {
3195
+ return [];
3196
+ }
3197
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3198
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3199
+ }
3200
+ function getCppSourceCalleeRows(db, symbol, limit) {
3201
+ const match = getFullSymbolMatch(db, symbol);
3202
+ if (!match || !isCppDocument(db, match.relativePath)) {
3203
+ return [];
3204
+ }
3205
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3206
+ const bindings = parseCppReceiverBindings(getSourceText(db, match.relativePath));
3207
+ return resolveSimpleSourceCallees(db, match, calls, bindings, limit);
3208
+ }
3209
+ function getRustSourceCalleeRows(db, symbol, limit) {
3210
+ const match = getFullSymbolMatch(db, symbol);
3211
+ if (!match || !isRustDocument(db, match.relativePath)) {
3212
+ return [];
3213
+ }
3214
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3215
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3216
+ }
3217
+ function getRubySourceCalleeRows(db, symbol, limit) {
3218
+ const match = getFullSymbolMatch(db, symbol);
3219
+ if (!match || !isRubyDocument(db, match.relativePath)) {
3220
+ return [];
3221
+ }
3222
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine, { allowInstanceVariables: true });
3223
+ const rubyCalls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine, {
3224
+ allowInstanceVariables: true,
3225
+ allowBareMemberCalls: true
3226
+ });
3227
+ const bindings = parseRubyReceiverBindings(db, getSourceText(db, match.relativePath));
3228
+ return resolveSimpleSourceCallees(db, match, rubyCalls.length > 0 ? rubyCalls : calls, bindings, limit);
3229
+ }
3230
+ function getDartSourceCalleeRows(db, symbol, limit) {
3231
+ const match = getFullSymbolMatch(db, symbol);
3232
+ if (!match || !isDartDocument(db, match.relativePath)) {
3233
+ return [];
3234
+ }
3235
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3236
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3237
+ }
3238
+ function getPhpSourceCalleeRows(db, symbol, limit) {
3239
+ const match = getFullSymbolMatch(db, symbol);
3240
+ if (!match || !isPhpDocument(db, match.relativePath)) {
3241
+ return [];
3242
+ }
3243
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3244
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3245
+ }
2345
3246
  function getSourceBackedCalleeRows(db, symbol, limit) {
2346
3247
  const match = getFullSymbolMatch(db, symbol);
2347
3248
  if (!match) {
@@ -2353,6 +3254,36 @@ function getSourceBackedCalleeRows(db, symbol, limit) {
2353
3254
  if (isJavaScriptDocument(db, match.relativePath)) {
2354
3255
  return getJavaScriptSourceCalleeRows(db, match, limit);
2355
3256
  }
3257
+ if (isJavaDocument(db, match.relativePath)) {
3258
+ return getJavaSourceCalleeRows(db, match, limit);
3259
+ }
3260
+ if (isScalaDocument(db, match.relativePath)) {
3261
+ return getScalaSourceCalleeRows(db, match, limit);
3262
+ }
3263
+ if (isKotlinDocument(db, match.relativePath)) {
3264
+ return getKotlinSourceCalleeRows(db, match, limit);
3265
+ }
3266
+ if (isCSharpDocument(db, match.relativePath)) {
3267
+ return getCSharpSourceCalleeRows(db, match, limit);
3268
+ }
3269
+ if (isVisualBasicDocument(db, match.relativePath)) {
3270
+ return getVisualBasicSourceCalleeRows(db, match, limit);
3271
+ }
3272
+ if (isCppDocument(db, match.relativePath)) {
3273
+ return getCppSourceCalleeRows(db, match, limit);
3274
+ }
3275
+ if (isRustDocument(db, match.relativePath)) {
3276
+ return getRustSourceCalleeRows(db, match, limit);
3277
+ }
3278
+ if (isRubyDocument(db, match.relativePath)) {
3279
+ return getRubySourceCalleeRows(db, match, limit);
3280
+ }
3281
+ if (isDartDocument(db, match.relativePath)) {
3282
+ return getDartSourceCalleeRows(db, match, limit);
3283
+ }
3284
+ if (isPhpDocument(db, match.relativePath)) {
3285
+ return getPhpSourceCalleeRows(db, match, limit);
3286
+ }
2356
3287
  return [];
2357
3288
  }
2358
3289
  function getPythonSourceCallerRows(db, target, limit) {
@@ -2378,27 +3309,49 @@ function getPythonSourceCallerRows(db, target, limit) {
2378
3309
  }
2379
3310
  return rows;
2380
3311
  }
2381
- function getFullSymbolMatch(db, symbol) {
2382
- if ("symbol" in symbol && "relativePath" in symbol) {
2383
- return symbol;
2384
- }
2385
- const row = db.get(
2386
- `SELECT gs.symbol, d.relative_path
3312
+ function getDefinitionRowsForSymbolId(db, symbolId) {
3313
+ const primary = db.all(
3314
+ `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path, gs.display_name
2387
3315
  FROM global_symbols gs
2388
3316
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2389
3317
  JOIN documents d ON der.document_id = d.id
2390
3318
  WHERE gs.id = ?
2391
- LIMIT 1`,
2392
- symbol.symbolId
3319
+ ORDER BY der.start_line, der.end_line`,
3320
+ symbolId
3321
+ );
3322
+ if (primary.length > 0) {
3323
+ return primary;
3324
+ }
3325
+ return db.all(
3326
+ `SELECT
3327
+ gs.id,
3328
+ gs.symbol,
3329
+ c.document_id,
3330
+ MIN(c.start_line) AS start_line,
3331
+ MAX(c.end_line) AS end_line,
3332
+ d.relative_path,
3333
+ gs.display_name
3334
+ FROM global_symbols gs
3335
+ JOIN mentions m ON m.symbol_id = gs.id
3336
+ JOIN chunks c ON m.chunk_id = c.id
3337
+ JOIN documents d ON c.document_id = d.id
3338
+ WHERE gs.id = ?
3339
+ AND m.role = 1
3340
+ ${db.pathExclusionsFor("d")}
3341
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path, gs.display_name
3342
+ ORDER BY start_line, end_line`,
3343
+ symbolId
2393
3344
  );
3345
+ }
3346
+ function getFullSymbolMatch(db, symbol) {
3347
+ if ("symbol" in symbol && "relativePath" in symbol) {
3348
+ return symbol;
3349
+ }
3350
+ const row = getDefinitionRowsForSymbolId(db, symbol.symbolId)[0];
2394
3351
  if (!row) {
2395
3352
  return null;
2396
3353
  }
2397
- return {
2398
- ...symbol,
2399
- symbol: row.symbol,
2400
- relativePath: row.relative_path
2401
- };
3354
+ return hydrateSymbolMatch(db, row);
2402
3355
  }
2403
3356
  function getDefinitionsForFile(db, relativePath) {
2404
3357
  let cache = FILE_DEFINITION_CACHE.get(db);
@@ -2410,8 +3363,18 @@ function getDefinitionsForFile(db, relativePath) {
2410
3363
  if (cached) {
2411
3364
  return cached;
2412
3365
  }
2413
- const definitions = db.all(
2414
- `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
3366
+ const primary = db.all(
3367
+ `SELECT
3368
+ gs.id,
3369
+ gs.symbol,
3370
+ der.document_id,
3371
+ der.start_line,
3372
+ der.end_line,
3373
+ d.relative_path,
3374
+ gs.display_name,
3375
+ gs.kind,
3376
+ gs.documentation,
3377
+ gs.enclosing_symbol
2415
3378
  FROM global_symbols gs
2416
3379
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2417
3380
  JOIN documents d ON der.document_id = d.id
@@ -2419,54 +3382,272 @@ function getDefinitionsForFile(db, relativePath) {
2419
3382
  ${db.symbolNoiseFor("gs")}
2420
3383
  ORDER BY der.start_line, der.end_line`,
2421
3384
  relativePath
2422
- ).map((row) => ({
2423
- symbolId: row.id,
2424
- symbol: row.symbol,
2425
- documentId: row.document_id,
2426
- startLine: row.start_line,
2427
- endLine: row.end_line,
2428
- relativePath: row.relative_path,
2429
- leaf: leafName(row.symbol),
2430
- parentTypeName: parentTypeName(row.symbol),
2431
- isFunctionLike: isFunctionLikeSymbol(row.symbol),
2432
- isTypeLike: leafSuffix(row.symbol) === "type"
2433
- }));
2434
- cache.set(relativePath, definitions);
2435
- return definitions;
2436
- }
2437
- function getAllFunctionLikeDefinitions(db) {
2438
- const rows = db.all(
2439
- `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
3385
+ );
3386
+ const fallback = primary.length > 0 ? [] : db.all(
3387
+ `SELECT
3388
+ gs.id,
3389
+ gs.symbol,
3390
+ c.document_id,
3391
+ MIN(c.start_line) AS start_line,
3392
+ MAX(c.end_line) AS end_line,
3393
+ d.relative_path,
3394
+ gs.display_name,
3395
+ gs.kind,
3396
+ gs.documentation,
3397
+ gs.enclosing_symbol
2440
3398
  FROM global_symbols gs
2441
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2442
- JOIN documents d ON der.document_id = d.id
2443
- WHERE 1 = 1
2444
- ${db.pathExclusionsFor("d")}
3399
+ JOIN mentions m ON m.symbol_id = gs.id
3400
+ JOIN chunks c ON m.chunk_id = c.id
3401
+ JOIN documents d ON c.document_id = d.id
3402
+ WHERE d.relative_path = ?
3403
+ AND m.role = 1
2445
3404
  ${db.symbolNoiseFor("gs")}
2446
- ORDER BY d.relative_path, der.start_line`
3405
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path
3406
+ ORDER BY start_line, end_line`,
3407
+ relativePath
3408
+ );
3409
+ const definitions = correctDefinitionRangesFromSource(
3410
+ db,
3411
+ relativePath,
3412
+ (primary.length > 0 ? primary : fallback).map((row) => ({
3413
+ symbolId: row.id,
3414
+ symbol: row.symbol,
3415
+ documentId: row.document_id,
3416
+ startLine: row.start_line,
3417
+ endLine: row.end_line,
3418
+ relativePath: row.relative_path,
3419
+ leaf: leafName(row.symbol),
3420
+ parentTypeName: parentTypeName(row.symbol),
3421
+ isFunctionLike: isFunctionLikeSymbol(row.symbol),
3422
+ isTypeLike: leafSuffix(row.symbol) === "type",
3423
+ kind: row.kind ?? null,
3424
+ documentation: row.documentation ?? null,
3425
+ enclosingSymbol: row.enclosing_symbol ?? null
3426
+ }))
2447
3427
  );
2448
- return rows.filter((row) => !db.isIgnored(row.relative_path)).filter((row) => isFunctionLikeSymbol(row.symbol)).map((row) => ({
3428
+ cache.set(relativePath, definitions);
3429
+ return definitions;
3430
+ }
3431
+ function hydrateSymbolMatch(db, row) {
3432
+ const corrected = getDefinitionsForFile(db, row.relative_path).find((definition) => definition.symbolId === row.id);
3433
+ if (corrected) {
3434
+ return {
3435
+ symbolId: corrected.symbolId,
3436
+ symbol: corrected.symbol,
3437
+ documentId: corrected.documentId,
3438
+ startLine: corrected.startLine,
3439
+ endLine: corrected.endLine,
3440
+ relativePath: corrected.relativePath
3441
+ };
3442
+ }
3443
+ return {
2449
3444
  symbolId: row.id,
2450
3445
  symbol: row.symbol,
2451
3446
  documentId: row.document_id,
2452
3447
  startLine: row.start_line,
2453
3448
  endLine: row.end_line,
2454
- relativePath: row.relative_path,
2455
- leaf: leafName(row.symbol),
2456
- parentTypeName: parentTypeName(row.symbol),
2457
- isFunctionLike: true,
2458
- isTypeLike: false
2459
- }));
3449
+ relativePath: row.relative_path
3450
+ };
2460
3451
  }
2461
- function resolvePythonCallTarget(db, current, currentFileDefinitions, imports2, constructorBindings, receiverName, calleeName) {
2462
- if (receiverName === "self" || receiverName === "cls") {
2463
- return findDefinitionByName(currentFileDefinitions, calleeName, current.parentTypeName, ["function"]);
3452
+ function findDirectSymbolCandidate(candidates, symbolPattern, cleanedPattern) {
3453
+ const trimmed = symbolPattern.trim();
3454
+ const directMatches = candidates.filter((row) => {
3455
+ const short = shortenSymbol(row.symbol);
3456
+ const display = (row.display_name ?? "").trim();
3457
+ return row.symbol === trimmed || short === trimmed || short === cleanedPattern || display === trimmed || display === cleanedPattern || `${display}()` === trimmed || row.relative_path === trimmed;
3458
+ });
3459
+ if (directMatches.length === 0) {
3460
+ return null;
2464
3461
  }
2465
- if (receiverName) {
2466
- const inferredType = constructorBindings.get(receiverName);
2467
- if (inferredType) {
2468
- const boundMethod = findDefinitionByName(currentFileDefinitions, calleeName, inferredType, ["function"]);
2469
- if (boundMethod) {
3462
+ directMatches.sort(
3463
+ (left, right) => left.end_line - left.start_line - (right.end_line - right.start_line) || left.relative_path.localeCompare(right.relative_path) || left.symbol.localeCompare(right.symbol)
3464
+ );
3465
+ return directMatches[0] ?? null;
3466
+ }
3467
+ function correctDefinitionRangesFromSource(db, relativePath, definitions) {
3468
+ const source = getSourceText(db, relativePath);
3469
+ if (!source) {
3470
+ return definitions;
3471
+ }
3472
+ const lines = source.split(/\r?\n/);
3473
+ const correctedStarts = /* @__PURE__ */ new Map();
3474
+ for (const definition of definitions) {
3475
+ correctedStarts.set(
3476
+ definition.symbolId,
3477
+ resolveCallableDefinitionStartLine(lines, definition)
3478
+ );
3479
+ }
3480
+ const correctedRanges = /* @__PURE__ */ new Map();
3481
+ const callableDefinitions = definitions.filter((definition) => isCallableDefinition(definition.symbol)).map((definition) => ({
3482
+ definition,
3483
+ startLine: correctedStarts.get(definition.symbolId) ?? definition.startLine
3484
+ })).sort(
3485
+ (left, right) => left.startLine - right.startLine || left.definition.startLine - right.definition.startLine || left.definition.symbol.localeCompare(right.definition.symbol)
3486
+ );
3487
+ for (let index = 0; index < callableDefinitions.length; index += 1) {
3488
+ const current = callableDefinitions[index];
3489
+ const next = callableDefinitions[index + 1];
3490
+ const maxEndLine = next ? Math.max(current.startLine, next.startLine - 1) : lines.length - 1;
3491
+ correctedRanges.set(current.definition.symbolId, {
3492
+ startLine: current.startLine,
3493
+ endLine: resolveCallableDefinitionEndLine(
3494
+ lines,
3495
+ current.definition,
3496
+ current.startLine,
3497
+ maxEndLine
3498
+ )
3499
+ });
3500
+ }
3501
+ return definitions.map((definition) => {
3502
+ const corrected = correctedRanges.get(definition.symbolId);
3503
+ if (!corrected) {
3504
+ return definition;
3505
+ }
3506
+ return {
3507
+ ...definition,
3508
+ startLine: corrected.startLine,
3509
+ endLine: corrected.endLine
3510
+ };
3511
+ });
3512
+ }
3513
+ function resolveCallableDefinitionStartLine(lines, definition) {
3514
+ if (!isCallableDefinition(definition.symbol)) {
3515
+ return definition.startLine;
3516
+ }
3517
+ const escapedLeaf = escapeRegex2(definition.leaf);
3518
+ const strongPatterns = [
3519
+ new RegExp(`\\b(?:function|def|fn)\\s+${escapedLeaf}\\b`),
3520
+ new RegExp(`\\b${escapedLeaf}\\b\\s*[:=]\\s*(?:async\\s*)?(?:function\\b|\\()`)
3521
+ ];
3522
+ const fallbackPatterns = [
3523
+ new RegExp(`\\b${escapedLeaf}\\b\\s*\\(`)
3524
+ ];
3525
+ return findNearestMatchingLine(
3526
+ lines,
3527
+ [...strongPatterns, ...fallbackPatterns],
3528
+ definition.startLine,
3529
+ definition.endLine
3530
+ );
3531
+ }
3532
+ function findNearestMatchingLine(lines, patterns, preferredStartLine, preferredEndLine) {
3533
+ const windowStart = Math.max(0, preferredStartLine - 40);
3534
+ const windowEnd = Math.min(lines.length - 1, Math.max(preferredEndLine + 40, preferredStartLine + 5));
3535
+ const windowMatch = matchNearestLine(lines, patterns, preferredStartLine, windowStart, windowEnd);
3536
+ if (windowMatch !== null) {
3537
+ return windowMatch;
3538
+ }
3539
+ const fullMatch = matchNearestLine(lines, patterns, preferredStartLine, 0, lines.length - 1);
3540
+ return fullMatch ?? Math.max(0, Math.min(preferredStartLine, lines.length - 1));
3541
+ }
3542
+ function matchNearestLine(lines, patterns, preferredLine, startLine, endLine) {
3543
+ let best = null;
3544
+ for (let lineIndex = startLine; lineIndex <= endLine; lineIndex += 1) {
3545
+ const line = lines[lineIndex] ?? "";
3546
+ if (!patterns.some((pattern) => pattern.test(line))) continue;
3547
+ const distance = Math.abs(lineIndex - preferredLine);
3548
+ if (!best || distance < best.distance) {
3549
+ best = { line: lineIndex, distance };
3550
+ }
3551
+ }
3552
+ return best?.line ?? null;
3553
+ }
3554
+ function resolveCallableDefinitionEndLine(lines, definition, startLine, maxEndLine) {
3555
+ const boundedEndLine = Math.max(startLine, Math.min(lines.length - 1, maxEndLine));
3556
+ const fallbackEndLine = Math.max(startLine, Math.min(boundedEndLine, definition.endLine));
3557
+ let braceDepth = 0;
3558
+ let parenDepth = 0;
3559
+ let sawOpeningBrace = false;
3560
+ for (let lineIndex = startLine; lineIndex <= boundedEndLine; lineIndex += 1) {
3561
+ const masked = maskStructuralLine(lines[lineIndex] ?? "");
3562
+ for (const char of masked) {
3563
+ if (char === "{") {
3564
+ braceDepth += 1;
3565
+ sawOpeningBrace = true;
3566
+ } else if (char === "}") {
3567
+ braceDepth = Math.max(0, braceDepth - 1);
3568
+ } else if (char === "(") {
3569
+ parenDepth += 1;
3570
+ } else if (char === ")") {
3571
+ parenDepth = Math.max(0, parenDepth - 1);
3572
+ }
3573
+ }
3574
+ if (sawOpeningBrace && braceDepth === 0) {
3575
+ return lineIndex;
3576
+ }
3577
+ if (!sawOpeningBrace && parenDepth === 0 && lineIndex >= fallbackEndLine) {
3578
+ return lineIndex;
3579
+ }
3580
+ }
3581
+ return fallbackEndLine;
3582
+ }
3583
+ function maskStructuralLine(line) {
3584
+ let masked = "";
3585
+ let quote = null;
3586
+ let escaping = false;
3587
+ for (let index = 0; index < line.length; index += 1) {
3588
+ const char = line[index];
3589
+ const next = line[index + 1];
3590
+ if (!quote && char === "/" && next === "/") {
3591
+ masked += " ".repeat(line.length - index);
3592
+ break;
3593
+ }
3594
+ if (quote) {
3595
+ if (escaping) {
3596
+ escaping = false;
3597
+ masked += " ";
3598
+ continue;
3599
+ }
3600
+ if (char === "\\") {
3601
+ escaping = true;
3602
+ masked += " ";
3603
+ continue;
3604
+ }
3605
+ if (char === quote) {
3606
+ quote = null;
3607
+ }
3608
+ masked += " ";
3609
+ continue;
3610
+ }
3611
+ if (char === '"' || char === "'" || char === "`") {
3612
+ quote = char;
3613
+ masked += " ";
3614
+ continue;
3615
+ }
3616
+ masked += char;
3617
+ }
3618
+ return masked;
3619
+ }
3620
+ function isCallableDefinition(symbol) {
3621
+ return symbol.includes("().");
3622
+ }
3623
+ function escapeRegex2(value) {
3624
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3625
+ }
3626
+ function getAllDefinitions(db, opts = {}) {
3627
+ const { scope } = opts;
3628
+ const rows = db.all(
3629
+ `SELECT relative_path
3630
+ FROM documents
3631
+ WHERE 1 = 1
3632
+ ${db.pathExclusionsFor("documents")}
3633
+ ${scope ? "AND relative_path LIKE ?" : ""}
3634
+ ORDER BY relative_path`,
3635
+ ...scope ? [`%${scope}%`] : []
3636
+ );
3637
+ return rows.filter((row) => !db.isIgnored(row.relative_path)).flatMap((row) => getDefinitionsForFile(db, row.relative_path));
3638
+ }
3639
+ function getAllFunctionLikeDefinitions(db) {
3640
+ return getAllDefinitions(db).filter((definition) => definition.isFunctionLike);
3641
+ }
3642
+ function resolvePythonCallTarget(db, current, currentFileDefinitions, imports2, constructorBindings, receiverName, calleeName) {
3643
+ if (receiverName === "self" || receiverName === "cls") {
3644
+ return findDefinitionByName(currentFileDefinitions, calleeName, current.parentTypeName, ["function"]);
3645
+ }
3646
+ if (receiverName) {
3647
+ const inferredType = constructorBindings.get(receiverName);
3648
+ if (inferredType) {
3649
+ const boundMethod = findDefinitionByName(currentFileDefinitions, calleeName, inferredType, ["function"]);
3650
+ if (boundMethod) {
2470
3651
  return boundMethod;
2471
3652
  }
2472
3653
  for (const entry of imports2) {
@@ -2557,6 +3738,225 @@ function resolveJavaScriptCallTarget(db, current, currentFileDefinitions, import
2557
3738
  }
2558
3739
  return findDefinitionByName(currentFileDefinitions, calleeName, null, ["function", "type"]);
2559
3740
  }
3741
+ function resolveSimpleSourceCallees(db, current, calls, bindings, limit) {
3742
+ const currentFileDefinitions = getDefinitionsForFile(db, current.relativePath);
3743
+ const currentDefinition = currentFileDefinitions.find((definition) => definition.symbolId === current.symbolId);
3744
+ if (!currentDefinition) {
3745
+ return [];
3746
+ }
3747
+ const rows = [];
3748
+ const seen = /* @__PURE__ */ new Set();
3749
+ for (const call of calls) {
3750
+ const resolved = resolveSimpleSourceCallTarget(
3751
+ db,
3752
+ currentDefinition,
3753
+ currentFileDefinitions,
3754
+ bindings,
3755
+ call.receiverName,
3756
+ call.calleeName
3757
+ );
3758
+ if (!resolved || resolved.symbolId === current.symbolId || db.isIgnored(resolved.relativePath)) continue;
3759
+ const chunkId = 2e9 + call.line;
3760
+ const key = `${resolved.symbol}|${resolved.relativePath}|${chunkId}`;
3761
+ if (seen.has(key)) continue;
3762
+ seen.add(key);
3763
+ rows.push({
3764
+ symbol: resolved.symbol,
3765
+ file: resolved.relativePath,
3766
+ chunkId
3767
+ });
3768
+ }
3769
+ return applyLimit(rows, limit);
3770
+ }
3771
+ function resolveSimpleSourceCallTarget(db, current, currentFileDefinitions, bindings, receiverName, calleeName) {
3772
+ if (!receiverName) {
3773
+ const localMethod = findDefinitionByName(currentFileDefinitions, calleeName, current.parentTypeName, ["function"]);
3774
+ if (localMethod) {
3775
+ return localMethod;
3776
+ }
3777
+ if (current.parentTypeName) {
3778
+ return findProjectDefinitionByTypeAndLeaf(db, current.parentTypeName, calleeName);
3779
+ }
3780
+ return findDefinitionByName(currentFileDefinitions, calleeName, null, ["function", "type"]);
3781
+ }
3782
+ const normalizedReceiver = normalizeReceiverName(receiverName);
3783
+ const inferredType = bindings.get(normalizedReceiver) ?? inferTypeNameFromReceiver(db, normalizedReceiver);
3784
+ if (!inferredType) {
3785
+ return null;
3786
+ }
3787
+ return findProjectDefinitionByTypeAndLeaf(db, inferredType, calleeName);
3788
+ }
3789
+ function findProjectDefinitionByTypeAndLeaf(db, typeName, calleeName) {
3790
+ const definitions = getAllDefinitions(db).filter((definition) => definition.isFunctionLike || definition.symbol.endsWith("()."));
3791
+ const exact = definitions.find((definition) => definition.leaf === calleeName && (definition.parentTypeName === typeName || definition.symbol.includes(typeName)));
3792
+ if (exact) {
3793
+ return exact;
3794
+ }
3795
+ const normalizedType = normalizeLookupName(typeName);
3796
+ const normalizedMatch = definitions.find((definition) => definition.leaf === calleeName && normalizeLookupName(definition.parentTypeName ?? "").includes(normalizedType));
3797
+ if (normalizedMatch) {
3798
+ return normalizedMatch;
3799
+ }
3800
+ return findLooseProjectDefinitionByTypeAndLeaf(db, typeName, calleeName);
3801
+ }
3802
+ function inferTypeNameFromReceiver(db, receiverName) {
3803
+ const normalizedReceiver = normalizeLookupName(receiverName);
3804
+ if (!normalizedReceiver) {
3805
+ return null;
3806
+ }
3807
+ const candidates = getAllDefinitions(db).filter((definition) => definition.isTypeLike || definition.symbol.endsWith("#")).map((definition) => definition.leaf).filter((leaf) => leaf.length > 0);
3808
+ let best = null;
3809
+ for (const candidate of candidates) {
3810
+ const normalizedCandidate = normalizeLookupName(candidate);
3811
+ let score = 0;
3812
+ if (normalizedCandidate === normalizedReceiver) score += 100;
3813
+ if (normalizedCandidate.endsWith(normalizedReceiver)) score += 80;
3814
+ if (normalizedCandidate.includes(normalizedReceiver)) score += 40;
3815
+ if (normalizedReceiver.includes(normalizedCandidate)) score += 20;
3816
+ if (score > 0 && (!best || score > best.score || score === best.score && candidate.length < best.name.length)) {
3817
+ best = { name: candidate, score };
3818
+ }
3819
+ }
3820
+ return best?.name ?? null;
3821
+ }
3822
+ function getSimpleSourceCalls(db, relativePath, startLine, endLine, opts = {}) {
3823
+ const source = getSourceText(db, relativePath);
3824
+ if (!source) {
3825
+ return [];
3826
+ }
3827
+ const lines = source.split("\n");
3828
+ const scoped = lines.slice(startLine, endLine + 1);
3829
+ const calls = [];
3830
+ const seen = /* @__PURE__ */ new Set();
3831
+ const pattern = opts.allowInstanceVariables ? /(@?[A-Za-z_][\w]*)\s*(?:\.|::)\s*([A-Za-z_][\w!?=]*)\s*\(/g : /\b([A-Za-z_][\w]*)\s*(?:\.|::)\s*([A-Za-z_][\w]*)\s*\(/g;
3832
+ for (let index = 0; index < scoped.length; index++) {
3833
+ const rawLine = scoped[index] ?? "";
3834
+ for (const match of rawLine.matchAll(pattern)) {
3835
+ const receiverName = match[1];
3836
+ const calleeName = match[2];
3837
+ if (!receiverName || !calleeName) continue;
3838
+ const key = `${startLine + index}|${receiverName}|${calleeName}`;
3839
+ if (seen.has(key)) continue;
3840
+ seen.add(key);
3841
+ calls.push({
3842
+ receiverName,
3843
+ calleeName,
3844
+ line: startLine + index
3845
+ });
3846
+ }
3847
+ if (opts.allowBareMemberCalls) {
3848
+ for (const match of rawLine.matchAll(/(@?[A-Za-z_][\w]*)\s*\.\s*([A-Za-z_][\w!?=]*)\b(?!\s*[:=])/g)) {
3849
+ const receiverName = match[1];
3850
+ const calleeName = match[2];
3851
+ if (!receiverName || !calleeName) continue;
3852
+ const key = `${startLine + index}|${receiverName}|${calleeName}`;
3853
+ if (seen.has(key)) continue;
3854
+ seen.add(key);
3855
+ calls.push({
3856
+ receiverName,
3857
+ calleeName,
3858
+ line: startLine + index
3859
+ });
3860
+ }
3861
+ }
3862
+ for (const match of rawLine.matchAll(/\b([A-Za-z_][\w]*)\s*\(/g)) {
3863
+ const calleeName = match[1];
3864
+ const start = match.index ?? -1;
3865
+ if (!calleeName || start < 0) continue;
3866
+ const prefix = rawLine.slice(0, start).trimEnd();
3867
+ if (prefix.endsWith("def") || prefix.endsWith("fun") || prefix.endsWith("fn") || /\b(?:class|interface|trait|module|object)\s*$/.test(prefix)) {
3868
+ continue;
3869
+ }
3870
+ if (rawLine.slice(Math.max(0, start - 3), start).includes(".")) {
3871
+ continue;
3872
+ }
3873
+ calls.push({
3874
+ receiverName: null,
3875
+ calleeName,
3876
+ line: startLine + index
3877
+ });
3878
+ }
3879
+ }
3880
+ return calls;
3881
+ }
3882
+ function parseJavaFieldBindings(source) {
3883
+ const bindings = /* @__PURE__ */ new Map();
3884
+ for (const match of source.matchAll(/\b(?:private|protected|public)\s+(?:final\s+)?([A-Z][A-Za-z0-9_$<>?, ]*)\s+([A-Za-z_][\w$]*)\s*;/g)) {
3885
+ const typeName = stripGenericType(match[1]);
3886
+ const localName = match[2];
3887
+ if (typeName && localName) {
3888
+ bindings.set(localName, typeName);
3889
+ }
3890
+ }
3891
+ return bindings;
3892
+ }
3893
+ function parseKotlinFieldBindings(source) {
3894
+ const bindings = /* @__PURE__ */ new Map();
3895
+ for (const match of source.matchAll(/\b(?:private|protected|public)?\s*(?:val|var)\s+([A-Za-z_][\w]*)\s*:\s*([A-Z][A-Za-z0-9_.<>?]*)/g)) {
3896
+ const localName = match[1];
3897
+ const typeName = stripGenericType(match[2]);
3898
+ if (typeName && localName) {
3899
+ bindings.set(localName, typeName);
3900
+ }
3901
+ }
3902
+ return bindings;
3903
+ }
3904
+ function parseCppReceiverBindings(source) {
3905
+ const bindings = /* @__PURE__ */ new Map();
3906
+ const constructorMatch = source.match(/RunCoordinator::RunCoordinator\s*\(([\s\S]*?)\)\s*(?::\s*([\s\S]*?))?\s*\{/);
3907
+ if (!constructorMatch) {
3908
+ return bindings;
3909
+ }
3910
+ const params = constructorMatch[1] ?? "";
3911
+ const initializers = constructorMatch[2] ?? "";
3912
+ const parameterTypes = /* @__PURE__ */ new Map();
3913
+ for (const param of params.split(",")) {
3914
+ const trimmed = param.trim();
3915
+ const match = trimmed.match(/(.+?)\s+([A-Za-z_][\w]*)$/);
3916
+ if (!match) continue;
3917
+ const [, rawType, rawName] = match;
3918
+ if (!rawType || !rawName) continue;
3919
+ const typeName = stripGenericType(rawType.replace(/[&*]+/g, " ").replace(/\bconst\b/g, " ").trim());
3920
+ if (!typeName) continue;
3921
+ parameterTypes.set(normalizeReceiverName(rawName), typeName);
3922
+ }
3923
+ for (const initializer of initializers.split(",")) {
3924
+ const trimmed = initializer.trim();
3925
+ const match = trimmed.match(/([A-Za-z_][\w]*)\s*\(\s*([A-Za-z_][\w]*)\s*\)$/);
3926
+ if (!match) continue;
3927
+ const [, fieldName, sourceName] = match;
3928
+ if (!fieldName || !sourceName) continue;
3929
+ const typeName = parameterTypes.get(normalizeReceiverName(sourceName));
3930
+ if (!typeName) continue;
3931
+ bindings.set(normalizeReceiverName(fieldName), typeName);
3932
+ }
3933
+ return bindings;
3934
+ }
3935
+ function parseRubyReceiverBindings(db, source) {
3936
+ const bindings = /* @__PURE__ */ new Map();
3937
+ for (const match of source.matchAll(/@([A-Za-z_][\w]*)\s*=\s*([A-Za-z_][\w]*)/g)) {
3938
+ const ivarName = `@${match[1]}`;
3939
+ const localName = match[2];
3940
+ const typeName = inferTypeNameFromReceiver(db, localName ?? match[1] ?? "");
3941
+ if (typeName) {
3942
+ bindings.set(ivarName, typeName);
3943
+ bindings.set(match[1], typeName);
3944
+ }
3945
+ }
3946
+ return bindings;
3947
+ }
3948
+ function stripGenericType(typeName) {
3949
+ return (typeName ?? "").replace(/<.*$/, "").replace(/^.*\./, "").trim();
3950
+ }
3951
+ function normalizeReceiverName(receiverName) {
3952
+ return receiverName.replace(/^@/, "").replace(/^this(?:\.|->)/, "").replace(/^_+/, "").replace(/_+$/, "").trim();
3953
+ }
3954
+ function normalizeLookupName(value) {
3955
+ return value.replace(/[^A-Za-z0-9]+/g, "").toLowerCase();
3956
+ }
3957
+ function isLikelyTestPath(relativePath) {
3958
+ return /(^|\/)(tests?|__tests__)\//.test(relativePath) || /\.(?:spec|test)\./.test(relativePath) || /_test\./.test(relativePath);
3959
+ }
2560
3960
  function findDefinitionByName(definitions, leaf, parentType, preference) {
2561
3961
  const candidates = definitions.filter((definition) => definition.leaf === leaf && definition.parentTypeName === parentType);
2562
3962
  if (candidates.length === 0) {
@@ -2570,6 +3970,58 @@ function findDefinitionByName(definitions, leaf, parentType, preference) {
2570
3970
  }
2571
3971
  return candidates[0] ?? null;
2572
3972
  }
3973
+ function findLooseProjectDefinitionByTypeAndLeaf(db, typeName, calleeName) {
3974
+ const rows = db.all(
3975
+ `SELECT
3976
+ gs.id,
3977
+ gs.symbol,
3978
+ c.document_id,
3979
+ MIN(c.start_line) AS start_line,
3980
+ MAX(c.end_line) AS end_line,
3981
+ d.relative_path,
3982
+ gs.kind,
3983
+ gs.documentation,
3984
+ gs.enclosing_symbol
3985
+ FROM global_symbols gs
3986
+ JOIN mentions m ON m.symbol_id = gs.id
3987
+ JOIN chunks c ON c.id = m.chunk_id
3988
+ JOIN documents d ON d.id = c.document_id
3989
+ WHERE gs.symbol LIKE ?
3990
+ AND gs.symbol LIKE ?
3991
+ ${db.pathExclusionsFor("d")}
3992
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path, gs.kind, gs.documentation, gs.enclosing_symbol`,
3993
+ `%${typeName}%`,
3994
+ `%${calleeName}%`
3995
+ );
3996
+ const normalizedType = normalizeLookupName(typeName);
3997
+ const candidates = rows.filter((row2) => leafName(row2.symbol) === calleeName).filter((row2) => {
3998
+ const parentType = parentTypeName(row2.symbol);
3999
+ return parentType === typeName || row2.symbol.includes(typeName) || normalizeLookupName(parentType ?? "").includes(normalizedType);
4000
+ }).sort((left, right) => {
4001
+ const leftTest = isLikelyTestPath(left.relative_path) ? 1 : 0;
4002
+ const rightTest = isLikelyTestPath(right.relative_path) ? 1 : 0;
4003
+ return leftTest - rightTest || left.relative_path.localeCompare(right.relative_path) || left.start_line - right.start_line;
4004
+ });
4005
+ const row = candidates[0];
4006
+ if (!row) {
4007
+ return null;
4008
+ }
4009
+ return {
4010
+ symbolId: row.id,
4011
+ symbol: row.symbol,
4012
+ documentId: row.document_id,
4013
+ startLine: row.start_line,
4014
+ endLine: row.end_line,
4015
+ relativePath: row.relative_path,
4016
+ leaf: leafName(row.symbol),
4017
+ parentTypeName: parentTypeName(row.symbol),
4018
+ isFunctionLike: isFunctionLikeSymbol(row.symbol),
4019
+ isTypeLike: leafSuffix(row.symbol) === "type",
4020
+ kind: row.kind,
4021
+ documentation: row.documentation,
4022
+ enclosingSymbol: row.enclosing_symbol
4023
+ };
4024
+ }
2573
4025
  function hasUniqueLeafDefinition(db, leaf, symbolId) {
2574
4026
  const rows = db.all(
2575
4027
  `SELECT id, symbol
@@ -2615,6 +4067,76 @@ function isJavaScriptDocument(db, relativePath) {
2615
4067
  );
2616
4068
  return row?.language === "typescript" || row?.language === "javascript" || /\.(?:[cm]?[jt]sx?)$/.test(relativePath);
2617
4069
  }
4070
+ function isJavaDocument(db, relativePath) {
4071
+ const row = db.get(
4072
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4073
+ relativePath
4074
+ );
4075
+ return row?.language === "java" || relativePath.endsWith(".java");
4076
+ }
4077
+ function isKotlinDocument(db, relativePath) {
4078
+ const row = db.get(
4079
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4080
+ relativePath
4081
+ );
4082
+ return row?.language === "kotlin" || relativePath.endsWith(".kt") || relativePath.endsWith(".kts");
4083
+ }
4084
+ function isScalaDocument(db, relativePath) {
4085
+ const row = db.get(
4086
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4087
+ relativePath
4088
+ );
4089
+ return row?.language === "scala" || relativePath.endsWith(".scala");
4090
+ }
4091
+ function isCSharpDocument(db, relativePath) {
4092
+ const row = db.get(
4093
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4094
+ relativePath
4095
+ );
4096
+ return row?.language === "C#" || relativePath.endsWith(".cs");
4097
+ }
4098
+ function isVisualBasicDocument(db, relativePath) {
4099
+ const row = db.get(
4100
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4101
+ relativePath
4102
+ );
4103
+ return row?.language === "Visual Basic" || relativePath.endsWith(".vb");
4104
+ }
4105
+ function isCppDocument(db, relativePath) {
4106
+ const row = db.get(
4107
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4108
+ relativePath
4109
+ );
4110
+ return row?.language === "CPP" || /\.(?:cc|cpp|cxx|hpp|hh|hxx)$/.test(relativePath);
4111
+ }
4112
+ function isRustDocument(db, relativePath) {
4113
+ const row = db.get(
4114
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4115
+ relativePath
4116
+ );
4117
+ return row?.language === "Rust" || relativePath.endsWith(".rs");
4118
+ }
4119
+ function isRubyDocument(db, relativePath) {
4120
+ const row = db.get(
4121
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4122
+ relativePath
4123
+ );
4124
+ return row?.language === "ruby" || relativePath.endsWith(".rb");
4125
+ }
4126
+ function isDartDocument(db, relativePath) {
4127
+ const row = db.get(
4128
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4129
+ relativePath
4130
+ );
4131
+ return row?.language === "Dart" || relativePath.endsWith(".dart");
4132
+ }
4133
+ function isPhpDocument(db, relativePath) {
4134
+ const row = db.get(
4135
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
4136
+ relativePath
4137
+ );
4138
+ return row?.language === "PHP" || relativePath.endsWith(".php");
4139
+ }
2618
4140
  function applyLimit(values, limit) {
2619
4141
  return typeof limit === "number" ? values.slice(0, limit) : values;
2620
4142
  }
@@ -2635,7 +4157,14 @@ function resolveDocumentCandidates(db, filePattern, opts) {
2635
4157
  score: scoreDocumentPath(row.relative_path, normalizedPattern)
2636
4158
  })).filter((row) => row.score > 0).sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
2637
4159
  if (scored.length === 0) {
2638
- return [];
4160
+ const symbolMatch = findFirstSymbolMatch(db, filePattern);
4161
+ if (!symbolMatch || db.isIgnored(symbolMatch.relativePath)) {
4162
+ return [];
4163
+ }
4164
+ return [{
4165
+ relativePath: symbolMatch.relativePath,
4166
+ score: 700
4167
+ }];
2639
4168
  }
2640
4169
  const exactish = scored.filter((row) => row.score >= 800);
2641
4170
  if (exactish.length > 0) {
@@ -2645,8 +4174,8 @@ function resolveDocumentCandidates(db, filePattern, opts) {
2645
4174
  }
2646
4175
  function scoreDocumentPath(relativePath, rawPattern) {
2647
4176
  const normalizedPath = normalizeLookupPath(relativePath);
2648
- const pathBase = basename(normalizedPath);
2649
- const patternBase = basename(rawPattern);
4177
+ const pathBase = basename2(normalizedPath);
4178
+ const patternBase = basename2(rawPattern);
2650
4179
  let score = 0;
2651
4180
  if (normalizedPath === rawPattern) score += 1200;
2652
4181
  if (normalizedPath.endsWith(`/${rawPattern}`)) score += 1100;
@@ -2674,62 +4203,69 @@ function symbols(db, filePattern) {
2674
4203
  if (resolvedPaths.length === 0) {
2675
4204
  return [];
2676
4205
  }
2677
- const placeholders = resolvedPaths.map(() => "?").join(", ");
2678
- const rows = db.all(
2679
- `SELECT
2680
- der.start_line,
2681
- der.end_line,
2682
- REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig,
2683
- gs.symbol,
2684
- d.relative_path
2685
- FROM defn_enclosing_ranges der
2686
- JOIN global_symbols gs ON der.symbol_id = gs.id
2687
- JOIN documents d ON der.document_id = d.id
2688
- WHERE d.relative_path IN (${placeholders})
2689
- AND ${db.localSymbolPredicate}
2690
- ${db.symbolNoise}
2691
- ORDER BY d.relative_path, der.start_line`,
2692
- ...resolvedPaths
2693
- );
2694
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
2695
- startLine: r.start_line,
2696
- endLine: r.end_line,
2697
- symbol: r.symbol,
2698
- shortName: shortenSymbol(r.symbol),
2699
- signature: cleanSignature(r.sig)
2700
- }));
4206
+ return resolvedPaths.flatMap((relativePath) => getDefinitionsForFile(db, relativePath)).filter((row) => !db.isIgnored(row.relativePath)).map((row) => {
4207
+ const docRow = db.get(
4208
+ `SELECT REPLACE(SUBSTR(documentation, INSTR(documentation, '|') + 1), char(10), ' ') AS sig
4209
+ FROM global_symbols
4210
+ WHERE id = ?`,
4211
+ row.symbolId
4212
+ );
4213
+ return {
4214
+ startLine: row.startLine,
4215
+ endLine: row.endLine,
4216
+ symbol: row.symbol,
4217
+ shortName: shortenSymbol(row.symbol),
4218
+ signature: cleanSignature(docRow?.sig ?? null)
4219
+ };
4220
+ });
2701
4221
  }
2702
4222
 
2703
4223
  // src/queries/methods.ts
4224
+ import { basename as basename3 } from "path";
2704
4225
  function methods(db, className) {
2705
- const rows = db.all(
2706
- `SELECT der.start_line, der.end_line, gs.symbol
2707
- FROM global_symbols gs
2708
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2709
- WHERE gs.symbol LIKE ?
2710
- AND ${db.localSymbolPredicate}
2711
- AND gs.symbol LIKE '%().%'
2712
- ${db.symbolNoise}
2713
- ORDER BY der.start_line`,
2714
- `%${className}#%`
2715
- );
2716
- return rows.map((r) => ({
2717
- startLine: r.start_line,
2718
- endLine: r.end_line,
2719
- name: leafName(r.symbol)
4226
+ const classMatch = findFirstSymbolMatch(db, className);
4227
+ if (!classMatch) {
4228
+ return [];
4229
+ }
4230
+ const ownerName = leafName(classMatch.symbol);
4231
+ const definitions = getDefinitionsForFile(db, classMatch.relativePath).filter((definition) => isCallableSymbol(definition.symbol));
4232
+ const directMethods = definitions.filter((definition) => definition.parentTypeName === ownerName || definition.symbol.includes(ownerName));
4233
+ const fileScopedMethods = directMethods.length > 0 ? directMethods : stripExtension(basename3(classMatch.relativePath)) === ownerName ? definitions.filter((definition) => definition.symbol.includes("<invalid-global-code>")) : [];
4234
+ return fileScopedMethods.map((definition) => ({
4235
+ startLine: definition.startLine,
4236
+ endLine: definition.endLine,
4237
+ name: leafName(definition.symbol)
2720
4238
  }));
2721
4239
  }
4240
+ function isCallableSymbol(rawSymbol) {
4241
+ return rawSymbol.endsWith("().") || leafSuffix(rawSymbol) === "method";
4242
+ }
4243
+ function stripExtension(relativePath) {
4244
+ return relativePath.replace(/\.[^.]+$/, "");
4245
+ }
2722
4246
 
2723
4247
  // src/queries/refs.ts
2724
4248
  function refs(db, symbolPattern) {
2725
4249
  const match = findFirstSymbolMatch(db, symbolPattern);
2726
4250
  if (match) {
4251
+ const includeDefinitionSite = !isFunctionLikeSymbol(match.symbol);
4252
+ const definitionRows = includeDefinitionSite ? [{
4253
+ relativePath: match.relativePath,
4254
+ line: match.startLine
4255
+ }] : [];
2727
4256
  const sourceSites = getSourceReferenceSites(db, match).filter((site) => !db.isIgnored(site.file)).map((site) => ({
2728
4257
  relativePath: site.file,
2729
4258
  line: site.line
2730
4259
  }));
2731
4260
  if (sourceSites.length > 0) {
2732
- return sourceSites;
4261
+ const seen2 = /* @__PURE__ */ new Set();
4262
+ const rows2 = [...definitionRows, ...sourceSites, ...getRubySemanticRefs(db, match)].filter((site) => {
4263
+ const key = `${site.relativePath}:${site.line}`;
4264
+ if (seen2.has(key)) return false;
4265
+ seen2.add(key);
4266
+ return true;
4267
+ });
4268
+ return rows2;
2733
4269
  }
2734
4270
  }
2735
4271
  const rows = db.all(
@@ -2744,10 +4280,70 @@ function refs(db, symbolPattern) {
2744
4280
  ORDER BY d.relative_path, c.start_line`,
2745
4281
  match?.symbolId ?? -1
2746
4282
  );
2747
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
4283
+ const referenceRows = rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
2748
4284
  relativePath: r.relative_path,
2749
4285
  line: r.start_line
2750
4286
  }));
4287
+ if (!match || db.isIgnored(match.relativePath) || isFunctionLikeSymbol(match.symbol)) {
4288
+ return referenceRows;
4289
+ }
4290
+ const seen = new Set(referenceRows.map((row) => `${row.relativePath}:${row.line}`));
4291
+ if (!seen.has(`${match.relativePath}:${match.startLine}`)) {
4292
+ referenceRows.unshift({
4293
+ relativePath: match.relativePath,
4294
+ line: match.startLine
4295
+ });
4296
+ }
4297
+ for (const row of getRubySemanticRefs(db, match)) {
4298
+ const key = `${row.relativePath}:${row.line}`;
4299
+ if (seen.has(key)) continue;
4300
+ seen.add(key);
4301
+ referenceRows.push(row);
4302
+ }
4303
+ return referenceRows;
4304
+ }
4305
+ function getRubySemanticRefs(db, match) {
4306
+ if (!match.relativePath.endsWith(".rb")) {
4307
+ return [];
4308
+ }
4309
+ const tokens = rubyReferenceTokens(match.symbol);
4310
+ if (tokens.length === 0) {
4311
+ return [];
4312
+ }
4313
+ const rows = db.all(
4314
+ `SELECT relative_path
4315
+ FROM documents
4316
+ WHERE relative_path LIKE '%.rb'
4317
+ ${db.pathExclusionsFor("documents")}
4318
+ ORDER BY relative_path`
4319
+ );
4320
+ const results = [];
4321
+ for (const row of rows) {
4322
+ if (db.isIgnored(row.relative_path)) continue;
4323
+ const source = getSourceText(db, row.relative_path);
4324
+ if (!source) continue;
4325
+ const lines = source.split("\n");
4326
+ for (let index = 0; index < lines.length; index++) {
4327
+ const line = lines[index] ?? "";
4328
+ if (tokens.some((token) => new RegExp(`@${token}\\b|\\b${token}:`).test(line))) {
4329
+ results.push({
4330
+ relativePath: row.relative_path,
4331
+ line: index
4332
+ });
4333
+ }
4334
+ }
4335
+ }
4336
+ return results;
4337
+ }
4338
+ function rubyReferenceTokens(rawSymbol) {
4339
+ const leaf = rawSymbol.split(":").pop() ?? rawSymbol;
4340
+ const snake = leaf.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9_]+/g, "_").toLowerCase().replace(/^_+|_+$/g, "");
4341
+ const parts = snake.split("_").filter(Boolean);
4342
+ const candidates = /* @__PURE__ */ new Set();
4343
+ if (snake) candidates.add(snake);
4344
+ if (parts.length >= 1) candidates.add(parts[parts.length - 1]);
4345
+ if (parts.length >= 2) candidates.add(parts.slice(-2).join("_"));
4346
+ return [...candidates];
2751
4347
  }
2752
4348
 
2753
4349
  // src/queries/trace.ts
@@ -2756,24 +4352,22 @@ function trace(db, symbolPattern) {
2756
4352
  if (!match) {
2757
4353
  return { definitions: [], referencedBy: [] };
2758
4354
  }
2759
- const defRows = db.all(
2760
- `SELECT d.relative_path, der.start_line, der.end_line,
4355
+ const definitionMeta = db.get(
4356
+ `SELECT
2761
4357
  gs.display_name,
2762
4358
  REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig
2763
4359
  FROM global_symbols gs
2764
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2765
- JOIN documents d ON der.document_id = d.id
2766
4360
  WHERE gs.id = ?
2767
- ORDER BY d.relative_path, der.start_line
2768
4361
  LIMIT 10`,
2769
4362
  match.symbolId
2770
4363
  );
2771
- const definitions = defRows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
2772
- relativePath: r.relative_path,
2773
- startLine: r.start_line,
2774
- endLine: r.end_line,
2775
- signature: buildTraceSignature(r.sig, r.display_name, match.symbol)
2776
- }));
4364
+ const definitions = db.isIgnored(match.relativePath) ? [] : [{
4365
+ relativePath: match.relativePath,
4366
+ startLine: match.startLine,
4367
+ endLine: match.endLine,
4368
+ signature: buildTraceSignature(definitionMeta?.sig ?? null, definitionMeta?.display_name ?? null, match.symbol),
4369
+ source: definitionSource(db, match.relativePath, match.startLine, match.endLine)
4370
+ }];
2777
4371
  const sourceSites = getSourceReferenceSites(db, match);
2778
4372
  const referencedBy = sourceSites.length > 0 ? sourceSites.filter((site) => !db.isIgnored(site.file)).map((site) => ({
2779
4373
  relativePath: site.file,
@@ -2806,6 +4400,15 @@ function trace(db, symbolPattern) {
2806
4400
  }));
2807
4401
  return { definitions, referencedBy };
2808
4402
  }
4403
+ function definitionSource(db, relativePath, startLine, endLine) {
4404
+ const source = getSourceText(db, relativePath);
4405
+ if (!source) {
4406
+ return null;
4407
+ }
4408
+ const lines = source.split("\n");
4409
+ const slice2 = lines.slice(startLine, endLine + 1).join("\n").trimEnd();
4410
+ return slice2.length > 0 ? slice2 : null;
4411
+ }
2809
4412
  function buildTraceSignature(signature, displayName, rawSymbol) {
2810
4413
  const cleaned = cleanSignature(signature);
2811
4414
  if (cleaned && !looksBogusSignature(cleaned)) {
@@ -2946,22 +4549,54 @@ function surface(db, modulePattern) {
2946
4549
  JOIN chunks c ON m.chunk_id = c.id
2947
4550
  JOIN documents d1 ON c.document_id = d1.id
2948
4551
  JOIN global_symbols gs ON m.symbol_id = gs.id
2949
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2950
- JOIN documents d2 ON der.document_id = d2.id
2951
- WHERE d2.relative_path IN (${placeholders})
2952
- AND d1.relative_path NOT IN (${placeholders})
2953
- AND ${db.localSymbolPredicate}
4552
+ WHERE d1.relative_path NOT IN (${placeholders})
4553
+ AND (
4554
+ EXISTS (
4555
+ SELECT 1
4556
+ FROM defn_enclosing_ranges der
4557
+ JOIN documents d2 ON der.document_id = d2.id
4558
+ WHERE der.symbol_id = gs.id
4559
+ AND d2.relative_path IN (${placeholders})
4560
+ )
4561
+ OR EXISTS (
4562
+ SELECT 1
4563
+ FROM mentions def_m
4564
+ JOIN chunks def_c ON def_m.chunk_id = def_c.id
4565
+ JOIN documents d2 ON def_c.document_id = d2.id
4566
+ WHERE def_m.symbol_id = gs.id
4567
+ AND def_m.role = 1
4568
+ AND d2.relative_path IN (${placeholders})
4569
+ )
4570
+ )
2954
4571
  AND m.role != 1
4572
+ AND ${db.localSymbolPredicate}
4573
+ ${db.pathExclusionsFor("d1")}
2955
4574
  ORDER BY d1.relative_path`,
2956
4575
  ...matchedPaths,
4576
+ ...matchedPaths,
2957
4577
  ...matchedPaths
2958
4578
  );
2959
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
4579
+ const exposedDefinitions = matchedPaths.flatMap(
4580
+ (relativePath) => getDefinitionsForFile(db, relativePath).filter((definition) => isCallableSymbol2(definition.symbol)).map((definition) => ({
4581
+ relative_path: relativePath,
4582
+ symbol: definition.symbol
4583
+ }))
4584
+ );
4585
+ const seen = /* @__PURE__ */ new Set();
4586
+ return [...rows, ...exposedDefinitions].filter((r) => !db.isIgnored(r.relative_path)).filter((r) => {
4587
+ const key = `${r.relative_path}|${r.symbol}`;
4588
+ if (seen.has(key)) return false;
4589
+ seen.add(key);
4590
+ return true;
4591
+ }).map((r) => ({
2960
4592
  consumer: r.relative_path,
2961
4593
  symbol: r.symbol,
2962
4594
  shortName: shortenSymbol(r.symbol)
2963
4595
  }));
2964
4596
  }
4597
+ function isCallableSymbol2(rawSymbol) {
4598
+ return rawSymbol.endsWith("().") || leafSuffix(rawSymbol) === "method";
4599
+ }
2965
4600
 
2966
4601
  // src/entry-surfaces.ts
2967
4602
  var liveBarrelCache = /* @__PURE__ */ new WeakMap();
@@ -2979,11 +4614,11 @@ function isWorkerEntrySurface(path2) {
2979
4614
  function isStructuralEntrySurface(path2) {
2980
4615
  const normalized = normalizePath2(path2);
2981
4616
  const segments = normalized.split("/");
2982
- const basename2 = segments[segments.length - 1] ?? normalized;
2983
- if (basename2 === "cli.ts" || basename2 === "cli.js" || basename2 === "postinstall.ts" || basename2 === "postinstall.js" || basename2 === "main.ts" || basename2 === "main.js" || basename2 === "main.rs" || basename2 === "main.go" || basename2 === "main.py") {
4617
+ const basename5 = segments[segments.length - 1] ?? normalized;
4618
+ if (basename5 === "cli.ts" || basename5 === "cli.js" || basename5 === "postinstall.ts" || basename5 === "postinstall.js" || basename5 === "main.ts" || basename5 === "main.js" || basename5 === "main.rs" || basename5 === "main.go" || basename5 === "main.py") {
2984
4619
  return true;
2985
4620
  }
2986
- if (basename2 === "index.ts" || basename2 === "index.js") {
4621
+ if (basename5 === "index.ts" || basename5 === "index.js") {
2987
4622
  return segments.length <= 2;
2988
4623
  }
2989
4624
  return normalized.endsWith("/mod.rs") || normalized.endsWith("/__init__.py");
@@ -3046,62 +4681,49 @@ function dead(db, opts = {}) {
3046
4681
  skipBarrels = false,
3047
4682
  includeMembers = false
3048
4683
  } = opts;
3049
- const params = [minLoc];
3050
- let testFileExclusions = "";
3051
- let memberExclusion = "";
3052
- let barrelExclusions = "";
3053
- if (scope) {
3054
- params.push(`%${scope}%`);
3055
- }
3056
- if (!includeTests) {
3057
- testFileExclusions = `
3058
- AND ${testFileExclusionSql("d", TEST_SUPPORT_PATH_PATTERNS)}
3059
- `;
3060
- }
3061
- if (!includeMembers) {
3062
- memberExclusion = `AND gs.symbol NOT LIKE '%#%'`;
3063
- }
3064
- if (skipBarrels) {
3065
- const inactiveBarrelPaths = getInactiveBarrelPaths(db);
3066
- if (inactiveBarrelPaths.length > 0) {
3067
- barrelExclusions = `AND ref_d.relative_path NOT IN (${inactiveBarrelPaths.map(() => "?").join(", ")})`;
3068
- params.push(...inactiveBarrelPaths);
3069
- }
3070
- }
3071
- const sql = `
3072
- SELECT
4684
+ const inactiveBarrelPaths = skipBarrels ? new Set(getInactiveBarrelPaths(db)) : /* @__PURE__ */ new Set();
4685
+ const referenceRows = db.all(
4686
+ `SELECT
4687
+ m.symbol_id,
3073
4688
  d.relative_path,
3074
- der.start_line,
3075
- der.end_line,
3076
- (der.end_line - der.start_line + 1) AS loc,
3077
- gs.symbol,
3078
- (SELECT COUNT(*) FROM mentions m2
3079
- JOIN chunks c2 ON m2.chunk_id = c2.id
3080
- WHERE m2.symbol_id = gs.id AND m2.role != 1 AND c2.document_id = d.id
3081
- ) AS same_file_refs
3082
- FROM global_symbols gs
3083
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3084
- JOIN documents d ON der.document_id = d.id
3085
- WHERE 1 = 1
3086
- ${db.pathExclusionsFor("d")}
3087
- ${db.symbolNoiseFor("gs")}
3088
- AND (der.end_line - der.start_line + 1) >= ?
3089
- ${scope ? "AND d.relative_path LIKE ?" : ""}
3090
- ${testFileExclusions}
3091
- ${memberExclusion}
3092
- AND NOT EXISTS (
3093
- SELECT 1
3094
- FROM mentions ref_m
3095
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
3096
- JOIN documents ref_d ON ref_c.document_id = ref_d.id
3097
- WHERE ref_m.symbol_id = gs.id
3098
- AND ref_m.role != 1
3099
- AND ref_d.id != d.id
3100
- ${barrelExclusions}
3101
- )
3102
- ORDER BY (der.end_line - der.start_line + 1) DESC, d.relative_path, der.start_line
3103
- `;
3104
- const rows = db.all(sql, ...params);
4689
+ COUNT(*) AS ref_count
4690
+ FROM mentions m
4691
+ JOIN chunks c ON m.chunk_id = c.id
4692
+ JOIN documents d ON c.document_id = d.id
4693
+ WHERE m.role != 1
4694
+ ${db.pathExclusionsFor("d")}
4695
+ GROUP BY m.symbol_id, d.relative_path`
4696
+ );
4697
+ const referencesBySymbol = /* @__PURE__ */ new Map();
4698
+ for (const row of referenceRows) {
4699
+ if (db.isIgnored(row.relative_path)) continue;
4700
+ if (inactiveBarrelPaths.has(row.relative_path)) continue;
4701
+ let refsForSymbol = referencesBySymbol.get(row.symbol_id);
4702
+ if (!refsForSymbol) {
4703
+ refsForSymbol = /* @__PURE__ */ new Map();
4704
+ referencesBySymbol.set(row.symbol_id, refsForSymbol);
4705
+ }
4706
+ refsForSymbol.set(row.relative_path, row.ref_count);
4707
+ }
4708
+ const definitions = getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).filter((definition) => !isModuleLikeSymbol(definition.symbol)).filter((definition) => looksValueLikeDefinition(definition.symbol)).filter((definition) => definition.isFunctionLike || !definition.enclosingSymbol || !looksValueLikeDefinition(definition.enclosingSymbol)).filter((definition) => includeTests || passesTestFileFilter(definition.relativePath)).filter((definition) => includeMembers || looksValueLikeDefinition(definition.symbol)).filter((definition) => definition.endLine - definition.startLine + 1 >= minLoc);
4709
+ const rows = definitions.map((definition) => {
4710
+ const refMap = referencesBySymbol.get(definition.symbolId) ?? /* @__PURE__ */ new Map();
4711
+ const sameFileRefs = refMap.get(definition.relativePath) ?? 0;
4712
+ let crossFileRefs = 0;
4713
+ for (const [relativePath, count] of refMap) {
4714
+ if (relativePath === definition.relativePath) continue;
4715
+ crossFileRefs += count;
4716
+ }
4717
+ return {
4718
+ relative_path: definition.relativePath,
4719
+ start_line: definition.startLine,
4720
+ end_line: definition.endLine,
4721
+ loc: definition.endLine - definition.startLine + 1,
4722
+ symbol: definition.symbol,
4723
+ same_file_refs: sameFileRefs,
4724
+ cross_file_refs: crossFileRefs
4725
+ };
4726
+ }).filter((row) => row.cross_file_refs === 0).sort((a, b) => b.loc - a.loc || a.relative_path.localeCompare(b.relative_path) || a.start_line - b.start_line);
3105
4727
  let deadCodeCount = 0;
3106
4728
  let fileInternalCount = 0;
3107
4729
  let totalLoc = 0;
@@ -3129,6 +4751,17 @@ function dead(db, opts = {}) {
3129
4751
  totalLoc
3130
4752
  };
3131
4753
  }
4754
+ function passesTestFileFilter(relativePath) {
4755
+ const patterns = [.../* @__PURE__ */ new Set([...TEST_FILE_PATTERNS, ...TEST_SUPPORT_PATH_PATTERNS])];
4756
+ return patterns.every((pattern) => !likeMatches(relativePath, pattern));
4757
+ }
4758
+ function likeMatches(value, pattern) {
4759
+ const regex = new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".")}$`);
4760
+ return regex.test(value);
4761
+ }
4762
+ function looksValueLikeDefinition(rawSymbol) {
4763
+ return isFunctionLikeSymbol(rawSymbol) || rawSymbol.endsWith("().") || rawSymbol.endsWith(".");
4764
+ }
3132
4765
 
3133
4766
  // src/queries/hotspots.ts
3134
4767
  function hotspots(db, opts = {}) {
@@ -3155,13 +4788,26 @@ function hotspots(db, opts = {}) {
3155
4788
  LIMIT ?`,
3156
4789
  limit
3157
4790
  );
3158
- return rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
4791
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
3159
4792
  symbol: r.symbol,
3160
4793
  shortName: shortenSymbol(r.symbol),
3161
4794
  refCount: r.ref_count,
3162
4795
  fileCount: r.file_count,
3163
4796
  definedIn: r.defined_in
3164
4797
  }));
4798
+ if (indexedResults.length > 0) {
4799
+ return indexedResults;
4800
+ }
4801
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).map((definition) => {
4802
+ const callerRows = getCallerRowsForSymbol(db, definition, { limit: 500 });
4803
+ return {
4804
+ symbol: definition.symbol,
4805
+ shortName: shortenSymbol(definition.symbol),
4806
+ refCount: callerRows.length,
4807
+ fileCount: new Set(callerRows.map((row) => row.file)).size,
4808
+ definedIn: definition.relativePath
4809
+ };
4810
+ }).filter((row) => row.refCount > 0).sort((left, right) => right.refCount - left.refCount || right.fileCount - left.fileCount).slice(0, limit);
3165
4811
  }
3166
4812
 
3167
4813
  // src/queries/imports.ts
@@ -3234,6 +4880,14 @@ function importedBy(db, symbolPattern) {
3234
4880
  if (targetFile && normalizePath3(entry.sourcePath) !== normalizePath3(targetFile)) {
3235
4881
  continue;
3236
4882
  }
4883
+ if (entry.kind === "side-effect") {
4884
+ importers.add(row.relative_path);
4885
+ continue;
4886
+ }
4887
+ if (targetFile && isCLikeImporter(row.relative_path)) {
4888
+ importers.add(row.relative_path);
4889
+ continue;
4890
+ }
3237
4891
  if (targetIsModule) {
3238
4892
  importers.add(row.relative_path);
3239
4893
  continue;
@@ -3307,6 +4961,9 @@ function renderImportSymbol(importedName, localName, kind) {
3307
4961
  function normalizePath3(path2) {
3308
4962
  return path2.replace(/\\/g, "/");
3309
4963
  }
4964
+ function isCLikeImporter(relativePath) {
4965
+ return /\.(?:c|h|cc|cpp|cxx|hpp|hh|hxx)$/i.test(relativePath);
4966
+ }
3310
4967
 
3311
4968
  // src/queries/outline.ts
3312
4969
  function outline(db, filePattern) {
@@ -3411,10 +5068,22 @@ function fanOut(db, filePattern) {
3411
5068
  ORDER BY symbol_count DESC`,
3412
5069
  resolvedFile
3413
5070
  );
3414
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
5071
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
3415
5072
  name: r.relative_path,
3416
5073
  count: r.symbol_count
3417
5074
  }));
5075
+ if (indexedResults.length > 0) {
5076
+ return indexedResults;
5077
+ }
5078
+ const graph = buildFileDepGraph(db);
5079
+ const deps2 = graph.get(resolvedFile);
5080
+ if (!deps2 || deps2.size === 0) {
5081
+ return [];
5082
+ }
5083
+ return [{
5084
+ name: resolvedFile,
5085
+ count: deps2.size
5086
+ }];
3418
5087
  }
3419
5088
  function topFanIn(db, opts = {}) {
3420
5089
  const { limit = 30, scope } = opts;
@@ -3633,7 +5302,7 @@ function bottlenecks(db, opts = {}) {
3633
5302
  minFanOut,
3634
5303
  limit
3635
5304
  );
3636
- return rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
5305
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
3637
5306
  symbol: r.symbol,
3638
5307
  shortName: shortenSymbol(r.symbol),
3639
5308
  fanIn: r.fan_in,
@@ -3641,51 +5310,42 @@ function bottlenecks(db, opts = {}) {
3641
5310
  score: r.fan_in * r.fan_out,
3642
5311
  definedIn: r.defined_in
3643
5312
  }));
5313
+ if (indexedResults.length > 0) {
5314
+ return indexedResults;
5315
+ }
5316
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).map((definition) => {
5317
+ const fanIn2 = new Set(
5318
+ getCallerRowsForSymbol(db, definition, { limit: 500 }).map((row) => row.file)
5319
+ ).size;
5320
+ const fanOut2 = new Set(
5321
+ getCalleeRowsForSymbol(db, definition, { limit: 500 }).filter((row) => row.file !== definition.relativePath).map((row) => `${row.symbol}|${row.file}`)
5322
+ ).size;
5323
+ return {
5324
+ symbol: definition.symbol,
5325
+ shortName: shortenSymbol(definition.symbol),
5326
+ fanIn: fanIn2,
5327
+ fanOut: fanOut2,
5328
+ score: fanIn2 * fanOut2,
5329
+ definedIn: definition.relativePath
5330
+ };
5331
+ }).filter((row) => row.fanIn >= minFanIn && row.fanOut >= minFanOut).sort((left, right) => right.score - left.score || right.fanIn - left.fanIn).slice(0, limit);
3644
5332
  }
3645
5333
 
3646
5334
  // src/queries/isolated.ts
3647
5335
  function isolated(db, opts = {}) {
3648
5336
  const { scope, minLoc = 3 } = opts;
3649
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3650
- const rows = db.all(
3651
- `SELECT
3652
- gs.symbol,
3653
- d.relative_path,
3654
- der.start_line,
3655
- der.end_line,
3656
- (der.end_line - der.start_line + 1) AS loc
3657
- FROM global_symbols gs
3658
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3659
- JOIN documents d ON der.document_id = d.id
3660
- WHERE 1 = 1
3661
- ${db.pathExclusionsFor("d")}
3662
- AND ${testFileExclusionSql("d")}
3663
- ${db.symbolNoiseFor("gs")}
3664
- AND gs.symbol NOT LIKE '%#%'
3665
- AND (der.end_line - der.start_line + 1) >= ?
3666
- ${scopeFilter}
3667
- -- No cross-file references TO this symbol
3668
- AND NOT EXISTS (
3669
- SELECT 1 FROM mentions m
3670
- JOIN chunks c ON m.chunk_id = c.id
3671
- WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id != d.id
3672
- )
3673
- -- No same-file references either
3674
- AND NOT EXISTS (
3675
- SELECT 1 FROM mentions m
3676
- JOIN chunks c ON m.chunk_id = c.id
3677
- WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id = d.id
3678
- )
3679
- ORDER BY loc DESC, d.relative_path`,
3680
- minLoc
3681
- );
3682
- return rows.filter((r) => !db.isIgnored(r.relative_path)).filter((r) => !isEntrySurface(db, r.relative_path)).map((r) => ({
3683
- symbol: r.symbol,
3684
- shortName: shortenSymbol(r.symbol),
3685
- relativePath: r.relative_path,
3686
- startLine: r.start_line,
3687
- endLine: r.end_line,
3688
- loc: r.loc
5337
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).filter((definition) => !isEntrySurface(db, definition.relativePath)).filter((definition) => definition.isFunctionLike).map((definition) => ({
5338
+ definition,
5339
+ loc: definition.endLine - definition.startLine + 1
5340
+ })).filter((entry) => entry.loc >= minLoc).filter((entry) => getCallerRowsForSymbol(db, entry.definition, { limit: 1 }).length === 0).filter((entry) => getCalleeRowsForSymbol(db, entry.definition, { limit: 1 }).length === 0).sort(
5341
+ (left, right) => right.loc - left.loc || left.definition.relativePath.localeCompare(right.definition.relativePath) || left.definition.startLine - right.definition.startLine
5342
+ ).map((entry) => ({
5343
+ symbol: entry.definition.symbol,
5344
+ shortName: shortenSymbol(entry.definition.symbol),
5345
+ relativePath: entry.definition.relativePath,
5346
+ startLine: entry.definition.startLine,
5347
+ endLine: entry.definition.endLine,
5348
+ loc: entry.loc
3689
5349
  }));
3690
5350
  }
3691
5351
 
@@ -3828,24 +5488,18 @@ function kindCounts(db, opts = {}) {
3828
5488
  }));
3829
5489
  }
3830
5490
  function loadKindRows(db, scope) {
3831
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3832
- return db.all(
3833
- `SELECT
3834
- gs.symbol,
3835
- gs.kind,
3836
- gs.documentation,
3837
- gs.enclosing_symbol,
3838
- d.relative_path,
3839
- der.start_line,
3840
- der.end_line
3841
- FROM global_symbols gs
3842
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3843
- JOIN documents d ON der.document_id = d.id
3844
- WHERE 1 = 1
3845
- ${db.pathExclusionsFor("d")}
3846
- ${scopeFilter}
3847
- ORDER BY d.relative_path, der.start_line`
3848
- ).filter((row) => !db.isIgnored(row.relative_path));
5491
+ return getAllDefinitions(db, { scope }).map(mapDefinitionToKindRow);
5492
+ }
5493
+ function mapDefinitionToKindRow(definition) {
5494
+ return {
5495
+ symbol: definition.symbol,
5496
+ kind: definition.kind,
5497
+ documentation: definition.documentation,
5498
+ enclosing_symbol: definition.enclosingSymbol,
5499
+ relative_path: definition.relativePath,
5500
+ start_line: definition.startLine,
5501
+ end_line: definition.endLine
5502
+ };
3849
5503
  }
3850
5504
  function resolveKindNumber(row) {
3851
5505
  if (row.kind !== null && row.kind !== 0) {
@@ -4027,7 +5681,7 @@ function uniqueRows(rows) {
4027
5681
  function similar(db, symbolPattern, opts = {}) {
4028
5682
  const { minSimilarity = 0.4, limit = 20 } = opts;
4029
5683
  const target = findCallees(db, symbolPattern);
4030
- if (!target || target.callees.size === 0) return [];
5684
+ if (!target) return [];
4031
5685
  if (!isFunctionLikeSymbol(target.symbol)) return [];
4032
5686
  const candidates = getAllCalleeFingerprints(db, {
4033
5687
  minCallees: 3,
@@ -4038,7 +5692,7 @@ function similar(db, symbolPattern, opts = {}) {
4038
5692
  const results = [];
4039
5693
  for (const candidate of candidates) {
4040
5694
  if (candidate.callees.size < 3) continue;
4041
- const { similarity, significantShared, trivialShared } = weightedSimilarity(
5695
+ const { similarity, significantShared } = weightedSimilarity(
4042
5696
  target.callees,
4043
5697
  candidate.callees,
4044
5698
  idfWeights
@@ -4059,7 +5713,10 @@ function similar(db, symbolPattern, opts = {}) {
4059
5713
  });
4060
5714
  }
4061
5715
  results.sort((a, b) => b.similarity - a.similarity);
4062
- return results.slice(0, limit);
5716
+ if (results.length > 0) {
5717
+ return results.slice(0, limit);
5718
+ }
5719
+ return similarBySourceShape(db, symbolPattern, { minSimilarity, limit });
4063
5720
  }
4064
5721
  function similarAll(db, opts = {}) {
4065
5722
  const { minSimilarity = 0.5, limit = 20, scope, minCallees = 4 } = opts;
@@ -4206,10 +5863,146 @@ function difference(a, b) {
4206
5863
  }
4207
5864
  return result;
4208
5865
  }
5866
+ function similarBySourceShape(db, symbolPattern, opts) {
5867
+ const target = findSourceFingerprint(db, symbolPattern);
5868
+ if (!target || target.tokens.size < 3) {
5869
+ return [];
5870
+ }
5871
+ const minSimilarity = opts.minSimilarity >= 0.5 ? opts.minSimilarity : 0.3;
5872
+ const results = [];
5873
+ for (const candidate of getAllSourceFingerprints(db)) {
5874
+ if (candidate.symbol === target.symbol || candidate.tokens.size < 3) continue;
5875
+ const shared = intersection(target.tokens, candidate.tokens);
5876
+ if (shared.size < 2) continue;
5877
+ const union = /* @__PURE__ */ new Set([...target.tokens, ...candidate.tokens]);
5878
+ const similarity = union.size > 0 ? shared.size / union.size : 0;
5879
+ if (similarity < minSimilarity) continue;
5880
+ results.push({
5881
+ symbolA: target.symbol,
5882
+ shortNameA: shortenSymbol(target.symbol),
5883
+ fileA: target.file,
5884
+ symbolB: candidate.symbol,
5885
+ shortNameB: shortenSymbol(candidate.symbol),
5886
+ fileB: candidate.file,
5887
+ similarity,
5888
+ sharedCallees: [...shared].sort(),
5889
+ uniqueToA: [...difference(target.tokens, candidate.tokens)].sort(),
5890
+ uniqueToB: [...difference(candidate.tokens, target.tokens)].sort()
5891
+ });
5892
+ }
5893
+ results.sort((a, b) => b.similarity - a.similarity || a.shortNameB.localeCompare(b.shortNameB));
5894
+ return results.slice(0, opts.limit);
5895
+ }
5896
+ function findSourceFingerprint(db, symbolPattern) {
5897
+ const match = findFirstSymbolMatch(db, symbolPattern);
5898
+ if (!match || !isFunctionLikeSymbol(match.symbol)) {
5899
+ return null;
5900
+ }
5901
+ const snippet = definitionSnippet(db, match.relativePath, match.startLine, match.endLine, leafName(match.symbol));
5902
+ const tokens = sourceTokens(snippet, leafName(match.symbol));
5903
+ if (tokens.size === 0) {
5904
+ return null;
5905
+ }
5906
+ return {
5907
+ symbol: match.symbol,
5908
+ file: match.relativePath,
5909
+ tokens
5910
+ };
5911
+ }
5912
+ function getAllSourceFingerprints(db) {
5913
+ return getAllDefinitions(db).filter((definition) => definition.isFunctionLike).map((definition) => ({
5914
+ symbol: definition.symbol,
5915
+ file: definition.relativePath,
5916
+ tokens: sourceTokens(
5917
+ definitionSnippet(db, definition.relativePath, definition.startLine, definition.endLine, definition.leaf),
5918
+ definition.leaf
5919
+ )
5920
+ })).filter((fingerprint) => fingerprint.tokens.size > 0);
5921
+ }
5922
+ function definitionSnippet(db, relativePath, startLine, endLine, leaf) {
5923
+ const source = getSourceText(db, relativePath);
5924
+ if (!source) {
5925
+ return "";
5926
+ }
5927
+ const lines = source.split("\n");
5928
+ if (endLine >= startLine && endLine - startLine <= 12) {
5929
+ return lines.slice(startLine, endLine + 1).join("\n");
5930
+ }
5931
+ const markerPatterns = [
5932
+ new RegExp(`\\bdef\\s+${escapeRegex3(leaf)}\\b`),
5933
+ new RegExp(`\\bfun\\s+${escapeRegex3(leaf)}\\b`),
5934
+ new RegExp(`\\bfn\\s+${escapeRegex3(leaf)}\\b`),
5935
+ new RegExp(`\\bfunction\\s+${escapeRegex3(leaf)}\\b`),
5936
+ new RegExp(`\\b${escapeRegex3(leaf)}\\s*\\(`)
5937
+ ];
5938
+ const definitionStart = lines.findIndex((line) => markerPatterns.some((pattern) => pattern.test(line)));
5939
+ if (definitionStart >= 0) {
5940
+ let definitionEnd = definitionStart;
5941
+ for (let index = definitionStart + 1; index < lines.length && index <= definitionStart + 8; index++) {
5942
+ const line = lines[index] ?? "";
5943
+ if (index > definitionStart && looksLikeDefinitionBoundary(line)) {
5944
+ break;
5945
+ }
5946
+ definitionEnd = index;
5947
+ if (line.trim() === "" && index > definitionStart + 1) {
5948
+ break;
5949
+ }
5950
+ }
5951
+ return lines.slice(definitionStart, definitionEnd + 1).join("\n");
5952
+ }
5953
+ return lines.slice(startLine, Math.min(lines.length, startLine + 8)).join("\n");
5954
+ }
5955
+ function sourceTokens(snippet, leaf) {
5956
+ if (!snippet) {
5957
+ return /* @__PURE__ */ new Set();
5958
+ }
5959
+ const stopWords = /* @__PURE__ */ new Set([
5960
+ "public",
5961
+ "private",
5962
+ "protected",
5963
+ "final",
5964
+ "static",
5965
+ "class",
5966
+ "def",
5967
+ "fun",
5968
+ "fn",
5969
+ "function",
5970
+ "return",
5971
+ "string",
5972
+ "bool",
5973
+ "boolean",
5974
+ "void",
5975
+ "unit",
5976
+ "self",
5977
+ "this",
5978
+ "new",
5979
+ "const",
5980
+ "let",
5981
+ "var",
5982
+ "end",
5983
+ "pub"
5984
+ ]);
5985
+ const normalizedLeafParts = splitIdentifier(leaf);
5986
+ const normalized = snippet.replace(/["'`]/g, " ").replace(/\b\d+\b/g, " NUM ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z0-9_]+/g, " ").replace(/_/g, " ").toLowerCase();
5987
+ const tokens = normalized.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 1).filter((token) => !stopWords.has(token)).filter((token) => !normalizedLeafParts.has(token));
5988
+ return new Set(tokens);
5989
+ }
5990
+ function splitIdentifier(value) {
5991
+ return new Set(
5992
+ value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9_]+|_/).map((part) => part.toLowerCase()).filter((part) => part.length > 1)
5993
+ );
5994
+ }
5995
+ function looksLikeDefinitionBoundary(line) {
5996
+ return /^\s*(?:def|fun|fn|function|class|trait|module|object|enum|interface|public|private|protected)\b/.test(line);
5997
+ }
5998
+ function escapeRegex3(value) {
5999
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6000
+ }
4209
6001
 
4210
6002
  // src/queries/similar-files.ts
4211
6003
  function similarFiles(db, opts = {}) {
4212
- const { minSimilarity = 0.5, limit = 20, scope, minDeps = 3, filePattern } = opts;
6004
+ const { minSimilarity = 0.5, limit = 20, scope, filePattern } = opts;
6005
+ const minDeps = opts.minDeps ?? (filePattern ? 1 : 3);
4213
6006
  const profiles = buildFileProfiles(db, { scope, minDeps });
4214
6007
  const results = [];
4215
6008
  if (filePattern) {
@@ -4258,7 +6051,7 @@ function findUniversalDependencies(depMap) {
4258
6051
  }
4259
6052
  }
4260
6053
  for (const [dep, count] of depCounts) {
4261
- if (count / fileCount > 0.5) {
6054
+ if (count >= 6 && count / fileCount > 0.8) {
4262
6055
  universalDeps.add(dep);
4263
6056
  }
4264
6057
  }
@@ -4317,8 +6110,8 @@ function similarChains(db, opts = {}) {
4317
6110
  tailFreq.set(chain[t], (tailFreq.get(chain[t]) ?? 0) + 1);
4318
6111
  }
4319
6112
  }
4320
- const infraThreshold = rawChains.length * 0.4;
4321
- const tailThreshold = rawChains.length * 0.3;
6113
+ const infraThreshold = rawChains.length * 0.9;
6114
+ const tailThreshold = rawChains.length * 0.8;
4322
6115
  const infraNodes = /* @__PURE__ */ new Set();
4323
6116
  for (const [node, freq] of nodeFreq) {
4324
6117
  if (freq > infraThreshold) infraNodes.add(node);
@@ -4328,13 +6121,13 @@ function similarChains(db, opts = {}) {
4328
6121
  }
4329
6122
  const structuralNames = ["index.ts", "index.js", "cli.ts", "main.ts", "health.ts", "health.js"];
4330
6123
  for (const node of nodeFreq.keys()) {
4331
- const basename2 = node.split("/").pop() ?? "";
4332
- if (structuralNames.includes(basename2)) infraNodes.add(node);
6124
+ const basename5 = node.split("/").pop() ?? "";
6125
+ if (structuralNames.includes(basename5)) infraNodes.add(node);
4333
6126
  }
4334
6127
  const filteredChains = [];
4335
6128
  for (const chain of rawChains) {
4336
6129
  const filtered = chain.filter((n) => !infraNodes.has(n));
4337
- if (filtered.length >= 3) {
6130
+ if (filtered.length >= 2) {
4338
6131
  filteredChains.push({ original: chain, filtered });
4339
6132
  }
4340
6133
  }
@@ -4499,31 +6292,13 @@ function isSubChain(sub, full) {
4499
6292
  // src/queries/extract-candidates.ts
4500
6293
  function extractCandidates(db, opts = {}) {
4501
6294
  const { scope, minLoc = 10, minCallees = 6, limit = 20 } = opts;
4502
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
4503
- const symbols2 = db.all(
4504
- `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
4505
- FROM global_symbols gs
4506
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
4507
- JOIN documents d ON der.document_id = d.id
4508
- WHERE 1 = 1
4509
- ${db.pathExclusionsFor("d")}
4510
- ${db.symbolNoiseFor("gs")}
4511
- AND (der.end_line - der.start_line + 1) >= ?
4512
- ${scopeFilter}
4513
- ORDER BY (der.end_line - der.start_line + 1) DESC`,
4514
- minLoc
4515
- );
6295
+ const symbols2 = getScopedDefinitions(db, scope).filter((definition) => definitionLoc(definition) >= minLoc && isFunctionLikeSymbol(definition.symbol)).sort((left, right) => definitionLoc(right) - definitionLoc(left));
4516
6296
  const results = [];
4517
6297
  for (const sym of symbols2) {
4518
- if (db.isIgnored(sym.relative_path)) continue;
4519
- const basename2 = sym.relative_path.split("/").pop() ?? "";
4520
- if (basename2.includes("types")) continue;
4521
- const calleeChunks = getCalleeRowsForSymbol(db, {
4522
- documentId: sym.document_id,
4523
- startLine: sym.start_line,
4524
- endLine: sym.end_line,
4525
- symbolId: sym.id
4526
- });
6298
+ if (db.isIgnored(sym.relativePath)) continue;
6299
+ const basename5 = sym.relativePath.split("/").pop() ?? "";
6300
+ if (basename5.includes("types")) continue;
6301
+ const calleeChunks = getCalleeRowsForSymbol(db, sym);
4527
6302
  const calleeSet = new Set(calleeChunks.map((c) => c.symbol));
4528
6303
  if (calleeSet.size < minCallees) continue;
4529
6304
  const cooccurrence = /* @__PURE__ */ new Map();
@@ -4586,10 +6361,10 @@ function extractCandidates(db, opts = {}) {
4586
6361
  results.push({
4587
6362
  symbol: sym.symbol,
4588
6363
  shortName: shortenSymbol(sym.symbol),
4589
- relativePath: sym.relative_path,
4590
- startLine: sym.start_line,
4591
- endLine: sym.end_line,
4592
- loc: sym.end_line - sym.start_line + 1,
6364
+ relativePath: sym.relativePath,
6365
+ startLine: sym.startLine,
6366
+ endLine: sym.endLine,
6367
+ loc: definitionLoc(sym),
4593
6368
  totalCallees: calleeSet.size,
4594
6369
  clusters: scoredClusters
4595
6370
  });
@@ -4598,6 +6373,20 @@ function extractCandidates(db, opts = {}) {
4598
6373
  results.sort((a, b) => b.clusters.length - a.clusters.length || b.loc - a.loc);
4599
6374
  return results.slice(0, limit);
4600
6375
  }
6376
+ function getScopedDefinitions(db, scope) {
6377
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6378
+ return db.all(
6379
+ `SELECT relative_path
6380
+ FROM documents
6381
+ WHERE 1 = 1
6382
+ ${db.pathExclusionsFor("documents")}
6383
+ ${scopeFilter}
6384
+ ORDER BY relative_path`
6385
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6386
+ }
6387
+ function definitionLoc(definition) {
6388
+ return definition.endLine - definition.startLine + 1;
6389
+ }
4601
6390
 
4602
6391
  // src/queries/affected.ts
4603
6392
  function affected(db, symbolPattern, opts = {}) {
@@ -4638,73 +6427,39 @@ function affected(db, symbolPattern, opts = {}) {
4638
6427
  return results;
4639
6428
  }
4640
6429
  function getDirectAffectedRows(db, target, scope) {
4641
- const sourceSites = getSourceReferenceSites(db, target).filter((site) => !db.isIgnored(site.file)).filter((site) => !scope || site.file.includes(scope));
4642
- if (sourceSites.length > 0) {
4643
- const rows2 = [];
4644
- const seen = /* @__PURE__ */ new Set();
4645
- for (const site of sourceSites) {
4646
- if (!site.enclosingSymbol || site.enclosingSymbol === target.symbol) {
4647
- const key2 = `${site.file}|(top-level)`;
4648
- if (seen.has(key2)) continue;
4649
- seen.add(key2);
4650
- rows2.push({
4651
- symbolId: null,
4652
- symbol: site.file,
4653
- shortName: "(top-level)",
4654
- file: site.file,
4655
- symbolMatch: null
4656
- });
4657
- continue;
4658
- }
4659
- const enclosing = findExactSymbolMatch(db, site.enclosingSymbol);
4660
- if (!enclosing || enclosing.symbolId === target.symbolId || db.isIgnored(enclosing.relativePath)) {
4661
- continue;
4662
- }
4663
- const key = `${enclosing.symbolId}|${enclosing.relativePath}`;
4664
- if (seen.has(key)) continue;
4665
- seen.add(key);
4666
- rows2.push({
4667
- symbolId: enclosing.symbolId,
4668
- symbol: enclosing.symbol,
4669
- shortName: shortenSymbol(enclosing.symbol),
4670
- file: enclosing.relativePath,
4671
- symbolMatch: enclosing
6430
+ const callerRows = getCallerRowsForSymbol(db, target, { limit: 500 }).filter((row) => !db.isIgnored(row.file)).filter((row) => !scope || row.file.includes(scope));
6431
+ const results = [];
6432
+ const seen = /* @__PURE__ */ new Set();
6433
+ for (const row of callerRows) {
6434
+ const match = findExactSymbolMatch(db, row.symbol);
6435
+ if (!match) {
6436
+ const key2 = `${row.file}|${row.symbol}`;
6437
+ if (seen.has(key2)) continue;
6438
+ seen.add(key2);
6439
+ results.push({
6440
+ symbolId: null,
6441
+ symbol: row.symbol,
6442
+ shortName: shortenSymbol(row.symbol),
6443
+ file: row.file,
6444
+ symbolMatch: null
4672
6445
  });
6446
+ continue;
4673
6447
  }
4674
- return rows2;
4675
- }
4676
- const rows = db.all(
4677
- `SELECT DISTINCT
4678
- enc_gs.id AS symbol_id,
4679
- enc_gs.symbol AS symbol,
4680
- enc_d.relative_path AS relative_path
4681
- FROM mentions m
4682
- JOIN chunks c ON m.chunk_id = c.id
4683
- JOIN documents ref_d ON c.document_id = ref_d.id
4684
- JOIN defn_enclosing_ranges enc_der
4685
- ON enc_der.document_id = ref_d.id
4686
- AND c.start_line >= enc_der.start_line
4687
- AND c.end_line <= enc_der.end_line
4688
- JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id
4689
- JOIN documents enc_d ON enc_der.document_id = enc_d.id
4690
- WHERE m.symbol_id = ?
4691
- AND m.role != 1
4692
- AND enc_gs.id != ?
4693
- ${db.symbolNoiseFor("enc_gs")}
4694
- ${db.pathExclusionsFor("enc_d")}
4695
- ${scope ? `AND enc_d.relative_path LIKE '%${scope}%'` : ""}
4696
- ORDER BY enc_d.relative_path
4697
- LIMIT 1`,
4698
- target.symbolId,
4699
- target.symbolId
4700
- );
4701
- return rows.filter((row) => !db.isIgnored(row.relative_path)).map((row) => ({
4702
- symbolId: row.symbol_id,
4703
- symbol: row.symbol,
4704
- shortName: shortenSymbol(row.symbol),
4705
- file: row.relative_path,
4706
- symbolMatch: findExactSymbolMatch(db, row.symbol)
4707
- }));
6448
+ if (match.symbolId === target.symbolId || db.isIgnored(match.relativePath)) {
6449
+ continue;
6450
+ }
6451
+ const key = `${match.symbolId}|${match.relativePath}`;
6452
+ if (seen.has(key)) continue;
6453
+ seen.add(key);
6454
+ results.push({
6455
+ symbolId: match.symbolId,
6456
+ symbol: match.symbol,
6457
+ shortName: shortenSymbol(match.symbol),
6458
+ file: match.relativePath,
6459
+ symbolMatch: match
6460
+ });
6461
+ }
6462
+ return results;
4708
6463
  }
4709
6464
 
4710
6465
  // src/queries/change-surface.ts
@@ -5054,140 +6809,129 @@ function isLikelyTypeOnlyDep(dep) {
5054
6809
  function shouldSkipDriftFile(filePath) {
5055
6810
  return isStructuralRole(path.basename(filePath)) || isTestLikePath(filePath);
5056
6811
  }
5057
- function isStructuralRole(basename2) {
5058
- if (basename2 === "index.ts" || basename2 === "index.js") return true;
5059
- if (basename2 === "cli.ts" || basename2 === "main.ts" || basename2 === "main.rs") return true;
5060
- if (basename2.includes("worker.") || basename2.includes("postinstall.")) return true;
5061
- if (basename2 === "health.ts" || basename2 === "health.js") return true;
6812
+ function isStructuralRole(basename5) {
6813
+ if (basename5 === "index.ts" || basename5 === "index.js") return true;
6814
+ if (basename5 === "cli.ts" || basename5 === "main.ts" || basename5 === "main.rs") return true;
6815
+ if (basename5.includes("worker.") || basename5.includes("postinstall.")) return true;
6816
+ if (basename5 === "health.ts" || basename5 === "health.js") return true;
5062
6817
  return false;
5063
6818
  }
5064
6819
  function isTestLikePath(filePath) {
5065
6820
  const normalized = filePath.replace(/\\/g, "/");
5066
- const basename2 = path.basename(normalized);
5067
- return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/") || /\.(test|spec)\.[A-Za-z0-9]+$/.test(basename2) || /_(test|spec)\.[A-Za-z0-9]+$/.test(basename2) || /^test[_-]/.test(basename2) || /^test\./.test(basename2);
6821
+ const basename5 = path.basename(normalized);
6822
+ return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/") || /\.(test|spec)\.[A-Za-z0-9]+$/.test(basename5) || /_(test|spec)\.[A-Za-z0-9]+$/.test(basename5) || /^test[_-]/.test(basename5) || /^test\./.test(basename5);
5068
6823
  }
5069
6824
 
5070
6825
  // src/queries/wrapper-candidates.ts
6826
+ import { basename as basename4, extname as extname3 } from "path";
5071
6827
  function wrapperCandidates(db, opts) {
5072
6828
  const { scope, maxLoc = 15, limit = 30 } = opts ?? {};
5073
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5074
- const rows = db.all(
5075
- `SELECT * FROM (
5076
- SELECT
5077
- gs.symbol,
5078
- d.relative_path AS file,
5079
- der.start_line,
5080
- der.end_line,
5081
- (der.end_line - der.start_line + 1) AS loc,
5082
- -- The single caller: the symbol whose definition range contains
5083
- -- the chunk that references our target
5084
- (SELECT caller_gs.symbol
5085
- FROM mentions ref_m
5086
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
5087
- JOIN defn_enclosing_ranges caller_der
5088
- ON caller_der.document_id = ref_c.document_id
5089
- AND ref_c.start_line >= caller_der.start_line
5090
- AND ref_c.end_line <= caller_der.end_line
5091
- JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id
5092
- WHERE ref_m.symbol_id = gs.id
5093
- AND ref_m.role != 1
5094
- AND ref_c.document_id != der.document_id
5095
- LIMIT 1
5096
- ) AS caller_symbol,
5097
- -- Fan-in of that single caller
5098
- (SELECT COUNT(DISTINCT caller_ref_c.document_id)
5099
- FROM mentions caller_ref_m
5100
- JOIN chunks caller_ref_c ON caller_ref_m.chunk_id = caller_ref_c.id
5101
- WHERE caller_ref_m.symbol_id = (
5102
- SELECT caller_der2.symbol_id
5103
- FROM mentions ref_m2
5104
- JOIN chunks ref_c2 ON ref_m2.chunk_id = ref_c2.id
5105
- JOIN defn_enclosing_ranges caller_der2
5106
- ON caller_der2.document_id = ref_c2.document_id
5107
- AND ref_c2.start_line >= caller_der2.start_line
5108
- AND ref_c2.end_line <= caller_der2.end_line
5109
- WHERE ref_m2.symbol_id = gs.id
5110
- AND ref_m2.role != 1
5111
- AND ref_c2.document_id != der.document_id
5112
- LIMIT 1
5113
- )
5114
- AND caller_ref_m.role != 1
5115
- ) AS caller_fan_in
5116
- FROM global_symbols gs
5117
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5118
- JOIN documents d ON der.document_id = d.id
5119
- WHERE 1 = 1
5120
- ${db.pathExclusionsFor("d")}
5121
- AND ${testFileExclusionSql("d")}
5122
- ${db.symbolNoiseFor("gs")}
5123
- -- Only functions/terms, not type definitions (types with # are not wrappers)
5124
- AND gs.symbol NOT LIKE '%#'
5125
- AND (der.end_line - der.start_line + 1) <= ?
5126
- AND (der.end_line - der.start_line + 1) >= 2
5127
- ${scopeFilter}
5128
- -- Exactly 1 cross-file consumer
5129
- AND (
5130
- SELECT COUNT(DISTINCT ref_c.document_id)
5131
- FROM mentions ref_m
5132
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
5133
- WHERE ref_m.symbol_id = gs.id
5134
- AND ref_m.role != 1
5135
- AND ref_c.document_id != der.document_id
5136
- ) = 1
5137
- ) WHERE caller_symbol IS NOT NULL AND caller_fan_in > 3
5138
- ORDER BY caller_fan_in DESC, loc DESC
5139
- LIMIT ?`,
5140
- maxLoc,
5141
- limit
5142
- );
5143
- return rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
5144
- symbol: r.symbol,
5145
- shortName: shortenSymbol(r.symbol),
5146
- file: r.file,
5147
- startLine: r.start_line,
5148
- endLine: r.end_line,
5149
- loc: r.loc,
5150
- singleCaller: r.caller_symbol,
5151
- singleCallerShort: shortenSymbol(r.caller_symbol),
5152
- callerFanIn: r.caller_fan_in
5153
- }));
6829
+ const reverseFanIn = buildReverseFileFanIn(buildFileDepGraph(db, scope));
6830
+ const symbols2 = getScopedDefinitions2(db, scope).filter((definition) => definitionLoc2(definition) <= maxLoc && definitionLoc2(definition) >= 2);
6831
+ const results = [];
6832
+ for (const symbol of symbols2) {
6833
+ if (db.isIgnored(symbol.relativePath) || !isFunctionLikeSymbol(symbol.symbol)) continue;
6834
+ const symbolStem = basename4(symbol.relativePath, extname3(symbol.relativePath));
6835
+ const callerRows = dedupeRows(
6836
+ getCallerRowsForSymbol(db, symbol, { limit: 200 }).filter((row) => row.file !== symbol.relativePath)
6837
+ ).filter((row) => basename4(row.file, extname3(row.file)) !== symbolStem);
6838
+ if (callerRows.length !== 1) continue;
6839
+ const caller = callerRows[0];
6840
+ const callerDefinition = getDefinitionsForFile(db, caller.file).find((definition) => definition.symbol === caller.symbol);
6841
+ const useDefinitionFanIn = callerDefinition?.isFunctionLike ?? false;
6842
+ let callerFanIn;
6843
+ if (useDefinitionFanIn && callerDefinition) {
6844
+ callerFanIn = new Set(
6845
+ getCallerRowsForSymbol(db, callerDefinition, { limit: 500 }).filter((row) => row.file !== callerDefinition.relativePath).map((row) => row.file)
6846
+ ).size;
6847
+ } else {
6848
+ callerFanIn = fallbackCallerFanIn(db, reverseFanIn, caller.file);
6849
+ }
6850
+ if (callerFanIn <= 3) continue;
6851
+ results.push({
6852
+ symbol: symbol.symbol,
6853
+ shortName: shortenSymbol(symbol.symbol),
6854
+ file: symbol.relativePath,
6855
+ startLine: symbol.startLine,
6856
+ endLine: symbol.endLine,
6857
+ loc: definitionLoc2(symbol),
6858
+ singleCaller: caller.symbol,
6859
+ singleCallerShort: useDefinitionFanIn ? shortenSymbol(caller.symbol) : basename4(caller.file),
6860
+ callerFanIn
6861
+ });
6862
+ }
6863
+ results.sort((left, right) => right.callerFanIn - left.callerFanIn || right.loc - left.loc);
6864
+ return results.slice(0, limit);
6865
+ }
6866
+ function definitionLoc2(definition) {
6867
+ return definition.endLine - definition.startLine + 1;
6868
+ }
6869
+ function getScopedDefinitions2(db, scope) {
6870
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6871
+ return db.all(
6872
+ `SELECT relative_path
6873
+ FROM documents
6874
+ WHERE 1 = 1
6875
+ ${db.pathExclusionsFor("documents")}
6876
+ ${scopeFilter}
6877
+ ORDER BY relative_path`
6878
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6879
+ }
6880
+ function dedupeRows(rows) {
6881
+ const seen = /* @__PURE__ */ new Set();
6882
+ const unique = [];
6883
+ for (const row of rows) {
6884
+ const key = `${row.symbol}|${row.file}`;
6885
+ if (seen.has(key)) continue;
6886
+ seen.add(key);
6887
+ unique.push(row);
6888
+ }
6889
+ return unique;
6890
+ }
6891
+ function buildReverseFileFanIn(graph) {
6892
+ const reverse = /* @__PURE__ */ new Map();
6893
+ for (const [fromFile, deps2] of graph) {
6894
+ if (!reverse.has(fromFile)) {
6895
+ reverse.set(fromFile, reverse.get(fromFile) ?? 0);
6896
+ }
6897
+ for (const dep of deps2) {
6898
+ reverse.set(dep, (reverse.get(dep) ?? 0) + 1);
6899
+ }
6900
+ }
6901
+ return reverse;
6902
+ }
6903
+ function fallbackCallerFanIn(db, reverseFanIn, callerFile) {
6904
+ const functionLikeFanIn = getDefinitionsForFile(db, callerFile).filter((definition) => definition.isFunctionLike).map((definition) => new Set(
6905
+ getCallerRowsForSymbol(db, definition, { limit: 500 }).filter((row) => row.file !== definition.relativePath).map((row) => row.file)
6906
+ ).size).sort((left, right) => right - left)[0] ?? 0;
6907
+ if (functionLikeFanIn > 0) {
6908
+ return functionLikeFanIn;
6909
+ }
6910
+ const direct = reverseFanIn.get(callerFile) ?? 0;
6911
+ if (direct > 0) {
6912
+ return direct;
6913
+ }
6914
+ const stem = basename4(callerFile, extname3(callerFile));
6915
+ let best = 0;
6916
+ for (const [file, fanIn2] of reverseFanIn) {
6917
+ if (file === callerFile) continue;
6918
+ if (basename4(file, extname3(file)) !== stem) continue;
6919
+ if (fanIn2 > best) {
6920
+ best = fanIn2;
6921
+ }
6922
+ }
6923
+ return best;
5154
6924
  }
5155
6925
 
5156
6926
  // src/queries/passthrough-candidates.ts
5157
6927
  function passthroughCandidates(db, opts) {
5158
6928
  const { scope, maxLoc = 15, limit = 30 } = opts ?? {};
5159
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5160
- const symbols2 = db.all(
5161
- `SELECT
5162
- gs.id,
5163
- gs.symbol,
5164
- der.document_id,
5165
- der.start_line,
5166
- der.end_line,
5167
- (der.end_line - der.start_line + 1) AS loc,
5168
- d.relative_path
5169
- FROM global_symbols gs
5170
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5171
- JOIN documents d ON der.document_id = d.id
5172
- WHERE 1 = 1
5173
- ${db.pathExclusionsFor("d")}
5174
- AND ${testFileExclusionSql("d")}
5175
- ${db.symbolNoiseFor("gs")}
5176
- AND (der.end_line - der.start_line + 1) >= 3
5177
- AND (der.end_line - der.start_line + 1) <= ?
5178
- ${scopeFilter}
5179
- ORDER BY d.relative_path`,
5180
- maxLoc
5181
- );
6929
+ const symbols2 = getScopedDefinitions3(db, scope).filter((definition) => definitionLoc3(definition) >= 3 && definitionLoc3(definition) <= maxLoc);
5182
6930
  const results = [];
5183
6931
  for (const sym of symbols2) {
5184
- if (db.isIgnored(sym.relative_path)) continue;
5185
- const callees = getCalleeRowsForSymbol(db, {
5186
- documentId: sym.document_id,
5187
- startLine: sym.start_line,
5188
- endLine: sym.end_line,
5189
- symbolId: sym.id
5190
- });
6932
+ if (db.isIgnored(sym.relativePath) || !isFunctionLikeSymbol(sym.symbol)) continue;
6933
+ const rawCallees = getCalleeRowsForSymbol(db, sym);
6934
+ const callees = rawCallees.some((callee2) => isFunctionLikeSymbol(callee2.symbol)) ? rawCallees.filter((callee2) => isFunctionLikeSymbol(callee2.symbol)) : rawCallees;
5191
6935
  const uniqueCallees = /* @__PURE__ */ new Map();
5192
6936
  for (const c of callees) {
5193
6937
  if (!uniqueCallees.has(c.symbol)) {
@@ -5199,10 +6943,10 @@ function passthroughCandidates(db, opts) {
5199
6943
  results.push({
5200
6944
  symbol: sym.symbol,
5201
6945
  shortName: shortenSymbol(sym.symbol),
5202
- file: sym.relative_path,
5203
- startLine: sym.start_line,
5204
- endLine: sym.end_line,
5205
- loc: sym.loc,
6946
+ file: sym.relativePath,
6947
+ startLine: sym.startLine,
6948
+ endLine: sym.endLine,
6949
+ loc: definitionLoc3(sym),
5206
6950
  forwardsTo: callee.symbol,
5207
6951
  forwardsToShort: shortenSymbol(callee.symbol),
5208
6952
  forwardsToFile: callee.file
@@ -5211,42 +6955,32 @@ function passthroughCandidates(db, opts) {
5211
6955
  results.sort((a, b) => a.loc - b.loc || a.file.localeCompare(b.file));
5212
6956
  return results.slice(0, limit);
5213
6957
  }
6958
+ function getScopedDefinitions3(db, scope) {
6959
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6960
+ return db.all(
6961
+ `SELECT relative_path
6962
+ FROM documents
6963
+ WHERE 1 = 1
6964
+ ${db.pathExclusionsFor("documents")}
6965
+ ${scopeFilter}
6966
+ ORDER BY relative_path`
6967
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6968
+ }
6969
+ function definitionLoc3(definition) {
6970
+ return definition.endLine - definition.startLine + 1;
6971
+ }
5214
6972
 
5215
6973
  // src/queries/stale-abstractions.ts
5216
6974
  function staleAbstractions(db, opts) {
5217
6975
  const { scope, minLoc = 3, limit = 30 } = opts ?? {};
5218
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5219
- const rows = db.all(
5220
- `SELECT * FROM (
5221
- SELECT
5222
- gs.symbol,
5223
- d.relative_path AS file,
5224
- der.start_line,
5225
- der.end_line,
5226
- (der.end_line - der.start_line + 1) AS loc,
5227
- (SELECT COUNT(DISTINCT ref_c.document_id)
5228
- FROM mentions ref_m
5229
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
5230
- WHERE ref_m.symbol_id = gs.id
5231
- AND ref_m.role != 1
5232
- AND ref_c.document_id != der.document_id
5233
- ) AS consumers
5234
- FROM global_symbols gs
5235
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5236
- JOIN documents d ON der.document_id = d.id
5237
- WHERE 1 = 1
5238
- ${db.pathExclusionsFor("d")}
5239
- AND ${testFileExclusionSql("d")}
5240
- ${db.symbolNoiseFor("gs")}
5241
- -- Top-level type symbols: ends with # but does not contain nested #
5242
- AND gs.symbol LIKE '%#'
5243
- AND gs.symbol NOT LIKE '%#%#%'
5244
- AND (der.end_line - der.start_line + 1) >= ?
5245
- ${scopeFilter}
5246
- ) WHERE consumers <= 1
5247
- ORDER BY loc DESC, file ASC, start_line ASC`,
5248
- minLoc
5249
- );
6976
+ const rows = getScopedDefinitions4(db, scope).filter((definition) => definition.isTypeLike && definitionLoc4(definition) >= minLoc).map((definition) => ({
6977
+ symbol: definition.symbol,
6978
+ file: definition.relativePath,
6979
+ start_line: definition.startLine,
6980
+ end_line: definition.endLine,
6981
+ loc: definitionLoc4(definition),
6982
+ consumers: countCrossFileConsumers(db, definition)
6983
+ })).filter((row) => row.consumers <= 1).sort((left, right) => right.loc - left.loc || left.file.localeCompare(right.file) || left.start_line - right.start_line);
5250
6984
  const filesWithFunctions = getFilesWithFunctions(db, scope);
5251
6985
  return rows.filter((r) => !db.isIgnored(r.file)).filter((r) => isTrueStaleAbstraction(r, filesWithFunctions)).map((r) => ({
5252
6986
  symbol: r.symbol,
@@ -5259,22 +6993,11 @@ function staleAbstractions(db, opts) {
5259
6993
  })).slice(0, limit);
5260
6994
  }
5261
6995
  function getFilesWithFunctions(db, scope) {
5262
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5263
- return new Set(
5264
- db.all(
5265
- `SELECT DISTINCT d.relative_path
5266
- FROM global_symbols gs
5267
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5268
- JOIN documents d ON der.document_id = d.id
5269
- WHERE gs.symbol LIKE '%().'
5270
- ${db.pathExclusionsFor("d")}
5271
- ${scopeFilter}`
5272
- ).map((row) => row.relative_path).filter((path2) => !db.isIgnored(path2))
5273
- );
6996
+ return new Set(getScopedDefinitions4(db, scope).filter((definition) => definition.isFunctionLike).map((definition) => definition.relativePath));
5274
6997
  }
5275
6998
  function isTrueStaleAbstraction(row, filesWithFunctions) {
5276
- const basename2 = row.file.split("/").pop() ?? "";
5277
- const isTypeFile = basename2.includes("types") || row.file.includes("/types/");
6999
+ const basename5 = row.file.split("/").pop() ?? "";
7000
+ const isTypeFile = basename5.includes("types") || row.file.includes("/types/");
5278
7001
  if (isTypeFile && row.consumers > 0) {
5279
7002
  return false;
5280
7003
  }
@@ -5283,6 +7006,33 @@ function isTrueStaleAbstraction(row, filesWithFunctions) {
5283
7006
  }
5284
7007
  return true;
5285
7008
  }
7009
+ function getScopedDefinitions4(db, scope) {
7010
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
7011
+ return db.all(
7012
+ `SELECT relative_path
7013
+ FROM documents
7014
+ WHERE 1 = 1
7015
+ ${db.pathExclusionsFor("documents")}
7016
+ ${scopeFilter}
7017
+ ORDER BY relative_path`
7018
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
7019
+ }
7020
+ function countCrossFileConsumers(db, definition) {
7021
+ const callers = db.all(
7022
+ `SELECT DISTINCT d.relative_path
7023
+ FROM mentions m
7024
+ JOIN chunks c ON m.chunk_id = c.id
7025
+ JOIN documents d ON c.document_id = d.id
7026
+ WHERE m.symbol_id = ?
7027
+ AND m.role != 1
7028
+ ${db.pathExclusionsFor("d")}`,
7029
+ definition.symbolId
7030
+ ).map((row) => row.relative_path).filter((relativePath) => relativePath !== definition.relativePath && !db.isIgnored(relativePath));
7031
+ return new Set(callers).size;
7032
+ }
7033
+ function definitionLoc4(definition) {
7034
+ return definition.endLine - definition.startLine + 1;
7035
+ }
5286
7036
 
5287
7037
  // src/queries/complexity-hotspots.ts
5288
7038
  function complexityHotspots(db, opts) {
@@ -5359,7 +7109,7 @@ function complexityHotspots(db, opts) {
5359
7109
  minLoc,
5360
7110
  limit
5361
7111
  );
5362
- return rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
7112
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
5363
7113
  symbol: r.symbol,
5364
7114
  shortName: shortenSymbol(r.symbol),
5365
7115
  file: r.file,
@@ -5373,6 +7123,31 @@ function complexityHotspots(db, opts) {
5373
7123
  r.loc / 50 * (r.fan_in / 5) * Math.max(r.fan_out / 5, 1) * 100
5374
7124
  ) / 100
5375
7125
  }));
7126
+ if (indexedResults.length > 0) {
7127
+ return indexedResults;
7128
+ }
7129
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).map((definition) => {
7130
+ const loc = definition.endLine - definition.startLine + 1;
7131
+ const callerRows = getCallerRowsForSymbol(db, definition, { limit: 500 });
7132
+ const calleeRows = getCalleeRowsForSymbol(db, definition, { limit: 500 });
7133
+ const fanIn2 = new Set(callerRows.map((row) => row.file)).size;
7134
+ const fanOut2 = new Set(
7135
+ calleeRows.filter((row) => row.file !== definition.relativePath).map((row) => `${row.symbol}|${row.file}`)
7136
+ ).size;
7137
+ const calleeCount = new Set(calleeRows.map((row) => `${row.symbol}|${row.file}`)).size;
7138
+ return {
7139
+ symbol: definition.symbol,
7140
+ shortName: shortenSymbol(definition.symbol),
7141
+ file: definition.relativePath,
7142
+ startLine: definition.startLine,
7143
+ endLine: definition.endLine,
7144
+ loc,
7145
+ fanIn: fanIn2,
7146
+ fanOut: fanOut2,
7147
+ calleeCount,
7148
+ score: Math.round(loc / 50 * (fanIn2 / 5) * Math.max(fanOut2 / 5, 1) * 100) / 100
7149
+ };
7150
+ }).filter((row) => row.loc >= minLoc).sort((left, right) => right.score - left.score || right.loc - left.loc).slice(0, limit);
5376
7151
  }
5377
7152
 
5378
7153
  // src/queries/health.ts
@@ -5620,8 +7395,8 @@ function convergence(db, symbolPatternA, symbolPatternB) {
5620
7395
  }
5621
7396
 
5622
7397
  // src/queries/code.ts
5623
- import { readFileSync as readFileSync5 } from "fs";
5624
- import { join as join7 } from "path";
7398
+ import { readFileSync as readFileSync4 } from "fs";
7399
+ import { join as join9 } from "path";
5625
7400
  function code(db, symbolPattern, opts = {}) {
5626
7401
  const { context = 0 } = opts;
5627
7402
  const fileLineMatch = symbolPattern.match(/^(.+\.\w+):(\d+)-(\d+)$/);
@@ -5634,10 +7409,10 @@ function code(db, symbolPattern, opts = {}) {
5634
7409
  `SELECT language FROM documents WHERE relative_path = ?`,
5635
7410
  match.relativePath
5636
7411
  );
5637
- const filePath = join7(db.config.projectRoot, match.relativePath);
7412
+ const filePath = join9(db.config.projectRoot, match.relativePath);
5638
7413
  let fileContent;
5639
7414
  try {
5640
- fileContent = readFileSync5(filePath, "utf-8");
7415
+ fileContent = readFileSync4(filePath, "utf-8");
5641
7416
  } catch {
5642
7417
  return null;
5643
7418
  }
@@ -5664,10 +7439,10 @@ function readFileRange(db, filePath, startLine, endLine, context) {
5664
7439
  resolvedPath
5665
7440
  );
5666
7441
  if (!doc) return null;
5667
- const fullPath = join7(db.config.projectRoot, doc.relative_path);
7442
+ const fullPath = join9(db.config.projectRoot, doc.relative_path);
5668
7443
  let fileContent;
5669
7444
  try {
5670
- fileContent = readFileSync5(fullPath, "utf-8");
7445
+ fileContent = readFileSync4(fullPath, "utf-8");
5671
7446
  } catch {
5672
7447
  return null;
5673
7448
  }
@@ -5687,8 +7462,8 @@ function readFileRange(db, filePath, startLine, endLine, context) {
5687
7462
  }
5688
7463
 
5689
7464
  // src/queries/complexity.ts
5690
- import { readFileSync as readFileSync6 } from "fs";
5691
- import { join as join8 } from "path";
7465
+ import { readFileSync as readFileSync5 } from "fs";
7466
+ import { join as join10 } from "path";
5692
7467
  function complexity(db, symbolPattern) {
5693
7468
  const match = findFirstSymbolMatch(db, symbolPattern);
5694
7469
  if (!match) return null;
@@ -5697,10 +7472,10 @@ function complexity(db, symbolPattern) {
5697
7472
  match.relativePath
5698
7473
  );
5699
7474
  const language = doc?.language ?? "unknown";
5700
- const filePath = join8(db.config.projectRoot, match.relativePath);
7475
+ const filePath = join10(db.config.projectRoot, match.relativePath);
5701
7476
  let source = "";
5702
7477
  try {
5703
- const lines = readFileSync6(filePath, "utf-8").split("\n");
7478
+ const lines = readFileSync5(filePath, "utf-8").split("\n");
5704
7479
  source = lines.slice(match.startLine, match.endLine + 1).join("\n");
5705
7480
  } catch {
5706
7481
  }
@@ -6037,46 +7812,100 @@ function redundantReexports(db, opts = {}) {
6037
7812
  results.sort(
6038
7813
  (a, b) => b.directConsumers - a.directConsumers || a.barrelFile.localeCompare(b.barrelFile) || a.shortName.localeCompare(b.shortName)
6039
7814
  );
6040
- return limit ? results.slice(0, limit) : results;
7815
+ const withDartFallback = dedupeReexports([
7816
+ ...results,
7817
+ ...findSourceRedundantReexports(db, scope)
7818
+ ]);
7819
+ withDartFallback.sort(
7820
+ (a, b) => b.directConsumers - a.directConsumers || a.barrelFile.localeCompare(b.barrelFile) || a.shortName.localeCompare(b.shortName)
7821
+ );
7822
+ return limit ? withDartFallback.slice(0, limit) : withDartFallback;
7823
+ }
7824
+ function findSourceRedundantReexports(db, scope) {
7825
+ const files2 = db.all(
7826
+ `SELECT relative_path
7827
+ FROM documents
7828
+ WHERE 1 = 1
7829
+ ${scope ? "AND relative_path LIKE ?" : ""}
7830
+ ${db.pathExclusionsFor("documents")}
7831
+ ORDER BY relative_path`,
7832
+ ...scope ? [`%${scope}%`] : []
7833
+ );
7834
+ const candidates = files2.map((row) => row.relative_path).filter((relativePath) => !db.isIgnored(relativePath)).filter((relativePath) => getSourceExports(db, relativePath).length > 0);
7835
+ const results = [];
7836
+ for (const barrelPath of candidates) {
7837
+ const exports = getSourceExports(db, barrelPath).filter((entry) => entry.sourcePath && !db.isIgnored(entry.sourcePath));
7838
+ if (exports.length === 0) continue;
7839
+ const barrelConsumers = countDirectImporters(db, barrelPath, barrelPath);
7840
+ if (barrelConsumers > 0) continue;
7841
+ for (const exported of exports) {
7842
+ const sourcePath = exported.sourcePath;
7843
+ const representative = representativeExportSymbol(db, sourcePath);
7844
+ if (!representative) continue;
7845
+ results.push({
7846
+ barrelFile: barrelPath,
7847
+ symbol: representative.symbol,
7848
+ shortName: shortenSymbol(representative.symbol),
7849
+ originalFile: sourcePath,
7850
+ barrelConsumers: 0,
7851
+ directConsumers: countDirectImporters(db, sourcePath, barrelPath)
7852
+ });
7853
+ }
7854
+ }
7855
+ return results;
7856
+ }
7857
+ function countDirectImporters(db, targetPath, excludedPath) {
7858
+ const files2 = db.all(
7859
+ `SELECT relative_path
7860
+ FROM documents
7861
+ WHERE 1 = 1
7862
+ ${db.pathExclusionsFor("documents")}
7863
+ ORDER BY relative_path`
7864
+ );
7865
+ const importers = /* @__PURE__ */ new Set();
7866
+ for (const row of files2) {
7867
+ if (db.isIgnored(row.relative_path) || row.relative_path === excludedPath) continue;
7868
+ for (const imported of getSourceImports(db, row.relative_path)) {
7869
+ if (imported.sourcePath === targetPath) {
7870
+ importers.add(row.relative_path);
7871
+ }
7872
+ }
7873
+ }
7874
+ return importers.size;
7875
+ }
7876
+ function representativeExportSymbol(db, sourcePath) {
7877
+ const definitions = getDefinitionsForFile(db, sourcePath);
7878
+ return definitions.find((definition) => leafSuffix(definition.symbol) === "method") ?? definitions[0] ?? null;
7879
+ }
7880
+ function dedupeReexports(rows) {
7881
+ const seen = /* @__PURE__ */ new Set();
7882
+ const unique = [];
7883
+ for (const row of rows) {
7884
+ const key = `${row.barrelFile}|${row.symbol}|${row.originalFile}`;
7885
+ if (seen.has(key)) continue;
7886
+ seen.add(key);
7887
+ unique.push(row);
7888
+ }
7889
+ return unique;
6041
7890
  }
6042
7891
 
6043
7892
  // src/queries/similar-signatures.ts
6044
7893
  function similarSignatures(db, opts = {}) {
6045
7894
  const { scope, minLoc = 1, limit } = opts;
6046
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
6047
- const rows = db.all(
6048
- `SELECT
6049
- gs.symbol,
6050
- d.relative_path,
6051
- der.start_line,
6052
- der.end_line,
6053
- (der.end_line - der.start_line + 1) AS loc,
6054
- REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig
6055
- FROM global_symbols gs
6056
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
6057
- JOIN documents d ON der.document_id = d.id
6058
- WHERE gs.documentation IS NOT NULL
6059
- AND gs.documentation != ''
6060
- AND INSTR(gs.documentation, '|') > 0
6061
- AND (der.end_line - der.start_line + 1) >= ?
6062
- ${db.pathExclusionsFor("d")}
6063
- ${db.symbolNoiseFor("gs")}
6064
- ${scopeFilter}
6065
- ORDER BY d.relative_path, der.start_line`,
6066
- minLoc
6067
- );
6068
7895
  const sigGroups = /* @__PURE__ */ new Map();
6069
- for (const row of rows) {
6070
- if (db.isIgnored(row.relative_path)) continue;
6071
- const normalized = normalizeSignature(row.sig);
7896
+ for (const definition of getAllDefinitions(db, { scope })) {
7897
+ if (!definition.isFunctionLike || db.isIgnored(definition.relativePath)) continue;
7898
+ const loc = definition.endLine - definition.startLine + 1;
7899
+ if (loc < minLoc) continue;
7900
+ const normalized = resolveNormalizedSignature(db, definition);
6072
7901
  if (!normalized) continue;
6073
7902
  const entry = {
6074
- symbol: row.symbol,
6075
- shortName: shortenSymbol(row.symbol),
6076
- file: row.relative_path,
6077
- startLine: row.start_line,
6078
- endLine: row.end_line,
6079
- loc: row.loc
7903
+ symbol: definition.symbol,
7904
+ shortName: shortenSymbol(definition.symbol),
7905
+ file: definition.relativePath,
7906
+ startLine: definition.startLine,
7907
+ endLine: definition.endLine,
7908
+ loc
6080
7909
  };
6081
7910
  const existing = sigGroups.get(normalized);
6082
7911
  if (existing) {
@@ -6099,6 +7928,52 @@ function similarSignatures(db, opts = {}) {
6099
7928
  });
6100
7929
  return limit ? results.slice(0, limit) : results;
6101
7930
  }
7931
+ function resolveNormalizedSignature(db, definition) {
7932
+ const documented = extractDocumentedSignature(definition.documentation);
7933
+ const normalizedDocumented = documented ? normalizeSignature(documented) : null;
7934
+ if (normalizedDocumented) {
7935
+ return normalizedDocumented;
7936
+ }
7937
+ return normalizeSourceSignature(
7938
+ extractDeclarationHead(db, definition.relativePath, definition.startLine, definition.endLine, definition.leaf),
7939
+ definition.leaf
7940
+ );
7941
+ }
7942
+ function extractDocumentedSignature(documentation) {
7943
+ if (!documentation || !documentation.includes("|")) {
7944
+ return null;
7945
+ }
7946
+ const extracted = documentation.slice(documentation.indexOf("|") + 1).replace(/\n/g, " ").trim();
7947
+ return extracted.length > 0 ? extracted : null;
7948
+ }
7949
+ function extractDeclarationHead(db, relativePath, startLine, endLine, leaf) {
7950
+ const source = getSourceText(db, relativePath);
7951
+ if (!source) return null;
7952
+ const lines = source.split(/\r?\n/);
7953
+ const candidates = declarationStartLines(lines, startLine, endLine, leaf);
7954
+ for (const candidate of candidates) {
7955
+ const maxLine = Math.min(lines.length - 1, Math.max(candidate, candidate + 4));
7956
+ let collected = "";
7957
+ for (let lineIndex = candidate; lineIndex <= maxLine; lineIndex += 1) {
7958
+ const line = lines[lineIndex]?.trim();
7959
+ if (!line) continue;
7960
+ collected = collected ? `${collected} ${line}` : line;
7961
+ if (looksCompleteDeclaration(collected)) {
7962
+ return collected;
7963
+ }
7964
+ }
7965
+ if (collected && collected.includes("(")) {
7966
+ return collected;
7967
+ }
7968
+ }
7969
+ return null;
7970
+ }
7971
+ function looksCompleteDeclaration(declaration) {
7972
+ const normalized = declaration.replace(/\s+/g, " ").trim();
7973
+ if (!normalized.includes("(")) return false;
7974
+ if (parenBalance(normalized) > 0) return false;
7975
+ return /[;{]$/.test(normalized) || /\)\s*(?::\s*[^={]+)?\s*(?:=>|=|throws\b|where\b|$)/i.test(normalized) || /\)\s*As\s+.+$/i.test(normalized);
7976
+ }
6102
7977
  function normalizeSignature(raw) {
6103
7978
  if (!raw || !raw.trim()) return null;
6104
7979
  let sig = raw.replace(/^```\w*\s*/, "").replace(/\s*```$/, "").replace(/^\(method\)\s*/, "").replace(/^\(property\)\s*/, "").replace(/^\(function\)\s*/, "").replace(/^\(class\)\s*/, "").replace(/^\(interface\)\s*/, "").replace(/^\(enum\)\s*/, "").replace(/^\(type alias\)\s*/, "").replace(/^\(const\)\s*/, "").replace(/^\(var\)\s*/, "").trim();
@@ -6109,28 +7984,88 @@ function normalizeSignature(raw) {
6109
7984
  if (sig.length < 3) return null;
6110
7985
  return sig;
6111
7986
  }
7987
+ function normalizeSourceSignature(raw, leaf) {
7988
+ if (!raw || !raw.trim()) return null;
7989
+ const declaration = raw.replace(/\s+/g, " ").trim();
7990
+ const parenIdx = declaration.indexOf("(");
7991
+ if (parenIdx === -1) return null;
7992
+ let prefix = declaration.slice(0, parenIdx);
7993
+ const leafPattern = new RegExp(`\\b${escapeRegex4(leaf)}\\b`, "i");
7994
+ const leafMatch = leafPattern.exec(prefix);
7995
+ if (leafMatch && typeof leafMatch.index === "number") {
7996
+ prefix = prefix.slice(0, leafMatch.index);
7997
+ }
7998
+ prefix = prefix.replace(/\b(public|private|protected|internal|final|static|abstract|sealed|virtual|override|async|suspend|inline|constexpr|consteval|constinit|const|pub|fn|function|def|sub|friend|shared|readonly|new|open|partial|export)\b/gi, " ").replace(/\s+/g, " ").trim();
7999
+ const suffix = declaration.slice(parenIdx).replace(/\s*\{[\s\S]*$/, "").replace(/\s*=>[\s\S]*$/, "").replace(/\)\s*=\s*[\s\S]*$/, ")").replace(/\s+throws\s+[^={]+$/i, "").replace(/\s+where\s+.+$/i, "").replace(/\s+/g, " ").trim();
8000
+ if (!suffix.startsWith("(")) {
8001
+ return null;
8002
+ }
8003
+ const normalized = `${prefix ? `${prefix} ` : ""}${suffix}`.replace(/\s+/g, "").toLowerCase();
8004
+ return normalized.length >= 3 ? normalized : null;
8005
+ }
8006
+ function declarationStartLines(lines, startLine, endLine, leaf) {
8007
+ const escapedLeaf = escapeRegex4(leaf);
8008
+ const callablePattern = new RegExp(`\\b${escapedLeaf}\\b\\s*\\(`, "i");
8009
+ const rubyPattern = new RegExp(`\\bdef\\s+${escapedLeaf}\\b`, "i");
8010
+ const candidates = [];
8011
+ const seen = /* @__PURE__ */ new Set();
8012
+ const preferredStart = Math.max(0, Math.min(startLine, lines.length - 1));
8013
+ const preferredEnd = Math.max(preferredStart, Math.min(lines.length - 1, Math.max(endLine, startLine + 4)));
8014
+ for (let lineIndex = preferredStart; lineIndex <= preferredEnd; lineIndex += 1) {
8015
+ const line = lines[lineIndex] ?? "";
8016
+ if ((callablePattern.test(line) || rubyPattern.test(line)) && !seen.has(lineIndex)) {
8017
+ seen.add(lineIndex);
8018
+ candidates.push(lineIndex);
8019
+ }
8020
+ }
8021
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
8022
+ const line = lines[lineIndex] ?? "";
8023
+ if ((callablePattern.test(line) || rubyPattern.test(line)) && !seen.has(lineIndex)) {
8024
+ seen.add(lineIndex);
8025
+ candidates.push(lineIndex);
8026
+ }
8027
+ }
8028
+ return candidates;
8029
+ }
8030
+ function parenBalance(value) {
8031
+ let balance = 0;
8032
+ for (const char of value) {
8033
+ if (char === "(") balance += 1;
8034
+ if (char === ")") balance -= 1;
8035
+ }
8036
+ return balance;
8037
+ }
8038
+ function escapeRegex4(value) {
8039
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8040
+ }
6112
8041
 
6113
8042
  // src/setup.ts
6114
8043
  import {
6115
- existsSync as existsSync7,
8044
+ existsSync as existsSync9,
6116
8045
  mkdirSync as mkdirSync2,
6117
8046
  symlinkSync,
6118
8047
  readlinkSync,
6119
8048
  unlinkSync
6120
8049
  } from "fs";
6121
- import { join as join9, dirname as dirname3, resolve as resolve3 } from "path";
8050
+ import { join as join11, dirname as dirname3, resolve as resolve3 } from "path";
6122
8051
  import { homedir as homedir2, platform as platform3 } from "os";
6123
8052
  import { fileURLToPath } from "url";
6124
8053
  var IS_WINDOWS3 = platform3() === "win32";
6125
- var SKILLS = ["concrete-plan", "scip-explore", "scip-debloat", "scip-verify"];
8054
+ var BUILTIN_SKILLS = [
8055
+ "concrete-plan",
8056
+ "scip-explore",
8057
+ "scip-debloat",
8058
+ "scip-verify",
8059
+ "scip-language-playbook"
8060
+ ];
6126
8061
  function installSkills(opts = {}) {
6127
8062
  const log = opts.quiet ? () => {
6128
8063
  } : console.log;
6129
8064
  const thisFile = fileURLToPath(import.meta.url);
6130
8065
  const skillsSource = resolve3(dirname3(thisFile), "..", "skills");
6131
8066
  const targets = [
6132
- join9(homedir2(), ".claude", "skills"),
6133
- join9(homedir2(), ".codex", "skills")
8067
+ join11(homedir2(), ".claude", "skills"),
8068
+ join11(homedir2(), ".codex", "skills")
6134
8069
  ];
6135
8070
  const result = {
6136
8071
  installed: [],
@@ -6139,19 +8074,19 @@ function installSkills(opts = {}) {
6139
8074
  };
6140
8075
  for (const targetDir of targets) {
6141
8076
  const parentDir = dirname3(targetDir);
6142
- if (!existsSync7(parentDir)) {
8077
+ if (!existsSync9(parentDir)) {
6143
8078
  continue;
6144
8079
  }
6145
8080
  mkdirSync2(targetDir, { recursive: true });
6146
8081
  const toolName = targetDir.includes(".codex") ? "Codex" : "Claude";
6147
- for (const skill of SKILLS) {
6148
- const source = join9(skillsSource, skill);
6149
- const target = join9(targetDir, skill);
6150
- if (!existsSync7(source)) {
8082
+ for (const skill of BUILTIN_SKILLS) {
8083
+ const source = join11(skillsSource, skill);
8084
+ const target = join11(targetDir, skill);
8085
+ if (!existsSync9(source)) {
6151
8086
  result.skipped.push(`${toolName}/${skill}`);
6152
8087
  continue;
6153
8088
  }
6154
- if (existsSync7(target)) {
8089
+ if (existsSync9(target)) {
6155
8090
  try {
6156
8091
  const existing = readlinkSync(target);
6157
8092
  if (resolve3(existing) === resolve3(source)) {
@@ -6183,14 +8118,14 @@ function resolveProjectRoot() {
6183
8118
  function resolveActiveDbPath(projectRoot) {
6184
8119
  const config = loadProjectConfig(projectRoot);
6185
8120
  const paths = resolveIndexPaths(projectRoot, config);
6186
- return process.env["SCIP_QUERY_INDEX_DB"] ?? (existsSync8(paths.dbPath) ? paths.dbPath : join10(projectRoot, "index.db"));
8121
+ return process.env["SCIP_QUERY_INDEX_DB"] ?? (existsSync10(paths.dbPath) ? paths.dbPath : join12(projectRoot, "index.db"));
6187
8122
  }
6188
8123
  function openDb() {
6189
8124
  const projectRoot = resolveProjectRoot();
6190
8125
  const config = loadProjectConfig(projectRoot);
6191
8126
  const paths = resolveIndexPaths(projectRoot, config);
6192
8127
  const dbPath = resolveActiveDbPath(projectRoot);
6193
- if (!existsSync8(dbPath)) {
8128
+ if (!existsSync10(dbPath)) {
6194
8129
  console.error(`error: No index.db found. Run: scip-query reindex`);
6195
8130
  process.exit(1);
6196
8131
  }
@@ -6361,6 +8296,10 @@ program.command("trace <symbol>").description("Trace a symbol: definition + all
6361
8296
  for (const d of result.definitions) {
6362
8297
  const sig = d.signature ? ` \u2014 ${d.signature}` : "";
6363
8298
  console.log(` ${displayPathRange(d.relativePath, d.startLine, d.endLine)}${sig}`);
8299
+ if (d.source) {
8300
+ const numbered = d.source.split("\n").map((line, index) => ` ${displayLine(d.startLine + index)} ${line}`).join("\n");
8301
+ console.log(numbered);
8302
+ }
6364
8303
  }
6365
8304
  console.log("\n\u2550\u2550\u2550 REFERENCED BY \u2550\u2550\u2550");
6366
8305
  let prevFile = "";
@@ -6767,7 +8706,7 @@ ${results.length} similar pair(s) found.`);
6767
8706
  }
6768
8707
  });
6769
8708
  });
6770
- program.command("similar-files [file]").description("Find files with similar dependency profiles").option("--min-similarity <n>", "Minimum Jaccard similarity (0-1)", parseFloat, 0.5).option("-n, --limit <n>", "Number of results", parseIntSafe, 20).option("-s, --scope <path>", "Limit to files matching path").option("--min-deps <n>", "Minimum dependencies to consider", parseIntSafe, 3).action((file, opts) => {
8709
+ program.command("similar-files [file]").description("Find files with similar dependency profiles").option("--min-similarity <n>", "Minimum Jaccard similarity (0-1)", parseFloat, 0.5).option("-n, --limit <n>", "Number of results", parseIntSafe, 20).option("-s, --scope <path>", "Limit to files matching path").option("--min-deps <n>", "Minimum dependencies to consider", parseIntSafe).action((file, opts) => {
6771
8710
  runQuery(
6772
8711
  (db) => queries.similarFiles(db, {
6773
8712
  minSimilarity: opts.minSimilarity,
@@ -7167,7 +9106,7 @@ ${result.connectedSymbols.length} connected symbol(s).`);
7167
9106
  }
7168
9107
  db.close();
7169
9108
  });
7170
- program.command("install-skills").description("Install skills (concrete-plan, scip-explore, scip-debloat, scip-verify) into Claude Code and Codex").action(() => {
9109
+ program.command("install-skills").description(`Install skills (${BUILTIN_SKILLS.join(", ")}) into Claude Code and Codex`).action(() => {
7171
9110
  const result = installSkills();
7172
9111
  const total = result.installed.length + result.alreadyLinked.length;
7173
9112
  console.log(`
@@ -7176,12 +9115,41 @@ ${result.installed.length} installed, ${result.alreadyLinked.length} already lin
7176
9115
  console.log("Skills will be available in your next Claude Code / Codex session.");
7177
9116
  }
7178
9117
  });
7179
- program.command("check-deps").description("Check if required dependencies (scip CLI) are installed").action(() => {
9118
+ program.command("check-deps").description("Check whether scip-query and the detected language indexers are actually runnable").action(() => {
9119
+ let hasProblems = false;
7180
9120
  if (isScipInstalled()) {
7181
9121
  console.log("scip CLI: installed");
7182
9122
  } else {
7183
9123
  printScipInstallInstructions();
9124
+ hasProblems = true;
9125
+ }
9126
+ const projectRoot = resolveProjectRoot();
9127
+ const config = loadProjectConfig(projectRoot);
9128
+ const languages = config.languages ?? detectLanguages(projectRoot);
9129
+ if (languages.length === 0) {
9130
+ console.log("\nNo supported project languages detected in the current directory.");
9131
+ process.exitCode = hasProblems ? 1 : 0;
9132
+ return;
9133
+ }
9134
+ console.log(`
9135
+ Detected languages: ${languages.join(", ")}`);
9136
+ console.log("\nIndexer readiness:");
9137
+ for (const language of languages) {
9138
+ const status = getIndexerDependencyStatus(getIndexerConfig(language), projectRoot);
9139
+ const prefix = status.runnable ? " OK" : status.installed ? " WARN" : " MISSING";
9140
+ const resolved = status.resolvedBinary ? ` (${status.resolvedBinary})` : "";
9141
+ console.log(`${prefix} ${language}: ${status.binaryLabel}${resolved}`);
9142
+ if (status.note) {
9143
+ console.log(` ${status.note}`);
9144
+ }
9145
+ if (!status.installed && status.installUrl) {
9146
+ console.log(` install: ${status.installUrl}`);
9147
+ }
9148
+ if (!status.runnable) {
9149
+ hasProblems = true;
9150
+ }
7184
9151
  }
9152
+ process.exitCode = hasProblems ? 1 : 0;
7185
9153
  });
7186
9154
  program.command("redundant-reexports").description("Find barrel re-exports that nobody imports through").option("-s, --scope <path>", "Limit to files matching path").option("-n, --limit <n>", "Number of results", parseIntSafe, 30).action((opts) => {
7187
9155
  const db = openDb();
@@ -7269,8 +9237,8 @@ program.command("status").description("Show index status for this project").acti
7269
9237
  if (dbPath !== paths.dbPath) {
7270
9238
  console.log(`Config: ${paths.dbPath} (fallback to project root index.db)`);
7271
9239
  }
7272
- console.log(`Exists: ${existsSync8(dbPath) ? "yes" : "no"}`);
7273
- if (existsSync8(dbPath)) {
9240
+ console.log(`Exists: ${existsSync10(dbPath) ? "yes" : "no"}`);
9241
+ if (existsSync10(dbPath)) {
7274
9242
  withDb((db) => {
7275
9243
  const s = queries.stats(db);
7276
9244
  console.log(`Symbols: ${s.symbols}`);