homegraph 1.2.0 → 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 (560) hide show
  1. package/README.md +48 -11
  2. package/dist/bin/homegraph.js +298 -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 +117 -11
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/db/index.d.ts +207 -6
  8. package/dist/db/index.d.ts.map +1 -1
  9. package/dist/db/index.js +461 -20
  10. package/dist/db/index.js.map +1 -1
  11. package/dist/db/migrations.d.ts +1 -1
  12. package/dist/db/migrations.d.ts.map +1 -1
  13. package/dist/db/migrations.js +19 -1
  14. package/dist/db/migrations.js.map +1 -1
  15. package/dist/db/queries.d.ts +157 -3
  16. package/dist/db/queries.d.ts.map +1 -1
  17. package/dist/db/queries.js +426 -9
  18. package/dist/db/queries.js.map +1 -1
  19. package/dist/db/schema.sql +32 -1
  20. package/dist/db/sqlite-adapter.d.ts +24 -17
  21. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  22. package/dist/db/sqlite-adapter.js +292 -36
  23. package/dist/db/sqlite-adapter.js.map +1 -1
  24. package/dist/db/wal-valve.d.ts +119 -0
  25. package/dist/db/wal-valve.d.ts.map +1 -0
  26. package/dist/db/wal-valve.js +290 -0
  27. package/dist/db/wal-valve.js.map +1 -0
  28. package/dist/directory.d.ts +9 -5
  29. package/dist/directory.d.ts.map +1 -1
  30. package/dist/directory.js +210 -19
  31. package/dist/directory.js.map +1 -1
  32. package/dist/extraction/arkts-batch-worker.js +3 -2
  33. package/dist/extraction/arkts-batch-worker.js.map +1 -1
  34. package/dist/extraction/cfml-extractor.d.ts +107 -0
  35. package/dist/extraction/cfml-extractor.d.ts.map +1 -0
  36. package/dist/extraction/cfml-extractor.js +494 -0
  37. package/dist/extraction/cfml-extractor.js.map +1 -0
  38. package/dist/extraction/default-ignore.d.ts +39 -0
  39. package/dist/extraction/default-ignore.d.ts.map +1 -0
  40. package/dist/extraction/default-ignore.js +221 -0
  41. package/dist/extraction/default-ignore.js.map +1 -0
  42. package/dist/extraction/grammars.d.ts +25 -1
  43. package/dist/extraction/grammars.d.ts.map +1 -1
  44. package/dist/extraction/grammars.js +177 -24
  45. package/dist/extraction/grammars.js.map +1 -1
  46. package/dist/extraction/index.d.ts +52 -14
  47. package/dist/extraction/index.d.ts.map +1 -1
  48. package/dist/extraction/index.js +521 -314
  49. package/dist/extraction/index.js.map +1 -1
  50. package/dist/extraction/languages/arkts.d.ts +90 -6
  51. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  52. package/dist/extraction/languages/arkts.js +1579 -128
  53. package/dist/extraction/languages/arkts.js.map +1 -1
  54. package/dist/extraction/languages/c-cpp.d.ts +42 -0
  55. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  56. package/dist/extraction/languages/c-cpp.js +369 -4
  57. package/dist/extraction/languages/c-cpp.js.map +1 -1
  58. package/dist/extraction/languages/cfquery.d.ts +12 -0
  59. package/dist/extraction/languages/cfquery.d.ts.map +1 -0
  60. package/dist/extraction/languages/cfquery.js +28 -0
  61. package/dist/extraction/languages/cfquery.js.map +1 -0
  62. package/dist/extraction/languages/cfscript.d.ts +3 -0
  63. package/dist/extraction/languages/cfscript.d.ts.map +1 -0
  64. package/dist/extraction/languages/cfscript.js +73 -0
  65. package/dist/extraction/languages/cfscript.js.map +1 -0
  66. package/dist/extraction/languages/cobol.d.ts +33 -0
  67. package/dist/extraction/languages/cobol.d.ts.map +1 -0
  68. package/dist/extraction/languages/cobol.js +499 -0
  69. package/dist/extraction/languages/cobol.js.map +1 -0
  70. package/dist/extraction/languages/erlang.d.ts +3 -0
  71. package/dist/extraction/languages/erlang.d.ts.map +1 -0
  72. package/dist/extraction/languages/erlang.js +350 -0
  73. package/dist/extraction/languages/erlang.js.map +1 -0
  74. package/dist/extraction/languages/index.d.ts.map +1 -1
  75. package/dist/extraction/languages/index.js +16 -0
  76. package/dist/extraction/languages/index.js.map +1 -1
  77. package/dist/extraction/languages/nix.d.ts +3 -0
  78. package/dist/extraction/languages/nix.d.ts.map +1 -0
  79. package/dist/extraction/languages/nix.js +294 -0
  80. package/dist/extraction/languages/nix.js.map +1 -0
  81. package/dist/extraction/languages/solidity.d.ts +3 -0
  82. package/dist/extraction/languages/solidity.d.ts.map +1 -0
  83. package/dist/extraction/languages/solidity.js +293 -0
  84. package/dist/extraction/languages/solidity.js.map +1 -0
  85. package/dist/extraction/languages/terraform.d.ts +3 -0
  86. package/dist/extraction/languages/terraform.d.ts.map +1 -0
  87. package/dist/extraction/languages/terraform.js +641 -0
  88. package/dist/extraction/languages/terraform.js.map +1 -0
  89. package/dist/extraction/languages/vbnet.d.ts +11 -0
  90. package/dist/extraction/languages/vbnet.d.ts.map +1 -0
  91. package/dist/extraction/languages/vbnet.js +141 -0
  92. package/dist/extraction/languages/vbnet.js.map +1 -0
  93. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  94. package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  95. package/dist/extraction/mybatis-extractor.js +140 -38
  96. package/dist/extraction/mybatis-extractor.js.map +1 -1
  97. package/dist/extraction/parse-pool.d.ts +27 -1
  98. package/dist/extraction/parse-pool.d.ts.map +1 -1
  99. package/dist/extraction/parse-pool.js +75 -7
  100. package/dist/extraction/parse-pool.js.map +1 -1
  101. package/dist/extraction/parse-worker.js +9 -2
  102. package/dist/extraction/parse-worker.js.map +1 -1
  103. package/dist/extraction/store-worker.d.ts +20 -0
  104. package/dist/extraction/store-worker.d.ts.map +1 -0
  105. package/dist/extraction/store-worker.js +102 -0
  106. package/dist/extraction/store-worker.js.map +1 -0
  107. package/dist/extraction/store-writer.d.ts +48 -0
  108. package/dist/extraction/store-writer.d.ts.map +1 -0
  109. package/dist/extraction/store-writer.js +167 -0
  110. package/dist/extraction/store-writer.js.map +1 -0
  111. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  112. package/dist/extraction/tree-sitter-helpers.js +1 -0
  113. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  114. package/dist/extraction/tree-sitter-types.d.ts +3 -1
  115. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  116. package/dist/extraction/tree-sitter.d.ts +38 -0
  117. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  118. package/dist/extraction/tree-sitter.js +801 -10
  119. package/dist/extraction/tree-sitter.js.map +1 -1
  120. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  121. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  122. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  123. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  124. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  125. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  126. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  127. package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  128. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  129. package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
  130. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  131. package/dist/extraction/wasm-runtime-flags.js +55 -7
  132. package/dist/extraction/wasm-runtime-flags.js.map +1 -1
  133. package/dist/graph/traversal.d.ts.map +1 -1
  134. package/dist/graph/traversal.js +1 -1
  135. package/dist/graph/traversal.js.map +1 -1
  136. package/dist/index.d.ts +74 -5
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +436 -55
  139. package/dist/index.js.map +1 -1
  140. package/dist/installer/index.d.ts +42 -0
  141. package/dist/installer/index.d.ts.map +1 -1
  142. package/dist/installer/index.js +97 -2
  143. package/dist/installer/index.js.map +1 -1
  144. package/dist/mcp/daemon.d.ts +25 -3
  145. package/dist/mcp/daemon.d.ts.map +1 -1
  146. package/dist/mcp/daemon.js +62 -8
  147. package/dist/mcp/daemon.js.map +1 -1
  148. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  149. package/dist/mcp/dynamic-boundaries.js +2 -1
  150. package/dist/mcp/dynamic-boundaries.js.map +1 -1
  151. package/dist/mcp/early-ppid.d.ts +26 -0
  152. package/dist/mcp/early-ppid.d.ts.map +1 -0
  153. package/dist/mcp/early-ppid.js +29 -0
  154. package/dist/mcp/early-ppid.js.map +1 -0
  155. package/dist/mcp/engine.d.ts +4 -5
  156. package/dist/mcp/engine.d.ts.map +1 -1
  157. package/dist/mcp/engine.js +27 -1
  158. package/dist/mcp/engine.js.map +1 -1
  159. package/dist/mcp/index.d.ts.map +1 -1
  160. package/dist/mcp/index.js +41 -6
  161. package/dist/mcp/index.js.map +1 -1
  162. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  163. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  164. package/dist/mcp/liveness-watchdog.js +73 -13
  165. package/dist/mcp/liveness-watchdog.js.map +1 -1
  166. package/dist/mcp/memory-budget.d.ts +30 -0
  167. package/dist/mcp/memory-budget.d.ts.map +1 -0
  168. package/dist/mcp/memory-budget.js +123 -0
  169. package/dist/mcp/memory-budget.js.map +1 -0
  170. package/dist/mcp/proxy.d.ts.map +1 -1
  171. package/dist/mcp/proxy.js +88 -4
  172. package/dist/mcp/proxy.js.map +1 -1
  173. package/dist/mcp/query-cache.d.ts +35 -4
  174. package/dist/mcp/query-cache.d.ts.map +1 -1
  175. package/dist/mcp/query-cache.js +89 -19
  176. package/dist/mcp/query-cache.js.map +1 -1
  177. package/dist/mcp/query-pool.d.ts +46 -12
  178. package/dist/mcp/query-pool.d.ts.map +1 -1
  179. package/dist/mcp/query-pool.js +100 -23
  180. package/dist/mcp/query-pool.js.map +1 -1
  181. package/dist/mcp/query-worker.d.ts +2 -2
  182. package/dist/mcp/query-worker.js +2 -2
  183. package/dist/mcp/server-instructions.d.ts +4 -27
  184. package/dist/mcp/server-instructions.d.ts.map +1 -1
  185. package/dist/mcp/server-instructions.js +39 -84
  186. package/dist/mcp/server-instructions.js.map +1 -1
  187. package/dist/mcp/session.d.ts +14 -0
  188. package/dist/mcp/session.d.ts.map +1 -1
  189. package/dist/mcp/session.js +28 -1
  190. package/dist/mcp/session.js.map +1 -1
  191. package/dist/mcp/startup-handshake.d.ts +44 -0
  192. package/dist/mcp/startup-handshake.d.ts.map +1 -0
  193. package/dist/mcp/startup-handshake.js +73 -0
  194. package/dist/mcp/startup-handshake.js.map +1 -0
  195. package/dist/mcp/tools.d.ts +104 -4
  196. package/dist/mcp/tools.d.ts.map +1 -1
  197. package/dist/mcp/tools.js +2432 -217
  198. package/dist/mcp/tools.js.map +1 -1
  199. package/dist/mcp/transport.d.ts.map +1 -1
  200. package/dist/mcp/transport.js +18 -0
  201. package/dist/mcp/transport.js.map +1 -1
  202. package/dist/project-config.d.ts +38 -0
  203. package/dist/project-config.d.ts.map +1 -1
  204. package/dist/project-config.js +101 -2
  205. package/dist/project-config.js.map +1 -1
  206. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  207. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  208. package/dist/resolution/c-fnptr-synthesizer.js +178 -146
  209. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  210. package/dist/resolution/callback-synthesizer.d.ts +54 -10
  211. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  212. package/dist/resolution/callback-synthesizer.js +1299 -257
  213. package/dist/resolution/callback-synthesizer.js.map +1 -1
  214. package/dist/resolution/cooperative-yield.d.ts +10 -2
  215. package/dist/resolution/cooperative-yield.d.ts.map +1 -1
  216. package/dist/resolution/cooperative-yield.js +6 -4
  217. package/dist/resolution/cooperative-yield.js.map +1 -1
  218. package/dist/resolution/frameworks/cics.d.ts +20 -0
  219. package/dist/resolution/frameworks/cics.d.ts.map +1 -0
  220. package/dist/resolution/frameworks/cics.js +90 -0
  221. package/dist/resolution/frameworks/cics.js.map +1 -0
  222. package/dist/resolution/frameworks/go.d.ts.map +1 -1
  223. package/dist/resolution/frameworks/go.js +29 -6
  224. package/dist/resolution/frameworks/go.js.map +1 -1
  225. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  226. package/dist/resolution/frameworks/index.js +3 -0
  227. package/dist/resolution/frameworks/index.js.map +1 -1
  228. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  229. package/dist/resolution/frameworks/java.js +14 -6
  230. package/dist/resolution/frameworks/java.js.map +1 -1
  231. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  232. package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  233. package/dist/resolution/frameworks/terraform.js +277 -0
  234. package/dist/resolution/frameworks/terraform.js.map +1 -0
  235. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  236. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  237. package/dist/resolution/goframe-synthesizer.js +8 -3
  238. package/dist/resolution/goframe-synthesizer.js.map +1 -1
  239. package/dist/resolution/import-resolver.d.ts +9 -3
  240. package/dist/resolution/import-resolver.d.ts.map +1 -1
  241. package/dist/resolution/import-resolver.js +295 -11
  242. package/dist/resolution/import-resolver.js.map +1 -1
  243. package/dist/resolution/index.d.ts +146 -8
  244. package/dist/resolution/index.d.ts.map +1 -1
  245. package/dist/resolution/index.js +1089 -131
  246. package/dist/resolution/index.js.map +1 -1
  247. package/dist/resolution/memory-budget.d.ts +48 -0
  248. package/dist/resolution/memory-budget.d.ts.map +1 -0
  249. package/dist/resolution/memory-budget.js +162 -0
  250. package/dist/resolution/memory-budget.js.map +1 -0
  251. package/dist/resolution/name-matcher.d.ts +12 -4
  252. package/dist/resolution/name-matcher.d.ts.map +1 -1
  253. package/dist/resolution/name-matcher.js +793 -130
  254. package/dist/resolution/name-matcher.js.map +1 -1
  255. package/dist/resolution/resolver-pool.d.ts +106 -0
  256. package/dist/resolution/resolver-pool.d.ts.map +1 -0
  257. package/dist/resolution/resolver-pool.js +344 -0
  258. package/dist/resolution/resolver-pool.js.map +1 -0
  259. package/dist/resolution/resolver-worker.d.ts +17 -0
  260. package/dist/resolution/resolver-worker.d.ts.map +1 -0
  261. package/dist/resolution/resolver-worker.js +141 -0
  262. package/dist/resolution/resolver-worker.js.map +1 -0
  263. package/dist/resolution/strip-comments.d.ts +1 -1
  264. package/dist/resolution/strip-comments.d.ts.map +1 -1
  265. package/dist/resolution/strip-comments.js +49 -0
  266. package/dist/resolution/strip-comments.js.map +1 -1
  267. package/dist/resolution/types.d.ts +32 -0
  268. package/dist/resolution/types.d.ts.map +1 -1
  269. package/dist/resolution/workspace-packages.d.ts +10 -0
  270. package/dist/resolution/workspace-packages.d.ts.map +1 -1
  271. package/dist/resolution/workspace-packages.js +142 -4
  272. package/dist/resolution/workspace-packages.js.map +1 -1
  273. package/dist/search/identifier-segments.d.ts +60 -0
  274. package/dist/search/identifier-segments.d.ts.map +1 -0
  275. package/dist/search/identifier-segments.js +176 -0
  276. package/dist/search/identifier-segments.js.map +1 -0
  277. package/dist/search/query-utils.d.ts +201 -0
  278. package/dist/search/query-utils.d.ts.map +1 -1
  279. package/dist/search/query-utils.js +919 -2
  280. package/dist/search/query-utils.js.map +1 -1
  281. package/dist/spec/build/diff-parser.d.ts +55 -0
  282. package/dist/spec/build/diff-parser.d.ts.map +1 -0
  283. package/dist/spec/build/diff-parser.js +205 -0
  284. package/dist/spec/build/diff-parser.js.map +1 -0
  285. package/dist/spec/build/git-scanner.d.ts +93 -0
  286. package/dist/spec/build/git-scanner.d.ts.map +1 -0
  287. package/dist/spec/build/git-scanner.js +254 -0
  288. package/dist/spec/build/git-scanner.js.map +1 -0
  289. package/dist/spec/build/pipeline.d.ts +54 -0
  290. package/dist/spec/build/pipeline.d.ts.map +1 -0
  291. package/dist/spec/build/pipeline.js +170 -0
  292. package/dist/spec/build/pipeline.js.map +1 -0
  293. package/dist/spec/build/scan.d.ts +48 -0
  294. package/dist/spec/build/scan.d.ts.map +1 -0
  295. package/dist/spec/build/scan.js +89 -0
  296. package/dist/spec/build/scan.js.map +1 -0
  297. package/dist/spec/build/scope-resolver.d.ts +50 -0
  298. package/dist/spec/build/scope-resolver.d.ts.map +1 -0
  299. package/dist/spec/build/scope-resolver.js +105 -0
  300. package/dist/spec/build/scope-resolver.js.map +1 -0
  301. package/dist/spec/build/spec-extractor.d.ts +74 -0
  302. package/dist/spec/build/spec-extractor.d.ts.map +1 -0
  303. package/dist/spec/build/spec-extractor.js +344 -0
  304. package/dist/spec/build/spec-extractor.js.map +1 -0
  305. package/dist/spec/config.d.ts +35 -1
  306. package/dist/spec/config.d.ts.map +1 -1
  307. package/dist/spec/config.js +30 -5
  308. package/dist/spec/config.js.map +1 -1
  309. package/dist/spec/db/fts.d.ts.map +1 -1
  310. package/dist/spec/db/fts.js +19 -50
  311. package/dist/spec/db/fts.js.map +1 -1
  312. package/dist/spec/db/index.d.ts +5 -3
  313. package/dist/spec/db/index.d.ts.map +1 -1
  314. package/dist/spec/db/index.js +16 -1
  315. package/dist/spec/db/index.js.map +1 -1
  316. package/dist/spec/db/persist.d.ts +45 -0
  317. package/dist/spec/db/persist.d.ts.map +1 -0
  318. package/dist/spec/db/persist.js +66 -0
  319. package/dist/spec/db/persist.js.map +1 -0
  320. package/dist/spec/db/relations.d.ts +80 -1
  321. package/dist/spec/db/relations.d.ts.map +1 -1
  322. package/dist/spec/db/relations.js +175 -4
  323. package/dist/spec/db/relations.js.map +1 -1
  324. package/dist/spec/db/schema.d.ts +4 -0
  325. package/dist/spec/db/schema.d.ts.map +1 -1
  326. package/dist/spec/db/schema.js +4 -0
  327. package/dist/spec/db/schema.js.map +1 -1
  328. package/dist/spec/db/schema.sql +5 -1
  329. package/dist/spec/db/spec-node.d.ts +5 -0
  330. package/dist/spec/db/spec-node.d.ts.map +1 -1
  331. package/dist/spec/db/spec-node.js +11 -5
  332. package/dist/spec/db/spec-node.js.map +1 -1
  333. package/dist/spec/db/sql-utils.d.ts +11 -0
  334. package/dist/spec/db/sql-utils.d.ts.map +1 -0
  335. package/dist/spec/db/sql-utils.js +19 -0
  336. package/dist/spec/db/sql-utils.js.map +1 -0
  337. package/dist/spec/evolve/cluster-context.d.ts +30 -0
  338. package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
  339. package/dist/spec/evolve/cluster-context.js +78 -0
  340. package/dist/spec/evolve/cluster-context.js.map +1 -0
  341. package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
  342. package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
  343. package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
  344. package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
  345. package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
  346. package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
  347. package/dist/spec/evolve/commit-spec-persister.js +93 -0
  348. package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
  349. package/dist/spec/evolve/impact-locator.d.ts +23 -7
  350. package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
  351. package/dist/spec/evolve/impact-locator.js +58 -14
  352. package/dist/spec/evolve/impact-locator.js.map +1 -1
  353. package/dist/spec/evolve/pipeline.d.ts +62 -25
  354. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  355. package/dist/spec/evolve/pipeline.js +436 -457
  356. package/dist/spec/evolve/pipeline.js.map +1 -1
  357. package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
  358. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  359. package/dist/spec/evolve/spec-rewriter.js +45 -45
  360. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  361. package/dist/spec/git/commits.d.ts +85 -0
  362. package/dist/spec/git/commits.d.ts.map +1 -0
  363. package/dist/spec/git/commits.js +218 -0
  364. package/dist/spec/git/commits.js.map +1 -0
  365. package/dist/spec/git/exec.d.ts +13 -0
  366. package/dist/spec/git/exec.d.ts.map +1 -0
  367. package/dist/spec/git/exec.js +19 -0
  368. package/dist/spec/git/exec.js.map +1 -0
  369. package/dist/spec/git/index.d.ts +9 -0
  370. package/dist/spec/git/index.d.ts.map +1 -0
  371. package/dist/spec/git/index.js +22 -0
  372. package/dist/spec/git/index.js.map +1 -0
  373. package/dist/spec/git-utils.d.ts +8 -0
  374. package/dist/spec/git-utils.d.ts.map +1 -0
  375. package/dist/spec/git-utils.js +14 -0
  376. package/dist/spec/git-utils.js.map +1 -0
  377. package/dist/spec/graph/index.d.ts +8 -0
  378. package/dist/spec/graph/index.d.ts.map +1 -0
  379. package/dist/spec/graph/index.js +17 -0
  380. package/dist/spec/graph/index.js.map +1 -0
  381. package/dist/spec/graph/queries.d.ts +6 -35
  382. package/dist/spec/graph/queries.d.ts.map +1 -1
  383. package/dist/spec/graph/queries.js +71 -248
  384. package/dist/spec/graph/queries.js.map +1 -1
  385. package/dist/spec/llm/agent-client.d.ts +59 -0
  386. package/dist/spec/llm/agent-client.d.ts.map +1 -0
  387. package/dist/spec/llm/agent-client.js +213 -0
  388. package/dist/spec/llm/agent-client.js.map +1 -0
  389. package/dist/spec/llm/agents/claude-code.d.ts +32 -0
  390. package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
  391. package/dist/spec/llm/agents/claude-code.js +125 -0
  392. package/dist/spec/llm/agents/claude-code.js.map +1 -0
  393. package/dist/spec/llm/agents/codex.d.ts +32 -0
  394. package/dist/spec/llm/agents/codex.d.ts.map +1 -0
  395. package/dist/spec/llm/agents/codex.js +145 -0
  396. package/dist/spec/llm/agents/codex.js.map +1 -0
  397. package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
  398. package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
  399. package/dist/spec/llm/agents/detect-utils.js +91 -0
  400. package/dist/spec/llm/agents/detect-utils.js.map +1 -0
  401. package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
  402. package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
  403. package/dist/spec/llm/agents/deveco-code.js +165 -0
  404. package/dist/spec/llm/agents/deveco-code.js.map +1 -0
  405. package/dist/spec/llm/agents/index.d.ts +31 -0
  406. package/dist/spec/llm/agents/index.d.ts.map +1 -0
  407. package/dist/spec/llm/agents/index.js +65 -0
  408. package/dist/spec/llm/agents/index.js.map +1 -0
  409. package/dist/spec/llm/agents/types.d.ts +76 -0
  410. package/dist/spec/llm/agents/types.d.ts.map +1 -0
  411. package/dist/spec/llm/agents/types.js +15 -0
  412. package/dist/spec/llm/agents/types.js.map +1 -0
  413. package/dist/spec/llm/client.d.ts +31 -11
  414. package/dist/spec/llm/client.d.ts.map +1 -1
  415. package/dist/spec/llm/client.js +143 -81
  416. package/dist/spec/llm/client.js.map +1 -1
  417. package/dist/spec/llm/factory.d.ts +41 -0
  418. package/dist/spec/llm/factory.d.ts.map +1 -0
  419. package/dist/spec/llm/factory.js +80 -0
  420. package/dist/spec/llm/factory.js.map +1 -0
  421. package/dist/spec/llm/prompts.d.ts +24 -8
  422. package/dist/spec/llm/prompts.d.ts.map +1 -1
  423. package/dist/spec/llm/prompts.js +100 -50
  424. package/dist/spec/llm/prompts.js.map +1 -1
  425. package/dist/spec/llm/retry.d.ts +53 -0
  426. package/dist/spec/llm/retry.d.ts.map +1 -0
  427. package/dist/spec/llm/retry.js +153 -0
  428. package/dist/spec/llm/retry.js.map +1 -0
  429. package/dist/spec/mine/clusterer.d.ts +63 -0
  430. package/dist/spec/mine/clusterer.d.ts.map +1 -0
  431. package/dist/spec/mine/clusterer.js +904 -0
  432. package/dist/spec/mine/clusterer.js.map +1 -0
  433. package/dist/spec/mine/clustering/features.d.ts +38 -0
  434. package/dist/spec/mine/clustering/features.d.ts.map +1 -0
  435. package/dist/spec/mine/clustering/features.js +108 -0
  436. package/dist/spec/mine/clustering/features.js.map +1 -0
  437. package/dist/spec/mine/clustering/index.d.ts +62 -0
  438. package/dist/spec/mine/clustering/index.d.ts.map +1 -0
  439. package/dist/spec/mine/clustering/index.js +259 -0
  440. package/dist/spec/mine/clustering/index.js.map +1 -0
  441. package/dist/spec/mine/clustering/leiden.d.ts +61 -0
  442. package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
  443. package/dist/spec/mine/clustering/leiden.js +489 -0
  444. package/dist/spec/mine/clustering/leiden.js.map +1 -0
  445. package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
  446. package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
  447. package/dist/spec/mine/clustering/text-similarity.js +98 -0
  448. package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
  449. package/dist/spec/mine/generator.d.ts +54 -0
  450. package/dist/spec/mine/generator.d.ts.map +1 -0
  451. package/dist/spec/mine/generator.js +267 -0
  452. package/dist/spec/mine/generator.js.map +1 -0
  453. package/dist/spec/mine/persist.d.ts +37 -0
  454. package/dist/spec/mine/persist.d.ts.map +1 -0
  455. package/dist/spec/mine/persist.js +170 -0
  456. package/dist/spec/mine/persist.js.map +1 -0
  457. package/dist/spec/mine/pipeline.d.ts +41 -0
  458. package/dist/spec/mine/pipeline.d.ts.map +1 -0
  459. package/dist/spec/mine/pipeline.js +236 -0
  460. package/dist/spec/mine/pipeline.js.map +1 -0
  461. package/dist/spec/mine/progress-handler.d.ts +22 -0
  462. package/dist/spec/mine/progress-handler.d.ts.map +1 -0
  463. package/dist/spec/mine/progress-handler.js +108 -0
  464. package/dist/spec/mine/progress-handler.js.map +1 -0
  465. package/dist/spec/mine/progress.d.ts +23 -0
  466. package/dist/spec/mine/progress.d.ts.map +1 -0
  467. package/dist/spec/mine/progress.js +12 -0
  468. package/dist/spec/mine/progress.js.map +1 -0
  469. package/dist/spec/mine/scanner.d.ts +59 -0
  470. package/dist/spec/mine/scanner.d.ts.map +1 -0
  471. package/dist/spec/mine/scanner.js +383 -0
  472. package/dist/spec/mine/scanner.js.map +1 -0
  473. package/dist/spec/types.d.ts +32 -0
  474. package/dist/spec/types.d.ts.map +1 -1
  475. package/dist/spec/ui/index.d.ts +9 -0
  476. package/dist/spec/ui/index.d.ts.map +1 -0
  477. package/dist/spec/ui/index.js +16 -0
  478. package/dist/spec/ui/index.js.map +1 -0
  479. package/dist/spec/ui/progress-handler.d.ts +33 -0
  480. package/dist/spec/ui/progress-handler.d.ts.map +1 -0
  481. package/dist/spec/ui/progress-handler.js +132 -0
  482. package/dist/spec/ui/progress-handler.js.map +1 -0
  483. package/dist/spec/ui/progress.d.ts +24 -0
  484. package/dist/spec/ui/progress.d.ts.map +1 -0
  485. package/dist/spec/ui/progress.js +13 -0
  486. package/dist/spec/ui/progress.js.map +1 -0
  487. package/dist/spec/utils/fs.d.ts +45 -0
  488. package/dist/spec/utils/fs.d.ts.map +1 -0
  489. package/dist/spec/utils/fs.js +163 -0
  490. package/dist/spec/utils/fs.js.map +1 -0
  491. package/dist/spec/utils/index.d.ts +12 -0
  492. package/dist/spec/utils/index.d.ts.map +1 -0
  493. package/dist/spec/utils/index.js +26 -0
  494. package/dist/spec/utils/index.js.map +1 -0
  495. package/dist/spec/utils/meta.d.ts +32 -0
  496. package/dist/spec/utils/meta.d.ts.map +1 -0
  497. package/dist/spec/utils/meta.js +122 -0
  498. package/dist/spec/utils/meta.js.map +1 -0
  499. package/dist/spec/utils/truncate.d.ts +86 -0
  500. package/dist/spec/utils/truncate.d.ts.map +1 -0
  501. package/dist/spec/utils/truncate.js +172 -0
  502. package/dist/spec/utils/truncate.js.map +1 -0
  503. package/dist/spec/utils.d.ts +4 -16
  504. package/dist/spec/utils.d.ts.map +1 -1
  505. package/dist/spec/utils.js +6 -58
  506. package/dist/spec/utils.js.map +1 -1
  507. package/dist/sync/git-hooks.d.ts.map +1 -1
  508. package/dist/sync/git-hooks.js +2 -0
  509. package/dist/sync/git-hooks.js.map +1 -1
  510. package/dist/sync/watcher.d.ts +35 -6
  511. package/dist/sync/watcher.d.ts.map +1 -1
  512. package/dist/sync/watcher.js +127 -17
  513. package/dist/sync/watcher.js.map +1 -1
  514. package/dist/sync/worktree.d.ts.map +1 -1
  515. package/dist/sync/worktree.js +5 -0
  516. package/dist/sync/worktree.js.map +1 -1
  517. package/dist/telemetry/index.js +3 -3
  518. package/dist/telemetry/index.js.map +1 -1
  519. package/dist/types.d.ts +22 -1
  520. package/dist/types.d.ts.map +1 -1
  521. package/dist/types.js +10 -0
  522. package/dist/types.js.map +1 -1
  523. package/dist/upgrade/index.d.ts +32 -0
  524. package/dist/upgrade/index.d.ts.map +1 -1
  525. package/dist/upgrade/index.js +162 -14
  526. package/dist/upgrade/index.js.map +1 -1
  527. package/dist/upgrade/remove-binary.d.ts +87 -0
  528. package/dist/upgrade/remove-binary.d.ts.map +1 -0
  529. package/dist/upgrade/remove-binary.js +289 -0
  530. package/dist/upgrade/remove-binary.js.map +1 -0
  531. package/dist/upgrade/update-check.d.ts +92 -0
  532. package/dist/upgrade/update-check.d.ts.map +1 -0
  533. package/dist/upgrade/update-check.js +258 -0
  534. package/dist/upgrade/update-check.js.map +1 -0
  535. package/package.json +6 -2
  536. package/scripts/agent-eval/run-all.sh +6 -0
  537. package/scripts/build-bundle.sh +5 -0
  538. package/scripts/npm-shim.js +8 -1
  539. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  540. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  541. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  542. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  543. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  544. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  545. package/scripts/exp_boundary_eval/data/agents.json +0 -109
  546. package/scripts/exp_boundary_eval/data/experiments.json +0 -140
  547. package/scripts/qa_eval/README.md +0 -407
  548. package/scripts/qa_eval/_test_deveco_probe.py +0 -41
  549. package/scripts/qa_eval/agent_runner.py +0 -526
  550. package/scripts/qa_eval/data/.gitignore +0 -4
  551. package/scripts/qa_eval/data/test-set.jsonl +0 -2
  552. package/scripts/qa_eval/eval_metrics.py +0 -274
  553. package/scripts/qa_eval/external_agent.py +0 -976
  554. package/scripts/qa_eval/llm_config.py +0 -92
  555. package/scripts/qa_eval/memory_monitor.py +0 -132
  556. package/scripts/qa_eval/my_answer_accuracy.py +0 -187
  557. package/scripts/qa_eval/requirements.txt +0 -2
  558. package/scripts/qa_eval/run_pipeline.py +0 -804
  559. package/scripts/qa_eval/stats_efficiency.py +0 -279
  560. package/scripts/qa_eval/stats_scores.py +0 -207
@@ -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);
@@ -140,7 +144,9 @@ function pickClosestFileNode(candidates, ref) {
140
144
  const LANGUAGE_FAMILY = {
141
145
  java: 'jvm', kotlin: 'jvm', scala: 'jvm',
142
146
  swift: 'apple', objc: 'apple',
143
- typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web',
147
+ // ArkTS is a TS superset — every HarmonyOS project mixes `.ets` UI with
148
+ // `.ts` logic modules, so refs must cross freely between them.
149
+ typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web', arkts: 'web',
144
150
  c: 'c', cpp: 'c',
145
151
  // Razor/Blazor markup names C# types — same family so `@model Foo` /
146
152
  // `<MyComponent/>` resolve to their `.cs` class through the cross-family gate.
@@ -222,6 +228,7 @@ function matchFunctionRef(ref, context) {
222
228
  // implicit-self, Java/Kotlin method references.
223
229
  const bareFnOnly = ref.language === 'typescript' || ref.language === 'tsx' ||
224
230
  ref.language === 'javascript' || ref.language === 'jsx' ||
231
+ ref.language === 'arkts' ||
225
232
  ref.language === 'cpp' || ref.language === 'python' ||
226
233
  ref.language === 'php';
227
234
  // Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
@@ -325,6 +332,35 @@ function matchFunctionRef(ref, context) {
325
332
  }
326
333
  return null;
327
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
+ }
328
364
  /**
329
365
  * Try to resolve a reference by exact name match
330
366
  */
@@ -339,7 +375,9 @@ function matchByExactName(ref, context) {
339
375
  // findBestMatch — O(K²) per package, the dominant cost of "Resolving refs" on
340
376
  // large import-heavy (front-end + back-end) repos (#915).
341
377
  const candidates = applyLanguageGate(context.getNodesByName(ref.referenceName), ref)
342
- .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));
343
381
  if (candidates.length === 0) {
344
382
  return null;
345
383
  }
@@ -384,7 +422,17 @@ function matchByQualifiedName(ref, context) {
384
422
  if (!ref.referenceName.includes('::') && !ref.referenceName.includes('.')) {
385
423
  return null;
386
424
  }
387
- const candidates = context.getNodesByQualifiedName(ref.referenceName);
425
+ // A method call `receiver.method()` can share an exact qualified name with a
426
+ // config-file key: `service.process()` (a `calls` ref named `service.process`)
427
+ // vs the yaml key `service.process`. Config keys are bound to their code refs
428
+ // upstream by the framework resolvers (`@Value` → `references`); a `calls` ref
429
+ // must never resolve to a yaml/properties config node — that's a wrong edge
430
+ // AND it hides the real callee. Drop those from both the exact and the partial
431
+ // candidate sets so resolution falls through to method resolution below (#1180).
432
+ const keepForRef = (nodes) => ref.referenceKind === 'calls'
433
+ ? nodes.filter((n) => !(n.kind === 'constant' && (n.language === 'yaml' || n.language === 'properties')))
434
+ : nodes;
435
+ const candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
388
436
  if (candidates.length === 1) {
389
437
  return {
390
438
  original: ref,
@@ -413,8 +461,7 @@ function matchByQualifiedName(ref, context) {
413
461
  const parts = ref.referenceName.split(/[:.]/);
414
462
  const lastName = parts[parts.length - 1];
415
463
  if (lastName) {
416
- const partialCandidates = context
417
- .getNodesByName(lastName)
464
+ const partialCandidates = keepForRef(context.getNodesByName(lastName))
418
465
  .filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName));
419
466
  const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
420
467
  if (chosen) {
@@ -468,17 +515,28 @@ depth = 0) {
468
515
  // in-class (`class Foo { int bar() { ... } }`) or out-of-line in a separate
469
516
  // file (`int Foo::bar() { ... }` in foo.cpp while class Foo is in foo.hpp).
470
517
  // The previous same-file approach missed the latter — the typical C++ layout.
471
- const methodCandidates = context.getNodesByName(methodName);
472
- const want = `${typeName}::${methodName}`;
473
- const matches = [];
474
- for (const m of methodCandidates) {
475
- if (m.kind !== 'method')
476
- continue;
477
- if (m.language !== ref.language)
478
- continue;
479
- const qn = m.qualifiedName;
480
- if (qn === want || qn.endsWith(`::${want}`)) {
481
- matches.push(m);
518
+ // Prefer the context's per-(type, method) memo: the raw name lookup fetches
519
+ // EVERY node sharing the method name — tens of thousands of rows for a
520
+ // collision-heavy Java name like `execute` — and re-filtering that per ref
521
+ // was a dominant term in the #1122 watchdog kill on large repos. Only the
522
+ // ref-independent filter is memoized; per-ref disambiguation stays below.
523
+ let matches;
524
+ if (context.getMethodMatches) {
525
+ matches = context.getMethodMatches(typeName, methodName, ref.language);
526
+ }
527
+ else {
528
+ const methodCandidates = context.getNodesByName(methodName);
529
+ const want = `${typeName}::${methodName}`;
530
+ matches = [];
531
+ for (const m of methodCandidates) {
532
+ if (m.kind !== 'method')
533
+ continue;
534
+ if (m.language !== ref.language)
535
+ continue;
536
+ const qn = m.qualifiedName;
537
+ if (qn === want || qn.endsWith(`::${want}`)) {
538
+ matches.push(m);
539
+ }
482
540
  }
483
541
  }
484
542
  if (matches.length === 0) {
@@ -490,11 +548,16 @@ depth = 0) {
490
548
  // populated in the conformance pass. Still VALIDATED (the method must exist on
491
549
  // a supertype), so a wrong inference produces no edge.
492
550
  if (depth < 4 && context.getSupertypes) {
493
- for (const supertype of context.getSupertypes(typeName, ref.language)) {
494
- const via = resolveMethodOnType(supertype, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1);
495
- if (via)
496
- return via;
497
- }
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;
498
561
  }
499
562
  return null;
500
563
  }
@@ -565,10 +628,14 @@ function buildDeclaratorRegex(escapedReceiver) {
565
628
  return new RegExp(`([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`);
566
629
  }
567
630
  function inferCppReceiverType(receiverName, ref, context, depth = 0) {
568
- const source = context.readFile(ref.filePath);
569
- if (!source)
631
+ // Per-file lines cache when available — this runs per `receiver->method()`
632
+ // ref and re-splitting the file each time is the same quadratic as the
633
+ // shared inferrer's (#1122).
634
+ const lines = context.getFileLines
635
+ ? context.getFileLines(ref.filePath)
636
+ : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
637
+ if (!lines || lines.length === 0)
570
638
  return null;
571
- const lines = source.split(/\r?\n/);
572
639
  const callLineIndex = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
573
640
  const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
574
641
  const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`);
@@ -601,10 +668,12 @@ function inferCppReceiverType(receiverName, ref, context, depth = 0) {
601
668
  for (const headerPath of headerCandidates) {
602
669
  if (!context.fileExists(headerPath))
603
670
  continue;
604
- const headerSource = context.readFile(headerPath);
605
- if (!headerSource)
671
+ const headerLines = context.getFileLines
672
+ ? context.getFileLines(headerPath)
673
+ : (context.readFile(headerPath)?.split(/\r?\n/) ?? null);
674
+ if (!headerLines)
606
675
  continue;
607
- for (const line of headerSource.split(/\r?\n/)) {
676
+ for (const line of headerLines) {
608
677
  if (!receiverPattern.test(line))
609
678
  continue;
610
679
  const declaratorMatch = line.match(declaratorRegex);
@@ -810,10 +879,11 @@ function matchDottedCallChain(ref, context) {
810
879
  // CRITICAL: resolve the TARGET via a synthetic bare-name ref, but return the
811
880
  // match tied to the ORIGINAL `ref` (referenceName `inner().method`). The
812
881
  // batched resolver (resolveAndPersistBatched) reads unresolved rows from
813
- // offset 0 every pass and relies on deleteSpecificResolvedReferences —
814
- // keyed on referenceName — to clear each resolved row so the batch empties.
815
- // If we propagated the synthetic ref's bare `method` as `.original`, the
816
- // 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
817
887
  // never drain, and the loop would re-resolve + re-insert forever (a runaway
818
888
  // that grew gin's graph to 5M edges / 1.4 GB before this fix).
819
889
  const bareRef = { ...ref, referenceName: method };
@@ -969,15 +1039,66 @@ function normalizeInferredTypeName(raw) {
969
1039
  * PascalCase is required in the capture where the language convention allows,
970
1040
  * as a cheap false-positive guard on top of resolveMethodOnType's validation.
971
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
+ }
972
1082
  function localReceiverTypePatterns(language, r) {
1083
+ return memoPatterns(`${language}|${r}`, () => buildLocalReceiverTypePatterns(language, r));
1084
+ }
1085
+ function buildLocalReceiverTypePatterns(language, r) {
973
1086
  switch (language) {
974
1087
  case 'typescript':
975
1088
  case 'javascript':
976
1089
  case 'tsx':
977
1090
  case 'jsx':
1091
+ case 'arkts':
978
1092
  return [
979
1093
  new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), // = new Logger()
980
- new RegExp(`\\b(?:const|let|var)\\s+${r}\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger
1094
+ // No keyword requirement, so this matches BOTH a local annotation
1095
+ // (`const lg: Logger`) and a typed parameter (`function use(lg: Logger)`
1096
+ // / `(lg: Logger) =>`) — the parameter case the old `const|let|var`
1097
+ // prefix excluded (#1125). Mirrors Kotlin/Swift/Scala; the capture stops
1098
+ // at `<` so a generic-typed param (`repo: Repository<User>`) still yields
1099
+ // `Repository`. resolveMethodOnType validates the type actually declares
1100
+ // the method, so the looser match produces no edge on a mis-inference.
1101
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger (annotation or typed param)
981
1102
  ];
982
1103
  case 'python':
983
1104
  return [
@@ -1007,12 +1128,21 @@ function localReceiverTypePatterns(language, r) {
1007
1128
  case 'rust':
1008
1129
  return [
1009
1130
  new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\b(?:\\s*:[^=]+)?=\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg = Logger::new()/Logger{}/Logger
1010
- new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg: Logger
1131
+ // No `let`, so this covers a `let lg: Logger` binding AND a typed
1132
+ // parameter (`fn use(lg: &Logger)`, a closure `|lg: Logger|`) — the
1133
+ // parameter case the old `let`-anchored pattern excluded (#1125).
1134
+ new RegExp(`\\b${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // lg: Logger (binding or typed param)
1011
1135
  ];
1012
1136
  case 'go':
1013
1137
  return [
1014
1138
  new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{}
1015
1139
  new RegExp(`\\bvar\\s+${r}\\s+\\*?([A-Za-z_][\\w.]*)`), // var lg Logger / *Logger
1140
+ // A typed parameter / method receiver (`func use(lg Logger)`,
1141
+ // `func (l Logger) M()`) — name-before-type with no `var`/`:=` (#1125).
1142
+ // PascalCase-guarded (unlike the anchored patterns above) to keep the
1143
+ // keyword-free `ident Type` shape from matching unrelated pairs; the
1144
+ // enclosing-scope bound already excludes package-level struct fields.
1145
+ new RegExp(`\\b${r}\\s+\\*?([A-Z][\\w.]*)`), // func use(lg Logger) / (l Logger)
1016
1146
  ];
1017
1147
  case 'ruby':
1018
1148
  return [
@@ -1026,18 +1156,35 @@ function localReceiverTypePatterns(language, r) {
1026
1156
  case 'dart':
1027
1157
  return [
1028
1158
  new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // var lg = Logger(...)
1029
- new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;]`), // Logger lg = ...
1159
+ // Trailing `[=;,)]` (not just `[=;]`) so a typed parameter — `Logger lg)`
1160
+ // / `Logger lg,` — matches too, not only `Logger lg = ...` / `Logger lg;`
1161
+ // (#1125). Mirrors Java/C#.
1162
+ new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg = ... / param
1030
1163
  ];
1031
1164
  case 'php':
1032
1165
  return [
1033
1166
  new RegExp(`\\$?${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $lg = new Logger()
1167
+ // A typed parameter (`function use(Logger $lg)`, `?Logger $lg`,
1168
+ // `\\App\\Logger $lg`, `&$lg` by-ref) and a typed `catch (E $e)` — the
1169
+ // type sits before the `$`-variable (#1125). Namespace `\\` allowed.
1170
+ new RegExp(`\\b([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // Logger $lg (typed param)
1034
1171
  ];
1035
1172
  case 'lua':
1036
1173
  case 'luau':
1037
1174
  return [
1038
1175
  new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\.new\\b`), // local lg = Logger.new()
1039
1176
  new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\s*\\(`), // local lg = Logger(...) (callable table)
1040
- new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // Luau: local lg: Logger / typed param
1177
+ // Luau annotation (`local lg: Logger`) / typed param — but Lua's
1178
+ // method-call syntax is the IDENTICAL `receiver:Name` shape, and the
1179
+ // backward scan starts on the call's own line, so without a gate any
1180
+ // PascalCase method call (`lg:Log()`, the Roblox convention)
1181
+ // self-matches as "type = Log" before the scan reaches the real
1182
+ // declaration (#1124). The lookahead rejects a capture followed by
1183
+ // any of Lua's three call forms — `(args)`, `"s"`/`'s'`/`[[s]]`,
1184
+ // `{t}` — and its leading `[\w.]` alternative stops backtracking from
1185
+ // shrinking the capture to dodge the gate (`lg:Log()` would otherwise
1186
+ // still match, as `Lo`).
1187
+ new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)(?![\\w.]|\\s*[({"'\\[])`), // local lg: Logger / typed param
1041
1188
  ];
1042
1189
  case 'r':
1043
1190
  return [
@@ -1048,6 +1195,36 @@ function localReceiverTypePatterns(language, r) {
1048
1195
  new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w]*)`), // var lg: TLogger / param lg: TLogger
1049
1196
  new RegExp(`\\b${r}\\b\\s*:=\\s*([A-Z][\\w.]*)\\.Create\\b`), // lg := TLogger.Create
1050
1197
  ];
1198
+ case 'cfml':
1199
+ case 'cfscript':
1200
+ return [
1201
+ // svc = new UserService() / new path.to.UserService() — dotted component
1202
+ // paths reduce to their final segment via normalizeInferredTypeName.
1203
+ // Also matches inside tag markup (`<cfset svc = new UserService()>`)
1204
+ // since the scan reads raw source lines.
1205
+ new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`),
1206
+ // The classic form: svc = createObject("component", "path.to.UserService")
1207
+ // (casing of createObject varies in the wild), plus the modern
1208
+ // single-argument form createObject("path.to.UserService").
1209
+ new RegExp(`\\b${r}\\b\\s*=\\s*[Cc]reate[Oo]bject\\s*\\(\\s*["']component["']\\s*,\\s*["']([\\w.]+)["']`),
1210
+ new RegExp(`\\b${r}\\b\\s*=\\s*[Cc]reate[Oo]bject\\s*\\(\\s*["']([\\w.]+)["']\\s*\\)`),
1211
+ // Typed cfscript parameter: `function save(UserService svc)` /
1212
+ // `required UserService svc` — CFML's built-in types (string, numeric,
1213
+ // any, struct…) are lowercase by convention, so the PascalCase guard
1214
+ // excludes them.
1215
+ new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`),
1216
+ // Tag-form typed argument, either attribute order:
1217
+ // <cfargument name="svc" type="path.to.UserService">
1218
+ new RegExp(`\\bcfargument[^>\\n]*\\bname\\s*=\\s*["']${r}["'][^>\\n]*\\btype\\s*=\\s*["']([\\w.]+)["']`, 'i'),
1219
+ new RegExp(`\\bcfargument[^>\\n]*\\btype\\s*=\\s*["']([\\w.]+)["'][^>\\n]*\\bname\\s*=\\s*["']${r}["']`, 'i'),
1220
+ // Component property (incl. WireBox DI): `property name="svc"
1221
+ // inject="UserService";` / `<cfproperty name="svc" type="UserService">`,
1222
+ // either attribute order. An inject DSL value with a namespace
1223
+ // (`inject="svc@core"`) captures only the leading name and simply
1224
+ // fails type-validation — no edge, never a wrong one.
1225
+ new RegExp(`\\b(?:cf)?property\\b[^;\\n]*\\bname\\s*=\\s*["']${r}["'][^;\\n]*\\b(?:type|inject)\\s*=\\s*["']([\\w.]+)["']`, 'i'),
1226
+ new RegExp(`\\b(?:cf)?property\\b[^;\\n]*\\b(?:type|inject)\\s*=\\s*["']([\\w.]+)["'][^;\\n]*\\bname\\s*=\\s*["']${r}["']`, 'i'),
1227
+ ];
1051
1228
  default:
1052
1229
  return [];
1053
1230
  }
@@ -1074,20 +1251,70 @@ function enclosingScopeStartLine(ref, context) {
1074
1251
  * scope so a same-named variable in another function can't leak in.
1075
1252
  */
1076
1253
  function inferLocalReceiverType(receiverName, ref, context) {
1077
- const patterns = localReceiverTypePatterns(ref.language, receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1254
+ // CFML scope prefixes: `variables.svc` / `this.svc` name a COMPONENT-scoped
1255
+ // field whose assignment or `property` declaration usually lives outside the
1256
+ // calling function (the init-pseudoconstructor / WireBox-injection pattern),
1257
+ // and `local.svc` is an explicit function-local. Strip the prefix so the
1258
+ // declaration patterns match (`variables.svc = new X()`, `property
1259
+ // name="svc" …`, `var svc = …` all bind the bare name), and widen the scan
1260
+ // to the whole file for the component-scoped forms — nearest-declaration-
1261
+ // backward still wins, so a function-local shadowing the field is preferred.
1262
+ let scanReceiver = receiverName;
1263
+ let componentScoped = false;
1264
+ if (ref.language === 'cfml' || ref.language === 'cfscript') {
1265
+ const scoped = receiverName.match(/^(variables|this|local|arguments)\.(.+)$/i);
1266
+ if (scoped) {
1267
+ scanReceiver = scoped[2];
1268
+ const scope = scoped[1].toLowerCase();
1269
+ componentScoped = scope === 'variables' || scope === 'this';
1270
+ }
1271
+ }
1272
+ // PHP `$this->prop` receiver — the property's declaration lives outside the
1273
+ // calling method (a promoted constructor parameter `private readonly Foo $prop`,
1274
+ // a typed property `private Foo $prop;`, or a classic constructor parameter
1275
+ // `Foo $prop` assigned in __construct). Strip the prefix and widen the scan to
1276
+ // the whole file (the constructor may sit below the calling method), but —
1277
+ // unlike CFML's scopes above — switch to PROPERTY-shaped patterns: a plain
1278
+ // `$prop` local or parameter lives in a different namespace than `$this->prop`
1279
+ // and can never shadow it, so the generic local patterns would type the
1280
+ // property from unrelated same-named variables in other methods (a wrong
1281
+ // 0.9-confidence edge, not a missing one).
1282
+ let phpProperty = false;
1283
+ if (ref.language === 'php') {
1284
+ const scoped = receiverName.match(/^this->(.+)$/);
1285
+ if (scoped) {
1286
+ scanReceiver = scoped[1];
1287
+ componentScoped = true;
1288
+ phpProperty = true;
1289
+ }
1290
+ }
1291
+ const escapedReceiver = scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1292
+ const patterns = phpProperty
1293
+ ? phpPropertyTypePatterns(escapedReceiver)
1294
+ : localReceiverTypePatterns(ref.language, escapedReceiver);
1078
1295
  if (patterns.length === 0)
1079
1296
  return null;
1080
- const source = context.readFile(ref.filePath);
1081
- if (!source)
1297
+ // Split through the context's per-file lines cache when available: this runs
1298
+ // for EVERY `receiver.method()` ref, and re-splitting the whole file per ref
1299
+ // was ~20% of total index CPU on Java-heavy repos (#1122).
1300
+ const lines = context.getFileLines
1301
+ ? context.getFileLines(ref.filePath)
1302
+ : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
1303
+ if (!lines || lines.length === 0)
1082
1304
  return null;
1083
- const lines = source.split(/\r?\n/);
1084
1305
  const callIdx = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
1085
- const startIdx = Math.max(0, enclosingScopeStartLine(ref, context) - 1);
1086
- // Nearest declaration wins: scan backward from the call to the scope start.
1087
- for (let i = callIdx; i >= startIdx; i--) {
1306
+ const startIdx = componentScoped
1307
+ ? 0
1308
+ : Math.max(0, enclosingScopeStartLine(ref, context) - 1);
1309
+ const matchLine = (i) => {
1088
1310
  const line = lines[i];
1089
1311
  if (!line)
1090
- continue;
1312
+ return null;
1313
+ // A generated/minified line (one multi-KB statement) is not something a
1314
+ // human-written local declaration lives on, and regexing it per ref is
1315
+ // pure waste — skip it rather than scan it.
1316
+ if (line.length > 10_000)
1317
+ return null;
1091
1318
  for (const re of patterns) {
1092
1319
  const m = line.match(re);
1093
1320
  if (m && m[1]) {
@@ -1096,6 +1323,163 @@ function inferLocalReceiverType(receiverName, ref, context) {
1096
1323
  return type;
1097
1324
  }
1098
1325
  }
1326
+ return null;
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
+ }
1375
+ // Nearest declaration wins: scan backward from the call to the scope start.
1376
+ for (let i = callIdx; i >= startIdx; i--) {
1377
+ const type = matchLine(i);
1378
+ if (type)
1379
+ return type;
1380
+ }
1381
+ // A component-scoped field's declaration is position-independent — the
1382
+ // `variables.svc = new X()` pseudoconstructor assignment or `property`
1383
+ // declaration may sit BELOW the calling function in the file — so when the
1384
+ // backward pass finds nothing, sweep the remainder of the file too.
1385
+ if (componentScoped) {
1386
+ for (let i = callIdx + 1; i < lines.length; i++) {
1387
+ const type = matchLine(i);
1388
+ if (type)
1389
+ return type;
1390
+ }
1391
+ }
1392
+ // A PHP property with no statically-typed declaration (classic pre-7.4
1393
+ // style) may still be typed by what gets ASSIGNED to it — follow the
1394
+ // `$this->prop = $var` assignment to the assigned variable's own typed
1395
+ // declaration (a classic or multi-line constructor parameter, or a typed
1396
+ // setter's parameter).
1397
+ if (phpProperty) {
1398
+ return inferPhpAssignedPropertyType(escapedReceiver, lines, callIdx);
1399
+ }
1400
+ return null;
1401
+ }
1402
+ /**
1403
+ * Patterns that recover a PHP class property's declared type for a
1404
+ * `$this->prop` receiver. Deliberately NOT localReceiverTypePatterns: only
1405
+ * property-shaped declarations qualify —
1406
+ * 1. a modifier-prefixed typed declaration, which covers both a typed
1407
+ * property (`private ?Foo $prop;`) and a promoted constructor parameter
1408
+ * (`private readonly Foo $prop`), and
1409
+ * 2. the pseudoconstructor assignment (`$this->prop = new Foo(...)`).
1410
+ * A bare `X $prop` parameter or `$prop = new X()` local elsewhere in the
1411
+ * file must NOT match: those variables can never alias `$this->prop`.
1412
+ * Union-typed properties (`Foo|Bar $prop`) yield no match and thus no edge —
1413
+ * silent beats wrong. The classic untyped-property-assigned-in-constructor
1414
+ * shape is handled by inferPhpAssignedPropertyType instead.
1415
+ */
1416
+ function phpPropertyTypePatterns(r) {
1417
+ return memoPatterns(`php-prop|${r}`, () => buildPhpPropertyTypePatterns(r));
1418
+ }
1419
+ function buildPhpPropertyTypePatterns(r) {
1420
+ return [
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)
1422
+ new RegExp(`\\$this->${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $this->prop = new Foo()
1423
+ ];
1424
+ }
1425
+ /**
1426
+ * Second-chance typing for a PHP `$this->prop` receiver whose property
1427
+ * declaration carries no static type (classic pre-7.4 style): find the
1428
+ * `$this->prop = $var` assignment, then recover `$var`'s type from its own
1429
+ * declaration WITHIN the assignment's function — the constructor's (possibly
1430
+ * multi-line) parameter list, a typed setter's parameter, or a `= new X()`
1431
+ * local. The backward scan stops at the enclosing `function` line (checked
1432
+ * for a match first — a single-line `__construct(Foo $var) { ... }` carries
1433
+ * the typed parameter itself), so a same-named variable in another method
1434
+ * can never type the property.
1435
+ */
1436
+ function inferPhpAssignedPropertyType(escapedProp, lines, callIdx) {
1437
+ const assignRe = new RegExp(`\\$this->${escapedProp}\\b\\s*=\\s*\\$(\\w+)\\b`);
1438
+ const assignAt = (i) => {
1439
+ const line = lines[i];
1440
+ if (!line || line.length > 10_000)
1441
+ return null;
1442
+ return line.match(assignRe);
1443
+ };
1444
+ // The assignment is position-independent relative to the call — nearest-
1445
+ // backward first, then sweep forward, same order as the componentScoped scan.
1446
+ let assignIdx = -1;
1447
+ let varName = null;
1448
+ for (let i = callIdx; i >= 0; i--) {
1449
+ const m = assignAt(i);
1450
+ if (m) {
1451
+ assignIdx = i;
1452
+ varName = m[1];
1453
+ break;
1454
+ }
1455
+ }
1456
+ if (varName === null) {
1457
+ for (let i = callIdx + 1; i < lines.length; i++) {
1458
+ const m = assignAt(i);
1459
+ if (m) {
1460
+ assignIdx = i;
1461
+ varName = m[1];
1462
+ break;
1463
+ }
1464
+ }
1465
+ }
1466
+ if (varName === null)
1467
+ return null;
1468
+ const varPatterns = localReceiverTypePatterns('php', varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1469
+ for (let i = assignIdx; i >= 0; i--) {
1470
+ const line = lines[i];
1471
+ if (line && line.length <= 10_000) {
1472
+ for (const re of varPatterns) {
1473
+ const m = line.match(re);
1474
+ if (m && m[1]) {
1475
+ const type = normalizeInferredTypeName(m[1]);
1476
+ if (type)
1477
+ return type;
1478
+ }
1479
+ }
1480
+ }
1481
+ if (line && /\bfunction\b/.test(line))
1482
+ break;
1099
1483
  }
1100
1484
  return null;
1101
1485
  }
@@ -1112,7 +1496,17 @@ function matchMethodCall(ref, context) {
1112
1496
  // (with its existing single-candidate / receiver-overlap guards). Without this
1113
1497
  // a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
1114
1498
  // `Guard.Against.X()`) matched no pattern and never resolved.
1115
- 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);
1116
1510
  const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
1117
1511
  // Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
1118
1512
  // Recognize these receiver/method separators so local-variable receiver-type
@@ -1124,6 +1518,24 @@ function matchMethodCall(ref, context) {
1124
1518
  const rDollarMatch = ref.language === 'r'
1125
1519
  ? ref.referenceName.match(/^([\w.]+)\$(\w+)$/)
1126
1520
  : null;
1521
+ // PHP property receiver: `$this->prop->method()` reaches the resolver as
1522
+ // `this->prop.method` (the extractor records the receiver's raw text with the
1523
+ // leading `$` stripped). Resolve it EXCLUSIVELY through declared-type
1524
+ // inference + resolveMethodOnType validation — the name-similarity strategies
1525
+ // below must never see this shape, so a property whose type can't be
1526
+ // recovered stays unlinked rather than guessed (a wrong inference produces no
1527
+ // edge rather than a wrong one). Deeper chains (`this->a->b.method`) don't
1528
+ // match the single-property pattern and stay unlinked, same as before.
1529
+ const phpThisPropMatch = ref.language === 'php'
1530
+ ? ref.referenceName.match(/^(this->\w+)\.(\w+)$/)
1531
+ : null;
1532
+ if (phpThisPropMatch) {
1533
+ const [, receiver, phpMethodName] = phpThisPropMatch;
1534
+ const inferredType = inferLocalReceiverType(receiver, ref, context);
1535
+ if (!inferredType)
1536
+ return null;
1537
+ return resolveMethodOnType(inferredType, phpMethodName, ref, context, 0.9, 'instance-method', importedFqnOf(inferredType, ref, context));
1538
+ }
1127
1539
  const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
1128
1540
  if (!match) {
1129
1541
  return null;
@@ -1138,9 +1550,9 @@ function matchMethodCall(ref, context) {
1138
1550
  // shared source-based inferrer. resolveMethodOnType validates the method
1139
1551
  // exists on the inferred type, so a mis-inference produces no edge.
1140
1552
  if (inferableReceiver) {
1141
- const inferredType = ref.language === 'cpp'
1553
+ const inferredType = nmTimedT('mc-infer', ref, () => ref.language === 'cpp'
1142
1554
  ? inferCppReceiverType(objectOrClass, ref, context)
1143
- : inferLocalReceiverType(objectOrClass, ref, context);
1555
+ : inferLocalReceiverType(objectOrClass, ref, context));
1144
1556
  if (inferredType) {
1145
1557
  // Java/Kotlin: when two classes share the simple name, the file's import
1146
1558
  // pins WHICH one (#314). Other languages disambiguate by call-site file.
@@ -1149,12 +1561,26 @@ function matchMethodCall(ref, context) {
1149
1561
  .getImportMappings(ref.filePath, ref.language)
1150
1562
  .find((i) => i.localName === inferredType)?.source
1151
1563
  : undefined;
1152
- 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));
1153
1565
  if (typedMatch) {
1154
1566
  return typedMatch;
1155
1567
  }
1156
1568
  }
1157
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
+ }
1158
1584
  // Java/Kotlin: receiver may be a field whose name doesn't match the type by
1159
1585
  // Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
1160
1586
  // the field in the enclosing class to get its declared type, then resolve
@@ -1168,7 +1594,7 @@ function matchMethodCall(ref, context) {
1168
1594
  // imported FQN so resolveMethodOnType can disambiguate (#314).
1169
1595
  const imports = context.getImportMappings(ref.filePath, ref.language);
1170
1596
  const importedFqn = imports.find((i) => i.localName === inferredType)?.source;
1171
- 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));
1172
1598
  if (typedMatch) {
1173
1599
  return typedMatch;
1174
1600
  }
@@ -1179,33 +1605,23 @@ function matchMethodCall(ref, context) {
1179
1605
  // with a `Logger` in both `a/` and `b/`), try the class in the call site's
1180
1606
  // own file first — otherwise the first-indexed class wins and a call in `b/`
1181
1607
  // resolves to `a/`'s method (#1079).
1182
- const classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
1183
- for (const classNode of classCandidates) {
1184
- if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
1185
- // Skip cross-language class matches
1186
- if (classNode.language !== ref.language)
1187
- continue;
1188
- const nodesInFile = context.getNodesInFile(classNode.filePath);
1189
- const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
1190
- n.name === methodName &&
1191
- n.qualifiedName.includes(classNode.name));
1192
- if (methodNode) {
1193
- return {
1194
- original: ref,
1195
- targetNodeId: methodNode.id,
1196
- confidence: 0.85,
1197
- resolvedBy: 'qualified-name',
1198
- };
1199
- }
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);
1200
1619
  }
1201
- }
1202
- // Strategy 2: Instance variable receiver - try capitalized form to find class
1203
- // e.g., "permissionEngine" → look for classes containing "PermissionEngine"
1204
- const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
1205
- if (capitalizedReceiver !== objectOrClass) {
1206
- const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
1207
- for (const classNode of fuzzyClassCandidates) {
1208
- 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')) {
1209
1625
  // Skip cross-language class matches
1210
1626
  if (classNode.language !== ref.language)
1211
1627
  continue;
@@ -1217,67 +1633,192 @@ function matchMethodCall(ref, context) {
1217
1633
  return {
1218
1634
  original: ref,
1219
1635
  targetNodeId: methodNode.id,
1220
- confidence: 0.8,
1221
- resolvedBy: 'instance-method',
1636
+ confidence: 0.85,
1637
+ resolvedBy: 'qualified-name',
1222
1638
  };
1223
1639
  }
1224
1640
  }
1225
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;
1226
1677
  }
1227
1678
  // Strategy 3: Find methods by name across the codebase, match by receiver
1228
1679
  // name similarity with the containing class. Handles abbreviated variable
1229
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
+ }
1230
1686
  if (methodName) {
1231
- const methodCandidates = context.getNodesByName(methodName);
1232
- // Ubiquitous-method ceiling (#999): a method name re-declared across a
1233
- // vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
1234
- // K candidates that receiver-word overlap can't reliably disambiguate —
1235
- // and filtering + scoring all K per call is the O(K²) cost that wedged
1236
- // "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
1237
- // (class-name match) already had their precise shot above.
1238
- if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
1239
- return null;
1240
- }
1241
- const methods = methodCandidates.filter((n) => n.kind === 'method' && n.name === methodName);
1242
- // Filter to same-language candidates first
1243
- const sameLanguageMethods = methods.filter(m => m.language === ref.language);
1244
- const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
1245
- // If only one same-language method with this name exists, use it
1246
- if (targetMethods.length === 1 && targetMethods[0].language === ref.language) {
1247
- return {
1248
- original: ref,
1249
- targetNodeId: targetMethods[0].id,
1250
- confidence: 0.7,
1251
- resolvedBy: 'instance-method',
1252
- };
1253
- }
1254
- // Multiple methods: score by receiver name word overlap with class name
1255
- if (targetMethods.length > 1) {
1256
- const receiverWords = splitCamelCase(objectOrClass);
1257
- let bestMatch;
1258
- let bestScore = 0;
1259
- // Same-file candidates first, so a score tie (`score > bestScore` keeps
1260
- // the first seen) resolves to the call site's own file rather than the
1261
- // first-indexed duplicate (#1079).
1262
- for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
1263
- const classWords = splitCamelCase(method.qualifiedName);
1264
- let score = receiverWords.filter(w => classWords.some(cw => cw.toLowerCase() === w.toLowerCase())).length;
1265
- // Bonus for same language
1266
- if (method.language === ref.language)
1267
- score += 1;
1268
- if (score > bestScore) {
1269
- bestScore = score;
1270
- bestMatch = method;
1271
- }
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;
1272
1697
  }
1273
- 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) {
1274
1704
  return {
1275
1705
  original: ref,
1276
- targetNodeId: bestMatch.id,
1277
- confidence: 0.65,
1706
+ targetNodeId: targetMethods[0].id,
1707
+ confidence: 0.7,
1278
1708
  resolvedBy: 'instance-method',
1279
1709
  };
1280
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;
1281
1822
  }
1282
1823
  }
1283
1824
  return null;
@@ -1438,6 +1979,51 @@ function matchFuzzy(ref, context) {
1438
1979
  /**
1439
1980
  * Match all strategies in order of confidence
1440
1981
  */
1982
+ /** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
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
+ }
1441
2027
  function matchReference(ref, context) {
1442
2028
  // Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
1443
2029
  // never the fuzzy/qualified fallthrough below (a wrong callback edge is
@@ -1445,21 +2031,72 @@ function matchReference(ref, context) {
1445
2031
  if (ref.referenceKind === 'function_ref') {
1446
2032
  return matchFunctionRef(ref, context);
1447
2033
  }
2034
+ // ArkTS chained UI attributes — emitted with a leading dot (`.titleStyle`,
2035
+ // `.width`) by the extractor — resolve ONLY to decorator-marked attribute
2036
+ // helpers: `@Extend`/`@Styles`/`@AnimatableExtend` functions (and global
2037
+ // `@Builder`s used attribute-position). Framework attributes (`.width`,
2038
+ // `.fontSize` — on nearly every UI line) match no such helper and stay
2039
+ // unresolved, NEVER falling through to bare-name matching: on a samples
2040
+ // monorepo that fallthrough manufactured 36k wrong edges, giving single
2041
+ // same-named properties thousands of false callers. Ambiguity rule matches
2042
+ // the rest of the file: several same-named helpers → prefer the call-site
2043
+ // file, still ambiguous → drop the ref rather than guess.
2044
+ if (ref.language === 'arkts' && ref.referenceName.startsWith('.')) {
2045
+ const base = ref.referenceName.slice(1);
2046
+ const candidates = context
2047
+ .getNodesByName(base)
2048
+ .filter((n) => n.language === 'arkts' &&
2049
+ n.kind === 'function' &&
2050
+ (n.decorators ?? []).some((d) => ARKUI_ATTRIBUTE_DECORATORS.has(d)));
2051
+ const chosen = candidates.length > 1 ? preferCallSiteFile(candidates, ref.filePath) : candidates;
2052
+ if (chosen.length !== 1)
2053
+ return null;
2054
+ return {
2055
+ original: ref,
2056
+ targetNodeId: chosen[0].id,
2057
+ confidence: 0.85,
2058
+ resolvedBy: 'exact-match',
2059
+ };
2060
+ }
2061
+ // Erlang `-behaviour(m)` refs target a MODULE. Letting them fall through to
2062
+ // bare-name matching grabs any same-named symbol — on emqx,
2063
+ // `-behaviour(supervisor)` resolved to a `-define(supervisor, …)` macro
2064
+ // constant in an unrelated app. Resolve only to the behaviour module's
2065
+ // namespace; an out-of-repo behaviour (OTP's gen_server/supervisor) stays
2066
+ // unresolved rather than guessed. The same module-only rule applies to every
2067
+ // ref an `.app`/`.app.src` resource file emits — its `{mod, …}` callback and
2068
+ // `{applications, …}` dependency names can only mean modules, and on emqx
2069
+ // the `ssl` OTP app otherwise resolved to a test helper FUNCTION named ssl.
2070
+ if (ref.language === 'erlang' &&
2071
+ (ref.referenceKind === 'implements' || /\.app(?:\.src)?$/i.test(ref.filePath))) {
2072
+ const modules = context
2073
+ .getNodesByName(ref.referenceName)
2074
+ .filter((n) => n.language === 'erlang' && n.kind === 'namespace');
2075
+ const chosen = preferCallSiteFile(modules, ref.filePath)[0];
2076
+ if (!chosen)
2077
+ return null;
2078
+ return {
2079
+ original: ref,
2080
+ targetNodeId: chosen.id,
2081
+ confidence: 0.9,
2082
+ resolvedBy: 'exact-match',
2083
+ };
2084
+ }
1448
2085
  // Try strategies in order of confidence
1449
2086
  let result;
1450
2087
  // 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)
1451
- result = matchByFilePath(ref, context);
2088
+ result = nmTimed('filePath', ref, () => matchByFilePath(ref, context));
1452
2089
  if (result)
1453
2090
  return result;
1454
2091
  // 1. Qualified name match (highest confidence)
1455
- result = matchByQualifiedName(ref, context);
2092
+ result = nmTimed('qualifiedName', ref, () => matchByQualifiedName(ref, context));
1456
2093
  if (result)
1457
2094
  return result;
1458
2095
  // 1b. C++ chained call whose receiver is another call — `Foo::instance().bar()`
1459
2096
  // encoded as `Foo::instance().bar` by the extractor (#645). Resolve the
1460
2097
  // receiver's type from what the inner call returns, then the method on it.
1461
2098
  if (ref.language === 'cpp' || ref.language === 'c') {
1462
- result = matchCppCallChain(ref, context);
2099
+ result = nmTimed('cppChain', ref, () => matchCppCallChain(ref, context));
1463
2100
  if (result)
1464
2101
  return result;
1465
2102
  }
@@ -1468,7 +2105,7 @@ function matchReference(ref, context) {
1468
2105
  // type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
1469
2106
  // concrete return type.
1470
2107
  if (ref.language === 'php' || ref.language === 'rust') {
1471
- result = matchScopedCallChain(ref, context);
2108
+ result = nmTimed('scopedChain', ref, () => matchScopedCallChain(ref, context));
1472
2109
  if (result)
1473
2110
  return result;
1474
2111
  }
@@ -1488,20 +2125,46 @@ function matchReference(ref, context) {
1488
2125
  ref.language === 'dart' ||
1489
2126
  ref.language === 'objc' ||
1490
2127
  ref.language === 'pascal') {
1491
- result = matchDottedCallChain(ref, context);
2128
+ result = nmTimed('dottedChain', ref, () => matchDottedCallChain(ref, context));
1492
2129
  if (result)
1493
2130
  return result;
1494
2131
  }
1495
2132
  // 2. Method call pattern
1496
- result = matchMethodCall(ref, context);
2133
+ result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
1497
2134
  if (result)
1498
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
+ }
1499
2162
  // 3. Exact name match
1500
- result = matchByExactName(ref, context);
2163
+ result = nmTimed('exactName', ref, () => matchByExactName(ref, context));
1501
2164
  if (result)
1502
2165
  return result;
1503
2166
  // 4. Fuzzy match (lowest confidence)
1504
- result = matchFuzzy(ref, context);
2167
+ result = nmTimed('fuzzy', ref, () => matchFuzzy(ref, context));
1505
2168
  if (result)
1506
2169
  return result;
1507
2170
  return null;