homegraph 1.4.1 → 1.5.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 (332) hide show
  1. package/README.md +48 -11
  2. package/dist/bin/homegraph.js +295 -72
  3. package/dist/bin/homegraph.js.map +1 -1
  4. package/dist/context/index.d.ts.map +1 -1
  5. package/dist/context/index.js +32 -11
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/db/index.d.ts +143 -1
  8. package/dist/db/index.d.ts.map +1 -1
  9. package/dist/db/index.js +293 -16
  10. package/dist/db/index.js.map +1 -1
  11. package/dist/db/queries.d.ts +43 -4
  12. package/dist/db/queries.d.ts.map +1 -1
  13. package/dist/db/queries.js +138 -14
  14. package/dist/db/queries.js.map +1 -1
  15. package/dist/db/sqlite-adapter.d.ts +24 -17
  16. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  17. package/dist/db/sqlite-adapter.js +292 -36
  18. package/dist/db/sqlite-adapter.js.map +1 -1
  19. package/dist/db/wal-valve.d.ts +15 -2
  20. package/dist/db/wal-valve.d.ts.map +1 -1
  21. package/dist/db/wal-valve.js +88 -6
  22. package/dist/db/wal-valve.js.map +1 -1
  23. package/dist/extraction/arkts-batch-worker.js +3 -2
  24. package/dist/extraction/arkts-batch-worker.js.map +1 -1
  25. package/dist/extraction/default-ignore.d.ts +39 -0
  26. package/dist/extraction/default-ignore.d.ts.map +1 -0
  27. package/dist/extraction/default-ignore.js +221 -0
  28. package/dist/extraction/default-ignore.js.map +1 -0
  29. package/dist/extraction/index.d.ts +9 -10
  30. package/dist/extraction/index.d.ts.map +1 -1
  31. package/dist/extraction/index.js +227 -323
  32. package/dist/extraction/index.js.map +1 -1
  33. package/dist/extraction/languages/arkts.d.ts +90 -6
  34. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  35. package/dist/extraction/languages/arkts.js +1579 -128
  36. package/dist/extraction/languages/arkts.js.map +1 -1
  37. package/dist/extraction/store-worker.d.ts +20 -0
  38. package/dist/extraction/store-worker.d.ts.map +1 -0
  39. package/dist/extraction/store-worker.js +102 -0
  40. package/dist/extraction/store-worker.js.map +1 -0
  41. package/dist/extraction/store-writer.d.ts +48 -0
  42. package/dist/extraction/store-writer.d.ts.map +1 -0
  43. package/dist/extraction/store-writer.js +167 -0
  44. package/dist/extraction/store-writer.js.map +1 -0
  45. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  46. package/dist/extraction/tree-sitter.js +47 -0
  47. package/dist/extraction/tree-sitter.js.map +1 -1
  48. package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
  49. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  50. package/dist/extraction/wasm-runtime-flags.js +55 -7
  51. package/dist/extraction/wasm-runtime-flags.js.map +1 -1
  52. package/dist/index.d.ts +16 -5
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +135 -97
  55. package/dist/index.js.map +1 -1
  56. package/dist/mcp/query-worker.d.ts +2 -2
  57. package/dist/mcp/query-worker.js +2 -2
  58. package/dist/mcp/tools.d.ts.map +1 -1
  59. package/dist/mcp/tools.js +42 -26
  60. package/dist/mcp/tools.js.map +1 -1
  61. package/dist/resolution/callback-synthesizer.d.ts +52 -16
  62. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  63. package/dist/resolution/callback-synthesizer.js +234 -180
  64. package/dist/resolution/callback-synthesizer.js.map +1 -1
  65. package/dist/resolution/cooperative-yield.d.ts +10 -2
  66. package/dist/resolution/cooperative-yield.d.ts.map +1 -1
  67. package/dist/resolution/cooperative-yield.js +6 -4
  68. package/dist/resolution/cooperative-yield.js.map +1 -1
  69. package/dist/resolution/frameworks/go.d.ts.map +1 -1
  70. package/dist/resolution/frameworks/go.js +29 -6
  71. package/dist/resolution/frameworks/go.js.map +1 -1
  72. package/dist/resolution/import-resolver.d.ts +2 -3
  73. package/dist/resolution/import-resolver.d.ts.map +1 -1
  74. package/dist/resolution/import-resolver.js +166 -7
  75. package/dist/resolution/import-resolver.js.map +1 -1
  76. package/dist/resolution/index.d.ts +87 -2
  77. package/dist/resolution/index.d.ts.map +1 -1
  78. package/dist/resolution/index.js +739 -131
  79. package/dist/resolution/index.js.map +1 -1
  80. package/dist/resolution/memory-budget.d.ts +48 -0
  81. package/dist/resolution/memory-budget.d.ts.map +1 -0
  82. package/dist/resolution/memory-budget.js +162 -0
  83. package/dist/resolution/memory-budget.js.map +1 -0
  84. package/dist/resolution/name-matcher.d.ts +12 -1
  85. package/dist/resolution/name-matcher.d.ts.map +1 -1
  86. package/dist/resolution/name-matcher.js +439 -97
  87. package/dist/resolution/name-matcher.js.map +1 -1
  88. package/dist/resolution/resolver-pool.d.ts +106 -0
  89. package/dist/resolution/resolver-pool.d.ts.map +1 -0
  90. package/dist/resolution/resolver-pool.js +344 -0
  91. package/dist/resolution/resolver-pool.js.map +1 -0
  92. package/dist/resolution/resolver-worker.d.ts +17 -0
  93. package/dist/resolution/resolver-worker.d.ts.map +1 -0
  94. package/dist/resolution/resolver-worker.js +141 -0
  95. package/dist/resolution/resolver-worker.js.map +1 -0
  96. package/dist/resolution/types.d.ts +3 -0
  97. package/dist/resolution/types.d.ts.map +1 -1
  98. package/dist/spec/build/diff-parser.d.ts +55 -0
  99. package/dist/spec/build/diff-parser.d.ts.map +1 -0
  100. package/dist/spec/build/diff-parser.js +205 -0
  101. package/dist/spec/build/diff-parser.js.map +1 -0
  102. package/dist/spec/build/git-scanner.d.ts +93 -0
  103. package/dist/spec/build/git-scanner.d.ts.map +1 -0
  104. package/dist/spec/build/git-scanner.js +254 -0
  105. package/dist/spec/build/git-scanner.js.map +1 -0
  106. package/dist/spec/build/pipeline.d.ts +54 -0
  107. package/dist/spec/build/pipeline.d.ts.map +1 -0
  108. package/dist/spec/build/pipeline.js +170 -0
  109. package/dist/spec/build/pipeline.js.map +1 -0
  110. package/dist/spec/build/scan.d.ts +48 -0
  111. package/dist/spec/build/scan.d.ts.map +1 -0
  112. package/dist/spec/build/scan.js +89 -0
  113. package/dist/spec/build/scan.js.map +1 -0
  114. package/dist/spec/build/scope-resolver.d.ts +50 -0
  115. package/dist/spec/build/scope-resolver.d.ts.map +1 -0
  116. package/dist/spec/build/scope-resolver.js +105 -0
  117. package/dist/spec/build/scope-resolver.js.map +1 -0
  118. package/dist/spec/build/spec-extractor.d.ts +74 -0
  119. package/dist/spec/build/spec-extractor.d.ts.map +1 -0
  120. package/dist/spec/build/spec-extractor.js +344 -0
  121. package/dist/spec/build/spec-extractor.js.map +1 -0
  122. package/dist/spec/config.d.ts +35 -1
  123. package/dist/spec/config.d.ts.map +1 -1
  124. package/dist/spec/config.js +30 -5
  125. package/dist/spec/config.js.map +1 -1
  126. package/dist/spec/db/fts.d.ts.map +1 -1
  127. package/dist/spec/db/fts.js +19 -50
  128. package/dist/spec/db/fts.js.map +1 -1
  129. package/dist/spec/db/index.d.ts +5 -3
  130. package/dist/spec/db/index.d.ts.map +1 -1
  131. package/dist/spec/db/index.js +16 -1
  132. package/dist/spec/db/index.js.map +1 -1
  133. package/dist/spec/db/persist.d.ts +45 -0
  134. package/dist/spec/db/persist.d.ts.map +1 -0
  135. package/dist/spec/db/persist.js +66 -0
  136. package/dist/spec/db/persist.js.map +1 -0
  137. package/dist/spec/db/relations.d.ts +80 -1
  138. package/dist/spec/db/relations.d.ts.map +1 -1
  139. package/dist/spec/db/relations.js +175 -4
  140. package/dist/spec/db/relations.js.map +1 -1
  141. package/dist/spec/db/schema.d.ts +4 -0
  142. package/dist/spec/db/schema.d.ts.map +1 -1
  143. package/dist/spec/db/schema.js +4 -0
  144. package/dist/spec/db/schema.js.map +1 -1
  145. package/dist/spec/db/schema.sql +5 -1
  146. package/dist/spec/db/spec-node.d.ts +5 -0
  147. package/dist/spec/db/spec-node.d.ts.map +1 -1
  148. package/dist/spec/db/spec-node.js +11 -5
  149. package/dist/spec/db/spec-node.js.map +1 -1
  150. package/dist/spec/db/sql-utils.d.ts +11 -0
  151. package/dist/spec/db/sql-utils.d.ts.map +1 -0
  152. package/dist/spec/db/sql-utils.js +19 -0
  153. package/dist/spec/db/sql-utils.js.map +1 -0
  154. package/dist/spec/evolve/cluster-context.d.ts +30 -0
  155. package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
  156. package/dist/spec/evolve/cluster-context.js +78 -0
  157. package/dist/spec/evolve/cluster-context.js.map +1 -0
  158. package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
  159. package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
  160. package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
  161. package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
  162. package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
  163. package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
  164. package/dist/spec/evolve/commit-spec-persister.js +93 -0
  165. package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
  166. package/dist/spec/evolve/impact-locator.d.ts +23 -7
  167. package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
  168. package/dist/spec/evolve/impact-locator.js +58 -14
  169. package/dist/spec/evolve/impact-locator.js.map +1 -1
  170. package/dist/spec/evolve/pipeline.d.ts +62 -25
  171. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  172. package/dist/spec/evolve/pipeline.js +436 -457
  173. package/dist/spec/evolve/pipeline.js.map +1 -1
  174. package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
  175. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  176. package/dist/spec/evolve/spec-rewriter.js +45 -45
  177. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  178. package/dist/spec/git/commits.d.ts +85 -0
  179. package/dist/spec/git/commits.d.ts.map +1 -0
  180. package/dist/spec/git/commits.js +218 -0
  181. package/dist/spec/git/commits.js.map +1 -0
  182. package/dist/spec/git/exec.d.ts +13 -0
  183. package/dist/spec/git/exec.d.ts.map +1 -0
  184. package/dist/spec/git/exec.js +19 -0
  185. package/dist/spec/git/exec.js.map +1 -0
  186. package/dist/spec/git/index.d.ts +9 -0
  187. package/dist/spec/git/index.d.ts.map +1 -0
  188. package/dist/spec/git/index.js +22 -0
  189. package/dist/spec/git/index.js.map +1 -0
  190. package/dist/spec/git-utils.d.ts +8 -0
  191. package/dist/spec/git-utils.d.ts.map +1 -0
  192. package/dist/spec/git-utils.js +14 -0
  193. package/dist/spec/git-utils.js.map +1 -0
  194. package/dist/spec/graph/index.d.ts +8 -0
  195. package/dist/spec/graph/index.d.ts.map +1 -0
  196. package/dist/spec/graph/index.js +17 -0
  197. package/dist/spec/graph/index.js.map +1 -0
  198. package/dist/spec/graph/queries.d.ts +6 -35
  199. package/dist/spec/graph/queries.d.ts.map +1 -1
  200. package/dist/spec/graph/queries.js +71 -248
  201. package/dist/spec/graph/queries.js.map +1 -1
  202. package/dist/spec/llm/agent-client.d.ts +59 -0
  203. package/dist/spec/llm/agent-client.d.ts.map +1 -0
  204. package/dist/spec/llm/agent-client.js +213 -0
  205. package/dist/spec/llm/agent-client.js.map +1 -0
  206. package/dist/spec/llm/agents/claude-code.d.ts +32 -0
  207. package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
  208. package/dist/spec/llm/agents/claude-code.js +125 -0
  209. package/dist/spec/llm/agents/claude-code.js.map +1 -0
  210. package/dist/spec/llm/agents/codex.d.ts +32 -0
  211. package/dist/spec/llm/agents/codex.d.ts.map +1 -0
  212. package/dist/spec/llm/agents/codex.js +145 -0
  213. package/dist/spec/llm/agents/codex.js.map +1 -0
  214. package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
  215. package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
  216. package/dist/spec/llm/agents/detect-utils.js +91 -0
  217. package/dist/spec/llm/agents/detect-utils.js.map +1 -0
  218. package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
  219. package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
  220. package/dist/spec/llm/agents/deveco-code.js +165 -0
  221. package/dist/spec/llm/agents/deveco-code.js.map +1 -0
  222. package/dist/spec/llm/agents/index.d.ts +31 -0
  223. package/dist/spec/llm/agents/index.d.ts.map +1 -0
  224. package/dist/spec/llm/agents/index.js +65 -0
  225. package/dist/spec/llm/agents/index.js.map +1 -0
  226. package/dist/spec/llm/agents/types.d.ts +76 -0
  227. package/dist/spec/llm/agents/types.d.ts.map +1 -0
  228. package/dist/spec/llm/agents/types.js +15 -0
  229. package/dist/spec/llm/agents/types.js.map +1 -0
  230. package/dist/spec/llm/client.d.ts +31 -11
  231. package/dist/spec/llm/client.d.ts.map +1 -1
  232. package/dist/spec/llm/client.js +143 -81
  233. package/dist/spec/llm/client.js.map +1 -1
  234. package/dist/spec/llm/factory.d.ts +41 -0
  235. package/dist/spec/llm/factory.d.ts.map +1 -0
  236. package/dist/spec/llm/factory.js +80 -0
  237. package/dist/spec/llm/factory.js.map +1 -0
  238. package/dist/spec/llm/prompts.d.ts +24 -8
  239. package/dist/spec/llm/prompts.d.ts.map +1 -1
  240. package/dist/spec/llm/prompts.js +100 -50
  241. package/dist/spec/llm/prompts.js.map +1 -1
  242. package/dist/spec/llm/retry.d.ts +53 -0
  243. package/dist/spec/llm/retry.d.ts.map +1 -0
  244. package/dist/spec/llm/retry.js +153 -0
  245. package/dist/spec/llm/retry.js.map +1 -0
  246. package/dist/spec/mine/clusterer.d.ts +63 -0
  247. package/dist/spec/mine/clusterer.d.ts.map +1 -0
  248. package/dist/spec/mine/clusterer.js +904 -0
  249. package/dist/spec/mine/clusterer.js.map +1 -0
  250. package/dist/spec/mine/clustering/features.d.ts +38 -0
  251. package/dist/spec/mine/clustering/features.d.ts.map +1 -0
  252. package/dist/spec/mine/clustering/features.js +108 -0
  253. package/dist/spec/mine/clustering/features.js.map +1 -0
  254. package/dist/spec/mine/clustering/index.d.ts +62 -0
  255. package/dist/spec/mine/clustering/index.d.ts.map +1 -0
  256. package/dist/spec/mine/clustering/index.js +259 -0
  257. package/dist/spec/mine/clustering/index.js.map +1 -0
  258. package/dist/spec/mine/clustering/leiden.d.ts +61 -0
  259. package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
  260. package/dist/spec/mine/clustering/leiden.js +489 -0
  261. package/dist/spec/mine/clustering/leiden.js.map +1 -0
  262. package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
  263. package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
  264. package/dist/spec/mine/clustering/text-similarity.js +98 -0
  265. package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
  266. package/dist/spec/mine/generator.d.ts +54 -0
  267. package/dist/spec/mine/generator.d.ts.map +1 -0
  268. package/dist/spec/mine/generator.js +267 -0
  269. package/dist/spec/mine/generator.js.map +1 -0
  270. package/dist/spec/mine/persist.d.ts +37 -0
  271. package/dist/spec/mine/persist.d.ts.map +1 -0
  272. package/dist/spec/mine/persist.js +170 -0
  273. package/dist/spec/mine/persist.js.map +1 -0
  274. package/dist/spec/mine/pipeline.d.ts +41 -0
  275. package/dist/spec/mine/pipeline.d.ts.map +1 -0
  276. package/dist/spec/mine/pipeline.js +236 -0
  277. package/dist/spec/mine/pipeline.js.map +1 -0
  278. package/dist/spec/mine/progress-handler.d.ts +22 -0
  279. package/dist/spec/mine/progress-handler.d.ts.map +1 -0
  280. package/dist/spec/mine/progress-handler.js +108 -0
  281. package/dist/spec/mine/progress-handler.js.map +1 -0
  282. package/dist/spec/mine/progress.d.ts +23 -0
  283. package/dist/spec/mine/progress.d.ts.map +1 -0
  284. package/dist/spec/mine/progress.js +12 -0
  285. package/dist/spec/mine/progress.js.map +1 -0
  286. package/dist/spec/mine/scanner.d.ts +59 -0
  287. package/dist/spec/mine/scanner.d.ts.map +1 -0
  288. package/dist/spec/mine/scanner.js +383 -0
  289. package/dist/spec/mine/scanner.js.map +1 -0
  290. package/dist/spec/types.d.ts +32 -0
  291. package/dist/spec/types.d.ts.map +1 -1
  292. package/dist/spec/ui/index.d.ts +9 -0
  293. package/dist/spec/ui/index.d.ts.map +1 -0
  294. package/dist/spec/ui/index.js +16 -0
  295. package/dist/spec/ui/index.js.map +1 -0
  296. package/dist/spec/ui/progress-handler.d.ts +33 -0
  297. package/dist/spec/ui/progress-handler.d.ts.map +1 -0
  298. package/dist/spec/ui/progress-handler.js +132 -0
  299. package/dist/spec/ui/progress-handler.js.map +1 -0
  300. package/dist/spec/ui/progress.d.ts +24 -0
  301. package/dist/spec/ui/progress.d.ts.map +1 -0
  302. package/dist/spec/ui/progress.js +13 -0
  303. package/dist/spec/ui/progress.js.map +1 -0
  304. package/dist/spec/utils/fs.d.ts +45 -0
  305. package/dist/spec/utils/fs.d.ts.map +1 -0
  306. package/dist/spec/utils/fs.js +163 -0
  307. package/dist/spec/utils/fs.js.map +1 -0
  308. package/dist/spec/utils/index.d.ts +12 -0
  309. package/dist/spec/utils/index.d.ts.map +1 -0
  310. package/dist/spec/utils/index.js +26 -0
  311. package/dist/spec/utils/index.js.map +1 -0
  312. package/dist/spec/utils/meta.d.ts +32 -0
  313. package/dist/spec/utils/meta.d.ts.map +1 -0
  314. package/dist/spec/utils/meta.js +122 -0
  315. package/dist/spec/utils/meta.js.map +1 -0
  316. package/dist/spec/utils/truncate.d.ts +86 -0
  317. package/dist/spec/utils/truncate.d.ts.map +1 -0
  318. package/dist/spec/utils/truncate.js +172 -0
  319. package/dist/spec/utils/truncate.js.map +1 -0
  320. package/dist/spec/utils.d.ts +4 -16
  321. package/dist/spec/utils.d.ts.map +1 -1
  322. package/dist/spec/utils.js +6 -58
  323. package/dist/spec/utils.js.map +1 -1
  324. package/dist/sync/watcher.d.ts +25 -1
  325. package/dist/sync/watcher.d.ts.map +1 -1
  326. package/dist/sync/watcher.js +76 -3
  327. package/dist/sync/watcher.js.map +1 -1
  328. package/dist/telemetry/index.js +3 -3
  329. package/dist/telemetry/index.js.map +1 -1
  330. package/dist/types.d.ts +3 -0
  331. package/dist/types.d.ts.map +1 -1
  332. package/package.json +7 -3
@@ -17,8 +17,12 @@ exports.resolveMethodOnType = resolveMethodOnType;
17
17
  exports.matchCppCallChain = matchCppCallChain;
18
18
  exports.matchScopedCallChain = matchScopedCallChain;
19
19
  exports.matchDottedCallChain = matchDottedCallChain;
20
+ exports.normalizeInferredTypeName = normalizeInferredTypeName;
21
+ exports.clearNameMatcherMemos = clearNameMatcherMemos;
22
+ exports.localReceiverTypePatterns = localReceiverTypePatterns;
20
23
  exports.matchMethodCall = matchMethodCall;
21
24
  exports.matchFuzzy = matchFuzzy;
25
+ exports.dumpNameMatcherProfile = dumpNameMatcherProfile;
22
26
  exports.matchReference = matchReference;
23
27
  /**
24
28
  * Ceiling on how many same-named definitions a FUZZY name-match strategy will
@@ -36,11 +40,11 @@ exports.matchReference = matchReference;
36
40
  * resolution all still run and resolve a ubiquitous name when the context names
37
41
  * its exact target. Real repos top out near ~40 same-named methods, so a normal
38
42
  * codebase never reaches this; only bulk-vendored code does. Tune via
39
- * `CODEGRAPH_AMBIGUOUS_NAME_CEILING`.
43
+ * `HOMEGRAPH_AMBIGUOUS_NAME_CEILING`.
40
44
  */
41
45
  const DEFAULT_AMBIGUOUS_NAME_CEILING = 500;
42
46
  function resolveAmbiguousNameCeiling() {
43
- const raw = process.env.CODEGRAPH_AMBIGUOUS_NAME_CEILING;
47
+ const raw = process.env.HOMEGRAPH_AMBIGUOUS_NAME_CEILING;
44
48
  if (!raw)
45
49
  return DEFAULT_AMBIGUOUS_NAME_CEILING;
46
50
  const parsed = Number.parseInt(raw, 10);
@@ -328,6 +332,35 @@ function matchFunctionRef(ref, context) {
328
332
  }
329
333
  return null;
330
334
  }
335
+ /**
336
+ * A function nested inside another FUNCTION is only callable from within its
337
+ * container — Python, JS/TS, and every closure language scope it lexically.
338
+ * Resolving a bare name from elsewhere to a nested local fabricates an edge
339
+ * scope already rules out: `join(...)` in one function must never bind to a
340
+ * `join` defined inside a DIFFERENT function (#1230). A candidate whose
341
+ * qualifiedName parent is a same-file function/method is kept only when the
342
+ * ref originates inside that parent's line range. Class members are
343
+ * unaffected (their parent resolves to a class-like node), as are top-level
344
+ * symbols and C++ namespace-prefixed names (the prefix has no node).
345
+ */
346
+ function isLexicallyReachable(candidate, ref, context) {
347
+ if (candidate.kind !== 'function')
348
+ return true;
349
+ const qn = candidate.qualifiedName;
350
+ if (!qn || !qn.includes('::'))
351
+ return true;
352
+ const parentQn = qn.slice(0, qn.lastIndexOf('::'));
353
+ const containers = context
354
+ .getNodesByQualifiedName(parentQn)
355
+ .filter((p) => p.filePath === candidate.filePath &&
356
+ (p.kind === 'function' || p.kind === 'method') &&
357
+ p.startLine <= candidate.startLine &&
358
+ p.endLine >= candidate.endLine);
359
+ if (containers.length === 0)
360
+ return true;
361
+ return (ref.filePath === candidate.filePath &&
362
+ containers.some((p) => ref.line >= p.startLine && ref.line <= p.endLine));
363
+ }
331
364
  /**
332
365
  * Try to resolve a reference by exact name match
333
366
  */
@@ -342,7 +375,9 @@ function matchByExactName(ref, context) {
342
375
  // findBestMatch — O(K²) per package, the dominant cost of "Resolving refs" on
343
376
  // large import-heavy (front-end + back-end) repos (#915).
344
377
  const candidates = applyLanguageGate(context.getNodesByName(ref.referenceName), ref)
345
- .filter((n) => n.kind !== 'import');
378
+ .filter((n) => n.kind !== 'import')
379
+ // Nested locals are only reachable from inside their container (#1230).
380
+ .filter((n) => isLexicallyReachable(n, ref, context));
346
381
  if (candidates.length === 0) {
347
382
  return null;
348
383
  }
@@ -513,11 +548,16 @@ depth = 0) {
513
548
  // populated in the conformance pass. Still VALIDATED (the method must exist on
514
549
  // a supertype), so a wrong inference produces no edge.
515
550
  if (depth < 4 && context.getSupertypes) {
516
- for (const supertype of context.getSupertypes(typeName, ref.language)) {
517
- const via = resolveMethodOnType(supertype, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1);
518
- if (via)
519
- return via;
520
- }
551
+ const viaSupers = nmTimedT('rmot-supers', ref, () => {
552
+ for (const supertype of context.getSupertypes(typeName, ref.language)) {
553
+ const via = resolveMethodOnType(supertype, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1);
554
+ if (via)
555
+ return via;
556
+ }
557
+ return null;
558
+ });
559
+ if (viaSupers)
560
+ return viaSupers;
521
561
  }
522
562
  return null;
523
563
  }
@@ -839,10 +879,11 @@ function matchDottedCallChain(ref, context) {
839
879
  // CRITICAL: resolve the TARGET via a synthetic bare-name ref, but return the
840
880
  // match tied to the ORIGINAL `ref` (referenceName `inner().method`). The
841
881
  // batched resolver (resolveAndPersistBatched) reads unresolved rows from
842
- // offset 0 every pass and relies on deleteSpecificResolvedReferences —
843
- // keyed on referenceName — to clear each resolved row so the batch empties.
844
- // If we propagated the synthetic ref's bare `method` as `.original`, the
845
- // delete would never match the stored `inner().method` row, the batch would
882
+ // offset 0 every pass and relies on the post-batch cleanup (row-id delete
883
+ // for DB-loaded refs, referenceName-keyed delete otherwise, #1269) to
884
+ // clear each resolved row so the batch empties. If we propagated the
885
+ // synthetic ref's bare `method` as `.original`, a key-based delete
886
+ // would never match the stored `inner().method` row, the batch would
846
887
  // never drain, and the loop would re-resolve + re-insert forever (a runaway
847
888
  // that grew gin's graph to 5M edges / 1.4 GB before this fix).
848
889
  const bareRef = { ...ref, referenceName: method };
@@ -998,7 +1039,50 @@ function normalizeInferredTypeName(raw) {
998
1039
  * PascalCase is required in the capture where the language convention allows,
999
1040
  * as a cheap false-positive guard on top of resolveMethodOnType's validation.
1000
1041
  */
1042
+ /**
1043
+ * Compiled-pattern memo for the receiver-type pattern builders below. They
1044
+ * run for EVERY `receiver.method()` ref the matcher attempts, compiling 2–4
1045
+ * fresh RegExp objects per call — and receivers repeat massively (`self`
1046
+ * alone accounts for tens of thousands of refs on a Lua repo, measured 41µs
1047
+ * per methodCall miss on kong with compilation a large slice). The patterns
1048
+ * are a pure function of (language, receiver) and non-global (`.match()`
1049
+ * never touches lastIndex), so shared instances are behavior-identical.
1050
+ * FIFO-capped with no per-get mutation (the §7a.6 LRU-churn lesson): a hit
1051
+ * costs one Map lookup, overflow evicts oldest, and an evicted entry simply
1052
+ * recompiles exactly as every call did before this memo.
1053
+ */
1054
+ const PATTERN_MEMO = new Map();
1055
+ const PATTERN_MEMO_CAP = 8192;
1056
+ const INFER_SCAN_STATES = new WeakMap();
1057
+ function getInferScanStates(context) {
1058
+ let m = INFER_SCAN_STATES.get(context);
1059
+ if (!m) {
1060
+ m = new Map();
1061
+ INFER_SCAN_STATES.set(context, m);
1062
+ }
1063
+ return m;
1064
+ }
1065
+ /** Drop the per-context scan states (see ReferenceResolver.clearCaches). */
1066
+ function clearNameMatcherMemos(context) {
1067
+ INFER_SCAN_STATES.delete(context);
1068
+ }
1069
+ function memoPatterns(key, build) {
1070
+ const hit = PATTERN_MEMO.get(key);
1071
+ if (hit)
1072
+ return hit;
1073
+ const patterns = build();
1074
+ if (PATTERN_MEMO.size >= PATTERN_MEMO_CAP) {
1075
+ const oldest = PATTERN_MEMO.keys().next().value;
1076
+ if (oldest !== undefined)
1077
+ PATTERN_MEMO.delete(oldest);
1078
+ }
1079
+ PATTERN_MEMO.set(key, patterns);
1080
+ return patterns;
1081
+ }
1001
1082
  function localReceiverTypePatterns(language, r) {
1083
+ return memoPatterns(`${language}|${r}`, () => buildLocalReceiverTypePatterns(language, r));
1084
+ }
1085
+ function buildLocalReceiverTypePatterns(language, r) {
1002
1086
  switch (language) {
1003
1087
  case 'typescript':
1004
1088
  case 'javascript':
@@ -1241,6 +1325,53 @@ function inferLocalReceiverType(receiverName, ref, context) {
1241
1325
  }
1242
1326
  return null;
1243
1327
  };
1328
+ // Incremental-scan memo (INFER_SCAN_STATES): this scan runs for EVERY
1329
+ // `receiver.method()` ref and was measured at 61µs/ref on kong (2.4s of
1330
+ // worker time, 99% misses — `self:` calls hunting a declaration Lua never
1331
+ // writes). Refs for the same (file, scope, receiver) arrive in ~ascending
1332
+ // line order, and the scan is a pure function of the file's immutable
1333
+ // lines, so each line pays its regex matches ONCE per key instead of once
1334
+ // per ref: query(c) = highest matching line in [startIdx..c]; a monotonic
1335
+ // call extends the stored watermark by scanning only (hi..c] (the region
1336
+ // at-or-below the previous answer is already proven empty above it); a
1337
+ // non-monotonic call (rare — refs are rowid-ordered) falls back to the
1338
+ // plain bounded scan and leaves the state alone. componentScoped is keyed
1339
+ // out — its position-independent whole-file sweep below has different
1340
+ // semantics.
1341
+ if (!componentScoped) {
1342
+ const states = getInferScanStates(context);
1343
+ const key = `${ref.filePath}|${startIdx}|${ref.language}|${scanReceiver}`;
1344
+ const state = states.get(key);
1345
+ if (!state) {
1346
+ for (let i = callIdx; i >= startIdx; i--) {
1347
+ const type = matchLine(i);
1348
+ if (type) {
1349
+ states.set(key, { hi: callIdx, ansIdx: i, ansType: type });
1350
+ return type;
1351
+ }
1352
+ }
1353
+ states.set(key, { hi: callIdx, ansIdx: -1, ansType: null });
1354
+ return null;
1355
+ }
1356
+ if (callIdx >= state.hi) {
1357
+ for (let i = callIdx; i > state.hi; i--) {
1358
+ const type = matchLine(i);
1359
+ if (type) {
1360
+ state.ansIdx = i;
1361
+ state.ansType = type;
1362
+ break;
1363
+ }
1364
+ }
1365
+ state.hi = callIdx;
1366
+ return state.ansIdx >= startIdx ? state.ansType : null;
1367
+ }
1368
+ for (let i = callIdx; i >= startIdx; i--) {
1369
+ const type = matchLine(i);
1370
+ if (type)
1371
+ return type;
1372
+ }
1373
+ return null;
1374
+ }
1244
1375
  // Nearest declaration wins: scan backward from the call to the scope start.
1245
1376
  for (let i = callIdx; i >= startIdx; i--) {
1246
1377
  const type = matchLine(i);
@@ -1283,6 +1414,9 @@ function inferLocalReceiverType(receiverName, ref, context) {
1283
1414
  * shape is handled by inferPhpAssignedPropertyType instead.
1284
1415
  */
1285
1416
  function phpPropertyTypePatterns(r) {
1417
+ return memoPatterns(`php-prop|${r}`, () => buildPhpPropertyTypePatterns(r));
1418
+ }
1419
+ function buildPhpPropertyTypePatterns(r) {
1286
1420
  return [
1287
1421
  new RegExp(`\\b(?:(?:private|protected|public|readonly|static|final)(?:\\(set\\))?\\s+)+\\??([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // private readonly ?Foo $prop (typed property / promoted param)
1288
1422
  new RegExp(`\\$this->${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $this->prop = new Foo()
@@ -1362,7 +1496,17 @@ function matchMethodCall(ref, context) {
1362
1496
  // (with its existing single-candidate / receiver-overlap guards). Without this
1363
1497
  // a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
1364
1498
  // `Guard.Against.X()`) matched no pattern and never resolved.
1365
- const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/);
1499
+ // C++ explicit operator call `a.operator+(b)` reaches the resolver as
1500
+ // `a.operator+` (#1247) — the operator's symbol chars (`+`, `==`, `[]`, `()`)
1501
+ // fail the \w method part of the plain pattern, so admit them explicitly.
1502
+ // Names like `operatorTable` stay on the plain pattern (tried first); the
1503
+ // operator form requires at least one non-word char after `operator`, and
1504
+ // every downstream strategy compares the method part by exact string
1505
+ // equality, so a stray match can't invent an edge.
1506
+ const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/) ??
1507
+ (ref.language === 'cpp'
1508
+ ? ref.referenceName.match(/^([\w.]+)\.(operator[^\w\s.]+)$/)
1509
+ : null);
1366
1510
  const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
1367
1511
  // Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
1368
1512
  // Recognize these receiver/method separators so local-variable receiver-type
@@ -1406,9 +1550,9 @@ function matchMethodCall(ref, context) {
1406
1550
  // shared source-based inferrer. resolveMethodOnType validates the method
1407
1551
  // exists on the inferred type, so a mis-inference produces no edge.
1408
1552
  if (inferableReceiver) {
1409
- const inferredType = ref.language === 'cpp'
1553
+ const inferredType = nmTimedT('mc-infer', ref, () => ref.language === 'cpp'
1410
1554
  ? inferCppReceiverType(objectOrClass, ref, context)
1411
- : inferLocalReceiverType(objectOrClass, ref, context);
1555
+ : inferLocalReceiverType(objectOrClass, ref, context));
1412
1556
  if (inferredType) {
1413
1557
  // Java/Kotlin: when two classes share the simple name, the file's import
1414
1558
  // pins WHICH one (#314). Other languages disambiguate by call-site file.
@@ -1417,12 +1561,26 @@ function matchMethodCall(ref, context) {
1417
1561
  .getImportMappings(ref.filePath, ref.language)
1418
1562
  .find((i) => i.localName === inferredType)?.source
1419
1563
  : undefined;
1420
- const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
1564
+ const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
1421
1565
  if (typedMatch) {
1422
1566
  return typedMatch;
1423
1567
  }
1424
1568
  }
1425
1569
  }
1570
+ // Go 2-hop field chain `base.field.Method` (#1276): the base's type comes
1571
+ // from the enclosing scope (typed parameter / method receiver / local var),
1572
+ // the field's declared type from that struct's own declaration lines, and
1573
+ // the method is VALIDATED on the field's type by resolveMethodOnType. This
1574
+ // branch is EXCLUSIVE for chained Go receivers: when the hop can't be
1575
+ // inferred or the field's type is external (`conn *sql.DB` — no project
1576
+ // node), the ref stays unresolved rather than falling through to the
1577
+ // bare-name strategies below, which is exactly how `target.conn.Exec(...)`
1578
+ // fabricated a dependency on an unrelated local interface's same-named
1579
+ // method. Chained Go receivers were never emitted before #1276, so there
1580
+ // is no prior recall to preserve on the fallback path.
1581
+ if (ref.language === 'go' && dotMatch && objectOrClass.includes('.')) {
1582
+ return matchGoFieldChainCall(objectOrClass, methodName, ref, context);
1583
+ }
1426
1584
  // Java/Kotlin: receiver may be a field whose name doesn't match the type by
1427
1585
  // Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
1428
1586
  // the field in the enclosing class to get its declared type, then resolve
@@ -1436,7 +1594,7 @@ function matchMethodCall(ref, context) {
1436
1594
  // imported FQN so resolveMethodOnType can disambiguate (#314).
1437
1595
  const imports = context.getImportMappings(ref.filePath, ref.language);
1438
1596
  const importedFqn = imports.find((i) => i.localName === inferredType)?.source;
1439
- const typedMatch = resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn);
1597
+ const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
1440
1598
  if (typedMatch) {
1441
1599
  return typedMatch;
1442
1600
  }
@@ -1447,33 +1605,23 @@ function matchMethodCall(ref, context) {
1447
1605
  // with a `Logger` in both `a/` and `b/`), try the class in the call site's
1448
1606
  // own file first — otherwise the first-indexed class wins and a call in `b/`
1449
1607
  // resolves to `a/`'s method (#1079).
1450
- const classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
1451
- for (const classNode of classCandidates) {
1452
- if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
1453
- // Skip cross-language class matches
1454
- if (classNode.language !== ref.language)
1455
- continue;
1456
- const nodesInFile = context.getNodesInFile(classNode.filePath);
1457
- const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
1458
- n.name === methodName &&
1459
- n.qualifiedName.includes(classNode.name));
1460
- if (methodNode) {
1461
- return {
1462
- original: ref,
1463
- targetNodeId: methodNode.id,
1464
- confidence: 0.85,
1465
- resolvedBy: 'qualified-name',
1466
- };
1467
- }
1608
+ const strat1 = nmTimedT('mc-class', ref, () => {
1609
+ // ArkTS: same-file OR imported local name only. Unrestricted project-wide
1610
+ // matches welded every CFG `Foo.getInstance` seed onto one method; requiring
1611
+ // `import { Foo }` (or a same-file class) keeps cross-module `Lib.use` while
1612
+ // dropping ambient / unimported stubs.
1613
+ let classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
1614
+ if (ref.language === 'arkts') {
1615
+ const imported = context
1616
+ .getImportMappings(ref.filePath, ref.language)
1617
+ .some((i) => i.localName === objectOrClass);
1618
+ classCandidates = classCandidates.filter((n) => n.filePath === ref.filePath || imported);
1468
1619
  }
1469
- }
1470
- // Strategy 2: Instance variable receiver - try capitalized form to find class
1471
- // e.g., "permissionEngine" → look for classes containing "PermissionEngine"
1472
- const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
1473
- if (capitalizedReceiver !== objectOrClass) {
1474
- const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
1475
- for (const classNode of fuzzyClassCandidates) {
1476
- if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
1620
+ for (const classNode of classCandidates) {
1621
+ if (classNode.kind === 'class' ||
1622
+ classNode.kind === 'struct' ||
1623
+ classNode.kind === 'interface' ||
1624
+ (ref.language === 'arkts' && classNode.kind === 'component')) {
1477
1625
  // Skip cross-language class matches
1478
1626
  if (classNode.language !== ref.language)
1479
1627
  continue;
@@ -1485,67 +1633,192 @@ function matchMethodCall(ref, context) {
1485
1633
  return {
1486
1634
  original: ref,
1487
1635
  targetNodeId: methodNode.id,
1488
- confidence: 0.8,
1489
- resolvedBy: 'instance-method',
1636
+ confidence: 0.85,
1637
+ resolvedBy: 'qualified-name',
1490
1638
  };
1491
1639
  }
1492
1640
  }
1493
1641
  }
1642
+ return null;
1643
+ });
1644
+ if (strat1)
1645
+ return strat1;
1646
+ // Strategy 2: Instance variable receiver - try capitalized form to find class
1647
+ // e.g., "permissionEngine" → look for classes containing "PermissionEngine"
1648
+ const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
1649
+ // ArkTS: skip — cross-module seeds are already `Class.method`; capitalizing
1650
+ // locals caused wrong bindings on large Harmony trees.
1651
+ if (ref.language !== 'arkts' && capitalizedReceiver !== objectOrClass) {
1652
+ const strat2 = nmTimedT('mc-capital', ref, () => {
1653
+ const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
1654
+ for (const classNode of fuzzyClassCandidates) {
1655
+ if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
1656
+ // Skip cross-language class matches
1657
+ if (classNode.language !== ref.language)
1658
+ continue;
1659
+ const nodesInFile = context.getNodesInFile(classNode.filePath);
1660
+ const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
1661
+ n.name === methodName &&
1662
+ n.qualifiedName.includes(classNode.name));
1663
+ if (methodNode) {
1664
+ return {
1665
+ original: ref,
1666
+ targetNodeId: methodNode.id,
1667
+ confidence: 0.8,
1668
+ resolvedBy: 'instance-method',
1669
+ };
1670
+ }
1671
+ }
1672
+ }
1673
+ return null;
1674
+ });
1675
+ if (strat2)
1676
+ return strat2;
1494
1677
  }
1495
1678
  // Strategy 3: Find methods by name across the codebase, match by receiver
1496
1679
  // name similarity with the containing class. Handles abbreviated variable
1497
1680
  // names like permissionEngine → PermissionRuleEngine.
1681
+ // ArkTS: never — unique/score fallthrough turns `Foo.getInstance` into a
1682
+ // project-wide getInstance magnet when Class is wrong/missing.
1683
+ if (ref.language === 'arkts') {
1684
+ return null;
1685
+ }
1498
1686
  if (methodName) {
1499
- const methodCandidates = context.getNodesByName(methodName);
1500
- // Ubiquitous-method ceiling (#999): a method name re-declared across a
1501
- // vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
1502
- // K candidates that receiver-word overlap can't reliably disambiguate —
1503
- // and filtering + scoring all K per call is the O(K²) cost that wedged
1504
- // "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
1505
- // (class-name match) already had their precise shot above.
1506
- if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
1507
- return null;
1508
- }
1509
- const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
1510
- // Filter to same-language candidates first
1511
- const sameLanguageMethods = methods.filter(m => m.language === ref.language);
1512
- const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
1513
- // If only one same-language method with this name exists, use it
1514
- if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
1515
- return {
1516
- original: ref,
1517
- targetNodeId: targetMethods[0].id,
1518
- confidence: 0.7,
1519
- resolvedBy: 'instance-method',
1520
- };
1521
- }
1522
- // Multiple methods: score by receiver name word overlap with class name
1523
- if (targetMethods.length > 1) {
1524
- const receiverWords = splitCamelCase(objectOrClass);
1525
- let bestMatch;
1526
- let bestScore = 0;
1527
- // Same-file candidates first, so a score tie (`score > bestScore` keeps
1528
- // the first seen) resolves to the call site's own file rather than the
1529
- // first-indexed duplicate (#1079).
1530
- for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
1531
- const classWords = splitCamelCase(method.qualifiedName);
1532
- let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
1533
- // Bonus for same language
1534
- if (method.language === ref.language)
1535
- score += 1;
1536
- if (score > bestScore) {
1537
- bestScore = score;
1538
- bestMatch = method;
1539
- }
1687
+ const strat3 = nmTimedT('mc-byname', ref, () => {
1688
+ const methodCandidates = context.getNodesByName(methodName);
1689
+ // Ubiquitous-method ceiling (#999): a method name re-declared across a
1690
+ // vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
1691
+ // K candidates that receiver-word overlap can't reliably disambiguate —
1692
+ // and filtering + scoring all K per call is the O(K²) cost that wedged
1693
+ // "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
1694
+ // (class-name match) already had their precise shot above.
1695
+ if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
1696
+ return null;
1540
1697
  }
1541
- if (bestMatch && bestScore >= 2) {
1698
+ const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
1699
+ // Filter to same-language candidates first
1700
+ const sameLanguageMethods = methods.filter(m => m.language === ref.language);
1701
+ const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
1702
+ // If only one same-language method with this name exists, use it
1703
+ if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
1542
1704
  return {
1543
1705
  original: ref,
1544
- targetNodeId: bestMatch.id,
1545
- confidence: 0.65,
1706
+ targetNodeId: targetMethods[0].id,
1707
+ confidence: 0.7,
1546
1708
  resolvedBy: 'instance-method',
1547
1709
  };
1548
1710
  }
1711
+ // Multiple methods: score by receiver name word overlap with class name
1712
+ if (targetMethods.length > 1) {
1713
+ const receiverWords = splitCamelCase(objectOrClass);
1714
+ let bestMatch;
1715
+ let bestScore = 0;
1716
+ // Same-file candidates first, so a score tie (`score > bestScore` keeps
1717
+ // the first seen) resolves to the call site's own file rather than the
1718
+ // first-indexed duplicate (#1079).
1719
+ for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
1720
+ const classWords = splitCamelCase(method.qualifiedName);
1721
+ let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
1722
+ // Bonus for same language
1723
+ if (method.language === ref.language)
1724
+ score += 1;
1725
+ if (score > bestScore) {
1726
+ bestScore = score;
1727
+ bestMatch = method;
1728
+ }
1729
+ }
1730
+ if (bestMatch && bestScore >= 2) {
1731
+ return {
1732
+ original: ref,
1733
+ targetNodeId: bestMatch.id,
1734
+ confidence: 0.65,
1735
+ resolvedBy: 'instance-method',
1736
+ };
1737
+ }
1738
+ }
1739
+ return null;
1740
+ });
1741
+ if (strat3)
1742
+ return strat3;
1743
+ }
1744
+ return null;
1745
+ }
1746
+ /** Go builtin/primitive field types that can never carry a project method. */
1747
+ const GO_BUILTIN_FIELD_TYPES = new Set([
1748
+ 'string', 'bool', 'byte', 'rune', 'error', 'any',
1749
+ 'int', 'int8', 'int16', 'int32', 'int64',
1750
+ 'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
1751
+ 'float32', 'float64', 'complex64', 'complex128',
1752
+ 'chan', 'map', 'func', 'struct', 'interface',
1753
+ ]);
1754
+ /**
1755
+ * Resolve a Go 2-hop field-chain call `base.field.Method(...)` (#1276):
1756
+ * `target.conn.Exec("insert")` where `func (target *Target) Write()` and
1757
+ * `type Target struct { conn *sql.DB }`. Two inference hops, both read from
1758
+ * source the same way #1108 does:
1759
+ * 1. `base`'s type from the enclosing scope (method receiver, typed
1760
+ * parameter, or local declaration) via inferLocalReceiverType;
1761
+ * 2. `field`'s declared type from the struct's own declaration lines.
1762
+ * The method is then resolved AND VALIDATED on the field's type. A field
1763
+ * whose type has no project node (`sql.DB`, any external dependency) yields
1764
+ * null — the caller treats this branch as exclusive for chained Go
1765
+ * receivers, so the ref stays unresolved instead of name-guessing.
1766
+ */
1767
+ function matchGoFieldChainCall(receiverChain, methodName, ref, context) {
1768
+ const segs = receiverChain.split('.');
1769
+ if (segs.length !== 2 || !segs[0] || !segs[1])
1770
+ return null;
1771
+ const [base, field] = segs;
1772
+ const baseType = inferLocalReceiverType(base, ref, context);
1773
+ if (!baseType)
1774
+ return null;
1775
+ const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1776
+ const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`);
1777
+ const structs = preferCallSiteFile(context.getNodesByName(baseType), ref.filePath).filter((n) => (n.kind === 'struct' || n.kind === 'class') && n.language === 'go');
1778
+ for (const s of structs) {
1779
+ const source = context.readFile(s.filePath);
1780
+ if (!source)
1781
+ continue;
1782
+ // Only the struct's own declaration lines — a same-named identifier
1783
+ // elsewhere in the file can't donate a type. Matched LINE BY LINE with
1784
+ // comments stripped: chi's `Mux` has a doc comment reading "the tree
1785
+ // router" right above `tree *node`, and a whole-block match captured
1786
+ // `router` from the prose instead of `node` from the field.
1787
+ const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
1788
+ for (const rawLine of declLines) {
1789
+ const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
1790
+ const m = line.match(fieldTypeRe);
1791
+ if (!m || !m[1])
1792
+ continue;
1793
+ const rawType = m[1];
1794
+ // A package-qualified field type (`http.Handler`, `sql.DB`) is only
1795
+ // followed when the package is IN-MODULE: stripping the qualifier and
1796
+ // matching the bare name would conflate a stdlib/third-party type with
1797
+ // any same-named project type — on chi, `handler http.Handler` bound
1798
+ // to an example app's unrelated local `Handler`. That is the exact
1799
+ // fabrication this matcher exists to prevent (#1276).
1800
+ if (rawType.includes('.')) {
1801
+ const pkg = rawType.split('.')[0];
1802
+ const mod = context.getGoModule?.();
1803
+ const imp = context
1804
+ .getImportMappings(s.filePath, 'go')
1805
+ .find((i) => i.localName === pkg);
1806
+ const inModule = !!mod &&
1807
+ !!imp &&
1808
+ (imp.source === mod.modulePath || imp.source.startsWith(mod.modulePath + '/'));
1809
+ if (!inModule)
1810
+ continue;
1811
+ }
1812
+ // Unexported (lowercase) types are idiomatic Go and stay eligible —
1813
+ // chi's `mx.tree.FindRoute()` chains through `tree *node`. A
1814
+ // mis-capture is harmless: resolveMethodOnType only returns a
1815
+ // validated `<type>::<method>` match.
1816
+ const fieldType = rawType.split('.').pop();
1817
+ if (!fieldType || !/^[A-Za-z_]/.test(fieldType) || GO_BUILTIN_FIELD_TYPES.has(fieldType))
1818
+ continue;
1819
+ const resolved = resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
1820
+ if (resolved)
1821
+ return resolved;
1549
1822
  }
1550
1823
  }
1551
1824
  return null;
@@ -1708,6 +1981,49 @@ function matchFuzzy(ref, context) {
1708
1981
  */
1709
1982
  /** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
1710
1983
  const ARKUI_ATTRIBUTE_DECORATORS = new Set(['Extend', 'Styles', 'AnimatableExtend', 'Builder']);
1984
+ /**
1985
+ * HOMEGRAPH_RESOLVE_PROFILE=2 sub-stage attribution for matchReference's
1986
+ * strategy pipeline (`nm:<stage>|<refKind>|hit/miss`). Module-global because
1987
+ * the matcher is a free function; each thread (main + every pool worker) has
1988
+ * its own module instance, and dumpNameMatcherProfile is invoked from
1989
+ * ReferenceResolver.dumpResolveProfile so worker tables surface too.
1990
+ * (CODEGRAPH_RESOLVE_PROFILE still accepted for upstream-compat.)
1991
+ */
1992
+ const NM_PROFILE = process.env.HOMEGRAPH_RESOLVE_PROFILE === '2' ||
1993
+ process.env.CODEGRAPH_RESOLVE_PROFILE === '2'
1994
+ ? new Map()
1995
+ : null;
1996
+ function nmTimedT(stage, ref, fn) {
1997
+ if (!NM_PROFILE)
1998
+ return fn();
1999
+ const t0 = process.hrtime.bigint();
2000
+ const r = fn();
2001
+ const dt = process.hrtime.bigint() - t0;
2002
+ const key = `nm:${stage}|${ref.referenceKind}|${r ? 'hit' : 'miss'}`;
2003
+ const slot = NM_PROFILE.get(key);
2004
+ if (slot) {
2005
+ slot.n++;
2006
+ slot.ns += dt;
2007
+ }
2008
+ else {
2009
+ NM_PROFILE.set(key, { n: 1, ns: dt });
2010
+ }
2011
+ return r;
2012
+ }
2013
+ function nmTimed(stage, ref, fn) {
2014
+ return nmTimedT(stage, ref, fn);
2015
+ }
2016
+ /** Dump this thread's matchReference sub-stage table to stderr (no-op unless =2). */
2017
+ function dumpNameMatcherProfile(label) {
2018
+ if (!NM_PROFILE || NM_PROFILE.size === 0)
2019
+ return;
2020
+ const rows = [...NM_PROFILE.entries()]
2021
+ .map(([k, v]) => ({ k, n: v.n, ms: Number(v.ns / 1000000n) }))
2022
+ .sort((a, b) => b.ms - a.ms);
2023
+ for (const r of rows) {
2024
+ console.error(`[resolve-profile] ${label} ${r.k}: n=${r.n} total=${(r.ms / 1000).toFixed(1)}s avg=${((r.ms * 1000) / Math.max(1, r.n)).toFixed(0)}µs`);
2025
+ }
2026
+ }
1711
2027
  function matchReference(ref, context) {
1712
2028
  // Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
1713
2029
  // never the fuzzy/qualified fallthrough below (a wrong callback edge is
@@ -1769,18 +2085,18 @@ function matchReference(ref, context) {
1769
2085
  // Try strategies in order of confidence
1770
2086
  let result;
1771
2087
  // 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)
1772
- result = matchByFilePath(ref, context);
2088
+ result = nmTimed('filePath', ref, () => matchByFilePath(ref, context));
1773
2089
  if (result)
1774
2090
  return result;
1775
2091
  // 1. Qualified name match (highest confidence)
1776
- result = matchByQualifiedName(ref, context);
2092
+ result = nmTimed('qualifiedName', ref, () => matchByQualifiedName(ref, context));
1777
2093
  if (result)
1778
2094
  return result;
1779
2095
  // 1b. C++ chained call whose receiver is another call — `Foo::instance().bar()`
1780
2096
  // encoded as `Foo::instance().bar` by the extractor (#645). Resolve the
1781
2097
  // receiver's type from what the inner call returns, then the method on it.
1782
2098
  if (ref.language === 'cpp' || ref.language === 'c') {
1783
- result = matchCppCallChain(ref, context);
2099
+ result = nmTimed('cppChain', ref, () => matchCppCallChain(ref, context));
1784
2100
  if (result)
1785
2101
  return result;
1786
2102
  }
@@ -1789,7 +2105,7 @@ function matchReference(ref, context) {
1789
2105
  // type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
1790
2106
  // concrete return type.
1791
2107
  if (ref.language === 'php' || ref.language === 'rust') {
1792
- result = matchScopedCallChain(ref, context);
2108
+ result = nmTimed('scopedChain', ref, () => matchScopedCallChain(ref, context));
1793
2109
  if (result)
1794
2110
  return result;
1795
2111
  }
@@ -1809,20 +2125,46 @@ function matchReference(ref, context) {
1809
2125
  ref.language === 'dart' ||
1810
2126
  ref.language === 'objc' ||
1811
2127
  ref.language === 'pascal') {
1812
- result = matchDottedCallChain(ref, context);
2128
+ result = nmTimed('dottedChain', ref, () => matchDottedCallChain(ref, context));
1813
2129
  if (result)
1814
2130
  return result;
1815
2131
  }
1816
2132
  // 2. Method call pattern
1817
- result = matchMethodCall(ref, context);
2133
+ result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
1818
2134
  if (result)
1819
2135
  return result;
2136
+ // ArkTS calls / instantiates: no bare exact-match or fuzzy. Cross-module
2137
+ // seeds are `Class.method` (handled above) or a class name for `new`
2138
+ // (`instantiates`). Falling through previously linked every `when`/`toString`
2139
+ // invoke to a single same-named symbol (100k+ false edges on scene_board).
2140
+ if (ref.language === 'arkts' &&
2141
+ (ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates')) {
2142
+ if (ref.referenceKind === 'instantiates' && !ref.referenceName.includes('.')) {
2143
+ // Same-file only — project-wide unique class still over-links constructor
2144
+ // seeds (`GridOccupyStatus` × thousands). Cross-module `new Foo` stays
2145
+ // unresolved rather than guessed (import path is also disabled above).
2146
+ const sameFile = context
2147
+ .getNodesByName(ref.referenceName)
2148
+ .filter((n) => n.language === 'arkts' &&
2149
+ n.filePath === ref.filePath &&
2150
+ (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component'));
2151
+ if (sameFile.length === 1) {
2152
+ return {
2153
+ original: ref,
2154
+ targetNodeId: sameFile[0].id,
2155
+ confidence: 0.85,
2156
+ resolvedBy: 'exact-match',
2157
+ };
2158
+ }
2159
+ }
2160
+ return null;
2161
+ }
1820
2162
  // 3. Exact name match
1821
- result = matchByExactName(ref, context);
2163
+ result = nmTimed('exactName', ref, () => matchByExactName(ref, context));
1822
2164
  if (result)
1823
2165
  return result;
1824
2166
  // 4. Fuzzy match (lowest confidence)
1825
- result = matchFuzzy(ref, context);
2167
+ result = nmTimed('fuzzy', ref, () => matchFuzzy(ref, context));
1826
2168
  if (result)
1827
2169
  return result;
1828
2170
  return null;