scip-query 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +1 -0
  2. package/dist/{chunk-6FKIA6EI.js → chunk-2F2WH5WQ.js} +12 -12
  3. package/dist/{chunk-C7H5WBTJ.js → chunk-2UISVZGQ.js} +2 -2
  4. package/dist/{chunk-EN2Z2CLO.js → chunk-4ZT7UGWW.js} +16 -19
  5. package/dist/chunk-5AJJGPZE.js +60 -0
  6. package/dist/chunk-5RKYZSQ6.js +75 -0
  7. package/dist/chunk-7YBLWIXY.js +115 -0
  8. package/dist/{chunk-O7Q7FDUJ.js → chunk-A4GWYETB.js} +2 -2
  9. package/dist/{chunk-CHDJXYBG.js → chunk-A5BGEBM7.js} +2 -2
  10. package/dist/{chunk-KKCHYLVI.js → chunk-A7YY7IDA.js} +2 -2
  11. package/dist/chunk-AS7N27JK.js +115 -0
  12. package/dist/{chunk-F7XU27LU.js → chunk-CQRYLK33.js} +26 -2
  13. package/dist/{chunk-NFS5W3PP.js → chunk-CQUNEJYM.js} +2 -2
  14. package/dist/chunk-D4I3ZMN5.js +38 -0
  15. package/dist/{chunk-J3JSOSUO.js → chunk-E7J7Q7UW.js} +2 -2
  16. package/dist/{chunk-GEXE2T6I.js → chunk-EOHPASDV.js} +22 -14
  17. package/dist/chunk-GIBETK3W.js +37 -0
  18. package/dist/{chunk-SMDCNPMK.js → chunk-H3FPW5YN.js} +2 -2
  19. package/dist/{chunk-VMM4SYV4.js → chunk-HNURMDF4.js} +16 -3
  20. package/dist/{chunk-7KIMF5PV.js → chunk-HRDPUTIQ.js} +2 -2
  21. package/dist/{chunk-OIDHN6GD.js → chunk-I2JM34UV.js} +146 -5
  22. package/dist/{chunk-EPWLXXBL.js → chunk-IV6NZ426.js} +27 -29
  23. package/dist/{chunk-VT4JBH6L.js → chunk-KDCQJTYW.js} +2 -2
  24. package/dist/{chunk-5OMVSV6E.js → chunk-LOVDB4C6.js} +2 -2
  25. package/dist/chunk-MA3B3IUT.js +75 -0
  26. package/dist/{chunk-26DOJ63W.js → chunk-N2LH3M2P.js} +13 -2
  27. package/dist/chunk-NWCJWA36.js +162 -0
  28. package/dist/{chunk-NG5F43OU.js → chunk-P3VCDYMJ.js} +70 -1
  29. package/dist/{chunk-P3E6L7KW.js → chunk-P42KQKJZ.js} +83 -3
  30. package/dist/{chunk-YDBXNPYU.js → chunk-PGHN5UTM.js} +2 -2
  31. package/dist/{chunk-UGQKAVCD.js → chunk-QCYR4S6T.js} +2 -2
  32. package/dist/chunk-QGCEAVJD.js +2529 -0
  33. package/dist/{chunk-KBOQX573.js → chunk-RIEA5DOB.js} +19 -2
  34. package/dist/{chunk-VIYSWZCO.js → chunk-SL674KAW.js} +31 -3
  35. package/dist/chunk-SRELHCMG.js +110 -0
  36. package/dist/chunk-UTRKBUCB.js +87 -0
  37. package/dist/{chunk-TRESG7OB.js → chunk-VCOJRQPP.js} +2 -2
  38. package/dist/chunk-VISMEWYP.js +34 -0
  39. package/dist/{chunk-LFJQVJYJ.js → chunk-VU7FDTWV.js} +2 -2
  40. package/dist/{chunk-HLUS2HEB.js → chunk-VUBLUTMU.js} +5 -4
  41. package/dist/{chunk-WGAD3GNR.js → chunk-WNPF2I25.js} +5 -5
  42. package/dist/{chunk-YY4QGUQ5.js → chunk-X3J4VPWM.js} +2 -2
  43. package/dist/{chunk-GSH2FPKV.js → chunk-XH56HXLC.js} +2 -2
  44. package/dist/{chunk-DH7G3DDV.js → chunk-ZU2AQQB5.js} +2 -2
  45. package/dist/cli.js +2607 -805
  46. package/dist/{db-viWlyVtv.d.ts → db-C4rPbKkI.d.ts} +6 -1
  47. package/dist/index.d.ts +4 -4
  48. package/dist/index.js +371 -138
  49. package/dist/postinstall.js +9 -3
  50. package/dist/queries/affected.d.ts +1 -1
  51. package/dist/queries/affected.js +2 -2
  52. package/dist/queries/bottlenecks.d.ts +1 -1
  53. package/dist/queries/bottlenecks.js +2 -1
  54. package/dist/queries/by-kind.d.ts +1 -1
  55. package/dist/queries/by-kind.js +2 -1
  56. package/dist/queries/call-graph.d.ts +1 -1
  57. package/dist/queries/call-graph.js +2 -2
  58. package/dist/queries/change-surface.d.ts +1 -1
  59. package/dist/queries/change-surface.js +2 -2
  60. package/dist/queries/code.d.ts +1 -1
  61. package/dist/queries/code.js +2 -2
  62. package/dist/queries/complexity-hotspots.d.ts +1 -1
  63. package/dist/queries/complexity-hotspots.js +2 -2
  64. package/dist/queries/complexity.d.ts +1 -1
  65. package/dist/queries/complexity.js +2 -2
  66. package/dist/queries/convergence.d.ts +1 -1
  67. package/dist/queries/convergence.js +2 -2
  68. package/dist/queries/coupling.d.ts +1 -1
  69. package/dist/queries/coupling.js +2 -2
  70. package/dist/queries/cycles.d.ts +1 -1
  71. package/dist/queries/cycles.js +2 -2
  72. package/dist/queries/dataflow.d.ts +1 -1
  73. package/dist/queries/dataflow.js +2 -2
  74. package/dist/queries/dead.d.ts +1 -1
  75. package/dist/queries/dead.js +3 -3
  76. package/dist/queries/deep-chains.d.ts +1 -1
  77. package/dist/queries/deep-chains.js +2 -2
  78. package/dist/queries/deps.d.ts +1 -1
  79. package/dist/queries/deps.js +2 -2
  80. package/dist/queries/diff-impact.d.ts +1 -1
  81. package/dist/queries/drift.d.ts +1 -1
  82. package/dist/queries/drift.js +2 -2
  83. package/dist/queries/extract-candidates.d.ts +1 -1
  84. package/dist/queries/extract-candidates.js +2 -2
  85. package/dist/queries/fan.d.ts +1 -1
  86. package/dist/queries/fan.js +2 -2
  87. package/dist/queries/files.d.ts +1 -1
  88. package/dist/queries/health.d.ts +1 -1
  89. package/dist/queries/health.js +13 -13
  90. package/dist/queries/hierarchy.d.ts +1 -1
  91. package/dist/queries/hierarchy.js +2 -2
  92. package/dist/queries/hotspots.d.ts +1 -1
  93. package/dist/queries/hotspots.js +2 -1
  94. package/dist/queries/imports.d.ts +1 -1
  95. package/dist/queries/imports.js +2 -2
  96. package/dist/queries/index.d.ts +1 -1
  97. package/dist/queries/index.js +42 -42
  98. package/dist/queries/isolated.d.ts +3 -4
  99. package/dist/queries/isolated.js +3 -3
  100. package/dist/queries/members.d.ts +1 -1
  101. package/dist/queries/members.js +2 -2
  102. package/dist/queries/methods.d.ts +1 -1
  103. package/dist/queries/methods.js +2 -1
  104. package/dist/queries/outline.d.ts +1 -1
  105. package/dist/queries/outline.js +2 -2
  106. package/dist/queries/passthrough-candidates.d.ts +1 -1
  107. package/dist/queries/passthrough-candidates.js +2 -2
  108. package/dist/queries/redundant-reexports.d.ts +1 -1
  109. package/dist/queries/redundant-reexports.js +3 -3
  110. package/dist/queries/refs.d.ts +1 -1
  111. package/dist/queries/refs.js +2 -2
  112. package/dist/queries/similar-chains.d.ts +1 -1
  113. package/dist/queries/similar-chains.js +2 -2
  114. package/dist/queries/similar-files.d.ts +1 -1
  115. package/dist/queries/similar-files.js +2 -2
  116. package/dist/queries/similar-signatures.d.ts +5 -3
  117. package/dist/queries/similar-signatures.js +2 -1
  118. package/dist/queries/similar.d.ts +1 -1
  119. package/dist/queries/similar.js +2 -2
  120. package/dist/queries/slice.d.ts +1 -1
  121. package/dist/queries/slice.js +2 -2
  122. package/dist/queries/stale-abstractions.d.ts +1 -1
  123. package/dist/queries/stale-abstractions.js +2 -2
  124. package/dist/queries/stats.d.ts +1 -1
  125. package/dist/queries/surface.d.ts +1 -1
  126. package/dist/queries/surface.js +2 -2
  127. package/dist/queries/symbols.d.ts +1 -1
  128. package/dist/queries/symbols.js +2 -2
  129. package/dist/queries/system.d.ts +1 -1
  130. package/dist/queries/system.js +2 -2
  131. package/dist/queries/trace.d.ts +1 -1
  132. package/dist/queries/trace.js +2 -2
  133. package/dist/queries/wrapper-candidates.d.ts +1 -1
  134. package/dist/queries/wrapper-candidates.js +2 -2
  135. package/dist/reindex-worker.js +213 -62
  136. package/package.json +1 -1
  137. package/skills/scip-language-playbook/SKILL.md +371 -0
  138. package/dist/chunk-4JCSOF2O.js +0 -97
  139. package/dist/chunk-AXQKUYKF.js +0 -1442
  140. package/dist/chunk-CPVAQJEC.js +0 -46
  141. package/dist/chunk-EOROMIFO.js +0 -41
  142. package/dist/chunk-GU2H5QRN.js +0 -28
  143. package/dist/chunk-LQJUPXQY.js +0 -109
  144. package/dist/chunk-MPGIHELS.js +0 -39
  145. package/dist/chunk-TOIEB3LG.js +0 -78
  146. package/dist/chunk-UQEQ6AHX.js +0 -60
  147. package/dist/chunk-VJJKSGIX.js +0 -121
  148. package/dist/chunk-YGGFLMTM.js +0 -83
  149. 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
  });
@@ -780,9 +1051,10 @@ async function reindex(opts) {
780
1051
  Make sure ${binaryLabel} is installed and available on PATH.`
781
1052
  );
782
1053
  }
1054
+ moveDefaultOutputIfNeeded(config, projectRoot, outputScip);
783
1055
  }
784
1056
  onStatus("Converting to SQLite...");
785
- if (!existsSync4(outputScip)) {
1057
+ if (!existsSync6(outputScip)) {
786
1058
  throw new Error(`SCIP index not found at ${outputScip} after indexing`);
787
1059
  }
788
1060
  try {
@@ -799,11 +1071,20 @@ Make sure ${binaryLabel} is installed and available on PATH.`
799
1071
  onStatus(`Done in ${(durationMs / 1e3).toFixed(1)}s`);
800
1072
  return { languages, indexPath: outputScip, dbPath: outputDb, durationMs };
801
1073
  }
1074
+ function moveDefaultOutputIfNeeded(config, projectRoot, outputScip) {
1075
+ if (!config.defaultOutputPath) {
1076
+ return;
1077
+ }
1078
+ const defaultOutputPath = join6(projectRoot, config.defaultOutputPath);
1079
+ if (outputScip !== defaultOutputPath && existsSync6(defaultOutputPath)) {
1080
+ renameSync(defaultOutputPath, outputScip);
1081
+ }
1082
+ }
802
1083
 
803
1084
  // src/watch.ts
804
1085
  import { watch } from "fs";
805
- import { existsSync as existsSync5, renameSync } from "fs";
806
- import { join as join5, relative } from "path";
1086
+ import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
1087
+ import { join as join7, relative } from "path";
807
1088
  import { fork } from "child_process";
808
1089
  import ignore2 from "ignore";
809
1090
  var Watcher = class {
@@ -881,7 +1162,7 @@ var Watcher = class {
881
1162
  }
882
1163
  // ── Internal ─────────────────────────────────────────────
883
1164
  handleFileChange(filename) {
884
- const rel = relative(this.projectRoot, join5(this.projectRoot, filename));
1165
+ const rel = relative(this.projectRoot, join7(this.projectRoot, filename));
885
1166
  if (this.gitignoreFilter.isIgnored(rel)) return;
886
1167
  if (this.extraIgnore.ignores(rel)) return;
887
1168
  if (filename.endsWith("index.db") || filename.endsWith("index.scip") || filename.endsWith("index.db.tmp") || filename.endsWith(".scipquery.json")) {
@@ -984,11 +1265,11 @@ var Watcher = class {
984
1265
  child.on("exit", (code2) => {
985
1266
  if (code2 === 0) {
986
1267
  try {
987
- if (existsSync5(tmpDb)) {
988
- renameSync(tmpDb, this.indexPaths.dbPath);
1268
+ if (existsSync7(tmpDb)) {
1269
+ renameSync2(tmpDb, this.indexPaths.dbPath);
989
1270
  }
990
- if (existsSync5(tmpScip)) {
991
- renameSync(tmpScip, this.indexPaths.indexPath);
1271
+ if (existsSync7(tmpScip)) {
1272
+ renameSync2(tmpScip, this.indexPaths.indexPath);
992
1273
  }
993
1274
  resolve4(Date.now() - start);
994
1275
  } catch (err) {
@@ -1042,27 +1323,36 @@ function files(db, pattern) {
1042
1323
  }
1043
1324
 
1044
1325
  // src/query-support.ts
1045
- import { basename } from "path";
1326
+ import { basename as basename2 } from "path";
1046
1327
 
1047
1328
  // src/source-analysis.ts
1048
1329
  import {
1049
- existsSync as existsSync6,
1330
+ existsSync as existsSync8,
1050
1331
  readFileSync as readFileSync4
1051
1332
  } from "fs";
1052
1333
  import {
1334
+ basename,
1053
1335
  dirname as dirname2,
1054
- extname,
1055
- join as join6,
1336
+ extname as extname2,
1337
+ join as join8,
1056
1338
  relative as relative2,
1057
1339
  resolve as resolve2
1058
1340
  } from "path";
1059
1341
  var SOURCE_IMPORT_CACHE = /* @__PURE__ */ new WeakMap();
1342
+ var SOURCE_EXPORT_CACHE = /* @__PURE__ */ new WeakMap();
1060
1343
  var SOURCE_TEXT_CACHE = /* @__PURE__ */ new WeakMap();
1061
1344
  var SOURCE_CALL_CACHE = /* @__PURE__ */ new WeakMap();
1062
1345
  var SOURCE_BINDING_CACHE = /* @__PURE__ */ new WeakMap();
1063
1346
  var INDEXED_PATH_CACHE = /* @__PURE__ */ new WeakMap();
1064
1347
  var SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1065
1348
  var PYTHON_SOURCE_EXTENSIONS = [".py", ".pyi"];
1349
+ var JVM_SOURCE_EXTENSIONS = [".java", ".scala", ".kt", ".kts"];
1350
+ var RUST_SOURCE_EXTENSIONS = [".rs"];
1351
+ var RUBY_SOURCE_EXTENSIONS = [".rb"];
1352
+ var C_LIKE_SOURCE_EXTENSIONS = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh", ".hxx"];
1353
+ var DOTNET_SOURCE_EXTENSIONS = [".cs", ".vb"];
1354
+ var DART_SOURCE_EXTENSIONS = [".dart"];
1355
+ var PHP_SOURCE_EXTENSIONS = [".php"];
1066
1356
  function getSourceImports(db, relativePath) {
1067
1357
  const cache = getCachedMap(SOURCE_IMPORT_CACHE, db);
1068
1358
  const normalized = normalizePath(relativePath);
@@ -1070,13 +1360,30 @@ function getSourceImports(db, relativePath) {
1070
1360
  if (cached) {
1071
1361
  return cached;
1072
1362
  }
1073
- const fullPath = join6(db.config.projectRoot, normalized);
1074
- if (!existsSync6(fullPath)) {
1363
+ const fullPath = join8(db.config.projectRoot, normalized);
1364
+ if (!existsSync8(fullPath)) {
1365
+ cache.set(normalized, []);
1366
+ return [];
1367
+ }
1368
+ const source = readFileSync4(fullPath, "utf-8");
1369
+ 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) : [];
1370
+ cache.set(normalized, parsed);
1371
+ return parsed;
1372
+ }
1373
+ function getSourceExports(db, relativePath) {
1374
+ const cache = getCachedMap(SOURCE_EXPORT_CACHE, db);
1375
+ const normalized = normalizePath(relativePath);
1376
+ const cached = cache.get(normalized);
1377
+ if (cached) {
1378
+ return cached;
1379
+ }
1380
+ const fullPath = join8(db.config.projectRoot, normalized);
1381
+ if (!existsSync8(fullPath)) {
1075
1382
  cache.set(normalized, []);
1076
1383
  return [];
1077
1384
  }
1078
1385
  const source = readFileSync4(fullPath, "utf-8");
1079
- const parsed = isPythonSourcePath(normalized) ? parsePythonImports(db, normalized, source) : parseJavaScriptImports(db, normalized, source);
1386
+ const parsed = isDartSourcePath(normalized) ? parseDartExports(db, normalized, source) : isRustSourcePath(normalized) ? parseRustExports(db, normalized, source) : [];
1080
1387
  cache.set(normalized, parsed);
1081
1388
  return parsed;
1082
1389
  }
@@ -1225,96 +1532,357 @@ function parseJavaScriptImportStatement(db, importerPath, clause, specifier, sta
1225
1532
  };
1226
1533
  });
1227
1534
  }
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
- }
1535
+ function parseJvmImports(db, importerPath, source) {
1536
+ const statements = [];
1537
+ for (const match of source.matchAll(/^[ \t]*import\s+(?:static\s+)?(.+?)\s*;?$/gm)) {
1538
+ const clause = match[1]?.trim();
1539
+ const full = match[0];
1540
+ if (!clause || !full || typeof match.index !== "number") continue;
1541
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1542
+ statements.push(...parseJvmImportClause(db, importerPath, clause, body));
1280
1543
  }
1281
- return calls;
1544
+ return statements;
1282
1545
  }
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
- }
1546
+ function parseJvmImportClause(db, importerPath, clause, body) {
1547
+ if (clause.includes("{") && clause.includes("}")) {
1548
+ const prefix = clause.slice(0, clause.indexOf("{")).replace(/\.$/, "").trim();
1549
+ const inner = clause.slice(clause.indexOf("{") + 1, clause.lastIndexOf("}")).trim();
1550
+ return splitTopLevel(inner).flatMap((entry) => {
1551
+ const cleaned = entry.trim();
1552
+ if (!cleaned) return [];
1553
+ const [importedPart, aliasPart] = cleaned.includes("=>") ? cleaned.split(/\s*=>\s*/) : cleaned.split(/\s+as\s+/);
1554
+ const importedName = importedPart?.trim();
1555
+ if (!importedName || importedName === "_") return [];
1556
+ const localName = (aliasPart ?? importedName.split(".").pop() ?? importedName).trim();
1557
+ const qualified = importedName === "_" ? prefix : `${prefix}.${importedName}`.replace(/\.\./g, ".");
1558
+ return [buildSimpleImport(db, importerPath, body, qualified, importedName, localName)];
1559
+ });
1560
+ }
1561
+ return [buildSimpleImport(
1562
+ db,
1563
+ importerPath,
1564
+ body,
1565
+ clause,
1566
+ clause.split(".").pop() ?? clause,
1567
+ clause.split(".").pop() ?? clause
1568
+ )];
1569
+ }
1570
+ function parseRustImports(db, importerPath, source) {
1571
+ const statements = [];
1572
+ for (const match of source.matchAll(/^[ \t]*use\s+(.+?)\s*;$/gm)) {
1573
+ const clause = match[1]?.trim();
1574
+ const full = match[0];
1575
+ if (!clause || !full || typeof match.index !== "number") continue;
1576
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1577
+ statements.push(...parseRustUseClause(db, importerPath, clause, body));
1578
+ }
1579
+ return statements;
1580
+ }
1581
+ function parseRustUseClause(db, importerPath, clause, body) {
1582
+ const trimmed = clause.trim();
1583
+ if (trimmed.includes("{") && trimmed.includes("}")) {
1584
+ const prefix = trimmed.slice(0, trimmed.indexOf("{")).replace(/::$/, "").trim();
1585
+ const inner = trimmed.slice(trimmed.indexOf("{") + 1, trimmed.lastIndexOf("}")).trim();
1586
+ return splitTopLevel(inner).flatMap((entry) => {
1587
+ const cleaned = entry.trim();
1588
+ if (!cleaned || cleaned === "self") return [];
1589
+ const [importedPart2, aliasPart2] = cleaned.split(/\s+as\s+/);
1590
+ const importedName2 = importedPart2?.trim();
1591
+ if (!importedName2) return [];
1592
+ const localName2 = (aliasPart2 ?? importedName2.split("::").pop() ?? importedName2).trim();
1593
+ const moduleSpecifier = `${prefix}::${importedName2}`.replace(/::::/g, "::");
1594
+ return [buildSimpleImport(
1595
+ db,
1596
+ importerPath,
1597
+ body,
1598
+ moduleSpecifier,
1599
+ importedName2.split("::").pop() ?? importedName2,
1600
+ localName2,
1601
+ resolveRustImportPath(db, importerPath, prefix)
1602
+ )];
1603
+ });
1604
+ }
1605
+ const [importedPart, aliasPart] = trimmed.split(/\s+as\s+/);
1606
+ const importedName = importedPart?.trim() ?? trimmed;
1607
+ const localName = (aliasPart ?? importedName.split("::").pop() ?? importedName).trim();
1608
+ const resolved = resolveRustImportPath(db, importerPath, importedName) ?? resolveRustImportPath(db, importerPath, importedName.split("::").slice(0, -1).join("::"));
1609
+ return [buildSimpleImport(
1610
+ db,
1611
+ importerPath,
1612
+ body,
1613
+ importedName,
1614
+ importedName.split("::").pop() ?? importedName,
1615
+ localName,
1616
+ resolved
1617
+ )];
1618
+ }
1619
+ function parseRustExports(db, importerPath, source) {
1620
+ const statements = [];
1621
+ for (const match of source.matchAll(/^[ \t]*pub\s+use\s+(.+?)\s*;$/gm)) {
1622
+ const clause = match[1]?.trim();
1623
+ if (!clause) continue;
1624
+ statements.push(...parseRustExportClause(db, importerPath, clause));
1625
+ }
1626
+ return statements;
1627
+ }
1628
+ function parseRustExportClause(db, importerPath, clause) {
1629
+ const trimmed = clause.trim();
1630
+ if (trimmed.includes("{") && trimmed.includes("}")) {
1631
+ const prefix = trimmed.slice(0, trimmed.indexOf("{")).replace(/::$/, "").trim();
1632
+ const inner = trimmed.slice(trimmed.indexOf("{") + 1, trimmed.lastIndexOf("}")).trim();
1633
+ return splitTopLevel(inner).flatMap((entry) => {
1634
+ const cleaned = entry.trim();
1635
+ if (!cleaned || cleaned === "self") return [];
1636
+ const [qualifiedPart] = cleaned.split(/\s+as\s+/);
1637
+ const qualified = `${prefix}::${qualifiedPart?.trim() ?? cleaned}`.replace(/::::/g, "::");
1638
+ return [buildRustExport(db, importerPath, qualified)];
1639
+ });
1640
+ }
1641
+ return [buildRustExport(db, importerPath, trimmed)];
1642
+ }
1643
+ function buildRustExport(db, importerPath, specifier) {
1644
+ return {
1645
+ specifier,
1646
+ sourcePath: resolveRustImportPath(db, importerPath, specifier) ?? resolveRustImportPath(db, importerPath, specifier.split("::").slice(0, -1).join("::"))
1647
+ };
1648
+ }
1649
+ function parseRubyImports(db, importerPath, source) {
1650
+ const statements = [];
1651
+ for (const match of source.matchAll(/^[ \t]*(require_relative|require)\s+["']([^"']+)["']\s*$/gm)) {
1652
+ const kind = match[1];
1653
+ const specifier = match[2];
1654
+ const full = match[0];
1655
+ if (!kind || !specifier || !full || typeof match.index !== "number") continue;
1656
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1657
+ const sourcePath = kind === "require_relative" ? resolveRubyImportPath(db, importerPath, specifier) : null;
1658
+ if (sourcePath) {
1659
+ const localName = rubyConstantName(specifier);
1660
+ statements.push({
1661
+ importedName: localName,
1662
+ localName,
1663
+ sourcePath,
1664
+ kind: "named",
1665
+ used: hasIdentifierUsage(body, localName),
1666
+ usedMembers: []
1667
+ });
1668
+ continue;
1669
+ }
1670
+ statements.push({
1671
+ importedName: specifier,
1672
+ localName: null,
1673
+ sourcePath,
1674
+ kind: "side-effect",
1675
+ used: true,
1676
+ usedMembers: []
1677
+ });
1678
+ }
1679
+ return statements;
1680
+ }
1681
+ function rubyConstantName(specifier) {
1682
+ return basename(specifier).replace(/\.[^.]+$/, "").split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1683
+ }
1684
+ function parseCLikeImports(db, importerPath, source) {
1685
+ const statements = [];
1686
+ for (const match of source.matchAll(/^[ \t]*#include\s+[<"]([^">]+)[">]\s*$/gm)) {
1687
+ const specifier = match[1]?.trim();
1688
+ const full = match[0];
1689
+ if (!specifier || !full || typeof match.index !== "number") continue;
1690
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1691
+ const localName = basename(specifier).replace(/\.[^.]+$/, "");
1692
+ statements.push({
1693
+ importedName: specifier,
1694
+ localName,
1695
+ sourcePath: resolveCLikeImportPath(db, importerPath, specifier),
1696
+ kind: "named",
1697
+ used: hasIdentifierUsage(body, localName),
1698
+ usedMembers: []
1699
+ });
1700
+ }
1701
+ return statements;
1702
+ }
1703
+ function parseDotNetImports(db, importerPath, source) {
1704
+ const statements = [];
1705
+ const lineRegex = isVisualBasicSourcePath(importerPath) ? /^[ \t]*Imports\s+(.+?)\s*$/gm : /^[ \t]*using\s+(.+?)\s*;$/gm;
1706
+ for (const match of source.matchAll(lineRegex)) {
1707
+ const clause = match[1]?.trim();
1708
+ const full = match[0];
1709
+ if (!clause || !full || typeof match.index !== "number") continue;
1710
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1711
+ const [aliasPart, targetPart] = isVisualBasicSourcePath(importerPath) ? clause.split(/\s*=\s*/) : clause.split(/\s*=\s*/);
1712
+ const hasAlias = Boolean(targetPart);
1713
+ const qualified = (hasAlias ? targetPart : aliasPart)?.trim() ?? clause;
1714
+ const importedName = qualified.split(".").pop() ?? qualified;
1715
+ const localName = hasAlias ? aliasPart?.trim() ?? importedName : importedName;
1716
+ statements.push(buildSimpleImport(
1717
+ db,
1718
+ importerPath,
1719
+ body,
1720
+ qualified,
1721
+ importedName,
1722
+ localName,
1723
+ resolveQualifiedImportPath(db, qualified, DOTNET_SOURCE_EXTENSIONS)
1724
+ ));
1725
+ }
1726
+ return statements;
1727
+ }
1728
+ function parseDartImports(db, importerPath, source) {
1729
+ const statements = [];
1730
+ for (const match of source.matchAll(/^[ \t]*import\s+['"]([^'"]+)['"](?:\s+as\s+([A-Za-z_]\w*))?[\s\S]*?;$/gm)) {
1731
+ const specifier = match[1]?.trim();
1732
+ const alias = match[2]?.trim() ?? null;
1733
+ const full = match[0];
1734
+ if (!specifier || !full || typeof match.index !== "number") continue;
1735
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1736
+ statements.push({
1737
+ importedName: specifier,
1738
+ localName: alias,
1739
+ sourcePath: resolveDartImportPath(db, importerPath, specifier),
1740
+ kind: alias ? "namespace" : "side-effect",
1741
+ used: alias ? hasIdentifierUsage(body, alias) : true,
1742
+ usedMembers: alias ? collectNamespaceMembers(body, alias) : []
1743
+ });
1744
+ }
1745
+ return statements;
1746
+ }
1747
+ function parseDartExports(db, importerPath, source) {
1748
+ const statements = [];
1749
+ for (const match of source.matchAll(/^[ \t]*export\s+['"]([^'"]+)['"][\s\S]*?;$/gm)) {
1750
+ const specifier = match[1]?.trim();
1751
+ if (!specifier) continue;
1752
+ statements.push({
1753
+ specifier,
1754
+ sourcePath: resolveDartImportPath(db, importerPath, specifier)
1755
+ });
1756
+ }
1757
+ return statements;
1758
+ }
1759
+ function parsePhpImports(db, importerPath, source) {
1760
+ const statements = [];
1761
+ for (const match of source.matchAll(/^[ \t]*use\s+(.+?)\s*;$/gm)) {
1762
+ const clause = match[1]?.trim();
1763
+ const full = match[0];
1764
+ if (!clause || !full || typeof match.index !== "number") continue;
1765
+ const body = buildUsageBody(source, match.index, match.index + full.length);
1766
+ for (const entry of splitTopLevel(clause)) {
1767
+ const cleaned = entry.trim();
1768
+ if (!cleaned) continue;
1769
+ const [qualifiedPart, aliasPart] = cleaned.split(/\s+as\s+/i);
1770
+ const qualified = qualifiedPart?.trim() ?? cleaned;
1771
+ const importedName = qualified.split("\\").pop() ?? qualified;
1772
+ const localName = (aliasPart ?? importedName).trim();
1773
+ statements.push(buildSimpleImport(
1774
+ db,
1775
+ importerPath,
1776
+ body,
1777
+ qualified,
1778
+ importedName,
1779
+ localName,
1780
+ resolveQualifiedImportPath(db, qualified.replace(/\\/g, "."), PHP_SOURCE_EXTENSIONS)
1781
+ ));
1782
+ }
1783
+ }
1784
+ return statements;
1785
+ }
1786
+ function buildSimpleImport(db, importerPath, body, qualifiedName, importedName, localName, sourcePath) {
1787
+ return {
1788
+ importedName,
1789
+ localName,
1790
+ sourcePath: sourcePath ?? resolveQualifiedImportPath(db, qualifiedName, extensionFamilyFor(importerPath)),
1791
+ kind: "named",
1792
+ used: hasIdentifierUsage(body, localName),
1793
+ usedMembers: []
1794
+ };
1795
+ }
1796
+ function parsePythonCalls(lines, baseLine) {
1797
+ const calls = [];
1798
+ const controlKeywords = /* @__PURE__ */ new Set([
1799
+ "if",
1800
+ "for",
1801
+ "while",
1802
+ "with",
1803
+ "except",
1804
+ "elif",
1805
+ "return",
1806
+ "yield",
1807
+ "assert",
1808
+ "raise",
1809
+ "lambda",
1810
+ "class",
1811
+ "def"
1812
+ ]);
1813
+ for (let index = 0; index < lines.length; index++) {
1814
+ const rawLine = lines[index] ?? "";
1815
+ const stripped = stripCommentsAndStrings(rawLine);
1816
+ if (!stripped.trim()) continue;
1817
+ const attributeMatches = [...stripped.matchAll(/\b([A-Za-z_][\w]*)\s*\.\s*([A-Za-z_][\w]*)\s*\(/g)];
1818
+ const attributeRanges = attributeMatches.map((match) => ({
1819
+ start: match.index ?? -1,
1820
+ end: (match.index ?? -1) + match[0].length
1821
+ }));
1822
+ for (const match of attributeMatches) {
1823
+ const receiverName = match[1];
1824
+ const calleeName = match[2];
1825
+ if (!receiverName || !calleeName) continue;
1826
+ calls.push({
1827
+ receiverName,
1828
+ calleeName,
1829
+ line: baseLine + index
1830
+ });
1831
+ }
1832
+ for (const match of stripped.matchAll(/\b([A-Za-z_][\w]*)\s*\(/g)) {
1833
+ const calleeName = match[1];
1834
+ const start = match.index ?? -1;
1835
+ if (!calleeName || start < 0) continue;
1836
+ if (controlKeywords.has(calleeName)) continue;
1837
+ if (attributeRanges.some((range) => start >= range.start && start < range.end)) continue;
1838
+ const prefix = stripped.slice(0, start).trimEnd();
1839
+ if (prefix.endsWith("def") || prefix.endsWith("class") || prefix.endsWith("async def")) {
1840
+ continue;
1841
+ }
1842
+ calls.push({
1843
+ receiverName: null,
1844
+ calleeName,
1845
+ line: baseLine + index
1846
+ });
1847
+ }
1848
+ }
1849
+ return calls;
1850
+ }
1851
+ function parseJavaScriptCalls(lines, baseLine) {
1852
+ const calls = [];
1853
+ const controlKeywords = /* @__PURE__ */ new Set([
1854
+ "if",
1855
+ "for",
1856
+ "while",
1857
+ "switch",
1858
+ "catch",
1859
+ "function",
1860
+ "class",
1861
+ "return",
1862
+ "typeof",
1863
+ "import"
1864
+ ]);
1865
+ for (let index = 0; index < lines.length; index++) {
1866
+ const stripped = stripCommentsAndStrings(lines[index] ?? "");
1867
+ if (!stripped.trim()) continue;
1868
+ const attributeMatches = [
1869
+ ...stripped.matchAll(/\b([A-Za-z_$][\w$]*)\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*\(/g),
1870
+ ...stripped.matchAll(/\bthis\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)\s*\(/g)
1871
+ ];
1872
+ const attributeRanges = attributeMatches.map((match) => ({
1873
+ start: match.index ?? -1,
1874
+ end: (match.index ?? -1) + match[0].length
1875
+ }));
1876
+ for (const match of attributeMatches) {
1877
+ const receiverName = match[2] ? match[1] : "this";
1878
+ const calleeName = match[2] ?? match[1];
1879
+ if (!receiverName || !calleeName) continue;
1880
+ calls.push({
1881
+ receiverName,
1882
+ calleeName,
1883
+ line: baseLine + index
1884
+ });
1885
+ }
1318
1886
  for (const match of stripped.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
1319
1887
  const calleeName = match[1];
1320
1888
  const start = match.index ?? -1;
@@ -1574,18 +2142,33 @@ function resolveImportPath(db, importerPath, specifier) {
1574
2142
  if (isPythonSourcePath(importerPath)) {
1575
2143
  return resolvePythonImportPath(db, importerPath, specifier);
1576
2144
  }
2145
+ if (isRustSourcePath(importerPath)) {
2146
+ return resolveRustImportPath(db, importerPath, specifier);
2147
+ }
2148
+ if (isRubySourcePath(importerPath)) {
2149
+ return resolveRubyImportPath(db, importerPath, specifier);
2150
+ }
2151
+ if (isCLikeSourcePath(importerPath)) {
2152
+ return resolveCLikeImportPath(db, importerPath, specifier);
2153
+ }
2154
+ if (isJvmSourcePath(importerPath) || isDotNetSourcePath(importerPath) || isPhpSourcePath(importerPath)) {
2155
+ return resolveQualifiedImportPath(db, specifier.replace(/\\/g, "."), extensionFamilyFor(importerPath));
2156
+ }
2157
+ if (isDartSourcePath(importerPath)) {
2158
+ return resolveDartImportPath(db, importerPath, specifier);
2159
+ }
1577
2160
  return resolveJavaScriptImportPath(db, importerPath, specifier);
1578
2161
  }
1579
2162
  function resolveJavaScriptImportPath(db, importerPath, specifier) {
1580
2163
  if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
1581
2164
  return null;
1582
2165
  }
1583
- const importerDir = dirname2(join6(db.config.projectRoot, importerPath));
2166
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
1584
2167
  const absolute = resolve2(importerDir, specifier);
1585
2168
  const indexedPaths = getIndexedPaths(db);
1586
2169
  for (const candidate of candidateImportPaths(absolute)) {
1587
2170
  const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
1588
- if (indexedPaths.has(relativeCandidate) || existsSync6(candidate)) {
2171
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
1589
2172
  return relativeCandidate;
1590
2173
  }
1591
2174
  }
@@ -1599,7 +2182,7 @@ function resolvePythonImportPath(db, importerPath, specifier) {
1599
2182
  if (!match) return null;
1600
2183
  const dots = match[1].length;
1601
2184
  const remainder = match[2].replace(/^\./, "");
1602
- let baseDir = dirname2(join6(db.config.projectRoot, importerPath));
2185
+ let baseDir = dirname2(join8(db.config.projectRoot, importerPath));
1603
2186
  for (let i = 1; i < dots; i++) {
1604
2187
  baseDir = dirname2(baseDir);
1605
2188
  }
@@ -1609,26 +2192,145 @@ function resolvePythonImportPath(db, importerPath, specifier) {
1609
2192
  }
1610
2193
  for (const candidate of pythonCandidateImportPaths(basePath)) {
1611
2194
  const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
1612
- if (indexedPaths.has(relativeCandidate) || existsSync6(candidate)) {
2195
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
2196
+ return relativeCandidate;
2197
+ }
2198
+ }
2199
+ return null;
2200
+ }
2201
+ function resolveRustImportPath(db, importerPath, specifier) {
2202
+ if (!specifier) return null;
2203
+ const normalizedSpecifier = specifier.replace(/\s+as\s+.+$/, "").trim();
2204
+ if (!normalizedSpecifier.startsWith("crate::") && !normalizedSpecifier.startsWith("self::") && !normalizedSpecifier.startsWith("super::")) {
2205
+ return null;
2206
+ }
2207
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2208
+ let basePath;
2209
+ if (normalizedSpecifier.startsWith("crate::")) {
2210
+ basePath = resolve2(db.config.projectRoot, "src", normalizedSpecifier.slice("crate::".length).replace(/::/g, "/"));
2211
+ } else if (normalizedSpecifier.startsWith("self::")) {
2212
+ basePath = resolve2(importerDir, normalizedSpecifier.slice("self::".length).replace(/::/g, "/"));
2213
+ } else {
2214
+ basePath = resolve2(dirname2(importerDir), normalizedSpecifier.slice("super::".length).replace(/::/g, "/"));
2215
+ }
2216
+ for (const candidate of rustCandidateImportPaths(basePath)) {
2217
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2218
+ if (getIndexedPaths(db).has(relativeCandidate) || existsSync8(candidate)) {
2219
+ return relativeCandidate;
2220
+ }
2221
+ }
2222
+ return null;
2223
+ }
2224
+ function resolveRubyImportPath(db, importerPath, specifier) {
2225
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2226
+ const absolute = resolve2(importerDir, specifier);
2227
+ for (const candidate of rubyCandidateImportPaths(absolute)) {
2228
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2229
+ if (getIndexedPaths(db).has(relativeCandidate) || existsSync8(candidate)) {
2230
+ return relativeCandidate;
2231
+ }
2232
+ }
2233
+ return null;
2234
+ }
2235
+ function resolveCLikeImportPath(db, importerPath, specifier) {
2236
+ const indexedPaths = getIndexedPaths(db);
2237
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2238
+ const candidates = [
2239
+ resolve2(importerDir, specifier),
2240
+ resolve2(db.config.projectRoot, specifier),
2241
+ resolve2(db.config.projectRoot, "include", specifier),
2242
+ resolve2(db.config.projectRoot, "src", specifier)
2243
+ ];
2244
+ for (const candidate of candidates) {
2245
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2246
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
2247
+ return relativeCandidate;
2248
+ }
2249
+ }
2250
+ return null;
2251
+ }
2252
+ function resolveQualifiedImportPath(db, specifier, extensions) {
2253
+ const indexedPaths = getIndexedPaths(db);
2254
+ const normalized = specifier.replace(/\\/g, ".").replace(/::/g, ".").replace(/^global::/, "");
2255
+ const pathified = normalized.replace(/\./g, "/");
2256
+ const basenameOnly = normalized.split(".").pop() ?? normalized;
2257
+ for (const ext of extensions) {
2258
+ const exactSuffix = `${pathified}${ext}`;
2259
+ const exact = [...indexedPaths].find((relativePath) => relativePath.endsWith(exactSuffix));
2260
+ if (exact) return exact;
2261
+ }
2262
+ for (const ext of extensions) {
2263
+ const basenameMatch = [...indexedPaths].find((relativePath) => basename(relativePath) === `${basenameOnly}${ext}`);
2264
+ if (basenameMatch) return basenameMatch;
2265
+ }
2266
+ const folderMatches = [...indexedPaths].filter((relativePath) => extensions.includes(extname2(relativePath).toLowerCase())).filter((relativePath) => relativePath.includes(`/${pathified}/`) || relativePath.includes(`/${basenameOnly}/`)).sort((left, right) => left.localeCompare(right));
2267
+ if (folderMatches.length === 1) {
2268
+ return folderMatches[0];
2269
+ }
2270
+ return null;
2271
+ }
2272
+ function resolveDartImportPath(db, importerPath, specifier) {
2273
+ const indexedPaths = getIndexedPaths(db);
2274
+ if (specifier.startsWith("package:")) {
2275
+ const withoutScheme = specifier.slice("package:".length);
2276
+ const slashIndex = withoutScheme.indexOf("/");
2277
+ if (slashIndex < 0) return null;
2278
+ const packageRelative = withoutScheme.slice(slashIndex + 1);
2279
+ const candidate = normalizePath(packageRelative.startsWith("lib/") ? packageRelative : `lib/${packageRelative}`);
2280
+ if (indexedPaths.has(candidate)) return candidate;
2281
+ return null;
2282
+ }
2283
+ const importerDir = dirname2(join8(db.config.projectRoot, importerPath));
2284
+ const absolute = resolve2(importerDir, specifier);
2285
+ for (const candidate of dartCandidateImportPaths(absolute)) {
2286
+ const relativeCandidate = normalizePath(relative2(db.config.projectRoot, candidate));
2287
+ if (indexedPaths.has(relativeCandidate) || existsSync8(candidate)) {
1613
2288
  return relativeCandidate;
1614
2289
  }
1615
2290
  }
1616
2291
  return null;
1617
2292
  }
1618
2293
  function pythonCandidateImportPaths(basePath) {
1619
- const ext = extname(basePath);
2294
+ const ext = extname2(basePath);
1620
2295
  if (PYTHON_SOURCE_EXTENSIONS.includes(ext)) {
1621
2296
  return [basePath];
1622
2297
  }
1623
2298
  return [
1624
2299
  `${basePath}.py`,
1625
2300
  `${basePath}.pyi`,
1626
- join6(basePath, "__init__.py"),
1627
- join6(basePath, "__init__.pyi")
2301
+ join8(basePath, "__init__.py"),
2302
+ join8(basePath, "__init__.pyi")
2303
+ ];
2304
+ }
2305
+ function rustCandidateImportPaths(basePath) {
2306
+ const ext = extname2(basePath);
2307
+ if (RUST_SOURCE_EXTENSIONS.includes(ext)) {
2308
+ return [basePath];
2309
+ }
2310
+ return [
2311
+ `${basePath}.rs`,
2312
+ join8(basePath, "mod.rs")
2313
+ ];
2314
+ }
2315
+ function rubyCandidateImportPaths(basePath) {
2316
+ const ext = extname2(basePath);
2317
+ if (RUBY_SOURCE_EXTENSIONS.includes(ext)) {
2318
+ return [basePath];
2319
+ }
2320
+ return [
2321
+ `${basePath}.rb`,
2322
+ join8(basePath, "index.rb")
1628
2323
  ];
1629
2324
  }
2325
+ function dartCandidateImportPaths(basePath) {
2326
+ const ext = extname2(basePath);
2327
+ if (DART_SOURCE_EXTENSIONS.includes(ext)) {
2328
+ return [basePath];
2329
+ }
2330
+ return [`${basePath}.dart`, basePath];
2331
+ }
1630
2332
  function candidateImportPaths(absolute) {
1631
- const ext = extname(absolute);
2333
+ const ext = extname2(absolute);
1632
2334
  const candidates = /* @__PURE__ */ new Set();
1633
2335
  if (ext) {
1634
2336
  candidates.add(absolute);
@@ -1638,7 +2340,7 @@ function candidateImportPaths(absolute) {
1638
2340
  } else {
1639
2341
  for (const sourceExt of SOURCE_EXTENSIONS) {
1640
2342
  candidates.add(`${absolute}${sourceExt}`);
1641
- candidates.add(join6(absolute, `index${sourceExt}`));
2343
+ candidates.add(join8(absolute, `index${sourceExt}`));
1642
2344
  }
1643
2345
  }
1644
2346
  return [...candidates];
@@ -1671,10 +2373,44 @@ function normalizePath(path2) {
1671
2373
  return path2.replace(/\\/g, "/");
1672
2374
  }
1673
2375
  function isJavaScriptSourcePath(relativePath) {
1674
- return SOURCE_EXTENSIONS.includes(extname(relativePath).toLowerCase());
2376
+ return SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
1675
2377
  }
1676
2378
  function isPythonSourcePath(relativePath) {
1677
- return PYTHON_SOURCE_EXTENSIONS.includes(extname(relativePath).toLowerCase());
2379
+ return PYTHON_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2380
+ }
2381
+ function isJvmSourcePath(relativePath) {
2382
+ return JVM_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2383
+ }
2384
+ function isRustSourcePath(relativePath) {
2385
+ return RUST_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2386
+ }
2387
+ function isRubySourcePath(relativePath) {
2388
+ return RUBY_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2389
+ }
2390
+ function isCLikeSourcePath(relativePath) {
2391
+ return C_LIKE_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2392
+ }
2393
+ function isDotNetSourcePath(relativePath) {
2394
+ return DOTNET_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2395
+ }
2396
+ function isVisualBasicSourcePath(relativePath) {
2397
+ return extname2(relativePath).toLowerCase() === ".vb";
2398
+ }
2399
+ function isDartSourcePath(relativePath) {
2400
+ return DART_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2401
+ }
2402
+ function isPhpSourcePath(relativePath) {
2403
+ return PHP_SOURCE_EXTENSIONS.includes(extname2(relativePath).toLowerCase());
2404
+ }
2405
+ function extensionFamilyFor(relativePath) {
2406
+ if (isJvmSourcePath(relativePath)) return JVM_SOURCE_EXTENSIONS;
2407
+ if (isDotNetSourcePath(relativePath)) return DOTNET_SOURCE_EXTENSIONS;
2408
+ if (isPhpSourcePath(relativePath)) return PHP_SOURCE_EXTENSIONS;
2409
+ if (isDartSourcePath(relativePath)) return DART_SOURCE_EXTENSIONS;
2410
+ if (isCLikeSourcePath(relativePath)) return C_LIKE_SOURCE_EXTENSIONS;
2411
+ if (isRustSourcePath(relativePath)) return RUST_SOURCE_EXTENSIONS;
2412
+ if (isRubySourcePath(relativePath)) return RUBY_SOURCE_EXTENSIONS;
2413
+ return SOURCE_EXTENSIONS;
1678
2414
  }
1679
2415
  function getSourceText(db, relativePath) {
1680
2416
  const cache = getCachedMap(SOURCE_TEXT_CACHE, db);
@@ -1683,8 +2419,8 @@ function getSourceText(db, relativePath) {
1683
2419
  if (typeof cached === "string") {
1684
2420
  return cached;
1685
2421
  }
1686
- const fullPath = join6(db.config.projectRoot, normalized);
1687
- if (!existsSync6(fullPath)) {
2422
+ const fullPath = join8(db.config.projectRoot, normalized);
2423
+ if (!existsSync8(fullPath)) {
1688
2424
  cache.set(normalized, "");
1689
2425
  return "";
1690
2426
  }
@@ -1942,18 +2678,39 @@ 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
- }
1952
- function findFirstSymbolMatch(db, symbolPattern) {
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
+ }
2709
+ function findFirstSymbolMatch(db, symbolPattern) {
1953
2710
  const fileLineMatch = symbolPattern.match(/^(.+):(\d+)-(\d+)$/);
1954
2711
  if (fileLineMatch) {
1955
2712
  const [, filePath, startStr, endStr] = fileLineMatch;
1956
- const row = db.get(
2713
+ let row = db.get(
1957
2714
  `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
1958
2715
  FROM global_symbols gs
1959
2716
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
@@ -1962,11 +2719,30 @@ function findFirstSymbolMatch(db, symbolPattern) {
1962
2719
  AND der.start_line <= ? AND der.end_line >= ?
1963
2720
  ${db.pathExclusionsFor("d")}
1964
2721
  ORDER BY (der.end_line - der.start_line) ASC
1965
- LIMIT 1`,
2722
+ LIMIT 1`,
1966
2723
  `%${filePath}%`,
1967
2724
  parseInt(startStr, 10),
1968
2725
  parseInt(endStr, 10)
1969
2726
  );
2727
+ if (!row) {
2728
+ row = db.get(
2729
+ `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
2730
+ FROM global_symbols gs
2731
+ JOIN mentions m ON m.symbol_id = gs.id
2732
+ JOIN chunks c ON m.chunk_id = c.id
2733
+ JOIN documents d ON c.document_id = d.id
2734
+ WHERE m.role = 1
2735
+ AND d.relative_path LIKE ?
2736
+ AND c.start_line <= ? AND c.end_line >= ?
2737
+ ${db.pathExclusionsFor("d")}
2738
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path
2739
+ ORDER BY (MAX(c.end_line) - MIN(c.start_line)) ASC
2740
+ LIMIT 1`,
2741
+ `%${filePath}%`,
2742
+ parseInt(startStr, 10),
2743
+ parseInt(endStr, 10)
2744
+ );
2745
+ }
1970
2746
  if (row && !db.isIgnored(row.relative_path)) {
1971
2747
  return {
1972
2748
  symbolId: row.id,
@@ -2048,28 +2824,63 @@ function getSymbolLookupCandidates(db, tokens) {
2048
2824
  const like = `%${token}%`;
2049
2825
  return [like, like, like];
2050
2826
  });
2051
- return db.all(
2827
+ const primary = db.all(
2052
2828
  `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path, gs.display_name
2053
2829
  FROM global_symbols gs
2054
2830
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2055
2831
  JOIN documents d ON der.document_id = d.id
2056
2832
  WHERE ${tokenClauses.join("\n AND ")}
2057
2833
  ${db.pathExclusionsFor("d")}
2834
+ LIMIT 200`,
2835
+ ...params
2836
+ );
2837
+ if (primary.length > 0) {
2838
+ return primary;
2839
+ }
2840
+ return db.all(
2841
+ `SELECT
2842
+ gs.id,
2843
+ gs.symbol,
2844
+ c.document_id,
2845
+ MIN(c.start_line) AS start_line,
2846
+ MAX(c.end_line) AS end_line,
2847
+ d.relative_path,
2848
+ gs.display_name
2849
+ FROM global_symbols gs
2850
+ JOIN mentions m ON m.symbol_id = gs.id
2851
+ JOIN chunks c ON m.chunk_id = c.id
2852
+ JOIN documents d ON c.document_id = d.id
2853
+ WHERE m.role = 1
2854
+ AND ${tokenClauses.join("\n AND ")}
2855
+ ${db.pathExclusionsFor("d")}
2856
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path, gs.display_name
2058
2857
  LIMIT 200`,
2059
2858
  ...params
2060
2859
  );
2061
2860
  }
2062
2861
  function scoreSymbolCandidate(row, originalPattern, cleanedPattern, tokens) {
2862
+ const originalCase = originalPattern.trim();
2863
+ const cleanedCase = cleanedPattern;
2864
+ const noParensCase = cleanedCase.replace(/\(\)$/, "");
2063
2865
  const original = originalPattern.toLowerCase();
2064
2866
  const cleaned = cleanedPattern.toLowerCase();
2065
2867
  const noParens = cleaned.replace(/\(\)$/, "");
2868
+ const rawCase = row.symbol;
2869
+ const shortCase = shortenSymbol(row.symbol);
2870
+ const leafCase = leafName(row.symbol);
2871
+ const displayCase = row.display_name ?? "";
2066
2872
  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();
2873
+ const short = shortCase.toLowerCase();
2874
+ const leaf = leafCase.toLowerCase();
2875
+ const display = displayCase.toLowerCase();
2070
2876
  const path2 = row.relative_path.toLowerCase();
2071
2877
  const looksPathLike = /[/:.]/.test(cleanedPattern);
2072
2878
  let score = 0;
2879
+ if (rawCase === originalCase || rawCase === cleanedCase) score += 1150;
2880
+ if (shortCase === originalCase || shortCase === cleanedCase) score += 1100;
2881
+ if (displayCase === noParensCase) score += 980;
2882
+ if (leafCase === noParensCase) score += 960;
2883
+ if (`${leafCase}()` === originalCase || `${leafCase}()` === cleanedCase) score += 955;
2073
2884
  if (raw === original || raw === cleaned) score += 1e3;
2074
2885
  if (short === original || short === cleaned) score += 950;
2075
2886
  if (path2 === original || path2 === cleaned) score += 925;
@@ -2098,32 +2909,37 @@ function scoreSymbolCandidate(row, originalPattern, cleanedPattern, tokens) {
2098
2909
  return score;
2099
2910
  }
2100
2911
  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 ?" : ""}`,
2912
+ const mentions = db.all(
2913
+ `SELECT DISTINCT m.symbol_id, c.id AS chunk_id
2914
+ FROM mentions m
2915
+ JOIN chunks c ON m.chunk_id = c.id
2916
+ WHERE c.document_id = ?
2917
+ AND c.start_line >= ?
2918
+ AND c.end_line <= ?
2919
+ AND m.role != 1
2920
+ AND m.symbol_id != ?
2921
+ ${opts.limit ? "LIMIT ?" : ""}`,
2120
2922
  ...calleeQueryParams(symbol, opts.limit)
2121
2923
  );
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
- }));
2924
+ const primary = [];
2925
+ const directSeen = /* @__PURE__ */ new Set();
2926
+ for (const mention of mentions) {
2927
+ const callee = getFullSymbolMatch(db, {
2928
+ symbolId: mention.symbol_id,
2929
+ documentId: symbol.documentId,
2930
+ startLine: symbol.startLine,
2931
+ endLine: symbol.endLine
2932
+ });
2933
+ if (!callee || db.isIgnored(callee.relativePath)) continue;
2934
+ const key = `${callee.symbol}|${callee.relativePath}|${mention.chunk_id}`;
2935
+ if (directSeen.has(key)) continue;
2936
+ directSeen.add(key);
2937
+ primary.push({
2938
+ symbol: callee.symbol,
2939
+ file: callee.relativePath,
2940
+ chunkId: mention.chunk_id
2941
+ });
2942
+ }
2127
2943
  const sourceFallback = getSourceBackedCalleeRows(db, symbol, opts.limit);
2128
2944
  if (sourceFallback.length === 0) {
2129
2945
  return primary;
@@ -2131,9 +2947,15 @@ function getCalleeRowsForSymbol(db, symbol, opts = {}) {
2131
2947
  if (primary.length === 0) {
2132
2948
  return applyLimit(sourceFallback, opts.limit);
2133
2949
  }
2134
- const seen = new Set(primary.map((row) => `${row.symbol}|${row.file}`));
2135
- const merged = [...primary];
2136
- for (const row of sourceFallback) {
2950
+ const merged = [...sourceFallback];
2951
+ const seen = new Set(merged.map((row) => `${row.symbol}|${row.file}`));
2952
+ const preferredFallbackLeaves = new Set(
2953
+ sourceFallback.filter((row) => !isLikelyTestPath(row.file)).map((row) => leafName(row.symbol))
2954
+ );
2955
+ for (const row of primary) {
2956
+ if (isLikelyTestPath(row.file) && preferredFallbackLeaves.has(leafName(row.symbol))) {
2957
+ continue;
2958
+ }
2137
2959
  const key = `${row.symbol}|${row.file}`;
2138
2960
  if (seen.has(key)) continue;
2139
2961
  seen.add(key);
@@ -2147,33 +2969,34 @@ function getCallerRowsForSymbol(db, symbol, opts = {}) {
2147
2969
  return [];
2148
2970
  }
2149
2971
  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);
2972
+ `SELECT DISTINCT ref_d.relative_path AS caller_file, c.start_line AS line
2973
+ FROM mentions m
2974
+ JOIN chunks c ON m.chunk_id = c.id
2975
+ JOIN documents ref_d ON c.document_id = ref_d.id
2976
+ WHERE m.symbol_id = ?
2977
+ AND m.role != 1
2978
+ ${db.pathExclusionsFor("ref_d")}
2979
+ ORDER BY ref_d.relative_path
2980
+ ${opts.limit ? "LIMIT ?" : ""}`,
2981
+ ...referenceQueryParams(match, opts.limit)
2982
+ ).filter((row) => !db.isIgnored(row.caller_file)).flatMap((row) => {
2983
+ const enclosing = findEnclosingDefinition(getDefinitionsForFile(db, row.caller_file), row.line);
2984
+ if (!enclosing || enclosing.symbolId === match.symbolId) {
2985
+ return [];
2986
+ }
2987
+ return [{
2988
+ symbol: enclosing.symbol,
2989
+ file: row.caller_file
2990
+ }];
2991
+ });
2992
+ const sourceFallback = dedupeCallerRows([
2993
+ ...getPythonSourceCallerRows(db, match, opts.limit),
2994
+ ...getGenericSourceCallerRows(db, match, opts.limit)
2995
+ ]);
2173
2996
  if (sourceFallback.length === 0) {
2174
2997
  return primary;
2175
2998
  }
2176
- const merged = sourceFallback.length > 0 ? [...sourceFallback] : [];
2999
+ const merged = [...sourceFallback];
2177
3000
  const seen = new Set(merged.map((row) => `${row.symbol}|${row.file}`));
2178
3001
  for (const row of primary) {
2179
3002
  const key = `${row.symbol}|${row.file}`;
@@ -2185,6 +3008,26 @@ function getCallerRowsForSymbol(db, symbol, opts = {}) {
2185
3008
  }
2186
3009
  return applyLimit(merged, opts.limit);
2187
3010
  }
3011
+ function getGenericSourceCallerRows(db, symbol, limit) {
3012
+ return applyLimit(
3013
+ getSourceReferenceSites(db, symbol).filter((site) => site.enclosingSymbol && site.enclosingSymbol !== symbol.symbol).map((site) => ({
3014
+ symbol: site.enclosingSymbol,
3015
+ file: site.file
3016
+ })),
3017
+ limit
3018
+ );
3019
+ }
3020
+ function dedupeCallerRows(rows) {
3021
+ const seen = /* @__PURE__ */ new Set();
3022
+ const unique = [];
3023
+ for (const row of rows) {
3024
+ const key = `${row.symbol}|${row.file}`;
3025
+ if (seen.has(key)) continue;
3026
+ seen.add(key);
3027
+ unique.push(row);
3028
+ }
3029
+ return unique;
3030
+ }
2188
3031
  function getSourceReferenceSites(db, symbol) {
2189
3032
  const match = getFullSymbolMatch(db, symbol);
2190
3033
  if (!match) {
@@ -2206,21 +3049,9 @@ function getSourceReferenceSites(db, symbol) {
2206
3049
  for (const document of documents) {
2207
3050
  if (db.isIgnored(document.relative_path)) continue;
2208
3051
  const lines = findIdentifierLines(db, document.relative_path, identifier, document.relative_path === match.relativePath ? { excludeStartLine: match.startLine, excludeEndLine: match.endLine } : {});
3052
+ const definitions = getDefinitionsForFile(db, document.relative_path);
2209
3053
  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
- );
3054
+ const enclosing = findEnclosingDefinition(definitions, line);
2224
3055
  const key = `${document.relative_path}|${line}|${enclosing?.symbol ?? ""}`;
2225
3056
  if (seen.has(key)) continue;
2226
3057
  seen.add(key);
@@ -2245,13 +3076,23 @@ function calleeQueryParams(symbol, limit) {
2245
3076
  }
2246
3077
  return params;
2247
3078
  }
2248
- function callerQueryParams(symbol, limit) {
2249
- const params = [symbol.symbolId, symbol.symbolId];
3079
+ function referenceQueryParams(symbol, limit) {
3080
+ const params = [symbol.symbolId];
2250
3081
  if (typeof limit === "number") {
2251
3082
  params.push(limit);
2252
3083
  }
2253
3084
  return params;
2254
3085
  }
3086
+ function findEnclosingDefinition(definitions, line) {
3087
+ let best = null;
3088
+ for (const definition of definitions) {
3089
+ if (definition.startLine > line || definition.endLine < line) continue;
3090
+ if (!best || definition.endLine - definition.startLine < best.endLine - best.startLine) {
3091
+ best = definition;
3092
+ }
3093
+ }
3094
+ return best;
3095
+ }
2255
3096
  function getPythonSourceCalleeRows(db, symbol, limit) {
2256
3097
  const match = getFullSymbolMatch(db, symbol);
2257
3098
  if (!match || !isPythonDocument(db, match.relativePath)) {
@@ -2342,6 +3183,94 @@ function getJavaScriptSourceCalleeRows(db, symbol, limit) {
2342
3183
  }
2343
3184
  return applyLimit(rows, limit);
2344
3185
  }
3186
+ function getJavaSourceCalleeRows(db, symbol, limit) {
3187
+ const match = getFullSymbolMatch(db, symbol);
3188
+ if (!match || !isJavaDocument(db, match.relativePath)) {
3189
+ return [];
3190
+ }
3191
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3192
+ const bindings = parseJavaFieldBindings(getSourceText(db, match.relativePath));
3193
+ return resolveSimpleSourceCallees(db, match, calls, bindings, limit);
3194
+ }
3195
+ function getKotlinSourceCalleeRows(db, symbol, limit) {
3196
+ const match = getFullSymbolMatch(db, symbol);
3197
+ if (!match || !isKotlinDocument(db, match.relativePath)) {
3198
+ return [];
3199
+ }
3200
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3201
+ const bindings = parseKotlinFieldBindings(getSourceText(db, match.relativePath));
3202
+ return resolveSimpleSourceCallees(db, match, calls, bindings, limit);
3203
+ }
3204
+ function getScalaSourceCalleeRows(db, symbol, limit) {
3205
+ const match = getFullSymbolMatch(db, symbol);
3206
+ if (!match || !isScalaDocument(db, match.relativePath)) {
3207
+ return [];
3208
+ }
3209
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3210
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3211
+ }
3212
+ function getCSharpSourceCalleeRows(db, symbol, limit) {
3213
+ const match = getFullSymbolMatch(db, symbol);
3214
+ if (!match || !isCSharpDocument(db, match.relativePath)) {
3215
+ return [];
3216
+ }
3217
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3218
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3219
+ }
3220
+ function getVisualBasicSourceCalleeRows(db, symbol, limit) {
3221
+ const match = getFullSymbolMatch(db, symbol);
3222
+ if (!match || !isVisualBasicDocument(db, match.relativePath)) {
3223
+ return [];
3224
+ }
3225
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3226
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3227
+ }
3228
+ function getCppSourceCalleeRows(db, symbol, limit) {
3229
+ const match = getFullSymbolMatch(db, symbol);
3230
+ if (!match || !isCppDocument(db, match.relativePath)) {
3231
+ return [];
3232
+ }
3233
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3234
+ const bindings = parseCppReceiverBindings(getSourceText(db, match.relativePath));
3235
+ return resolveSimpleSourceCallees(db, match, calls, bindings, limit);
3236
+ }
3237
+ function getRustSourceCalleeRows(db, symbol, limit) {
3238
+ const match = getFullSymbolMatch(db, symbol);
3239
+ if (!match || !isRustDocument(db, match.relativePath)) {
3240
+ return [];
3241
+ }
3242
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3243
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3244
+ }
3245
+ function getRubySourceCalleeRows(db, symbol, limit) {
3246
+ const match = getFullSymbolMatch(db, symbol);
3247
+ if (!match || !isRubyDocument(db, match.relativePath)) {
3248
+ return [];
3249
+ }
3250
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine, { allowInstanceVariables: true });
3251
+ const rubyCalls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine, {
3252
+ allowInstanceVariables: true,
3253
+ allowBareMemberCalls: true
3254
+ });
3255
+ const bindings = parseRubyReceiverBindings(db, getSourceText(db, match.relativePath));
3256
+ return resolveSimpleSourceCallees(db, match, rubyCalls.length > 0 ? rubyCalls : calls, bindings, limit);
3257
+ }
3258
+ function getDartSourceCalleeRows(db, symbol, limit) {
3259
+ const match = getFullSymbolMatch(db, symbol);
3260
+ if (!match || !isDartDocument(db, match.relativePath)) {
3261
+ return [];
3262
+ }
3263
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3264
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3265
+ }
3266
+ function getPhpSourceCalleeRows(db, symbol, limit) {
3267
+ const match = getFullSymbolMatch(db, symbol);
3268
+ if (!match || !isPhpDocument(db, match.relativePath)) {
3269
+ return [];
3270
+ }
3271
+ const calls = getSimpleSourceCalls(db, match.relativePath, match.startLine, match.endLine);
3272
+ return resolveSimpleSourceCallees(db, match, calls, /* @__PURE__ */ new Map(), limit);
3273
+ }
2345
3274
  function getSourceBackedCalleeRows(db, symbol, limit) {
2346
3275
  const match = getFullSymbolMatch(db, symbol);
2347
3276
  if (!match) {
@@ -2353,6 +3282,36 @@ function getSourceBackedCalleeRows(db, symbol, limit) {
2353
3282
  if (isJavaScriptDocument(db, match.relativePath)) {
2354
3283
  return getJavaScriptSourceCalleeRows(db, match, limit);
2355
3284
  }
3285
+ if (isJavaDocument(db, match.relativePath)) {
3286
+ return getJavaSourceCalleeRows(db, match, limit);
3287
+ }
3288
+ if (isScalaDocument(db, match.relativePath)) {
3289
+ return getScalaSourceCalleeRows(db, match, limit);
3290
+ }
3291
+ if (isKotlinDocument(db, match.relativePath)) {
3292
+ return getKotlinSourceCalleeRows(db, match, limit);
3293
+ }
3294
+ if (isCSharpDocument(db, match.relativePath)) {
3295
+ return getCSharpSourceCalleeRows(db, match, limit);
3296
+ }
3297
+ if (isVisualBasicDocument(db, match.relativePath)) {
3298
+ return getVisualBasicSourceCalleeRows(db, match, limit);
3299
+ }
3300
+ if (isCppDocument(db, match.relativePath)) {
3301
+ return getCppSourceCalleeRows(db, match, limit);
3302
+ }
3303
+ if (isRustDocument(db, match.relativePath)) {
3304
+ return getRustSourceCalleeRows(db, match, limit);
3305
+ }
3306
+ if (isRubyDocument(db, match.relativePath)) {
3307
+ return getRubySourceCalleeRows(db, match, limit);
3308
+ }
3309
+ if (isDartDocument(db, match.relativePath)) {
3310
+ return getDartSourceCalleeRows(db, match, limit);
3311
+ }
3312
+ if (isPhpDocument(db, match.relativePath)) {
3313
+ return getPhpSourceCalleeRows(db, match, limit);
3314
+ }
2356
3315
  return [];
2357
3316
  }
2358
3317
  function getPythonSourceCallerRows(db, target, limit) {
@@ -2378,24 +3337,53 @@ function getPythonSourceCallerRows(db, target, limit) {
2378
3337
  }
2379
3338
  return rows;
2380
3339
  }
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
3340
+ function getDefinitionRowsForSymbolId(db, symbolId) {
3341
+ const primary = db.all(
3342
+ `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path, gs.display_name
2387
3343
  FROM global_symbols gs
2388
3344
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2389
3345
  JOIN documents d ON der.document_id = d.id
2390
3346
  WHERE gs.id = ?
2391
- LIMIT 1`,
2392
- symbol.symbolId
3347
+ ORDER BY der.start_line, der.end_line`,
3348
+ symbolId
3349
+ );
3350
+ if (primary.length > 0) {
3351
+ return primary;
3352
+ }
3353
+ return db.all(
3354
+ `SELECT
3355
+ gs.id,
3356
+ gs.symbol,
3357
+ c.document_id,
3358
+ MIN(c.start_line) AS start_line,
3359
+ MAX(c.end_line) AS end_line,
3360
+ d.relative_path,
3361
+ gs.display_name
3362
+ FROM global_symbols gs
3363
+ JOIN mentions m ON m.symbol_id = gs.id
3364
+ JOIN chunks c ON m.chunk_id = c.id
3365
+ JOIN documents d ON c.document_id = d.id
3366
+ WHERE gs.id = ?
3367
+ AND m.role = 1
3368
+ ${db.pathExclusionsFor("d")}
3369
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path, gs.display_name
3370
+ ORDER BY start_line, end_line`,
3371
+ symbolId
2393
3372
  );
3373
+ }
3374
+ function getFullSymbolMatch(db, symbol) {
3375
+ if ("symbol" in symbol && "relativePath" in symbol) {
3376
+ return symbol;
3377
+ }
3378
+ const row = getDefinitionRowsForSymbolId(db, symbol.symbolId)[0];
2394
3379
  if (!row) {
2395
3380
  return null;
2396
3381
  }
2397
3382
  return {
2398
- ...symbol,
3383
+ symbolId: row.id,
3384
+ documentId: row.document_id,
3385
+ startLine: row.start_line,
3386
+ endLine: row.end_line,
2399
3387
  symbol: row.symbol,
2400
3388
  relativePath: row.relative_path
2401
3389
  };
@@ -2410,8 +3398,17 @@ function getDefinitionsForFile(db, relativePath) {
2410
3398
  if (cached) {
2411
3399
  return cached;
2412
3400
  }
2413
- const definitions = db.all(
2414
- `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
3401
+ const primary = db.all(
3402
+ `SELECT
3403
+ gs.id,
3404
+ gs.symbol,
3405
+ der.document_id,
3406
+ der.start_line,
3407
+ der.end_line,
3408
+ d.relative_path,
3409
+ gs.kind,
3410
+ gs.documentation,
3411
+ gs.enclosing_symbol
2415
3412
  FROM global_symbols gs
2416
3413
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
2417
3414
  JOIN documents d ON der.document_id = d.id
@@ -2419,7 +3416,30 @@ function getDefinitionsForFile(db, relativePath) {
2419
3416
  ${db.symbolNoiseFor("gs")}
2420
3417
  ORDER BY der.start_line, der.end_line`,
2421
3418
  relativePath
2422
- ).map((row) => ({
3419
+ );
3420
+ const fallback = primary.length > 0 ? [] : db.all(
3421
+ `SELECT
3422
+ gs.id,
3423
+ gs.symbol,
3424
+ c.document_id,
3425
+ MIN(c.start_line) AS start_line,
3426
+ MAX(c.end_line) AS end_line,
3427
+ d.relative_path,
3428
+ gs.kind,
3429
+ gs.documentation,
3430
+ gs.enclosing_symbol
3431
+ FROM global_symbols gs
3432
+ JOIN mentions m ON m.symbol_id = gs.id
3433
+ JOIN chunks c ON m.chunk_id = c.id
3434
+ JOIN documents d ON c.document_id = d.id
3435
+ WHERE d.relative_path = ?
3436
+ AND m.role = 1
3437
+ ${db.symbolNoiseFor("gs")}
3438
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path
3439
+ ORDER BY start_line, end_line`,
3440
+ relativePath
3441
+ );
3442
+ const definitions = (primary.length > 0 ? primary : fallback).map((row) => ({
2423
3443
  symbolId: row.id,
2424
3444
  symbol: row.symbol,
2425
3445
  documentId: row.document_id,
@@ -2429,34 +3449,29 @@ function getDefinitionsForFile(db, relativePath) {
2429
3449
  leaf: leafName(row.symbol),
2430
3450
  parentTypeName: parentTypeName(row.symbol),
2431
3451
  isFunctionLike: isFunctionLikeSymbol(row.symbol),
2432
- isTypeLike: leafSuffix(row.symbol) === "type"
3452
+ isTypeLike: leafSuffix(row.symbol) === "type",
3453
+ kind: row.kind,
3454
+ documentation: row.documentation,
3455
+ enclosingSymbol: row.enclosing_symbol
2433
3456
  }));
2434
3457
  cache.set(relativePath, definitions);
2435
3458
  return definitions;
2436
3459
  }
2437
- function getAllFunctionLikeDefinitions(db) {
3460
+ function getAllDefinitions(db, opts = {}) {
3461
+ const { scope } = opts;
2438
3462
  const rows = db.all(
2439
- `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
2440
- 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
3463
+ `SELECT relative_path
3464
+ FROM documents
2443
3465
  WHERE 1 = 1
2444
- ${db.pathExclusionsFor("d")}
2445
- ${db.symbolNoiseFor("gs")}
2446
- ORDER BY d.relative_path, der.start_line`
3466
+ ${db.pathExclusionsFor("documents")}
3467
+ ${scope ? "AND relative_path LIKE ?" : ""}
3468
+ ORDER BY relative_path`,
3469
+ ...scope ? [`%${scope}%`] : []
2447
3470
  );
2448
- return rows.filter((row) => !db.isIgnored(row.relative_path)).filter((row) => isFunctionLikeSymbol(row.symbol)).map((row) => ({
2449
- symbolId: row.id,
2450
- symbol: row.symbol,
2451
- documentId: row.document_id,
2452
- startLine: row.start_line,
2453
- 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
- }));
3471
+ return rows.filter((row) => !db.isIgnored(row.relative_path)).flatMap((row) => getDefinitionsForFile(db, row.relative_path));
3472
+ }
3473
+ function getAllFunctionLikeDefinitions(db) {
3474
+ return getAllDefinitions(db).filter((definition) => definition.isFunctionLike);
2460
3475
  }
2461
3476
  function resolvePythonCallTarget(db, current, currentFileDefinitions, imports2, constructorBindings, receiverName, calleeName) {
2462
3477
  if (receiverName === "self" || receiverName === "cls") {
@@ -2557,6 +3572,225 @@ function resolveJavaScriptCallTarget(db, current, currentFileDefinitions, import
2557
3572
  }
2558
3573
  return findDefinitionByName(currentFileDefinitions, calleeName, null, ["function", "type"]);
2559
3574
  }
3575
+ function resolveSimpleSourceCallees(db, current, calls, bindings, limit) {
3576
+ const currentFileDefinitions = getDefinitionsForFile(db, current.relativePath);
3577
+ const currentDefinition = currentFileDefinitions.find((definition) => definition.symbolId === current.symbolId);
3578
+ if (!currentDefinition) {
3579
+ return [];
3580
+ }
3581
+ const rows = [];
3582
+ const seen = /* @__PURE__ */ new Set();
3583
+ for (const call of calls) {
3584
+ const resolved = resolveSimpleSourceCallTarget(
3585
+ db,
3586
+ currentDefinition,
3587
+ currentFileDefinitions,
3588
+ bindings,
3589
+ call.receiverName,
3590
+ call.calleeName
3591
+ );
3592
+ if (!resolved || resolved.symbolId === current.symbolId || db.isIgnored(resolved.relativePath)) continue;
3593
+ const chunkId = 2e9 + call.line;
3594
+ const key = `${resolved.symbol}|${resolved.relativePath}|${chunkId}`;
3595
+ if (seen.has(key)) continue;
3596
+ seen.add(key);
3597
+ rows.push({
3598
+ symbol: resolved.symbol,
3599
+ file: resolved.relativePath,
3600
+ chunkId
3601
+ });
3602
+ }
3603
+ return applyLimit(rows, limit);
3604
+ }
3605
+ function resolveSimpleSourceCallTarget(db, current, currentFileDefinitions, bindings, receiverName, calleeName) {
3606
+ if (!receiverName) {
3607
+ const localMethod = findDefinitionByName(currentFileDefinitions, calleeName, current.parentTypeName, ["function"]);
3608
+ if (localMethod) {
3609
+ return localMethod;
3610
+ }
3611
+ if (current.parentTypeName) {
3612
+ return findProjectDefinitionByTypeAndLeaf(db, current.parentTypeName, calleeName);
3613
+ }
3614
+ return findDefinitionByName(currentFileDefinitions, calleeName, null, ["function", "type"]);
3615
+ }
3616
+ const normalizedReceiver = normalizeReceiverName(receiverName);
3617
+ const inferredType = bindings.get(normalizedReceiver) ?? inferTypeNameFromReceiver(db, normalizedReceiver);
3618
+ if (!inferredType) {
3619
+ return null;
3620
+ }
3621
+ return findProjectDefinitionByTypeAndLeaf(db, inferredType, calleeName);
3622
+ }
3623
+ function findProjectDefinitionByTypeAndLeaf(db, typeName, calleeName) {
3624
+ const definitions = getAllDefinitions(db).filter((definition) => definition.isFunctionLike || definition.symbol.endsWith("()."));
3625
+ const exact = definitions.find((definition) => definition.leaf === calleeName && (definition.parentTypeName === typeName || definition.symbol.includes(typeName)));
3626
+ if (exact) {
3627
+ return exact;
3628
+ }
3629
+ const normalizedType = normalizeLookupName(typeName);
3630
+ const normalizedMatch = definitions.find((definition) => definition.leaf === calleeName && normalizeLookupName(definition.parentTypeName ?? "").includes(normalizedType));
3631
+ if (normalizedMatch) {
3632
+ return normalizedMatch;
3633
+ }
3634
+ return findLooseProjectDefinitionByTypeAndLeaf(db, typeName, calleeName);
3635
+ }
3636
+ function inferTypeNameFromReceiver(db, receiverName) {
3637
+ const normalizedReceiver = normalizeLookupName(receiverName);
3638
+ if (!normalizedReceiver) {
3639
+ return null;
3640
+ }
3641
+ const candidates = getAllDefinitions(db).filter((definition) => definition.isTypeLike || definition.symbol.endsWith("#")).map((definition) => definition.leaf).filter((leaf) => leaf.length > 0);
3642
+ let best = null;
3643
+ for (const candidate of candidates) {
3644
+ const normalizedCandidate = normalizeLookupName(candidate);
3645
+ let score = 0;
3646
+ if (normalizedCandidate === normalizedReceiver) score += 100;
3647
+ if (normalizedCandidate.endsWith(normalizedReceiver)) score += 80;
3648
+ if (normalizedCandidate.includes(normalizedReceiver)) score += 40;
3649
+ if (normalizedReceiver.includes(normalizedCandidate)) score += 20;
3650
+ if (score > 0 && (!best || score > best.score || score === best.score && candidate.length < best.name.length)) {
3651
+ best = { name: candidate, score };
3652
+ }
3653
+ }
3654
+ return best?.name ?? null;
3655
+ }
3656
+ function getSimpleSourceCalls(db, relativePath, startLine, endLine, opts = {}) {
3657
+ const source = getSourceText(db, relativePath);
3658
+ if (!source) {
3659
+ return [];
3660
+ }
3661
+ const lines = source.split("\n");
3662
+ const scoped = lines.slice(startLine, endLine + 1);
3663
+ const calls = [];
3664
+ const seen = /* @__PURE__ */ new Set();
3665
+ 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;
3666
+ for (let index = 0; index < scoped.length; index++) {
3667
+ const rawLine = scoped[index] ?? "";
3668
+ for (const match of rawLine.matchAll(pattern)) {
3669
+ const receiverName = match[1];
3670
+ const calleeName = match[2];
3671
+ if (!receiverName || !calleeName) continue;
3672
+ const key = `${startLine + index}|${receiverName}|${calleeName}`;
3673
+ if (seen.has(key)) continue;
3674
+ seen.add(key);
3675
+ calls.push({
3676
+ receiverName,
3677
+ calleeName,
3678
+ line: startLine + index
3679
+ });
3680
+ }
3681
+ if (opts.allowBareMemberCalls) {
3682
+ for (const match of rawLine.matchAll(/(@?[A-Za-z_][\w]*)\s*\.\s*([A-Za-z_][\w!?=]*)\b(?!\s*[:=])/g)) {
3683
+ const receiverName = match[1];
3684
+ const calleeName = match[2];
3685
+ if (!receiverName || !calleeName) continue;
3686
+ const key = `${startLine + index}|${receiverName}|${calleeName}`;
3687
+ if (seen.has(key)) continue;
3688
+ seen.add(key);
3689
+ calls.push({
3690
+ receiverName,
3691
+ calleeName,
3692
+ line: startLine + index
3693
+ });
3694
+ }
3695
+ }
3696
+ for (const match of rawLine.matchAll(/\b([A-Za-z_][\w]*)\s*\(/g)) {
3697
+ const calleeName = match[1];
3698
+ const start = match.index ?? -1;
3699
+ if (!calleeName || start < 0) continue;
3700
+ const prefix = rawLine.slice(0, start).trimEnd();
3701
+ if (prefix.endsWith("def") || prefix.endsWith("fun") || prefix.endsWith("fn") || /\b(?:class|interface|trait|module|object)\s*$/.test(prefix)) {
3702
+ continue;
3703
+ }
3704
+ if (rawLine.slice(Math.max(0, start - 3), start).includes(".")) {
3705
+ continue;
3706
+ }
3707
+ calls.push({
3708
+ receiverName: null,
3709
+ calleeName,
3710
+ line: startLine + index
3711
+ });
3712
+ }
3713
+ }
3714
+ return calls;
3715
+ }
3716
+ function parseJavaFieldBindings(source) {
3717
+ const bindings = /* @__PURE__ */ new Map();
3718
+ 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)) {
3719
+ const typeName = stripGenericType(match[1]);
3720
+ const localName = match[2];
3721
+ if (typeName && localName) {
3722
+ bindings.set(localName, typeName);
3723
+ }
3724
+ }
3725
+ return bindings;
3726
+ }
3727
+ function parseKotlinFieldBindings(source) {
3728
+ const bindings = /* @__PURE__ */ new Map();
3729
+ 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)) {
3730
+ const localName = match[1];
3731
+ const typeName = stripGenericType(match[2]);
3732
+ if (typeName && localName) {
3733
+ bindings.set(localName, typeName);
3734
+ }
3735
+ }
3736
+ return bindings;
3737
+ }
3738
+ function parseCppReceiverBindings(source) {
3739
+ const bindings = /* @__PURE__ */ new Map();
3740
+ const constructorMatch = source.match(/RunCoordinator::RunCoordinator\s*\(([\s\S]*?)\)\s*(?::\s*([\s\S]*?))?\s*\{/);
3741
+ if (!constructorMatch) {
3742
+ return bindings;
3743
+ }
3744
+ const params = constructorMatch[1] ?? "";
3745
+ const initializers = constructorMatch[2] ?? "";
3746
+ const parameterTypes = /* @__PURE__ */ new Map();
3747
+ for (const param of params.split(",")) {
3748
+ const trimmed = param.trim();
3749
+ const match = trimmed.match(/(.+?)\s+([A-Za-z_][\w]*)$/);
3750
+ if (!match) continue;
3751
+ const [, rawType, rawName] = match;
3752
+ if (!rawType || !rawName) continue;
3753
+ const typeName = stripGenericType(rawType.replace(/[&*]+/g, " ").replace(/\bconst\b/g, " ").trim());
3754
+ if (!typeName) continue;
3755
+ parameterTypes.set(normalizeReceiverName(rawName), typeName);
3756
+ }
3757
+ for (const initializer of initializers.split(",")) {
3758
+ const trimmed = initializer.trim();
3759
+ const match = trimmed.match(/([A-Za-z_][\w]*)\s*\(\s*([A-Za-z_][\w]*)\s*\)$/);
3760
+ if (!match) continue;
3761
+ const [, fieldName, sourceName] = match;
3762
+ if (!fieldName || !sourceName) continue;
3763
+ const typeName = parameterTypes.get(normalizeReceiverName(sourceName));
3764
+ if (!typeName) continue;
3765
+ bindings.set(normalizeReceiverName(fieldName), typeName);
3766
+ }
3767
+ return bindings;
3768
+ }
3769
+ function parseRubyReceiverBindings(db, source) {
3770
+ const bindings = /* @__PURE__ */ new Map();
3771
+ for (const match of source.matchAll(/@([A-Za-z_][\w]*)\s*=\s*([A-Za-z_][\w]*)/g)) {
3772
+ const ivarName = `@${match[1]}`;
3773
+ const localName = match[2];
3774
+ const typeName = inferTypeNameFromReceiver(db, localName ?? match[1] ?? "");
3775
+ if (typeName) {
3776
+ bindings.set(ivarName, typeName);
3777
+ bindings.set(match[1], typeName);
3778
+ }
3779
+ }
3780
+ return bindings;
3781
+ }
3782
+ function stripGenericType(typeName) {
3783
+ return (typeName ?? "").replace(/<.*$/, "").replace(/^.*\./, "").trim();
3784
+ }
3785
+ function normalizeReceiverName(receiverName) {
3786
+ return receiverName.replace(/^@/, "").replace(/^this(?:\.|->)/, "").replace(/^_+/, "").replace(/_+$/, "").trim();
3787
+ }
3788
+ function normalizeLookupName(value) {
3789
+ return value.replace(/[^A-Za-z0-9]+/g, "").toLowerCase();
3790
+ }
3791
+ function isLikelyTestPath(relativePath) {
3792
+ return /(^|\/)(tests?|__tests__)\//.test(relativePath) || /\.(?:spec|test)\./.test(relativePath) || /_test\./.test(relativePath);
3793
+ }
2560
3794
  function findDefinitionByName(definitions, leaf, parentType, preference) {
2561
3795
  const candidates = definitions.filter((definition) => definition.leaf === leaf && definition.parentTypeName === parentType);
2562
3796
  if (candidates.length === 0) {
@@ -2570,6 +3804,58 @@ function findDefinitionByName(definitions, leaf, parentType, preference) {
2570
3804
  }
2571
3805
  return candidates[0] ?? null;
2572
3806
  }
3807
+ function findLooseProjectDefinitionByTypeAndLeaf(db, typeName, calleeName) {
3808
+ const rows = db.all(
3809
+ `SELECT
3810
+ gs.id,
3811
+ gs.symbol,
3812
+ c.document_id,
3813
+ MIN(c.start_line) AS start_line,
3814
+ MAX(c.end_line) AS end_line,
3815
+ d.relative_path,
3816
+ gs.kind,
3817
+ gs.documentation,
3818
+ gs.enclosing_symbol
3819
+ FROM global_symbols gs
3820
+ JOIN mentions m ON m.symbol_id = gs.id
3821
+ JOIN chunks c ON c.id = m.chunk_id
3822
+ JOIN documents d ON d.id = c.document_id
3823
+ WHERE gs.symbol LIKE ?
3824
+ AND gs.symbol LIKE ?
3825
+ ${db.pathExclusionsFor("d")}
3826
+ GROUP BY gs.id, gs.symbol, c.document_id, d.relative_path, gs.kind, gs.documentation, gs.enclosing_symbol`,
3827
+ `%${typeName}%`,
3828
+ `%${calleeName}%`
3829
+ );
3830
+ const normalizedType = normalizeLookupName(typeName);
3831
+ const candidates = rows.filter((row2) => leafName(row2.symbol) === calleeName).filter((row2) => {
3832
+ const parentType = parentTypeName(row2.symbol);
3833
+ return parentType === typeName || row2.symbol.includes(typeName) || normalizeLookupName(parentType ?? "").includes(normalizedType);
3834
+ }).sort((left, right) => {
3835
+ const leftTest = isLikelyTestPath(left.relative_path) ? 1 : 0;
3836
+ const rightTest = isLikelyTestPath(right.relative_path) ? 1 : 0;
3837
+ return leftTest - rightTest || left.relative_path.localeCompare(right.relative_path) || left.start_line - right.start_line;
3838
+ });
3839
+ const row = candidates[0];
3840
+ if (!row) {
3841
+ return null;
3842
+ }
3843
+ return {
3844
+ symbolId: row.id,
3845
+ symbol: row.symbol,
3846
+ documentId: row.document_id,
3847
+ startLine: row.start_line,
3848
+ endLine: row.end_line,
3849
+ relativePath: row.relative_path,
3850
+ leaf: leafName(row.symbol),
3851
+ parentTypeName: parentTypeName(row.symbol),
3852
+ isFunctionLike: isFunctionLikeSymbol(row.symbol),
3853
+ isTypeLike: leafSuffix(row.symbol) === "type",
3854
+ kind: row.kind,
3855
+ documentation: row.documentation,
3856
+ enclosingSymbol: row.enclosing_symbol
3857
+ };
3858
+ }
2573
3859
  function hasUniqueLeafDefinition(db, leaf, symbolId) {
2574
3860
  const rows = db.all(
2575
3861
  `SELECT id, symbol
@@ -2615,6 +3901,76 @@ function isJavaScriptDocument(db, relativePath) {
2615
3901
  );
2616
3902
  return row?.language === "typescript" || row?.language === "javascript" || /\.(?:[cm]?[jt]sx?)$/.test(relativePath);
2617
3903
  }
3904
+ function isJavaDocument(db, relativePath) {
3905
+ const row = db.get(
3906
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3907
+ relativePath
3908
+ );
3909
+ return row?.language === "java" || relativePath.endsWith(".java");
3910
+ }
3911
+ function isKotlinDocument(db, relativePath) {
3912
+ const row = db.get(
3913
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3914
+ relativePath
3915
+ );
3916
+ return row?.language === "kotlin" || relativePath.endsWith(".kt") || relativePath.endsWith(".kts");
3917
+ }
3918
+ function isScalaDocument(db, relativePath) {
3919
+ const row = db.get(
3920
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3921
+ relativePath
3922
+ );
3923
+ return row?.language === "scala" || relativePath.endsWith(".scala");
3924
+ }
3925
+ function isCSharpDocument(db, relativePath) {
3926
+ const row = db.get(
3927
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3928
+ relativePath
3929
+ );
3930
+ return row?.language === "C#" || relativePath.endsWith(".cs");
3931
+ }
3932
+ function isVisualBasicDocument(db, relativePath) {
3933
+ const row = db.get(
3934
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3935
+ relativePath
3936
+ );
3937
+ return row?.language === "Visual Basic" || relativePath.endsWith(".vb");
3938
+ }
3939
+ function isCppDocument(db, relativePath) {
3940
+ const row = db.get(
3941
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3942
+ relativePath
3943
+ );
3944
+ return row?.language === "CPP" || /\.(?:cc|cpp|cxx|hpp|hh|hxx)$/.test(relativePath);
3945
+ }
3946
+ function isRustDocument(db, relativePath) {
3947
+ const row = db.get(
3948
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3949
+ relativePath
3950
+ );
3951
+ return row?.language === "Rust" || relativePath.endsWith(".rs");
3952
+ }
3953
+ function isRubyDocument(db, relativePath) {
3954
+ const row = db.get(
3955
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3956
+ relativePath
3957
+ );
3958
+ return row?.language === "ruby" || relativePath.endsWith(".rb");
3959
+ }
3960
+ function isDartDocument(db, relativePath) {
3961
+ const row = db.get(
3962
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3963
+ relativePath
3964
+ );
3965
+ return row?.language === "Dart" || relativePath.endsWith(".dart");
3966
+ }
3967
+ function isPhpDocument(db, relativePath) {
3968
+ const row = db.get(
3969
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3970
+ relativePath
3971
+ );
3972
+ return row?.language === "PHP" || relativePath.endsWith(".php");
3973
+ }
2618
3974
  function applyLimit(values, limit) {
2619
3975
  return typeof limit === "number" ? values.slice(0, limit) : values;
2620
3976
  }
@@ -2635,7 +3991,14 @@ function resolveDocumentCandidates(db, filePattern, opts) {
2635
3991
  score: scoreDocumentPath(row.relative_path, normalizedPattern)
2636
3992
  })).filter((row) => row.score > 0).sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
2637
3993
  if (scored.length === 0) {
2638
- return [];
3994
+ const symbolMatch = findFirstSymbolMatch(db, filePattern);
3995
+ if (!symbolMatch || db.isIgnored(symbolMatch.relativePath)) {
3996
+ return [];
3997
+ }
3998
+ return [{
3999
+ relativePath: symbolMatch.relativePath,
4000
+ score: 700
4001
+ }];
2639
4002
  }
2640
4003
  const exactish = scored.filter((row) => row.score >= 800);
2641
4004
  if (exactish.length > 0) {
@@ -2645,8 +4008,8 @@ function resolveDocumentCandidates(db, filePattern, opts) {
2645
4008
  }
2646
4009
  function scoreDocumentPath(relativePath, rawPattern) {
2647
4010
  const normalizedPath = normalizeLookupPath(relativePath);
2648
- const pathBase = basename(normalizedPath);
2649
- const patternBase = basename(rawPattern);
4011
+ const pathBase = basename2(normalizedPath);
4012
+ const patternBase = basename2(rawPattern);
2650
4013
  let score = 0;
2651
4014
  if (normalizedPath === rawPattern) score += 1200;
2652
4015
  if (normalizedPath.endsWith(`/${rawPattern}`)) score += 1100;
@@ -2674,62 +4037,69 @@ function symbols(db, filePattern) {
2674
4037
  if (resolvedPaths.length === 0) {
2675
4038
  return [];
2676
4039
  }
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
- }));
4040
+ return resolvedPaths.flatMap((relativePath) => getDefinitionsForFile(db, relativePath)).filter((row) => !db.isIgnored(row.relativePath)).map((row) => {
4041
+ const docRow = db.get(
4042
+ `SELECT REPLACE(SUBSTR(documentation, INSTR(documentation, '|') + 1), char(10), ' ') AS sig
4043
+ FROM global_symbols
4044
+ WHERE id = ?`,
4045
+ row.symbolId
4046
+ );
4047
+ return {
4048
+ startLine: row.startLine,
4049
+ endLine: row.endLine,
4050
+ symbol: row.symbol,
4051
+ shortName: shortenSymbol(row.symbol),
4052
+ signature: cleanSignature(docRow?.sig ?? null)
4053
+ };
4054
+ });
2701
4055
  }
2702
4056
 
2703
4057
  // src/queries/methods.ts
4058
+ import { basename as basename3 } from "path";
2704
4059
  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)
4060
+ const classMatch = findFirstSymbolMatch(db, className);
4061
+ if (!classMatch) {
4062
+ return [];
4063
+ }
4064
+ const ownerName = leafName(classMatch.symbol);
4065
+ const definitions = getDefinitionsForFile(db, classMatch.relativePath).filter((definition) => isCallableSymbol(definition.symbol));
4066
+ const directMethods = definitions.filter((definition) => definition.parentTypeName === ownerName || definition.symbol.includes(ownerName));
4067
+ const fileScopedMethods = directMethods.length > 0 ? directMethods : stripExtension(basename3(classMatch.relativePath)) === ownerName ? definitions.filter((definition) => definition.symbol.includes("<invalid-global-code>")) : [];
4068
+ return fileScopedMethods.map((definition) => ({
4069
+ startLine: definition.startLine,
4070
+ endLine: definition.endLine,
4071
+ name: leafName(definition.symbol)
2720
4072
  }));
2721
4073
  }
4074
+ function isCallableSymbol(rawSymbol) {
4075
+ return rawSymbol.endsWith("().") || leafSuffix(rawSymbol) === "method";
4076
+ }
4077
+ function stripExtension(relativePath) {
4078
+ return relativePath.replace(/\.[^.]+$/, "");
4079
+ }
2722
4080
 
2723
4081
  // src/queries/refs.ts
2724
4082
  function refs(db, symbolPattern) {
2725
4083
  const match = findFirstSymbolMatch(db, symbolPattern);
2726
4084
  if (match) {
4085
+ const includeDefinitionSite = !isFunctionLikeSymbol(match.symbol);
4086
+ const definitionRows = includeDefinitionSite ? [{
4087
+ relativePath: match.relativePath,
4088
+ line: match.startLine
4089
+ }] : [];
2727
4090
  const sourceSites = getSourceReferenceSites(db, match).filter((site) => !db.isIgnored(site.file)).map((site) => ({
2728
4091
  relativePath: site.file,
2729
4092
  line: site.line
2730
4093
  }));
2731
4094
  if (sourceSites.length > 0) {
2732
- return sourceSites;
4095
+ const seen2 = /* @__PURE__ */ new Set();
4096
+ const rows2 = [...definitionRows, ...sourceSites, ...getRubySemanticRefs(db, match)].filter((site) => {
4097
+ const key = `${site.relativePath}:${site.line}`;
4098
+ if (seen2.has(key)) return false;
4099
+ seen2.add(key);
4100
+ return true;
4101
+ });
4102
+ return rows2;
2733
4103
  }
2734
4104
  }
2735
4105
  const rows = db.all(
@@ -2744,10 +4114,70 @@ function refs(db, symbolPattern) {
2744
4114
  ORDER BY d.relative_path, c.start_line`,
2745
4115
  match?.symbolId ?? -1
2746
4116
  );
2747
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
4117
+ const referenceRows = rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
2748
4118
  relativePath: r.relative_path,
2749
4119
  line: r.start_line
2750
4120
  }));
4121
+ if (!match || db.isIgnored(match.relativePath) || isFunctionLikeSymbol(match.symbol)) {
4122
+ return referenceRows;
4123
+ }
4124
+ const seen = new Set(referenceRows.map((row) => `${row.relativePath}:${row.line}`));
4125
+ if (!seen.has(`${match.relativePath}:${match.startLine}`)) {
4126
+ referenceRows.unshift({
4127
+ relativePath: match.relativePath,
4128
+ line: match.startLine
4129
+ });
4130
+ }
4131
+ for (const row of getRubySemanticRefs(db, match)) {
4132
+ const key = `${row.relativePath}:${row.line}`;
4133
+ if (seen.has(key)) continue;
4134
+ seen.add(key);
4135
+ referenceRows.push(row);
4136
+ }
4137
+ return referenceRows;
4138
+ }
4139
+ function getRubySemanticRefs(db, match) {
4140
+ if (!match.relativePath.endsWith(".rb")) {
4141
+ return [];
4142
+ }
4143
+ const tokens = rubyReferenceTokens(match.symbol);
4144
+ if (tokens.length === 0) {
4145
+ return [];
4146
+ }
4147
+ const rows = db.all(
4148
+ `SELECT relative_path
4149
+ FROM documents
4150
+ WHERE relative_path LIKE '%.rb'
4151
+ ${db.pathExclusionsFor("documents")}
4152
+ ORDER BY relative_path`
4153
+ );
4154
+ const results = [];
4155
+ for (const row of rows) {
4156
+ if (db.isIgnored(row.relative_path)) continue;
4157
+ const source = getSourceText(db, row.relative_path);
4158
+ if (!source) continue;
4159
+ const lines = source.split("\n");
4160
+ for (let index = 0; index < lines.length; index++) {
4161
+ const line = lines[index] ?? "";
4162
+ if (tokens.some((token) => new RegExp(`@${token}\\b|\\b${token}:`).test(line))) {
4163
+ results.push({
4164
+ relativePath: row.relative_path,
4165
+ line: index
4166
+ });
4167
+ }
4168
+ }
4169
+ }
4170
+ return results;
4171
+ }
4172
+ function rubyReferenceTokens(rawSymbol) {
4173
+ const leaf = rawSymbol.split(":").pop() ?? rawSymbol;
4174
+ const snake = leaf.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9_]+/g, "_").toLowerCase().replace(/^_+|_+$/g, "");
4175
+ const parts = snake.split("_").filter(Boolean);
4176
+ const candidates = /* @__PURE__ */ new Set();
4177
+ if (snake) candidates.add(snake);
4178
+ if (parts.length >= 1) candidates.add(parts[parts.length - 1]);
4179
+ if (parts.length >= 2) candidates.add(parts.slice(-2).join("_"));
4180
+ return [...candidates];
2751
4181
  }
2752
4182
 
2753
4183
  // src/queries/trace.ts
@@ -2756,24 +4186,22 @@ function trace(db, symbolPattern) {
2756
4186
  if (!match) {
2757
4187
  return { definitions: [], referencedBy: [] };
2758
4188
  }
2759
- const defRows = db.all(
2760
- `SELECT d.relative_path, der.start_line, der.end_line,
4189
+ const definitionMeta = db.get(
4190
+ `SELECT
2761
4191
  gs.display_name,
2762
4192
  REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig
2763
4193
  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
4194
  WHERE gs.id = ?
2767
- ORDER BY d.relative_path, der.start_line
2768
4195
  LIMIT 10`,
2769
4196
  match.symbolId
2770
4197
  );
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
- }));
4198
+ const definitions = db.isIgnored(match.relativePath) ? [] : [{
4199
+ relativePath: match.relativePath,
4200
+ startLine: match.startLine,
4201
+ endLine: match.endLine,
4202
+ signature: buildTraceSignature(definitionMeta?.sig ?? null, definitionMeta?.display_name ?? null, match.symbol),
4203
+ source: definitionSource(db, match.relativePath, match.startLine, match.endLine)
4204
+ }];
2777
4205
  const sourceSites = getSourceReferenceSites(db, match);
2778
4206
  const referencedBy = sourceSites.length > 0 ? sourceSites.filter((site) => !db.isIgnored(site.file)).map((site) => ({
2779
4207
  relativePath: site.file,
@@ -2806,6 +4234,15 @@ function trace(db, symbolPattern) {
2806
4234
  }));
2807
4235
  return { definitions, referencedBy };
2808
4236
  }
4237
+ function definitionSource(db, relativePath, startLine, endLine) {
4238
+ const source = getSourceText(db, relativePath);
4239
+ if (!source) {
4240
+ return null;
4241
+ }
4242
+ const lines = source.split("\n");
4243
+ const slice2 = lines.slice(startLine, endLine + 1).join("\n").trimEnd();
4244
+ return slice2.length > 0 ? slice2 : null;
4245
+ }
2809
4246
  function buildTraceSignature(signature, displayName, rawSymbol) {
2810
4247
  const cleaned = cleanSignature(signature);
2811
4248
  if (cleaned && !looksBogusSignature(cleaned)) {
@@ -2946,22 +4383,54 @@ function surface(db, modulePattern) {
2946
4383
  JOIN chunks c ON m.chunk_id = c.id
2947
4384
  JOIN documents d1 ON c.document_id = d1.id
2948
4385
  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}
4386
+ WHERE d1.relative_path NOT IN (${placeholders})
4387
+ AND (
4388
+ EXISTS (
4389
+ SELECT 1
4390
+ FROM defn_enclosing_ranges der
4391
+ JOIN documents d2 ON der.document_id = d2.id
4392
+ WHERE der.symbol_id = gs.id
4393
+ AND d2.relative_path IN (${placeholders})
4394
+ )
4395
+ OR EXISTS (
4396
+ SELECT 1
4397
+ FROM mentions def_m
4398
+ JOIN chunks def_c ON def_m.chunk_id = def_c.id
4399
+ JOIN documents d2 ON def_c.document_id = d2.id
4400
+ WHERE def_m.symbol_id = gs.id
4401
+ AND def_m.role = 1
4402
+ AND d2.relative_path IN (${placeholders})
4403
+ )
4404
+ )
2954
4405
  AND m.role != 1
4406
+ AND ${db.localSymbolPredicate}
4407
+ ${db.pathExclusionsFor("d1")}
2955
4408
  ORDER BY d1.relative_path`,
2956
4409
  ...matchedPaths,
4410
+ ...matchedPaths,
2957
4411
  ...matchedPaths
2958
4412
  );
2959
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
4413
+ const exposedDefinitions = matchedPaths.flatMap(
4414
+ (relativePath) => getDefinitionsForFile(db, relativePath).filter((definition) => isCallableSymbol2(definition.symbol)).map((definition) => ({
4415
+ relative_path: relativePath,
4416
+ symbol: definition.symbol
4417
+ }))
4418
+ );
4419
+ const seen = /* @__PURE__ */ new Set();
4420
+ return [...rows, ...exposedDefinitions].filter((r) => !db.isIgnored(r.relative_path)).filter((r) => {
4421
+ const key = `${r.relative_path}|${r.symbol}`;
4422
+ if (seen.has(key)) return false;
4423
+ seen.add(key);
4424
+ return true;
4425
+ }).map((r) => ({
2960
4426
  consumer: r.relative_path,
2961
4427
  symbol: r.symbol,
2962
4428
  shortName: shortenSymbol(r.symbol)
2963
4429
  }));
2964
4430
  }
4431
+ function isCallableSymbol2(rawSymbol) {
4432
+ return rawSymbol.endsWith("().") || leafSuffix(rawSymbol) === "method";
4433
+ }
2965
4434
 
2966
4435
  // src/entry-surfaces.ts
2967
4436
  var liveBarrelCache = /* @__PURE__ */ new WeakMap();
@@ -2979,11 +4448,11 @@ function isWorkerEntrySurface(path2) {
2979
4448
  function isStructuralEntrySurface(path2) {
2980
4449
  const normalized = normalizePath2(path2);
2981
4450
  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") {
4451
+ const basename5 = segments[segments.length - 1] ?? normalized;
4452
+ 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
4453
  return true;
2985
4454
  }
2986
- if (basename2 === "index.ts" || basename2 === "index.js") {
4455
+ if (basename5 === "index.ts" || basename5 === "index.js") {
2987
4456
  return segments.length <= 2;
2988
4457
  }
2989
4458
  return normalized.endsWith("/mod.rs") || normalized.endsWith("/__init__.py");
@@ -3046,62 +4515,49 @@ function dead(db, opts = {}) {
3046
4515
  skipBarrels = false,
3047
4516
  includeMembers = false
3048
4517
  } = 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
4518
+ const inactiveBarrelPaths = skipBarrels ? new Set(getInactiveBarrelPaths(db)) : /* @__PURE__ */ new Set();
4519
+ const referenceRows = db.all(
4520
+ `SELECT
4521
+ m.symbol_id,
3073
4522
  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);
4523
+ COUNT(*) AS ref_count
4524
+ FROM mentions m
4525
+ JOIN chunks c ON m.chunk_id = c.id
4526
+ JOIN documents d ON c.document_id = d.id
4527
+ WHERE m.role != 1
4528
+ ${db.pathExclusionsFor("d")}
4529
+ GROUP BY m.symbol_id, d.relative_path`
4530
+ );
4531
+ const referencesBySymbol = /* @__PURE__ */ new Map();
4532
+ for (const row of referenceRows) {
4533
+ if (db.isIgnored(row.relative_path)) continue;
4534
+ if (inactiveBarrelPaths.has(row.relative_path)) continue;
4535
+ let refsForSymbol = referencesBySymbol.get(row.symbol_id);
4536
+ if (!refsForSymbol) {
4537
+ refsForSymbol = /* @__PURE__ */ new Map();
4538
+ referencesBySymbol.set(row.symbol_id, refsForSymbol);
4539
+ }
4540
+ refsForSymbol.set(row.relative_path, row.ref_count);
4541
+ }
4542
+ 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);
4543
+ const rows = definitions.map((definition) => {
4544
+ const refMap = referencesBySymbol.get(definition.symbolId) ?? /* @__PURE__ */ new Map();
4545
+ const sameFileRefs = refMap.get(definition.relativePath) ?? 0;
4546
+ let crossFileRefs = 0;
4547
+ for (const [relativePath, count] of refMap) {
4548
+ if (relativePath === definition.relativePath) continue;
4549
+ crossFileRefs += count;
4550
+ }
4551
+ return {
4552
+ relative_path: definition.relativePath,
4553
+ start_line: definition.startLine,
4554
+ end_line: definition.endLine,
4555
+ loc: definition.endLine - definition.startLine + 1,
4556
+ symbol: definition.symbol,
4557
+ same_file_refs: sameFileRefs,
4558
+ cross_file_refs: crossFileRefs
4559
+ };
4560
+ }).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
4561
  let deadCodeCount = 0;
3106
4562
  let fileInternalCount = 0;
3107
4563
  let totalLoc = 0;
@@ -3129,6 +4585,17 @@ function dead(db, opts = {}) {
3129
4585
  totalLoc
3130
4586
  };
3131
4587
  }
4588
+ function passesTestFileFilter(relativePath) {
4589
+ const patterns = [.../* @__PURE__ */ new Set([...TEST_FILE_PATTERNS, ...TEST_SUPPORT_PATH_PATTERNS])];
4590
+ return patterns.every((pattern) => !likeMatches(relativePath, pattern));
4591
+ }
4592
+ function likeMatches(value, pattern) {
4593
+ const regex = new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".")}$`);
4594
+ return regex.test(value);
4595
+ }
4596
+ function looksValueLikeDefinition(rawSymbol) {
4597
+ return isFunctionLikeSymbol(rawSymbol) || rawSymbol.endsWith("().") || rawSymbol.endsWith(".");
4598
+ }
3132
4599
 
3133
4600
  // src/queries/hotspots.ts
3134
4601
  function hotspots(db, opts = {}) {
@@ -3155,13 +4622,26 @@ function hotspots(db, opts = {}) {
3155
4622
  LIMIT ?`,
3156
4623
  limit
3157
4624
  );
3158
- return rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
4625
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
3159
4626
  symbol: r.symbol,
3160
4627
  shortName: shortenSymbol(r.symbol),
3161
4628
  refCount: r.ref_count,
3162
4629
  fileCount: r.file_count,
3163
4630
  definedIn: r.defined_in
3164
4631
  }));
4632
+ if (indexedResults.length > 0) {
4633
+ return indexedResults;
4634
+ }
4635
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).map((definition) => {
4636
+ const callerRows = getCallerRowsForSymbol(db, definition, { limit: 500 });
4637
+ return {
4638
+ symbol: definition.symbol,
4639
+ shortName: shortenSymbol(definition.symbol),
4640
+ refCount: callerRows.length,
4641
+ fileCount: new Set(callerRows.map((row) => row.file)).size,
4642
+ definedIn: definition.relativePath
4643
+ };
4644
+ }).filter((row) => row.refCount > 0).sort((left, right) => right.refCount - left.refCount || right.fileCount - left.fileCount).slice(0, limit);
3165
4645
  }
3166
4646
 
3167
4647
  // src/queries/imports.ts
@@ -3234,6 +4714,14 @@ function importedBy(db, symbolPattern) {
3234
4714
  if (targetFile && normalizePath3(entry.sourcePath) !== normalizePath3(targetFile)) {
3235
4715
  continue;
3236
4716
  }
4717
+ if (entry.kind === "side-effect") {
4718
+ importers.add(row.relative_path);
4719
+ continue;
4720
+ }
4721
+ if (targetFile && isCLikeImporter(row.relative_path)) {
4722
+ importers.add(row.relative_path);
4723
+ continue;
4724
+ }
3237
4725
  if (targetIsModule) {
3238
4726
  importers.add(row.relative_path);
3239
4727
  continue;
@@ -3307,6 +4795,9 @@ function renderImportSymbol(importedName, localName, kind) {
3307
4795
  function normalizePath3(path2) {
3308
4796
  return path2.replace(/\\/g, "/");
3309
4797
  }
4798
+ function isCLikeImporter(relativePath) {
4799
+ return /\.(?:c|h|cc|cpp|cxx|hpp|hh|hxx)$/i.test(relativePath);
4800
+ }
3310
4801
 
3311
4802
  // src/queries/outline.ts
3312
4803
  function outline(db, filePattern) {
@@ -3411,10 +4902,22 @@ function fanOut(db, filePattern) {
3411
4902
  ORDER BY symbol_count DESC`,
3412
4903
  resolvedFile
3413
4904
  );
3414
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
4905
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
3415
4906
  name: r.relative_path,
3416
4907
  count: r.symbol_count
3417
4908
  }));
4909
+ if (indexedResults.length > 0) {
4910
+ return indexedResults;
4911
+ }
4912
+ const graph = buildFileDepGraph(db);
4913
+ const deps2 = graph.get(resolvedFile);
4914
+ if (!deps2 || deps2.size === 0) {
4915
+ return [];
4916
+ }
4917
+ return [{
4918
+ name: resolvedFile,
4919
+ count: deps2.size
4920
+ }];
3418
4921
  }
3419
4922
  function topFanIn(db, opts = {}) {
3420
4923
  const { limit = 30, scope } = opts;
@@ -3633,7 +5136,7 @@ function bottlenecks(db, opts = {}) {
3633
5136
  minFanOut,
3634
5137
  limit
3635
5138
  );
3636
- return rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
5139
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
3637
5140
  symbol: r.symbol,
3638
5141
  shortName: shortenSymbol(r.symbol),
3639
5142
  fanIn: r.fan_in,
@@ -3641,51 +5144,42 @@ function bottlenecks(db, opts = {}) {
3641
5144
  score: r.fan_in * r.fan_out,
3642
5145
  definedIn: r.defined_in
3643
5146
  }));
5147
+ if (indexedResults.length > 0) {
5148
+ return indexedResults;
5149
+ }
5150
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).map((definition) => {
5151
+ const fanIn2 = new Set(
5152
+ getCallerRowsForSymbol(db, definition, { limit: 500 }).map((row) => row.file)
5153
+ ).size;
5154
+ const fanOut2 = new Set(
5155
+ getCalleeRowsForSymbol(db, definition, { limit: 500 }).filter((row) => row.file !== definition.relativePath).map((row) => `${row.symbol}|${row.file}`)
5156
+ ).size;
5157
+ return {
5158
+ symbol: definition.symbol,
5159
+ shortName: shortenSymbol(definition.symbol),
5160
+ fanIn: fanIn2,
5161
+ fanOut: fanOut2,
5162
+ score: fanIn2 * fanOut2,
5163
+ definedIn: definition.relativePath
5164
+ };
5165
+ }).filter((row) => row.fanIn >= minFanIn && row.fanOut >= minFanOut).sort((left, right) => right.score - left.score || right.fanIn - left.fanIn).slice(0, limit);
3644
5166
  }
3645
5167
 
3646
5168
  // src/queries/isolated.ts
3647
5169
  function isolated(db, opts = {}) {
3648
5170
  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
5171
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).filter((definition) => !isEntrySurface(db, definition.relativePath)).filter((definition) => definition.isFunctionLike).map((definition) => ({
5172
+ definition,
5173
+ loc: definition.endLine - definition.startLine + 1
5174
+ })).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(
5175
+ (left, right) => right.loc - left.loc || left.definition.relativePath.localeCompare(right.definition.relativePath) || left.definition.startLine - right.definition.startLine
5176
+ ).map((entry) => ({
5177
+ symbol: entry.definition.symbol,
5178
+ shortName: shortenSymbol(entry.definition.symbol),
5179
+ relativePath: entry.definition.relativePath,
5180
+ startLine: entry.definition.startLine,
5181
+ endLine: entry.definition.endLine,
5182
+ loc: entry.loc
3689
5183
  }));
3690
5184
  }
3691
5185
 
@@ -3828,24 +5322,18 @@ function kindCounts(db, opts = {}) {
3828
5322
  }));
3829
5323
  }
3830
5324
  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));
5325
+ return getAllDefinitions(db, { scope }).map(mapDefinitionToKindRow);
5326
+ }
5327
+ function mapDefinitionToKindRow(definition) {
5328
+ return {
5329
+ symbol: definition.symbol,
5330
+ kind: definition.kind,
5331
+ documentation: definition.documentation,
5332
+ enclosing_symbol: definition.enclosingSymbol,
5333
+ relative_path: definition.relativePath,
5334
+ start_line: definition.startLine,
5335
+ end_line: definition.endLine
5336
+ };
3849
5337
  }
3850
5338
  function resolveKindNumber(row) {
3851
5339
  if (row.kind !== null && row.kind !== 0) {
@@ -4027,7 +5515,7 @@ function uniqueRows(rows) {
4027
5515
  function similar(db, symbolPattern, opts = {}) {
4028
5516
  const { minSimilarity = 0.4, limit = 20 } = opts;
4029
5517
  const target = findCallees(db, symbolPattern);
4030
- if (!target || target.callees.size === 0) return [];
5518
+ if (!target) return [];
4031
5519
  if (!isFunctionLikeSymbol(target.symbol)) return [];
4032
5520
  const candidates = getAllCalleeFingerprints(db, {
4033
5521
  minCallees: 3,
@@ -4059,7 +5547,10 @@ function similar(db, symbolPattern, opts = {}) {
4059
5547
  });
4060
5548
  }
4061
5549
  results.sort((a, b) => b.similarity - a.similarity);
4062
- return results.slice(0, limit);
5550
+ if (results.length > 0) {
5551
+ return results.slice(0, limit);
5552
+ }
5553
+ return similarBySourceShape(db, symbolPattern, { minSimilarity, limit });
4063
5554
  }
4064
5555
  function similarAll(db, opts = {}) {
4065
5556
  const { minSimilarity = 0.5, limit = 20, scope, minCallees = 4 } = opts;
@@ -4206,10 +5697,146 @@ function difference(a, b) {
4206
5697
  }
4207
5698
  return result;
4208
5699
  }
5700
+ function similarBySourceShape(db, symbolPattern, opts) {
5701
+ const target = findSourceFingerprint(db, symbolPattern);
5702
+ if (!target || target.tokens.size < 3) {
5703
+ return [];
5704
+ }
5705
+ const minSimilarity = opts.minSimilarity >= 0.5 ? opts.minSimilarity : 0.3;
5706
+ const results = [];
5707
+ for (const candidate of getAllSourceFingerprints(db)) {
5708
+ if (candidate.symbol === target.symbol || candidate.tokens.size < 3) continue;
5709
+ const shared = intersection(target.tokens, candidate.tokens);
5710
+ if (shared.size < 2) continue;
5711
+ const union = /* @__PURE__ */ new Set([...target.tokens, ...candidate.tokens]);
5712
+ const similarity = union.size > 0 ? shared.size / union.size : 0;
5713
+ if (similarity < minSimilarity) continue;
5714
+ results.push({
5715
+ symbolA: target.symbol,
5716
+ shortNameA: shortenSymbol(target.symbol),
5717
+ fileA: target.file,
5718
+ symbolB: candidate.symbol,
5719
+ shortNameB: shortenSymbol(candidate.symbol),
5720
+ fileB: candidate.file,
5721
+ similarity,
5722
+ sharedCallees: [...shared].sort(),
5723
+ uniqueToA: [...difference(target.tokens, candidate.tokens)].sort(),
5724
+ uniqueToB: [...difference(candidate.tokens, target.tokens)].sort()
5725
+ });
5726
+ }
5727
+ results.sort((a, b) => b.similarity - a.similarity || a.shortNameB.localeCompare(b.shortNameB));
5728
+ return results.slice(0, opts.limit);
5729
+ }
5730
+ function findSourceFingerprint(db, symbolPattern) {
5731
+ const match = findFirstSymbolMatch(db, symbolPattern);
5732
+ if (!match || !isFunctionLikeSymbol(match.symbol)) {
5733
+ return null;
5734
+ }
5735
+ const snippet = definitionSnippet(db, match.relativePath, match.startLine, match.endLine, leafName(match.symbol));
5736
+ const tokens = sourceTokens(snippet, leafName(match.symbol));
5737
+ if (tokens.size === 0) {
5738
+ return null;
5739
+ }
5740
+ return {
5741
+ symbol: match.symbol,
5742
+ file: match.relativePath,
5743
+ tokens
5744
+ };
5745
+ }
5746
+ function getAllSourceFingerprints(db) {
5747
+ return getAllDefinitions(db).filter((definition) => definition.isFunctionLike).map((definition) => ({
5748
+ symbol: definition.symbol,
5749
+ file: definition.relativePath,
5750
+ tokens: sourceTokens(
5751
+ definitionSnippet(db, definition.relativePath, definition.startLine, definition.endLine, definition.leaf),
5752
+ definition.leaf
5753
+ )
5754
+ })).filter((fingerprint) => fingerprint.tokens.size > 0);
5755
+ }
5756
+ function definitionSnippet(db, relativePath, startLine, endLine, leaf) {
5757
+ const source = getSourceText(db, relativePath);
5758
+ if (!source) {
5759
+ return "";
5760
+ }
5761
+ const lines = source.split("\n");
5762
+ if (endLine >= startLine && endLine - startLine <= 12) {
5763
+ return lines.slice(startLine, endLine + 1).join("\n");
5764
+ }
5765
+ const markerPatterns = [
5766
+ new RegExp(`\\bdef\\s+${escapeRegex2(leaf)}\\b`),
5767
+ new RegExp(`\\bfun\\s+${escapeRegex2(leaf)}\\b`),
5768
+ new RegExp(`\\bfn\\s+${escapeRegex2(leaf)}\\b`),
5769
+ new RegExp(`\\bfunction\\s+${escapeRegex2(leaf)}\\b`),
5770
+ new RegExp(`\\b${escapeRegex2(leaf)}\\s*\\(`)
5771
+ ];
5772
+ const definitionStart = lines.findIndex((line) => markerPatterns.some((pattern) => pattern.test(line)));
5773
+ if (definitionStart >= 0) {
5774
+ let definitionEnd = definitionStart;
5775
+ for (let index = definitionStart + 1; index < lines.length && index <= definitionStart + 8; index++) {
5776
+ const line = lines[index] ?? "";
5777
+ if (index > definitionStart && looksLikeDefinitionBoundary(line)) {
5778
+ break;
5779
+ }
5780
+ definitionEnd = index;
5781
+ if (line.trim() === "" && index > definitionStart + 1) {
5782
+ break;
5783
+ }
5784
+ }
5785
+ return lines.slice(definitionStart, definitionEnd + 1).join("\n");
5786
+ }
5787
+ return lines.slice(startLine, Math.min(lines.length, startLine + 8)).join("\n");
5788
+ }
5789
+ function sourceTokens(snippet, leaf) {
5790
+ if (!snippet) {
5791
+ return /* @__PURE__ */ new Set();
5792
+ }
5793
+ const stopWords = /* @__PURE__ */ new Set([
5794
+ "public",
5795
+ "private",
5796
+ "protected",
5797
+ "final",
5798
+ "static",
5799
+ "class",
5800
+ "def",
5801
+ "fun",
5802
+ "fn",
5803
+ "function",
5804
+ "return",
5805
+ "string",
5806
+ "bool",
5807
+ "boolean",
5808
+ "void",
5809
+ "unit",
5810
+ "self",
5811
+ "this",
5812
+ "new",
5813
+ "const",
5814
+ "let",
5815
+ "var",
5816
+ "end",
5817
+ "pub"
5818
+ ]);
5819
+ const normalizedLeafParts = splitIdentifier(leaf);
5820
+ 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();
5821
+ const tokens = normalized.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 1).filter((token) => !stopWords.has(token)).filter((token) => !normalizedLeafParts.has(token));
5822
+ return new Set(tokens);
5823
+ }
5824
+ function splitIdentifier(value) {
5825
+ return new Set(
5826
+ value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9_]+|_/).map((part) => part.toLowerCase()).filter((part) => part.length > 1)
5827
+ );
5828
+ }
5829
+ function looksLikeDefinitionBoundary(line) {
5830
+ return /^\s*(?:def|fun|fn|function|class|trait|module|object|enum|interface|public|private|protected)\b/.test(line);
5831
+ }
5832
+ function escapeRegex2(value) {
5833
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5834
+ }
4209
5835
 
4210
5836
  // src/queries/similar-files.ts
4211
5837
  function similarFiles(db, opts = {}) {
4212
- const { minSimilarity = 0.5, limit = 20, scope, minDeps = 3, filePattern } = opts;
5838
+ const { minSimilarity = 0.5, limit = 20, scope, filePattern } = opts;
5839
+ const minDeps = opts.minDeps ?? (filePattern ? 1 : 3);
4213
5840
  const profiles = buildFileProfiles(db, { scope, minDeps });
4214
5841
  const results = [];
4215
5842
  if (filePattern) {
@@ -4258,7 +5885,7 @@ function findUniversalDependencies(depMap) {
4258
5885
  }
4259
5886
  }
4260
5887
  for (const [dep, count] of depCounts) {
4261
- if (count / fileCount > 0.5) {
5888
+ if (count >= 6 && count / fileCount > 0.8) {
4262
5889
  universalDeps.add(dep);
4263
5890
  }
4264
5891
  }
@@ -4317,8 +5944,8 @@ function similarChains(db, opts = {}) {
4317
5944
  tailFreq.set(chain[t], (tailFreq.get(chain[t]) ?? 0) + 1);
4318
5945
  }
4319
5946
  }
4320
- const infraThreshold = rawChains.length * 0.4;
4321
- const tailThreshold = rawChains.length * 0.3;
5947
+ const infraThreshold = rawChains.length * 0.9;
5948
+ const tailThreshold = rawChains.length * 0.8;
4322
5949
  const infraNodes = /* @__PURE__ */ new Set();
4323
5950
  for (const [node, freq] of nodeFreq) {
4324
5951
  if (freq > infraThreshold) infraNodes.add(node);
@@ -4328,13 +5955,13 @@ function similarChains(db, opts = {}) {
4328
5955
  }
4329
5956
  const structuralNames = ["index.ts", "index.js", "cli.ts", "main.ts", "health.ts", "health.js"];
4330
5957
  for (const node of nodeFreq.keys()) {
4331
- const basename2 = node.split("/").pop() ?? "";
4332
- if (structuralNames.includes(basename2)) infraNodes.add(node);
5958
+ const basename5 = node.split("/").pop() ?? "";
5959
+ if (structuralNames.includes(basename5)) infraNodes.add(node);
4333
5960
  }
4334
5961
  const filteredChains = [];
4335
5962
  for (const chain of rawChains) {
4336
5963
  const filtered = chain.filter((n) => !infraNodes.has(n));
4337
- if (filtered.length >= 3) {
5964
+ if (filtered.length >= 2) {
4338
5965
  filteredChains.push({ original: chain, filtered });
4339
5966
  }
4340
5967
  }
@@ -4499,31 +6126,13 @@ function isSubChain(sub, full) {
4499
6126
  // src/queries/extract-candidates.ts
4500
6127
  function extractCandidates(db, opts = {}) {
4501
6128
  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
- );
6129
+ const symbols2 = getScopedDefinitions(db, scope).filter((definition) => definitionLoc(definition) >= minLoc && isFunctionLikeSymbol(definition.symbol)).sort((left, right) => definitionLoc(right) - definitionLoc(left));
4516
6130
  const results = [];
4517
6131
  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
- });
6132
+ if (db.isIgnored(sym.relativePath)) continue;
6133
+ const basename5 = sym.relativePath.split("/").pop() ?? "";
6134
+ if (basename5.includes("types")) continue;
6135
+ const calleeChunks = getCalleeRowsForSymbol(db, sym);
4527
6136
  const calleeSet = new Set(calleeChunks.map((c) => c.symbol));
4528
6137
  if (calleeSet.size < minCallees) continue;
4529
6138
  const cooccurrence = /* @__PURE__ */ new Map();
@@ -4586,10 +6195,10 @@ function extractCandidates(db, opts = {}) {
4586
6195
  results.push({
4587
6196
  symbol: sym.symbol,
4588
6197
  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,
6198
+ relativePath: sym.relativePath,
6199
+ startLine: sym.startLine,
6200
+ endLine: sym.endLine,
6201
+ loc: definitionLoc(sym),
4593
6202
  totalCallees: calleeSet.size,
4594
6203
  clusters: scoredClusters
4595
6204
  });
@@ -4598,6 +6207,20 @@ function extractCandidates(db, opts = {}) {
4598
6207
  results.sort((a, b) => b.clusters.length - a.clusters.length || b.loc - a.loc);
4599
6208
  return results.slice(0, limit);
4600
6209
  }
6210
+ function getScopedDefinitions(db, scope) {
6211
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6212
+ return db.all(
6213
+ `SELECT relative_path
6214
+ FROM documents
6215
+ WHERE 1 = 1
6216
+ ${db.pathExclusionsFor("documents")}
6217
+ ${scopeFilter}
6218
+ ORDER BY relative_path`
6219
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6220
+ }
6221
+ function definitionLoc(definition) {
6222
+ return definition.endLine - definition.startLine + 1;
6223
+ }
4601
6224
 
4602
6225
  // src/queries/affected.ts
4603
6226
  function affected(db, symbolPattern, opts = {}) {
@@ -4638,73 +6261,39 @@ function affected(db, symbolPattern, opts = {}) {
4638
6261
  return results;
4639
6262
  }
4640
6263
  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
4672
- });
4673
- }
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
- }));
6264
+ const callerRows = getCallerRowsForSymbol(db, target, { limit: 500 }).filter((row) => !db.isIgnored(row.file)).filter((row) => !scope || row.file.includes(scope));
6265
+ const results = [];
6266
+ const seen = /* @__PURE__ */ new Set();
6267
+ for (const row of callerRows) {
6268
+ const match = findExactSymbolMatch(db, row.symbol);
6269
+ if (!match) {
6270
+ const key2 = `${row.file}|${row.symbol}`;
6271
+ if (seen.has(key2)) continue;
6272
+ seen.add(key2);
6273
+ results.push({
6274
+ symbolId: null,
6275
+ symbol: row.symbol,
6276
+ shortName: shortenSymbol(row.symbol),
6277
+ file: row.file,
6278
+ symbolMatch: null
6279
+ });
6280
+ continue;
6281
+ }
6282
+ if (match.symbolId === target.symbolId || db.isIgnored(match.relativePath)) {
6283
+ continue;
6284
+ }
6285
+ const key = `${match.symbolId}|${match.relativePath}`;
6286
+ if (seen.has(key)) continue;
6287
+ seen.add(key);
6288
+ results.push({
6289
+ symbolId: match.symbolId,
6290
+ symbol: match.symbol,
6291
+ shortName: shortenSymbol(match.symbol),
6292
+ file: match.relativePath,
6293
+ symbolMatch: match
6294
+ });
6295
+ }
6296
+ return results;
4708
6297
  }
4709
6298
 
4710
6299
  // src/queries/change-surface.ts
@@ -5054,140 +6643,129 @@ function isLikelyTypeOnlyDep(dep) {
5054
6643
  function shouldSkipDriftFile(filePath) {
5055
6644
  return isStructuralRole(path.basename(filePath)) || isTestLikePath(filePath);
5056
6645
  }
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;
6646
+ function isStructuralRole(basename5) {
6647
+ if (basename5 === "index.ts" || basename5 === "index.js") return true;
6648
+ if (basename5 === "cli.ts" || basename5 === "main.ts" || basename5 === "main.rs") return true;
6649
+ if (basename5.includes("worker.") || basename5.includes("postinstall.")) return true;
6650
+ if (basename5 === "health.ts" || basename5 === "health.js") return true;
5062
6651
  return false;
5063
6652
  }
5064
6653
  function isTestLikePath(filePath) {
5065
6654
  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);
6655
+ const basename5 = path.basename(normalized);
6656
+ 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
6657
  }
5069
6658
 
5070
6659
  // src/queries/wrapper-candidates.ts
6660
+ import { basename as basename4, extname as extname3 } from "path";
5071
6661
  function wrapperCandidates(db, opts) {
5072
6662
  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
- }));
6663
+ const reverseFanIn = buildReverseFileFanIn(buildFileDepGraph(db, scope));
6664
+ const symbols2 = getScopedDefinitions2(db, scope).filter((definition) => definitionLoc2(definition) <= maxLoc && definitionLoc2(definition) >= 2);
6665
+ const results = [];
6666
+ for (const symbol of symbols2) {
6667
+ if (db.isIgnored(symbol.relativePath) || !isFunctionLikeSymbol(symbol.symbol)) continue;
6668
+ const symbolStem = basename4(symbol.relativePath, extname3(symbol.relativePath));
6669
+ const callerRows = dedupeRows(
6670
+ getCallerRowsForSymbol(db, symbol, { limit: 200 }).filter((row) => row.file !== symbol.relativePath)
6671
+ ).filter((row) => basename4(row.file, extname3(row.file)) !== symbolStem);
6672
+ if (callerRows.length !== 1) continue;
6673
+ const caller = callerRows[0];
6674
+ const callerDefinition = getDefinitionsForFile(db, caller.file).find((definition) => definition.symbol === caller.symbol);
6675
+ const useDefinitionFanIn = callerDefinition?.isFunctionLike ?? false;
6676
+ let callerFanIn;
6677
+ if (useDefinitionFanIn && callerDefinition) {
6678
+ callerFanIn = new Set(
6679
+ getCallerRowsForSymbol(db, callerDefinition, { limit: 500 }).filter((row) => row.file !== callerDefinition.relativePath).map((row) => row.file)
6680
+ ).size;
6681
+ } else {
6682
+ callerFanIn = fallbackCallerFanIn(db, reverseFanIn, caller.file);
6683
+ }
6684
+ if (callerFanIn <= 3) continue;
6685
+ results.push({
6686
+ symbol: symbol.symbol,
6687
+ shortName: shortenSymbol(symbol.symbol),
6688
+ file: symbol.relativePath,
6689
+ startLine: symbol.startLine,
6690
+ endLine: symbol.endLine,
6691
+ loc: definitionLoc2(symbol),
6692
+ singleCaller: caller.symbol,
6693
+ singleCallerShort: useDefinitionFanIn ? shortenSymbol(caller.symbol) : basename4(caller.file),
6694
+ callerFanIn
6695
+ });
6696
+ }
6697
+ results.sort((left, right) => right.callerFanIn - left.callerFanIn || right.loc - left.loc);
6698
+ return results.slice(0, limit);
6699
+ }
6700
+ function definitionLoc2(definition) {
6701
+ return definition.endLine - definition.startLine + 1;
6702
+ }
6703
+ function getScopedDefinitions2(db, scope) {
6704
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6705
+ return db.all(
6706
+ `SELECT relative_path
6707
+ FROM documents
6708
+ WHERE 1 = 1
6709
+ ${db.pathExclusionsFor("documents")}
6710
+ ${scopeFilter}
6711
+ ORDER BY relative_path`
6712
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6713
+ }
6714
+ function dedupeRows(rows) {
6715
+ const seen = /* @__PURE__ */ new Set();
6716
+ const unique = [];
6717
+ for (const row of rows) {
6718
+ const key = `${row.symbol}|${row.file}`;
6719
+ if (seen.has(key)) continue;
6720
+ seen.add(key);
6721
+ unique.push(row);
6722
+ }
6723
+ return unique;
6724
+ }
6725
+ function buildReverseFileFanIn(graph) {
6726
+ const reverse = /* @__PURE__ */ new Map();
6727
+ for (const [fromFile, deps2] of graph) {
6728
+ if (!reverse.has(fromFile)) {
6729
+ reverse.set(fromFile, reverse.get(fromFile) ?? 0);
6730
+ }
6731
+ for (const dep of deps2) {
6732
+ reverse.set(dep, (reverse.get(dep) ?? 0) + 1);
6733
+ }
6734
+ }
6735
+ return reverse;
6736
+ }
6737
+ function fallbackCallerFanIn(db, reverseFanIn, callerFile) {
6738
+ const functionLikeFanIn = getDefinitionsForFile(db, callerFile).filter((definition) => definition.isFunctionLike).map((definition) => new Set(
6739
+ getCallerRowsForSymbol(db, definition, { limit: 500 }).filter((row) => row.file !== definition.relativePath).map((row) => row.file)
6740
+ ).size).sort((left, right) => right - left)[0] ?? 0;
6741
+ if (functionLikeFanIn > 0) {
6742
+ return functionLikeFanIn;
6743
+ }
6744
+ const direct = reverseFanIn.get(callerFile) ?? 0;
6745
+ if (direct > 0) {
6746
+ return direct;
6747
+ }
6748
+ const stem = basename4(callerFile, extname3(callerFile));
6749
+ let best = 0;
6750
+ for (const [file, fanIn2] of reverseFanIn) {
6751
+ if (file === callerFile) continue;
6752
+ if (basename4(file, extname3(file)) !== stem) continue;
6753
+ if (fanIn2 > best) {
6754
+ best = fanIn2;
6755
+ }
6756
+ }
6757
+ return best;
5154
6758
  }
5155
6759
 
5156
6760
  // src/queries/passthrough-candidates.ts
5157
6761
  function passthroughCandidates(db, opts) {
5158
6762
  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
- );
6763
+ const symbols2 = getScopedDefinitions3(db, scope).filter((definition) => definitionLoc3(definition) >= 3 && definitionLoc3(definition) <= maxLoc);
5182
6764
  const results = [];
5183
6765
  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
- });
6766
+ if (db.isIgnored(sym.relativePath) || !isFunctionLikeSymbol(sym.symbol)) continue;
6767
+ const rawCallees = getCalleeRowsForSymbol(db, sym);
6768
+ const callees = rawCallees.some((callee2) => isFunctionLikeSymbol(callee2.symbol)) ? rawCallees.filter((callee2) => isFunctionLikeSymbol(callee2.symbol)) : rawCallees;
5191
6769
  const uniqueCallees = /* @__PURE__ */ new Map();
5192
6770
  for (const c of callees) {
5193
6771
  if (!uniqueCallees.has(c.symbol)) {
@@ -5199,10 +6777,10 @@ function passthroughCandidates(db, opts) {
5199
6777
  results.push({
5200
6778
  symbol: sym.symbol,
5201
6779
  shortName: shortenSymbol(sym.symbol),
5202
- file: sym.relative_path,
5203
- startLine: sym.start_line,
5204
- endLine: sym.end_line,
5205
- loc: sym.loc,
6780
+ file: sym.relativePath,
6781
+ startLine: sym.startLine,
6782
+ endLine: sym.endLine,
6783
+ loc: definitionLoc3(sym),
5206
6784
  forwardsTo: callee.symbol,
5207
6785
  forwardsToShort: shortenSymbol(callee.symbol),
5208
6786
  forwardsToFile: callee.file
@@ -5211,42 +6789,32 @@ function passthroughCandidates(db, opts) {
5211
6789
  results.sort((a, b) => a.loc - b.loc || a.file.localeCompare(b.file));
5212
6790
  return results.slice(0, limit);
5213
6791
  }
6792
+ function getScopedDefinitions3(db, scope) {
6793
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6794
+ return db.all(
6795
+ `SELECT relative_path
6796
+ FROM documents
6797
+ WHERE 1 = 1
6798
+ ${db.pathExclusionsFor("documents")}
6799
+ ${scopeFilter}
6800
+ ORDER BY relative_path`
6801
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6802
+ }
6803
+ function definitionLoc3(definition) {
6804
+ return definition.endLine - definition.startLine + 1;
6805
+ }
5214
6806
 
5215
6807
  // src/queries/stale-abstractions.ts
5216
6808
  function staleAbstractions(db, opts) {
5217
6809
  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
- );
6810
+ const rows = getScopedDefinitions4(db, scope).filter((definition) => definition.isTypeLike && definitionLoc4(definition) >= minLoc).map((definition) => ({
6811
+ symbol: definition.symbol,
6812
+ file: definition.relativePath,
6813
+ start_line: definition.startLine,
6814
+ end_line: definition.endLine,
6815
+ loc: definitionLoc4(definition),
6816
+ consumers: countCrossFileConsumers(db, definition)
6817
+ })).filter((row) => row.consumers <= 1).sort((left, right) => right.loc - left.loc || left.file.localeCompare(right.file) || left.start_line - right.start_line);
5250
6818
  const filesWithFunctions = getFilesWithFunctions(db, scope);
5251
6819
  return rows.filter((r) => !db.isIgnored(r.file)).filter((r) => isTrueStaleAbstraction(r, filesWithFunctions)).map((r) => ({
5252
6820
  symbol: r.symbol,
@@ -5259,22 +6827,11 @@ function staleAbstractions(db, opts) {
5259
6827
  })).slice(0, limit);
5260
6828
  }
5261
6829
  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
- );
6830
+ return new Set(getScopedDefinitions4(db, scope).filter((definition) => definition.isFunctionLike).map((definition) => definition.relativePath));
5274
6831
  }
5275
6832
  function isTrueStaleAbstraction(row, filesWithFunctions) {
5276
- const basename2 = row.file.split("/").pop() ?? "";
5277
- const isTypeFile = basename2.includes("types") || row.file.includes("/types/");
6833
+ const basename5 = row.file.split("/").pop() ?? "";
6834
+ const isTypeFile = basename5.includes("types") || row.file.includes("/types/");
5278
6835
  if (isTypeFile && row.consumers > 0) {
5279
6836
  return false;
5280
6837
  }
@@ -5283,6 +6840,33 @@ function isTrueStaleAbstraction(row, filesWithFunctions) {
5283
6840
  }
5284
6841
  return true;
5285
6842
  }
6843
+ function getScopedDefinitions4(db, scope) {
6844
+ const scopeFilter = scope ? `AND relative_path LIKE '%${scope}%'` : "";
6845
+ return db.all(
6846
+ `SELECT relative_path
6847
+ FROM documents
6848
+ WHERE 1 = 1
6849
+ ${db.pathExclusionsFor("documents")}
6850
+ ${scopeFilter}
6851
+ ORDER BY relative_path`
6852
+ ).flatMap((row) => getDefinitionsForFile(db, row.relative_path)).filter((row) => !db.isIgnored(row.relativePath));
6853
+ }
6854
+ function countCrossFileConsumers(db, definition) {
6855
+ const callers = db.all(
6856
+ `SELECT DISTINCT d.relative_path
6857
+ FROM mentions m
6858
+ JOIN chunks c ON m.chunk_id = c.id
6859
+ JOIN documents d ON c.document_id = d.id
6860
+ WHERE m.symbol_id = ?
6861
+ AND m.role != 1
6862
+ ${db.pathExclusionsFor("d")}`,
6863
+ definition.symbolId
6864
+ ).map((row) => row.relative_path).filter((relativePath) => relativePath !== definition.relativePath && !db.isIgnored(relativePath));
6865
+ return new Set(callers).size;
6866
+ }
6867
+ function definitionLoc4(definition) {
6868
+ return definition.endLine - definition.startLine + 1;
6869
+ }
5286
6870
 
5287
6871
  // src/queries/complexity-hotspots.ts
5288
6872
  function complexityHotspots(db, opts) {
@@ -5359,7 +6943,7 @@ function complexityHotspots(db, opts) {
5359
6943
  minLoc,
5360
6944
  limit
5361
6945
  );
5362
- return rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
6946
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
5363
6947
  symbol: r.symbol,
5364
6948
  shortName: shortenSymbol(r.symbol),
5365
6949
  file: r.file,
@@ -5373,6 +6957,31 @@ function complexityHotspots(db, opts) {
5373
6957
  r.loc / 50 * (r.fan_in / 5) * Math.max(r.fan_out / 5, 1) * 100
5374
6958
  ) / 100
5375
6959
  }));
6960
+ if (indexedResults.length > 0) {
6961
+ return indexedResults;
6962
+ }
6963
+ return getAllDefinitions(db, { scope }).filter((definition) => !db.isIgnored(definition.relativePath)).map((definition) => {
6964
+ const loc = definition.endLine - definition.startLine + 1;
6965
+ const callerRows = getCallerRowsForSymbol(db, definition, { limit: 500 });
6966
+ const calleeRows = getCalleeRowsForSymbol(db, definition, { limit: 500 });
6967
+ const fanIn2 = new Set(callerRows.map((row) => row.file)).size;
6968
+ const fanOut2 = new Set(
6969
+ calleeRows.filter((row) => row.file !== definition.relativePath).map((row) => `${row.symbol}|${row.file}`)
6970
+ ).size;
6971
+ const calleeCount = new Set(calleeRows.map((row) => `${row.symbol}|${row.file}`)).size;
6972
+ return {
6973
+ symbol: definition.symbol,
6974
+ shortName: shortenSymbol(definition.symbol),
6975
+ file: definition.relativePath,
6976
+ startLine: definition.startLine,
6977
+ endLine: definition.endLine,
6978
+ loc,
6979
+ fanIn: fanIn2,
6980
+ fanOut: fanOut2,
6981
+ calleeCount,
6982
+ score: Math.round(loc / 50 * (fanIn2 / 5) * Math.max(fanOut2 / 5, 1) * 100) / 100
6983
+ };
6984
+ }).filter((row) => row.loc >= minLoc).sort((left, right) => right.score - left.score || right.loc - left.loc).slice(0, limit);
5376
6985
  }
5377
6986
 
5378
6987
  // src/queries/health.ts
@@ -5621,7 +7230,7 @@ function convergence(db, symbolPatternA, symbolPatternB) {
5621
7230
 
5622
7231
  // src/queries/code.ts
5623
7232
  import { readFileSync as readFileSync5 } from "fs";
5624
- import { join as join7 } from "path";
7233
+ import { join as join9 } from "path";
5625
7234
  function code(db, symbolPattern, opts = {}) {
5626
7235
  const { context = 0 } = opts;
5627
7236
  const fileLineMatch = symbolPattern.match(/^(.+\.\w+):(\d+)-(\d+)$/);
@@ -5634,7 +7243,7 @@ function code(db, symbolPattern, opts = {}) {
5634
7243
  `SELECT language FROM documents WHERE relative_path = ?`,
5635
7244
  match.relativePath
5636
7245
  );
5637
- const filePath = join7(db.config.projectRoot, match.relativePath);
7246
+ const filePath = join9(db.config.projectRoot, match.relativePath);
5638
7247
  let fileContent;
5639
7248
  try {
5640
7249
  fileContent = readFileSync5(filePath, "utf-8");
@@ -5664,7 +7273,7 @@ function readFileRange(db, filePath, startLine, endLine, context) {
5664
7273
  resolvedPath
5665
7274
  );
5666
7275
  if (!doc) return null;
5667
- const fullPath = join7(db.config.projectRoot, doc.relative_path);
7276
+ const fullPath = join9(db.config.projectRoot, doc.relative_path);
5668
7277
  let fileContent;
5669
7278
  try {
5670
7279
  fileContent = readFileSync5(fullPath, "utf-8");
@@ -5688,7 +7297,7 @@ function readFileRange(db, filePath, startLine, endLine, context) {
5688
7297
 
5689
7298
  // src/queries/complexity.ts
5690
7299
  import { readFileSync as readFileSync6 } from "fs";
5691
- import { join as join8 } from "path";
7300
+ import { join as join10 } from "path";
5692
7301
  function complexity(db, symbolPattern) {
5693
7302
  const match = findFirstSymbolMatch(db, symbolPattern);
5694
7303
  if (!match) return null;
@@ -5697,7 +7306,7 @@ function complexity(db, symbolPattern) {
5697
7306
  match.relativePath
5698
7307
  );
5699
7308
  const language = doc?.language ?? "unknown";
5700
- const filePath = join8(db.config.projectRoot, match.relativePath);
7309
+ const filePath = join10(db.config.projectRoot, match.relativePath);
5701
7310
  let source = "";
5702
7311
  try {
5703
7312
  const lines = readFileSync6(filePath, "utf-8").split("\n");
@@ -6037,46 +7646,100 @@ function redundantReexports(db, opts = {}) {
6037
7646
  results.sort(
6038
7647
  (a, b) => b.directConsumers - a.directConsumers || a.barrelFile.localeCompare(b.barrelFile) || a.shortName.localeCompare(b.shortName)
6039
7648
  );
6040
- return limit ? results.slice(0, limit) : results;
7649
+ const withDartFallback = dedupeReexports([
7650
+ ...results,
7651
+ ...findSourceRedundantReexports(db, scope)
7652
+ ]);
7653
+ withDartFallback.sort(
7654
+ (a, b) => b.directConsumers - a.directConsumers || a.barrelFile.localeCompare(b.barrelFile) || a.shortName.localeCompare(b.shortName)
7655
+ );
7656
+ return limit ? withDartFallback.slice(0, limit) : withDartFallback;
7657
+ }
7658
+ function findSourceRedundantReexports(db, scope) {
7659
+ const files2 = db.all(
7660
+ `SELECT relative_path
7661
+ FROM documents
7662
+ WHERE 1 = 1
7663
+ ${scope ? "AND relative_path LIKE ?" : ""}
7664
+ ${db.pathExclusionsFor("documents")}
7665
+ ORDER BY relative_path`,
7666
+ ...scope ? [`%${scope}%`] : []
7667
+ );
7668
+ const candidates = files2.map((row) => row.relative_path).filter((relativePath) => !db.isIgnored(relativePath)).filter((relativePath) => getSourceExports(db, relativePath).length > 0);
7669
+ const results = [];
7670
+ for (const barrelPath of candidates) {
7671
+ const exports = getSourceExports(db, barrelPath).filter((entry) => entry.sourcePath && !db.isIgnored(entry.sourcePath));
7672
+ if (exports.length === 0) continue;
7673
+ const barrelConsumers = countDirectImporters(db, barrelPath, barrelPath);
7674
+ if (barrelConsumers > 0) continue;
7675
+ for (const exported of exports) {
7676
+ const sourcePath = exported.sourcePath;
7677
+ const representative = representativeExportSymbol(db, sourcePath);
7678
+ if (!representative) continue;
7679
+ results.push({
7680
+ barrelFile: barrelPath,
7681
+ symbol: representative.symbol,
7682
+ shortName: shortenSymbol(representative.symbol),
7683
+ originalFile: sourcePath,
7684
+ barrelConsumers: 0,
7685
+ directConsumers: countDirectImporters(db, sourcePath, barrelPath)
7686
+ });
7687
+ }
7688
+ }
7689
+ return results;
7690
+ }
7691
+ function countDirectImporters(db, targetPath, excludedPath) {
7692
+ const files2 = db.all(
7693
+ `SELECT relative_path
7694
+ FROM documents
7695
+ WHERE 1 = 1
7696
+ ${db.pathExclusionsFor("documents")}
7697
+ ORDER BY relative_path`
7698
+ );
7699
+ const importers = /* @__PURE__ */ new Set();
7700
+ for (const row of files2) {
7701
+ if (db.isIgnored(row.relative_path) || row.relative_path === excludedPath) continue;
7702
+ for (const imported of getSourceImports(db, row.relative_path)) {
7703
+ if (imported.sourcePath === targetPath) {
7704
+ importers.add(row.relative_path);
7705
+ }
7706
+ }
7707
+ }
7708
+ return importers.size;
7709
+ }
7710
+ function representativeExportSymbol(db, sourcePath) {
7711
+ const definitions = getDefinitionsForFile(db, sourcePath);
7712
+ return definitions.find((definition) => leafSuffix(definition.symbol) === "method") ?? definitions[0] ?? null;
7713
+ }
7714
+ function dedupeReexports(rows) {
7715
+ const seen = /* @__PURE__ */ new Set();
7716
+ const unique = [];
7717
+ for (const row of rows) {
7718
+ const key = `${row.barrelFile}|${row.symbol}|${row.originalFile}`;
7719
+ if (seen.has(key)) continue;
7720
+ seen.add(key);
7721
+ unique.push(row);
7722
+ }
7723
+ return unique;
6041
7724
  }
6042
7725
 
6043
7726
  // src/queries/similar-signatures.ts
6044
7727
  function similarSignatures(db, opts = {}) {
6045
7728
  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
7729
  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);
7730
+ for (const definition of getAllDefinitions(db, { scope })) {
7731
+ if (!definition.isFunctionLike || db.isIgnored(definition.relativePath)) continue;
7732
+ const loc = definition.endLine - definition.startLine + 1;
7733
+ if (loc < minLoc) continue;
7734
+ const normalized = resolveNormalizedSignature(db, definition);
6072
7735
  if (!normalized) continue;
6073
7736
  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
7737
+ symbol: definition.symbol,
7738
+ shortName: shortenSymbol(definition.symbol),
7739
+ file: definition.relativePath,
7740
+ startLine: definition.startLine,
7741
+ endLine: definition.endLine,
7742
+ loc
6080
7743
  };
6081
7744
  const existing = sigGroups.get(normalized);
6082
7745
  if (existing) {
@@ -6099,6 +7762,52 @@ function similarSignatures(db, opts = {}) {
6099
7762
  });
6100
7763
  return limit ? results.slice(0, limit) : results;
6101
7764
  }
7765
+ function resolveNormalizedSignature(db, definition) {
7766
+ const documented = extractDocumentedSignature(definition.documentation);
7767
+ const normalizedDocumented = documented ? normalizeSignature(documented) : null;
7768
+ if (normalizedDocumented) {
7769
+ return normalizedDocumented;
7770
+ }
7771
+ return normalizeSourceSignature(
7772
+ extractDeclarationHead(db, definition.relativePath, definition.startLine, definition.endLine, definition.leaf),
7773
+ definition.leaf
7774
+ );
7775
+ }
7776
+ function extractDocumentedSignature(documentation) {
7777
+ if (!documentation || !documentation.includes("|")) {
7778
+ return null;
7779
+ }
7780
+ const extracted = documentation.slice(documentation.indexOf("|") + 1).replace(/\n/g, " ").trim();
7781
+ return extracted.length > 0 ? extracted : null;
7782
+ }
7783
+ function extractDeclarationHead(db, relativePath, startLine, endLine, leaf) {
7784
+ const source = getSourceText(db, relativePath);
7785
+ if (!source) return null;
7786
+ const lines = source.split(/\r?\n/);
7787
+ const candidates = declarationStartLines(lines, startLine, endLine, leaf);
7788
+ for (const candidate of candidates) {
7789
+ const maxLine = Math.min(lines.length - 1, Math.max(candidate, candidate + 4));
7790
+ let collected = "";
7791
+ for (let lineIndex = candidate; lineIndex <= maxLine; lineIndex += 1) {
7792
+ const line = lines[lineIndex]?.trim();
7793
+ if (!line) continue;
7794
+ collected = collected ? `${collected} ${line}` : line;
7795
+ if (looksCompleteDeclaration(collected)) {
7796
+ return collected;
7797
+ }
7798
+ }
7799
+ if (collected && collected.includes("(")) {
7800
+ return collected;
7801
+ }
7802
+ }
7803
+ return null;
7804
+ }
7805
+ function looksCompleteDeclaration(declaration) {
7806
+ const normalized = declaration.replace(/\s+/g, " ").trim();
7807
+ if (!normalized.includes("(")) return false;
7808
+ if (parenBalance(normalized) > 0) return false;
7809
+ return /[;{]$/.test(normalized) || /\)\s*(?::\s*[^={]+)?\s*(?:=>|=|throws\b|where\b|$)/i.test(normalized) || /\)\s*As\s+.+$/i.test(normalized);
7810
+ }
6102
7811
  function normalizeSignature(raw) {
6103
7812
  if (!raw || !raw.trim()) return null;
6104
7813
  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 +7818,88 @@ function normalizeSignature(raw) {
6109
7818
  if (sig.length < 3) return null;
6110
7819
  return sig;
6111
7820
  }
7821
+ function normalizeSourceSignature(raw, leaf) {
7822
+ if (!raw || !raw.trim()) return null;
7823
+ let declaration = raw.replace(/\s+/g, " ").trim();
7824
+ const parenIdx = declaration.indexOf("(");
7825
+ if (parenIdx === -1) return null;
7826
+ let prefix = declaration.slice(0, parenIdx);
7827
+ const leafPattern = new RegExp(`\\b${escapeRegex3(leaf)}\\b`, "i");
7828
+ const leafMatch = leafPattern.exec(prefix);
7829
+ if (leafMatch && typeof leafMatch.index === "number") {
7830
+ prefix = prefix.slice(0, leafMatch.index);
7831
+ }
7832
+ 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();
7833
+ let 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();
7834
+ if (!suffix.startsWith("(")) {
7835
+ return null;
7836
+ }
7837
+ const normalized = `${prefix ? `${prefix} ` : ""}${suffix}`.replace(/\s+/g, "").toLowerCase();
7838
+ return normalized.length >= 3 ? normalized : null;
7839
+ }
7840
+ function declarationStartLines(lines, startLine, endLine, leaf) {
7841
+ const escapedLeaf = escapeRegex3(leaf);
7842
+ const callablePattern = new RegExp(`\\b${escapedLeaf}\\b\\s*\\(`, "i");
7843
+ const rubyPattern = new RegExp(`\\bdef\\s+${escapedLeaf}\\b`, "i");
7844
+ const candidates = [];
7845
+ const seen = /* @__PURE__ */ new Set();
7846
+ const preferredStart = Math.max(0, Math.min(startLine, lines.length - 1));
7847
+ const preferredEnd = Math.max(preferredStart, Math.min(lines.length - 1, Math.max(endLine, startLine + 4)));
7848
+ for (let lineIndex = preferredStart; lineIndex <= preferredEnd; lineIndex += 1) {
7849
+ const line = lines[lineIndex] ?? "";
7850
+ if ((callablePattern.test(line) || rubyPattern.test(line)) && !seen.has(lineIndex)) {
7851
+ seen.add(lineIndex);
7852
+ candidates.push(lineIndex);
7853
+ }
7854
+ }
7855
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
7856
+ const line = lines[lineIndex] ?? "";
7857
+ if ((callablePattern.test(line) || rubyPattern.test(line)) && !seen.has(lineIndex)) {
7858
+ seen.add(lineIndex);
7859
+ candidates.push(lineIndex);
7860
+ }
7861
+ }
7862
+ return candidates;
7863
+ }
7864
+ function parenBalance(value) {
7865
+ let balance = 0;
7866
+ for (const char of value) {
7867
+ if (char === "(") balance += 1;
7868
+ if (char === ")") balance -= 1;
7869
+ }
7870
+ return balance;
7871
+ }
7872
+ function escapeRegex3(value) {
7873
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7874
+ }
6112
7875
 
6113
7876
  // src/setup.ts
6114
7877
  import {
6115
- existsSync as existsSync7,
7878
+ existsSync as existsSync9,
6116
7879
  mkdirSync as mkdirSync2,
6117
7880
  symlinkSync,
6118
7881
  readlinkSync,
6119
7882
  unlinkSync
6120
7883
  } from "fs";
6121
- import { join as join9, dirname as dirname3, resolve as resolve3 } from "path";
7884
+ import { join as join11, dirname as dirname3, resolve as resolve3 } from "path";
6122
7885
  import { homedir as homedir2, platform as platform3 } from "os";
6123
7886
  import { fileURLToPath } from "url";
6124
7887
  var IS_WINDOWS3 = platform3() === "win32";
6125
- var SKILLS = ["concrete-plan", "scip-explore", "scip-debloat", "scip-verify"];
7888
+ var BUILTIN_SKILLS = [
7889
+ "concrete-plan",
7890
+ "scip-explore",
7891
+ "scip-debloat",
7892
+ "scip-verify",
7893
+ "scip-language-playbook"
7894
+ ];
6126
7895
  function installSkills(opts = {}) {
6127
7896
  const log = opts.quiet ? () => {
6128
7897
  } : console.log;
6129
7898
  const thisFile = fileURLToPath(import.meta.url);
6130
7899
  const skillsSource = resolve3(dirname3(thisFile), "..", "skills");
6131
7900
  const targets = [
6132
- join9(homedir2(), ".claude", "skills"),
6133
- join9(homedir2(), ".codex", "skills")
7901
+ join11(homedir2(), ".claude", "skills"),
7902
+ join11(homedir2(), ".codex", "skills")
6134
7903
  ];
6135
7904
  const result = {
6136
7905
  installed: [],
@@ -6139,19 +7908,19 @@ function installSkills(opts = {}) {
6139
7908
  };
6140
7909
  for (const targetDir of targets) {
6141
7910
  const parentDir = dirname3(targetDir);
6142
- if (!existsSync7(parentDir)) {
7911
+ if (!existsSync9(parentDir)) {
6143
7912
  continue;
6144
7913
  }
6145
7914
  mkdirSync2(targetDir, { recursive: true });
6146
7915
  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)) {
7916
+ for (const skill of BUILTIN_SKILLS) {
7917
+ const source = join11(skillsSource, skill);
7918
+ const target = join11(targetDir, skill);
7919
+ if (!existsSync9(source)) {
6151
7920
  result.skipped.push(`${toolName}/${skill}`);
6152
7921
  continue;
6153
7922
  }
6154
- if (existsSync7(target)) {
7923
+ if (existsSync9(target)) {
6155
7924
  try {
6156
7925
  const existing = readlinkSync(target);
6157
7926
  if (resolve3(existing) === resolve3(source)) {
@@ -6183,14 +7952,14 @@ function resolveProjectRoot() {
6183
7952
  function resolveActiveDbPath(projectRoot) {
6184
7953
  const config = loadProjectConfig(projectRoot);
6185
7954
  const paths = resolveIndexPaths(projectRoot, config);
6186
- return process.env["SCIP_QUERY_INDEX_DB"] ?? (existsSync8(paths.dbPath) ? paths.dbPath : join10(projectRoot, "index.db"));
7955
+ return process.env["SCIP_QUERY_INDEX_DB"] ?? (existsSync10(paths.dbPath) ? paths.dbPath : join12(projectRoot, "index.db"));
6187
7956
  }
6188
7957
  function openDb() {
6189
7958
  const projectRoot = resolveProjectRoot();
6190
7959
  const config = loadProjectConfig(projectRoot);
6191
7960
  const paths = resolveIndexPaths(projectRoot, config);
6192
7961
  const dbPath = resolveActiveDbPath(projectRoot);
6193
- if (!existsSync8(dbPath)) {
7962
+ if (!existsSync10(dbPath)) {
6194
7963
  console.error(`error: No index.db found. Run: scip-query reindex`);
6195
7964
  process.exit(1);
6196
7965
  }
@@ -6361,6 +8130,10 @@ program.command("trace <symbol>").description("Trace a symbol: definition + all
6361
8130
  for (const d of result.definitions) {
6362
8131
  const sig = d.signature ? ` \u2014 ${d.signature}` : "";
6363
8132
  console.log(` ${displayPathRange(d.relativePath, d.startLine, d.endLine)}${sig}`);
8133
+ if (d.source) {
8134
+ const numbered = d.source.split("\n").map((line, index) => ` ${displayLine(d.startLine + index)} ${line}`).join("\n");
8135
+ console.log(numbered);
8136
+ }
6364
8137
  }
6365
8138
  console.log("\n\u2550\u2550\u2550 REFERENCED BY \u2550\u2550\u2550");
6366
8139
  let prevFile = "";
@@ -6767,7 +8540,7 @@ ${results.length} similar pair(s) found.`);
6767
8540
  }
6768
8541
  });
6769
8542
  });
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) => {
8543
+ 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
8544
  runQuery(
6772
8545
  (db) => queries.similarFiles(db, {
6773
8546
  minSimilarity: opts.minSimilarity,
@@ -7167,7 +8940,7 @@ ${result.connectedSymbols.length} connected symbol(s).`);
7167
8940
  }
7168
8941
  db.close();
7169
8942
  });
7170
- program.command("install-skills").description("Install skills (concrete-plan, scip-explore, scip-debloat, scip-verify) into Claude Code and Codex").action(() => {
8943
+ program.command("install-skills").description(`Install skills (${BUILTIN_SKILLS.join(", ")}) into Claude Code and Codex`).action(() => {
7171
8944
  const result = installSkills();
7172
8945
  const total = result.installed.length + result.alreadyLinked.length;
7173
8946
  console.log(`
@@ -7176,12 +8949,41 @@ ${result.installed.length} installed, ${result.alreadyLinked.length} already lin
7176
8949
  console.log("Skills will be available in your next Claude Code / Codex session.");
7177
8950
  }
7178
8951
  });
7179
- program.command("check-deps").description("Check if required dependencies (scip CLI) are installed").action(() => {
8952
+ program.command("check-deps").description("Check whether scip-query and the detected language indexers are actually runnable").action(() => {
8953
+ let hasProblems = false;
7180
8954
  if (isScipInstalled()) {
7181
8955
  console.log("scip CLI: installed");
7182
8956
  } else {
7183
8957
  printScipInstallInstructions();
8958
+ hasProblems = true;
8959
+ }
8960
+ const projectRoot = resolveProjectRoot();
8961
+ const config = loadProjectConfig(projectRoot);
8962
+ const languages = config.languages ?? detectLanguages(projectRoot);
8963
+ if (languages.length === 0) {
8964
+ console.log("\nNo supported project languages detected in the current directory.");
8965
+ process.exitCode = hasProblems ? 1 : 0;
8966
+ return;
8967
+ }
8968
+ console.log(`
8969
+ Detected languages: ${languages.join(", ")}`);
8970
+ console.log("\nIndexer readiness:");
8971
+ for (const language of languages) {
8972
+ const status = getIndexerDependencyStatus(getIndexerConfig(language), projectRoot);
8973
+ const prefix = status.runnable ? " OK" : status.installed ? " WARN" : " MISSING";
8974
+ const resolved = status.resolvedBinary ? ` (${status.resolvedBinary})` : "";
8975
+ console.log(`${prefix} ${language}: ${status.binaryLabel}${resolved}`);
8976
+ if (status.note) {
8977
+ console.log(` ${status.note}`);
8978
+ }
8979
+ if (!status.installed && status.installUrl) {
8980
+ console.log(` install: ${status.installUrl}`);
8981
+ }
8982
+ if (!status.runnable) {
8983
+ hasProblems = true;
8984
+ }
7184
8985
  }
8986
+ process.exitCode = hasProblems ? 1 : 0;
7185
8987
  });
7186
8988
  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
8989
  const db = openDb();
@@ -7269,8 +9071,8 @@ program.command("status").description("Show index status for this project").acti
7269
9071
  if (dbPath !== paths.dbPath) {
7270
9072
  console.log(`Config: ${paths.dbPath} (fallback to project root index.db)`);
7271
9073
  }
7272
- console.log(`Exists: ${existsSync8(dbPath) ? "yes" : "no"}`);
7273
- if (existsSync8(dbPath)) {
9074
+ console.log(`Exists: ${existsSync10(dbPath) ? "yes" : "no"}`);
9075
+ if (existsSync10(dbPath)) {
7274
9076
  withDb((db) => {
7275
9077
  const s = queries.stats(db);
7276
9078
  console.log(`Symbols: ${s.symbols}`);