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
package/dist/mcp/tools.js CHANGED
@@ -6,12 +6,16 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.NotIndexedError = void 0;
9
+ exports.normalizeQuerySpelling = normalizeQuerySpelling;
9
10
  exports.getExploreBudget = getExploreBudget;
10
11
  exports.getExploreOutputBudget = getExploreOutputBudget;
12
+ exports.tightenExploreBudgetForQuery = tightenExploreBudgetForQuery;
11
13
  exports.formatStaleBanner = formatStaleBanner;
12
14
  exports.formatStaleFooter = formatStaleFooter;
13
15
  exports.formatDegradedBanner = formatDegradedBanner;
14
16
  exports.getStaticTools = getStaticTools;
17
+ const query_pool_1 = require("./query-pool");
18
+ const memory_budget_1 = require("./memory-budget");
15
19
  const directory_1 = require("../directory");
16
20
  // Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
17
21
  // helper in engine.ts. ToolHandler must load to answer tools/list (static
@@ -36,6 +40,7 @@ const VIEWTREE_STRUCTURE_VIAS = new Set([
36
40
  'builder',
37
41
  'builder-param',
38
42
  ]);
43
+ const sqlite_adapter_1 = require("../db/sqlite-adapter");
39
44
  /**
40
45
  * An expected, recoverable "homegraph can't serve this" condition — most
41
46
  * importantly a project with no index. The dispatch catch converts these to
@@ -96,6 +101,25 @@ function lastQualifierPart(symbol) {
96
101
  const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
97
102
  return parts[parts.length - 1] ?? symbol;
98
103
  }
104
+ /**
105
+ * Normalize Erlang-native symbol spellings in an explore query into the shapes
106
+ * the rest of the pipeline already understands. Agents working Erlang code
107
+ * name symbols the way the language spells them — `mod:fn/3`, `init/2` — and
108
+ * those tokens previously died in both consumers: the flow-builder's token
109
+ * filter rejects `:` and `/arity` outright, and the search-side field parser
110
+ * eats `mod:fn` as an unknown `field:value`.
111
+ *
112
+ * - `fn/3` → `fn` (arity tail after an identifier)
113
+ * - `mod:fn` → `mod.fn` (exactly one colon between identifiers)
114
+ *
115
+ * Safe cross-language: Lua's `t:m` spelling maps to the same `t.m` its
116
+ * qualified names use.
117
+ */
118
+ function normalizeQuerySpelling(query) {
119
+ return query
120
+ .replace(/\b([A-Za-z_][\w@]*)\/(\d{1,3})(?=$|[\s,()[\]/])/g, '$1')
121
+ .replace(/(^|[\s,()[\]])(?!(?:kind|lang|language|path|name):)([a-z_][\w@]*):([A-Za-z_][\w@]*)(?=$|[\s,()[\]])/g, '$1$2.$3');
122
+ }
99
123
  /**
100
124
  * Calculate the recommended number of homegraph_explore calls based on project size.
101
125
  * Larger codebases need more exploration calls to cover their surface area,
@@ -211,6 +235,46 @@ function getExploreOutputBudget(fileCount) {
211
235
  excludeLowValueFiles: false,
212
236
  };
213
237
  }
238
+ /**
239
+ * Shrink explore ceilings for local-detail / no-flow named-symbol questions.
240
+ * Large repos otherwise dump ~24K related source that the agent then still
241
+ * greps/reads — the main token regression vs without-homegraph.
242
+ */
243
+ function tightenExploreBudgetForQuery(budget, query, opts) {
244
+ const hasFlow = opts?.hasFlowPath === true;
245
+ if (hasFlow && ((0, query_utils_1.queryAsMechanismSurvey)(query) || (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query))) {
246
+ return budget;
247
+ }
248
+ const local = (0, query_utils_1.queryAsLocalSymbolDetail)(query);
249
+ const compact = (0, query_utils_1.shouldUseCompactExploreBudget)(query);
250
+ if (!local && !compact && hasFlow)
251
+ return budget;
252
+ if (!local && !compact && hasFlow === false) {
253
+ // Generic no-flow explore: still trim meta + file count a bit.
254
+ return {
255
+ ...budget,
256
+ maxOutputChars: Math.min(budget.maxOutputChars, 14000),
257
+ defaultMaxFiles: Math.min(budget.defaultMaxFiles, 3),
258
+ maxCharsPerFile: Math.min(budget.maxCharsPerFile, 5000),
259
+ includeRelationships: false,
260
+ includeAdditionalFiles: false,
261
+ includeCompletenessSignal: false,
262
+ includeBudgetNote: false,
263
+ };
264
+ }
265
+ if (!local && !compact)
266
+ return budget;
267
+ return {
268
+ ...budget,
269
+ maxOutputChars: Math.min(budget.maxOutputChars, local ? 9000 : 12000),
270
+ defaultMaxFiles: Math.min(budget.defaultMaxFiles, local ? 2 : 3),
271
+ maxCharsPerFile: Math.min(budget.maxCharsPerFile, local ? 4000 : 5000),
272
+ includeRelationships: false,
273
+ includeAdditionalFiles: false,
274
+ includeCompletenessSignal: false,
275
+ includeBudgetNote: false,
276
+ };
277
+ }
214
278
  /**
215
279
  * Whether `homegraph_explore` should prefix source lines with their line
216
280
  * numbers (cat -n style: `<num>\t<code>`).
@@ -329,18 +393,26 @@ function fileSectionHeader(filePath, suffix) {
329
393
  * The agent uses this to fall back to Read for those specific files
330
394
  * without waiting for the debounced sync (issue #403).
331
395
  */
332
- function formatStaleBanner(stale) {
396
+ function formatStaleBanner(stale, opts) {
333
397
  const now = Date.now();
334
398
  const lines = stale.map((p) => {
335
399
  const ageMs = Math.max(0, now - p.lastSeenMs);
336
400
  const label = p.indexing ? 'indexing in progress' : 'pending sync';
337
401
  return ` - ${p.path} (edited ${ageMs}ms ago, ${label})`;
338
402
  });
403
+ // On large indexes catch-up is skipped for RSS — "Read them" teaches the agent
404
+ // to abandon homegraph after every save and burn tokens on whole-file Reads.
405
+ const guidance = opts?.catchUpDeferred
406
+ ? 'Callers / defs / explore locations above are still usable for structural answers. ' +
407
+ 'Read a listed file only if you need byte-exact content that may have changed mid-session — ' +
408
+ 'do not re-read the whole file just because of this notice.'
409
+ : 'For accurate content of those specific files, Read them directly. ' +
410
+ 'The rest of this response is fresh.';
339
411
  return ('⚠️ Some files referenced below were edited since the last index sync — ' +
340
412
  'their homegraph entries may be stale:\n' +
341
413
  lines.join('\n') +
342
- '\nFor accurate content of those specific files, Read them directly. ' +
343
- 'The rest of this response is fresh.');
414
+ '\n' +
415
+ guidance);
344
416
  }
345
417
  /**
346
418
  * Compact footer listing pending files that are NOT referenced in this
@@ -409,7 +481,7 @@ const READ_ONLY_ANNOTATIONS = {
409
481
  exports.tools = [
410
482
  {
411
483
  name: 'homegraph_search',
412
- description: 'Quick symbol search by name. Returns locations only (no code). Use homegraph_explore instead to get the actual source / understand an area in one call.',
484
+ description: 'LAST RESORT spelling lookup locations only, no source. Prefer homegraph_explore whenever the question already names a symbol/file/@kit. If you do call search with a bare name, HomeGraph may answer with a compact explore result instead of locations.',
413
485
  inputSchema: {
414
486
  type: 'object',
415
487
  properties: {
@@ -435,7 +507,7 @@ exports.tools = [
435
507
  },
436
508
  {
437
509
  name: 'homegraph_callers',
438
- description: 'List functions that call <symbol>. For the full flow, use homegraph_explore.',
510
+ description: 'Compact caller list for a NAMED in-repo symbol (no bodies). Use after you know the exact name. For full flows use homegraph_explore. DO NOT call for SDK catalogs, one-function semantics, or "what if X fails" hypothetics — Read that function instead. Prefer one explore over parallel callers+callees+node.',
439
511
  inputSchema: {
440
512
  type: 'object',
441
513
  properties: {
@@ -460,7 +532,7 @@ exports.tools = [
460
532
  },
461
533
  {
462
534
  name: 'homegraph_callees',
463
- description: 'List functions that <symbol> calls. For the full flow, use homegraph_explore.',
535
+ description: 'Compact callee list for a NAMED in-repo symbol (no bodies). For full flows use homegraph_explore. DO NOT use for out-of-repo SDK internals or counterfactual analysis. Prefer one explore over parallel node+callers+callees.',
464
536
  inputSchema: {
465
537
  type: 'object',
466
538
  properties: {
@@ -485,7 +557,7 @@ exports.tools = [
485
557
  },
486
558
  {
487
559
  name: 'homegraph_impact',
488
- description: 'List symbols affected by changing <symbol>. Use before a refactor.',
560
+ description: 'Blast radius for a NAMED in-repo symbol before a refactor. Not for SDK docs, permission judgments, or hypothetical failure effects — those need Read/Grep, not impact.',
489
561
  inputSchema: {
490
562
  type: 'object',
491
563
  properties: {
@@ -510,7 +582,7 @@ exports.tools = [
510
582
  },
511
583
  {
512
584
  name: 'homegraph_node',
513
- description: 'Two modes. (1) READ A FILE use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file\'s current on-disk source with line numbers, exactly the shape Read gives you (`<n>\\t<line>`, safe to Edit from), narrowable with `offset`/`limit` just like Read — PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL you can name its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition\'s body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use homegraph_explore for several related symbols or the full flow.',
585
+ description: 'Depth on ONE known in-repo symbol or indexed file not a survey tool. (1) FILE: pass `file` alone bounded line-numbered source + dependents. (2) SYMBOL: body (includeCode) + short trail; overloads return every body. USE after explore named the symbol and you still need one body. DO NOT call repeatedly to crawl a feature (prefer one explore). DO NOT call for: @kit/SDK catalogs, broad "how does X work", or after explore already returned that symbol\'s source. Treat returned source as already Read do not grep/read the same path.',
514
586
  inputSchema: {
515
587
  type: 'object',
516
588
  properties: {
@@ -552,13 +624,13 @@ exports.tools = [
552
624
  },
553
625
  {
554
626
  name: 'homegraph_explore',
555
- description: 'PRIMARY TOOL call FIRST for almost any question OR before an edit: how does X work, architecture, a bug, where/what is X, surveying an area, or the symbols you are about to change. Returns the verbatim source of the relevant symbols grouped by file in ONE capped call (Read-equivalent — treat the shown source as already Read; do NOT re-open those files), plus the call path among them. Query can be a natural-language question OR a bag of symbol/file names. Usually the ONLY call you need more accurate context, in far fewer tokens and round-trips than a search/Read/Grep loop.',
627
+ description: 'PRIMARY tool for THIS REPO\'s symbol graph (call paths + often line-numbered source). CALL when you need in-repo structure: how a named feature/component is wired, A→B path, callers/callees, Type.member who uses it, click/handler flow, in-repo @kit import usages put concrete symbol/file/@kit names in query; skip search. If the question does not need that graph, do not call. One explore; answer from returned Source + trail; treat as already Read; do not re-grep/search the same names. Busy/partial retry same explore once.',
556
628
  inputSchema: {
557
629
  type: 'object',
558
630
  properties: {
559
631
  query: {
560
632
  type: 'string',
561
- description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). For a flow question, name the symbols spanning the flow (e.g. "mutateElement renderScene"). A natural-language question works too no prior homegraph_search needed.',
633
+ description: 'In-repo symbols, file basenames, or @kit names for USAGE questions (import/call sites), not for asking the kit\'s official full API list. For flows, name both endpoints. Prefer concrete names from the question.',
562
634
  },
563
635
  maxFiles: {
564
636
  type: 'number',
@@ -584,7 +656,7 @@ exports.tools = [
584
656
  },
585
657
  {
586
658
  name: 'homegraph_files',
587
- description: 'Indexed file tree with language + symbol counts. Faster than Glob for project layout.',
659
+ description: 'Indexed directory tree (paths and symbol counts only — NO source code). Do NOT use to answer where/what/how code questions; use homegraph_explore. Only for coarse folder layout when explore cannot help.',
588
660
  inputSchema: {
589
661
  type: 'object',
590
662
  properties: {
@@ -642,10 +714,6 @@ exports.tools = [
642
714
  description: 'Whether to include full code diffs per commit (default: true).',
643
715
  default: true,
644
716
  },
645
- dbPath: {
646
- type: 'string',
647
- description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
648
- },
649
717
  },
650
718
  required: ['query'],
651
719
  },
@@ -668,10 +736,6 @@ exports.tools = [
668
736
  type: 'string',
669
737
  description: 'Path to the repository root. Defaults to the current working directory.',
670
738
  },
671
- dbPath: {
672
- type: 'string',
673
- description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
674
- },
675
739
  },
676
740
  required: ['filePath'],
677
741
  },
@@ -710,10 +774,6 @@ exports.tools = [
710
774
  description: 'Maximum number of matching Specs to return (default: 10).',
711
775
  default: 10,
712
776
  },
713
- dbPath: {
714
- type: 'string',
715
- description: 'Explicit path to the Commit4Spec database. Overrides repoPath-based resolution.',
716
- },
717
777
  },
718
778
  required: ['symbol'],
719
779
  },
@@ -1064,6 +1124,7 @@ class ToolHandler {
1064
1124
  freshen(cg) {
1065
1125
  try {
1066
1126
  if (cg.reopenIfReplaced()) {
1127
+ (0, query_cache_1.getMcpQueryCacheIndex)(cg.getProjectRoot()).reset();
1067
1128
  process.stderr.write('[HomeGraph MCP] The index was replaced on disk (e.g. a git worktree ' +
1068
1129
  'recreated at the same path); reopened the live database in place.\n');
1069
1130
  }
@@ -1270,7 +1331,18 @@ class ToolHandler {
1270
1331
  }
1271
1332
  let banner = '';
1272
1333
  if (inResponse.length > 0) {
1273
- banner = formatStaleBanner(inResponse);
1334
+ let dbPath = null;
1335
+ try {
1336
+ // Large indexes skip catch-up — soft banner so agents don't abandon HG for Read.
1337
+ const root = cg.getProjectRoot();
1338
+ dbPath = (0, path_1.resolve)(root, '.homegraph', 'homegraph.db');
1339
+ }
1340
+ catch {
1341
+ dbPath = null;
1342
+ }
1343
+ banner = formatStaleBanner(inResponse, {
1344
+ catchUpDeferred: (0, memory_budget_1.shouldSkipCatchUpSync)(dbPath),
1345
+ });
1274
1346
  }
1275
1347
  let footer = '';
1276
1348
  if (elsewhere.length > 0) {
@@ -1299,6 +1371,11 @@ class ToolHandler {
1299
1371
  this.catchUpGate = null;
1300
1372
  await this.awaitCatchUpGate(gate);
1301
1373
  }
1374
+ // Hard process RSS ceiling — any tool/path that already pushed us over must
1375
+ // stop with success-shaped Partial (never OOM / multi-GB growth).
1376
+ if ((0, memory_budget_1.isOverRssBudget)()) {
1377
+ return (0, memory_budget_1.rssBudgetPartialResult)(toolName);
1378
+ }
1302
1379
  // Honor the optional tool allowlist (HOMEGRAPH_MCP_TOOLS): a trimmed
1303
1380
  // surface rejects ablated tools defensively even if a client cached them.
1304
1381
  if (!this.isToolAllowed(toolName)) {
@@ -1328,11 +1405,13 @@ class ToolHandler {
1328
1405
  const cacheEnabled = (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
1329
1406
  let cacheKey;
1330
1407
  let cacheQueries;
1408
+ let cacheIndex;
1331
1409
  if (cacheEnabled) {
1332
1410
  try {
1333
1411
  const cacheCg = this.getHomeGraph(projectPath);
1334
1412
  cacheQueries = cacheCg.getQueryBuilder();
1335
- (0, query_cache_1.ensureMcpQueryCacheValid)(cacheQueries, () => cacheCg.getLastIndexedAt());
1413
+ cacheIndex = (0, query_cache_1.getMcpQueryCacheIndex)(cacheCg.getProjectRoot());
1414
+ cacheIndex.ensureValid(cacheQueries, () => cacheCg.getLastIndexedAt());
1336
1415
  let fileCount;
1337
1416
  try {
1338
1417
  fileCount = cacheCg.getStats().fileCount;
@@ -1341,7 +1420,7 @@ class ToolHandler {
1341
1420
  fileCount = undefined;
1342
1421
  }
1343
1422
  cacheKey = (0, query_cache_1.buildMcpQueryCacheKey)(toolName, args, fileCount);
1344
- const cached = (0, query_cache_1.getMcpQueryCacheEntry)(cacheQueries, cacheKey);
1423
+ const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
1345
1424
  if (cached) {
1346
1425
  const withWorktree = this.withWorktreeNotice(cached, projectPath);
1347
1426
  return this.withStalenessNotice(withWorktree, projectPath);
@@ -1359,20 +1438,43 @@ class ToolHandler {
1359
1438
  if (toolName === 'homegraph_status') {
1360
1439
  return await this.handleStatus(args);
1361
1440
  }
1362
- // Read tools: off-load the CPU-heavy dispatch to the worker pool when one
1363
- // is attached and healthy (daemon mode), so the daemon's single event loop
1364
- // stays free for the MCP transport under concurrent load otherwise N
1365
- // concurrent explores serialize AND starve the transport until the whole
1366
- // batch drains (clients then time out). With no pool (direct mode) or a
1367
- // degraded one, dispatch runs in-process exactly as before. Either way the
1368
- // result flows through the cross-cutting notices worktree-index mismatch
1369
- // (#155) and per-file staleness (#403) — which need the watched MAIN
1370
- // instance and so are always applied here, never in the worker.
1371
- const result = (this.queryPool && this.queryPool.healthy)
1372
- ? await this.queryPool.run(toolName, args)
1373
- : await this.executeReadTool(toolName, args);
1374
- if (cacheEnabled && cacheKey && cacheQueries && !result.isError) {
1375
- (0, query_cache_1.setMcpQueryCacheEntry)(cacheQueries, cacheKey, toolName, result);
1441
+ // Every read tool races a deadline MCP client ~60s hard timeout and
1442
+ // Named-member / local-compact questions finish in tens of ms on the warm
1443
+ // main connection. Serving them here before the query-pool offload
1444
+ // avoids cold-worker / wedged-daemon paths that otherwise surface as empty
1445
+ // MCP client `-32001` (the handler itself is fine; the transport times out).
1446
+ if (toolName === 'homegraph_explore' || toolName === 'homegraph_search') {
1447
+ const q = typeof args.query === 'string' ? args.query : '';
1448
+ if (q) {
1449
+ try {
1450
+ const cgFast = this.getHomeGraph(projectPath);
1451
+ const rootFast = cgFast.getProjectRoot();
1452
+ const fast = (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
1453
+ ? this.tryFastInventoryExplore(cgFast, q, rootFast)
1454
+ : null)
1455
+ ?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast)
1456
+ ?? (toolName === 'homegraph_explore'
1457
+ ? this.tryLightMechanismExplore(cgFast, q, rootFast)
1458
+ : null);
1459
+ if (fast) {
1460
+ if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !fast.isError) {
1461
+ cacheIndex.setEntry(cacheQueries, cacheKey, toolName, fast);
1462
+ }
1463
+ const withWorktree = this.withWorktreeNotice(fast, projectPath);
1464
+ return this.withStalenessNotice(withWorktree, projectPath);
1465
+ }
1466
+ }
1467
+ catch {
1468
+ // Not indexed / path issue — fall through to normal dispatch.
1469
+ }
1470
+ }
1471
+ }
1472
+ // prefers the query pool so sync SQLite/CPU cannot freeze the transport
1473
+ // (a frozen main loop prevents setTimeout deadlines from firing → empty
1474
+ // `-32001`). Fast-path surveys run inside the worker via executeReadTool.
1475
+ const result = await this.runReadToolWithDeadline(toolName, args);
1476
+ if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !result.isError) {
1477
+ cacheIndex.setEntry(cacheQueries, cacheKey, toolName, result);
1376
1478
  }
1377
1479
  const withWorktree = this.withWorktreeNotice(result, projectPath);
1378
1480
  return this.withStalenessNotice(withWorktree, projectPath);
@@ -1393,6 +1495,64 @@ class ToolHandler {
1393
1495
  'continue without homegraph for this task.');
1394
1496
  }
1395
1497
  }
1498
+ /**
1499
+ * Dispatch a read tool with a hard deadline under the typical ~60s MCP client
1500
+ * timeout.
1501
+ *
1502
+ * - **Heavy** tools (explore/impact) → query pool (keeps transport free).
1503
+ * - **Light** tools (search/node/callers/…) → warm main connection (pool cold
1504
+ * open of a large WAL index routinely outruns the client; search used to
1505
+ * finish in <1s on the already-open main DB).
1506
+ *
1507
+ * Soft/deadline replies are **static busy text only** — never FTS / explore
1508
+ * on the main thread. A previous path called `searchNodes` from the timeout
1509
+ * callback with the full natural-language query, freezing the event loop so
1510
+ * the success-shaped reply never flushed → empty client `-32001`.
1511
+ */
1512
+ async runReadToolWithDeadline(toolName, args) {
1513
+ const deadlineMs = (0, query_pool_1.resolveToolDeadlineMs)();
1514
+ const light = toolName === 'homegraph_search'
1515
+ || toolName === 'homegraph_node'
1516
+ || toolName === 'homegraph_callers'
1517
+ || toolName === 'homegraph_callees'
1518
+ || toolName === 'homegraph_files';
1519
+ const work = () => {
1520
+ if (!light && this.queryPool && this.queryPool.healthy) {
1521
+ return this.queryPool.run(toolName, args, {
1522
+ // Static Partial only — never DB/FTS on the soft-timeout callback
1523
+ // (any sync work here can freeze the MCP transport → empty -32001).
1524
+ onSoftTimeout: () => this.deadlineBusyResult(deadlineMs),
1525
+ });
1526
+ }
1527
+ return (async () => {
1528
+ await new Promise((r) => setImmediate(r));
1529
+ return this.executeReadTool(toolName, args);
1530
+ })();
1531
+ };
1532
+ let timer;
1533
+ const deadline = new Promise((resolve) => {
1534
+ // Keep this timer ref'd so a blocked sync stretch still flushes Partial
1535
+ // once the event loop runs again — do not call wrapPartialBusyResult here.
1536
+ timer = setTimeout(() => {
1537
+ resolve(this.deadlineBusyResult(deadlineMs));
1538
+ }, deadlineMs);
1539
+ });
1540
+ try {
1541
+ return await Promise.race([work(), deadline]);
1542
+ }
1543
+ finally {
1544
+ if (timer)
1545
+ clearTimeout(timer);
1546
+ }
1547
+ }
1548
+ /** Success-shaped busy note — no DB, never blocks the transport. */
1549
+ deadlineBusyResult(deadlineMs) {
1550
+ const secs = Math.max(1, Math.round(deadlineMs / 1000));
1551
+ return this.textResult(`⚠️ **Partial result** — HomeGraph hit its ${secs}s response deadline / is busy ` +
1552
+ `(MCP clients typically kill the call at ~60s with an empty timeout). ` +
1553
+ `This is NOT an error. Retry ONE \`homegraph_explore\` with concrete symbol/file names from the question — ` +
1554
+ `do not fire search+explore or node+callers+callees in parallel, and do not grep/read symbols you already named.`);
1555
+ }
1396
1556
  /**
1397
1557
  * Run a single read tool to completion and return its raw {@link ToolResult},
1398
1558
  * classifying expected failures the same way {@link execute}'s catch does so
@@ -1408,6 +1568,12 @@ class ToolHandler {
1408
1568
  */
1409
1569
  async executeReadTool(toolName, args) {
1410
1570
  try {
1571
+ // Compact inventory / one-symbol surveys — safe on the worker (keeps the
1572
+ // daemon main loop free). Never run these unprotected on the MCP transport
1573
+ // thread: they can block long enough for the client to emit empty `-32001`.
1574
+ const fastPath = this.tryFastPathResult(toolName, args);
1575
+ if (fastPath)
1576
+ return fastPath;
1411
1577
  return await this.dispatchTool(toolName, args);
1412
1578
  }
1413
1579
  catch (err) {
@@ -1443,14 +1609,31 @@ class ToolHandler {
1443
1609
  default: return this.errorResult(`Unknown tool: ${toolName}`);
1444
1610
  }
1445
1611
  }
1446
- /**
1447
- * Handle homegraph_search
1448
- */
1449
1612
  async handleSearch(args) {
1450
1613
  const query = this.validateString(args.query, 'query');
1451
1614
  if (typeof query !== 'string')
1452
1615
  return query;
1453
1616
  const cg = this.getHomeGraph(args.projectPath);
1617
+ // Explore redirect is best-effort — incomplete/faked graphs (or missing
1618
+ // getProjectRoot) must fall through to FTS search rather than error.
1619
+ try {
1620
+ const projectRoot = cg.getProjectRoot();
1621
+ if ((0, query_utils_1.queryShouldPreferExploreOverSearch)(query)) {
1622
+ const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
1623
+ ?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
1624
+ ?? this.tryLightMechanismExplore(cg, query, projectRoot);
1625
+ if (exploreRedirect)
1626
+ return exploreRedirect;
1627
+ }
1628
+ const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
1629
+ ?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
1630
+ ?? this.tryLightMechanismExplore(cg, query, projectRoot);
1631
+ if (exploreRedirect)
1632
+ return exploreRedirect;
1633
+ }
1634
+ catch {
1635
+ // Fall through to FTS search.
1636
+ }
1454
1637
  const rawKind = args.kind;
1455
1638
  // The schema enum says 'type' (what agents naturally reach for); the
1456
1639
  // NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
@@ -1474,7 +1657,9 @@ class ToolHandler {
1474
1657
  return aGen - bGen;
1475
1658
  });
1476
1659
  const formatted = this.formatSearchResults(ranked);
1477
- return this.textResult(this.truncateOutput(formatted));
1660
+ const steer = '\n\n> Locations only. For source / callers / how it works, call `homegraph_explore` ' +
1661
+ 'with these symbol names next — do not re-search or grep the same names.';
1662
+ return this.textResult(this.truncateOutput(formatted + steer));
1478
1663
  }
1479
1664
  /**
1480
1665
  * Group symbol matches into DISTINCT DEFINITIONS — one group per
@@ -1536,6 +1721,8 @@ class ToolHandler {
1536
1721
  const callers = [];
1537
1722
  const labels = new Map();
1538
1723
  for (const node of defNodes) {
1724
+ if ((0, memory_budget_1.isOverRssBudget)())
1725
+ break;
1539
1726
  for (const c of cg.getCallers(node.id)) {
1540
1727
  if (!seen.has(c.node.id)) {
1541
1728
  seen.add(c.node.id);
@@ -1544,6 +1731,8 @@ class ToolHandler {
1544
1731
  if (label)
1545
1732
  labels.set(c.node.id, label);
1546
1733
  }
1734
+ if (callers.length >= limit)
1735
+ return { callers, labels };
1547
1736
  }
1548
1737
  }
1549
1738
  return { callers, labels };
@@ -1557,7 +1746,9 @@ class ToolHandler {
1557
1746
  // A successful `file` narrowing makes the multi-symbol aggregation note
1558
1747
  // stale — suppress it.
1559
1748
  const note = fileFilter && !filteredOut ? '' : allMatches.note;
1560
- const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote;
1749
+ const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels)
1750
+ + '\n\n> Caller listing complete — answer from this list; no read/grep needed.'
1751
+ + note + filterNote;
1561
1752
  return this.textResult(this.truncateOutput(formatted));
1562
1753
  }
1563
1754
  // Multiple DISTINCT definitions (#764): one section per definition so an
@@ -1579,7 +1770,7 @@ class ToolHandler {
1579
1770
  lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
1580
1771
  }
1581
1772
  }
1582
- return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
1773
+ return this.textResult(this.truncateOutput(lines.join('\n') + filterNote + '\n\n> Caller listing complete — answer from this list; no read/grep needed.'));
1583
1774
  }
1584
1775
  /**
1585
1776
  * Handle homegraph_callees
@@ -1603,6 +1794,12 @@ class ToolHandler {
1603
1794
  const seen = new Set();
1604
1795
  const callees = [];
1605
1796
  const labels = new Map();
1797
+ const defPaths = defNodes.map((n) => n.filePath);
1798
+ const affinity = (fp) => defPaths.some((dp) => {
1799
+ const a = dp.replace(/\\/g, '/').split('/');
1800
+ const b = fp.replace(/\\/g, '/').split('/');
1801
+ return a.length >= 2 && b.length >= 2 && a[0] === b[0] && a[1] === b[1];
1802
+ });
1606
1803
  for (const node of defNodes) {
1607
1804
  for (const c of cg.getCallees(node.id)) {
1608
1805
  if (!seen.has(c.node.id)) {
@@ -1614,7 +1811,10 @@ class ToolHandler {
1614
1811
  }
1615
1812
  }
1616
1813
  }
1617
- return { callees, labels };
1814
+ // Prefer same-package callees when present — cross-package logError/logInfo
1815
+ // homonyms poison upstream/downstream answers.
1816
+ const near = callees.filter((n) => affinity(n.filePath));
1817
+ return { callees: near.length > 0 ? near : callees, labels };
1618
1818
  };
1619
1819
  if (groups.length === 1) {
1620
1820
  const { callees, labels } = collect(groups[0]);
@@ -2275,130 +2475,1704 @@ class ToolHandler {
2275
2475
  supers.set(s.id, e);
2276
2476
  }
2277
2477
  }
2278
- // Pick the supertype with the most TRUE implementers (graph-wide), among
2279
- // those genuinely shared by the token's definers.
2280
- let best = null;
2281
- for (const { node, count, targets } of supers.values()) {
2282
- if (count < MIN_SUPPORT)
2478
+ // Pick the supertype with the most TRUE implementers (graph-wide), among
2479
+ // those genuinely shared by the token's definers.
2480
+ let best = null;
2481
+ for (const { node, count, targets } of supers.values()) {
2482
+ if (count < MIN_SUPPORT)
2483
+ continue;
2484
+ let impl = 0;
2485
+ try {
2486
+ impl = cg.getIncomingEdges(node.id).filter((e) => e.kind === 'implements' || e.kind === 'extends').length;
2487
+ }
2488
+ catch { /* leave 0 — gated out below */ }
2489
+ if (impl < MIN_IMPL)
2490
+ continue;
2491
+ if (!best || impl > best.impl)
2492
+ best = { node, impl, targets };
2493
+ }
2494
+ if (!best || seenSuper.has(best.node.id))
2495
+ continue;
2496
+ seenSuper.add(best.node.id);
2497
+ const namedNames = new Set([...named.values()].map((n) => n.name));
2498
+ const eg = best.targets.slice(0, 4).map((m) => {
2499
+ const cont = containerOf(m);
2500
+ const disp = cont ? `${cont.name}.${m.name}` : (m.qualifiedName || m.name);
2501
+ const mark = cont && namedNames.has(cont.name) ? ' ← you named this' : '';
2502
+ return `\`${disp}\` (${rel(m.filePath)}:${m.startLine})${mark}`;
2503
+ });
2504
+ const more = best.impl > eg.length ? ` +${best.impl - eg.length} more` : '';
2505
+ notes.push(`- \`${token}\` → runtime dispatch to **${best.impl}** types implementing \`${best.node.name}\` — the static path ends here, the target is chosen at runtime. e.g. ${eg.join(', ')}${more}`);
2506
+ }
2507
+ if (notes.length === 0)
2508
+ return '';
2509
+ return [
2510
+ '**Interface dispatch (a named method has many implementations)**',
2511
+ '',
2512
+ ...notes,
2513
+ '',
2514
+ '> The method above is dispatched at runtime to one of the listed implementations (a registry / plugin / strategy interface) — there is no single static caller→callee edge; the implementations ARE the continuations. To follow one, run homegraph_explore on a listed target.',
2515
+ '',
2516
+ ].join('\n');
2517
+ }
2518
+ /**
2519
+ * Shortlist candidate runtime targets for a dispatch key surfaced by
2520
+ * {@link buildDynamicBoundaries}. Exact conventional names first (`save` →
2521
+ * `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a
2522
+ * normalized-containment post-filter (FTS camel-splitting is fuzzier than a
2523
+ * candidate list should be). Symbols the agent already named sort first and
2524
+ * are marked — that's the "you were right, here's the wiring" case.
2525
+ */
2526
+ boundaryCandidates(cg, key, keyIsType, named, selfId) {
2527
+ const CALLABLE = new Set(['method', 'function', 'component', 'constructor', 'class']);
2528
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
2529
+ const keyNorm = norm(key);
2530
+ if (keyNorm.length < 3)
2531
+ return '';
2532
+ const cands = new Map();
2533
+ const consider = (n) => {
2534
+ if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id))
2535
+ return;
2536
+ const nameNorm = norm(n.name || '');
2537
+ if (nameNorm.length < 3)
2538
+ return;
2539
+ if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm))
2540
+ return;
2541
+ cands.set(n.id, n);
2542
+ };
2543
+ const cap = key.charAt(0).toUpperCase() + key.slice(1);
2544
+ const probes = keyIsType
2545
+ ? [`${key}Handler`, key]
2546
+ : [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`];
2547
+ for (const p of probes) {
2548
+ try {
2549
+ for (const n of cg.getNodesByName(p))
2550
+ consider(n);
2551
+ }
2552
+ catch { /* exact probe miss is fine */ }
2553
+ }
2554
+ let raw = 0;
2555
+ try {
2556
+ const results = cg.searchNodes(key, { limit: 12 });
2557
+ raw = results.length;
2558
+ for (const r of results)
2559
+ consider(r.node);
2560
+ }
2561
+ catch { /* FTS syntax edge — exact probes already ran */ }
2562
+ if (cands.size === 0) {
2563
+ return raw >= 12 && key.length < 5 ? `key \`${key}\` is too generic to shortlist (${raw}+ matches)` : '';
2564
+ }
2565
+ // A constructor candidate duplicates its class: extractors emit ctors as
2566
+ // METHOD nodes named like the class (C#/Java `Foo::Foo`) — keep the class.
2567
+ const all = [...cands.values()];
2568
+ const classKey = new Set(all.filter((n) => n.kind === 'class').map((n) => `${n.name}|${n.filePath}`));
2569
+ const namedNames = new Set([...named.values()].map((n) => n.name));
2570
+ const isNamed = (n) => named.has(n.id) || namedNames.has(n.name); // the flow's named set holds callables only — transfer the mark to the class
2571
+ const list = all
2572
+ .filter((n) => !(n.kind !== 'class' && classKey.has(`${n.name}|${n.filePath}`)))
2573
+ .sort((a, b) => (isNamed(b) ? 1 : 0) - (isNamed(a) ? 1 : 0))
2574
+ .slice(0, 4)
2575
+ .map((n) => {
2576
+ // Typed-bus convention: the runtime target is the candidate class's
2577
+ // Handle/Execute/Consume method — name the exact node, not just the class.
2578
+ let display = n.qualifiedName || n.name;
2579
+ let at = `${n.filePath}:${n.startLine}`;
2580
+ if (keyIsType && n.kind === 'class') {
2581
+ try {
2582
+ const HANDLER_METHODS = /^(handle|handleAsync|execute|executeAsync|consume|consumeAsync|run|__invoke)$/i;
2583
+ const method = cg.getOutgoingEdges(n.id)
2584
+ .filter((e) => e.kind === 'contains')
2585
+ .map((e) => { try {
2586
+ return cg.getNode(e.target);
2587
+ }
2588
+ catch {
2589
+ return null;
2590
+ } })
2591
+ .find((c) => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name));
2592
+ if (method) {
2593
+ display = `${n.name}.${method.name}`;
2594
+ at = `${method.filePath}:${method.startLine}`;
2595
+ }
2596
+ }
2597
+ catch { /* class without resolvable members — show the class itself */ }
2598
+ }
2599
+ return `\`${display}\` (${at})${isNamed(n) ? ' ← you named this' : ''}`;
2600
+ });
2601
+ return `candidates for key \`${key}\`: ${list.join(', ')}`;
2602
+ }
2603
+ /**
2604
+ * Import sites for @kit.* / *Kit module names — surfaces full `import { … } from '@kit.X'`
2605
+ * lines. When the query also names a symbol (taskpool), only matching imports are listed.
2606
+ */
2607
+ buildImportSitesSection(cg, query, projectRoot) {
2608
+ const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
2609
+ const depSymbols = (0, query_utils_1.extractDependencySymbolsFromQuery)(query);
2610
+ const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
2611
+ const seen = new Set();
2612
+ const sites = [];
2613
+ const tryAdd = (node, lineText) => {
2614
+ const lineLc = lineText.toLowerCase();
2615
+ if (kitTerms.length > 0) {
2616
+ const matchesKit = kitTerms.some((k) => lineLc.includes(`@kit.${k.toLowerCase()}`));
2617
+ if (!matchesKit)
2618
+ return;
2619
+ }
2620
+ if (depSymbols.length > 0) {
2621
+ const matchesSym = depSymbols.some((s) => lineLc.includes(s.toLowerCase()));
2622
+ if (!matchesSym)
2623
+ return;
2624
+ }
2625
+ if (depSymbols.includes('taskpool')) {
2626
+ if (!lineLc.includes('taskpool') || !lineLc.includes('@kit.arkts'))
2627
+ return;
2628
+ }
2629
+ const key = `${node.filePath}:${node.startLine}`;
2630
+ if (seen.has(key))
2631
+ return;
2632
+ seen.add(key);
2633
+ sites.push({ file: node.filePath, line: node.startLine, lineText });
2634
+ };
2635
+ const resolveImportLine = (node) => (0, query_utils_1.resolveImportLineFromNode)(node, projectRoot);
2636
+ const importLimit = depSymbols.includes('taskpool') ? 200
2637
+ : depSymbols.length > 0 ? 60 : 20;
2638
+ // Symbol-first search: "taskpool" hits `import { taskpool } from '@kit.ArkTS'`.
2639
+ for (const sym of depSymbols) {
2640
+ let hits = [];
2641
+ try {
2642
+ hits = cg.searchNodes(sym, { kinds: ['import'], limit: importLimit });
2643
+ }
2644
+ catch {
2645
+ continue;
2646
+ }
2647
+ for (const r of hits) {
2648
+ tryAdd(r.node, resolveImportLine(r.node));
2649
+ }
2650
+ }
2651
+ // Kit-module search (when no symbol filter, or to catch re-exports).
2652
+ if (sites.length === 0 || depSymbols.length === 0) {
2653
+ for (const term of kitSearchTerms) {
2654
+ const termLc = term.toLowerCase().replace(/^@kit\./, '');
2655
+ let hits = [];
2656
+ try {
2657
+ hits = cg.searchNodes(term, { kinds: ['import'], limit: importLimit });
2658
+ }
2659
+ catch {
2660
+ continue;
2661
+ }
2662
+ for (const r of hits) {
2663
+ const lineText = resolveImportLine(r.node);
2664
+ if (!lineText.toLowerCase().includes(termLc))
2665
+ continue;
2666
+ if (depSymbols.length > 0) {
2667
+ const matchesSym = depSymbols.some((s) => lineText.toLowerCase().includes(s.toLowerCase()));
2668
+ if (!matchesSym)
2669
+ continue;
2670
+ }
2671
+ tryAdd(r.node, lineText);
2672
+ }
2673
+ }
2674
+ }
2675
+ if (sites.length === 0) {
2676
+ return { section: '', siteCount: 0, compactListing: false };
2677
+ }
2678
+ sites.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
2679
+ const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
2680
+ const taskpoolInventory = depSymbols.includes('taskpool');
2681
+ const compactListing = taskpoolInventory || (0, query_utils_1.shouldCompactImportListing)(sites.length, importInventoryFilter);
2682
+ const cap = compactListing ? (taskpoolInventory ? sites.length : 40) : 15;
2683
+ const lines = compactListing
2684
+ ? ['**Dependency list**', '', `Files importing the queried symbol(s) (${sites.length} total):`, '']
2685
+ : ['**Import sites**', ''];
2686
+ for (const s of sites.slice(0, cap)) {
2687
+ if (compactListing) {
2688
+ lines.push(`- \`${s.file}\` (line ${s.line})`);
2689
+ }
2690
+ else {
2691
+ lines.push(`- \`${s.file}:${s.line}\` — \`${s.lineText}\``);
2692
+ }
2693
+ }
2694
+ if (sites.length > cap) {
2695
+ lines.push(`- … and ${sites.length - cap} more`);
2696
+ }
2697
+ if (compactListing) {
2698
+ lines.push('');
2699
+ if (taskpoolInventory) {
2700
+ lines.push(`> Complete list of **${sites.length}** \`import { taskpool } from '@kit.ArkTS'\` site(s) — answer from this section; no grep/read needed.`);
2701
+ }
2702
+ else {
2703
+ lines.push('> Listing complete — answer from this section; no grep/read needed for the dependency set.');
2704
+ }
2705
+ }
2706
+ lines.push('');
2707
+ return { section: lines.join('\n'), siteCount: sites.length, compactListing };
2708
+ }
2709
+ /**
2710
+ * External-caller inventory for a named class — methods → who calls them (paths only).
2711
+ */
2712
+ buildCallerListingSection(cg, query) {
2713
+ const typeNames = (0, query_utils_1.extractCallerSurveySymbols)(query);
2714
+ if (typeNames.length === 0)
2715
+ return '';
2716
+ const rel = (p) => p.replace(/\\/g, '/');
2717
+ // Only filter to other files when the user asked for "external" callers.
2718
+ // Applying this to every caller-survey query wiped same-file call sites
2719
+ // (common in C++/ARK .cpp units) and fell through to a fat full explore.
2720
+ const externalOnly = /\bexternal\b/i.test(query) || /外部/.test(query);
2721
+ const lines = ['**Caller inventory**', ''];
2722
+ let substantive = 0;
2723
+ // Function / method symbols named directly (e.g. SortWidgets).
2724
+ for (const sym of typeNames.slice(0, 4)) {
2725
+ const funcs = cg.getNodesByName(sym).filter((n) => (n.kind === 'function' || n.kind === 'method') && !(0, query_utils_1.isTestFile)(n.filePath));
2726
+ for (const fn of funcs.slice(0, 3)) {
2727
+ let callers = [];
2728
+ try {
2729
+ callers = cg.getCallers(fn.id);
2730
+ }
2731
+ catch {
2732
+ continue;
2733
+ }
2734
+ const uniq = new Map();
2735
+ for (const c of callers) {
2736
+ if (!c?.node)
2737
+ continue;
2738
+ if (externalOnly && c.node.filePath === fn.filePath)
2739
+ continue;
2740
+ if ((0, query_utils_1.isTestFile)(c.node.filePath))
2741
+ continue;
2742
+ uniq.set(c.node.id, c.node);
2743
+ }
2744
+ if (uniq.size === 0)
2745
+ continue;
2746
+ const callerList = [...uniq.values()].slice(0, 12)
2747
+ .map((n) => `\`${rel(n.filePath)}:${n.startLine}\` (\`${n.name}\`)`)
2748
+ .join(', ');
2749
+ const more = uniq.size > 12 ? ` +${uniq.size - 12} more` : '';
2750
+ lines.push(`- \`${fn.name}\` (\`${rel(fn.filePath)}:${fn.startLine}\`) ← ${callerList}${more}`);
2751
+ substantive++;
2752
+ }
2753
+ }
2754
+ for (const typeName of typeNames.slice(0, 3)) {
2755
+ const classes = cg.getNodesByName(typeName)
2756
+ .filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
2757
+ for (const cls of classes.slice(0, 2)) {
2758
+ const methods = [];
2759
+ for (const e of cg.getOutgoingEdges(cls.id)) {
2760
+ if (e.kind !== 'contains')
2761
+ continue;
2762
+ const m = cg.getNode(e.target);
2763
+ if (m && (m.kind === 'method' || m.kind === 'function'))
2764
+ methods.push(m);
2765
+ }
2766
+ if (methods.length === 0) {
2767
+ for (const n of cg.getNodesByName(typeName)) {
2768
+ if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function')) {
2769
+ methods.push(n);
2770
+ }
2771
+ }
2772
+ }
2773
+ const methodLines = [];
2774
+ for (const method of methods.slice(0, 25)) {
2775
+ let callers = [];
2776
+ try {
2777
+ callers = cg.getCallers(method.id);
2778
+ }
2779
+ catch {
2780
+ continue;
2781
+ }
2782
+ const uniq = new Map();
2783
+ for (const c of callers) {
2784
+ if (!c?.node)
2785
+ continue;
2786
+ if (externalOnly && c.node.filePath === cls.filePath)
2787
+ continue;
2788
+ if ((0, query_utils_1.isTestFile)(c.node.filePath))
2789
+ continue;
2790
+ uniq.set(c.node.id, c.node);
2791
+ }
2792
+ if (uniq.size === 0)
2793
+ continue;
2794
+ const callerList = [...uniq.values()].slice(0, 8)
2795
+ .map((n) => `\`${rel(n.filePath)}:${n.startLine}\` (\`${n.name}\`)`)
2796
+ .join(', ');
2797
+ const more = uniq.size > 8 ? ` +${uniq.size - 8} more` : '';
2798
+ methodLines.push(`- \`${method.name}\` ← ${callerList}${more}`);
2799
+ substantive++;
2800
+ }
2801
+ if (methodLines.length === 0)
2802
+ continue;
2803
+ lines.push(`### \`${cls.name}\` (\`${rel(cls.filePath)}\`)`);
2804
+ lines.push(...methodLines);
2805
+ lines.push('');
2806
+ }
2807
+ }
2808
+ if (substantive === 0)
2809
+ return '';
2810
+ lines.push('> Caller inventory complete — answer from this section.');
2811
+ lines.push('');
2812
+ return lines.join('\n');
2813
+ }
2814
+ /**
2815
+ * Upstream data-source survey — which services/symbols feed a Manager (e.g. BadgeManager → notification).
2816
+ */
2817
+ buildDataSourceSection(cg, query) {
2818
+ const anchors = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((n) => /Manager|Service|Handler|Store|Provider|Controller/i.test(n));
2819
+ if (anchors.length === 0)
2820
+ return { section: '', edgeCount: 0 };
2821
+ const rel = (p) => p.replace(/\\/g, '/');
2822
+ // Prefer system/SDK / service APIs — not local UI helpers (getBadgeOffsetX).
2823
+ const SERVICE_RE = /notification|@ohos|ans|subscribe|publish|bundle|ability|vibrator|telephony|NumBadge|notificationManager|badgeManager|wantAgent|distributed/i;
2824
+ const lines = ['**Data sources / upstream services**', ''];
2825
+ let edgeCount = 0;
2826
+ const addEdge = (bucket, node, via, rank) => {
2827
+ if ((0, query_utils_1.isTestFile)(node.filePath))
2828
+ return;
2829
+ const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
2830
+ if (!SERVICE_RE.test(sig))
2831
+ return;
2832
+ const prev = bucket.get(node.id);
2833
+ if (!prev || rank < prev.rank) {
2834
+ bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank });
2835
+ if (!prev)
2836
+ edgeCount++;
2837
+ }
2838
+ };
2839
+ for (const name of anchors.slice(0, 3)) {
2840
+ const classes = cg.getNodesByName(name).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
2841
+ for (const cls of classes.slice(0, 2)) {
2842
+ const upstream = new Map();
2843
+ const methods = [];
2844
+ for (const e of cg.getOutgoingEdges(cls.id)) {
2845
+ if (e.kind !== 'contains')
2846
+ continue;
2847
+ const m = cg.getNode(e.target);
2848
+ if (m && (m.kind === 'method' || m.kind === 'function'))
2849
+ methods.push(m);
2850
+ }
2851
+ if (methods.length === 0) {
2852
+ for (const n of cg.getNodesByName(name)) {
2853
+ if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function'))
2854
+ methods.push(n);
2855
+ }
2856
+ }
2857
+ // 1) Same-file @ohos / kit imports — the usual system-service surface.
2858
+ try {
2859
+ for (const r of cg.searchNodes('@ohos', { kinds: ['import'], limit: 40 })) {
2860
+ if (r.node.filePath !== cls.filePath)
2861
+ continue;
2862
+ addEdge(upstream, r.node, 'import', 0);
2863
+ }
2864
+ for (const term of ['notification', 'badge', 'NumBadge', 'subscribe', 'bundle', 'ability']) {
2865
+ for (const r of cg.searchNodes(term, { kinds: ['import'], limit: 30 })) {
2866
+ if (r.node.filePath !== cls.filePath)
2867
+ continue;
2868
+ addEdge(upstream, r.node, 'import', 1);
2869
+ }
2870
+ }
2871
+ }
2872
+ catch { /* */ }
2873
+ // 2) Callees that look like services/APIs.
2874
+ for (const method of methods.slice(0, 25)) {
2875
+ let callees = [];
2876
+ try {
2877
+ callees = cg.getCallees(method.id);
2878
+ }
2879
+ catch {
2880
+ continue;
2881
+ }
2882
+ for (const c of callees) {
2883
+ if (c?.node)
2884
+ addEdge(upstream, c.node, method.name, 2);
2885
+ }
2886
+ }
2887
+ if (upstream.size === 0)
2888
+ continue;
2889
+ lines.push(`### \`${cls.name}\` (\`${rel(cls.filePath)}\`)`);
2890
+ const ranked = [...upstream.values()].sort((a, b) => a.rank - b.rank || a.name.localeCompare(b.name));
2891
+ for (const u of ranked.slice(0, 15)) {
2892
+ lines.push(`- \`${u.name}\` at \`${u.file}:${u.line}\` (via \`${u.via}\`)`);
2893
+ }
2894
+ if (ranked.length > 15)
2895
+ lines.push(`- … and ${ranked.length - 15} more upstream symbol(s)`);
2896
+ lines.push('');
2897
+ lines.push('> Prefer `@ohos.*` / notification / bundle imports as the system-service answer; local helpers are secondary.');
2898
+ lines.push('');
2899
+ }
2900
+ }
2901
+ if (edgeCount === 0)
2902
+ return { section: '', edgeCount: 0 };
2903
+ lines.push('> Data-source survey — answer from upstream symbols above; source bodies omitted.');
2904
+ lines.push('');
2905
+ return { section: lines.join('\n'), edgeCount };
2906
+ }
2907
+ /**
2908
+ * Main-thread fast path for inventory surveys — skips the worker queue.
2909
+ */
2910
+ tryFastPathResult(toolName, args) {
2911
+ const query = args.query;
2912
+ if (typeof query !== 'string')
2913
+ return null;
2914
+ try {
2915
+ const cg = this.getHomeGraph(args.projectPath);
2916
+ const projectRoot = cg.getProjectRoot();
2917
+ if (toolName === 'homegraph_explore') {
2918
+ return this.tryFastInventoryExplore(cg, query, projectRoot)
2919
+ ?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
2920
+ ?? this.tryLightMechanismExplore(cg, query, projectRoot);
2921
+ }
2922
+ if (toolName === 'homegraph_search') {
2923
+ return this.tryFastInventoryExplore(cg, query, projectRoot)
2924
+ ?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
2925
+ ?? this.tryLightMechanismExplore(cg, query, projectRoot);
2926
+ }
2927
+ }
2928
+ catch {
2929
+ return null;
2930
+ }
2931
+ return null;
2932
+ }
2933
+ /**
2934
+ * Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
2935
+ */
2936
+ tryFastInventoryExplore(cg, query, projectRoot) {
2937
+ if (!(0, query_utils_1.shouldTryFastInventoryExplore)(query))
2938
+ return null;
2939
+ const lines = [`**Exploration: ${query}**`, '', ''];
2940
+ const summaryLineIdx = 2;
2941
+ const inheritanceOnly = (0, query_utils_1.shouldBuildInheritanceSurvey)(query);
2942
+ const typeSurface = inheritanceOnly
2943
+ || (0, query_utils_1.shouldBuildCallerInventory)(query)
2944
+ || (0, query_utils_1.queryIsTypeNameFocus)(query);
2945
+ // Type / hierarchy / method-caller surveys: return inventory first — never a
2946
+ // definition dump or full-graph explore. Agent already named the type.
2947
+ // Exception: caller + co-named "definition visible" needs bodies (compact path).
2948
+ if (typeSurface && !(0, query_utils_1.queryNeedsCoNamedUseBridge)(query)) {
2949
+ const inheritanceSection = this.buildInheritanceSurveySection(cg, query);
2950
+ const callerSection = (0, query_utils_1.shouldBuildCallerInventory)(query)
2951
+ ? this.buildCallerListingSection(cg, query)
2952
+ : '';
2953
+ const inheritanceListed = inheritanceSection
2954
+ ? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
2955
+ : false;
2956
+ const callerBulletCount = callerSection
2957
+ ? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
2958
+ : 0;
2959
+ if (inheritanceListed || callerBulletCount > 0) {
2960
+ const parts = [
2961
+ `**Exploration: ${query}**`,
2962
+ '',
2963
+ inheritanceListed && callerBulletCount > 0
2964
+ ? 'Type surface: inheritance + method caller inventory below — answer from it; do not grep `extends` and do not `homegraph_callers` each method.'
2965
+ : inheritanceListed
2966
+ ? 'Inheritance survey above lists all direct subtypes found.'
2967
+ : 'Caller inventory lists method→caller sites — answer from it; do not fan out `homegraph_callers` per method.',
2968
+ ];
2969
+ if (inheritanceSection)
2970
+ parts.push(inheritanceSection);
2971
+ if (callerSection)
2972
+ parts.push(callerSection);
2973
+ return this.textResult(parts.join('\n'));
2974
+ }
2975
+ // Nothing graph-visible — fall through; import/domain may still help.
2976
+ }
2977
+ const importResult = (inheritanceOnly || (0, query_utils_1.queryIsTypeNameFocus)(query) || (0, query_utils_1.queryAsInheritanceSurvey)(query))
2978
+ ? { section: '', siteCount: 0, compactListing: false }
2979
+ : this.buildImportSitesSection(cg, query, projectRoot);
2980
+ if (importResult.section)
2981
+ lines.push(importResult.section);
2982
+ const kitUsageResult = (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
2983
+ ? this.buildKitModuleUsageSection(cg, query, projectRoot)
2984
+ : { section: '', symbolCount: 0 };
2985
+ if (kitUsageResult.section)
2986
+ lines.push(kitUsageResult.section);
2987
+ const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query)
2988
+ ? this.buildDomainFileSurveySection(cg, query)
2989
+ : { section: '', fileCount: 0 };
2990
+ if (domainFileResult.section)
2991
+ lines.push(domainFileResult.section);
2992
+ const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
2993
+ ? this.buildApiUsageSection(cg, query, projectRoot)
2994
+ : { section: '', fileCount: 0 };
2995
+ if (apiUsageResult.section)
2996
+ lines.push(apiUsageResult.section);
2997
+ const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
2998
+ ? this.buildDataSourceSection(cg, query)
2999
+ : { section: '', edgeCount: 0 };
3000
+ if (dataSourceResult.section)
3001
+ lines.push(dataSourceResult.section);
3002
+ const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
3003
+ const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query);
3004
+ // Type / caller surveys: skip expensive named-symbol flow synthesize —
3005
+ // it wanders unrelated dynamic edges and burns tokens.
3006
+ const skipFlow = typeSurface || (0, query_utils_1.queryAsCallerOrMethodSurvey)(query);
3007
+ const flow = skipFlow
3008
+ ? { pathNodeIds: new Set(), text: '' }
3009
+ : this.buildFlowFromNamedSymbols(cg, query);
3010
+ const hasFlowPath = flow.pathNodeIds.size > 0;
3011
+ const inheritanceSection = !hasFlowPath && !multiAnchor
3012
+ ? this.buildInheritanceSurveySection(cg, query) : '';
3013
+ const callerSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildCallerInventory)(query)
3014
+ ? this.buildCallerListingSection(cg, query) : '';
3015
+ const memberSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildMemberSurvey)(query)
3016
+ ? this.buildMemberSurveySection(cg, query, projectRoot) : '';
3017
+ const configSection = (0, query_utils_1.shouldBuildConfigSection)(query)
3018
+ ? this.buildConfigFileSection(cg, query, projectRoot) : '';
3019
+ if (inheritanceSection)
3020
+ lines.push(inheritanceSection);
3021
+ if (callerSection)
3022
+ lines.push(callerSection);
3023
+ if (memberSection)
3024
+ lines.push(memberSection);
3025
+ if (configSection)
3026
+ lines.push(configSection);
3027
+ const finishCompact = (summary) => {
3028
+ lines[summaryLineIdx] = summary;
3029
+ return this.textResult(lines.join('\n'));
3030
+ };
3031
+ const memberFileCount = memberSection
3032
+ ? memberSection.split('\n').filter((l) => l.startsWith('- ')).length
3033
+ : 0;
3034
+ const callerBulletCount = callerSection
3035
+ ? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
3036
+ : 0;
3037
+ const inheritanceListed = inheritanceSection
3038
+ ? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
3039
+ : false;
3040
+ const hasAnySection = importResult.section || kitUsageResult.section || domainFileResult.section
3041
+ || apiUsageResult.section || dataSourceResult.section || inheritanceSection
3042
+ || callerSection || memberSection || configSection;
3043
+ if (!hasAnySection)
3044
+ return null;
3045
+ const omitSource = (0, query_utils_1.shouldOmitSourceBodies)({
3046
+ importSiteCount: importResult.siteCount,
3047
+ hasFilteredImports: importInventoryFilter && importResult.siteCount > 0,
3048
+ callerBulletCount,
3049
+ memberFileCount,
3050
+ apiUsageFileCount: apiUsageResult.fileCount,
3051
+ configRendered: !!configSection,
3052
+ kitModuleSurveyRendered: !!kitUsageResult.section,
3053
+ inheritanceListed,
3054
+ domainFileCount: domainFileResult.fileCount,
3055
+ dataSourceEdgeCount: dataSourceResult.edgeCount,
3056
+ }, hasFlowPath, multiAnchor);
3057
+ if (!omitSource)
3058
+ return null;
3059
+ if (configSection)
3060
+ return finishCompact('Config/manifest content above — answer from it directly.');
3061
+ if (kitUsageResult.section) {
3062
+ return finishCompact(`Kit module usage survey — **${kitUsageResult.symbolCount}** imported symbol(s) across the repo.`);
3063
+ }
3064
+ if (domainFileResult.fileCount > 0) {
3065
+ return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above. ` +
3066
+ 'This is the exhaustive related-file inventory; no glob/search needed.');
3067
+ }
3068
+ if (apiUsageResult.fileCount > 0) {
3069
+ return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s) referencing the queried symbol(s).`);
3070
+ }
3071
+ if (dataSourceResult.edgeCount > 0) {
3072
+ return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s) listed above.`);
3073
+ }
3074
+ if (inheritanceListed)
3075
+ return finishCompact('Inheritance survey above lists all direct subtypes found.');
3076
+ if (importResult.compactListing) {
3077
+ return finishCompact(`Listed **${importResult.siteCount}** import site(s) for the queried symbol(s).`);
3078
+ }
3079
+ if (callerBulletCount >= 1) {
3080
+ return finishCompact(`Caller inventory lists **${callerBulletCount}** call site(s) — answer from the section above.`);
3081
+ }
3082
+ if (memberFileCount >= 2) {
3083
+ return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s) — answer from the inventory above.`);
3084
+ }
3085
+ return finishCompact('Inventory sections above are complete for this query.');
3086
+ }
3087
+ /**
3088
+ * Render a compact symbol-bounded slice of one file (lightweight mechanism path).
3089
+ */
3090
+ renderLightMechanismSource(projectRoot, filePath, nodes, maxChars) {
3091
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
3092
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
3093
+ return null;
3094
+ const relevant = nodes.filter((n) => n.startLine > 0 && n.kind !== 'import' && n.kind !== 'export');
3095
+ if (relevant.length === 0)
3096
+ return null;
3097
+ let content;
3098
+ try {
3099
+ content = (0, fs_1.readFileSync)(absPath, 'utf-8');
3100
+ }
3101
+ catch {
3102
+ return null;
3103
+ }
3104
+ const fileLines = content.split('\n');
3105
+ const start = Math.max(1, Math.min(...relevant.map((n) => n.startLine)) - 8);
3106
+ const end = Math.min(fileLines.length, Math.max(...relevant.map((n) => n.endLine ?? n.startLine)) + 8);
3107
+ const lang = relevant[0]?.language || '';
3108
+ const names = [...new Set(relevant.map((n) => n.name))].slice(0, 4).join(', ');
3109
+ const out = [fileSectionHeader(filePath, names), '', '```' + lang];
3110
+ for (let i = start; i <= end; i++) {
3111
+ out.push(`${i}\t${fileLines[i - 1] ?? ''}`);
3112
+ }
3113
+ out.push('```', '');
3114
+ let text = out.join('\n');
3115
+ if (text.length > maxChars) {
3116
+ text = `${text.slice(0, maxChars)}\n... (section trimmed — another explore with a specific symbol is cheaper than grep/read)\n\`\`\`\n`;
3117
+ }
3118
+ return text;
3119
+ }
3120
+ /**
3121
+ * Lightweight mechanism explore — seed entry symbols + flow spine, skip
3122
+ * findRelevantContext. Fast enough for MCP budget; complete enough to avoid
3123
+ * agent grep/read loops (token savings).
3124
+ */
3125
+ tryLightMechanismExplore(cg, query, projectRoot) {
3126
+ if (!(0, query_utils_1.shouldTryLightMechanismExplore)(query))
3127
+ return null;
3128
+ const STRUCTURE_KINDS = new Set(['class', 'struct', 'interface', 'component', 'method', 'function']);
3129
+ const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
3130
+ const fileNodes = new Map();
3131
+ const seedIds = new Set();
3132
+ const addNode = (n) => {
3133
+ if (isTestPath(n.filePath))
3134
+ return;
3135
+ if (n.kind !== 'import' && !STRUCTURE_KINDS.has(n.kind))
3136
+ return;
3137
+ seedIds.add(n.id);
3138
+ const list = fileNodes.get(n.filePath) ?? [];
3139
+ if (!list.some((x) => x.id === n.id))
3140
+ list.push(n);
3141
+ fileNodes.set(n.filePath, list);
3142
+ };
3143
+ for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
3144
+ if (seed.startsWith('@')) {
3145
+ let hits = [];
3146
+ try {
3147
+ hits = cg.searchNodes(seed, { kinds: ['import'], limit: 12 });
3148
+ }
3149
+ catch {
3150
+ continue;
3151
+ }
3152
+ for (const r of hits)
3153
+ addNode(r.node);
3154
+ continue;
3155
+ }
3156
+ for (const n of cg.getNodesByName(seed)) {
3157
+ if (STRUCTURE_KINDS.has(n.kind))
3158
+ addNode(n);
3159
+ }
3160
+ }
3161
+ if (seedIds.size < 2) {
3162
+ const domainTerms = (0, query_utils_1.extractDomainSearchTerms)(query);
3163
+ const asciiTerms = domainTerms.filter((t) => /^[\x00-\x7F]+$/.test(t));
3164
+ for (const term of asciiTerms.slice(0, 8)) {
3165
+ let hits = [];
3166
+ try {
3167
+ hits = cg.searchNodes(term, {
3168
+ kinds: ['class', 'struct', 'interface', 'function', 'method', 'import'],
3169
+ limit: 16,
3170
+ });
3171
+ }
3172
+ catch {
3173
+ continue;
3174
+ }
3175
+ for (const r of hits) {
3176
+ // English FTS expansions from Chinese concepts need not be *Manager.
3177
+ const nameHit = new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(r.node.name);
3178
+ const entryOk = (0, query_utils_1.isImplementationEntrySymbol)(r.node.name, domainTerms)
3179
+ || r.node.kind === 'import'
3180
+ || ((r.node.kind === 'function' || r.node.kind === 'method') && nameHit);
3181
+ if (!entryOk)
3182
+ continue;
3183
+ addNode(r.node);
3184
+ if (seedIds.size >= 10)
3185
+ break;
3186
+ }
3187
+ if (seedIds.size >= 10)
3188
+ break;
3189
+ }
3190
+ }
3191
+ if (seedIds.size === 0)
3192
+ return null;
3193
+ const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
3194
+ const flow = this.buildFlowFromNamedSymbols(cg, `${query} ${seeds.join(' ')}`);
3195
+ const lines = [
3196
+ `**Exploration: ${query}**`,
3197
+ '',
3198
+ `Mechanism anchors: **${seedIds.size}** symbol(s) — lightweight explore (seed + flow spine).`,
3199
+ '',
3200
+ ];
3201
+ const importResult = this.buildImportSitesSection(cg, query, projectRoot);
3202
+ if (importResult.section)
3203
+ lines.push(importResult.section);
3204
+ if (flow.text)
3205
+ lines.push(flow.text);
3206
+ const fileScores = new Map();
3207
+ for (const [fp, nodes] of fileNodes) {
3208
+ let score = 0;
3209
+ for (const n of nodes) {
3210
+ if (flow.pathNodeIds.has(n.id))
3211
+ score += 20;
3212
+ else if (flow.uniqueNamedNodeIds.has(n.id))
3213
+ score += 10;
3214
+ else
3215
+ score += 5;
3216
+ }
3217
+ fileScores.set(fp, score);
3218
+ }
3219
+ const sortedFiles = [...fileScores.entries()]
3220
+ .filter(([, s]) => s > 0)
3221
+ .sort((a, b) => b[1] - a[1])
3222
+ .slice(0, 4);
3223
+ lines.push('**Source Code**', '');
3224
+ lines.push('> Line-numbered source below — treat as already Read. Do not grep/read these files again; ' +
3225
+ 'another `homegraph_explore` with more symbol names is cheaper if you need more.');
3226
+ lines.push('');
3227
+ let totalChars = lines.join('\n').length;
3228
+ let filesRendered = 0;
3229
+ for (const [fp] of sortedFiles) {
3230
+ if (filesRendered >= 4 || totalChars > 14_000)
3231
+ break;
3232
+ const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [], 4000);
3233
+ if (!chunk)
3234
+ continue;
3235
+ lines.push(chunk);
3236
+ totalChars += chunk.length;
3237
+ filesRendered++;
3238
+ }
3239
+ if (filesRendered === 0 && !importResult.section && !flow.text)
3240
+ return null;
3241
+ lines.push('---');
3242
+ lines.push('> **Mechanism explore complete** for anchored symbols — answer from sections above. ' +
3243
+ 'Retry explore with more names before grep/read (grep duplicates token cost).');
3244
+ lines.push('');
3245
+ return this.textResult(lines.join('\n'));
3246
+ }
3247
+ /**
3248
+ * Compact explore for local-symbol behavior questions — skips findRelevantContext
3249
+ * and caps to 1–2 defining files (avoids the ~24K related-file dump).
3250
+ */
3251
+ tryCompactLocalSymbolExplore(cg, query, projectRoot) {
3252
+ // Inventory runs *before* this on the call sites. Do not refuse compact
3253
+ // merely because inventory *intent* matched — empty inventory must fall
3254
+ // through here (bare callbacks like OnSurfaceChangedCB).
3255
+ // Hard rule: 1–3 named anchors → compact for local-detail / bare-id shapes.
3256
+ // Multi-anchor flow bags (routeSave/onSave, aboutToAppear/build, thunk→thunk)
3257
+ // must fall through to full explore so Flow / Dynamic-dispatch / adaptive
3258
+ // sizing still surface — compact trail is not a substitute.
3259
+ const bareId = /^[A-Za-z_][\w]*$/.test(query.trim());
3260
+ if (!(0, query_utils_1.queryAsLocalSymbolDetail)(query) && !bareId && !(0, query_utils_1.queryHasFocusedNamedAnchors)(query)) {
3261
+ return null;
3262
+ }
3263
+ const STRUCTURE_KINDS = new Set([
3264
+ 'class', 'struct', 'interface', 'component', 'method', 'function', 'constant', 'variable', 'property', 'field',
3265
+ ]);
3266
+ const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
3267
+ const names = (0, query_utils_1.extractLocalDetailAnchors)(query).slice(0, 4);
3268
+ if (names.length === 0)
3269
+ return null;
3270
+ // Multi-anchor bags that form a Flow / Dynamic-dispatch section must use
3271
+ // full explore (synth notes, adaptive sizing, boundary announcements).
3272
+ // Compact trail alone loses those sections. Probe using the raw query —
3273
+ // extractLocalDetailAnchors can drop snake_case tokens (handle_save) that
3274
+ // buildFlowFromNamedSymbols still resolves.
3275
+ const rawTokens = query.split(/[\s,()[\]]+/).filter((t) => t.length >= 3 && /^[A-Za-z_][\w]*$/.test(t));
3276
+ if (names.length >= 2 || rawTokens.length >= 2) {
3277
+ try {
3278
+ const flow = this.buildFlowFromNamedSymbols(cg, query);
3279
+ if (flow.text.length > 0)
3280
+ return null;
3281
+ }
3282
+ catch {
3283
+ // Probe is best-effort — stay on compact if flow build fails.
3284
+ }
3285
+ }
3286
+ const fileNodes = new Map();
3287
+ const seedIds = new Set();
3288
+ const addNode = (n) => {
3289
+ if (isTestPath(n.filePath) && !(0, query_utils_1.queryAsTestOnlyInterpretation)(query))
3290
+ return;
3291
+ if (!STRUCTURE_KINDS.has(n.kind))
3292
+ return;
3293
+ seedIds.add(n.id);
3294
+ const list = fileNodes.get(n.filePath) ?? [];
3295
+ if (!list.some((x) => x.id === n.id))
3296
+ list.push(n);
3297
+ fileNodes.set(n.filePath, list);
3298
+ };
3299
+ for (const seed of names) {
3300
+ for (const n of cg.getNodesByName(seed)) {
3301
+ addNode(n);
3302
+ if (seedIds.size >= 12)
3303
+ break;
3304
+ }
3305
+ if (seedIds.size >= 12)
3306
+ break;
3307
+ }
3308
+ if (seedIds.size === 0)
3309
+ return null;
3310
+ const pathAffinity = (seedPath, otherPath) => {
3311
+ const a = seedPath.replace(/\\/g, '/').split('/');
3312
+ const b = otherPath.replace(/\\/g, '/').split('/');
3313
+ // Same package only (e.g. feature/foldeffect/…). Do NOT fallback to
3314
+ // top-level alone — that keeps cross-feature logError homonyms.
3315
+ return a.length >= 2 && b.length >= 2 && a[0] === b[0] && a[1] === b[1];
3316
+ };
3317
+ const seedPaths = [...seedIds].map((id) => {
3318
+ try {
3319
+ return cg.getNode(id)?.filePath;
3320
+ }
3321
+ catch {
3322
+ return undefined;
3323
+ }
3324
+ }).filter((p) => !!p);
3325
+ const nearSeed = (fp) => seedPaths.some((sp) => sp === fp || pathAffinity(sp, fp));
3326
+ // Neighbor policy (agents often search("Foo") alone — do not dump 11 callees):
3327
+ // - bareId / caller-bridge: callers of PRIMARY name only, no callee fan-out
3328
+ // - UI action / pinpoint local-detail: callers + callees
3329
+ const bridge = (0, query_utils_1.queryNeedsCoNamedUseBridge)(query);
3330
+ const callersOnly = bareId
3331
+ || bridge
3332
+ || ((0, query_utils_1.shouldBuildCallerInventory)(query) && !(0, query_utils_1.queryAsNamedComponentAction)(query));
3333
+ const primaryName = names[0];
3334
+ const expandIds = [...seedIds].filter((id) => {
3335
+ try {
3336
+ return cg.getNode(id)?.name === primaryName;
3337
+ }
3338
+ catch {
3339
+ return false;
3340
+ }
3341
+ });
3342
+ // Pull one-hop neighbors into the focus set so the answer is graph-complete
3343
+ // without dumping an import inventory for isExpired/onClick.
3344
+ const neighborIds = new Set();
3345
+ for (const id of expandIds) {
3346
+ let seedPath = '';
3347
+ try {
3348
+ seedPath = cg.getNode(id)?.filePath ?? '';
3349
+ }
3350
+ catch { /* */ }
3351
+ try {
3352
+ for (const { node: c } of cg.getCallers(id).slice(0, callersOnly ? 8 : 10)) {
3353
+ if (!nearSeed(c.filePath) && seedPath && !pathAffinity(seedPath, c.filePath))
3354
+ continue;
3355
+ if (!isTestPath(c.filePath) || (0, query_utils_1.queryAsTestOnlyInterpretation)(query)) {
3356
+ neighborIds.add(c.id);
3357
+ // List callers in the trail, but don't pull their whole files into Source
3358
+ // for bare-name / caller-bridge shapes (that ballooned search("SortWidgets")).
3359
+ if (!callersOnly)
3360
+ addNode(c);
3361
+ }
3362
+ }
3363
+ if (!callersOnly) {
3364
+ for (const { node: c } of cg.getCallees(id).slice(0, 10)) {
3365
+ if (!nearSeed(c.filePath) && seedPath && !pathAffinity(seedPath, c.filePath))
3366
+ continue;
3367
+ if (!isTestPath(c.filePath) || (0, query_utils_1.queryAsTestOnlyInterpretation)(query)) {
3368
+ neighborIds.add(c.id);
3369
+ addNode(c);
3370
+ }
3371
+ }
3372
+ }
3373
+ }
3374
+ catch {
3375
+ // skip edge trail for this seed
3376
+ }
3377
+ }
3378
+ const lines = [
3379
+ `**Exploration: ${query}**`,
3380
+ '',
3381
+ `Local-symbol focus: **${seedIds.size}** seed(s)` +
3382
+ (neighborIds.size > 0 ? `, **${neighborIds.size}** caller/callee neighbor(s)` : '') +
3383
+ ' — compact explore (definition + edge trail, no related-file dump).',
3384
+ '',
3385
+ ];
3386
+ // Blast radius for entry seeds (locations only) — same signal full explore
3387
+ // always-on section gives, so bare-name edits know what to update/verify.
3388
+ {
3389
+ const nodes = new Map();
3390
+ for (const id of seedIds) {
3391
+ try {
3392
+ const n = cg.getNode(id);
3393
+ if (n)
3394
+ nodes.set(id, n);
3395
+ }
3396
+ catch { /* skip */ }
3397
+ }
3398
+ const blast = this.buildBlastRadiusSection(cg, {
3399
+ nodes,
3400
+ edges: [],
3401
+ roots: [...seedIds],
3402
+ });
3403
+ if (blast)
3404
+ lines.push(blast);
3405
+ }
3406
+ // Always list a short edge trail — property→UI and button→action questions
3407
+ // need callers/callees; agents otherwise re-grep the same names.
3408
+ {
3409
+ const trail = ['**Call / use trail**', ''];
3410
+ let trailBullets = 0;
3411
+ const trailIds = (callersOnly ? expandIds : [...seedIds]).slice(0, 6);
3412
+ for (const id of trailIds) {
3413
+ let seedNode;
3414
+ try {
3415
+ seedNode = cg.getNode(id);
3416
+ }
3417
+ catch {
3418
+ continue;
3419
+ }
3420
+ if (!seedNode)
3421
+ continue;
3422
+ let callers = [];
3423
+ let callees = [];
3424
+ try {
3425
+ callers = cg.getCallers(id).slice(0, 8);
3426
+ }
3427
+ catch { /* */ }
3428
+ if (!callersOnly) {
3429
+ try {
3430
+ callees = cg.getCallees(id).slice(0, 8);
3431
+ }
3432
+ catch { /* */ }
3433
+ }
3434
+ callers = callers.filter((c) => nearSeed(c.node.filePath) || pathAffinity(seedNode.filePath, c.node.filePath));
3435
+ callees = callees.filter((c) => nearSeed(c.node.filePath) || pathAffinity(seedNode.filePath, c.node.filePath));
3436
+ if (callers.length === 0 && callees.length === 0)
3437
+ continue;
3438
+ trail.push(`- \`${seedNode.name}\` (${seedNode.kind}) — ${seedNode.filePath}:${seedNode.startLine}`);
3439
+ for (const c of callers) {
3440
+ const loc = c.node.startLine ? `:${c.node.startLine}` : '';
3441
+ trail.push(` ← used by \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
3442
+ trailBullets++;
3443
+ }
3444
+ for (const c of callees) {
3445
+ const loc = c.node.startLine ? `:${c.node.startLine}` : '';
3446
+ trail.push(` → calls \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
3447
+ trailBullets++;
3448
+ }
3449
+ }
3450
+ if (trailBullets > 0) {
3451
+ trail.push('');
3452
+ trail.push('> Prefer answering from this trail + Source below. Do not grep/search the same symbols again.');
3453
+ trail.push('');
3454
+ lines.push(...trail);
3455
+ }
3456
+ }
3457
+ // When the graph has no call edges (common for static Type.member reads),
3458
+ // fall back to a bounded indexed-file text scan for `name(` usages so the
3459
+ // agent does not pay a Grep round-trip for the same anchors.
3460
+ const textUsageHits = [];
3461
+ // Also scan when the agent asks upstream/registration — callee neighbors
3462
+ // alone do not show Export / OH_NativeXComponent wiring sites.
3463
+ const needUsageScan = neighborIds.size === 0
3464
+ || /上下游|上游|下游|注册|挂到|callback|upstream|downstream/i.test(query)
3465
+ || names.every((n) => /(?:CB|Callback)$/i.test(n));
3466
+ if (needUsageScan) {
3467
+ // Scan ALL focused anchors (incl. PascalCase callables) — previously only
3468
+ // lowercase names were scanned, so OnSurfaceChangedCB registration sites
3469
+ // never surfaced and agents fell through to callers/grep/read.
3470
+ const typeSeeds = (0, query_utils_1.extractTypeNamesFromQuery)(query);
3471
+ const scanNames = names.slice(0, 3);
3472
+ if (scanNames.length > 0) {
3473
+ try {
3474
+ const defLineKeys = new Set();
3475
+ for (const nodes of fileNodes.values()) {
3476
+ for (const n of nodes) {
3477
+ if (scanNames.includes(n.name))
3478
+ defLineKeys.add(`${n.filePath}:${n.startLine}`);
3479
+ }
3480
+ }
3481
+ const defDirs = new Set([...fileNodes.keys()].map((fp) => fp.split('/').slice(0, 4).join('/')));
3482
+ const files = cg.getFiles()
3483
+ .map((f) => f.path)
3484
+ .filter((fp) => {
3485
+ if (isTestPath(fp))
3486
+ return false;
3487
+ if (defDirs.size === 0)
3488
+ return true;
3489
+ return [...defDirs].some((d) => fp === d || fp.startsWith(`${d}/`));
3490
+ })
3491
+ .slice(0, 120);
3492
+ for (const fp of files) {
3493
+ if (textUsageHits.length >= 12)
3494
+ break;
3495
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
3496
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
3497
+ continue;
3498
+ let content;
3499
+ try {
3500
+ content = (0, fs_1.readFileSync)(absPath, 'utf-8');
3501
+ }
3502
+ catch {
3503
+ continue;
3504
+ }
3505
+ // Prefer files that also mention a co-named type when both present
3506
+ // (skip for single-anchor upstream/registration scans).
3507
+ if (typeSeeds.length > 1
3508
+ && names.length > 1
3509
+ && !typeSeeds.some((t) => content.includes(t))) {
3510
+ continue;
3511
+ }
3512
+ const fileLines = content.split('\n');
3513
+ for (const sym of scanNames) {
3514
+ const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3515
+ // Match call OR bare identifier use (callback registration tables).
3516
+ const re = new RegExp(`\\b${escaped}\\b`);
3517
+ for (let i = 0; i < fileLines.length; i++) {
3518
+ const lineText = fileLines[i] ?? '';
3519
+ if (!re.test(lineText))
3520
+ continue;
3521
+ if (defLineKeys.has(`${fp}:${i + 1}`))
3522
+ continue;
3523
+ textUsageHits.push({ filePath: fp, line: i + 1, text: lineText.trim(), symbol: sym });
3524
+ if (!fileNodes.has(fp))
3525
+ fileNodes.set(fp, []);
3526
+ if (textUsageHits.length >= 12)
3527
+ break;
3528
+ }
3529
+ if (textUsageHits.length >= 12)
3530
+ break;
3531
+ }
3532
+ }
3533
+ }
3534
+ catch {
3535
+ // text scan is best-effort
3536
+ }
3537
+ }
3538
+ if (textUsageHits.length > 0) {
3539
+ lines.push('**Text usage sites** (no call-edge indexed for these names — scanned in-repo)', '');
3540
+ for (const h of textUsageHits.slice(0, 10)) {
3541
+ lines.push(`- \`${h.symbol}\` — ${h.filePath}:${h.line} \`${h.text.slice(0, 120)}\``);
3542
+ }
3543
+ lines.push('');
3544
+ lines.push('> These lines are the in-repo use sites. Answer from them + Source; do not grep the same names again.');
3545
+ lines.push('');
3546
+ }
3547
+ }
3548
+ // Also surface #include / import lines that make a co-named type visible
3549
+ // near the primary symbol's use sites (IntGrid ↔ SortWidgets shapes).
3550
+ if (names.length >= 2
3551
+ && /include|import|visible|定义可见|可见|头文件|definition/i.test(query)) {
3552
+ const includeHits = [];
3553
+ const primaryFiles = new Set([
3554
+ ...fileNodes.keys(),
3555
+ ...textUsageHits.map((h) => h.filePath),
3556
+ ]);
3557
+ // Files that call / define earlier anchors.
3558
+ for (const id of seedIds) {
3559
+ try {
3560
+ for (const { node: c } of cg.getCallers(id).slice(0, 8)) {
3561
+ primaryFiles.add(c.filePath);
3562
+ }
3563
+ }
3564
+ catch { /* */ }
3565
+ }
3566
+ const secondary = names.slice(1);
3567
+ for (const fp of [...primaryFiles].slice(0, 12)) {
3568
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
3569
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
3570
+ continue;
3571
+ let content;
3572
+ try {
3573
+ content = (0, fs_1.readFileSync)(absPath, 'utf-8');
3574
+ }
3575
+ catch {
3576
+ continue;
3577
+ }
3578
+ const fileLines = content.split('\n');
3579
+ for (let i = 0; i < Math.min(fileLines.length, 120); i++) {
3580
+ const lineText = fileLines[i] ?? '';
3581
+ if (!/^\s*(?:#\s*include|import\s)/.test(lineText))
3582
+ continue;
3583
+ if (!secondary.some((s) => lineText.includes(s)))
3584
+ continue;
3585
+ includeHits.push(`- \`${fp}:${i + 1}\` \`${lineText.trim().slice(0, 140)}\``);
3586
+ if (includeHits.length >= 8)
3587
+ break;
3588
+ }
3589
+ if (includeHits.length >= 8)
3590
+ break;
3591
+ }
3592
+ if (includeHits.length > 0) {
3593
+ lines.push('**Include / import visibility** (how co-named types become visible at use sites)', '');
3594
+ lines.push(...includeHits);
3595
+ lines.push('');
3596
+ lines.push('> Answer the visibility question from these include/import lines — do not re-read headers.');
3597
+ lines.push('');
3598
+ }
3599
+ }
3600
+ const callerSection = (0, query_utils_1.shouldBuildCallerInventory)(query)
3601
+ ? this.buildCallerListingSection(cg, query)
3602
+ : '';
3603
+ if (callerSection)
3604
+ lines.push(callerSection);
3605
+ // Prefer files whose nodes exactly match query names; then neighbor / usage files.
3606
+ // Primary named symbol (first anchor) always ranks first — secondary types like
3607
+ // IntGrid used to drown SortWidgets' defining .cpp via higher exact-count on the header.
3608
+ const nameSet = new Set(names);
3609
+ const usageFiles = new Set(textUsageHits.map((h) => h.filePath));
3610
+ const maxFiles = ((0, query_utils_1.queryAsNamedComponentAction)(query) || (0, query_utils_1.queryHasNamedMemberFocus)(query) || usageFiles.size > 0 || bridge)
3611
+ ? 4
3612
+ : 2;
3613
+ const ranked = [...fileNodes.entries()]
3614
+ .map(([fp, nodes]) => {
3615
+ const exact = nodes.filter((n) => nameSet.has(n.name)).length;
3616
+ const primaryHit = nodes.some((n) => n.name === primaryName) ? 100 : 0;
3617
+ const neighborHit = nodes.filter((n) => neighborIds.has(n.id)).length;
3618
+ const usageHit = usageFiles.has(fp) ? 15 : 0;
3619
+ return {
3620
+ fp,
3621
+ nodes,
3622
+ exact,
3623
+ score: primaryHit + exact * 20 + neighborHit * 5 + usageHit + nodes.length,
3624
+ };
3625
+ })
3626
+ .sort((a, b) => b.score - a.score || a.fp.localeCompare(b.fp))
3627
+ .slice(0, maxFiles);
3628
+ lines.push('**Source Code**', '');
3629
+ lines.push('> Line-numbered source — treat as already Read. Answer from it; do not re-explore/search/grep the same symbols.');
3630
+ lines.push('');
3631
+ let totalChars = lines.join('\n').length;
3632
+ const maxTotal = 9000;
3633
+ const maxPerFile = 4000;
3634
+ let rendered = 0;
3635
+ for (const { fp, nodes } of ranked) {
3636
+ if (rendered >= maxFiles || totalChars > maxTotal)
3637
+ break;
3638
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
3639
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
3640
+ continue;
3641
+ let fileContent;
3642
+ try {
3643
+ fileContent = (0, fs_1.readFileSync)(absPath, 'utf-8');
3644
+ }
3645
+ catch {
3646
+ continue;
3647
+ }
3648
+ const fileLines = fileContent.split('\n');
3649
+ const focusNodes = nodes
3650
+ .filter((n) => nameSet.has(n.name) || STRUCTURE_KINDS.has(n.kind))
3651
+ .sort((a, b) => a.startLine - b.startLine);
3652
+ // Prefer contiguous windows around named symbols; usage-only files
3653
+ // window around the first text-hit line.
3654
+ let start = 1;
3655
+ let end = Math.min(fileLines.length, 120);
3656
+ const usageLine = textUsageHits.find((h) => h.filePath === fp)?.line;
3657
+ if (focusNodes.length > 0) {
3658
+ const named = focusNodes.filter((n) => nameSet.has(n.name));
3659
+ const use = named.length > 0 ? named : focusNodes.slice(0, 3);
3660
+ start = Math.max(1, Math.min(...use.map((n) => n.startLine)) - 2);
3661
+ end = Math.min(fileLines.length, Math.max(...use.map((n) => n.endLine)) + 2);
3662
+ // Keep window bounded
3663
+ if ((end - start + 1) * 40 > maxPerFile) {
3664
+ end = Math.min(fileLines.length, start + Math.floor(maxPerFile / 40) - 1);
3665
+ }
3666
+ }
3667
+ else if (usageLine) {
3668
+ start = Math.max(1, usageLine - 15);
3669
+ end = Math.min(fileLines.length, usageLine + 25);
3670
+ }
3671
+ const header = fileSectionHeader(fp, focusNodes.map((n) => `${n.name}(${n.kind})`).slice(0, 6).join(', '));
3672
+ const withLineNumbers = exploreLineNumbersEnabled();
3673
+ const bodyLines = [header, '```' + (nodes[0]?.language || ''), ''];
3674
+ for (let i = start; i <= end; i++) {
3675
+ const code = fileLines[i - 1] ?? '';
3676
+ bodyLines.push(withLineNumbers ? `${i}\t${code}` : code);
3677
+ }
3678
+ bodyLines.push('```', '');
3679
+ let chunk = bodyLines.join('\n');
3680
+ if (chunk.length > maxPerFile) {
3681
+ chunk = `${chunk.slice(0, maxPerFile)}\n... (trimmed)\n\`\`\`\n`;
3682
+ }
3683
+ if (totalChars + chunk.length > maxTotal && rendered > 0)
3684
+ break;
3685
+ lines.push(chunk);
3686
+ totalChars += chunk.length;
3687
+ rendered++;
3688
+ }
3689
+ if (rendered === 0)
3690
+ return null;
3691
+ lines.push('---');
3692
+ lines.push('> **Compact local explore complete** — answer from the definition(s) above. ' +
3693
+ 'Do not call `homegraph_search` or another full explore for the same symbol.');
3694
+ lines.push('');
3695
+ return this.textResult(lines.join('\n'));
3696
+ }
3697
+ /**
3698
+ * @kit module capability survey — repo import/usage only (SDK defs are not indexed).
3699
+ */
3700
+ buildKitModuleUsageSection(cg, query, projectRoot) {
3701
+ const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
3702
+ const submodules = (0, query_utils_1.extractKitSubmoduleNamesFromQuery)(query);
3703
+ if (kitTerms.length === 0)
3704
+ return { section: '', symbolCount: 0 };
3705
+ const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
3706
+ const seen = new Set();
3707
+ const imports = [];
3708
+ const parseImportedSymbols = (lineText) => {
3709
+ const syms = new Set();
3710
+ const brace = lineText.match(/import\s*\{([^}]+)\}/);
3711
+ if (brace?.[1]) {
3712
+ for (const part of brace[1].split(',')) {
3713
+ const tok = part.trim().split(/\s+as\s+/i)[0]?.trim();
3714
+ if (tok)
3715
+ syms.add(tok);
3716
+ }
3717
+ }
3718
+ const def = lineText.match(/import\s+(\w+)\s+from\b/);
3719
+ if (def?.[1])
3720
+ syms.add(def[1]);
3721
+ const ns = lineText.match(/import\s+\*\s+as\s+(\w+)/);
3722
+ if (ns?.[1])
3723
+ syms.add(ns[1]);
3724
+ return [...syms];
3725
+ };
3726
+ const matchesSubmodule = (lineText, symbols) => {
3727
+ if (submodules.length === 0)
3728
+ return true;
3729
+ const lc = lineText.toLowerCase();
3730
+ return submodules.some((sm) => {
3731
+ const s = sm.toLowerCase();
3732
+ return symbols.some((sym) => sym.toLowerCase() === s || sym.toLowerCase().startsWith(`${s}.`))
3733
+ || lc.includes(`.${s}`) || lc.includes(`'${s}'`) || lc.includes(`"${s}"`);
3734
+ });
3735
+ };
3736
+ const resolveImportLine = (node) => (0, query_utils_1.resolveImportLineFromNode)(node, projectRoot);
3737
+ for (const term of kitSearchTerms) {
3738
+ const termLc = term.toLowerCase().replace(/^@kit\./, '');
3739
+ let hits = [];
3740
+ try {
3741
+ hits = cg.searchNodes(term, { kinds: ['import'], limit: 80 });
3742
+ }
3743
+ catch {
3744
+ continue;
3745
+ }
3746
+ for (const r of hits) {
3747
+ const lineText = resolveImportLine(r.node);
3748
+ if (!lineText.toLowerCase().includes(termLc) && !lineText.toLowerCase().includes('@kit.'))
3749
+ continue;
3750
+ const symbols = parseImportedSymbols(lineText);
3751
+ if (!matchesSubmodule(lineText, symbols))
3752
+ continue;
3753
+ const key = `${r.node.filePath}:${r.node.startLine}`;
3754
+ if (seen.has(key))
3755
+ continue;
3756
+ seen.add(key);
3757
+ imports.push({
3758
+ file: r.node.filePath,
3759
+ line: r.node.startLine,
3760
+ lineText,
3761
+ symbols,
3762
+ });
3763
+ }
3764
+ }
3765
+ if (imports.length === 0) {
3766
+ const kitLabel = kitTerms.map((k) => `@kit.${k}`).join(', ');
3767
+ const sub = submodules.length > 0 ? ` (${submodules.join(', ')} submodule)` : '';
3768
+ return {
3769
+ section: [
3770
+ '**Kit module usage (this repo)**',
3771
+ '',
3772
+ `No imports from ${kitLabel}${sub} were found in the indexed codebase.`,
3773
+ '',
3774
+ `> @kit module API definitions live in the HarmonyOS SDK, not this repository. Answer from repo imports/usages only.`,
3775
+ '',
3776
+ ].join('\n'),
3777
+ symbolCount: 0,
3778
+ };
3779
+ }
3780
+ const symbolFiles = new Map();
3781
+ for (const imp of imports) {
3782
+ for (const sym of imp.symbols) {
3783
+ const set = symbolFiles.get(sym) ?? new Set();
3784
+ set.add(imp.file);
3785
+ symbolFiles.set(sym, set);
3786
+ }
3787
+ }
3788
+ const lines = [
3789
+ '**Kit module usage (this repo)**',
3790
+ '',
3791
+ `> @kit module API definitions live in the HarmonyOS SDK, not this repository. Below: how **this project** imports and uses them.`,
3792
+ '',
3793
+ `Imports from ${kitTerms.map((k) => `\`@kit.${k}\``).join(', ')} (${imports.length} site(s)):`,
3794
+ '',
3795
+ ];
3796
+ for (const imp of imports.slice(0, 35)) {
3797
+ const symStr = imp.symbols.length > 0 ? imp.symbols.join(', ') : '(namespace)';
3798
+ lines.push(`- \`${imp.file}:${imp.line}\` — \`${imp.lineText}\` (${symStr})`);
3799
+ }
3800
+ if (imports.length > 35)
3801
+ lines.push(`- … and ${imports.length - 35} more import site(s)`);
3802
+ if (symbolFiles.size > 0) {
3803
+ lines.push('');
3804
+ lines.push('**Symbols imported (unique):**');
3805
+ for (const [sym, files] of [...symbolFiles.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(0, 40)) {
3806
+ const fileList = [...files].slice(0, 5).map((f) => `\`${f}\``).join(', ');
3807
+ const more = files.size > 5 ? ` +${files.size - 5} files` : '';
3808
+ lines.push(`- \`${sym}\` — imported in ${fileList}${more}`);
3809
+ }
3810
+ }
3811
+ lines.push('');
3812
+ lines.push('> Kit usage survey complete — answer from this section; do not search external SDK docs or grep the repo again.');
3813
+ lines.push('');
3814
+ return { section: lines.join('\n'), symbolCount: symbolFiles.size };
3815
+ }
3816
+ /** Disambiguate homonymous types (Configuration, Rectangle) when several defs exist. */
3817
+ buildHomonymDefinitionsSection(cg, query) {
3818
+ const typeNames = (0, query_utils_1.extractTypeNamesFromQuery)(query);
3819
+ const queryFiles = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
3820
+ const lines = [];
3821
+ for (const name of typeNames.slice(0, 4)) {
3822
+ const defs = cg.getNodesByName(name).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface' || n.kind === 'type_alias')
3823
+ && !(0, query_utils_1.isTestFile)(n.filePath));
3824
+ if (defs.length <= 1)
3825
+ continue;
3826
+ lines.push(`**${name}** — ${defs.length} distinct definitions (pin with file basename in a follow-up explore):`);
3827
+ for (const d of defs.slice(0, 8)) {
3828
+ const pin = (0, query_utils_1.fileMatchesQueryBasename)(d.filePath, queryFiles) ? ' ← matches query file' : '';
3829
+ lines.push(`- \`${d.qualifiedName || d.name}\` (\`${d.filePath}:${d.startLine}\`)${pin}`);
3830
+ }
3831
+ if (defs.length > 8)
3832
+ lines.push(`- … and ${defs.length - 8} more`);
3833
+ lines.push('');
3834
+ }
3835
+ if (lines.length === 0)
3836
+ return '';
3837
+ return lines.join('\n');
3838
+ }
3839
+ /** Direct subclasses of a named type — paths only. */
3840
+ buildInheritanceSurveySection(cg, query) {
3841
+ if (!(0, query_utils_1.shouldBuildInheritanceSurvey)(query))
3842
+ return '';
3843
+ const rel = (p) => p.replace(/\\/g, '/');
3844
+ const lines = ['**Inheritance survey**', ''];
3845
+ let listed = 0;
3846
+ for (const typeName of (0, query_utils_1.extractTypeNamesFromQuery)(query).slice(0, 2)) {
3847
+ const bases = cg.getNodesByName(typeName).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface') && !(0, query_utils_1.isTestFile)(n.filePath));
3848
+ for (const base of bases.slice(0, 2)) {
3849
+ const subs = [];
3850
+ for (const e of cg.getIncomingEdges(base.id)) {
3851
+ if (e.kind !== 'extends' && e.kind !== 'implements')
3852
+ continue;
3853
+ try {
3854
+ const child = cg.getNode(e.source);
3855
+ if (child && !(0, query_utils_1.isTestFile)(child.filePath))
3856
+ subs.push(child);
3857
+ }
3858
+ catch { /* skip */ }
3859
+ }
3860
+ if (subs.length === 0)
3861
+ continue;
3862
+ lines.push(`### Subtypes of \`${base.name}\` (\`${rel(base.filePath)}\`)`);
3863
+ for (const s of subs.slice(0, 30)) {
3864
+ lines.push(`- \`${s.name}\` (\`${rel(s.filePath)}:${s.startLine}\`)`);
3865
+ listed++;
3866
+ }
3867
+ if (subs.length > 30)
3868
+ lines.push(`- … and ${subs.length - 30} more`);
3869
+ lines.push('');
3870
+ }
3871
+ }
3872
+ if (listed === 0)
3873
+ return '';
3874
+ lines.push('> Inheritance survey complete — answer from this list; no read/grep needed for the subtype set.');
3875
+ lines.push('');
3876
+ return lines.join('\n');
3877
+ }
3878
+ /**
3879
+ * Files/lines using a member or literal pattern (.drawModifier, .width('100%')).
3880
+ */
3881
+ buildMemberSurveySection(cg, query, projectRoot) {
3882
+ const members = (0, query_utils_1.extractMemberAccessFromQuery)(query);
3883
+ const patterns = [];
3884
+ for (const ma of members) {
3885
+ if (ma.dotted.startsWith('.'))
3886
+ patterns.push(ma.dotted);
3887
+ else
3888
+ patterns.push(ma.dotted);
3889
+ }
3890
+ // Literal chains in quotes: .width('100%')
3891
+ for (const m of query.matchAll(/(\.[a-zA-Z_][\w]*\s*\([^)]*\))/g)) {
3892
+ patterns.push(m[1].replace(/\s+/g, ''));
3893
+ }
3894
+ if (patterns.length === 0)
3895
+ return '';
3896
+ const rel = (p) => p.replace(/\\/g, '/');
3897
+ const hits = new Map();
3898
+ const addHit = (file, line) => {
3899
+ const arr = hits.get(file) ?? [];
3900
+ if (!arr.includes(line))
3901
+ arr.push(line);
3902
+ hits.set(file, arr);
3903
+ };
3904
+ for (const pat of patterns) {
3905
+ const bare = pat.replace(/^\./, '');
3906
+ let nodes = [];
3907
+ try {
3908
+ nodes = cg.searchNodes(bare, { limit: 30 });
3909
+ }
3910
+ catch { /* skip */ }
3911
+ for (const r of nodes) {
3912
+ if ((0, query_utils_1.isTestFile)(r.node.filePath))
3913
+ continue;
3914
+ addHit(rel(r.node.filePath), r.node.startLine);
3915
+ }
3916
+ }
3917
+ // Scan top FTS files for literal pattern in source
3918
+ for (const pat of patterns.slice(0, 3)) {
3919
+ const literal = pat.startsWith('.') ? pat : `.${pat}`;
3920
+ const re = new RegExp(literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
3921
+ let files = [];
3922
+ try {
3923
+ files = cg.searchNodes(patterns[0].replace(/^\./, ''), { limit: 25 });
3924
+ }
3925
+ catch {
3926
+ continue;
3927
+ }
3928
+ for (const r of files) {
3929
+ if ((0, query_utils_1.isTestFile)(r.node.filePath))
3930
+ continue;
3931
+ const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
3932
+ if (!abs)
3933
+ continue;
3934
+ let content;
3935
+ try {
3936
+ content = (0, fs_1.readFileSync)(abs, 'utf-8');
3937
+ }
3938
+ catch {
3939
+ continue;
3940
+ }
3941
+ const fileLines = content.split('\n');
3942
+ for (let i = 0; i < fileLines.length; i++) {
3943
+ if (re.test(fileLines[i] ?? ''))
3944
+ addHit(rel(r.node.filePath), i + 1);
3945
+ }
3946
+ }
3947
+ }
3948
+ if (hits.size === 0)
3949
+ return '';
3950
+ const lines = ['**Member / pattern usage**', ''];
3951
+ let fileCount = 0;
3952
+ for (const [fp, lineNos] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
3953
+ if (fileCount >= 40)
3954
+ break;
3955
+ const sorted = lineNos.sort((a, b) => a - b).slice(0, 6);
3956
+ const lineStr = sorted.join(', ');
3957
+ const more = lineNos.length > 6 ? ` +${lineNos.length - 6} lines` : '';
3958
+ lines.push(`- \`${fp}\` — lines ${lineStr}${more}`);
3959
+ fileCount++;
3960
+ }
3961
+ if (hits.size > 40)
3962
+ lines.push(`- … and ${hits.size - 40} more file(s)`);
3963
+ lines.push('');
3964
+ lines.push('> Pattern survey complete — answer from this list; run another explore naming missing symbols if needed.');
3965
+ lines.push('');
3966
+ return lines.join('\n');
3967
+ }
3968
+ /**
3969
+ * API call-site survey — where a named API/symbol (statfs, napi_*) appears in repo source.
3970
+ */
3971
+ buildApiUsageSection(cg, query, projectRoot) {
3972
+ // Include PascalCase SDK modules (Telephony) — deps-only missed them.
3973
+ const symbols = (0, query_utils_1.extractApiUsageTokens)(query).slice(0, 6);
3974
+ if (symbols.length === 0)
3975
+ return { section: '', fileCount: 0 };
3976
+ const rel = (p) => p.replace(/\\/g, '/');
3977
+ const hits = new Map();
3978
+ const addHit = (file, line) => {
3979
+ if ((0, query_utils_1.isTestFile)(file))
3980
+ return;
3981
+ const fp = rel(file);
3982
+ const arr = hits.get(fp) ?? [];
3983
+ if (!arr.includes(line))
3984
+ arr.push(line);
3985
+ hits.set(fp, arr);
3986
+ };
3987
+ for (const sym of symbols.slice(0, 4)) {
3988
+ const symLc = sym.toLowerCase();
3989
+ let nodes = [];
3990
+ try {
3991
+ nodes = cg.searchNodes(sym, { limit: 40 });
3992
+ }
3993
+ catch { /* skip */ }
3994
+ for (const r of nodes) {
3995
+ const n = r.node;
3996
+ if (n.name.toLowerCase() === symLc || (n.signature || '').toLowerCase().includes(symLc)) {
3997
+ addHit(n.filePath, n.startLine);
3998
+ }
3999
+ }
4000
+ const wordRe = new RegExp(`\\b${sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
4001
+ let scanHits = [];
4002
+ try {
4003
+ scanHits = cg.searchNodes(sym, { limit: 30 });
4004
+ }
4005
+ catch {
4006
+ continue;
4007
+ }
4008
+ const seenFiles = new Set();
4009
+ for (const r of scanHits) {
4010
+ if ((0, query_utils_1.isTestFile)(r.node.filePath))
2283
4011
  continue;
2284
- let impl = 0;
4012
+ const fp = rel(r.node.filePath);
4013
+ if (seenFiles.has(fp))
4014
+ continue;
4015
+ seenFiles.add(fp);
4016
+ const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
4017
+ if (!abs)
4018
+ continue;
4019
+ let content;
2285
4020
  try {
2286
- impl = cg.getIncomingEdges(node.id).filter((e) => e.kind === 'implements' || e.kind === 'extends').length;
4021
+ content = (0, fs_1.readFileSync)(abs, 'utf-8');
2287
4022
  }
2288
- catch { /* leave 0 — gated out below */ }
2289
- if (impl < MIN_IMPL)
4023
+ catch {
2290
4024
  continue;
2291
- if (!best || impl > best.impl)
2292
- best = { node, impl, targets };
4025
+ }
4026
+ const fileLines = content.split('\n');
4027
+ for (let i = 0; i < fileLines.length; i++) {
4028
+ if (wordRe.test(fileLines[i] ?? ''))
4029
+ addHit(fp, i + 1);
4030
+ }
2293
4031
  }
2294
- if (!best || seenSuper.has(best.node.id))
2295
- continue;
2296
- seenSuper.add(best.node.id);
2297
- const namedNames = new Set([...named.values()].map((n) => n.name));
2298
- const eg = best.targets.slice(0, 4).map((m) => {
2299
- const cont = containerOf(m);
2300
- const disp = cont ? `${cont.name}.${m.name}` : (m.qualifiedName || m.name);
2301
- const mark = cont && namedNames.has(cont.name) ? ' ← you named this' : '';
2302
- return `\`${disp}\` (${rel(m.filePath)}:${m.startLine})${mark}`;
2303
- });
2304
- const more = best.impl > eg.length ? ` +${best.impl - eg.length} more` : '';
2305
- notes.push(`- \`${token}\` → runtime dispatch to **${best.impl}** types implementing \`${best.node.name}\` — the static path ends here, the target is chosen at runtime. e.g. ${eg.join(', ')}${more}`);
2306
4032
  }
2307
- if (notes.length === 0)
2308
- return '';
2309
- return [
2310
- '**Interface dispatch (a named method has many implementations)**',
2311
- '',
2312
- ...notes,
4033
+ if (hits.size === 0)
4034
+ return { section: '', fileCount: 0 };
4035
+ const lines = [
4036
+ '**API usage sites**',
2313
4037
  '',
2314
- '> The method above is dispatched at runtime to one of the listed implementations (a registry / plugin / strategy interface) there is no single static caller→callee edge; the implementations ARE the continuations. To follow one, run homegraph_explore on a listed target.',
4038
+ `> Files referencing \`${symbols.join('`, `')}\` in this repo (${hits.size} file(s)). Answer from this list; source bodies omitted.`,
2315
4039
  '',
2316
- ].join('\n');
4040
+ ];
4041
+ let shown = 0;
4042
+ for (const [fp, lineNos] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
4043
+ if (shown >= 45)
4044
+ break;
4045
+ const sortedLines = [...lineNos].sort((a, b) => a - b).slice(0, 6);
4046
+ const lineStr = sortedLines.map((l) => `L${l}`).join(', ');
4047
+ const more = lineNos.length > sortedLines.length ? ` +${lineNos.length - sortedLines.length} more` : '';
4048
+ lines.push(`- \`${fp}\` (${lineStr}${more})`);
4049
+ shown++;
4050
+ }
4051
+ if (hits.size > shown)
4052
+ lines.push(`- … and ${hits.size - shown} more file(s)`);
4053
+ lines.push('');
4054
+ lines.push(`> API usage survey complete — **${hits.size}** file(s) listed.`);
4055
+ lines.push('');
4056
+ return { section: lines.join('\n'), fileCount: hits.size };
2317
4057
  }
2318
4058
  /**
2319
- * Shortlist candidate runtime targets for a dispatch key surfaced by
2320
- * {@link buildDynamicBoundaries}. Exact conventional names first (`save`
2321
- * `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a
2322
- * normalized-containment post-filter (FTS camel-splitting is fuzzier than a
2323
- * candidate list should be). Symbols the agent already named sort first and
2324
- * are marked — that's the "you were right, here's the wiring" case.
4059
+ * Domain file inventory related files, usage existence, or concept comparison.
4060
+ * Paths + top symbols only; source bodies omitted when this section renders.
2325
4061
  */
2326
- boundaryCandidates(cg, key, keyIsType, named, selfId) {
2327
- const CALLABLE = new Set(['method', 'function', 'component', 'constructor', 'class']);
2328
- const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
2329
- const keyNorm = norm(key);
2330
- if (keyNorm.length < 3)
2331
- return '';
2332
- const cands = new Map();
2333
- const consider = (n) => {
2334
- if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id))
2335
- return;
2336
- const nameNorm = norm(n.name || '');
2337
- if (nameNorm.length < 3)
2338
- return;
2339
- if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm))
4062
+ buildDomainFileSurveySection(cg, query) {
4063
+ const terms = (0, query_utils_1.extractDomainSearchTerms)(query);
4064
+ if (terms.length === 0)
4065
+ return { section: '', fileCount: 0 };
4066
+ const rel = (p) => p.replace(/\\/g, '/');
4067
+ const fileScores = new Map();
4068
+ const addFile = (filePath, score, symbol) => {
4069
+ if ((0, query_utils_1.isTestFile)(filePath))
2340
4070
  return;
2341
- cands.set(n.id, n);
4071
+ const fp = rel(filePath);
4072
+ const existing = fileScores.get(fp) ?? { score: 0, symbols: [] };
4073
+ existing.score += score;
4074
+ if (symbol && existing.symbols.length < 5 && !existing.symbols.includes(symbol)) {
4075
+ existing.symbols.push(symbol);
4076
+ }
4077
+ fileScores.set(fp, existing);
2342
4078
  };
2343
- const cap = key.charAt(0).toUpperCase() + key.slice(1);
2344
- const probes = keyIsType
2345
- ? [`${key}Handler`, key]
2346
- : [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`];
2347
- for (const p of probes) {
4079
+ for (const term of terms) {
4080
+ let hits = [];
2348
4081
  try {
2349
- for (const n of cg.getNodesByName(p))
2350
- consider(n);
4082
+ hits = cg.searchNodes(term, { limit: 45 });
4083
+ }
4084
+ catch {
4085
+ continue;
4086
+ }
4087
+ for (const r of hits) {
4088
+ addFile(r.node.filePath, (r.score ?? 1) + 2, r.node.name);
2351
4089
  }
2352
- catch { /* exact probe miss is fine */ }
2353
4090
  }
2354
- let raw = 0;
2355
4091
  try {
2356
- const results = cg.searchNodes(key, { limit: 12 });
2357
- raw = results.length;
2358
- for (const r of results)
2359
- consider(r.node);
4092
+ for (const f of cg.getFiles()) {
4093
+ const fpLc = f.path.toLowerCase();
4094
+ for (const term of terms) {
4095
+ if (term.length >= 3 && fpLc.includes(term.toLowerCase())) {
4096
+ addFile(f.path, 6);
4097
+ }
4098
+ }
4099
+ }
2360
4100
  }
2361
- catch { /* FTS syntax edge — exact probes already ran */ }
2362
- if (cands.size === 0) {
2363
- return raw >= 12 && key.length < 5 ? `key \`${key}\` is too generic to shortlist (${raw}+ matches)` : '';
4101
+ catch { /* getFiles unavailable */ }
4102
+ if (fileScores.size === 0)
4103
+ return { section: '', fileCount: 0 };
4104
+ const sorted = [...fileScores.entries()]
4105
+ .sort((a, b) => b[1].score - a[1].score)
4106
+ .slice(0, 50);
4107
+ const lines = [
4108
+ '**Related files (domain survey)**',
4109
+ '',
4110
+ `> File paths matching the query domain (${terms.map((t) => `\`${t}\``).join(', ')}). Answer from this inventory; source bodies omitted.`,
4111
+ '',
4112
+ ];
4113
+ for (const [fp, info] of sorted.slice(0, 40)) {
4114
+ const symStr = info.symbols.length > 0 ? ` — ${info.symbols.join(', ')}` : '';
4115
+ lines.push(`- \`${fp}\`${symStr}`);
2364
4116
  }
2365
- // A constructor candidate duplicates its class: extractors emit ctors as
2366
- // METHOD nodes named like the class (C#/Java `Foo::Foo`) — keep the class.
2367
- const all = [...cands.values()];
2368
- const classKey = new Set(all.filter((n) => n.kind === 'class').map((n) => `${n.name}|${n.filePath}`));
2369
- const namedNames = new Set([...named.values()].map((n) => n.name));
2370
- const isNamed = (n) => named.has(n.id) || namedNames.has(n.name); // the flow's named set holds callables only — transfer the mark to the class
2371
- const list = all
2372
- .filter((n) => !(n.kind !== 'class' && classKey.has(`${n.name}|${n.filePath}`)))
2373
- .sort((a, b) => (isNamed(b) ? 1 : 0) - (isNamed(a) ? 1 : 0))
2374
- .slice(0, 4)
2375
- .map((n) => {
2376
- // Typed-bus convention: the runtime target is the candidate class's
2377
- // Handle/Execute/Consume method — name the exact node, not just the class.
2378
- let display = n.qualifiedName || n.name;
2379
- let at = `${n.filePath}:${n.startLine}`;
2380
- if (keyIsType && n.kind === 'class') {
4117
+ if (sorted.length > 40)
4118
+ lines.push(`- and ${sorted.length - 40} more file(s)`);
4119
+ lines.push('');
4120
+ lines.push(`> Domain file survey complete **${sorted.length}** file(s) listed. ` +
4121
+ 'Exhaustive related-file inventory; no glob/search needed.');
4122
+ lines.push('');
4123
+ return { section: lines.join('\n'), fileCount: sorted.length };
4124
+ }
4125
+ /**
4126
+ * Render a small config/manifest file named in the query (build-profile.json5, ).
4127
+ */
4128
+ buildConfigFileSection(cg, query, projectRoot) {
4129
+ const basenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
4130
+ if (basenames.length === 0)
4131
+ return '';
4132
+ const CONFIG_EXT = /\.(?:json5?|ya?ml|toml|xml|ini|properties)$/i;
4133
+ const rel = (p) => p.replace(/\\/g, '/');
4134
+ const lines = ['**Config / manifest**', ''];
4135
+ let any = false;
4136
+ for (const name of basenames.slice(0, 3)) {
4137
+ let files = [];
4138
+ try {
4139
+ files = cg.searchNodes(name, { kinds: ['file'], limit: 15 });
4140
+ }
4141
+ catch {
4142
+ continue;
4143
+ }
4144
+ for (const r of files) {
4145
+ const base = rel(r.node.filePath).split('/').pop() ?? '';
4146
+ if (!base.toLowerCase().includes(name.toLowerCase()))
4147
+ continue;
4148
+ if (!CONFIG_EXT.test(base))
4149
+ continue;
4150
+ const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
4151
+ if (!abs)
4152
+ continue;
4153
+ let content;
2381
4154
  try {
2382
- const HANDLER_METHODS = /^(handle|handleAsync|execute|executeAsync|consume|consumeAsync|run|__invoke)$/i;
2383
- const method = cg.getOutgoingEdges(n.id)
2384
- .filter((e) => e.kind === 'contains')
2385
- .map((e) => { try {
2386
- return cg.getNode(e.target);
2387
- }
2388
- catch {
2389
- return null;
2390
- } })
2391
- .find((c) => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name));
2392
- if (method) {
2393
- display = `${n.name}.${method.name}`;
2394
- at = `${method.filePath}:${method.startLine}`;
2395
- }
4155
+ content = (0, fs_1.readFileSync)(abs, 'utf-8');
2396
4156
  }
2397
- catch { /* class without resolvable members — show the class itself */ }
4157
+ catch {
4158
+ continue;
4159
+ }
4160
+ if (content.length > 12000)
4161
+ continue;
4162
+ any = true;
4163
+ lines.push(`**\`${rel(r.node.filePath)}\`**`);
4164
+ lines.push('');
4165
+ lines.push('```json5');
4166
+ lines.push(content.replace(/\n+$/, ''));
4167
+ lines.push('```');
4168
+ lines.push('');
2398
4169
  }
2399
- return `\`${display}\` (${at})${isNamed(n) ? ' ← you named this' : ''}`;
2400
- });
2401
- return `candidates for key \`${key}\`: ${list.join(', ')}`;
4170
+ }
4171
+ if (!any)
4172
+ return '';
4173
+ lines.push('> Config content above is verbatim — answer from it; no broad grep needed.');
4174
+ lines.push('');
4175
+ return lines.join('\n');
2402
4176
  }
2403
4177
  /**
2404
4178
  * Compact "blast radius" for the entry symbols of an explore result: who
@@ -2553,12 +4327,22 @@ class ToolHandler {
2553
4327
  * tax on small projects while earning its keep on large ones.
2554
4328
  */
2555
4329
  async handleExplore(args) {
2556
- const query = this.validateString(args.query, 'query');
2557
- if (typeof query !== 'string')
2558
- return query;
4330
+ const rawQuery = this.validateString(args.query, 'query');
4331
+ if (typeof rawQuery !== 'string')
4332
+ return rawQuery;
4333
+ // One normalization point so the flow-builder, relevance search, and
4334
+ // ranking all see the same canonical spelling (Erlang `mod:fn/arity`).
4335
+ const query = normalizeQuerySpelling(rawQuery);
2559
4336
  const cg = this.getHomeGraph(args.projectPath);
2560
4337
  const projectRoot = cg.getProjectRoot();
2561
- // Resolve adaptive output budget from project size. Falls back to the
4338
+ const compactLocal = this.tryFastInventoryExplore(cg, query, projectRoot)
4339
+ ?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
4340
+ if (compactLocal)
4341
+ return compactLocal;
4342
+ const lightMechanism = this.tryLightMechanismExplore(cg, query, projectRoot);
4343
+ if (lightMechanism)
4344
+ return lightMechanism;
4345
+ // Resolve adaptive output budget from project size.
2562
4346
  // largest-tier defaults if stats aren't available, which preserves
2563
4347
  // pre-#185 behavior for callers that hit the rare stats failure.
2564
4348
  let budget;
@@ -2568,20 +4352,72 @@ class ToolHandler {
2568
4352
  catch {
2569
4353
  budget = getExploreOutputBudget(Infinity);
2570
4354
  }
2571
- const maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
4355
+ budget = tightenExploreBudgetForQuery(budget, query);
4356
+ const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
4357
+ let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
4358
+ const queryFileBasenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
4359
+ const interpretationQuery = (0, query_utils_1.queryAsInterpretationSurvey)(query);
4360
+ const testOnlyInterpretation = (0, query_utils_1.queryAsTestOnlyInterpretation)(query);
4361
+ const crossModuleFlow = (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query);
2572
4362
  // Step 1: Find relevant context with generous parameters.
2573
- // Use a large maxNodes budget explore has its own 35k char output limit
2574
- // that prevents context bloat, so more nodes just means better coverage
2575
- // across entry points (especially for large files like Svelte components).
2576
- const subgraph = await cg.findRelevantContext(query, {
2577
- searchLimit: 8,
2578
- traversalDepth: 3,
2579
- maxNodes: 200,
2580
- minScore: 0.2,
2581
- });
4363
+ const contextOpts = interpretationQuery && queryFileBasenames.length === 1
4364
+ ? { searchLimit: 6, traversalDepth: 2, maxNodes: 60, minScore: 0.25 }
4365
+ : { searchLimit: 8, traversalDepth: 3, maxNodes: 200, minScore: 0.2 };
4366
+ const contextQuery = interpretationQuery && queryFileBasenames.length === 1
4367
+ ? `${queryFileBasenames[0]} ${query}`
4368
+ : query;
4369
+ const subgraph = await cg.findRelevantContext(contextQuery, contextOpts);
2582
4370
  if (subgraph.nodes.size === 0) {
2583
4371
  return this.textResult(`No relevant code found for "${query}"`);
2584
4372
  }
4373
+ // Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
4374
+ const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
4375
+ const depSymbols = (0, query_utils_1.extractDependencySymbolsFromQuery)(query);
4376
+ const seedImport = (r, lineText) => {
4377
+ const lineLc = lineText.toLowerCase();
4378
+ if (importTerms.length > 0) {
4379
+ const kitNames = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
4380
+ if (kitNames.length > 0 && !kitNames.some((k) => lineLc.includes(`@kit.${k.toLowerCase()}`))) {
4381
+ return;
4382
+ }
4383
+ }
4384
+ if (depSymbols.length > 0 && !depSymbols.some((s) => lineLc.includes(s.toLowerCase()))) {
4385
+ return;
4386
+ }
4387
+ if (!subgraph.nodes.has(r.node.id)) {
4388
+ subgraph.nodes.set(r.node.id, r.node);
4389
+ subgraph.roots.push(r.node.id);
4390
+ }
4391
+ };
4392
+ for (const sym of depSymbols) {
4393
+ let hits = [];
4394
+ try {
4395
+ hits = cg.searchNodes(sym, { kinds: ['import'], limit: 40 });
4396
+ }
4397
+ catch {
4398
+ continue;
4399
+ }
4400
+ for (const r of hits) {
4401
+ const sig = (r.node.signature || r.node.name || '').trim();
4402
+ seedImport(r, sig);
4403
+ }
4404
+ }
4405
+ for (const term of importTerms) {
4406
+ const termLc = term.toLowerCase().replace(/^@kit\./, '');
4407
+ let hits = [];
4408
+ try {
4409
+ hits = cg.searchNodes(term, { kinds: ['import'], limit: 12 });
4410
+ }
4411
+ catch {
4412
+ continue;
4413
+ }
4414
+ for (const r of hits) {
4415
+ const sig = (r.node.signature || r.node.name || '').trim();
4416
+ if (!sig.toLowerCase().includes(termLc))
4417
+ continue;
4418
+ seedImport(r, sig);
4419
+ }
4420
+ }
2585
4421
  // Graph-aware glue: findRelevantContext builds the subgraph from name/text
2586
4422
  // search, so a method that BRIDGES named symbols — e.g. App.tsx's
2587
4423
  // triggerRender, which calls the named triggerUpdate — is never a search hit
@@ -2639,6 +4475,13 @@ class ToolHandler {
2639
4475
  {
2640
4476
  const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i;
2641
4477
  const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
4478
+ // Named types (BridgeInterceptor, ResponseFormatter) must also seed — the
4479
+ // adaptive sibling-skeleton path needs those files in the subgraph even
4480
+ // when they hold no callable the agent also named.
4481
+ const SEED_KINDS = new Set([
4482
+ ...CALLABLE,
4483
+ 'class', 'struct', 'interface', 'trait', 'protocol', 'constant', 'variable',
4484
+ ]);
2642
4485
  const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
2643
4486
  const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
2644
4487
  const callerCount = (n) => { try {
@@ -2647,9 +4490,28 @@ class ToolHandler {
2647
4490
  catch {
2648
4491
  return 0;
2649
4492
  } };
2650
- const tokens = [...new Set(query.split(/[\s,()[\]]+/)
2651
- .map((t) => t.replace(FILE_EXT, '').trim())
2652
- .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
4493
+ const namedParts = [];
4494
+ for (const kit of (0, query_utils_1.extractKitModuleNamesFromQuery)(query))
4495
+ namedParts.push(kit);
4496
+ for (const base of (0, query_utils_1.extractFileBasenamesFromQuery)(query))
4497
+ namedParts.push(base);
4498
+ for (const ma of (0, query_utils_1.extractMemberAccessFromQuery)(query)) {
4499
+ namedParts.push(ma.member);
4500
+ if (ma.receiver)
4501
+ namedParts.push(ma.receiver);
4502
+ }
4503
+ for (const m of query.matchAll(/\b([A-Za-z_][\w]*)(::)([A-Za-z_][\w]*)\b/g)) {
4504
+ if (m[1])
4505
+ namedParts.push(m[1]);
4506
+ if (m[3])
4507
+ namedParts.push(m[3]);
4508
+ }
4509
+ const tokens = [...new Set([
4510
+ ...namedParts,
4511
+ ...query.split(/[\s,()[\]]+/)
4512
+ .map((t) => t.replace(FILE_EXT, '').trim())
4513
+ .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)),
4514
+ ])].slice(0, 16);
2653
4515
  // PascalCase tokens in the query are type/file disambiguators — when the
2654
4516
  // agent writes "DataRequest task validate", the `task`/`validate` it wants
2655
4517
  // are DataRequest's, NOT the same-named overloads in Validation.swift /
@@ -2672,9 +4534,38 @@ class ToolHandler {
2672
4534
  // homegraph_node's findSymbolMatches.) Qualified tokens keep findAllSymbols.
2673
4535
  const isQual = /[.\/]|::/.test(t);
2674
4536
  const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
2675
- const cands = raw
2676
- .filter((n) => CALLABLE.has(n.kind) && !isTestPath(n.filePath))
2677
- .sort((a, b) => (bodyLines(b) > 1 ? 1 : 0) - (bodyLines(a) > 1 ? 1 : 0) || bodyLines(b) - bodyLines(a));
4537
+ let cands = raw
4538
+ .filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath))
4539
+ .sort((a, b) => {
4540
+ // Prefer callables over types when both share a name, then body size.
4541
+ const ac = CALLABLE.has(a.kind) ? 1 : 0;
4542
+ const bc = CALLABLE.has(b.kind) ? 1 : 0;
4543
+ if (ac !== bc)
4544
+ return bc - ac;
4545
+ return (bodyLines(b) > 1 ? 1 : 0) - (bodyLines(a) > 1 ? 1 : 0) || bodyLines(b) - bodyLines(a);
4546
+ });
4547
+ // Field-name seeding fallback (#1196): a camelCase token that names NO
4548
+ // definition of its own is usually an object-literal key / API field —
4549
+ // seed its camel-infix callable definers instead.
4550
+ if (cands.length === 0 && !isQual && /[a-z][A-Z]/.test(t)) {
4551
+ const lcToken = t.toLowerCase();
4552
+ cands = cg
4553
+ .getNodesByNameSubstring(t, {
4554
+ kinds: ['function', 'method', 'component'],
4555
+ limit: 60,
4556
+ })
4557
+ .filter((n) => CALLABLE.has(n.kind) && !isTestPath(n.filePath))
4558
+ .filter((n) => {
4559
+ const idx = n.name.toLowerCase().indexOf(lcToken);
4560
+ if (idx < 0)
4561
+ return false;
4562
+ if (idx === 0)
4563
+ return n.name.length > t.length;
4564
+ return /[A-Z]/.test(n.name.charAt(idx));
4565
+ })
4566
+ .sort((a, b) => a.name.length - b.name.length)
4567
+ .slice(0, 3);
4568
+ }
2678
4569
  // A specific name (<=3 defs) injects all its defs. An overloaded name
2679
4570
  // (`validate` = 10, `request` = 44) would flood the subgraph, so inject
2680
4571
  // only: the overloads whose file/class the query ALSO names (the agent
@@ -2713,6 +4604,68 @@ class ToolHandler {
2713
4604
  for (const n of tierPicks)
2714
4605
  tierSeedIds.add(n.id);
2715
4606
  }
4607
+ // Mechanism survey: seed query-shaped entry symbols first, then domain search.
4608
+ if ((0, query_utils_1.queryAsMechanismSurvey)(query)) {
4609
+ const STRUCTURE_KINDS = new Set(['class', 'struct', 'interface', 'component']);
4610
+ const implEntryNames = [];
4611
+ for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
4612
+ if (seed.startsWith('@')) {
4613
+ let hits = [];
4614
+ try {
4615
+ hits = cg.searchNodes(seed, { kinds: ['import'], limit: 20 });
4616
+ }
4617
+ catch {
4618
+ continue;
4619
+ }
4620
+ for (const r of hits) {
4621
+ if (isTestPath(r.node.filePath))
4622
+ continue;
4623
+ if (!subgraph.nodes.has(r.node.id))
4624
+ subgraph.nodes.set(r.node.id, r.node);
4625
+ namedSeedIds.add(r.node.id);
4626
+ tierSeedIds.add(r.node.id);
4627
+ }
4628
+ continue;
4629
+ }
4630
+ const nodes = cg.getNodesByName(seed).filter((n) => STRUCTURE_KINDS.has(n.kind) && !isTestPath(n.filePath));
4631
+ for (const n of nodes.slice(0, 3)) {
4632
+ if (!subgraph.nodes.has(n.id))
4633
+ subgraph.nodes.set(n.id, n);
4634
+ namedSeedIds.add(n.id);
4635
+ tierSeedIds.add(n.id);
4636
+ if (!implEntryNames.includes(n.name))
4637
+ implEntryNames.push(n.name);
4638
+ }
4639
+ }
4640
+ if (implEntryNames.length < 2) {
4641
+ const domainTerms = (0, query_utils_1.extractDomainSearchTerms)(query);
4642
+ for (const term of domainTerms.slice(0, 5)) {
4643
+ let hits = [];
4644
+ try {
4645
+ hits = cg.searchNodes(term, { kinds: ['class', 'struct', 'interface'], limit: 30 });
4646
+ }
4647
+ catch {
4648
+ continue;
4649
+ }
4650
+ for (const r of hits) {
4651
+ if (isTestPath(r.node.filePath))
4652
+ continue;
4653
+ if (!(0, query_utils_1.isImplementationEntrySymbol)(r.node.name, domainTerms))
4654
+ continue;
4655
+ if (!subgraph.nodes.has(r.node.id))
4656
+ subgraph.nodes.set(r.node.id, r.node);
4657
+ namedSeedIds.add(r.node.id);
4658
+ tierSeedIds.add(r.node.id);
4659
+ if (!implEntryNames.includes(r.node.name))
4660
+ implEntryNames.push(r.node.name);
4661
+ if (implEntryNames.length >= 6)
4662
+ break;
4663
+ }
4664
+ if (implEntryNames.length >= 6)
4665
+ break;
4666
+ }
4667
+ }
4668
+ }
2716
4669
  }
2717
4670
  // Step 2: Group nodes by file, score by relevance
2718
4671
  const fileGroups = new Map();
@@ -2797,6 +4750,14 @@ class ToolHandler {
2797
4750
  }
2798
4751
  fileGroups.set(node.filePath, group);
2799
4752
  }
4753
+ if (testOnlyInterpretation) {
4754
+ for (const [, group] of fileGroups) {
4755
+ const fp = group.nodes[0]?.filePath ?? '';
4756
+ if (fp && !(0, query_utils_1.isTestFile)(fp) && !(0, query_utils_1.fileMatchesQueryBasename)(fp, queryFileBasenames)) {
4757
+ group.score = Math.max(0, group.score - 50);
4758
+ }
4759
+ }
4760
+ }
2800
4761
  // Only include files that have entry points or nodes directly connected to entry points
2801
4762
  let relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
2802
4763
  // Extract query terms for relevance checking
@@ -2974,9 +4935,16 @@ class ToolHandler {
2974
4935
  const isCorroborated = (fp) => !MULTITERM_OFF &&
2975
4936
  (fileTermHits.get(fp) ?? 0) >= 2 &&
2976
4937
  (entryFiles.has(fp) || centralFiles.has(fp));
4938
+ const queryMemberAccesses = (0, query_utils_1.extractMemberAccessFromQuery)(query);
2977
4939
  const sortedFiles = relevantFiles.sort((a, b) => {
2978
4940
  const aPath = a[0].toLowerCase();
2979
4941
  const bPath = b[0].toLowerCase();
4942
+ // Query-named file (LocationController.ets in the question) before partial
4943
+ // substring matches (control.ets matching "Controller" inside LocationController).
4944
+ const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
4945
+ const bExactBase = (0, query_utils_1.fileMatchesQueryBasename)(b[0], queryFileBasenames) ? 1 : 0;
4946
+ if (aExactBase !== bExactBase)
4947
+ return bExactBase - aExactBase;
2980
4948
  // Agent-named files first (it asked for a symbol defined here by name).
2981
4949
  const aNamed = namedSeedFiles.has(a[0]) ? 1 : 0;
2982
4950
  const bNamed = namedSeedFiles.has(b[0]) ? 1 : 0;
@@ -3029,16 +4997,154 @@ class ToolHandler {
3029
4997
  '',
3030
4998
  ];
3031
4999
  const summaryLineIdx = 2;
3032
- // Blast radius (always-on, compact): for the entry symbols, who depends on
3033
- // them + which tests cover them locations only, no source so the agent
3034
- // knows what to update/verify before editing without a separate call.
3035
- const blastRadius = this.buildBlastRadiusSection(cg, subgraph);
3036
- if (blastRadius)
3037
- lines.push(blastRadius);
3038
- // Relationship map — show how symbols connect
5000
+ if (testOnlyInterpretation) {
5001
+ lines.push('> **Test-file scope only** answer from the named `.test.ets` file below; ' +
5002
+ 'production handlers are out of scope unless explicitly referenced in the test.');
5003
+ lines.push('');
5004
+ }
5005
+ const importResult = this.buildImportSitesSection(cg, query, projectRoot);
5006
+ if (importResult.section)
5007
+ lines.push(importResult.section);
5008
+ const homonymSection = this.buildHomonymDefinitionsSection(cg, query);
5009
+ if (homonymSection)
5010
+ lines.push(homonymSection);
5011
+ const kitUsageResult = (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
5012
+ ? this.buildKitModuleUsageSection(cg, query, projectRoot)
5013
+ : { section: '', symbolCount: 0 };
5014
+ if (kitUsageResult.section)
5015
+ lines.push(kitUsageResult.section);
5016
+ const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query)
5017
+ ? this.buildDomainFileSurveySection(cg, query)
5018
+ : { section: '', fileCount: 0 };
5019
+ if (domainFileResult.section)
5020
+ lines.push(domainFileResult.section);
5021
+ const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
5022
+ ? this.buildApiUsageSection(cg, query, projectRoot)
5023
+ : { section: '', fileCount: 0 };
5024
+ if (apiUsageResult.section)
5025
+ lines.push(apiUsageResult.section);
5026
+ const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
5027
+ ? this.buildDataSourceSection(cg, query)
5028
+ : { section: '', edgeCount: 0 };
5029
+ if (dataSourceResult.section)
5030
+ lines.push(dataSourceResult.section);
5031
+ const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
5032
+ const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query) || crossModuleFlow;
5033
+ const mechanismSurvey = (0, query_utils_1.queryAsMechanismSurvey)(query);
5034
+ // Flow path — computed before omit-source so graph connectivity drives the decision,
5035
+ // not question-text keyword matching. Mechanism/cross-module surveys augment the
5036
+ // query with seeded entry symbol names so buildFlowFromNamedSymbols can connect them.
5037
+ let flowQuery = query;
5038
+ if (crossModuleFlow) {
5039
+ flowQuery = `${query} ${(0, query_utils_1.extractTypeNamesFromQuery)(query).join(' ')}`;
5040
+ }
5041
+ else if (mechanismSurvey) {
5042
+ const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
5043
+ if (seeds.length >= 2) {
5044
+ flowQuery = `${query} ${seeds.join(' ')}`;
5045
+ }
5046
+ else {
5047
+ const entryNames = [...subgraph.nodes.values()]
5048
+ .filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface')
5049
+ && (0, query_utils_1.isImplementationEntrySymbol)(n.name, (0, query_utils_1.extractDomainSearchTerms)(query)))
5050
+ .map((n) => n.name)
5051
+ .slice(0, 6);
5052
+ if (entryNames.length >= 2)
5053
+ flowQuery = `${query} ${entryNames.join(' ')}`;
5054
+ }
5055
+ }
5056
+ const flow = this.buildFlowFromNamedSymbols(cg, flowQuery);
5057
+ const hasFlowPath = flow.pathNodeIds.size > 0;
5058
+ budget = tightenExploreBudgetForQuery(budget, query, { hasFlowPath });
5059
+ // Honor an explicit maxFiles from the caller — budget.defaultMaxFiles is only
5060
+ // a default when the agent didn't ask for more (adaptive sibling tests pass 12).
5061
+ if (!explicitMaxFiles) {
5062
+ maxFiles = Math.min(maxFiles, (0, utils_1.clamp)(budget.defaultMaxFiles, 1, 20));
5063
+ }
5064
+ const localDetail = (0, query_utils_1.queryAsLocalSymbolDetail)(query);
5065
+ const inheritanceSection = !hasFlowPath && !multiAnchor
5066
+ ? this.buildInheritanceSurveySection(cg, query) : '';
5067
+ const callerSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildCallerInventory)(query)
5068
+ ? this.buildCallerListingSection(cg, query) : '';
5069
+ const memberSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildMemberSurvey)(query)
5070
+ ? this.buildMemberSurveySection(cg, query, projectRoot) : '';
5071
+ const configSection = (0, query_utils_1.shouldBuildConfigSection)(query)
5072
+ ? this.buildConfigFileSection(cg, query, projectRoot) : '';
5073
+ if (inheritanceSection)
5074
+ lines.push(inheritanceSection);
5075
+ if (callerSection)
5076
+ lines.push(callerSection);
5077
+ if (memberSection)
5078
+ lines.push(memberSection);
5079
+ if (configSection)
5080
+ lines.push(configSection);
5081
+ const finishCompact = (summary) => {
5082
+ lines[summaryLineIdx] = summary;
5083
+ return this.textResult(lines.join('\n'));
5084
+ };
5085
+ const memberFileCount = memberSection
5086
+ ? memberSection.split('\n').filter((l) => l.startsWith('- ')).length
5087
+ : 0;
5088
+ const callerBulletCount = callerSection
5089
+ ? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
5090
+ : 0;
5091
+ const inheritanceListed = inheritanceSection
5092
+ ? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
5093
+ : false;
5094
+ const omitSource = (0, query_utils_1.shouldOmitSourceBodies)({
5095
+ importSiteCount: importResult.siteCount,
5096
+ hasFilteredImports: importInventoryFilter && importResult.siteCount > 0,
5097
+ callerBulletCount,
5098
+ memberFileCount,
5099
+ apiUsageFileCount: apiUsageResult.fileCount,
5100
+ configRendered: !!configSection,
5101
+ kitModuleSurveyRendered: !!kitUsageResult.section,
5102
+ inheritanceListed,
5103
+ domainFileCount: domainFileResult.fileCount,
5104
+ dataSourceEdgeCount: dataSourceResult.edgeCount,
5105
+ }, hasFlowPath, multiAnchor);
5106
+ if (omitSource) {
5107
+ if (configSection) {
5108
+ return finishCompact('Config/manifest content above — answer from it directly.');
5109
+ }
5110
+ if (kitUsageResult.section) {
5111
+ return finishCompact(`Kit module usage survey — **${kitUsageResult.symbolCount}** imported symbol(s) across the repo. SDK definitions are not in this project; answer from the usage list above.`);
5112
+ }
5113
+ if (domainFileResult.section && domainFileResult.fileCount > 0) {
5114
+ return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above. Source bodies omitted; answer from the inventory.`);
5115
+ }
5116
+ if (apiUsageResult.section && apiUsageResult.fileCount > 0) {
5117
+ return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s) referencing the queried symbol(s). Source bodies omitted; answer from the list above.`);
5118
+ }
5119
+ if (dataSourceResult.section && dataSourceResult.edgeCount > 0) {
5120
+ return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s) listed above. Source bodies omitted; answer from the inventory.`);
5121
+ }
5122
+ if (inheritanceListed) {
5123
+ return finishCompact('Inheritance survey above lists all direct subtypes found — source bodies omitted.');
5124
+ }
5125
+ if (importResult.compactListing) {
5126
+ return finishCompact(`Listed **${importResult.siteCount}** import site(s) for the queried symbol(s). Source bodies omitted — answer from the dependency list above.`);
5127
+ }
5128
+ if (callerBulletCount >= 1) {
5129
+ return finishCompact(`Caller inventory above lists **${callerBulletCount}** method(s) with external callers — source bodies omitted.`);
5130
+ }
5131
+ if (memberSection && memberFileCount >= 2) {
5132
+ return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s) — source bodies omitted; answer from the inventory above.`);
5133
+ }
5134
+ }
5135
+ // Blast radius only for structural flow / mechanism answers — skip on local-detail
5136
+ // and no-flow dumps (it's expensive and rarely changes the answer there).
5137
+ if (!localDetail && (hasFlowPath || mechanismSurvey || crossModuleFlow)) {
5138
+ const blastRadius = this.buildBlastRadiusSection(cg, subgraph);
5139
+ if (blastRadius)
5140
+ lines.push(blastRadius);
5141
+ }
5142
+ // Relationship map — show how symbols connect (skip when no flow path: saves
5143
+ // tokens on survey/dependency/how-to questions that don't need call graphs).
3039
5144
  const significantEdges = subgraph.edges.filter(e => e.kind !== 'contains' // skip contains — it's implied by file grouping
3040
5145
  );
3041
- if (budget.includeRelationships && significantEdges.length > 0) {
5146
+ if (budget.includeRelationships && hasFlowPath && !importResult.compactListing
5147
+ && significantEdges.length > 0) {
3042
5148
  lines.push('**Relationships**');
3043
5149
  lines.push('');
3044
5150
  // Group edges by kind for readability
@@ -3066,10 +5172,7 @@ class ToolHandler {
3066
5172
  }
3067
5173
  }
3068
5174
  // Step 4: Read contiguous file sections
3069
- // Compute the flow spine once used both to prepend the Flow section (below)
3070
- // and to gate adaptive source sizing: files on the spine get full source,
3071
- // off-spine peers skeletonize.
3072
- const flow = this.buildFlowFromNamedSymbols(cg, query);
5175
+ // (flow already computed above for relationship gating and adaptive sizing)
3073
5176
  // Polymorphic-sibling detector for adaptive sizing. A class that implements/
3074
5177
  // extends a supertype shared by >= MIN_SIBLINGS classes is one of many
3075
5178
  // INTERCHANGEABLE implementations (OkHttp's 14 `: Interceptor` classes —
@@ -3126,7 +5229,9 @@ class ToolHandler {
3126
5229
  };
3127
5230
  lines.push('**Source Code**');
3128
5231
  lines.push('');
3129
- lines.push('> The code below is the **verbatim, current on-disk source** of these files re-read from disk on this call and line-numbered, byte-for-byte identical to what the Read tool returns. It is NOT a summary, outline, or stale cache. Treat each block as a Read you have already performed: do not Read a file shown here.');
5232
+ lines.push('> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
5233
+ 'do not re-read/grep these files, and do not fan out `homegraph_node` / search for symbols already shown. ' +
5234
+ 'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
3130
5235
  lines.push('');
3131
5236
  let totalChars = lines.join('\n').length;
3132
5237
  let filesIncluded = 0;
@@ -3134,8 +5239,51 @@ class ToolHandler {
3134
5239
  // (#1046) — it must reflect what we show, not the raw candidate gather.
3135
5240
  const renderedFilePaths = [];
3136
5241
  let anyFileTrimmed = false;
3137
- for (const [filePath, group] of sortedFiles) {
3138
- if (filesIncluded >= maxFiles)
5242
+ const limitSingleFile = (0, query_utils_1.shouldLimitToQueryNamedFile)(query, hasFlowPath, multiAnchor)
5243
+ || (interpretationQuery && queryFileBasenames.length === 1);
5244
+ let filesToRender = sortedFiles;
5245
+ if (limitSingleFile) {
5246
+ const anchored = sortedFiles.filter(([fp]) => (0, query_utils_1.fileMatchesQueryBasename)(fp, queryFileBasenames));
5247
+ if (anchored.length > 0)
5248
+ filesToRender = anchored.slice(0, 1);
5249
+ }
5250
+ else if ((0, query_utils_1.shouldFocusOnNamedTypeFile)(query, hasFlowPath, multiAnchor)) {
5251
+ const typeName = (0, query_utils_1.extractTypeNamesFromQuery)(query)[0];
5252
+ const anchored = sortedFiles.filter(([, group]) => group.nodes.some((n) => n.name === typeName && (n.kind === 'class' || n.kind === 'interface' || n.kind === 'struct')));
5253
+ if (anchored.length > 0)
5254
+ filesToRender = anchored.slice(0, 1);
5255
+ }
5256
+ else if ((0, query_utils_1.shouldFocusOnQueryNamedDefs)(query, hasFlowPath, multiAnchor)) {
5257
+ const nameSet = new Set([
5258
+ ...(0, query_utils_1.extractTypeNamesFromQuery)(query),
5259
+ ...(0, query_utils_1.extractDependencySymbolsFromQuery)(query),
5260
+ ...(0, query_utils_1.extractMemberAccessFromQuery)(query).map((m) => m.member),
5261
+ ]);
5262
+ const anchored = sortedFiles.filter(([, group]) => group.nodes.some((n) => nameSet.has(n.name)));
5263
+ if (anchored.length > 0) {
5264
+ filesToRender = anchored.slice(0, localDetail ? 2 : 3);
5265
+ }
5266
+ }
5267
+ else if (crossModuleFlow && hasFlowPath) {
5268
+ const onSpine = sortedFiles.filter(([, group]) => group.nodes.some((n) => flow.pathNodeIds.has(n.id) || flow.uniqueNamedNodeIds.has(n.id)));
5269
+ if (onSpine.length > 0)
5270
+ filesToRender = onSpine;
5271
+ }
5272
+ const sourceFileCap = limitSingleFile && filesToRender.length === 1
5273
+ ? 1
5274
+ : localDetail
5275
+ ? Math.min(maxFiles, 2)
5276
+ : crossModuleFlow && hasFlowPath
5277
+ ? Math.min(maxFiles, 5)
5278
+ : mechanismSurvey && hasFlowPath
5279
+ ? Math.min(maxFiles, 4)
5280
+ : mechanismSurvey
5281
+ ? Math.min(maxFiles, 3)
5282
+ : !hasFlowPath
5283
+ ? Math.min(maxFiles, 3)
5284
+ : maxFiles;
5285
+ for (const [filePath, group] of filesToRender) {
5286
+ if (filesIncluded >= sourceFileCap)
3139
5287
  break;
3140
5288
  // A file DEFINES a named/spine symbol (the answer) vs merely references the
3141
5289
  // flow. Past 90% budget, stop pulling INCIDENTAL files — but keep scanning
@@ -3431,6 +5579,39 @@ class ToolHandler {
3431
5579
  ranges.push({ start: edge.line, end: edge.line, name: targetName, kind: edge.kind, importance: 2, spine: false });
3432
5580
  }
3433
5581
  }
5582
+ // Query member-access anchors: pin lines the question names (locationManager.on,
5583
+ // .drawModifier) so per-file budget gaps don't hide the exact call site.
5584
+ if (queryMemberAccesses.length > 0) {
5585
+ const anchorLines = new Set();
5586
+ for (const ma of queryMemberAccesses) {
5587
+ const patterns = [];
5588
+ if (ma.receiver) {
5589
+ const recv = ma.receiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5590
+ const mem = ma.member.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5591
+ patterns.push(new RegExp(`${recv}\\.${mem}\\s*\\(`));
5592
+ patterns.push(new RegExp(`${recv}\\.${mem}\\b`));
5593
+ }
5594
+ else if (ma.dotted.startsWith('.')) {
5595
+ const lit = ma.dotted.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5596
+ patterns.push(new RegExp(`${lit}\\b`));
5597
+ }
5598
+ for (let i = 0; i < fileLines.length; i++) {
5599
+ const line = fileLines[i] ?? '';
5600
+ if (patterns.some((p) => p.test(line)))
5601
+ anchorLines.add(i + 1);
5602
+ }
5603
+ }
5604
+ for (const lineNo of anchorLines) {
5605
+ ranges.push({
5606
+ start: lineNo,
5607
+ end: lineNo,
5608
+ name: 'query-anchor',
5609
+ kind: 'anchor',
5610
+ importance: 11,
5611
+ spine: false,
5612
+ });
5613
+ }
5614
+ }
3434
5615
  ranges.sort((a, b) => a.start - b.start);
3435
5616
  if (ranges.length === 0)
3436
5617
  continue;
@@ -3713,11 +5894,17 @@ class ToolHandler {
3713
5894
  const lastSection = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX);
3714
5895
  const boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
3715
5896
  const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
3716
- finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For any area not covered, run another homegraph_explore with the specific names — do NOT Read these files.)';
5897
+ finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For uncovered files/symbols, run another homegraph_explore with their exact names — not grep/read/node for symbols already shown.)';
3717
5898
  }
3718
5899
  else {
3719
5900
  finalText = output;
3720
5901
  }
5902
+ if (!finalText.includes('**Partial result**') && !finalText.includes('> **Explore complete**')) {
5903
+ finalText +=
5904
+ '\n\n> **Explore complete — ANSWER NOW.** Flow + Source above are authoritative for this query. ' +
5905
+ 'Do **not** grep/read/`homegraph_node`/`homegraph_search` for the same symbols or files. ' +
5906
+ 'Only call one tighter `homegraph_explore` if a named symbol essential to the answer is missing.';
5907
+ }
3721
5908
  // Curated header (#1046): substitute the sentinel with the count of files
3722
5909
  // whose source SURVIVES in the final text — not `subgraph`/`fileGroups` (the
3723
5910
  // raw gather a broad query inflates) and not `filesIncluded` (which can
@@ -3807,12 +5994,12 @@ class ToolHandler {
3807
5994
  const list = matches.map((n) => `- \`${n.name}\` (${n.kind}) — ${n.filePath}:${n.startLine}`);
3808
5995
  return this.textResult(this.truncateOutput([header, '', 'Re-query with `includeCode: true` to get every body in one call — no need to pick one first.', '', ...list].join('\n')));
3809
5996
  }
3810
- const BODY_BUDGET = 12000; // leaves room under MAX_OUTPUT_LENGTH for the header + list
5997
+ const BODY_BUDGET = 10_000; // leaves room under MAX_OUTPUT_LENGTH for the header + list
3811
5998
  // The CHAR budget is the real limiter — keep the count cap high so a set of
3812
5999
  // SHORT overloads (Alamofire's 10 `validate` variants, each a few lines) all
3813
6000
  // render in full rather than relegating the one the agent wanted to a
3814
6001
  // bodiless list. Only a set of many LARGE bodies hits the char budget first.
3815
- const HARD_CAP = 16;
6002
+ const HARD_CAP = 12;
3816
6003
  const rendered = [];
3817
6004
  const listed = [];
3818
6005
  let used = 0;
@@ -3948,12 +6135,12 @@ class ToolHandler {
3948
6135
  const fileLines = content.split('\n');
3949
6136
  const total = fileLines.length;
3950
6137
  // Read-parity windowing: `offset`/`limit` mean exactly what they do on Read
3951
- // (1-based start line; max line count). Default: the whole file, capped like
3952
- // Read at 2000 lines and bounded by a char budget that tracks explore's
3953
- // proven-safe ~38k response ceiling. Overflow is stated explicitly (Read
3954
- // paginates too) never the silent 15k truncateOutput chop.
3955
- const CHAR_BUDGET = 38000;
3956
- const DEFAULT_LIMIT = 2000;
6138
+ // (1-based start line; max line count). Default window is intentionally
6139
+ // smaller than explore's flow budget dumping a whole multi-kLOC file from
6140
+ // `homegraph_node` teaches the agent to over-drill and burns tokens.
6141
+ // Overflow is stated explicitly (pass offset/limit or name a symbol).
6142
+ const CHAR_BUDGET = 12_000;
6143
+ const DEFAULT_LIMIT = 400;
3957
6144
  const offset = Math.max(1, opts.offset ?? 1);
3958
6145
  if (offset > total) {
3959
6146
  return this.textResult(`**${filePath}** has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
@@ -3976,8 +6163,9 @@ class ToolHandler {
3976
6163
  const complete = offset === 1 && shownEnd >= total;
3977
6164
  const out = [header, '', ...numbered];
3978
6165
  if (!complete) {
3979
- out.push('', `(lines ${offset}–${shownEnd} of ${total} — pass \`offset\`/\`limit\` for another range, or \`homegraph_node <symbol>\` for one symbol in full)`);
6166
+ out.push('', `(lines ${offset}–${shownEnd} of ${total} — pass \`offset\`/\`limit\` for another range, or \`homegraph_node\` with a symbol name for one body in full)`);
3980
6167
  }
6168
+ out.push('', '> Treat this source as already Read. Prefer answering now; do not grep/read the same file or fan out more `homegraph_node` calls for symbols listed above.');
3981
6169
  // Self-bounded to CHAR_BUDGET — do NOT route through truncateOutput (15k).
3982
6170
  return this.textResult(out.join('\n'));
3983
6171
  }
@@ -4071,20 +6259,28 @@ class ToolHandler {
4071
6259
  lines.push(`> ⚠ ${(0, worktree_1.worktreeMismatchWarning)(mismatch).replace(/\n/g, '\n> ')}`, '');
4072
6260
  }
4073
6261
  lines.push(`**Files indexed:** ${stats.fileCount}`, `**Total nodes:** ${stats.nodeCount}`, `**Total edges:** ${stats.edgeCount}`, `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
4074
- // Surface the active SQLite backend (node:sqlite, Node's built-in real
4075
- // SQLite full WAL + FTS5, no native build).
4076
- lines.push(`**Backend:** node:sqlite (Node built-in) — full WAL + FTS5`);
6262
+ // Surface the active SQLite backend: node:sqlite better-sqlite3 → wasm.
6263
+ const backend = cg.getBackend();
6264
+ if (backend === 'node-sqlite') {
6265
+ lines.push(`**Backend:** node-sqlite (built-in)`);
6266
+ }
6267
+ else if (backend === 'native') {
6268
+ lines.push(`**Backend:** native (better-sqlite3)`);
6269
+ }
6270
+ else {
6271
+ lines.push(`**Backend:** ⚠ wasm (no WAL backend available) — ` +
6272
+ `5-10x slower than WAL. Fix: ${sqlite_adapter_1.WASM_FALLBACK_FIX_RECIPE}`);
6273
+ }
4077
6274
  // Effective journal mode. 'wal' ⇒ concurrent reads never block on a writer;
4078
- // anything else ⇒ they can ("database is locked"). node:sqlite supports WAL
4079
- // everywhere, so a non-wal mode means the filesystem can't (network/
4080
- // virtualized mounts, WSL2 /mnt). See issue #238.
6275
+ // anything else ⇒ they can ("database is locked"). node:sqlite / native
6276
+ // support WAL; wasm remaps to DELETE.
4081
6277
  const journalMode = cg.getJournalMode();
4082
6278
  if (journalMode === 'wal') {
4083
6279
  lines.push(`**Journal mode:** wal (concurrent reads safe)`);
4084
6280
  }
4085
6281
  else {
4086
6282
  lines.push(`**Journal mode:** ⚠ ${journalMode || 'unknown'} — WAL not active, so reads ` +
4087
- `can block on a concurrent write (WAL appears unsupported on this filesystem)`);
6283
+ `can block on a concurrent write`);
4088
6284
  }
4089
6285
  lines.push('', '**Nodes by Kind:**');
4090
6286
  for (const [kind, count] of Object.entries(stats.nodesByKind)) {
@@ -4300,7 +6496,6 @@ class ToolHandler {
4300
6496
  if (typeof query !== 'string')
4301
6497
  return query;
4302
6498
  const repoPath = args.repoPath;
4303
- const explicitDbPath = args.dbPath;
4304
6499
  const topKRaw = Number(args.topK);
4305
6500
  const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 5 : topKRaw, 50));
4306
6501
  const includeFragments = args.includeFragments !== false;
@@ -4310,7 +6505,7 @@ class ToolHandler {
4310
6505
  const { searchAndGetContext } = require('../spec/graph/queries');
4311
6506
  const { truncateCodeDiff, truncateSubtitles, computeBudgetProfile, } = require('../spec/utils');
4312
6507
  // Resolve the database path.
4313
- const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
6508
+ const dbPath = resolveDbPath(repoPath || process.cwd());
4314
6509
  // Open the database.
4315
6510
  let db;
4316
6511
  try {
@@ -4410,13 +6605,12 @@ class ToolHandler {
4410
6605
  if (typeof filePath !== 'string')
4411
6606
  return filePath;
4412
6607
  const repoPath = args.repoPath;
4413
- const explicitDbPath = args.dbPath;
4414
6608
  // Lazily require spec modules so the MCP startup path stays lean.
4415
6609
  const { resolveDbPath } = require('../spec/utils');
4416
6610
  const { createDatabase } = require('../db/sqlite-adapter');
4417
6611
  const { findSpecsByFilePath } = require('../spec/graph/queries');
4418
6612
  // Resolve the database path.
4419
- const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
6613
+ const dbPath = resolveDbPath(repoPath || process.cwd());
4420
6614
  // Open the database.
4421
6615
  let db;
4422
6616
  try {
@@ -4477,7 +6671,6 @@ class ToolHandler {
4477
6671
  const file = fileRaw;
4478
6672
  const line = typeof args.line === 'number' ? args.line : undefined;
4479
6673
  const repoPath = args.repoPath;
4480
- const explicitDbPath = args.dbPath;
4481
6674
  const topKRaw = Number(args.topK);
4482
6675
  const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 10 : topKRaw, 50));
4483
6676
  // Lazily require all needed modules
@@ -4532,7 +6725,7 @@ class ToolHandler {
4532
6725
  }, null, 2));
4533
6726
  }
4534
6727
  // Step 2: Resolve the Spec database path
4535
- const dbPath = resolveDbPath(repoPath || process.cwd(), explicitDbPath);
6728
+ const dbPath = resolveDbPath(repoPath || process.cwd());
4536
6729
  // Step 3: Open the Spec database
4537
6730
  let db;
4538
6731
  try {
@@ -4744,25 +6937,47 @@ class ToolHandler {
4744
6937
  if (tail && tail !== symbol)
4745
6938
  results = cg.searchNodes(tail, { limit: 50 });
4746
6939
  }
4747
- if (results.length === 0) {
6940
+ const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
6941
+ // Prefer exact name/qualified matches. Never fall back to the top FTS
6942
+ // hit — that resolved unrelated symbols (e.g. OnSurfaceChangedCB → a
6943
+ // function whose body merely mentions "surface") and poisoned callees.
6944
+ let pool = exactMatches.map((r) => r.node);
6945
+ if (pool.length === 0) {
6946
+ try {
6947
+ pool = cg.getNodesByName(symbol).filter((n) => this.matchesSymbol(n, symbol));
6948
+ }
6949
+ catch {
6950
+ pool = [];
6951
+ }
6952
+ }
6953
+ if (pool.length === 0 && /[.\/]|::/.test(symbol)) {
6954
+ const tail = lastQualifierPart(symbol);
6955
+ if (tail && tail !== symbol) {
6956
+ try {
6957
+ pool = cg.getNodesByName(tail).filter((n) => this.matchesSymbol(n, symbol) || this.matchesSymbol(n, tail));
6958
+ }
6959
+ catch {
6960
+ pool = [];
6961
+ }
6962
+ }
6963
+ }
6964
+ if (pool.length === 0) {
4748
6965
  return { nodes: [], note: '' };
4749
6966
  }
4750
- const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
4751
- if (exactMatches.length <= 1) {
4752
- const node = exactMatches[0]?.node ?? results[0].node;
4753
- return { nodes: [node], note: '' };
6967
+ if (pool.length === 1) {
6968
+ return { nodes: pool, note: '' };
4754
6969
  }
4755
6970
  // Same generated-file down-rank as findSymbol — keeps callers/callees
4756
6971
  // /impact aggregation aligned (a query against "Send" returns the
4757
6972
  // hand-written implementations before the protobuf scaffold).
4758
- const ranked = [...exactMatches].sort((a, b) => {
4759
- const aGen = (0, generated_detection_1.isGeneratedFile)(a.node.filePath) ? 1 : 0;
4760
- const bGen = (0, generated_detection_1.isGeneratedFile)(b.node.filePath) ? 1 : 0;
6973
+ const ranked = [...pool].sort((a, b) => {
6974
+ const aGen = (0, generated_detection_1.isGeneratedFile)(a.filePath) ? 1 : 0;
6975
+ const bGen = (0, generated_detection_1.isGeneratedFile)(b.filePath) ? 1 : 0;
4761
6976
  return aGen - bGen;
4762
6977
  });
4763
- const locations = ranked.map(r => `${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`);
6978
+ const locations = ranked.map(n => `${n.kind} at ${n.filePath}:${n.startLine}`);
4764
6979
  const note = `\n\n> **Note:** Aggregated results across ${ranked.length} symbols named "${symbol}": ${locations.join(', ')}`;
4765
- return { nodes: ranked.map(r => r.node), note };
6980
+ return { nodes: ranked, note };
4766
6981
  }
4767
6982
  /**
4768
6983
  * Truncate output if it exceeds the maximum length