scip-query 0.3.4 → 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 (154) hide show
  1. package/README.md +2 -26
  2. package/dist/{chunk-FYYOWQXK.js → chunk-2F2WH5WQ.js} +19 -36
  3. package/dist/{chunk-C7H5WBTJ.js → chunk-2UISVZGQ.js} +2 -2
  4. package/dist/{chunk-HLKAFWWJ.js → chunk-4ZT7UGWW.js} +56 -91
  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-FVH3Y44U.js +1 -0
  18. package/dist/chunk-GIBETK3W.js +37 -0
  19. package/dist/{chunk-SMDCNPMK.js → chunk-H3FPW5YN.js} +2 -2
  20. package/dist/{chunk-GJDHTTR2.js → chunk-HNURMDF4.js} +32 -17
  21. package/dist/{chunk-7KIMF5PV.js → chunk-HRDPUTIQ.js} +2 -2
  22. package/dist/{chunk-OIDHN6GD.js → chunk-I2JM34UV.js} +146 -5
  23. package/dist/{chunk-EPWLXXBL.js → chunk-IV6NZ426.js} +27 -29
  24. package/dist/{chunk-VT4JBH6L.js → chunk-KDCQJTYW.js} +2 -2
  25. package/dist/{chunk-5OMVSV6E.js → chunk-LOVDB4C6.js} +2 -2
  26. package/dist/chunk-MA3B3IUT.js +75 -0
  27. package/dist/{chunk-26DOJ63W.js → chunk-N2LH3M2P.js} +13 -2
  28. package/dist/chunk-NWCJWA36.js +162 -0
  29. package/dist/{chunk-NG5F43OU.js → chunk-P3VCDYMJ.js} +70 -1
  30. package/dist/{chunk-P3E6L7KW.js → chunk-P42KQKJZ.js} +83 -3
  31. package/dist/{chunk-YDBXNPYU.js → chunk-PGHN5UTM.js} +2 -2
  32. package/dist/{chunk-UGQKAVCD.js → chunk-QCYR4S6T.js} +2 -2
  33. package/dist/chunk-QGCEAVJD.js +2529 -0
  34. package/dist/{chunk-KBOQX573.js → chunk-RIEA5DOB.js} +19 -2
  35. package/dist/{chunk-VIYSWZCO.js → chunk-SL674KAW.js} +31 -3
  36. package/dist/chunk-SRELHCMG.js +110 -0
  37. package/dist/chunk-UTRKBUCB.js +87 -0
  38. package/dist/{chunk-HJZUSUPU.js → chunk-VCOJRQPP.js} +5 -5
  39. package/dist/chunk-VISMEWYP.js +34 -0
  40. package/dist/{chunk-LFJQVJYJ.js → chunk-VU7FDTWV.js} +2 -2
  41. package/dist/{chunk-HLUS2HEB.js → chunk-VUBLUTMU.js} +5 -4
  42. package/dist/{chunk-WGAD3GNR.js → chunk-WNPF2I25.js} +5 -5
  43. package/dist/{chunk-YY4QGUQ5.js → chunk-X3J4VPWM.js} +2 -2
  44. package/dist/{chunk-GSH2FPKV.js → chunk-XH56HXLC.js} +2 -2
  45. package/dist/{chunk-DH7G3DDV.js → chunk-ZU2AQQB5.js} +2 -2
  46. package/dist/cli.js +2722 -1036
  47. package/dist/{db-ShvwGDKf.d.ts → db-C4rPbKkI.d.ts} +7 -14
  48. package/dist/index.d.ts +4 -5
  49. package/dist/index.js +378 -149
  50. package/dist/postinstall.js +9 -3
  51. package/dist/queries/affected.d.ts +1 -1
  52. package/dist/queries/affected.js +2 -2
  53. package/dist/queries/bottlenecks.d.ts +1 -1
  54. package/dist/queries/bottlenecks.js +2 -1
  55. package/dist/queries/by-kind.d.ts +1 -1
  56. package/dist/queries/by-kind.js +2 -1
  57. package/dist/queries/call-graph.d.ts +1 -1
  58. package/dist/queries/call-graph.js +2 -2
  59. package/dist/queries/change-surface.d.ts +1 -1
  60. package/dist/queries/change-surface.js +2 -2
  61. package/dist/queries/code.d.ts +1 -1
  62. package/dist/queries/code.js +2 -2
  63. package/dist/queries/complexity-hotspots.d.ts +1 -1
  64. package/dist/queries/complexity-hotspots.js +2 -2
  65. package/dist/queries/complexity.d.ts +1 -1
  66. package/dist/queries/complexity.js +2 -2
  67. package/dist/queries/convergence.d.ts +1 -1
  68. package/dist/queries/convergence.js +2 -2
  69. package/dist/queries/coupling.d.ts +1 -1
  70. package/dist/queries/coupling.js +2 -2
  71. package/dist/queries/cycles.d.ts +1 -1
  72. package/dist/queries/cycles.js +2 -2
  73. package/dist/queries/dataflow.d.ts +1 -1
  74. package/dist/queries/dataflow.js +2 -2
  75. package/dist/queries/dead.d.ts +1 -1
  76. package/dist/queries/dead.js +3 -3
  77. package/dist/queries/deep-chains.d.ts +1 -1
  78. package/dist/queries/deep-chains.js +2 -2
  79. package/dist/queries/deps.d.ts +1 -1
  80. package/dist/queries/deps.js +2 -2
  81. package/dist/queries/diff-impact.d.ts +1 -1
  82. package/dist/queries/drift.d.ts +1 -1
  83. package/dist/queries/drift.js +2 -2
  84. package/dist/queries/extract-candidates.d.ts +1 -1
  85. package/dist/queries/extract-candidates.js +2 -2
  86. package/dist/queries/fan.d.ts +1 -1
  87. package/dist/queries/fan.js +2 -2
  88. package/dist/queries/files.d.ts +1 -1
  89. package/dist/queries/health.d.ts +1 -1
  90. package/dist/queries/health.js +13 -13
  91. package/dist/queries/hierarchy.d.ts +1 -1
  92. package/dist/queries/hierarchy.js +2 -2
  93. package/dist/queries/hotspots.d.ts +1 -1
  94. package/dist/queries/hotspots.js +2 -1
  95. package/dist/queries/imports.d.ts +1 -1
  96. package/dist/queries/imports.js +2 -2
  97. package/dist/queries/index.d.ts +1 -2
  98. package/dist/queries/index.js +49 -53
  99. package/dist/queries/isolated.d.ts +3 -4
  100. package/dist/queries/isolated.js +3 -3
  101. package/dist/queries/members.d.ts +1 -1
  102. package/dist/queries/members.js +2 -2
  103. package/dist/queries/methods.d.ts +1 -1
  104. package/dist/queries/methods.js +2 -1
  105. package/dist/queries/outline.d.ts +1 -1
  106. package/dist/queries/outline.js +2 -2
  107. package/dist/queries/passthrough-candidates.d.ts +1 -1
  108. package/dist/queries/passthrough-candidates.js +2 -2
  109. package/dist/queries/redundant-reexports.d.ts +1 -1
  110. package/dist/queries/redundant-reexports.js +3 -3
  111. package/dist/queries/refs.d.ts +1 -1
  112. package/dist/queries/refs.js +2 -2
  113. package/dist/queries/similar-chains.d.ts +1 -1
  114. package/dist/queries/similar-chains.js +2 -2
  115. package/dist/queries/similar-files.d.ts +1 -1
  116. package/dist/queries/similar-files.js +2 -2
  117. package/dist/queries/similar-signatures.d.ts +5 -3
  118. package/dist/queries/similar-signatures.js +2 -1
  119. package/dist/queries/similar.d.ts +1 -1
  120. package/dist/queries/similar.js +2 -2
  121. package/dist/queries/slice.d.ts +1 -1
  122. package/dist/queries/slice.js +2 -2
  123. package/dist/queries/stale-abstractions.d.ts +1 -1
  124. package/dist/queries/stale-abstractions.js +2 -2
  125. package/dist/queries/stats.d.ts +1 -1
  126. package/dist/queries/surface.d.ts +1 -1
  127. package/dist/queries/surface.js +2 -2
  128. package/dist/queries/symbols.d.ts +1 -1
  129. package/dist/queries/symbols.js +2 -2
  130. package/dist/queries/system.d.ts +1 -1
  131. package/dist/queries/system.js +2 -2
  132. package/dist/queries/trace.d.ts +1 -1
  133. package/dist/queries/trace.js +2 -2
  134. package/dist/queries/wrapper-candidates.d.ts +1 -1
  135. package/dist/queries/wrapper-candidates.js +2 -2
  136. package/dist/reindex-worker.js +213 -62
  137. package/package.json +1 -1
  138. package/skills/scip-language-playbook/SKILL.md +371 -0
  139. package/dist/chunk-2UELLEBI.js +0 -1
  140. package/dist/chunk-4JCSOF2O.js +0 -97
  141. package/dist/chunk-AXQKUYKF.js +0 -1442
  142. package/dist/chunk-CPVAQJEC.js +0 -46
  143. package/dist/chunk-EOROMIFO.js +0 -41
  144. package/dist/chunk-GU2H5QRN.js +0 -28
  145. package/dist/chunk-LQJUPXQY.js +0 -109
  146. package/dist/chunk-MPGIHELS.js +0 -39
  147. package/dist/chunk-P4WO3BBW.js +0 -64
  148. package/dist/chunk-TOIEB3LG.js +0 -78
  149. package/dist/chunk-UQEQ6AHX.js +0 -60
  150. package/dist/chunk-VJJKSGIX.js +0 -121
  151. package/dist/chunk-YZ6L7GFO.js +0 -73
  152. package/dist/chunk-ZEUCXQBN.js +0 -71
  153. package/dist/queries/doc-coverage.d.ts +0 -14
  154. package/dist/queries/doc-coverage.js +0 -8
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
@@ -2578,42 +3864,112 @@ function hasUniqueLeafDefinition(db, leaf, symbolId) {
2578
3864
  LIMIT 50`,
2579
3865
  `%${leaf}%`
2580
3866
  );
2581
- let count = 0;
2582
- for (const row of rows) {
2583
- if (leafName(row.symbol) !== leaf) continue;
2584
- count++;
2585
- if (count > 1 && row.id !== symbolId) {
2586
- return false;
2587
- }
2588
- }
2589
- return count === 1;
3867
+ let count = 0;
3868
+ for (const row of rows) {
3869
+ if (leafName(row.symbol) !== leaf) continue;
3870
+ count++;
3871
+ if (count > 1 && row.id !== symbolId) {
3872
+ return false;
3873
+ }
3874
+ }
3875
+ return count === 1;
3876
+ }
3877
+ function parentTypeName(rawSymbol) {
3878
+ const parsed = parseSymbol(rawSymbol);
3879
+ if ("kind" in parsed) {
3880
+ return null;
3881
+ }
3882
+ for (let index = parsed.descriptors.length - 2; index >= 0; index--) {
3883
+ const descriptor = parsed.descriptors[index];
3884
+ if (descriptor?.suffix === "type") {
3885
+ return descriptor.name;
3886
+ }
3887
+ }
3888
+ return null;
3889
+ }
3890
+ function isPythonDocument(db, relativePath) {
3891
+ const row = db.get(
3892
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3893
+ relativePath
3894
+ );
3895
+ return row?.language === "python" || relativePath.endsWith(".py") || relativePath.endsWith(".pyi");
3896
+ }
3897
+ function isJavaScriptDocument(db, relativePath) {
3898
+ const row = db.get(
3899
+ `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
3900
+ relativePath
3901
+ );
3902
+ return row?.language === "typescript" || row?.language === "javascript" || /\.(?:[cm]?[jt]sx?)$/.test(relativePath);
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);
2590
3945
  }
2591
- function parentTypeName(rawSymbol) {
2592
- const parsed = parseSymbol(rawSymbol);
2593
- if ("kind" in parsed) {
2594
- return null;
2595
- }
2596
- for (let index = parsed.descriptors.length - 2; index >= 0; index--) {
2597
- const descriptor = parsed.descriptors[index];
2598
- if (descriptor?.suffix === "type") {
2599
- return descriptor.name;
2600
- }
2601
- }
2602
- return null;
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");
2603
3952
  }
2604
- function isPythonDocument(db, relativePath) {
3953
+ function isRubyDocument(db, relativePath) {
2605
3954
  const row = db.get(
2606
3955
  `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
2607
3956
  relativePath
2608
3957
  );
2609
- return row?.language === "python" || relativePath.endsWith(".py") || relativePath.endsWith(".pyi");
3958
+ return row?.language === "ruby" || relativePath.endsWith(".rb");
2610
3959
  }
2611
- function isJavaScriptDocument(db, relativePath) {
3960
+ function isDartDocument(db, relativePath) {
2612
3961
  const row = db.get(
2613
3962
  `SELECT language FROM documents WHERE relative_path = ? LIMIT 1`,
2614
3963
  relativePath
2615
3964
  );
2616
- return row?.language === "typescript" || row?.language === "javascript" || /\.(?:[cm]?[jt]sx?)$/.test(relativePath);
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");
2617
3973
  }
2618
3974
  function applyLimit(values, limit) {
2619
3975
  return typeof limit === "number" ? values.slice(0, limit) : values;
@@ -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) {
@@ -3374,21 +4865,22 @@ function members(db, symbolPattern) {
3374
4865
 
3375
4866
  // src/queries/fan.ts
3376
4867
  function fanIn(db, symbolPattern) {
3377
- const rows = db.all(
3378
- `SELECT gs.symbol, COUNT(DISTINCT c.document_id) AS file_count
3379
- FROM mentions m
3380
- JOIN chunks c ON m.chunk_id = c.id
3381
- JOIN global_symbols gs ON m.symbol_id = gs.id
3382
- WHERE gs.symbol LIKE ?
3383
- AND m.role != 1
3384
- GROUP BY gs.id
3385
- ORDER BY file_count DESC`,
3386
- `%${symbolPattern}%`
4868
+ const match = findFirstSymbolMatch(db, symbolPattern);
4869
+ if (!match) {
4870
+ return [];
4871
+ }
4872
+ const row = db.get(
4873
+ `SELECT COUNT(DISTINCT c.document_id) AS file_count
4874
+ FROM mentions m
4875
+ JOIN chunks c ON m.chunk_id = c.id
4876
+ WHERE m.symbol_id = ?
4877
+ AND m.role != 1`,
4878
+ match.symbolId
3387
4879
  );
3388
- return rows.map((r) => ({
3389
- name: shortenSymbol(r.symbol),
3390
- count: r.file_count
3391
- }));
4880
+ return [{
4881
+ name: shortenSymbol(match.symbol),
4882
+ count: row?.file_count ?? 0
4883
+ }];
3392
4884
  }
3393
4885
  function fanOut(db, filePattern) {
3394
4886
  const resolvedFile = resolveIndexedFile(db, filePattern);
@@ -3410,10 +4902,22 @@ function fanOut(db, filePattern) {
3410
4902
  ORDER BY symbol_count DESC`,
3411
4903
  resolvedFile
3412
4904
  );
3413
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
4905
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
3414
4906
  name: r.relative_path,
3415
4907
  count: r.symbol_count
3416
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
+ }];
3417
4921
  }
3418
4922
  function topFanIn(db, opts = {}) {
3419
4923
  const { limit = 30, scope } = opts;
@@ -3632,59 +5136,50 @@ function bottlenecks(db, opts = {}) {
3632
5136
  minFanOut,
3633
5137
  limit
3634
5138
  );
3635
- return rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
3636
- symbol: r.symbol,
3637
- shortName: shortenSymbol(r.symbol),
3638
- fanIn: r.fan_in,
3639
- fanOut: r.fan_out,
3640
- score: r.fan_in * r.fan_out,
3641
- definedIn: r.defined_in
3642
- }));
3643
- }
3644
-
3645
- // src/queries/isolated.ts
3646
- function isolated(db, opts = {}) {
3647
- const { scope, minLoc = 3 } = opts;
3648
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3649
- const rows = db.all(
3650
- `SELECT
3651
- gs.symbol,
3652
- d.relative_path,
3653
- der.start_line,
3654
- der.end_line,
3655
- (der.end_line - der.start_line + 1) AS loc
3656
- FROM global_symbols gs
3657
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3658
- JOIN documents d ON der.document_id = d.id
3659
- WHERE 1 = 1
3660
- ${db.pathExclusionsFor("d")}
3661
- AND ${testFileExclusionSql("d")}
3662
- ${db.symbolNoiseFor("gs")}
3663
- AND gs.symbol NOT LIKE '%#%'
3664
- AND (der.end_line - der.start_line + 1) >= ?
3665
- ${scopeFilter}
3666
- -- No cross-file references TO this symbol
3667
- AND NOT EXISTS (
3668
- SELECT 1 FROM mentions m
3669
- JOIN chunks c ON m.chunk_id = c.id
3670
- WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id != d.id
3671
- )
3672
- -- No same-file references either
3673
- AND NOT EXISTS (
3674
- SELECT 1 FROM mentions m
3675
- JOIN chunks c ON m.chunk_id = c.id
3676
- WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id = d.id
3677
- )
3678
- ORDER BY loc DESC, d.relative_path`,
3679
- minLoc
3680
- );
3681
- return rows.filter((r) => !db.isIgnored(r.relative_path)).filter((r) => !isEntrySurface(db, r.relative_path)).map((r) => ({
5139
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.defined_in)).map((r) => ({
3682
5140
  symbol: r.symbol,
3683
- shortName: shortenSymbol(r.symbol),
3684
- relativePath: r.relative_path,
3685
- startLine: r.start_line,
3686
- endLine: r.end_line,
3687
- loc: r.loc
5141
+ shortName: shortenSymbol(r.symbol),
5142
+ fanIn: r.fan_in,
5143
+ fanOut: r.fan_out,
5144
+ score: r.fan_in * r.fan_out,
5145
+ definedIn: r.defined_in
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);
5166
+ }
5167
+
5168
+ // src/queries/isolated.ts
5169
+ function isolated(db, opts = {}) {
5170
+ const { scope, minLoc = 3 } = opts;
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
3688
5183
  }));
3689
5184
  }
3690
5185
 
@@ -3799,102 +5294,26 @@ function byKind(db, kindQuery, opts = {}) {
3799
5294
  if (kindNum === null) {
3800
5295
  return [];
3801
5296
  }
3802
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3803
- const hasKinds = db.get(
3804
- `SELECT COUNT(*) AS c FROM global_symbols WHERE kind IS NOT NULL`
3805
- );
3806
- if (!hasKinds || hasKinds.c === 0) {
3807
- return inferByKind(db, kindNum, scope, limit);
3808
- }
3809
- const rows = db.all(
3810
- `SELECT gs.symbol, gs.kind, d.relative_path, der.start_line, der.end_line
3811
- FROM global_symbols gs
3812
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3813
- JOIN documents d ON der.document_id = d.id
3814
- WHERE gs.kind = ?
3815
- ${db.pathExclusionsFor("d")}
3816
- ${scopeFilter}
3817
- ORDER BY d.relative_path, der.start_line
3818
- LIMIT ?`,
3819
- kindNum,
3820
- limit
3821
- );
3822
- return rows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
3823
- symbol: r.symbol,
3824
- shortName: shortenSymbol(r.symbol),
3825
- kind: r.kind,
3826
- kindName: KIND_NAMES[r.kind] ?? "Unknown",
3827
- relativePath: r.relative_path,
3828
- startLine: r.start_line,
3829
- endLine: r.end_line
3830
- }));
3831
- }
3832
- function kindCounts(db, opts = {}) {
3833
- const scopeFilter = opts.scope ? `AND d.relative_path LIKE '%${opts.scope}%'` : "";
3834
- const rows = db.all(
3835
- `SELECT gs.kind, COUNT(*) AS cnt
3836
- FROM global_symbols gs
3837
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3838
- JOIN documents d ON der.document_id = d.id
3839
- WHERE 1 = 1
3840
- ${db.pathExclusionsFor("d")}
3841
- AND gs.kind IS NOT NULL
3842
- AND gs.kind != 0
3843
- ${scopeFilter}
3844
- GROUP BY gs.kind
3845
- ORDER BY cnt DESC`
3846
- );
3847
- if (rows.length === 0) {
3848
- return inferKindCounts(db, opts.scope);
3849
- }
3850
- return rows.map((r) => ({
3851
- kind: r.kind,
3852
- kindName: KIND_NAMES[r.kind] ?? "Unknown",
3853
- count: r.cnt
3854
- }));
3855
- }
3856
- function inferByKind(db, kindNum, scope, limit = 100) {
3857
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3858
- const rows = db.all(
3859
- `SELECT gs.symbol, d.relative_path, der.start_line, der.end_line, gs.documentation, gs.enclosing_symbol
3860
- FROM global_symbols gs
3861
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3862
- JOIN documents d ON der.document_id = d.id
3863
- WHERE 1 = 1
3864
- ${db.pathExclusionsFor("d")}
3865
- ${scopeFilter}
3866
- ORDER BY d.relative_path, der.start_line`
3867
- );
3868
- return rows.filter((row) => !db.isIgnored(row.relative_path)).map((row) => ({
5297
+ const rows = loadKindRows(db, scope).map((row) => ({
3869
5298
  row,
3870
- inferredKind: inferKindNumber(row.symbol, row.documentation, row.enclosing_symbol)
3871
- })).filter((entry) => entry.inferredKind === kindNum).slice(0, limit).map(({ row, inferredKind }) => ({
5299
+ resolvedKind: resolveKindNumber(row)
5300
+ })).filter((entry) => entry.resolvedKind === kindNum).slice(0, limit);
5301
+ return rows.map(({ row, resolvedKind }) => ({
3872
5302
  symbol: row.symbol,
3873
5303
  shortName: shortenSymbol(row.symbol),
3874
- kind: inferredKind,
3875
- kindName: KIND_NAMES[inferredKind] ?? "Unknown",
5304
+ kind: resolvedKind,
5305
+ kindName: KIND_NAMES[resolvedKind] ?? "Unknown",
3876
5306
  relativePath: row.relative_path,
3877
5307
  startLine: row.start_line,
3878
5308
  endLine: row.end_line
3879
5309
  }));
3880
5310
  }
3881
- function inferKindCounts(db, scope) {
3882
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3883
- const rows = db.all(
3884
- `SELECT gs.symbol, gs.documentation, gs.enclosing_symbol, d.relative_path
3885
- FROM global_symbols gs
3886
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3887
- JOIN documents d ON der.document_id = d.id
3888
- WHERE 1 = 1
3889
- ${db.pathExclusionsFor("d")}
3890
- ${scopeFilter}`
3891
- );
5311
+ function kindCounts(db, opts = {}) {
3892
5312
  const counts = /* @__PURE__ */ new Map();
3893
- for (const row of rows) {
3894
- if (db.isIgnored(row.relative_path)) continue;
3895
- const inferred = inferKindNumber(row.symbol, row.documentation, row.enclosing_symbol);
3896
- if (inferred === null || inferred === 0) continue;
3897
- counts.set(inferred, (counts.get(inferred) ?? 0) + 1);
5313
+ for (const row of loadKindRows(db, opts.scope)) {
5314
+ const kind = resolveKindNumber(row);
5315
+ if (kind === null || kind === 0) continue;
5316
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
3898
5317
  }
3899
5318
  return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).map(([kind, count]) => ({
3900
5319
  kind,
@@ -3902,22 +5321,60 @@ function inferKindCounts(db, scope) {
3902
5321
  count
3903
5322
  }));
3904
5323
  }
5324
+ function loadKindRows(db, scope) {
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
+ };
5337
+ }
5338
+ function resolveKindNumber(row) {
5339
+ if (row.kind !== null && row.kind !== 0) {
5340
+ return normalizeIndexedKind(row.kind, row.symbol, row.documentation);
5341
+ }
5342
+ return inferKindNumber(row.symbol, row.documentation, row.enclosing_symbol);
5343
+ }
5344
+ function normalizeIndexedKind(kind, symbol, documentation) {
5345
+ const signature = (documentation ?? "").toLowerCase();
5346
+ const suffix = leafSuffix(symbol);
5347
+ if (suffix === "type") {
5348
+ if (signature.includes("type ")) return 76;
5349
+ if (signature.includes("interface ")) return 27;
5350
+ if (signature.includes("struct ")) return 68;
5351
+ if (signature.includes("trait ")) return 73;
5352
+ if (signature.includes("class ")) return 9;
5353
+ }
5354
+ return kind;
5355
+ }
3905
5356
  function inferKindNumber(symbol, documentation, enclosingSymbol) {
3906
5357
  const parsed = parseSymbol(symbol);
3907
5358
  if ("kind" in parsed) {
3908
5359
  return null;
3909
5360
  }
3910
5361
  const descriptors = parsed.descriptors;
3911
- const last = descriptors[descriptors.length - 1] ?? null;
3912
5362
  const parent = descriptors[descriptors.length - 2] ?? null;
3913
5363
  const suffix = leafSuffix(symbol);
3914
- if (suffix === "type") return 9;
5364
+ const signature = (documentation ?? "").toLowerCase();
5365
+ if (suffix === "type") {
5366
+ if (signature.includes("type ")) return 76;
5367
+ if (signature.includes("interface ")) return 27;
5368
+ if (signature.includes("struct ")) return 68;
5369
+ if (signature.includes("trait ")) return 73;
5370
+ if (signature.includes("class ")) return 9;
5371
+ return 9;
5372
+ }
3915
5373
  if (suffix === "method") {
3916
5374
  return parent?.suffix === "type" ? 33 : 23;
3917
5375
  }
3918
5376
  if (suffix === "namespace") return 39;
3919
5377
  if (suffix !== "term") return null;
3920
- const signature = (documentation ?? "").toLowerCase();
3921
5378
  if (signature.includes("async def ") || signature.includes("def ")) {
3922
5379
  return 23;
3923
5380
  }
@@ -3928,71 +5385,6 @@ function inferKindNumber(symbol, documentation, enclosingSymbol) {
3928
5385
  return 83;
3929
5386
  }
3930
5387
 
3931
- // src/queries/doc-coverage.ts
3932
- function docCoverage(db, opts = {}) {
3933
- const { scope, minLoc = 3, limit = 50 } = opts;
3934
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
3935
- const totalRow = db.get(
3936
- `SELECT COUNT(*) AS c
3937
- FROM global_symbols gs
3938
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3939
- JOIN documents d ON der.document_id = d.id
3940
- WHERE 1 = 1
3941
- ${db.pathExclusionsFor("d")}
3942
- ${db.symbolNoiseFor("gs")}
3943
- AND gs.symbol NOT LIKE '%#%'
3944
- AND (der.end_line - der.start_line + 1) >= ?
3945
- ${scopeFilter}`,
3946
- minLoc
3947
- );
3948
- const docRow = db.get(
3949
- `SELECT COUNT(*) AS c
3950
- FROM global_symbols gs
3951
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3952
- JOIN documents d ON der.document_id = d.id
3953
- WHERE 1 = 1
3954
- ${db.pathExclusionsFor("d")}
3955
- ${db.symbolNoiseFor("gs")}
3956
- AND gs.symbol NOT LIKE '%#%'
3957
- AND (der.end_line - der.start_line + 1) >= ?
3958
- AND gs.documentation IS NOT NULL
3959
- AND gs.documentation != ''
3960
- ${scopeFilter}`,
3961
- minLoc
3962
- );
3963
- const total = totalRow?.c ?? 0;
3964
- const documented = docRow?.c ?? 0;
3965
- const undocRows = db.all(
3966
- `SELECT gs.symbol, d.relative_path, der.start_line
3967
- FROM global_symbols gs
3968
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
3969
- JOIN documents d ON der.document_id = d.id
3970
- WHERE 1 = 1
3971
- ${db.pathExclusionsFor("d")}
3972
- ${db.symbolNoiseFor("gs")}
3973
- AND gs.symbol NOT LIKE '%#%'
3974
- AND (der.end_line - der.start_line + 1) >= ?
3975
- AND (gs.documentation IS NULL OR gs.documentation = '')
3976
- ${scopeFilter}
3977
- ORDER BY d.relative_path, der.start_line
3978
- LIMIT ?`,
3979
- minLoc,
3980
- limit
3981
- );
3982
- return {
3983
- totalSymbols: total,
3984
- documented,
3985
- undocumented: total - documented,
3986
- coveragePercent: total > 0 ? Math.round(documented / total * 100) : 0,
3987
- undocumentedSymbols: undocRows.filter((r) => !db.isIgnored(r.relative_path)).map((r) => ({
3988
- symbol: r.symbol,
3989
- shortName: shortenSymbol(r.symbol),
3990
- relativePath: r.relative_path,
3991
- startLine: r.start_line
3992
- }))
3993
- };
3994
- }
3995
-
3996
5388
  // src/queries/deep-chains.ts
3997
5389
  function deepChains(db, opts = {}) {
3998
5390
  const { limit = 10, scope, minDepth = 3 } = opts;
@@ -4123,7 +5515,7 @@ function uniqueRows(rows) {
4123
5515
  function similar(db, symbolPattern, opts = {}) {
4124
5516
  const { minSimilarity = 0.4, limit = 20 } = opts;
4125
5517
  const target = findCallees(db, symbolPattern);
4126
- if (!target || target.callees.size === 0) return [];
5518
+ if (!target) return [];
4127
5519
  if (!isFunctionLikeSymbol(target.symbol)) return [];
4128
5520
  const candidates = getAllCalleeFingerprints(db, {
4129
5521
  minCallees: 3,
@@ -4155,7 +5547,10 @@ function similar(db, symbolPattern, opts = {}) {
4155
5547
  });
4156
5548
  }
4157
5549
  results.sort((a, b) => b.similarity - a.similarity);
4158
- return results.slice(0, limit);
5550
+ if (results.length > 0) {
5551
+ return results.slice(0, limit);
5552
+ }
5553
+ return similarBySourceShape(db, symbolPattern, { minSimilarity, limit });
4159
5554
  }
4160
5555
  function similarAll(db, opts = {}) {
4161
5556
  const { minSimilarity = 0.5, limit = 20, scope, minCallees = 4 } = opts;
@@ -4302,10 +5697,146 @@ function difference(a, b) {
4302
5697
  }
4303
5698
  return result;
4304
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
+ }
4305
5835
 
4306
5836
  // src/queries/similar-files.ts
4307
5837
  function similarFiles(db, opts = {}) {
4308
- 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);
4309
5840
  const profiles = buildFileProfiles(db, { scope, minDeps });
4310
5841
  const results = [];
4311
5842
  if (filePattern) {
@@ -4354,7 +5885,7 @@ function findUniversalDependencies(depMap) {
4354
5885
  }
4355
5886
  }
4356
5887
  for (const [dep, count] of depCounts) {
4357
- if (count / fileCount > 0.5) {
5888
+ if (count >= 6 && count / fileCount > 0.8) {
4358
5889
  universalDeps.add(dep);
4359
5890
  }
4360
5891
  }
@@ -4413,8 +5944,8 @@ function similarChains(db, opts = {}) {
4413
5944
  tailFreq.set(chain[t], (tailFreq.get(chain[t]) ?? 0) + 1);
4414
5945
  }
4415
5946
  }
4416
- const infraThreshold = rawChains.length * 0.4;
4417
- const tailThreshold = rawChains.length * 0.3;
5947
+ const infraThreshold = rawChains.length * 0.9;
5948
+ const tailThreshold = rawChains.length * 0.8;
4418
5949
  const infraNodes = /* @__PURE__ */ new Set();
4419
5950
  for (const [node, freq] of nodeFreq) {
4420
5951
  if (freq > infraThreshold) infraNodes.add(node);
@@ -4424,13 +5955,13 @@ function similarChains(db, opts = {}) {
4424
5955
  }
4425
5956
  const structuralNames = ["index.ts", "index.js", "cli.ts", "main.ts", "health.ts", "health.js"];
4426
5957
  for (const node of nodeFreq.keys()) {
4427
- const basename2 = node.split("/").pop() ?? "";
4428
- if (structuralNames.includes(basename2)) infraNodes.add(node);
5958
+ const basename5 = node.split("/").pop() ?? "";
5959
+ if (structuralNames.includes(basename5)) infraNodes.add(node);
4429
5960
  }
4430
5961
  const filteredChains = [];
4431
5962
  for (const chain of rawChains) {
4432
5963
  const filtered = chain.filter((n) => !infraNodes.has(n));
4433
- if (filtered.length >= 3) {
5964
+ if (filtered.length >= 2) {
4434
5965
  filteredChains.push({ original: chain, filtered });
4435
5966
  }
4436
5967
  }
@@ -4595,31 +6126,13 @@ function isSubChain(sub, full) {
4595
6126
  // src/queries/extract-candidates.ts
4596
6127
  function extractCandidates(db, opts = {}) {
4597
6128
  const { scope, minLoc = 10, minCallees = 6, limit = 20 } = opts;
4598
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
4599
- const symbols2 = db.all(
4600
- `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path
4601
- FROM global_symbols gs
4602
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
4603
- JOIN documents d ON der.document_id = d.id
4604
- WHERE 1 = 1
4605
- ${db.pathExclusionsFor("d")}
4606
- ${db.symbolNoiseFor("gs")}
4607
- AND (der.end_line - der.start_line + 1) >= ?
4608
- ${scopeFilter}
4609
- ORDER BY (der.end_line - der.start_line + 1) DESC`,
4610
- minLoc
4611
- );
6129
+ const symbols2 = getScopedDefinitions(db, scope).filter((definition) => definitionLoc(definition) >= minLoc && isFunctionLikeSymbol(definition.symbol)).sort((left, right) => definitionLoc(right) - definitionLoc(left));
4612
6130
  const results = [];
4613
6131
  for (const sym of symbols2) {
4614
- if (db.isIgnored(sym.relative_path)) continue;
4615
- const basename2 = sym.relative_path.split("/").pop() ?? "";
4616
- if (basename2.includes("types")) continue;
4617
- const calleeChunks = getCalleeRowsForSymbol(db, {
4618
- documentId: sym.document_id,
4619
- startLine: sym.start_line,
4620
- endLine: sym.end_line,
4621
- symbolId: sym.id
4622
- });
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);
4623
6136
  const calleeSet = new Set(calleeChunks.map((c) => c.symbol));
4624
6137
  if (calleeSet.size < minCallees) continue;
4625
6138
  const cooccurrence = /* @__PURE__ */ new Map();
@@ -4682,10 +6195,10 @@ function extractCandidates(db, opts = {}) {
4682
6195
  results.push({
4683
6196
  symbol: sym.symbol,
4684
6197
  shortName: shortenSymbol(sym.symbol),
4685
- relativePath: sym.relative_path,
4686
- startLine: sym.start_line,
4687
- endLine: sym.end_line,
4688
- loc: sym.end_line - sym.start_line + 1,
6198
+ relativePath: sym.relativePath,
6199
+ startLine: sym.startLine,
6200
+ endLine: sym.endLine,
6201
+ loc: definitionLoc(sym),
4689
6202
  totalCallees: calleeSet.size,
4690
6203
  clusters: scoredClusters
4691
6204
  });
@@ -4694,6 +6207,20 @@ function extractCandidates(db, opts = {}) {
4694
6207
  results.sort((a, b) => b.clusters.length - a.clusters.length || b.loc - a.loc);
4695
6208
  return results.slice(0, limit);
4696
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
+ }
4697
6224
 
4698
6225
  // src/queries/affected.ts
4699
6226
  function affected(db, symbolPattern, opts = {}) {
@@ -4734,73 +6261,39 @@ function affected(db, symbolPattern, opts = {}) {
4734
6261
  return results;
4735
6262
  }
4736
6263
  function getDirectAffectedRows(db, target, scope) {
4737
- const sourceSites = getSourceReferenceSites(db, target).filter((site) => !db.isIgnored(site.file)).filter((site) => !scope || site.file.includes(scope));
4738
- if (sourceSites.length > 0) {
4739
- const rows2 = [];
4740
- const seen = /* @__PURE__ */ new Set();
4741
- for (const site of sourceSites) {
4742
- if (!site.enclosingSymbol || site.enclosingSymbol === target.symbol) {
4743
- const key2 = `${site.file}|(top-level)`;
4744
- if (seen.has(key2)) continue;
4745
- seen.add(key2);
4746
- rows2.push({
4747
- symbolId: null,
4748
- symbol: site.file,
4749
- shortName: "(top-level)",
4750
- file: site.file,
4751
- symbolMatch: null
4752
- });
4753
- continue;
4754
- }
4755
- const enclosing = findExactSymbolMatch(db, site.enclosingSymbol);
4756
- if (!enclosing || enclosing.symbolId === target.symbolId || db.isIgnored(enclosing.relativePath)) {
4757
- continue;
4758
- }
4759
- const key = `${enclosing.symbolId}|${enclosing.relativePath}`;
4760
- if (seen.has(key)) continue;
4761
- seen.add(key);
4762
- rows2.push({
4763
- symbolId: enclosing.symbolId,
4764
- symbol: enclosing.symbol,
4765
- shortName: shortenSymbol(enclosing.symbol),
4766
- file: enclosing.relativePath,
4767
- symbolMatch: enclosing
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
4768
6279
  });
6280
+ continue;
6281
+ }
6282
+ if (match.symbolId === target.symbolId || db.isIgnored(match.relativePath)) {
6283
+ continue;
4769
6284
  }
4770
- return rows2;
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
+ });
4771
6295
  }
4772
- const rows = db.all(
4773
- `SELECT DISTINCT
4774
- enc_gs.id AS symbol_id,
4775
- enc_gs.symbol AS symbol,
4776
- enc_d.relative_path AS relative_path
4777
- FROM mentions m
4778
- JOIN chunks c ON m.chunk_id = c.id
4779
- JOIN documents ref_d ON c.document_id = ref_d.id
4780
- JOIN defn_enclosing_ranges enc_der
4781
- ON enc_der.document_id = ref_d.id
4782
- AND c.start_line >= enc_der.start_line
4783
- AND c.end_line <= enc_der.end_line
4784
- JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id
4785
- JOIN documents enc_d ON enc_der.document_id = enc_d.id
4786
- WHERE m.symbol_id = ?
4787
- AND m.role != 1
4788
- AND enc_gs.id != ?
4789
- ${db.symbolNoiseFor("enc_gs")}
4790
- ${db.pathExclusionsFor("enc_d")}
4791
- ${scope ? `AND enc_d.relative_path LIKE '%${scope}%'` : ""}
4792
- ORDER BY enc_d.relative_path
4793
- LIMIT 1`,
4794
- target.symbolId,
4795
- target.symbolId
4796
- );
4797
- return rows.filter((row) => !db.isIgnored(row.relative_path)).map((row) => ({
4798
- symbolId: row.symbol_id,
4799
- symbol: row.symbol,
4800
- shortName: shortenSymbol(row.symbol),
4801
- file: row.relative_path,
4802
- symbolMatch: findExactSymbolMatch(db, row.symbol)
4803
- }));
6296
+ return results;
4804
6297
  }
4805
6298
 
4806
6299
  // src/queries/change-surface.ts
@@ -5150,140 +6643,129 @@ function isLikelyTypeOnlyDep(dep) {
5150
6643
  function shouldSkipDriftFile(filePath) {
5151
6644
  return isStructuralRole(path.basename(filePath)) || isTestLikePath(filePath);
5152
6645
  }
5153
- function isStructuralRole(basename2) {
5154
- if (basename2 === "index.ts" || basename2 === "index.js") return true;
5155
- if (basename2 === "cli.ts" || basename2 === "main.ts" || basename2 === "main.rs") return true;
5156
- if (basename2.includes("worker.") || basename2.includes("postinstall.")) return true;
5157
- if (basename2 === "health.ts" || basename2 === "health.js") return true;
5158
- return false;
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;
6651
+ return false;
6652
+ }
6653
+ function isTestLikePath(filePath) {
6654
+ const normalized = filePath.replace(/\\/g, "/");
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);
6657
+ }
6658
+
6659
+ // src/queries/wrapper-candidates.ts
6660
+ import { basename as basename4, extname as extname3 } from "path";
6661
+ function wrapperCandidates(db, opts) {
6662
+ const { scope, maxLoc = 15, limit = 30 } = opts ?? {};
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;
5159
6724
  }
5160
- function isTestLikePath(filePath) {
5161
- const normalized = filePath.replace(/\\/g, "/");
5162
- const basename2 = path.basename(normalized);
5163
- 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);
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;
5164
6736
  }
5165
-
5166
- // src/queries/wrapper-candidates.ts
5167
- function wrapperCandidates(db, opts) {
5168
- const { scope, maxLoc = 15, limit = 30 } = opts ?? {};
5169
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5170
- const rows = db.all(
5171
- `SELECT * FROM (
5172
- SELECT
5173
- gs.symbol,
5174
- d.relative_path AS file,
5175
- der.start_line,
5176
- der.end_line,
5177
- (der.end_line - der.start_line + 1) AS loc,
5178
- -- The single caller: the symbol whose definition range contains
5179
- -- the chunk that references our target
5180
- (SELECT caller_gs.symbol
5181
- FROM mentions ref_m
5182
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
5183
- JOIN defn_enclosing_ranges caller_der
5184
- ON caller_der.document_id = ref_c.document_id
5185
- AND ref_c.start_line >= caller_der.start_line
5186
- AND ref_c.end_line <= caller_der.end_line
5187
- JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id
5188
- WHERE ref_m.symbol_id = gs.id
5189
- AND ref_m.role != 1
5190
- AND ref_c.document_id != der.document_id
5191
- LIMIT 1
5192
- ) AS caller_symbol,
5193
- -- Fan-in of that single caller
5194
- (SELECT COUNT(DISTINCT caller_ref_c.document_id)
5195
- FROM mentions caller_ref_m
5196
- JOIN chunks caller_ref_c ON caller_ref_m.chunk_id = caller_ref_c.id
5197
- WHERE caller_ref_m.symbol_id = (
5198
- SELECT caller_der2.symbol_id
5199
- FROM mentions ref_m2
5200
- JOIN chunks ref_c2 ON ref_m2.chunk_id = ref_c2.id
5201
- JOIN defn_enclosing_ranges caller_der2
5202
- ON caller_der2.document_id = ref_c2.document_id
5203
- AND ref_c2.start_line >= caller_der2.start_line
5204
- AND ref_c2.end_line <= caller_der2.end_line
5205
- WHERE ref_m2.symbol_id = gs.id
5206
- AND ref_m2.role != 1
5207
- AND ref_c2.document_id != der.document_id
5208
- LIMIT 1
5209
- )
5210
- AND caller_ref_m.role != 1
5211
- ) AS caller_fan_in
5212
- FROM global_symbols gs
5213
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5214
- JOIN documents d ON der.document_id = d.id
5215
- WHERE 1 = 1
5216
- ${db.pathExclusionsFor("d")}
5217
- AND ${testFileExclusionSql("d")}
5218
- ${db.symbolNoiseFor("gs")}
5219
- -- Only functions/terms, not type definitions (types with # are not wrappers)
5220
- AND gs.symbol NOT LIKE '%#'
5221
- AND (der.end_line - der.start_line + 1) <= ?
5222
- AND (der.end_line - der.start_line + 1) >= 2
5223
- ${scopeFilter}
5224
- -- Exactly 1 cross-file consumer
5225
- AND (
5226
- SELECT COUNT(DISTINCT ref_c.document_id)
5227
- FROM mentions ref_m
5228
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
5229
- WHERE ref_m.symbol_id = gs.id
5230
- AND ref_m.role != 1
5231
- AND ref_c.document_id != der.document_id
5232
- ) = 1
5233
- ) WHERE caller_symbol IS NOT NULL AND caller_fan_in > 3
5234
- ORDER BY caller_fan_in DESC, loc DESC
5235
- LIMIT ?`,
5236
- maxLoc,
5237
- limit
5238
- );
5239
- return rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
5240
- symbol: r.symbol,
5241
- shortName: shortenSymbol(r.symbol),
5242
- file: r.file,
5243
- startLine: r.start_line,
5244
- endLine: r.end_line,
5245
- loc: r.loc,
5246
- singleCaller: r.caller_symbol,
5247
- singleCallerShort: shortenSymbol(r.caller_symbol),
5248
- callerFanIn: r.caller_fan_in
5249
- }));
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;
5250
6758
  }
5251
6759
 
5252
6760
  // src/queries/passthrough-candidates.ts
5253
6761
  function passthroughCandidates(db, opts) {
5254
6762
  const { scope, maxLoc = 15, limit = 30 } = opts ?? {};
5255
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5256
- const symbols2 = db.all(
5257
- `SELECT
5258
- gs.id,
5259
- gs.symbol,
5260
- der.document_id,
5261
- der.start_line,
5262
- der.end_line,
5263
- (der.end_line - der.start_line + 1) AS loc,
5264
- d.relative_path
5265
- FROM global_symbols gs
5266
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5267
- JOIN documents d ON der.document_id = d.id
5268
- WHERE 1 = 1
5269
- ${db.pathExclusionsFor("d")}
5270
- AND ${testFileExclusionSql("d")}
5271
- ${db.symbolNoiseFor("gs")}
5272
- AND (der.end_line - der.start_line + 1) >= 3
5273
- AND (der.end_line - der.start_line + 1) <= ?
5274
- ${scopeFilter}
5275
- ORDER BY d.relative_path`,
5276
- maxLoc
5277
- );
6763
+ const symbols2 = getScopedDefinitions3(db, scope).filter((definition) => definitionLoc3(definition) >= 3 && definitionLoc3(definition) <= maxLoc);
5278
6764
  const results = [];
5279
6765
  for (const sym of symbols2) {
5280
- if (db.isIgnored(sym.relative_path)) continue;
5281
- const callees = getCalleeRowsForSymbol(db, {
5282
- documentId: sym.document_id,
5283
- startLine: sym.start_line,
5284
- endLine: sym.end_line,
5285
- symbolId: sym.id
5286
- });
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;
5287
6769
  const uniqueCallees = /* @__PURE__ */ new Map();
5288
6770
  for (const c of callees) {
5289
6771
  if (!uniqueCallees.has(c.symbol)) {
@@ -5295,10 +6777,10 @@ function passthroughCandidates(db, opts) {
5295
6777
  results.push({
5296
6778
  symbol: sym.symbol,
5297
6779
  shortName: shortenSymbol(sym.symbol),
5298
- file: sym.relative_path,
5299
- startLine: sym.start_line,
5300
- endLine: sym.end_line,
5301
- loc: sym.loc,
6780
+ file: sym.relativePath,
6781
+ startLine: sym.startLine,
6782
+ endLine: sym.endLine,
6783
+ loc: definitionLoc3(sym),
5302
6784
  forwardsTo: callee.symbol,
5303
6785
  forwardsToShort: shortenSymbol(callee.symbol),
5304
6786
  forwardsToFile: callee.file
@@ -5307,50 +6789,34 @@ function passthroughCandidates(db, opts) {
5307
6789
  results.sort((a, b) => a.loc - b.loc || a.file.localeCompare(b.file));
5308
6790
  return results.slice(0, limit);
5309
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
+ }
5310
6806
 
5311
6807
  // src/queries/stale-abstractions.ts
5312
6808
  function staleAbstractions(db, opts) {
5313
6809
  const { scope, minLoc = 3, limit = 30 } = opts ?? {};
5314
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
5315
- const rows = db.all(
5316
- `SELECT * FROM (
5317
- SELECT
5318
- gs.symbol,
5319
- d.relative_path AS file,
5320
- der.start_line,
5321
- der.end_line,
5322
- (der.end_line - der.start_line + 1) AS loc,
5323
- (SELECT COUNT(DISTINCT ref_c.document_id)
5324
- FROM mentions ref_m
5325
- JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
5326
- WHERE ref_m.symbol_id = gs.id
5327
- AND ref_m.role != 1
5328
- AND ref_c.document_id != der.document_id
5329
- ) AS consumers
5330
- FROM global_symbols gs
5331
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5332
- JOIN documents d ON der.document_id = d.id
5333
- WHERE 1 = 1
5334
- ${db.pathExclusionsFor("d")}
5335
- AND ${testFileExclusionSql("d")}
5336
- ${db.symbolNoiseFor("gs")}
5337
- -- Top-level type symbols: ends with # but does not contain nested #
5338
- AND gs.symbol LIKE '%#'
5339
- AND gs.symbol NOT LIKE '%#%#%'
5340
- AND (der.end_line - der.start_line + 1) >= ?
5341
- ${scopeFilter}
5342
- ) WHERE consumers <= 1
5343
- ORDER BY loc DESC
5344
- LIMIT ?`,
5345
- minLoc,
5346
- limit
5347
- );
5348
- return rows.filter((r) => !db.isIgnored(r.file)).filter((r) => {
5349
- const basename2 = r.file.split("/").pop() ?? "";
5350
- const isTypeFile = basename2.includes("types") || r.file.includes("/types/");
5351
- if (isTypeFile && r.consumers > 0) return false;
5352
- return true;
5353
- }).map((r) => ({
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);
6818
+ const filesWithFunctions = getFilesWithFunctions(db, scope);
6819
+ return rows.filter((r) => !db.isIgnored(r.file)).filter((r) => isTrueStaleAbstraction(r, filesWithFunctions)).map((r) => ({
5354
6820
  symbol: r.symbol,
5355
6821
  shortName: shortenSymbol(r.symbol),
5356
6822
  file: r.file,
@@ -5358,7 +6824,48 @@ function staleAbstractions(db, opts) {
5358
6824
  endLine: r.end_line,
5359
6825
  loc: r.loc,
5360
6826
  consumers: r.consumers
5361
- }));
6827
+ })).slice(0, limit);
6828
+ }
6829
+ function getFilesWithFunctions(db, scope) {
6830
+ return new Set(getScopedDefinitions4(db, scope).filter((definition) => definition.isFunctionLike).map((definition) => definition.relativePath));
6831
+ }
6832
+ function isTrueStaleAbstraction(row, filesWithFunctions) {
6833
+ const basename5 = row.file.split("/").pop() ?? "";
6834
+ const isTypeFile = basename5.includes("types") || row.file.includes("/types/");
6835
+ if (isTypeFile && row.consumers > 0) {
6836
+ return false;
6837
+ }
6838
+ if (row.consumers === 0 && filesWithFunctions.has(row.file)) {
6839
+ return false;
6840
+ }
6841
+ return true;
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;
5362
6869
  }
5363
6870
 
5364
6871
  // src/queries/complexity-hotspots.ts
@@ -5436,7 +6943,7 @@ function complexityHotspots(db, opts) {
5436
6943
  minLoc,
5437
6944
  limit
5438
6945
  );
5439
- return rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
6946
+ const indexedResults = rows.filter((r) => !db.isIgnored(r.file)).map((r) => ({
5440
6947
  symbol: r.symbol,
5441
6948
  shortName: shortenSymbol(r.symbol),
5442
6949
  file: r.file,
@@ -5450,6 +6957,31 @@ function complexityHotspots(db, opts) {
5450
6957
  r.loc / 50 * (r.fan_in / 5) * Math.max(r.fan_out / 5, 1) * 100
5451
6958
  ) / 100
5452
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);
5453
6985
  }
5454
6986
 
5455
6987
  // src/queries/health.ts
@@ -5474,20 +7006,7 @@ function health(db, opts = {}) {
5474
7006
  const trueIsolatedCount = isolatedResult.filter(
5475
7007
  (s2) => !isEntrySurface(db, s2.relativePath)
5476
7008
  ).length;
5477
- const filesWithFunctions = new Set(
5478
- db.all(
5479
- `SELECT DISTINCT d.relative_path
5480
- FROM global_symbols gs
5481
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
5482
- JOIN documents d ON der.document_id = d.id
5483
- WHERE gs.symbol LIKE '%().'
5484
- ${db.pathExclusionsFor("d")}`
5485
- ).map((r) => r.relative_path)
5486
- );
5487
- const trueStaleCount = staleResult.filter((s2) => {
5488
- if (s2.consumers === 0 && filesWithFunctions.has(s2.file)) return false;
5489
- return true;
5490
- }).length;
7009
+ const trueStaleCount = staleResult.length;
5491
7010
  const trueDriftCount = driftResult.results.length;
5492
7011
  const trueSimilarCount = similarResult.length;
5493
7012
  const actions = [];
@@ -5562,11 +7081,7 @@ function health(db, opts = {}) {
5562
7081
  });
5563
7082
  }
5564
7083
  if (trueStaleCount > 0) {
5565
- const trueStaleSymbols = staleResult.filter((s2) => {
5566
- if (s2.consumers === 0 && filesWithFunctions.has(s2.file)) return false;
5567
- return true;
5568
- });
5569
- const unused = trueStaleSymbols.filter((s2) => s2.consumers === 0).length;
7084
+ const unused = staleResult.filter((s2) => s2.consumers === 0).length;
5570
7085
  const singleUse = trueStaleCount - unused;
5571
7086
  const parts = [];
5572
7087
  if (unused > 0) parts.push(`${unused} unused`);
@@ -5577,7 +7092,7 @@ function health(db, opts = {}) {
5577
7092
  effort: "low",
5578
7093
  impact: "medium",
5579
7094
  count: trueStaleCount,
5580
- locRecoverable: staleResult.filter((s2) => s2.consumers === 0 || !s2.file.includes("types")).reduce((sum, r) => sum + r.loc, 0)
7095
+ locRecoverable: staleResult.reduce((sum, r) => sum + r.loc, 0)
5581
7096
  });
5582
7097
  }
5583
7098
  if (trueDriftCount > 0) {
@@ -5680,11 +7195,11 @@ function convergence(db, symbolPatternA, symbolPatternB) {
5680
7195
  } else if (shared.length === 0) {
5681
7196
  strategy = "These functions do not share any callees. They are not a callee-based consolidation candidate.";
5682
7197
  } else if (uniqueA.length === 0 && uniqueB.length === 0) {
5683
- strategy = "These functions have identical callee sets. One can replace the other directly.";
7198
+ strategy = "These functions have identical tracked callee sets. They are a strong structural match, but identical callees do not prove interchangeable semantics; inspect signatures, control flow, and return values before consolidating.";
5684
7199
  } else if (uniqueA.length === 0) {
5685
- strategy = `A is a subset of B. A can be replaced by calling B (B does everything A does plus more).`;
7200
+ strategy = `A's tracked callees are a subset of B's. B may subsume part of A's structure, but verify signatures, guards, and non-call logic before replacing A with B.`;
5686
7201
  } else if (uniqueB.length === 0) {
5687
- strategy = `B is a subset of A. B can be replaced by calling A (A does everything B does plus more).`;
7202
+ strategy = `B's tracked callees are a subset of A's. A may subsume part of B's structure, but verify signatures, guards, and non-call logic before replacing B with A.`;
5688
7203
  } else if (uniqueA.length <= 2 && uniqueB.length <= 2) {
5689
7204
  strategy = `Create a shared function with the ${shared.length} common callees. Pass the ${uniqueA.length + uniqueB.length} divergent callees as parameters or strategy callbacks.`;
5690
7205
  } else {
@@ -5715,7 +7230,7 @@ function convergence(db, symbolPatternA, symbolPatternB) {
5715
7230
 
5716
7231
  // src/queries/code.ts
5717
7232
  import { readFileSync as readFileSync5 } from "fs";
5718
- import { join as join7 } from "path";
7233
+ import { join as join9 } from "path";
5719
7234
  function code(db, symbolPattern, opts = {}) {
5720
7235
  const { context = 0 } = opts;
5721
7236
  const fileLineMatch = symbolPattern.match(/^(.+\.\w+):(\d+)-(\d+)$/);
@@ -5728,7 +7243,7 @@ function code(db, symbolPattern, opts = {}) {
5728
7243
  `SELECT language FROM documents WHERE relative_path = ?`,
5729
7244
  match.relativePath
5730
7245
  );
5731
- const filePath = join7(db.config.projectRoot, match.relativePath);
7246
+ const filePath = join9(db.config.projectRoot, match.relativePath);
5732
7247
  let fileContent;
5733
7248
  try {
5734
7249
  fileContent = readFileSync5(filePath, "utf-8");
@@ -5758,7 +7273,7 @@ function readFileRange(db, filePath, startLine, endLine, context) {
5758
7273
  resolvedPath
5759
7274
  );
5760
7275
  if (!doc) return null;
5761
- const fullPath = join7(db.config.projectRoot, doc.relative_path);
7276
+ const fullPath = join9(db.config.projectRoot, doc.relative_path);
5762
7277
  let fileContent;
5763
7278
  try {
5764
7279
  fileContent = readFileSync5(fullPath, "utf-8");
@@ -5782,7 +7297,7 @@ function readFileRange(db, filePath, startLine, endLine, context) {
5782
7297
 
5783
7298
  // src/queries/complexity.ts
5784
7299
  import { readFileSync as readFileSync6 } from "fs";
5785
- import { join as join8 } from "path";
7300
+ import { join as join10 } from "path";
5786
7301
  function complexity(db, symbolPattern) {
5787
7302
  const match = findFirstSymbolMatch(db, symbolPattern);
5788
7303
  if (!match) return null;
@@ -5791,7 +7306,7 @@ function complexity(db, symbolPattern) {
5791
7306
  match.relativePath
5792
7307
  );
5793
7308
  const language = doc?.language ?? "unknown";
5794
- const filePath = join8(db.config.projectRoot, match.relativePath);
7309
+ const filePath = join10(db.config.projectRoot, match.relativePath);
5795
7310
  let source = "";
5796
7311
  try {
5797
7312
  const lines = readFileSync6(filePath, "utf-8").split("\n");
@@ -6131,46 +7646,100 @@ function redundantReexports(db, opts = {}) {
6131
7646
  results.sort(
6132
7647
  (a, b) => b.directConsumers - a.directConsumers || a.barrelFile.localeCompare(b.barrelFile) || a.shortName.localeCompare(b.shortName)
6133
7648
  );
6134
- 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;
6135
7724
  }
6136
7725
 
6137
7726
  // src/queries/similar-signatures.ts
6138
7727
  function similarSignatures(db, opts = {}) {
6139
7728
  const { scope, minLoc = 1, limit } = opts;
6140
- const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : "";
6141
- const rows = db.all(
6142
- `SELECT
6143
- gs.symbol,
6144
- d.relative_path,
6145
- der.start_line,
6146
- der.end_line,
6147
- (der.end_line - der.start_line + 1) AS loc,
6148
- REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig
6149
- FROM global_symbols gs
6150
- JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
6151
- JOIN documents d ON der.document_id = d.id
6152
- WHERE gs.documentation IS NOT NULL
6153
- AND gs.documentation != ''
6154
- AND INSTR(gs.documentation, '|') > 0
6155
- AND (der.end_line - der.start_line + 1) >= ?
6156
- ${db.pathExclusionsFor("d")}
6157
- ${db.symbolNoiseFor("gs")}
6158
- ${scopeFilter}
6159
- ORDER BY d.relative_path, der.start_line`,
6160
- minLoc
6161
- );
6162
7729
  const sigGroups = /* @__PURE__ */ new Map();
6163
- for (const row of rows) {
6164
- if (db.isIgnored(row.relative_path)) continue;
6165
- 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);
6166
7735
  if (!normalized) continue;
6167
7736
  const entry = {
6168
- symbol: row.symbol,
6169
- shortName: shortenSymbol(row.symbol),
6170
- file: row.relative_path,
6171
- startLine: row.start_line,
6172
- endLine: row.end_line,
6173
- 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
6174
7743
  };
6175
7744
  const existing = sigGroups.get(normalized);
6176
7745
  if (existing) {
@@ -6193,6 +7762,52 @@ function similarSignatures(db, opts = {}) {
6193
7762
  });
6194
7763
  return limit ? results.slice(0, limit) : results;
6195
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
+ }
6196
7811
  function normalizeSignature(raw) {
6197
7812
  if (!raw || !raw.trim()) return null;
6198
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();
@@ -6203,28 +7818,88 @@ function normalizeSignature(raw) {
6203
7818
  if (sig.length < 3) return null;
6204
7819
  return sig;
6205
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
+ }
6206
7875
 
6207
7876
  // src/setup.ts
6208
7877
  import {
6209
- existsSync as existsSync7,
7878
+ existsSync as existsSync9,
6210
7879
  mkdirSync as mkdirSync2,
6211
7880
  symlinkSync,
6212
7881
  readlinkSync,
6213
7882
  unlinkSync
6214
7883
  } from "fs";
6215
- 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";
6216
7885
  import { homedir as homedir2, platform as platform3 } from "os";
6217
7886
  import { fileURLToPath } from "url";
6218
7887
  var IS_WINDOWS3 = platform3() === "win32";
6219
- 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
+ ];
6220
7895
  function installSkills(opts = {}) {
6221
7896
  const log = opts.quiet ? () => {
6222
7897
  } : console.log;
6223
7898
  const thisFile = fileURLToPath(import.meta.url);
6224
7899
  const skillsSource = resolve3(dirname3(thisFile), "..", "skills");
6225
7900
  const targets = [
6226
- join9(homedir2(), ".claude", "skills"),
6227
- join9(homedir2(), ".codex", "skills")
7901
+ join11(homedir2(), ".claude", "skills"),
7902
+ join11(homedir2(), ".codex", "skills")
6228
7903
  ];
6229
7904
  const result = {
6230
7905
  installed: [],
@@ -6233,19 +7908,19 @@ function installSkills(opts = {}) {
6233
7908
  };
6234
7909
  for (const targetDir of targets) {
6235
7910
  const parentDir = dirname3(targetDir);
6236
- if (!existsSync7(parentDir)) {
7911
+ if (!existsSync9(parentDir)) {
6237
7912
  continue;
6238
7913
  }
6239
7914
  mkdirSync2(targetDir, { recursive: true });
6240
7915
  const toolName = targetDir.includes(".codex") ? "Codex" : "Claude";
6241
- for (const skill of SKILLS) {
6242
- const source = join9(skillsSource, skill);
6243
- const target = join9(targetDir, skill);
6244
- 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)) {
6245
7920
  result.skipped.push(`${toolName}/${skill}`);
6246
7921
  continue;
6247
7922
  }
6248
- if (existsSync7(target)) {
7923
+ if (existsSync9(target)) {
6249
7924
  try {
6250
7925
  const existing = readlinkSync(target);
6251
7926
  if (resolve3(existing) === resolve3(source)) {
@@ -6277,14 +7952,14 @@ function resolveProjectRoot() {
6277
7952
  function resolveActiveDbPath(projectRoot) {
6278
7953
  const config = loadProjectConfig(projectRoot);
6279
7954
  const paths = resolveIndexPaths(projectRoot, config);
6280
- 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"));
6281
7956
  }
6282
7957
  function openDb() {
6283
7958
  const projectRoot = resolveProjectRoot();
6284
7959
  const config = loadProjectConfig(projectRoot);
6285
7960
  const paths = resolveIndexPaths(projectRoot, config);
6286
7961
  const dbPath = resolveActiveDbPath(projectRoot);
6287
- if (!existsSync8(dbPath)) {
7962
+ if (!existsSync10(dbPath)) {
6288
7963
  console.error(`error: No index.db found. Run: scip-query reindex`);
6289
7964
  process.exit(1);
6290
7965
  }
@@ -6347,7 +8022,6 @@ var queries = {
6347
8022
  isolated,
6348
8023
  byKind,
6349
8024
  kindCounts,
6350
- docCoverage,
6351
8025
  deepChains,
6352
8026
  hierarchy,
6353
8027
  callGraph,
@@ -6456,6 +8130,10 @@ program.command("trace <symbol>").description("Trace a symbol: definition + all
6456
8130
  for (const d of result.definitions) {
6457
8131
  const sig = d.signature ? ` \u2014 ${d.signature}` : "";
6458
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
+ }
6459
8137
  }
6460
8138
  console.log("\n\u2550\u2550\u2550 REFERENCED BY \u2550\u2550\u2550");
6461
8139
  let prevFile = "";
@@ -6760,27 +8438,6 @@ program.command("kind-counts").description("Histogram of symbol kinds in the cod
6760
8438
  }
6761
8439
  );
6762
8440
  });
6763
- program.command("doc-coverage").description("Check documentation coverage across symbols").option("-s, --scope <path>", "Limit to files matching path").option("--min-loc <n>", "Minimum LOC to consider", parseIntSafe, 3).option("-n, --limit <n>", "Max undocumented symbols to show", parseIntSafe, 50).action((opts) => {
6764
- runQuery(
6765
- (db) => queries.docCoverage(db, {
6766
- scope: opts.scope,
6767
- minLoc: opts.minLoc,
6768
- limit: opts.limit
6769
- }),
6770
- (result) => {
6771
- console.log(`Documentation coverage: ${result.coveragePercent}%`);
6772
- console.log(` Total symbols: ${result.totalSymbols}`);
6773
- console.log(` Documented: ${result.documented}`);
6774
- console.log(` Undocumented: ${result.undocumented}`);
6775
- if (result.undocumentedSymbols.length > 0) {
6776
- console.log("\nUndocumented:");
6777
- for (const s of result.undocumentedSymbols) {
6778
- console.log(` ${s.relativePath}:${displayLine(s.startLine)} ${s.shortName}`);
6779
- }
6780
- }
6781
- }
6782
- );
6783
- });
6784
8441
  program.command("deep-chains").description("Find the longest transitive dependency chains").option("-n, --limit <n>", "Number of chains to show", parseIntSafe, 10).option("-s, --scope <path>", "Limit to files matching path").option("--min-depth <n>", "Minimum chain depth", parseIntSafe, 3).action((opts) => {
6785
8442
  runQuery(
6786
8443
  (db) => queries.deepChains(db, {
@@ -6883,7 +8540,7 @@ ${results.length} similar pair(s) found.`);
6883
8540
  }
6884
8541
  });
6885
8542
  });
6886
- 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) => {
6887
8544
  runQuery(
6888
8545
  (db) => queries.similarFiles(db, {
6889
8546
  minSimilarity: opts.minSimilarity,
@@ -7283,7 +8940,7 @@ ${result.connectedSymbols.length} connected symbol(s).`);
7283
8940
  }
7284
8941
  db.close();
7285
8942
  });
7286
- 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(() => {
7287
8944
  const result = installSkills();
7288
8945
  const total = result.installed.length + result.alreadyLinked.length;
7289
8946
  console.log(`
@@ -7292,12 +8949,41 @@ ${result.installed.length} installed, ${result.alreadyLinked.length} already lin
7292
8949
  console.log("Skills will be available in your next Claude Code / Codex session.");
7293
8950
  }
7294
8951
  });
7295
- 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;
7296
8954
  if (isScipInstalled()) {
7297
8955
  console.log("scip CLI: installed");
7298
8956
  } else {
7299
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
+ }
7300
8985
  }
8986
+ process.exitCode = hasProblems ? 1 : 0;
7301
8987
  });
7302
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) => {
7303
8989
  const db = openDb();
@@ -7385,8 +9071,8 @@ program.command("status").description("Show index status for this project").acti
7385
9071
  if (dbPath !== paths.dbPath) {
7386
9072
  console.log(`Config: ${paths.dbPath} (fallback to project root index.db)`);
7387
9073
  }
7388
- console.log(`Exists: ${existsSync8(dbPath) ? "yes" : "no"}`);
7389
- if (existsSync8(dbPath)) {
9074
+ console.log(`Exists: ${existsSync10(dbPath) ? "yes" : "no"}`);
9075
+ if (existsSync10(dbPath)) {
7390
9076
  withDb((db) => {
7391
9077
  const s = queries.stats(db);
7392
9078
  console.log(`Symbols: ${s.symbols}`);