homegraph 1.5.6 → 1.5.8

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/CHANGELOG.md +53 -0
  2. package/README.md +12 -6
  3. package/dist/bin/homegraph.js +16 -14
  4. package/dist/context/index.js +120 -15
  5. package/dist/db/index.d.ts +1 -1
  6. package/dist/db/index.js +11 -25
  7. package/dist/db/queries.d.ts +5 -1
  8. package/dist/db/queries.js +31 -2
  9. package/dist/db/sqlite-adapter.d.ts +6 -12
  10. package/dist/db/sqlite-adapter.js +13 -41
  11. package/dist/directory.d.ts +1 -1
  12. package/dist/directory.js +25 -1
  13. package/dist/extraction/grammars.d.ts +2 -0
  14. package/dist/extraction/grammars.js +7 -7
  15. package/dist/extraction/languages/arkts.d.ts +25 -3
  16. package/dist/extraction/languages/arkts.js +184 -22
  17. package/dist/graph/evidence-paths.d.ts +63 -0
  18. package/dist/graph/evidence-paths.js +245 -0
  19. package/dist/index.d.ts +6 -1
  20. package/dist/index.js +55 -17
  21. package/dist/mcp/arkts-evidence-packs.d.ts +67 -0
  22. package/dist/mcp/arkts-evidence-packs.js +352 -0
  23. package/dist/mcp/daemon.js +1 -1
  24. package/dist/mcp/engine.d.ts +20 -7
  25. package/dist/mcp/engine.js +113 -106
  26. package/dist/mcp/evidence-rendering.d.ts +10 -0
  27. package/dist/mcp/evidence-rendering.js +71 -0
  28. package/dist/mcp/explore-repeat-guard.d.ts +8 -13
  29. package/dist/mcp/explore-repeat-guard.js +54 -114
  30. package/dist/mcp/explore-session-state.d.ts +26 -4
  31. package/dist/mcp/explore-session-state.js +50 -4
  32. package/dist/mcp/index-availability.d.ts +23 -0
  33. package/dist/mcp/index-availability.js +78 -0
  34. package/dist/mcp/index.js +5 -4
  35. package/dist/mcp/query-cache.d.ts +1 -1
  36. package/dist/mcp/query-cache.js +90 -3
  37. package/dist/mcp/query-worker.d.ts +1 -1
  38. package/dist/mcp/query-worker.js +1 -1
  39. package/dist/mcp/server-instructions.d.ts +4 -9
  40. package/dist/mcp/server-instructions.js +34 -61
  41. package/dist/mcp/session.js +12 -4
  42. package/dist/mcp/source-slice-identity.d.ts +3 -0
  43. package/dist/mcp/source-slice-identity.js +11 -0
  44. package/dist/mcp/tools.d.ts +43 -9
  45. package/dist/mcp/tools.js +859 -155
  46. package/dist/search/literal-evidence.d.ts +44 -0
  47. package/dist/search/literal-evidence.js +278 -0
  48. package/dist/search/query-plan-provider.d.ts +6 -0
  49. package/dist/search/query-plan-provider.js +289 -0
  50. package/dist/search/query-plan.d.ts +98 -0
  51. package/dist/search/query-plan.js +334 -0
  52. package/dist/search/query-utils.d.ts +6 -1
  53. package/dist/search/query-utils.js +64 -5
  54. package/dist/spec/llm/client.js +3 -15
  55. package/dist/spec/llm/openai-sdk.d.ts +14 -0
  56. package/dist/spec/llm/openai-sdk.js +37 -0
  57. package/dist/spec/llm/retry.js +8 -9
  58. package/dist/types.d.ts +17 -0
  59. package/dist/utils.d.ts +5 -0
  60. package/dist/utils.js +24 -0
  61. package/package.json +3 -5
  62. package/dist/addons/dynamic-import.d.ts.map +0 -1
  63. package/dist/addons/dynamic-import.js.map +0 -1
  64. package/dist/addons/init-template.d.ts.map +0 -1
  65. package/dist/addons/init-template.js.map +0 -1
  66. package/dist/addons/loader.d.ts.map +0 -1
  67. package/dist/addons/loader.js.map +0 -1
  68. package/dist/addons/manager.d.ts.map +0 -1
  69. package/dist/addons/manager.js.map +0 -1
  70. package/dist/addons/paths.d.ts.map +0 -1
  71. package/dist/addons/paths.js.map +0 -1
  72. package/dist/addons/registry.d.ts.map +0 -1
  73. package/dist/addons/registry.js.map +0 -1
  74. package/dist/addons/semver.d.ts.map +0 -1
  75. package/dist/addons/semver.js.map +0 -1
  76. package/dist/addons/types.d.ts.map +0 -1
  77. package/dist/addons/types.js.map +0 -1
  78. package/dist/addons/validate.d.ts.map +0 -1
  79. package/dist/addons/validate.js.map +0 -1
  80. package/dist/arkui/index.d.ts.map +0 -1
  81. package/dist/arkui/index.js.map +0 -1
  82. package/dist/arkui/migrate-passage.d.ts.map +0 -1
  83. package/dist/arkui/migrate-passage.js.map +0 -1
  84. package/dist/arkui/migrate-semantics.d.ts.map +0 -1
  85. package/dist/arkui/migrate-semantics.js.map +0 -1
  86. package/dist/arkui/migrate-snapshot.d.ts.map +0 -1
  87. package/dist/arkui/migrate-snapshot.js.map +0 -1
  88. package/dist/bin/addon-commands.d.ts.map +0 -1
  89. package/dist/bin/addon-commands.js.map +0 -1
  90. package/dist/bin/command-supervision.d.ts.map +0 -1
  91. package/dist/bin/command-supervision.js.map +0 -1
  92. package/dist/bin/fatal-handler.d.ts.map +0 -1
  93. package/dist/bin/fatal-handler.js.map +0 -1
  94. package/dist/bin/homegraph.d.ts.map +0 -1
  95. package/dist/bin/homegraph.js.map +0 -1
  96. package/dist/bin/node-version-check.d.ts.map +0 -1
  97. package/dist/bin/node-version-check.js.map +0 -1
  98. package/dist/bin/uninstall.d.ts.map +0 -1
  99. package/dist/bin/uninstall.js.map +0 -1
  100. package/dist/context/formatter.d.ts.map +0 -1
  101. package/dist/context/formatter.js.map +0 -1
  102. package/dist/context/index.d.ts.map +0 -1
  103. package/dist/context/index.js.map +0 -1
  104. package/dist/context/markers.d.ts.map +0 -1
  105. package/dist/context/markers.js.map +0 -1
  106. package/dist/db/index.d.ts.map +0 -1
  107. package/dist/db/index.js.map +0 -1
  108. package/dist/db/migrations.d.ts.map +0 -1
  109. package/dist/db/migrations.js.map +0 -1
  110. package/dist/db/queries.d.ts.map +0 -1
  111. package/dist/db/queries.js.map +0 -1
  112. package/dist/db/sqlite-adapter.d.ts.map +0 -1
  113. package/dist/db/sqlite-adapter.js.map +0 -1
  114. package/dist/db/wal-valve.d.ts.map +0 -1
  115. package/dist/db/wal-valve.js.map +0 -1
  116. package/dist/directory.d.ts.map +0 -1
  117. package/dist/directory.js.map +0 -1
  118. package/dist/errors.d.ts.map +0 -1
  119. package/dist/errors.js.map +0 -1
  120. package/dist/extraction/arkts-batch-worker.d.ts.map +0 -1
  121. package/dist/extraction/arkts-batch-worker.js.map +0 -1
  122. package/dist/extraction/astro-extractor.d.ts.map +0 -1
  123. package/dist/extraction/astro-extractor.js.map +0 -1
  124. package/dist/extraction/cfml-extractor.d.ts.map +0 -1
  125. package/dist/extraction/cfml-extractor.js.map +0 -1
  126. package/dist/extraction/context.d.ts.map +0 -1
  127. package/dist/extraction/context.js.map +0 -1
  128. package/dist/extraction/default-ignore.d.ts.map +0 -1
  129. package/dist/extraction/default-ignore.js.map +0 -1
  130. package/dist/extraction/dfm-extractor.d.ts.map +0 -1
  131. package/dist/extraction/dfm-extractor.js.map +0 -1
  132. package/dist/extraction/extraction-version.d.ts.map +0 -1
  133. package/dist/extraction/extraction-version.js.map +0 -1
  134. package/dist/extraction/function-ref.d.ts.map +0 -1
  135. package/dist/extraction/function-ref.js.map +0 -1
  136. package/dist/extraction/generated-detection.d.ts.map +0 -1
  137. package/dist/extraction/generated-detection.js.map +0 -1
  138. package/dist/extraction/grammars.d.ts.map +0 -1
  139. package/dist/extraction/grammars.js.map +0 -1
  140. package/dist/extraction/index.d.ts.map +0 -1
  141. package/dist/extraction/index.js.map +0 -1
  142. package/dist/extraction/languages/arkts.d.ts.map +0 -1
  143. package/dist/extraction/languages/arkts.js.map +0 -1
  144. package/dist/extraction/languages/c-cpp.d.ts.map +0 -1
  145. package/dist/extraction/languages/c-cpp.js.map +0 -1
  146. package/dist/extraction/languages/cfquery.d.ts.map +0 -1
  147. package/dist/extraction/languages/cfquery.js.map +0 -1
  148. package/dist/extraction/languages/cfscript.d.ts.map +0 -1
  149. package/dist/extraction/languages/cfscript.js.map +0 -1
  150. package/dist/extraction/languages/cobol.d.ts.map +0 -1
  151. package/dist/extraction/languages/cobol.js.map +0 -1
  152. package/dist/extraction/languages/csharp.d.ts.map +0 -1
  153. package/dist/extraction/languages/csharp.js.map +0 -1
  154. package/dist/extraction/languages/dart.d.ts.map +0 -1
  155. package/dist/extraction/languages/dart.js.map +0 -1
  156. package/dist/extraction/languages/erlang.d.ts.map +0 -1
  157. package/dist/extraction/languages/erlang.js.map +0 -1
  158. package/dist/extraction/languages/go.d.ts.map +0 -1
  159. package/dist/extraction/languages/go.js.map +0 -1
  160. package/dist/extraction/languages/index.d.ts.map +0 -1
  161. package/dist/extraction/languages/index.js.map +0 -1
  162. package/dist/extraction/languages/java.d.ts.map +0 -1
  163. package/dist/extraction/languages/java.js.map +0 -1
  164. package/dist/extraction/languages/javascript.d.ts.map +0 -1
  165. package/dist/extraction/languages/javascript.js.map +0 -1
  166. package/dist/extraction/languages/kotlin.d.ts.map +0 -1
  167. package/dist/extraction/languages/kotlin.js.map +0 -1
  168. package/dist/extraction/languages/lua.d.ts.map +0 -1
  169. package/dist/extraction/languages/lua.js.map +0 -1
  170. package/dist/extraction/languages/luau.d.ts.map +0 -1
  171. package/dist/extraction/languages/luau.js.map +0 -1
  172. package/dist/extraction/languages/nix.d.ts.map +0 -1
  173. package/dist/extraction/languages/nix.js.map +0 -1
  174. package/dist/extraction/languages/objc.d.ts.map +0 -1
  175. package/dist/extraction/languages/objc.js.map +0 -1
  176. package/dist/extraction/languages/pascal.d.ts.map +0 -1
  177. package/dist/extraction/languages/pascal.js.map +0 -1
  178. package/dist/extraction/languages/php.d.ts.map +0 -1
  179. package/dist/extraction/languages/php.js.map +0 -1
  180. package/dist/extraction/languages/python.d.ts.map +0 -1
  181. package/dist/extraction/languages/python.js.map +0 -1
  182. package/dist/extraction/languages/r.d.ts.map +0 -1
  183. package/dist/extraction/languages/r.js.map +0 -1
  184. package/dist/extraction/languages/ruby.d.ts.map +0 -1
  185. package/dist/extraction/languages/ruby.js.map +0 -1
  186. package/dist/extraction/languages/rust.d.ts.map +0 -1
  187. package/dist/extraction/languages/rust.js.map +0 -1
  188. package/dist/extraction/languages/scala.d.ts.map +0 -1
  189. package/dist/extraction/languages/scala.js.map +0 -1
  190. package/dist/extraction/languages/solidity.d.ts.map +0 -1
  191. package/dist/extraction/languages/solidity.js.map +0 -1
  192. package/dist/extraction/languages/swift.d.ts.map +0 -1
  193. package/dist/extraction/languages/swift.js.map +0 -1
  194. package/dist/extraction/languages/terraform.d.ts.map +0 -1
  195. package/dist/extraction/languages/terraform.js.map +0 -1
  196. package/dist/extraction/languages/typescript.d.ts.map +0 -1
  197. package/dist/extraction/languages/typescript.js.map +0 -1
  198. package/dist/extraction/languages/vbnet.d.ts.map +0 -1
  199. package/dist/extraction/languages/vbnet.js.map +0 -1
  200. package/dist/extraction/liquid-extractor.d.ts.map +0 -1
  201. package/dist/extraction/liquid-extractor.js.map +0 -1
  202. package/dist/extraction/mybatis-extractor.d.ts.map +0 -1
  203. package/dist/extraction/mybatis-extractor.js.map +0 -1
  204. package/dist/extraction/parse-pool.d.ts.map +0 -1
  205. package/dist/extraction/parse-pool.js.map +0 -1
  206. package/dist/extraction/parse-worker.d.ts.map +0 -1
  207. package/dist/extraction/parse-worker.js.map +0 -1
  208. package/dist/extraction/razor-extractor.d.ts.map +0 -1
  209. package/dist/extraction/razor-extractor.js.map +0 -1
  210. package/dist/extraction/store-worker.d.ts.map +0 -1
  211. package/dist/extraction/store-worker.js.map +0 -1
  212. package/dist/extraction/store-writer.d.ts.map +0 -1
  213. package/dist/extraction/store-writer.js.map +0 -1
  214. package/dist/extraction/svelte-extractor.d.ts.map +0 -1
  215. package/dist/extraction/svelte-extractor.js.map +0 -1
  216. package/dist/extraction/tree-sitter-helpers.d.ts.map +0 -1
  217. package/dist/extraction/tree-sitter-helpers.js.map +0 -1
  218. package/dist/extraction/tree-sitter-types.d.ts.map +0 -1
  219. package/dist/extraction/tree-sitter-types.js.map +0 -1
  220. package/dist/extraction/tree-sitter.d.ts.map +0 -1
  221. package/dist/extraction/tree-sitter.js.map +0 -1
  222. package/dist/extraction/vue-extractor.d.ts.map +0 -1
  223. package/dist/extraction/vue-extractor.js.map +0 -1
  224. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  225. package/dist/extraction/wasm-runtime-flags.d.ts.map +0 -1
  226. package/dist/extraction/wasm-runtime-flags.js.map +0 -1
  227. package/dist/graph/index.d.ts.map +0 -1
  228. package/dist/graph/index.js.map +0 -1
  229. package/dist/graph/queries.d.ts.map +0 -1
  230. package/dist/graph/queries.js.map +0 -1
  231. package/dist/graph/traversal.d.ts.map +0 -1
  232. package/dist/graph/traversal.js.map +0 -1
  233. package/dist/graph-sources.d.ts.map +0 -1
  234. package/dist/graph-sources.js.map +0 -1
  235. package/dist/index.d.ts.map +0 -1
  236. package/dist/index.js.map +0 -1
  237. package/dist/installer/config-writer.d.ts.map +0 -1
  238. package/dist/installer/config-writer.js.map +0 -1
  239. package/dist/installer/index.d.ts.map +0 -1
  240. package/dist/installer/index.js.map +0 -1
  241. package/dist/installer/instructions-template.d.ts.map +0 -1
  242. package/dist/installer/instructions-template.js.map +0 -1
  243. package/dist/installer/targets/antigravity.d.ts.map +0 -1
  244. package/dist/installer/targets/antigravity.js.map +0 -1
  245. package/dist/installer/targets/claude.d.ts.map +0 -1
  246. package/dist/installer/targets/claude.js.map +0 -1
  247. package/dist/installer/targets/codebuddy.d.ts.map +0 -1
  248. package/dist/installer/targets/codebuddy.js.map +0 -1
  249. package/dist/installer/targets/codex.d.ts.map +0 -1
  250. package/dist/installer/targets/codex.js.map +0 -1
  251. package/dist/installer/targets/cursor.d.ts.map +0 -1
  252. package/dist/installer/targets/cursor.js.map +0 -1
  253. package/dist/installer/targets/deveco.d.ts.map +0 -1
  254. package/dist/installer/targets/deveco.js.map +0 -1
  255. package/dist/installer/targets/gemini.d.ts.map +0 -1
  256. package/dist/installer/targets/gemini.js.map +0 -1
  257. package/dist/installer/targets/hermes.d.ts.map +0 -1
  258. package/dist/installer/targets/hermes.js.map +0 -1
  259. package/dist/installer/targets/kiro.d.ts.map +0 -1
  260. package/dist/installer/targets/kiro.js.map +0 -1
  261. package/dist/installer/targets/opencode.d.ts.map +0 -1
  262. package/dist/installer/targets/opencode.js.map +0 -1
  263. package/dist/installer/targets/registry.d.ts.map +0 -1
  264. package/dist/installer/targets/registry.js.map +0 -1
  265. package/dist/installer/targets/shared.d.ts.map +0 -1
  266. package/dist/installer/targets/shared.js.map +0 -1
  267. package/dist/installer/targets/toml.d.ts.map +0 -1
  268. package/dist/installer/targets/toml.js.map +0 -1
  269. package/dist/installer/targets/types.d.ts.map +0 -1
  270. package/dist/installer/targets/types.js.map +0 -1
  271. package/dist/mcp/daemon-manager.d.ts.map +0 -1
  272. package/dist/mcp/daemon-manager.js.map +0 -1
  273. package/dist/mcp/daemon-paths.d.ts.map +0 -1
  274. package/dist/mcp/daemon-paths.js.map +0 -1
  275. package/dist/mcp/daemon-registry.d.ts.map +0 -1
  276. package/dist/mcp/daemon-registry.js.map +0 -1
  277. package/dist/mcp/daemon.d.ts.map +0 -1
  278. package/dist/mcp/daemon.js.map +0 -1
  279. package/dist/mcp/diff-impact.d.ts.map +0 -1
  280. package/dist/mcp/diff-impact.js.map +0 -1
  281. package/dist/mcp/dynamic-boundaries.d.ts.map +0 -1
  282. package/dist/mcp/dynamic-boundaries.js.map +0 -1
  283. package/dist/mcp/early-ppid.d.ts.map +0 -1
  284. package/dist/mcp/early-ppid.js.map +0 -1
  285. package/dist/mcp/engine.d.ts.map +0 -1
  286. package/dist/mcp/engine.js.map +0 -1
  287. package/dist/mcp/explore-dedup.d.ts.map +0 -1
  288. package/dist/mcp/explore-dedup.js.map +0 -1
  289. package/dist/mcp/explore-repeat-guard.d.ts.map +0 -1
  290. package/dist/mcp/explore-repeat-guard.js.map +0 -1
  291. package/dist/mcp/explore-session-state.d.ts.map +0 -1
  292. package/dist/mcp/explore-session-state.js.map +0 -1
  293. package/dist/mcp/index.d.ts.map +0 -1
  294. package/dist/mcp/index.js.map +0 -1
  295. package/dist/mcp/liveness-watchdog.d.ts.map +0 -1
  296. package/dist/mcp/liveness-watchdog.js.map +0 -1
  297. package/dist/mcp/memory-budget.d.ts.map +0 -1
  298. package/dist/mcp/memory-budget.js.map +0 -1
  299. package/dist/mcp/ppid-watchdog.d.ts.map +0 -1
  300. package/dist/mcp/ppid-watchdog.js.map +0 -1
  301. package/dist/mcp/proxy.d.ts.map +0 -1
  302. package/dist/mcp/proxy.js.map +0 -1
  303. package/dist/mcp/query-cache.d.ts.map +0 -1
  304. package/dist/mcp/query-cache.js.map +0 -1
  305. package/dist/mcp/query-pool.d.ts.map +0 -1
  306. package/dist/mcp/query-pool.js.map +0 -1
  307. package/dist/mcp/query-worker.d.ts.map +0 -1
  308. package/dist/mcp/query-worker.js.map +0 -1
  309. package/dist/mcp/server-instructions.d.ts.map +0 -1
  310. package/dist/mcp/server-instructions.js.map +0 -1
  311. package/dist/mcp/session.d.ts.map +0 -1
  312. package/dist/mcp/session.js.map +0 -1
  313. package/dist/mcp/startup-handshake.d.ts.map +0 -1
  314. package/dist/mcp/startup-handshake.js.map +0 -1
  315. package/dist/mcp/stdin-teardown.d.ts.map +0 -1
  316. package/dist/mcp/stdin-teardown.js.map +0 -1
  317. package/dist/mcp/tools.d.ts.map +0 -1
  318. package/dist/mcp/tools.js.map +0 -1
  319. package/dist/mcp/transport.d.ts.map +0 -1
  320. package/dist/mcp/transport.js.map +0 -1
  321. package/dist/mcp/version.d.ts.map +0 -1
  322. package/dist/mcp/version.js.map +0 -1
  323. package/dist/project-config.d.ts.map +0 -1
  324. package/dist/project-config.js.map +0 -1
  325. package/dist/project-map/index.d.ts.map +0 -1
  326. package/dist/project-map/index.js.map +0 -1
  327. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +0 -1
  328. package/dist/resolution/c-fnptr-synthesizer.js.map +0 -1
  329. package/dist/resolution/callback-synthesizer.d.ts.map +0 -1
  330. package/dist/resolution/callback-synthesizer.js.map +0 -1
  331. package/dist/resolution/cooperative-yield.d.ts.map +0 -1
  332. package/dist/resolution/cooperative-yield.js.map +0 -1
  333. package/dist/resolution/frameworks/arkts-entry.d.ts.map +0 -1
  334. package/dist/resolution/frameworks/arkts-entry.js.map +0 -1
  335. package/dist/resolution/frameworks/arkts-napi.d.ts.map +0 -1
  336. package/dist/resolution/frameworks/arkts-napi.js.map +0 -1
  337. package/dist/resolution/frameworks/astro.d.ts.map +0 -1
  338. package/dist/resolution/frameworks/astro.js.map +0 -1
  339. package/dist/resolution/frameworks/cargo-workspace.d.ts.map +0 -1
  340. package/dist/resolution/frameworks/cargo-workspace.js.map +0 -1
  341. package/dist/resolution/frameworks/cics.d.ts.map +0 -1
  342. package/dist/resolution/frameworks/cics.js.map +0 -1
  343. package/dist/resolution/frameworks/csharp.d.ts.map +0 -1
  344. package/dist/resolution/frameworks/csharp.js.map +0 -1
  345. package/dist/resolution/frameworks/drupal.d.ts.map +0 -1
  346. package/dist/resolution/frameworks/drupal.js.map +0 -1
  347. package/dist/resolution/frameworks/expo-modules.d.ts.map +0 -1
  348. package/dist/resolution/frameworks/expo-modules.js.map +0 -1
  349. package/dist/resolution/frameworks/express.d.ts.map +0 -1
  350. package/dist/resolution/frameworks/express.js.map +0 -1
  351. package/dist/resolution/frameworks/fabric.d.ts.map +0 -1
  352. package/dist/resolution/frameworks/fabric.js.map +0 -1
  353. package/dist/resolution/frameworks/go.d.ts.map +0 -1
  354. package/dist/resolution/frameworks/go.js.map +0 -1
  355. package/dist/resolution/frameworks/goframe.d.ts.map +0 -1
  356. package/dist/resolution/frameworks/goframe.js.map +0 -1
  357. package/dist/resolution/frameworks/index.d.ts.map +0 -1
  358. package/dist/resolution/frameworks/index.js.map +0 -1
  359. package/dist/resolution/frameworks/java.d.ts.map +0 -1
  360. package/dist/resolution/frameworks/java.js.map +0 -1
  361. package/dist/resolution/frameworks/laravel.d.ts.map +0 -1
  362. package/dist/resolution/frameworks/laravel.js.map +0 -1
  363. package/dist/resolution/frameworks/nestjs.d.ts.map +0 -1
  364. package/dist/resolution/frameworks/nestjs.js.map +0 -1
  365. package/dist/resolution/frameworks/play.d.ts.map +0 -1
  366. package/dist/resolution/frameworks/play.js.map +0 -1
  367. package/dist/resolution/frameworks/python.d.ts.map +0 -1
  368. package/dist/resolution/frameworks/python.js.map +0 -1
  369. package/dist/resolution/frameworks/react-native.d.ts.map +0 -1
  370. package/dist/resolution/frameworks/react-native.js.map +0 -1
  371. package/dist/resolution/frameworks/react.d.ts.map +0 -1
  372. package/dist/resolution/frameworks/react.js.map +0 -1
  373. package/dist/resolution/frameworks/ruby.d.ts.map +0 -1
  374. package/dist/resolution/frameworks/ruby.js.map +0 -1
  375. package/dist/resolution/frameworks/rust.d.ts.map +0 -1
  376. package/dist/resolution/frameworks/rust.js.map +0 -1
  377. package/dist/resolution/frameworks/svelte.d.ts.map +0 -1
  378. package/dist/resolution/frameworks/svelte.js.map +0 -1
  379. package/dist/resolution/frameworks/swift-objc.d.ts.map +0 -1
  380. package/dist/resolution/frameworks/swift-objc.js.map +0 -1
  381. package/dist/resolution/frameworks/swift.d.ts.map +0 -1
  382. package/dist/resolution/frameworks/swift.js.map +0 -1
  383. package/dist/resolution/frameworks/terraform.d.ts.map +0 -1
  384. package/dist/resolution/frameworks/terraform.js.map +0 -1
  385. package/dist/resolution/frameworks/vue.d.ts.map +0 -1
  386. package/dist/resolution/frameworks/vue.js.map +0 -1
  387. package/dist/resolution/go-module.d.ts.map +0 -1
  388. package/dist/resolution/go-module.js.map +0 -1
  389. package/dist/resolution/goframe-synthesizer.d.ts.map +0 -1
  390. package/dist/resolution/goframe-synthesizer.js.map +0 -1
  391. package/dist/resolution/import-resolver.d.ts.map +0 -1
  392. package/dist/resolution/import-resolver.js.map +0 -1
  393. package/dist/resolution/index.d.ts.map +0 -1
  394. package/dist/resolution/index.js.map +0 -1
  395. package/dist/resolution/lru-cache.d.ts.map +0 -1
  396. package/dist/resolution/lru-cache.js.map +0 -1
  397. package/dist/resolution/memory-budget.d.ts.map +0 -1
  398. package/dist/resolution/memory-budget.js.map +0 -1
  399. package/dist/resolution/name-matcher.d.ts.map +0 -1
  400. package/dist/resolution/name-matcher.js.map +0 -1
  401. package/dist/resolution/path-aliases.d.ts.map +0 -1
  402. package/dist/resolution/path-aliases.js.map +0 -1
  403. package/dist/resolution/resolver-pool.d.ts.map +0 -1
  404. package/dist/resolution/resolver-pool.js.map +0 -1
  405. package/dist/resolution/resolver-worker.d.ts.map +0 -1
  406. package/dist/resolution/resolver-worker.js.map +0 -1
  407. package/dist/resolution/strip-comments.d.ts.map +0 -1
  408. package/dist/resolution/strip-comments.js.map +0 -1
  409. package/dist/resolution/swift-objc-bridge.d.ts.map +0 -1
  410. package/dist/resolution/swift-objc-bridge.js.map +0 -1
  411. package/dist/resolution/types.d.ts.map +0 -1
  412. package/dist/resolution/types.js.map +0 -1
  413. package/dist/resolution/workspace-packages.d.ts.map +0 -1
  414. package/dist/resolution/workspace-packages.js.map +0 -1
  415. package/dist/search/identifier-segments.d.ts.map +0 -1
  416. package/dist/search/identifier-segments.js.map +0 -1
  417. package/dist/search/query-parser.d.ts.map +0 -1
  418. package/dist/search/query-parser.js.map +0 -1
  419. package/dist/search/query-utils.d.ts.map +0 -1
  420. package/dist/search/query-utils.js.map +0 -1
  421. package/dist/spec/build/diff-parser.d.ts.map +0 -1
  422. package/dist/spec/build/diff-parser.js.map +0 -1
  423. package/dist/spec/build/pipeline.d.ts.map +0 -1
  424. package/dist/spec/build/pipeline.js.map +0 -1
  425. package/dist/spec/build/scan.d.ts.map +0 -1
  426. package/dist/spec/build/scan.js.map +0 -1
  427. package/dist/spec/build/scope-resolver.d.ts.map +0 -1
  428. package/dist/spec/build/scope-resolver.js.map +0 -1
  429. package/dist/spec/build/spec-extractor.d.ts.map +0 -1
  430. package/dist/spec/build/spec-extractor.js.map +0 -1
  431. package/dist/spec/config.d.ts.map +0 -1
  432. package/dist/spec/config.js.map +0 -1
  433. package/dist/spec/db/commit-node.d.ts.map +0 -1
  434. package/dist/spec/db/commit-node.js.map +0 -1
  435. package/dist/spec/db/fragment-node.d.ts.map +0 -1
  436. package/dist/spec/db/fragment-node.js.map +0 -1
  437. package/dist/spec/db/fts.d.ts.map +0 -1
  438. package/dist/spec/db/fts.js.map +0 -1
  439. package/dist/spec/db/index.d.ts.map +0 -1
  440. package/dist/spec/db/index.js.map +0 -1
  441. package/dist/spec/db/persist.d.ts.map +0 -1
  442. package/dist/spec/db/persist.js.map +0 -1
  443. package/dist/spec/db/relations.d.ts.map +0 -1
  444. package/dist/spec/db/relations.js.map +0 -1
  445. package/dist/spec/db/schema.d.ts.map +0 -1
  446. package/dist/spec/db/schema.js.map +0 -1
  447. package/dist/spec/db/spec-node.d.ts.map +0 -1
  448. package/dist/spec/db/spec-node.js.map +0 -1
  449. package/dist/spec/db/sql-utils.d.ts.map +0 -1
  450. package/dist/spec/db/sql-utils.js.map +0 -1
  451. package/dist/spec/evolve/cluster-context.d.ts.map +0 -1
  452. package/dist/spec/evolve/cluster-context.js.map +0 -1
  453. package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +0 -1
  454. package/dist/spec/evolve/commit-spec-analyzer.js.map +0 -1
  455. package/dist/spec/evolve/commit-spec-persister.d.ts.map +0 -1
  456. package/dist/spec/evolve/commit-spec-persister.js.map +0 -1
  457. package/dist/spec/evolve/impact-locator.d.ts.map +0 -1
  458. package/dist/spec/evolve/impact-locator.js.map +0 -1
  459. package/dist/spec/evolve/pipeline.d.ts.map +0 -1
  460. package/dist/spec/evolve/pipeline.js.map +0 -1
  461. package/dist/spec/evolve/spec-rewriter.d.ts.map +0 -1
  462. package/dist/spec/evolve/spec-rewriter.js.map +0 -1
  463. package/dist/spec/git/commits.d.ts.map +0 -1
  464. package/dist/spec/git/commits.js.map +0 -1
  465. package/dist/spec/git/exec.d.ts.map +0 -1
  466. package/dist/spec/git/exec.js.map +0 -1
  467. package/dist/spec/git/index.d.ts.map +0 -1
  468. package/dist/spec/git/index.js.map +0 -1
  469. package/dist/spec/graph/index.d.ts.map +0 -1
  470. package/dist/spec/graph/index.js.map +0 -1
  471. package/dist/spec/graph/queries.d.ts.map +0 -1
  472. package/dist/spec/graph/queries.js.map +0 -1
  473. package/dist/spec/llm/agent-client.d.ts.map +0 -1
  474. package/dist/spec/llm/agent-client.js.map +0 -1
  475. package/dist/spec/llm/agents/claude-code.d.ts.map +0 -1
  476. package/dist/spec/llm/agents/claude-code.js.map +0 -1
  477. package/dist/spec/llm/agents/codex.d.ts.map +0 -1
  478. package/dist/spec/llm/agents/codex.js.map +0 -1
  479. package/dist/spec/llm/agents/detect-utils.d.ts.map +0 -1
  480. package/dist/spec/llm/agents/detect-utils.js.map +0 -1
  481. package/dist/spec/llm/agents/deveco-code.d.ts.map +0 -1
  482. package/dist/spec/llm/agents/deveco-code.js.map +0 -1
  483. package/dist/spec/llm/agents/index.d.ts.map +0 -1
  484. package/dist/spec/llm/agents/index.js.map +0 -1
  485. package/dist/spec/llm/agents/types.d.ts.map +0 -1
  486. package/dist/spec/llm/agents/types.js.map +0 -1
  487. package/dist/spec/llm/client.d.ts.map +0 -1
  488. package/dist/spec/llm/client.js.map +0 -1
  489. package/dist/spec/llm/factory.d.ts.map +0 -1
  490. package/dist/spec/llm/factory.js.map +0 -1
  491. package/dist/spec/llm/prompts.d.ts.map +0 -1
  492. package/dist/spec/llm/prompts.js.map +0 -1
  493. package/dist/spec/llm/retry.d.ts.map +0 -1
  494. package/dist/spec/llm/retry.js.map +0 -1
  495. package/dist/spec/mine/addon/adapter.d.ts.map +0 -1
  496. package/dist/spec/mine/addon/adapter.js.map +0 -1
  497. package/dist/spec/mine/addon/render.d.ts.map +0 -1
  498. package/dist/spec/mine/addon/render.js.map +0 -1
  499. package/dist/spec/mine/addon/types.d.ts.map +0 -1
  500. package/dist/spec/mine/addon/types.js.map +0 -1
  501. package/dist/spec/mine/clustering/features.d.ts.map +0 -1
  502. package/dist/spec/mine/clustering/features.js.map +0 -1
  503. package/dist/spec/mine/clustering/index.d.ts.map +0 -1
  504. package/dist/spec/mine/clustering/index.js.map +0 -1
  505. package/dist/spec/mine/clustering/leiden.d.ts.map +0 -1
  506. package/dist/spec/mine/clustering/leiden.js.map +0 -1
  507. package/dist/spec/mine/clustering/text-similarity.d.ts.map +0 -1
  508. package/dist/spec/mine/clustering/text-similarity.js.map +0 -1
  509. package/dist/spec/mine/generator.d.ts.map +0 -1
  510. package/dist/spec/mine/generator.js.map +0 -1
  511. package/dist/spec/mine/persist.d.ts.map +0 -1
  512. package/dist/spec/mine/persist.js.map +0 -1
  513. package/dist/spec/mine/pipeline.d.ts.map +0 -1
  514. package/dist/spec/mine/pipeline.js.map +0 -1
  515. package/dist/spec/mine/scanner.d.ts.map +0 -1
  516. package/dist/spec/mine/scanner.js.map +0 -1
  517. package/dist/spec/types.d.ts.map +0 -1
  518. package/dist/spec/types.js.map +0 -1
  519. package/dist/spec/ui/index.d.ts.map +0 -1
  520. package/dist/spec/ui/index.js.map +0 -1
  521. package/dist/spec/ui/progress-handler.d.ts.map +0 -1
  522. package/dist/spec/ui/progress-handler.js.map +0 -1
  523. package/dist/spec/ui/progress.d.ts.map +0 -1
  524. package/dist/spec/ui/progress.js.map +0 -1
  525. package/dist/spec/utils/fs.d.ts.map +0 -1
  526. package/dist/spec/utils/fs.js.map +0 -1
  527. package/dist/spec/utils/index.d.ts.map +0 -1
  528. package/dist/spec/utils/index.js.map +0 -1
  529. package/dist/spec/utils/meta.d.ts.map +0 -1
  530. package/dist/spec/utils/meta.js.map +0 -1
  531. package/dist/spec/utils/truncate.d.ts.map +0 -1
  532. package/dist/spec/utils/truncate.js.map +0 -1
  533. package/dist/sync/git-hooks.d.ts.map +0 -1
  534. package/dist/sync/git-hooks.js.map +0 -1
  535. package/dist/sync/index.d.ts.map +0 -1
  536. package/dist/sync/index.js.map +0 -1
  537. package/dist/sync/watch-policy.d.ts.map +0 -1
  538. package/dist/sync/watch-policy.js.map +0 -1
  539. package/dist/sync/watcher.d.ts.map +0 -1
  540. package/dist/sync/watcher.js.map +0 -1
  541. package/dist/sync/worktree.d.ts.map +0 -1
  542. package/dist/sync/worktree.js.map +0 -1
  543. package/dist/types.d.ts.map +0 -1
  544. package/dist/types.js.map +0 -1
  545. package/dist/ui/glyphs.d.ts.map +0 -1
  546. package/dist/ui/glyphs.js.map +0 -1
  547. package/dist/ui/shimmer-progress.d.ts.map +0 -1
  548. package/dist/ui/shimmer-progress.js.map +0 -1
  549. package/dist/ui/shimmer-worker.d.ts.map +0 -1
  550. package/dist/ui/shimmer-worker.js.map +0 -1
  551. package/dist/ui/types.d.ts.map +0 -1
  552. package/dist/ui/types.js.map +0 -1
  553. package/dist/upgrade/index.d.ts.map +0 -1
  554. package/dist/upgrade/index.js.map +0 -1
  555. package/dist/upgrade/remove-binary.d.ts.map +0 -1
  556. package/dist/upgrade/remove-binary.js.map +0 -1
  557. package/dist/upgrade/update-check.d.ts.map +0 -1
  558. package/dist/upgrade/update-check.js.map +0 -1
  559. package/dist/utils.d.ts.map +0 -1
  560. package/dist/utils.js.map +0 -1
package/dist/mcp/tools.js CHANGED
@@ -21,7 +21,13 @@ exports.queryAsBareSymbolInventory = queryAsBareSymbolInventory;
21
21
  exports.hasPositiveAnswerNowDirective = hasPositiveAnswerNowDirective;
22
22
  exports.reconcilePartialAnswerNow = reconcilePartialAnswerNow;
23
23
  const query_pool_1 = require("./query-pool");
24
+ const source_slice_identity_1 = require("./source-slice-identity");
25
+ const evidence_rendering_1 = require("./evidence-rendering");
26
+ const arkts_evidence_packs_1 = require("./arkts-evidence-packs");
27
+ const evidence_paths_1 = require("../graph/evidence-paths");
28
+ const query_plan_1 = require("../search/query-plan");
24
29
  const memory_budget_1 = require("./memory-budget");
30
+ const index_availability_1 = require("./index-availability");
25
31
  const directory_1 = require("../directory");
26
32
  // Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
27
33
  // helper in engine.ts. ToolHandler must load to answer tools/list (static
@@ -99,6 +105,20 @@ const MAX_OUTPUT_LENGTH = 15000;
99
105
  * far beyond any realistic legitimate query.
100
106
  */
101
107
  const MAX_INPUT_LENGTH = 10_000;
108
+ // Server-owned, structured-clone-safe request context. Never accept these from MCP clients.
109
+ const QUERY_PLAN_ARG = '__homegraphQueryPlan';
110
+ const QUERY_DEADLINE_ARG = '__homegraphQueryDeadlineAt';
111
+ const QUERY_STARTED_ARG = '__homegraphQueryStartedAt';
112
+ const QUERY_INDEX_STATE_ARG = '__homegraphQueryIndexState';
113
+ const QUERY_FAST_ATTEMPTED_ARG = '__homegraphQueryFastAttempted';
114
+ function readQueryPlan(args) {
115
+ const plan = args[QUERY_PLAN_ARG];
116
+ return plan?.version === query_plan_1.QUERY_PLAN_VERSION && typeof plan.originalQuery === 'string'
117
+ && typeof plan.canonicalQuery === 'string' && Array.isArray(plan.steps) ? plan : undefined;
118
+ }
119
+ function planFeature(plan, name, query, fallback) {
120
+ return plan?.features[name] ?? fallback(query);
121
+ }
102
122
  /** Example values for success-shaped bad-arg guidance (keyed by arg name). */
103
123
  const BAD_ARG_EXAMPLES = {
104
124
  query: 'authenticate login',
@@ -641,7 +661,7 @@ exports.tools = [
641
661
  {
642
662
  name: 'homegraph_search',
643
663
  description: 'LAST RESORT spelling lookup — locations only, no source. Required: `query` (e.g. "signIn"). ' +
644
- 'Prefer explore/callers/node when names are known. ' +
664
+ 'Use ordinary scoped search/read when names or paths are known; use graph tools only for missing structural evidence. ' +
645
665
  'DO NOT call for topic file-lists, concept compares, or SDK/@kit feature catalogs (those return Skip guidance). ' +
646
666
  'Also skip literal string/pattern greps — use Grep instead. ' +
647
667
  'Bare-name search may return a compact explore result instead of locations.',
@@ -671,7 +691,7 @@ exports.tools = [
671
691
  {
672
692
  name: 'homegraph_callers',
673
693
  description: 'Compact caller list for one NAMED in-repo symbol (no bodies). Required: `symbol` (e.g. "authenticate"). ' +
674
- 'Cheaper than explore when you only need who-calls-X. For multi-file flows use homegraph_explore. ' +
694
+ 'Cheaper than explore when you only need who-calls-X. For an unresolved cross-symbol flow, consider homegraph_explore. ' +
675
695
  'DO NOT call for SDK catalogs, topic file-lists, concept compares, or hypothetics.',
676
696
  inputSchema: {
677
697
  type: 'object',
@@ -698,7 +718,7 @@ exports.tools = [
698
718
  {
699
719
  name: 'homegraph_callees',
700
720
  description: 'Compact callee list for one NAMED in-repo symbol (no bodies). Required: `symbol` (e.g. "authenticate"). ' +
701
- 'Cheaper than explore when you only need what-X-calls. For multi-file flows use homegraph_explore. ' +
721
+ 'Cheaper than explore when you only need what-X-calls. For an unresolved cross-symbol flow, consider homegraph_explore. ' +
702
722
  'DO NOT use for out-of-repo SDK catalogs or counterfactual analysis.',
703
723
  inputSchema: {
704
724
  type: 'object',
@@ -799,9 +819,9 @@ exports.tools = [
799
819
  'Cheaper than explore when you already know the name and only need one body. ' +
800
820
  'FILE: `file` only → line-numbered source + dependents. ' +
801
821
  'SYMBOL: body via includeCode + short trail; overloads return every body. ' +
802
- 'DO NOT call after explore already returned that symbol/file (multiplies tokens). ' +
803
- 'DO NOT crawl a feature with repeated node calls (prefer one explore for flows). ' +
804
- 'Treat returned source as already Read.',
822
+ 'Reuse complete unchanged source ranges already returned by any tool; refresh missing, truncated or edited ranges. ' +
823
+ 'Avoid repeated node calls over the same evidence; use explore only for an unresolved cross-symbol flow. ' +
824
+ 'Treat complete returned source ranges as already Read, not an entire file inferred from an excerpt.',
805
825
  inputSchema: {
806
826
  type: 'object',
807
827
  properties: {
@@ -843,95 +863,97 @@ exports.tools = [
843
863
  annotations: READ_ONLY_ANNOTATIONS,
844
864
  },
845
865
  {
846
- name: 'homegraph_explore',
847
- description: 'PRIMARY entry for understanding THIS repo before you edit or answer structural questions. ' +
848
- 'Returns call paths + compact line-numbered source for the relevant symbols. Required: `query`. ' +
849
- 'CALL FIRST (alone, no parallel Grep/Read) when you will change an existing codebase — pass the user task or domain keywords ' +
850
- '(page/module/feature/component words); locate where to edit before writing code. Also CALL FIRST for how/wired questions, ' +
851
- 'named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, declaration/attribute sites, ' +
852
- 'ALL_CAPS constant / field-mutex usages, path-module NAPI/exports or inter-deps, and in-repo @kit/@ohos *usages/dependencies* ' +
853
- '(which files import a named export — not the SDK feature catalog). PascalCase names optional when domain keywords suffice. ' +
854
- 'Prefer callers/node when one named symbol is already enough. ' +
855
- 'DO NOT call for topic file-lists with no Type/file, literal copy hunts / pure existence compares with no anchors, ' +
856
- 'official-docs-only asks, empty-project-from-scratch scaffolds, git history, or media/binary asset inventories — those return Skip. ' +
857
- '@kit / OHOS API questions ARE in scope when the SDK API graph is available — call explore (usages or API symbols). ' +
858
- 'Literal string/pattern hunts → Grep; media assets → Glob; git history → git. ' +
859
- 'One explore; then answer or edit from Source + trail — do not re-grep/node/read the same symbols. ' +
860
- 'Overlapping paraphrase explores are refused (name a new Type/file/@kit to continue). ' +
861
- 'Busy/partial → retry ONCE with the named Next anchor or ONE narrow Grep — not a Grep/node storm '
862
- + '(session refuses further explore / depth fan-out after Partial).',
866
+ name: 'homegraph_usages',
867
+ description: 'Optional focused tool for an unresolved WHERE-USED question about one named API, `.member`, ALL_CAPS constant, field, or mutex. ' +
868
+ 'Choose this instead of homegraph_explore when the requested answer is usage/reference locations. ' +
869
+ 'Required: `query`. Returns usage files/lines only; it does not build a general flow or source dump. ' +
870
+ '`homegraph_explore` auto-routes equivalent high-confidence queries here only for compatibility.',
863
871
  inputSchema: {
864
872
  type: 'object',
865
873
  properties: {
866
874
  query: {
867
875
  type: 'string',
868
- description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
869
- 'For named flows, include Type / Type.member / component names. For @kit, ask usages (depend/import sites), not SDK catalogs.',
870
- },
871
- maxFiles: {
872
- type: 'number',
873
- description: 'Maximum number of files to include source code from (default: 12)',
874
- default: 12,
876
+ description: 'Required. Exact API/member/constant/field name, with or without where-used wording.',
875
877
  },
876
878
  projectPath: projectPathProperty,
877
879
  },
878
880
  required: ['query'],
879
881
  },
880
- annotations: READ_ONLY_ANNOTATIONS,
882
+ annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph Where-used Inventory' },
881
883
  },
882
884
  {
883
- name: 'homegraph_usages',
884
- description: 'Focused in-repo usage inventory for one named API, `.member`, ALL_CAPS constant, field, or mutex. ' +
885
- 'Required: `query`. Returns usage files/lines only; it does not build a general flow or source dump. ' +
886
- 'Use directly for a narrow where-used question; `homegraph_explore` auto-routes equivalent high-confidence queries here.',
885
+ name: 'homegraph_modules',
886
+ description: 'Optional focused tool for an unresolved DEPENDENCY/CYCLE question about named path modules or `*common` / `*service` / `*component` / `*constants` modules. ' +
887
+ 'Choose this instead of homegraph_explore when the requested answer is module topology. ' +
888
+ 'Required: `query`. It does not build a general code flow or scan unrelated survey families. ' +
889
+ '`homegraph_explore` auto-routes equivalent high-confidence dependency questions here only for compatibility.',
887
890
  inputSchema: {
888
891
  type: 'object',
889
892
  properties: {
890
893
  query: {
891
894
  type: 'string',
892
- description: 'Required. Exact API/member/constant/field name, with or without where-used wording.',
895
+ description: 'Required. Dependency/cycle question containing the exact module names or paths.',
893
896
  },
894
897
  projectPath: projectPathProperty,
895
898
  },
896
899
  required: ['query'],
897
900
  },
898
- annotations: READ_ONLY_ANNOTATIONS,
901
+ annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph Module Dependencies' },
899
902
  },
900
903
  {
901
- name: 'homegraph_modules',
902
- description: 'Focused dependency/cycle inventory for named path modules or `*common` / `*service` / `*component` / `*constants` modules. ' +
903
- 'Required: `query`. It does not build a general code flow or scan unrelated survey families. ' +
904
- '`homegraph_explore` auto-routes equivalent high-confidence dependency questions here.',
904
+ name: 'homegraph_native',
905
+ description: 'Optional focused tool for an unresolved NAPI/NATIVE EXPORT or registration question about one named path or Type. ' +
906
+ 'Choose this instead of homegraph_explore when the requested answer is the ArkTS↔native export surface. Required: `query`. ' +
907
+ 'Returns indexed export descriptors/registration sites without a general domain file dump. ' +
908
+ '`homegraph_explore` auto-routes equivalent high-confidence NAPI/export questions here only for compatibility.',
905
909
  inputSchema: {
906
910
  type: 'object',
907
911
  properties: {
908
912
  query: {
909
913
  type: 'string',
910
- description: 'Required. Dependency/cycle question containing the exact module names or paths.',
914
+ description: 'Required. NAPI/native export question containing the exact path or Type name.',
911
915
  },
912
916
  projectPath: projectPathProperty,
913
917
  },
914
918
  required: ['query'],
915
919
  },
916
- annotations: READ_ONLY_ANNOTATIONS,
920
+ annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph Native Exports' },
917
921
  },
918
922
  {
919
- name: 'homegraph_native',
920
- description: 'Focused NAPI/native export inventory for one named path or Type. Required: `query`. ' +
921
- 'Returns indexed export descriptors/registration sites without a general domain file dump. ' +
922
- '`homegraph_explore` auto-routes equivalent high-confidence NAPI/export questions here.',
923
+ name: 'homegraph_explore',
924
+ description: 'Optional graph evidence for a concrete unresolved cross-symbol mechanism in THIS repo. Required: `query`. ' +
925
+ 'Use ordinary bash/search/read for paths, symbols, literal strings and local changes; continue editing when that evidence suffices. ' +
926
+ 'Do not call for routine pre-edit orientation or merely because implementation is difficult. ' +
927
+ 'For a missing usage, dependency/cycle or native-registration relation, use ' +
928
+ 'homegraph_usages, homegraph_modules, or homegraph_native instead. ' +
929
+ 'Returns call paths and compact line-numbered source. ArkTS symbol evidence uses complete declarations and bounded directed paths with intermediate source dependencies; explicit Gaps and stop reasons name omitted or unverified evidence. Qualify ambiguous symbols by owning type or file. State the missing relation with known anchors, requested action, scope and constraints; taskContext can carry the full task. ' +
930
+ 'Reuse unchanged complete ranges; refresh missing, edited or truncated evidence. ' +
931
+ 'No new evidence → change to a targeted source inspection, not a paraphrased explore. ' +
932
+ 'Partial/busy → at most one focused recovery for the named gap; budgets are ceilings, not required calls. ' +
933
+ 'Retrieval completion is not task completion: continue implementation and required validation.',
923
934
  inputSchema: {
924
935
  type: 'object',
925
936
  properties: {
926
937
  query: {
927
938
  type: 'string',
928
- description: 'Required. NAPI/native export question containing the exact path or Type name.',
939
+ description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
940
+ 'For named flows, include Type / Type.member / component names. For @kit mechanism/flow, include module/export tokens; ' +
941
+ 'use homegraph_usages for a narrow import/usage inventory, not SDK catalogs.',
942
+ },
943
+ taskContext: {
944
+ type: 'string',
945
+ description: 'Optional original task, including requested changes, product/module scope, exclusions and acceptance requirements. Not source evidence; bounded to 4000 characters.',
946
+ },
947
+ maxFiles: {
948
+ type: 'number',
949
+ description: 'Maximum number of files to include source code from (default: 12)',
950
+ default: 12,
929
951
  },
930
952
  projectPath: projectPathProperty,
931
953
  },
932
954
  required: ['query'],
933
955
  },
934
- annotations: READ_ONLY_ANNOTATIONS,
956
+ annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph General Explore' },
935
957
  },
936
958
  {
937
959
  name: 'homegraph_status',
@@ -987,7 +1009,7 @@ exports.tools = [
987
1009
  {
988
1010
  name: 'homegraph_files',
989
1011
  description: 'Indexed directory tree (paths and symbol counts only — NO source). ' +
990
- 'Only for coarse folder layout when explore cannot help. For where/what/how code questions use homegraph_explore.',
1012
+ 'Optional coarse folder inventory; prefer ordinary file listing for known paths. Use graph tools only for missing structural evidence.',
991
1013
  inputSchema: {
992
1014
  type: 'object',
993
1015
  properties: {
@@ -1277,10 +1299,15 @@ function reconcilePartialAnswerNow(text) {
1277
1299
  if (!/\*\*Partial locator\*\*/i.test(text))
1278
1300
  return text;
1279
1301
  let changed = false;
1302
+ let fenced = false;
1280
1303
  const out = text
1281
1304
  .split('\n')
1282
1305
  .map((line) => {
1283
- if (!/ANSWER NOW/i.test(line))
1306
+ if (/^\s*```/.test(line)) {
1307
+ fenced = !fenced;
1308
+ return line;
1309
+ }
1310
+ if (fenced || !/ANSWER NOW/i.test(line))
1284
1311
  return line;
1285
1312
  const stripped = stripPositiveAnswerNowFromLine(line);
1286
1313
  if (stripped.changed)
@@ -1291,7 +1318,7 @@ function reconcilePartialAnswerNow(text) {
1291
1318
  .join('\n');
1292
1319
  if (!changed)
1293
1320
  return text;
1294
- return `${out}\n\n> This survey is a **Partial locator**: the sections above are anchors, not a closed answer. Continue with the narrower lookup named above, or a text search, before answering.`;
1321
+ return `${out}\n\n> This survey is a **Partial locator**: the sections above are anchors, not a closed answer. Inspect missing source with a focused lookup, continue the requested edits, and verify the resulting code.`;
1295
1322
  }
1296
1323
  class ToolHandler {
1297
1324
  cg;
@@ -1450,7 +1477,6 @@ class ToolHandler {
1450
1477
  return withRequiredProjectPath(visible);
1451
1478
  try {
1452
1479
  const stats = this.cg.getStats();
1453
- const budget = getExploreBudget(stats.fileCount);
1454
1480
  // Tiny-repo tool gating: on projects under TINY_REPO_FILE_THRESHOLD
1455
1481
  // files, only expose the core trio (search, node, explore) — one
1456
1482
  // below even the 4-tool default: at this scale callers, too, reduces
@@ -1482,14 +1508,16 @@ class ToolHandler {
1482
1508
  'homegraph_diff_impact',
1483
1509
  'homegraph_project',
1484
1510
  ]);
1485
- if (stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
1511
+ // An explicit host selection overrides the default size-based surface.
1512
+ // Otherwise small ArkTS repos silently lose requested specialized tools.
1513
+ if (!allow && stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
1486
1514
  visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name));
1487
1515
  }
1488
1516
  return visible.map(tool => {
1489
1517
  if (tool.name === 'homegraph_explore') {
1490
1518
  return {
1491
1519
  ...tool,
1492
- description: `${tool.description} Budget: make at most ${budget} calls for this project (${stats.fileCount.toLocaleString()} files indexed).`,
1520
+ description: `${tool.description} ${this.exploreSuffix(stats.fileCount)}`,
1493
1521
  };
1494
1522
  }
1495
1523
  return tool;
@@ -1499,6 +1527,53 @@ class ToolHandler {
1499
1527
  return visible;
1500
1528
  }
1501
1529
  }
1530
+ /**
1531
+ * Per-repo suffix appended to homegraph_explore's description (Spec 0027).
1532
+ *
1533
+ * fileCount === 0 must not read as "this project is empty": hosts snapshot
1534
+ * tools/list once at connect (#964), and an auto-init that is still building
1535
+ * reports 0 at that instant. Field evidence (DevEco Code bench, 3 sessions,
1536
+ * zero homegraph calls): agents read "make at most 1 calls for this project
1537
+ * (0 files indexed)" as terminal — "homegraph isn't indexed for this
1538
+ * project" — and permanently fell back to Read/Grep even though the index
1539
+ * completed seconds later. Mirror maybeDeepToolPhaseGate's success-shaped
1540
+ * guidance so the frozen description and the live call-time response tell
1541
+ * the same story: failed build → say so (indexing is the user's call, so
1542
+ * the agent relays it); still building → point at homegraph_project and
1543
+ * retry; genuinely empty → honest empty note. Only fileCount > 0 carries a
1544
+ * call budget — there is nothing to budget over an unbuilt index.
1545
+ */
1546
+ exploreSuffix(fileCount) {
1547
+ if (fileCount > 0) {
1548
+ return `Budget: make at most ${getExploreBudget(fileCount)} calls for this project (${fileCount.toLocaleString()} files indexed).`;
1549
+ }
1550
+ const cg = this.cg;
1551
+ if (cg) {
1552
+ try {
1553
+ // Failed check comes first: a failed full build rolls build_phase back
1554
+ // to 'fast' (startBackgroundFullBuild catch), so the building branch
1555
+ // below would otherwise mask the failure.
1556
+ if (cg.getQueryBuilder().getMetadata('index_state') === 'failed') {
1557
+ return 'The last full index build for this project failed, so results will be empty. Tell the user to re-run `homegraph index`.';
1558
+ }
1559
+ const state = (0, index_availability_1.resolveProductIndexState)(cg);
1560
+ if (state === 'empty') {
1561
+ return (0, index_availability_1.productIndexGuidance)('empty');
1562
+ }
1563
+ if (state === 'fast' || state === 'syncing') {
1564
+ // Keep the Spec 0027 "still building → homegraph_project" story for
1565
+ // frozen tools/list descriptions (hosts snapshot once at connect).
1566
+ return state === 'syncing'
1567
+ ? (0, index_availability_1.productIndexGuidance)('syncing')
1568
+ : "HomeGraph is still building this project's index. A call right now returns build-progress guidance; use `homegraph_project` for the module/file map, then retry once indexing finishes.";
1569
+ }
1570
+ }
1571
+ catch {
1572
+ /* metadata is advisory — fall through to the empty note */
1573
+ }
1574
+ }
1575
+ return 'No files are indexed for this project; results will be empty until an index is built.';
1576
+ }
1502
1577
  /**
1503
1578
  * Get HomeGraph instance for a project
1504
1579
  *
@@ -1876,6 +1951,10 @@ class ToolHandler {
1876
1951
  * it (the CLI does) and explore behaves exactly as before, untracked.
1877
1952
  */
1878
1953
  async execute(toolName, args, sessionState) {
1954
+ const requestStartedAt = Date.now();
1955
+ args = { ...args };
1956
+ for (const key of [QUERY_PLAN_ARG, QUERY_DEADLINE_ARG, QUERY_STARTED_ARG, QUERY_INDEX_STATE_ARG, QUERY_FAST_ATTEMPTED_ARG, '_hgEvidenceMaxChars'])
1957
+ delete args[key];
1879
1958
  try {
1880
1959
  // Block the first tool call on the engine's post-open reconcile so we
1881
1960
  // never serve rows for files deleted/edited while no MCP server was
@@ -1916,13 +1995,37 @@ class ToolHandler {
1916
1995
  return check;
1917
1996
  }
1918
1997
  const projectPath = args.projectPath;
1998
+ if (toolName === 'homegraph_explore' && process.env.HOMEGRAPH_QUERY_PLANNER !== 'off') {
1999
+ const query = this.validateString(args.query, 'query');
2000
+ if (typeof query !== 'string')
2001
+ return query;
2002
+ const cg = this.getHomeGraph(projectPath);
2003
+ const gated = this.maybeDeepToolPhaseGate(cg, toolName);
2004
+ if (gated)
2005
+ return gated;
2006
+ // Refused repeats must not spend a model call or re-serve cached evidence.
2007
+ if (sessionState) {
2008
+ const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionState.forProject(cg.getProjectRoot()), query);
2009
+ if (this.shouldRefuseRepeatedEvidence(repeat, cg.getProjectRoot())) {
2010
+ return this.textResult((0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query));
2011
+ }
2012
+ }
2013
+ args[QUERY_STARTED_ARG] = requestStartedAt;
2014
+ args[QUERY_DEADLINE_ARG] = Date.now() + (0, query_pool_1.resolveToolDeadlineMs)();
2015
+ args[QUERY_PLAN_ARG] = await (0, query_plan_1.planQuery)(query, {
2016
+ deadlineAt: args[QUERY_DEADLINE_ARG],
2017
+ taskContext: (0, query_plan_1.mergeQueryPlanTaskContext)(process.env.HOMEGRAPH_QUERY_TASK_CONTEXT, typeof args.taskContext === 'string' ? args.taskContext : undefined),
2018
+ validateAnchor: (anchor) => this.isExactPlanAnchor(cg, anchor),
2019
+ });
2020
+ args[QUERY_INDEX_STATE_ARG] = `${cg.getBuildPhase()}:${cg.getStats().nodeCount}:${cg.getLastIndexedAt() ?? 0}`;
2021
+ }
1919
2022
  // Wrong question shapes → short Skip (before cache / graph work).
1920
2023
  if (toolName === 'homegraph_explore' || toolName === 'homegraph_search') {
1921
2024
  const qEarly = typeof args.query === 'string' ? args.query : '';
1922
2025
  if (qEarly) {
1923
2026
  const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(qEarly);
1924
- if (deferKind) {
1925
- return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, qEarly));
2027
+ if (deferKind && readQueryPlan(args)?.source !== 'llm') {
2028
+ return this.withQueryPlanDiagnostics(this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, qEarly)), args);
1926
2029
  }
1927
2030
  }
1928
2031
  }
@@ -1939,7 +2042,9 @@ class ToolHandler {
1939
2042
  // Session-tracked explore must not hit the MCP query cache: a cache hit
1940
2043
  // would re-serve the first call's full source and defeat CG-18 dedup.
1941
2044
  const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
1942
- const cacheEnabled = !skipCacheForSession && (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
2045
+ const requestPlan = readQueryPlan(args);
2046
+ const cacheEnabled = !skipCacheForSession && requestPlan?.source !== 'llm'
2047
+ && !requestPlan?.telemetry.fallbackReason && (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
1943
2048
  let cacheKey;
1944
2049
  let cacheQueries;
1945
2050
  let cacheIndex;
@@ -1958,8 +2063,12 @@ class ToolHandler {
1958
2063
  }
1959
2064
  cacheKey = (0, query_cache_1.buildMcpQueryCacheKey)(toolName, args, fileCount);
1960
2065
  const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
1961
- if (cached) {
1962
- const withWorktree = this.withWorktreeNotice(cached, projectPath);
2066
+ const packMeta = cached?._meta?.homegraphEvidencePacks;
2067
+ const cachedFiles = cached?._meta?.homegraphEvidence?.files;
2068
+ if (cached && (!packMeta || (packMeta.status === 'complete'
2069
+ && this.areEvidenceFilesCurrent(cachedFiles ?? [], cacheCg.getProjectRoot())))) {
2070
+ const diagnosed = this.withQueryPlanDiagnostics(cached, args, true);
2071
+ const withWorktree = this.withWorktreeNotice(diagnosed, projectPath);
1963
2072
  return this.withStalenessNotice(withWorktree, projectPath);
1964
2073
  }
1965
2074
  }
@@ -1993,35 +2102,41 @@ class ToolHandler {
1993
2102
  // main connection. Serving them here — before the query-pool offload —
1994
2103
  // avoids cold-worker / wedged-daemon paths that otherwise surface as empty
1995
2104
  // MCP client `-32001` (the handler itself is fine; the transport times out).
1996
- if (toolName === 'homegraph_explore' || toolName === 'homegraph_search') {
2105
+ if ((toolName === 'homegraph_explore' || toolName === 'homegraph_search')
2106
+ && (!requestPlan || (requestPlan.source === 'rules' && requestPlan.steps.length === 1
2107
+ && !requestPlan.telemetry.requestCount))) {
1997
2108
  const q = typeof args.query === 'string' ? args.query : '';
1998
2109
  if (q) {
1999
2110
  try {
2000
2111
  const cgFast = this.getHomeGraph(projectPath);
2001
2112
  const rootFast = cgFast.getProjectRoot();
2002
- const fast = (toolName === 'homegraph_explore'
2003
- ? this.trySpecializedExploreRoute(cgFast, q, rootFast)
2004
- : null)
2005
- ?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
2006
- ? this.tryFastInventoryExplore(cgFast, q, rootFast)
2007
- : null)
2008
- ?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
2009
- ? this.tryLightMechanismExplore(cgFast, q, rootFast)
2113
+ const fast = requestPlan ? this.tryPlannedFastPath(cgFast, requestPlan, rootFast) :
2114
+ (toolName === 'homegraph_explore'
2115
+ ? this.trySpecializedExploreRoute(cgFast, q, rootFast)
2010
2116
  : null)
2011
- ?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast);
2117
+ ?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
2118
+ ? this.tryFastInventoryExplore(cgFast, q, rootFast)
2119
+ : null)
2120
+ ?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
2121
+ ? this.tryLightMechanismExplore(cgFast, q, rootFast)
2122
+ : null)
2123
+ ?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast);
2012
2124
  if (fast) {
2013
2125
  // Fast explore must still file Partial/ANSWER meta into the session
2014
2126
  // so explore + depth fuses see it (textResult-only paths used to skip).
2015
2127
  let served = fast;
2016
- if (toolName === 'homegraph_explore' && sessionState) {
2017
- served = this.takeExploreEmission(this.ensureExploreEmission(fast, rootFast, q), sessionState);
2128
+ if ((toolName === 'homegraph_explore' && sessionState) || fast._meta?.homegraphEvidencePacks) {
2129
+ served = this.takeExploreEmission(this.ensureExploreEmission(fast, rootFast, q), toolName === 'homegraph_explore' ? sessionState : undefined);
2018
2130
  }
2131
+ served = this.withQueryPlanDiagnostics(served, args);
2019
2132
  if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !served.isError) {
2020
2133
  cacheIndex.setEntry(cacheQueries, cacheKey, toolName, served);
2021
2134
  }
2022
2135
  const withWorktree = this.withWorktreeNotice(served, projectPath);
2023
2136
  return this.withStalenessNotice(withWorktree, projectPath);
2024
2137
  }
2138
+ if (requestPlan)
2139
+ args[QUERY_FAST_ATTEMPTED_ARG] = true;
2025
2140
  }
2026
2141
  catch {
2027
2142
  // Not indexed / path issue — fall through to normal dispatch.
@@ -2037,7 +2152,7 @@ class ToolHandler {
2037
2152
  // (a frozen main loop prevents setTimeout deadlines from firing → empty
2038
2153
  // `-32001`). Fast-path surveys run inside the worker via executeReadTool.
2039
2154
  const raw = await this.runReadToolWithDeadline(toolName, dispatchArgs);
2040
- const result = this.takeExploreEmission(raw, sessionState);
2155
+ const result = this.withQueryPlanDiagnostics(this.takeExploreEmission(raw, sessionState), args);
2041
2156
  if (sessionState
2042
2157
  && !result.isError
2043
2158
  && (toolName === 'homegraph_node'
@@ -2065,7 +2180,11 @@ class ToolHandler {
2065
2180
  if (err instanceof PathRefusalError) {
2066
2181
  return this.errorResult(err.message);
2067
2182
  }
2068
- return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
2183
+ const msg = err instanceof Error ? err.message : String(err);
2184
+ if ((0, index_availability_1.isSqliteBusyMessage)(msg)) {
2185
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
2186
+ }
2187
+ return this.errorResult(`Tool execution failed: ${msg}. ` +
2069
2188
  'This is an internal homegraph error — retry the call once; if it persists, ' +
2070
2189
  'continue without homegraph for this task.');
2071
2190
  }
@@ -2113,16 +2232,71 @@ class ToolHandler {
2113
2232
  return null;
2114
2233
  }
2115
2234
  }
2235
+ /** A repeated query is duplicate evidence only while its source is unchanged. */
2236
+ shouldRefuseRepeatedEvidence(decision, projectRoot) {
2237
+ if (!decision.refuse)
2238
+ return false;
2239
+ // Source changes do not reset the total retrieval budget.
2240
+ if (decision.reason !== 'overlap')
2241
+ return true;
2242
+ const files = decision.matched?.files.filter((file) => file.bytes > 0 && file.ranges.length > 0) ?? [];
2243
+ return this.areEvidenceFilesCurrent(files, projectRoot);
2244
+ }
2245
+ /** Same bounded fingerprint check for repeat protection and cached source packs. */
2246
+ areEvidenceFilesCurrent(files, projectRoot) {
2247
+ if (files.length === 0 || files.length > 24)
2248
+ return false;
2249
+ let remainingBytes = 2 * 1024 * 1024;
2250
+ for (const file of files) {
2251
+ if (!file.fingerprint)
2252
+ return false;
2253
+ const absolute = (0, utils_1.validatePathWithinRoot)(projectRoot, file.path);
2254
+ if (!absolute)
2255
+ return false;
2256
+ let descriptor;
2257
+ try {
2258
+ const stat = (0, fs_1.statSync)(absolute);
2259
+ const limit = Math.min(1024 * 1024, remainingBytes);
2260
+ if (!stat.isFile() || stat.size > limit)
2261
+ return false;
2262
+ // Fixed-size reads also remain bounded if the file grows after stat.
2263
+ descriptor = (0, fs_1.openSync)(absolute, 'r');
2264
+ const buffer = Buffer.alloc(stat.size + 1);
2265
+ let bytes = 0;
2266
+ while (bytes < buffer.length) {
2267
+ const read = (0, fs_1.readSync)(descriptor, buffer, bytes, buffer.length - bytes, bytes);
2268
+ if (read === 0)
2269
+ break;
2270
+ bytes += read;
2271
+ }
2272
+ if (bytes !== stat.size)
2273
+ return false;
2274
+ remainingBytes -= bytes;
2275
+ if ((0, explore_dedup_1.fileFingerprint)(buffer.subarray(0, bytes).toString('utf8')) !== file.fingerprint)
2276
+ return false;
2277
+ }
2278
+ catch {
2279
+ return false;
2280
+ }
2281
+ finally {
2282
+ if (descriptor !== undefined) {
2283
+ try {
2284
+ (0, fs_1.closeSync)(descriptor);
2285
+ }
2286
+ catch { /* bookkeeping only */ }
2287
+ }
2288
+ }
2289
+ }
2290
+ return true;
2291
+ }
2116
2292
  /** Count a successful depth-tool call when the latest explore was Partial. */
2117
2293
  noteDepthToolUse(args, sessionState) {
2118
2294
  try {
2119
2295
  const cg = this.getHomeGraph(args.projectPath);
2120
2296
  const root = cg.getProjectRoot();
2121
2297
  const prior = sessionState.forProject(root);
2122
- const last = prior?.calls.length
2123
- ? [...prior.calls].reverse().find((c) => (c.responseBytes || 0) >= 400)
2124
- : undefined;
2125
- if (!last?.partial)
2298
+ const last = prior?.calls[prior.calls.length - 1];
2299
+ if (!last || (0, explore_session_state_1.inferExploreEvidenceStatus)(last) === 'complete')
2126
2300
  return;
2127
2301
  sessionState.recordDepthTool(root);
2128
2302
  }
@@ -2142,17 +2316,22 @@ class ToolHandler {
2142
2316
  const emission = result?.[explore_session_state_1.EXPLORE_EMISSION_KEY];
2143
2317
  if (emission === undefined)
2144
2318
  return result;
2319
+ emission.evidenceStatus = (0, explore_session_state_1.inferExploreEvidenceStatus)(emission);
2320
+ emission.partial = emission.evidenceStatus !== 'complete';
2321
+ result._meta = { ...result._meta, homegraphEvidence: { status: emission.evidenceStatus,
2322
+ files: emission.files, locatedNodes: emission.locatedNodes, coveredObligations: emission.coveredObligations,
2323
+ uncoveredObligations: emission.uncoveredObligations } };
2145
2324
  delete result[explore_session_state_1.EXPLORE_EMISSION_KEY];
2146
2325
  if (sessionState) {
2147
2326
  try {
2148
2327
  const prior = sessionState.forProject(emission.projectRoot);
2149
- const priorPartials = (prior?.calls ?? []).filter((c) => c.partial === true && (c.responseBytes || 0) >= 400);
2328
+ const priorPartials = (prior?.calls ?? []).filter((c) => (0, explore_session_state_1.inferExploreEvidenceStatus)(c) !== 'complete');
2150
2329
  const metaPartial = emission.partial === true
2151
2330
  || (emission.partial === undefined
2152
2331
  && (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content?.[0]?.text ?? '').partial);
2153
2332
  if (metaPartial && priorPartials.length >= 1) {
2154
2333
  const text = result.content?.[0]?.text ?? '';
2155
- if (text && !/Second Partial — stop HomeGraph/i.test(text)) {
2334
+ if (text && !/Second Partial/i.test(text)) {
2156
2335
  const stop = (0, explore_repeat_guard_1.formatSecondPartialStopFooter)(emission.nextAnchor);
2157
2336
  result = {
2158
2337
  ...result,
@@ -2185,13 +2364,19 @@ class ToolHandler {
2185
2364
  * the success-shaped reply never flushed → empty client `-32001`.
2186
2365
  */
2187
2366
  async runReadToolWithDeadline(toolName, args) {
2188
- const deadlineMs = (0, query_pool_1.resolveToolDeadlineMs)();
2367
+ const deadlineMs = typeof args[QUERY_DEADLINE_ARG] === 'number'
2368
+ ? Math.max(0, Math.min((0, query_pool_1.resolveToolDeadlineMs)(), args[QUERY_DEADLINE_ARG] - Date.now()))
2369
+ : (0, query_pool_1.resolveToolDeadlineMs)();
2370
+ if (deadlineMs <= 0)
2371
+ return this.deadlineBusyResult((0, query_pool_1.resolveToolDeadlineMs)(), args);
2189
2372
  const light = toolName === 'homegraph_search'
2190
2373
  || toolName === 'homegraph_node'
2191
2374
  || toolName === 'homegraph_callers'
2192
2375
  || toolName === 'homegraph_callees'
2193
2376
  || toolName === 'homegraph_files'
2194
- || toolName === 'homegraph_project';
2377
+ || toolName === 'homegraph_project'
2378
+ // Project maps may be built on demand: never run them on a read worker.
2379
+ || (readQueryPlan(args)?.steps.some((step) => step.intent === 'overview') ?? false);
2195
2380
  const work = () => {
2196
2381
  if (!light && this.queryPool && this.queryPool.healthy) {
2197
2382
  return this.queryPool.run(toolName, args, {
@@ -2246,6 +2431,14 @@ class ToolHandler {
2246
2431
  */
2247
2432
  async executeReadTool(toolName, args) {
2248
2433
  try {
2434
+ if (toolName !== 'homegraph_project' && toolName !== 'homegraph_status') {
2435
+ const gate = this.maybeDeepToolPhaseGate(this.getHomeGraph(args.projectPath), toolName);
2436
+ if (gate)
2437
+ return gate;
2438
+ }
2439
+ const plan = toolName === 'homegraph_explore' ? readQueryPlan(args) : undefined;
2440
+ if (plan)
2441
+ return await this.executeQueryPlan(args, plan);
2249
2442
  // Compact inventory / one-symbol surveys — safe on the worker (keeps the
2250
2443
  // daemon main loop free). Never run these unprotected on the MCP transport
2251
2444
  // thread: they can block long enough for the client to emit empty `-32001`.
@@ -2264,7 +2457,11 @@ class ToolHandler {
2264
2457
  if (err instanceof PathRefusalError) {
2265
2458
  return this.errorResult(err.message);
2266
2459
  }
2267
- return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
2460
+ const msg = err instanceof Error ? err.message : String(err);
2461
+ if ((0, index_availability_1.isSqliteBusyMessage)(msg)) {
2462
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
2463
+ }
2464
+ return this.errorResult(`Tool execution failed: ${msg}. ` +
2268
2465
  'This is an internal homegraph error — retry the call once; if it persists, ' +
2269
2466
  'continue without homegraph for this task.');
2270
2467
  }
@@ -2307,34 +2504,31 @@ class ToolHandler {
2307
2504
  }
2308
2505
  }
2309
2506
  /**
2310
- * Gate symbol/graph tools while auto-init is still on the fast map or
2311
- * background full index (and no nodes exist yet). Success-shaped guidance —
2312
- * never isError — so agents keep using HomeGraph.
2507
+ * Gate symbol/graph tools by product index state (Spec 0032).
2508
+ * Success-shaped guidance — never isError — so agents keep using HomeGraph.
2313
2509
  */
2314
2510
  maybeDeepToolPhaseGate(cg, _toolName) {
2315
- const phase = cg.getBuildPhase();
2316
- if (phase === 'full')
2511
+ const state = (0, index_availability_1.resolveProductIndexState)(cg);
2512
+ if (state === 'full')
2317
2513
  return null;
2514
+ if (state === 'syncing') {
2515
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
2516
+ }
2517
+ if (state === 'empty') {
2518
+ return this.textResult((0, index_availability_1.productIndexGuidance)('empty'));
2519
+ }
2520
+ // fast: allow deep tools once some symbols exist (partial index).
2318
2521
  try {
2319
2522
  if (cg.getStats().nodeCount > 0)
2320
2523
  return null;
2321
2524
  }
2322
- catch {
2323
- /* ignore */
2324
- }
2325
- if (phase === 'fast' || phase === 'indexing') {
2326
- return this.textResult([
2327
- `Full symbol index is still building (phase=${phase}).`,
2328
- 'Use `homegraph_project` for the module/file map now, then retry this tool once indexing finishes.',
2329
- ].join('\n'));
2330
- }
2331
- if (phase === 'building_fast' || phase === 'none') {
2332
- return this.textResult([
2333
- 'HomeGraph is still preparing the project map.',
2334
- 'Retry in a few seconds, or call `homegraph_project` once the fast build completes.',
2335
- ].join('\n'));
2525
+ catch (err) {
2526
+ const msg = err instanceof Error ? err.message : String(err);
2527
+ if ((0, index_availability_1.isSqliteBusyMessage)(msg)) {
2528
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
2529
+ }
2336
2530
  }
2337
- return null;
2531
+ return this.textResult((0, index_availability_1.productIndexGuidance)('fast'));
2338
2532
  }
2339
2533
  async handleSearch(args) {
2340
2534
  const query = this.validateString(args.query, 'query');
@@ -5131,9 +5325,273 @@ class ToolHandler {
5131
5325
  lines.push('');
5132
5326
  return { section: lines.join('\n'), hitCount: locs.length + edges.length + extendsHits.length };
5133
5327
  }
5134
- /**
5135
- * Main-thread fast path for inventory surveys — skips the worker queue.
5136
- */
5328
+ /** Exact validation for model-proposed names: a fuzzy hit is not proof. */
5329
+ isExactPlanAnchor(cg, anchor) {
5330
+ if (!anchor || anchor.length > 256)
5331
+ return false;
5332
+ try {
5333
+ const matches = cg.searchNodes(anchor, { limit: 12 });
5334
+ return matches.some(({ node }) => node.name === anchor || node.qualifiedName === anchor
5335
+ || node.filePath.replace(/\\/g, '/') === anchor.replace(/\\/g, '/'));
5336
+ }
5337
+ catch {
5338
+ return false;
5339
+ }
5340
+ }
5341
+ /** Bind only declarations returned by this step, not input or global fuzzy hits. */
5342
+ locatedPlanBindings(cg, result, inherited) {
5343
+ const seen = new Set(inherited.map((binding) => binding.id));
5344
+ const declarations = new Set(inherited.map((binding) => `${binding.filePath}:${binding.startLine}:${binding.name}`));
5345
+ const located = [];
5346
+ for (const receipt of (result[explore_session_state_1.EXPLORE_EMISSION_KEY]?.locatedNodes ?? []).slice(0, 32)) {
5347
+ if (seen.has(receipt.id) || declarations.has(`${receipt.filePath}:${receipt.startLine}:${receipt.name}`))
5348
+ continue;
5349
+ const node = cg.getNode(receipt.id);
5350
+ if (!node || ['file', 'import', 'export', 'parameter'].includes(node.kind)
5351
+ || node.name === 'constructor' || node.name.startsWith('%AM') || node.filePath.includes('@dummy')
5352
+ || node.name !== receipt.name || node.filePath !== receipt.filePath || node.startLine !== receipt.startLine
5353
+ || node.qualifiedName !== receipt.qualifiedName)
5354
+ continue;
5355
+ seen.add(node.id);
5356
+ declarations.add(`${node.filePath}:${node.startLine}:${node.name}`);
5357
+ located.push({ id: node.id, name: node.name, qualifiedName: node.qualifiedName,
5358
+ filePath: node.filePath, startLine: node.startLine });
5359
+ if (located.length >= 8)
5360
+ break;
5361
+ }
5362
+ return located;
5363
+ }
5364
+ withQueryPlanDiagnostics(result, args, cacheHit = false) {
5365
+ const plan = readQueryPlan(args);
5366
+ if (!plan)
5367
+ return result;
5368
+ const prior = result._meta?.homegraphQueryPlan;
5369
+ const started = typeof args[QUERY_STARTED_ARG] === 'number' ? args[QUERY_STARTED_ARG] : Date.now();
5370
+ return { ...result, _meta: { ...result._meta, homegraphQueryPlan: {
5371
+ ...prior, version: plan.version, source: plan.source, intent: plan.intent, route: plan.route,
5372
+ confidence: plan.confidence,
5373
+ plannerSeeds: { anchors: plan.anchors, searchTerms: plan.searchTerms, literalTexts: plan.literalTexts,
5374
+ sourceScope: plan.sourceScope, relation: plan.relation },
5375
+ hasTaskContext: !!plan.taskContext,
5376
+ matchedFeatures: Object.entries(plan.features).filter(([, matched]) => matched).map(([name]) => name).slice(0, 12),
5377
+ planningMs: plan.telemetry.durationMs, durationMs: Math.max(plan.telemetry.durationMs, Date.now() - started),
5378
+ modelRequests: plan.telemetry.requestCount ?? 0,
5379
+ planningEligible: plan.telemetry.decision?.eligible,
5380
+ planningReason: plan.telemetry.decision?.reason,
5381
+ skip_reason: plan.telemetry.decision?.eligible === false ? plan.telemetry.decision.reason : undefined,
5382
+ ruleRoute: plan.telemetry.decision?.ruleRoute,
5383
+ inputTokens: plan.telemetry.inputTokens ?? (plan.telemetry.requestCount ? null : 0),
5384
+ outputTokens: plan.telemetry.outputTokens ?? (plan.telemetry.requestCount ? null : 0),
5385
+ fallbackReason: plan.telemetry.fallbackReason, cacheHit,
5386
+ steps: cacheHit ? [] : prior?.steps ?? [{ id: plan.steps[0]?.id, intent: plan.intent,
5387
+ status: result.isError ? 'failed'
5388
+ : (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content[0]?.text ?? '').partial ? 'partial'
5389
+ : /Status: (?:no_indexed_evidence|not_surveyed)|No relevant code|Skip HomeGraph/i.test(result.content[0]?.text ?? '')
5390
+ ? 'no_evidence' : 'evidence', resolvedAnchors: [] }],
5391
+ } } };
5392
+ }
5393
+ /** Select once; legacy section builders consume the same canonical query/features. */
5394
+ tryPlannedFastPath(cg, plan, root, evidenceMaxChars) {
5395
+ const query = plan.canonicalQuery;
5396
+ if (plan.route === 'usages' || plan.route === 'modules' || plan.route === 'native') {
5397
+ return this.runSpecializedExploreRoute(plan.route, cg, query, root, plan);
5398
+ }
5399
+ // These legacy paths re-extract seeds from text and cannot consume bound
5400
+ // node identity / step hints. Model general/flow plans use full explore;
5401
+ // rule/default and specialized routes retain their existing fast behavior.
5402
+ if (plan.source === 'llm' && (plan.intent === 'general' || plan.intent === 'flow'))
5403
+ return null;
5404
+ return this.tryFastInventoryExplore(cg, query, root, plan)
5405
+ ?? this.tryLightMechanismExplore(cg, query, root, plan, evidenceMaxChars)
5406
+ ?? this.tryCompactLocalSymbolExplore(cg, query, root, plan, evidenceMaxChars);
5407
+ }
5408
+ /** Internal execution only: no recursive MCP calls, model requests or new deadline. */
5409
+ async executePlannedStep(args, plan) {
5410
+ const cg = this.getHomeGraph(args.projectPath);
5411
+ if (plan.intent === 'overview') {
5412
+ return this.handleProject({ projectPath: args.projectPath });
5413
+ }
5414
+ const gate = this.maybeDeepToolPhaseGate(cg, 'homegraph_explore');
5415
+ if (gate)
5416
+ return gate;
5417
+ const query = plan.canonicalQuery;
5418
+ const defer = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
5419
+ if (defer && plan.source === 'rules')
5420
+ return this.textResult((0, query_utils_1.homegraphDeferGuidance)(defer, query));
5421
+ // A typed relationship still needs a real target. Discover source from the
5422
+ // same bounded hints first; do not turn an unanchored usage request into an
5423
+ // empty survey or claim that its reference obligation has been covered.
5424
+ if (plan.source === 'llm' && plan.route === 'usages'
5425
+ && !plan.anchors.length && !plan.bindings?.length) {
5426
+ const result = await this.handleExplore({ ...args, query, [QUERY_PLAN_ARG]: plan });
5427
+ const served = this.ensureExploreEmission(result, cg.getProjectRoot(), plan.originalQuery);
5428
+ const emission = served[explore_session_state_1.EXPLORE_EMISSION_KEY];
5429
+ emission.partial = true;
5430
+ emission.evidenceStatus = emission.sourceBytes > 0 ? 'partial' : 'empty';
5431
+ emission.coveredObligations = [];
5432
+ emission.uncoveredObligations = [plan.relation ?? 'incoming_references'];
5433
+ if (served.content[0]?.type === 'text')
5434
+ served.content[0].text =
5435
+ '**Reference target discovery — partial**\nSource candidates follow. Verify the relevant target before surveying its references; the reference obligation remains open.\n\n'
5436
+ + served.content[0].text;
5437
+ return served;
5438
+ }
5439
+ const fast = args[QUERY_FAST_ATTEMPTED_ARG] === true && plan.source === 'rules'
5440
+ ? null : this.tryPlannedFastPath(cg, plan, cg.getProjectRoot(), args._hgEvidenceMaxChars);
5441
+ if (fast)
5442
+ return fast;
5443
+ return this.handleExplore({ ...args, query, [QUERY_PLAN_ARG]: plan });
5444
+ }
5445
+ async executeQueryPlan(args, plan) {
5446
+ const cg = this.getHomeGraph(args.projectPath);
5447
+ const root = cg.getProjectRoot();
5448
+ const deadline = typeof args[QUERY_DEADLINE_ARG] === 'number'
5449
+ ? args[QUERY_DEADLINE_ARG] : Date.now() + (0, query_pool_1.resolveToolDeadlineMs)();
5450
+ const prior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), root);
5451
+ const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(prior, plan.originalQuery);
5452
+ if (this.shouldRefuseRepeatedEvidence(repeat, root)) {
5453
+ return this.textResult((0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, plan.originalQuery));
5454
+ }
5455
+ const multi = plan.steps.length > 1;
5456
+ let outputBudget = Math.min(MAX_OUTPUT_LENGTH, getExploreOutputBudget(cg.getStats().fileCount).maxOutputChars);
5457
+ if (!Number.isFinite(outputBudget))
5458
+ outputBudget = MAX_OUTPUT_LENGTH;
5459
+ const constraints = [plan.originalQuery, plan.taskContext].filter(Boolean).join('\n');
5460
+ const constraintLimit = Math.min(2000, Math.floor(outputBudget / 5));
5461
+ const constraintNotice = constraints.length > constraintLimit
5462
+ ? constraints.slice(0, constraintLimit) + '\n[Constraint display shortened; the original task remains authoritative.]'
5463
+ : constraints;
5464
+ const preamble = [
5465
+ '**Planned exploration — Partial locator**',
5466
+ '> **Partial locator** — scoped subquestion evidence, not proof that every requirement in the original question is covered.',
5467
+ '**Task constraints (not search seeds or verified source evidence)**\n' + constraintNotice,
5468
+ ].join('\n\n');
5469
+ const pieces = [];
5470
+ const diagnostics = [];
5471
+ const bindings = new Map();
5472
+ const files = [];
5473
+ const seenQueries = new Set();
5474
+ let single;
5475
+ let used = multi ? preamble.length + 400 : 400;
5476
+ for (const step of plan.steps.slice(0, 3)) {
5477
+ const started = Date.now();
5478
+ const diagnostic = { id: step.id, intent: step.intent, status: 'pending', resolvedAnchors: [],
5479
+ locatedNodes: [], durationMs: 0 };
5480
+ diagnostics.push(diagnostic);
5481
+ if (Date.now() >= deadline || used >= outputBudget) {
5482
+ diagnostic.status = 'budget_exhausted';
5483
+ pieces.push(`**Step ${step.id}: ${step.intent}** — not executed: shared request budget exhausted.`);
5484
+ continue;
5485
+ }
5486
+ if (step.dependsOn.some((id) => !bindings.get(id)?.length)) {
5487
+ diagnostic.status = 'dependency_unresolved';
5488
+ pieces.push(`**Step ${step.id}: ${step.intent}** — not executed: predecessor supplied no resolved symbol anchors.`);
5489
+ continue;
5490
+ }
5491
+ const resolved = step.dependsOn.flatMap((id) => bindings.get(id) ?? []);
5492
+ // Keep rule-only single queries byte-compatible; only model subplans are adapted.
5493
+ const compiled = plan.source === 'rules' && !multi ? plan : {
5494
+ ...(0, query_plan_1.compileQueryPlanStep)(plan, step, resolved.map((node) => node.qualifiedName || node.name)), bindings: resolved,
5495
+ };
5496
+ if (seenQueries.has(`${compiled.intent}:${compiled.canonicalQuery}`)) {
5497
+ diagnostic.status = 'duplicate_skipped';
5498
+ pieces.push(`**Step ${step.id}: ${step.intent}** — duplicate query skipped.`);
5499
+ continue;
5500
+ }
5501
+ seenQueries.add(`${compiled.intent}:${compiled.canonicalQuery}`);
5502
+ try {
5503
+ const stepCap = Math.max(0, Math.floor((outputBudget - used) / Math.max(1, plan.steps.length - diagnostics.length + 1)) - 100);
5504
+ const result = await this.executePlannedStep({ ...args, [QUERY_DEADLINE_ARG]: deadline,
5505
+ ...(multi ? { _hgEvidenceMaxChars: Math.max(0, stepCap - Math.min(1600, Math.floor(stepCap / 2)) - 2) } : {}),
5506
+ }, compiled);
5507
+ const body = result.content.map((part) => part.text).join('\n');
5508
+ const candidates = result.isError ? [] : this.locatedPlanBindings(cg, result, resolved);
5509
+ const childEmission = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
5510
+ const hasLocalEvidence = candidates.length > 0 || (childEmission?.sourceBytes ?? 0) > 0;
5511
+ diagnostic.status = result.isError ? 'failed'
5512
+ : childEmission?.evidenceStatus === 'complete' ? 'evidence'
5513
+ : hasLocalEvidence ? (childEmission?.partial ? 'partial' : 'evidence')
5514
+ : /HarmonyOS SDK API|ohos-sdk:/.test(body) ? 'sdk_only' : 'no_evidence';
5515
+ if (!multi) {
5516
+ diagnostic.locatedNodes = candidates;
5517
+ diagnostic.resolvedAnchors = candidates.map((node) => node.name);
5518
+ single = result;
5519
+ break;
5520
+ }
5521
+ // A child cannot declare the original multi-part question answered.
5522
+ let fenced = false;
5523
+ const neutralBody = body.split('\n').filter((line) => {
5524
+ if (/^\s*```/.test(line)) {
5525
+ fenced = !fenced;
5526
+ return true;
5527
+ }
5528
+ return fenced || !/ANSWER NOW|Compact local explore complete|Do \*\*not\*\* (?:Read|Grep)|ONE tighter|ONE narrow/i.test(line);
5529
+ }).join('\n');
5530
+ const cap = Math.max(0, Math.floor((outputBudget - used) / Math.max(1, plan.steps.length - diagnostics.length + 1)) - 100);
5531
+ // Keep the grounded location receipt before source trimming. Only the
5532
+ // identities visibly delivered here may be consumed by the next step.
5533
+ let receipt = '';
5534
+ const visibleBindings = [];
5535
+ const inline = (value) => value.replace(/[`\r\n]/g, ' ');
5536
+ for (const candidate of candidates) {
5537
+ const next = (receipt ? '' : '**Located source candidates — relevance still needs verification**\n')
5538
+ + `- \`${inline(candidate.qualifiedName || candidate.name)}\` — \`${inline(candidate.filePath)}:${candidate.startLine}\`\n`;
5539
+ if (receipt.length + next.length > Math.min(1600, Math.floor(cap / 2)))
5540
+ break;
5541
+ receipt += next;
5542
+ visibleBindings.push(candidate);
5543
+ }
5544
+ diagnostic.locatedNodes = visibleBindings;
5545
+ diagnostic.resolvedAnchors = visibleBindings.map((node) => node.name);
5546
+ bindings.set(step.id, visibleBindings);
5547
+ const bodyCap = Math.max(0, cap - receipt.length - 2);
5548
+ const trimmed = neutralBody.length > bodyCap;
5549
+ const atomic = !!result._meta?.homegraphEvidencePacks;
5550
+ const kept = atomic && trimmed
5551
+ ? '[Partial source: complete evidence pack omitted by the shared output budget.]'.slice(0, bodyCap)
5552
+ : atomic ? neutralBody : (0, evidence_rendering_1.trimEvidenceAtLine)(neutralBody, bodyCap);
5553
+ if (atomic && trimmed) {
5554
+ receipt = '';
5555
+ diagnostic.locatedNodes = [];
5556
+ diagnostic.resolvedAnchors = [];
5557
+ bindings.set(step.id, []);
5558
+ }
5559
+ if (trimmed)
5560
+ diagnostic.status = 'partial';
5561
+ else
5562
+ files.push(...(result[explore_session_state_1.EXPLORE_EMISSION_KEY]?.files ?? []));
5563
+ const piece = `**Step ${step.id}: ${step.intent}** (${diagnostic.status})\n${receipt}\n${kept}`;
5564
+ pieces.push(piece);
5565
+ used += piece.length;
5566
+ }
5567
+ catch (error) {
5568
+ if (error instanceof PathRefusalError)
5569
+ throw error;
5570
+ diagnostic.status = 'failed';
5571
+ pieces.push(`**Step ${step.id}: ${step.intent}** — retrieval failed; other step evidence is retained.`);
5572
+ }
5573
+ finally {
5574
+ diagnostic.durationMs = Date.now() - started;
5575
+ }
5576
+ // Yield between synchronous graph stages so deadlines and MCP I/O can run.
5577
+ await new Promise((resolve) => setImmediate(resolve));
5578
+ }
5579
+ const result = single ?? this.textResult([preamble, ...pieces].join('\n\n').slice(0, outputBudget));
5580
+ const served = this.ensureExploreEmission(result, root, plan.originalQuery);
5581
+ const emission = served[explore_session_state_1.EXPLORE_EMISSION_KEY];
5582
+ emission.query = plan.originalQuery;
5583
+ if (multi) {
5584
+ emission.partial = true;
5585
+ emission.evidenceStatus = 'partial';
5586
+ emission.coveredObligations = diagnostics.filter((step) => step.status === 'evidence').map((step) => step.id);
5587
+ emission.uncoveredObligations = diagnostics.filter((step) => step.status !== 'evidence').map((step) => step.id);
5588
+ emission.files = files;
5589
+ emission.sourceBytes = files.reduce((sum, file) => sum + file.bytes, 0);
5590
+ }
5591
+ served._meta = { ...served._meta, homegraphQueryPlan: { steps: diagnostics } };
5592
+ return served;
5593
+ }
5594
+ /** Main-thread fast path for inventory surveys — skips the worker queue. */
5137
5595
  tryFastPathResult(toolName, args) {
5138
5596
  const query = args.query;
5139
5597
  if (typeof query !== 'string')
@@ -5181,11 +5639,13 @@ class ToolHandler {
5181
5639
  : null;
5182
5640
  }
5183
5641
  /** Run exactly one bounded survey family and expose the selected route. */
5184
- runSpecializedExploreRoute(route, cg, query, projectRoot) {
5642
+ runSpecializedExploreRoute(route, cg, query, projectRoot, plan) {
5185
5643
  let section = '';
5186
5644
  let status = 'no_indexed_evidence';
5187
5645
  let coverage = '';
5188
5646
  if (route === 'modules') {
5647
+ if (plan?.relation === 'module_imports')
5648
+ return this.renderModuleImports(cg, plan);
5189
5649
  const manifestResult = this.buildFocusedModuleManifestSection(projectRoot, query);
5190
5650
  const graphResult = manifestResult.manifestCount === 0
5191
5651
  ? this.buildModuleDependencySurveySection(cg, query)
@@ -5203,10 +5663,10 @@ class ToolHandler {
5203
5663
  coverage = 'named path/Type NAPI export registrations only; no domain file dump was built';
5204
5664
  }
5205
5665
  else {
5206
- const apiUsage = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
5666
+ const apiUsage = planFeature(plan, 'shouldBuildApiUsageSurvey', query, query_utils_1.shouldBuildApiUsageSurvey)
5207
5667
  ? this.buildApiUsageSection(cg, query, projectRoot)
5208
5668
  : { section: '', fileCount: 0 };
5209
- const memberUsage = (0, query_utils_1.shouldBuildMemberSurvey)(query)
5669
+ const memberUsage = planFeature(plan, 'shouldBuildMemberSurvey', query, query_utils_1.shouldBuildMemberSurvey)
5210
5670
  && !((0, query_utils_1.queryAsFieldUsageSurvey)(query) && apiUsage.fileCount > 0)
5211
5671
  ? this.buildMemberSurveySection(cg, query, projectRoot)
5212
5672
  : '';
@@ -5224,13 +5684,43 @@ class ToolHandler {
5224
5684
  || (status === 'not_surveyed'
5225
5685
  ? '- No survey ran: this query carries no symbol name to scan. Re-run naming the symbol(s), or use `homegraph_explore`.'
5226
5686
  : '- No matching evidence was found in the current index for this focused survey.');
5227
- return this.textResult(this.truncateOutput([
5687
+ const text = this.truncateOutput([
5228
5688
  `**HomeGraph specialized route: ${route}**`,
5229
5689
  `Status: ${status}`,
5230
5690
  `Coverage: ${coverage}.`,
5231
5691
  '',
5232
5692
  body,
5233
- ].join('\n')));
5693
+ ].join('\n'));
5694
+ return this.exploreResult(text, { projectRoot, query, files: [], sourceBytes: 0,
5695
+ responseBytes: text.length, evidenceStatus: status === 'complete' ? 'complete' : 'empty',
5696
+ partial: status !== 'complete', coveredObligations: status === 'complete' ? [plan?.relation ?? route] : [],
5697
+ uncoveredObligations: status !== 'complete' ? [plan?.relation ?? route] : [] });
5698
+ }
5699
+ /** Directed file import witnesses for an explicit module-import relation. */
5700
+ renderModuleImports(cg, plan) {
5701
+ const scope = [...new Set([...plan.anchors, ...(plan.bindings ?? []).map(node => node.filePath)])]
5702
+ .map(value => value.replace(/\\/g, '/')).filter(Boolean);
5703
+ const paths = cg.getFiles().map(file => file.path).filter(file => scope.some(anchor => file === anchor || file.startsWith(anchor + '/') || file.split('/').includes(anchor)));
5704
+ const rows = [];
5705
+ let scanned = 0;
5706
+ for (const file of paths.slice(0, 120)) {
5707
+ scanned++;
5708
+ for (const target of cg.getFileDependencies(file)) {
5709
+ rows.push(`- \`${file}\` imports → \`${target}\``);
5710
+ if (rows.length >= 40)
5711
+ break;
5712
+ }
5713
+ if (rows.length >= 40)
5714
+ break;
5715
+ }
5716
+ const partial = scanned < paths.length || rows.length >= 40;
5717
+ const text = ['**Directed module imports**',
5718
+ rows.length ? rows.join('\n') : 'No indexed import edges found for the supplied module paths.',
5719
+ `Coverage: ${scanned} of ${paths.length} scoped files scanned; cycle analysis was not requested.`].join('\n\n');
5720
+ return this.exploreResult(text, { projectRoot: cg.getProjectRoot(), query: plan.originalQuery,
5721
+ files: [], sourceBytes: 0, responseBytes: text.length,
5722
+ evidenceStatus: !rows.length ? 'empty' : partial ? 'partial' : 'complete',
5723
+ partial: partial || !rows.length });
5234
5724
  }
5235
5725
  /** Usage sites for a bare symbol bag, including non-call textual references. */
5236
5726
  buildBareSymbolUsageSection(cg, query, projectRoot) {
@@ -5392,8 +5882,8 @@ class ToolHandler {
5392
5882
  /**
5393
5883
  * Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
5394
5884
  */
5395
- tryFastInventoryExplore(cg, query, projectRoot) {
5396
- if (!(0, query_utils_1.shouldTryFastInventoryExplore)(query))
5885
+ tryFastInventoryExplore(cg, query, projectRoot, plan) {
5886
+ if (!planFeature(plan, 'shouldTryFastInventoryExplore', query, query_utils_1.shouldTryFastInventoryExplore))
5397
5887
  return null;
5398
5888
  // Multi-Type dependency asks: inventory-only early exit (avoids fat compact / busy timeout).
5399
5889
  if ((0, query_utils_1.queryAsMultiTypeDependencySurvey)(query)) {
@@ -5759,9 +6249,29 @@ class ToolHandler {
5759
6249
  }
5760
6250
  return finishCompact('Inventory sections above are complete for this query. **ANSWER NOW.**');
5761
6251
  }
5762
- /**
5763
- * Render a compact symbol-bounded slice of one file (lightweight mechanism path).
5764
- */
6252
+ /** Pack already-located ArkTS evidence before legacy windowing can cut it. */
6253
+ tryArktsEvidenceExplore(cg, query, projectRoot, nodes, focusIds, maxChars, maxFiles, plan) {
6254
+ if (process.env.HOMEGRAPH_ARKTS_EVIDENCE_PACKS === '0')
6255
+ return null;
6256
+ if (!nodes.some(n => /\.ets$/i.test(n.filePath) && !n.filePath.startsWith('ohos-sdk:')))
6257
+ return null;
6258
+ const budget = getExploreOutputBudget(cg.getStats().fileCount);
6259
+ const queryPaths = process.env.HOMEGRAPH_ARKTS_QUERY_PATHS !== '0';
6260
+ if (queryPaths)
6261
+ nodes = (0, evidence_paths_1.completeEvidencePathCandidates)(query, nodes, (name, limit) => cg.getQueryBuilder().getNodesByQualifiedNameExact(name, limit), plan);
6262
+ const pathSearch = !queryPaths ? undefined : (0, evidence_paths_1.searchEvidencePaths)({
6263
+ getNode: id => cg.getNode(id),
6264
+ getEdges: (id, direction, kinds, limit, preferred) => cg.getQueryBuilder().getEvidenceEdges(id, direction, kinds, limit, preferred),
6265
+ }, (0, evidence_paths_1.resolveEvidencePathGoal)(query, nodes, focusIds, plan));
6266
+ const result = (0, arkts_evidence_packs_1.buildArktsEvidencePacks)(cg, { projectRoot, query, nodes, focusIds,
6267
+ maxChars: Math.min(budget.maxOutputChars, maxChars ?? budget.maxOutputChars), maxFiles, pathSearch });
6268
+ if (!result)
6269
+ return null;
6270
+ // The pack renderer already supplies neutral guidance and exact source bytes.
6271
+ return { content: [{ type: 'text', text: result.text }], [explore_session_state_1.EXPLORE_EMISSION_KEY]: result.emission,
6272
+ _meta: { homegraphEvidencePacks: result.metadata } };
6273
+ }
6274
+ /** Render a compact symbol-bounded slice on the legacy mechanism path. */
5765
6275
  renderLightMechanismSource(projectRoot, filePath, nodes, maxChars) {
5766
6276
  const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
5767
6277
  if (!absPath || !(0, fs_1.existsSync)(absPath))
@@ -5797,8 +6307,8 @@ class ToolHandler {
5797
6307
  * findRelevantContext. Fast enough for MCP budget; complete enough to avoid
5798
6308
  * agent grep/read loops (token savings).
5799
6309
  */
5800
- tryLightMechanismExplore(cg, query, projectRoot) {
5801
- if (!(0, query_utils_1.shouldTryLightMechanismExplore)(query))
6310
+ tryLightMechanismExplore(cg, query, projectRoot, plan, evidenceMaxChars) {
6311
+ if (!planFeature(plan, 'shouldTryLightMechanismExplore', query, query_utils_1.shouldTryLightMechanismExplore))
5802
6312
  return null;
5803
6313
  const STRUCTURE_KINDS = new Set(['class', 'struct', 'interface', 'component', 'method', 'function']);
5804
6314
  const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
@@ -5968,6 +6478,9 @@ class ToolHandler {
5968
6478
  const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
5969
6479
  const flow = this.buildFlowFromNamedSymbols(cg, `${query} ${seeds.join(' ')}`);
5970
6480
  const hasFlowPath = flow.pathNodeIds.size > 1;
6481
+ const evidence = this.tryArktsEvidenceExplore(cg, query, projectRoot, [...fileNodes.values()].flat().concat([...flow.pathNodeIds].flatMap(id => { const n = cg.getNode(id); return n ? [n] : []; })), new Set([...seedIds, ...flow.pathNodeIds]), evidenceMaxChars, undefined, plan);
6482
+ if (evidence)
6483
+ return evidence;
5971
6484
  const managerSection = this.formatDomainRoleInventory(managerHits.length > 0
5972
6485
  ? managerHits
5973
6486
  : [...seedIds].map((id) => cg.getNode(id)).filter((n) => !!n && (0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens)));
@@ -6435,7 +6948,7 @@ class ToolHandler {
6435
6948
  * Compact explore for local-symbol behavior questions — skips findRelevantContext
6436
6949
  * and caps to 1–2 defining files (avoids the ~24K related-file dump).
6437
6950
  */
6438
- tryCompactLocalSymbolExplore(cg, query, projectRoot) {
6951
+ tryCompactLocalSymbolExplore(cg, query, projectRoot, plan, evidenceMaxChars) {
6439
6952
  // Inventory runs *before* this on the call sites. Do not refuse compact
6440
6953
  // merely because inventory *intent* matched — empty inventory must fall
6441
6954
  // through here (bare callbacks like OnSurfaceChangedCB).
@@ -6449,8 +6962,13 @@ class ToolHandler {
6449
6962
  // Light-mechanism owns domain howtos. Bare "how/如何" NL must NOT veto compact
6450
6963
  // when a local/flag/lifecycle shape already owns the query (flag impact was
6451
6964
  // falling through to a 20k Dynamic-dispatch dump).
6452
- if ((0, query_utils_1.shouldTryLightMechanismExplore)(query))
6965
+ if (planFeature(plan, 'shouldTryLightMechanismExplore', query, query_utils_1.shouldTryLightMechanismExplore))
6453
6966
  return null;
6967
+ // Explicit `…/Foo.ets` path asks need file-scoped full explore, not compact
6968
+ // trails seeded from path-segment homonyms (`/order/` → `order` property).
6969
+ if ((0, query_utils_1.shouldLimitToQueryNamedFile)(query, false, (0, query_utils_1.queryNamesMultipleExploreAnchors)(query))) {
6970
+ return null;
6971
+ }
6454
6972
  if ((0, query_utils_1.queryAsMechanismSurvey)(query)
6455
6973
  && !(0, query_utils_1.queryAsLocalSymbolDetail)(query)
6456
6974
  && !(0, query_utils_1.queryAsAssignedFlagImpactSurvey)(query)
@@ -6610,6 +7128,9 @@ class ToolHandler {
6610
7128
  }
6611
7129
  if (seedIds.size === 0)
6612
7130
  return null;
7131
+ const evidence = this.tryArktsEvidenceExplore(cg, query, projectRoot, [...fileNodes.values()].flat(), seedIds, evidenceMaxChars, undefined, plan);
7132
+ if (evidence)
7133
+ return evidence;
6613
7134
  const pathAffinity = (seedPath, otherPath) => {
6614
7135
  const a = seedPath.replace(/\\/g, '/').split('/');
6615
7136
  const b = otherPath.replace(/\\/g, '/').split('/');
@@ -9071,8 +9592,10 @@ class ToolHandler {
9071
9592
  // One normalization point so the flow-builder, relevance search, and
9072
9593
  // ranking all see the same canonical spelling (Erlang `mod:fn/arity`).
9073
9594
  const query = normalizeQuerySpelling(rawQuery);
9595
+ const plan = readQueryPlan(args);
9596
+ const feature = (name, fallback) => planFeature(plan, name, query, fallback);
9074
9597
  const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
9075
- if (deferKind) {
9598
+ if (deferKind && plan?.source !== 'llm') {
9076
9599
  return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
9077
9600
  }
9078
9601
  const cg = this.getHomeGraph(args.projectPath);
@@ -9080,7 +9603,7 @@ class ToolHandler {
9080
9603
  // Same-bag / call-budget refuse (session view injected by execute).
9081
9604
  const sessionPrior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
9082
9605
  const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionPrior, query);
9083
- if (repeat.refuse) {
9606
+ if (!plan && this.shouldRefuseRepeatedEvidence(repeat, projectRoot)) {
9084
9607
  const text = (0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query);
9085
9608
  return this.exploreResult(text, {
9086
9609
  projectRoot,
@@ -9090,7 +9613,8 @@ class ToolHandler {
9090
9613
  responseBytes: text.length,
9091
9614
  });
9092
9615
  }
9093
- const compactLocal = this.tryFastInventoryExplore(cg, query, projectRoot)
9616
+ // A planned step already tried fast paths once, before entering full explore.
9617
+ const compactLocal = plan ? null : this.tryFastInventoryExplore(cg, query, projectRoot)
9094
9618
  ?? this.tryLightMechanismExplore(cg, query, projectRoot)
9095
9619
  ?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
9096
9620
  if (compactLocal)
@@ -9110,19 +9634,53 @@ class ToolHandler {
9110
9634
  const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
9111
9635
  let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
9112
9636
  const queryFileBasenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
9113
- const interpretationQuery = (0, query_utils_1.queryAsInterpretationSurvey)(query);
9114
- const testOnlyInterpretation = (0, query_utils_1.queryAsTestOnlyInterpretation)(query);
9115
- const crossModuleFlow = (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query);
9637
+ const interpretationQuery = feature('queryAsInterpretationSurvey', query_utils_1.queryAsInterpretationSurvey);
9638
+ const testOnlyInterpretation = feature('queryAsTestOnlyInterpretation', query_utils_1.queryAsTestOnlyInterpretation);
9639
+ const crossModuleFlow = feature('queryAsCrossModuleFlowSurvey', query_utils_1.queryAsCrossModuleFlowSurvey) || plan?.intent === 'flow';
9116
9640
  // Step 1: Find relevant context with generous parameters.
9117
9641
  const contextOpts = interpretationQuery && queryFileBasenames.length === 1
9118
9642
  ? { searchLimit: 6, traversalDepth: 2, maxNodes: 60, minScore: 0.25 }
9119
9643
  : { searchLimit: 8, traversalDepth: 3, maxNodes: 200, minScore: 0.2 };
9120
9644
  const contextQuery = interpretationQuery && queryFileBasenames.length === 1
9121
9645
  ? `${queryFileBasenames[0]} ${query}`
9122
- : query;
9123
- const subgraph = await cg.findRelevantContext(contextQuery, contextOpts);
9646
+ : queryFileBasenames.length === 1
9647
+ ? `${queryFileBasenames[0]} ${query}`
9648
+ : query;
9649
+ const subgraph = await cg.findRelevantContext(contextQuery, {
9650
+ ...contextOpts,
9651
+ ...(plan && (plan.source === 'llm' || plan.literalTexts?.length) ? { retrievalHints: {
9652
+ symbols: plan.anchors.filter((anchor) => !(plan.bindings ?? []).some((node) => anchor === node.name || anchor === node.qualifiedName)),
9653
+ searchTerms: plan.searchTerms, literalTexts: plan.literalTexts, sourceScope: plan.sourceScope, nodeIds: (plan.bindings ?? []).map((node) => node.id),
9654
+ } } : {}),
9655
+ });
9656
+ // Path-first: always seed nodes from an explicit `Foo.ets` basename so a
9657
+ // CJK-only ask + path (or a shared prop like showSearchIcon) cannot leave
9658
+ // the named file out of the subgraph / digests.
9659
+ if (queryFileBasenames.length > 0 && plan?.sourceScope !== 'sdk') {
9660
+ for (const base of queryFileBasenames.slice(0, 3)) {
9661
+ let hits = [];
9662
+ try {
9663
+ hits = cg.searchNodes(base, { limit: 50 });
9664
+ }
9665
+ catch {
9666
+ continue;
9667
+ }
9668
+ for (const r of hits) {
9669
+ if (!(0, query_utils_1.fileMatchesQueryBasename)(r.node.filePath, [base]))
9670
+ continue;
9671
+ if (!subgraph.nodes.has(r.node.id)) {
9672
+ subgraph.nodes.set(r.node.id, r.node);
9673
+ subgraph.roots.push(r.node.id);
9674
+ }
9675
+ }
9676
+ }
9677
+ }
9678
+ const literalSource = this.renderLiteralSource(cg, subgraph);
9124
9679
  if (subgraph.nodes.size === 0) {
9125
- return this.textResult(`No relevant code found for "${query}"`);
9680
+ const text = literalSource.text || `No relevant code found for "${query}"`;
9681
+ return this.exploreResult(text, { projectRoot, query, files: literalSource.files,
9682
+ sourceBytes: literalSource.files.reduce((sum, file) => sum + file.bytes, 0), responseBytes: text.length,
9683
+ locatedNodes: literalSource.nodes, partial: true, evidenceStatus: literalSource.text ? 'partial' : 'empty' });
9126
9684
  }
9127
9685
  // Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
9128
9686
  const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
@@ -9261,8 +9819,8 @@ class ToolHandler {
9261
9819
  namedParts.push(m[3]);
9262
9820
  }
9263
9821
  const tokens = [...new Set([
9264
- ...namedParts,
9265
- ...query.split(/[\s,()[\]]+/)
9822
+ ...(plan?.source === 'llm' ? plan.anchors : namedParts),
9823
+ ...(plan?.source === 'llm' ? plan.anchors.join(' ') : query).split(/[\s,()[\]]+/)
9266
9824
  .map((t) => t.replace(FILE_EXT, '').trim())
9267
9825
  .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)),
9268
9826
  ])].slice(0, 16);
@@ -9289,7 +9847,8 @@ class ToolHandler {
9289
9847
  const isQual = /[.\/]|::/.test(t);
9290
9848
  const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
9291
9849
  let cands = raw
9292
- .filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath))
9850
+ .filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath)
9851
+ && !(plan?.sourceScope === 'local' && (0, arkts_1.isOhosApiFilePath)(n.filePath)))
9293
9852
  .sort((a, b) => {
9294
9853
  // Prefer callables over types when both share a name, then body size.
9295
9854
  const ac = CALLABLE.has(a.kind) ? 1 : 0;
@@ -9504,6 +10063,8 @@ class ToolHandler {
9504
10063
  }
9505
10064
  fileGroups.set(node.filePath, group);
9506
10065
  }
10066
+ for (const group of fileGroups.values())
10067
+ group.nodes = (0, evidence_rendering_1.canonicalSourceDeclarations)(group.nodes);
9507
10068
  if (testOnlyInterpretation) {
9508
10069
  for (const [, group] of fileGroups) {
9509
10070
  const fp = group.nodes[0]?.filePath ?? '';
@@ -9699,6 +10260,15 @@ class ToolHandler {
9699
10260
  const sortedFiles = relevantFiles.sort((a, b) => {
9700
10261
  const aPath = a[0].toLowerCase();
9701
10262
  const bPath = b[0].toLowerCase();
10263
+ if (plan?.sourceScope === 'local') {
10264
+ const sdkOrder = Number((0, arkts_1.isOhosApiFilePath)(a[0])) - Number((0, arkts_1.isOhosApiFilePath)(b[0]));
10265
+ if (sdkOrder)
10266
+ return sdkOrder;
10267
+ }
10268
+ const literalOrder = Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === b[0]))
10269
+ - Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === a[0]));
10270
+ if (literalOrder)
10271
+ return literalOrder;
9702
10272
  // Query-named file (LocationController.ets in the question) before partial
9703
10273
  // substring matches (control.ets matching "Controller" inside LocationController).
9704
10274
  const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
@@ -9758,6 +10328,8 @@ class ToolHandler {
9758
10328
  '',
9759
10329
  ];
9760
10330
  const summaryLineIdx = 2;
10331
+ if (literalSource.text)
10332
+ lines.push(literalSource.text);
9761
10333
  if (testOnlyInterpretation) {
9762
10334
  lines.push('> **Test-file scope only** — answer from the named `.test.ets` file below; ' +
9763
10335
  'production handlers are out of scope unless explicitly referenced in the test.');
@@ -9782,30 +10354,30 @@ class ToolHandler {
9782
10354
  : { section: '', symbolCount: 0 };
9783
10355
  if (kitUsageResult.section)
9784
10356
  lines.push(kitUsageResult.section);
9785
- const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query)
10357
+ const domainFileResult = feature('shouldBuildDomainFileSurvey', query_utils_1.shouldBuildDomainFileSurvey)
9786
10358
  ? this.buildDomainFileSurveySection(cg, query)
9787
10359
  : { section: '', fileCount: 0 };
9788
10360
  if (domainFileResult.section)
9789
10361
  lines.push(domainFileResult.section);
9790
- const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
9791
- && !(0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
10362
+ const apiUsageResult = feature('shouldBuildApiUsageSurvey', query_utils_1.shouldBuildApiUsageSurvey)
10363
+ && !feature('shouldBuildKitModuleUsageSurvey', query_utils_1.shouldBuildKitModuleUsageSurvey)
9792
10364
  ? this.buildApiUsageSection(cg, query, projectRoot)
9793
10365
  : { section: '', fileCount: 0 };
9794
10366
  if (apiUsageResult.section)
9795
10367
  lines.push(apiUsageResult.section);
9796
- const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
10368
+ const dataSourceResult = feature('queryAsDataSourceSurvey', query_utils_1.queryAsDataSourceSurvey)
9797
10369
  ? this.buildDataSourceSection(cg, query)
9798
10370
  : { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
9799
10371
  if (dataSourceResult.section)
9800
10372
  lines.push(dataSourceResult.section);
9801
- const eventDispatchResult = (0, query_utils_1.queryAsEventDispatchSurvey)(query)
10373
+ const eventDispatchResult = feature('queryAsEventDispatchSurvey', query_utils_1.queryAsEventDispatchSurvey)
9802
10374
  ? this.buildEventDispatchSection(cg, query, projectRoot)
9803
10375
  : { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
9804
10376
  if (eventDispatchResult.section)
9805
10377
  lines.push(eventDispatchResult.section);
9806
10378
  const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
9807
10379
  const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query) || crossModuleFlow;
9808
- const mechanismSurvey = (0, query_utils_1.queryAsMechanismSurvey)(query);
10380
+ const mechanismSurvey = feature('queryAsMechanismSurvey', query_utils_1.queryAsMechanismSurvey);
9809
10381
  // Flow path — computed before omit-source so graph connectivity drives the decision,
9810
10382
  // not question-text keyword matching. Mechanism/cross-module surveys augment the
9811
10383
  // query with seeded entry symbol names so buildFlowFromNamedSymbols can connect them.
@@ -9830,6 +10402,13 @@ class ToolHandler {
9830
10402
  }
9831
10403
  const flow = this.buildFlowFromNamedSymbols(cg, flowQuery);
9832
10404
  const hasFlowPath = flow.pathNodeIds.size > 0;
10405
+ // Keep literal/resource and specialized inventory rendering intact. This
10406
+ // first batch changes symbol-grounded structural evidence only.
10407
+ if ((hasFlowPath || flow.text.length > 0 || plan?.intent === 'flow') && !subgraph.literalEvidence?.hits.length) {
10408
+ const evidence = this.tryArktsEvidenceExplore(cg, query, projectRoot, [...subgraph.nodes.values()].concat([...flow.pathNodeIds].flatMap(id => { const n = cg.getNode(id); return n ? [n] : []; })), new Set([...subgraph.roots, ...flow.namedNodeIds, ...flow.pathNodeIds]), args._hgEvidenceMaxChars, explicitMaxFiles ? maxFiles : undefined, plan);
10409
+ if (evidence)
10410
+ return evidence;
10411
+ }
9833
10412
  budget = tightenExploreBudgetForQuery(budget, query, { hasFlowPath });
9834
10413
  // Honor an explicit maxFiles from the caller — budget.defaultMaxFiles is only
9835
10414
  // a default when the agent didn't ask for more (adaptive sibling tests pass 12).
@@ -10036,6 +10615,17 @@ class ToolHandler {
10036
10615
  const priorCalls = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
10037
10616
  const dedupEnabled = (0, explore_dedup_1.exploreDedupEnabled)() && (priorCalls?.calls.length ?? 0) > 0;
10038
10617
  const emittedByFile = new Map();
10618
+ // Session emissions may include prior-call coverage or skeleton envelopes.
10619
+ // Dependency receipts instead require *fresh*, actually printed source and
10620
+ // a whole surviving file section (never the tail cut by the hard ceiling).
10621
+ const freshRangesByFile = new Map();
10622
+ const sourceEndByFile = new Map();
10623
+ const noteFreshSource = (fp, ranges) => {
10624
+ if (plan?.source !== 'llm' || !ranges.length)
10625
+ return;
10626
+ freshRangesByFile.set(fp, [...(freshRangesByFile.get(fp) ?? []), ...ranges]);
10627
+ sourceEndByFile.set(fp, lines.join('\n').length);
10628
+ };
10039
10629
  const noteEmitted = (fp, ranges, bytes, fingerprint) => {
10040
10630
  const existing = emittedByFile.get(fp);
10041
10631
  if (existing) {
@@ -10207,6 +10797,7 @@ class ToolHandler {
10207
10797
  }
10208
10798
  if (body.length > 0) {
10209
10799
  lines.push('```' + lang, body, '```', '');
10800
+ noteFreshSource(filePath, ranges);
10210
10801
  totalChars += body.length + lang.length + 11;
10211
10802
  noteEmitted(filePath, [...ranges, ...opts.covered], body.length, fingerprint);
10212
10803
  renderedFilePaths.push(filePath);
@@ -10293,6 +10884,7 @@ class ToolHandler {
10293
10884
  // signature line (capped, with a "+N more" tail so the structure map of a
10294
10885
  // god-file doesn't itself bloat the budget).
10295
10886
  const skel = [];
10887
+ const freshSkelRanges = [];
10296
10888
  let coveredUntil = 0; // skip symbols already inside an emitted body
10297
10889
  let sigCount = 0, sigDropped = 0;
10298
10890
  const SIG_MAX = Math.max(12, budget.maxSymbolsInFileHeader * 2);
@@ -10303,6 +10895,7 @@ class ToolHandler {
10303
10895
  const end = n.endLine;
10304
10896
  const body = fileLines.slice(n.startLine - 1, end).join('\n');
10305
10897
  skel.push(exploreLineNumbersEnabled() ? numberSourceLines(body, n.startLine) : body);
10898
+ freshSkelRanges.push({ start: n.startLine, end });
10306
10899
  coveredUntil = end;
10307
10900
  }
10308
10901
  else {
@@ -10325,6 +10918,7 @@ class ToolHandler {
10325
10918
  if (sig) {
10326
10919
  skel.push(exploreLineNumbersEnabled() ? `${lineNo}\t${sig}` : sig);
10327
10920
  sigCount++;
10921
+ freshSkelRanges.push({ start: lineNo, end: lineNo });
10328
10922
  }
10329
10923
  }
10330
10924
  }
@@ -10362,6 +10956,7 @@ class ToolHandler {
10362
10956
  return n ? { start: n.startLine, end: n.endLine || n.startLine } : null;
10363
10957
  }).filter((r) => !!r);
10364
10958
  lines.push(skelHeader, '', '```' + lang, skelBody, '```', '');
10959
+ noteFreshSource(filePath, freshSkelRanges);
10365
10960
  totalChars += skelBody.length + 120;
10366
10961
  noteEmitted(filePath, bodyRanges.length > 0 ? bodyRanges : [wholeRange], skelBody.length, fingerprint);
10367
10962
  renderedFilePaths.push(filePath);
@@ -10394,7 +10989,15 @@ class ToolHandler {
10394
10989
  ? Math.min(Math.max(0, budget.maxOutputChars - totalChars - 200), Math.round(budget.maxCharsPerFile * 1.5))
10395
10990
  : budget.maxCharsPerFile * 3;
10396
10991
  if (fileLines.length <= WHOLE_FILE_MAX_LINES && fileContent.length <= WHOLE_FILE_MAX_CHARS) {
10397
- const wholeRange = { start: 1, end: Math.max(1, fileLines.length) };
10992
+ let sourceStart = 0;
10993
+ if (fileLines[0]?.trim().startsWith('/*')) {
10994
+ const endComment = fileLines.findIndex((line) => line.includes('*/'));
10995
+ if (endComment >= 0 && endComment < 40)
10996
+ sourceStart = endComment + 1;
10997
+ }
10998
+ while (sourceStart < fileLines.length - 1 && !fileLines[sourceStart]?.trim())
10999
+ sourceStart++;
11000
+ const wholeRange = { start: sourceStart + 1, end: Math.max(1, fileLines.length) };
10398
11001
  const dd = (0, explore_dedup_1.dedupeRange)(wholeRange, served);
10399
11002
  const uniqSymbols = [...new Set(group.nodes
10400
11003
  .filter(n => n.kind !== 'import' && n.kind !== 'export')
@@ -10855,6 +11458,7 @@ class ToolHandler {
10855
11458
  const output = flow.text + lines.join('\n');
10856
11459
  const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
10857
11460
  let finalText;
11461
+ let sourceCutoff = output.length;
10858
11462
  if (output.length > hardCeiling) {
10859
11463
  // Prefer dropping trailing notes ("Not shown above", completeness, budget)
10860
11464
  // over dropping a whole file's source — notes are recoverable, source isn't.
@@ -10869,8 +11473,10 @@ class ToolHandler {
10869
11473
  if (trimmed.length <= hardCeiling)
10870
11474
  break;
10871
11475
  const at = trimmed.lastIndexOf(marker);
10872
- if (at > hardCeiling * 0.4)
11476
+ if (at > hardCeiling * 0.4) {
10873
11477
  trimmed = trimmed.slice(0, at).replace(/\n+$/, '');
11478
+ sourceCutoff = Math.min(sourceCutoff, trimmed.length);
11479
+ }
10874
11480
  }
10875
11481
  if (trimmed.length > hardCeiling) {
10876
11482
  // Cut at a FILE-SECTION boundary so we drop whole trailing file-sections
@@ -10899,6 +11505,7 @@ class ToolHandler {
10899
11505
  boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
10900
11506
  }
10901
11507
  const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
11508
+ sourceCutoff = Math.min(sourceCutoff, safe.length);
10902
11509
  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.)';
10903
11510
  }
10904
11511
  else {
@@ -10949,14 +11556,85 @@ class ToolHandler {
10949
11556
  });
10950
11557
  sourceBytes += emitted.bytes;
10951
11558
  }
11559
+ // Do not promote unseen subgraph hits or a large parent's unshown header.
11560
+ // The declaration's starting line must be in a surviving, fresh source span.
11561
+ const rootIds = new Set(subgraph.roots);
11562
+ for (const file of literalSource.files) {
11563
+ if (!finalText.includes(literalSource.text))
11564
+ break;
11565
+ emittedFiles.push(file);
11566
+ sourceBytes += file.bytes;
11567
+ }
11568
+ const locatedNodes = plan?.source === 'llm' ? [...(finalText.includes(literalSource.text) ? literalSource.nodes : []), ...emittedFiles.flatMap((file) => staleRendered.includes(file.path) || flow.text.length + (sourceEndByFile.get(file.path) ?? Infinity) > sourceCutoff
11569
+ ? [] : (fileGroups.get(file.path)?.nodes ?? []).filter((node) => !['file', 'import', 'export', 'parameter'].includes(node.kind)
11570
+ && (freshRangesByFile.get(file.path) ?? []).some((range) => node.startLine >= range.start && node.startLine <= range.end)))]
11571
+ .sort((a, b) => Number(rootIds.has(b.id)) - Number(rootIds.has(a.id)))
11572
+ .filter((node, i, nodes) => nodes.findIndex((other) => other.id === node.id) === i)
11573
+ .slice(0, 32).map((node) => ({ id: node.id, name: node.name, qualifiedName: node.qualifiedName,
11574
+ filePath: node.filePath, startLine: node.startLine })) : undefined;
10952
11575
  return this.exploreResult(finalText, {
10953
11576
  projectRoot,
10954
11577
  query,
10955
11578
  files: emittedFiles,
10956
11579
  sourceBytes,
11580
+ evidenceStatus: sourceBytes > 0 ? (anyFileTrimmed || subgraph.confidence === 'low' || sourceCutoff < output.length
11581
+ || (plan?.source === 'llm' && !(locatedNodes?.length)) ? 'partial' : 'complete')
11582
+ : filesIncluded > 0 && /HarmonyOS SDK API/.test(finalText) ? 'sdk-only' : 'empty',
10957
11583
  responseBytes: finalText.length,
11584
+ ...(locatedNodes ? { locatedNodes } : {}),
10958
11585
  });
10959
11586
  }
11587
+ /** Render literal witnesses before graph-heavy sections, including unindexed UI. */
11588
+ renderLiteralSource(cg, subgraph) {
11589
+ const sections = [];
11590
+ const files = [];
11591
+ const nodes = [];
11592
+ const seen = new Set();
11593
+ let chars = 0;
11594
+ for (const hit of subgraph.literalEvidence?.hits ?? []) {
11595
+ if (files.length >= 2 || seen.has(hit.filePath))
11596
+ continue;
11597
+ const absolute = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), hit.filePath);
11598
+ if (!absolute)
11599
+ continue;
11600
+ let source;
11601
+ try {
11602
+ source = (0, fs_1.readFileSync)(absolute, 'utf8');
11603
+ }
11604
+ catch {
11605
+ continue;
11606
+ }
11607
+ const lines = source.split('\n');
11608
+ // Never serve an earlier witness against a changed file.
11609
+ if (lines.slice(hit.startLine - 1, hit.endLine).join('\n') !== hit.text)
11610
+ continue;
11611
+ const containing = this.isFileStaleOnDisk(cg, hit.filePath, source) ? undefined
11612
+ : (0, evidence_rendering_1.canonicalSourceDeclarations)(cg.getNodesInFile(hit.filePath))
11613
+ .filter(node => !['file', 'import', 'export', 'parameter'].includes(node.kind)
11614
+ && !node.name.startsWith('%') && node.name !== 'constructor'
11615
+ && node.startLine <= hit.line && node.endLine >= hit.line)
11616
+ .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
11617
+ const ranges = [{ start: hit.startLine, end: hit.endLine }];
11618
+ if (containing && containing.startLine < hit.startLine) {
11619
+ ranges.unshift({ start: containing.startLine, end: Math.min(containing.startLine + 2, hit.startLine - 1) });
11620
+ }
11621
+ const body = ranges.map(range => lines.slice(range.start - 1, range.end)
11622
+ .map((line, index) => `${range.start + index}\t${line}`).join('\n')).join('\n... (gap) ...\n');
11623
+ const resource = hit.resource
11624
+ ? `\nResource: \`${hit.resource.filePath}:${hit.resource.line}\` — ${JSON.stringify(hit.resource.value)} → \`${hit.resource.key}\`.` : '';
11625
+ const section = `**Literal source witness: \`${hit.filePath}:${hit.line}\`**${resource}\n\n\`\`\`\n${body}\n\`\`\``;
11626
+ if (chars + section.length > 3000)
11627
+ continue;
11628
+ sections.push(section);
11629
+ chars += section.length;
11630
+ seen.add(hit.filePath);
11631
+ files.push({ path: hit.filePath, ranges, bytes: body.length, fingerprint: (0, explore_dedup_1.fileFingerprint)(source) });
11632
+ if (containing)
11633
+ nodes.push({ id: containing.id, name: containing.name,
11634
+ qualifiedName: containing.qualifiedName, filePath: containing.filePath, startLine: containing.startLine });
11635
+ }
11636
+ return { text: sections.join('\n\n'), files, nodes };
11637
+ }
10960
11638
  /**
10961
11639
  * An explore response plus the record of what it emitted (CG-17). The record
10962
11640
  * rides the result only as far as {@link execute}, which files it into the
@@ -10967,7 +11645,8 @@ class ToolHandler {
10967
11645
  const result = this.textResult(text);
10968
11646
  result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
10969
11647
  ...emission,
10970
- partial: emission.partial ?? meta.partial,
11648
+ evidenceStatus: emission.evidenceStatus ?? (emission.sourceBytes > 0 ? (meta.partial ? 'partial' : 'complete') : 'partial'),
11649
+ partial: emission.partial ?? (emission.evidenceStatus && emission.evidenceStatus !== 'complete' ? true : meta.partial),
10971
11650
  nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
10972
11651
  };
10973
11652
  return result;
@@ -10993,7 +11672,8 @@ class ToolHandler {
10993
11672
  files: [],
10994
11673
  sourceBytes: 0,
10995
11674
  responseBytes: text.length,
10996
- partial: meta.partial,
11675
+ partial: true,
11676
+ evidenceStatus: /HarmonyOS SDK API|ohos-sdk:/.test(text) ? 'sdk-only' : /No relevant code|No matching evidence|No survey ran/.test(text) ? 'empty' : 'partial',
10997
11677
  nextAnchor: meta.nextAnchor,
10998
11678
  };
10999
11679
  return result;
@@ -11028,7 +11708,7 @@ class ToolHandler {
11028
11708
  const symbol = this.validateString(args.symbol, 'symbol');
11029
11709
  if (typeof symbol !== 'string')
11030
11710
  return symbol;
11031
- let matches = this.findSymbolMatches(cg, symbol);
11711
+ let matches = (0, evidence_rendering_1.canonicalSourceDeclarations)(this.findSymbolMatches(cg, symbol));
11032
11712
  if (matches.length === 0) {
11033
11713
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
11034
11714
  }
@@ -11397,21 +12077,18 @@ class ToolHandler {
11397
12077
  lines.push(`**Build phase:** ${cg.getBuildPhase()}`, `**Files indexed:** ${stats.fileCount}`, `**Total nodes:** ${stats.nodeCount}`, `**Total edges:** ${stats.edgeCount}`, `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`, ...(stats.walSizeBytes > 0
11398
12078
  ? [`**WAL size:** ${(stats.walSizeBytes / 1024 / 1024).toFixed(2)} MB`]
11399
12079
  : []), `**Graph sources:** ${cg.getGraphSources()}`);
11400
- // Surface the active SQLite backend: node:sqlite → better-sqlite3 → wasm.
12080
+ // Surface the active SQLite backend: node:sqlite → wasm.
11401
12081
  const backend = cg.getBackend();
11402
12082
  if (backend === 'node-sqlite') {
11403
12083
  lines.push(`**Backend:** node-sqlite (built-in)`);
11404
12084
  }
11405
- else if (backend === 'native') {
11406
- lines.push(`**Backend:** native (better-sqlite3)`);
11407
- }
11408
12085
  else {
11409
12086
  lines.push(`**Backend:** ⚠ wasm (no WAL backend available) — ` +
11410
12087
  `5-10x slower than WAL. Fix: ${sqlite_adapter_1.WASM_FALLBACK_FIX_RECIPE}`);
11411
12088
  }
11412
12089
  // Effective journal mode. 'wal' ⇒ concurrent reads never block on a writer;
11413
- // anything else ⇒ they can ("database is locked"). node:sqlite / native
11414
- // support WAL; wasm remaps to DELETE.
12090
+ // anything else ⇒ they can ("database is locked"). node:sqlite supports WAL;
12091
+ // wasm remaps to DELETE.
11415
12092
  const journalMode = cg.getJournalMode();
11416
12093
  if (journalMode === 'wal') {
11417
12094
  lines.push(`**Journal mode:** wal (concurrent reads safe)`);
@@ -11464,10 +12141,27 @@ class ToolHandler {
11464
12141
  */
11465
12142
  async handleProject(args) {
11466
12143
  const cg = this.getHomeGraph(args.projectPath);
11467
- const phase = cg.getBuildPhase();
11468
- if (phase === 'building_fast') {
11469
- return this.textResult('Fast project map is still building — retry in a few seconds.');
12144
+ const state = (0, index_availability_1.resolveProductIndexState)(cg);
12145
+ if (state === 'empty') {
12146
+ return this.textResult((0, index_availability_1.productIndexGuidance)('empty'));
12147
+ }
12148
+ if (state === 'syncing') {
12149
+ // Prefer returning the map when readable; only hard-stop if phase has no map.
12150
+ try {
12151
+ const peek = cg.getProjectMap({ includeFiles: false });
12152
+ if (peek.modules.length === 0) {
12153
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
12154
+ }
12155
+ }
12156
+ catch (err) {
12157
+ const msg = err instanceof Error ? err.message : String(err);
12158
+ if ((0, index_availability_1.isSqliteBusyMessage)(msg)) {
12159
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
12160
+ }
12161
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
12162
+ }
11470
12163
  }
12164
+ const phase = cg.getBuildPhase();
11471
12165
  let map = cg.getProjectMap({
11472
12166
  module: typeof args.module === 'string' ? args.module : undefined,
11473
12167
  includeFiles: args.includeFiles !== false,
@@ -11486,6 +12180,9 @@ class ToolHandler {
11486
12180
  }
11487
12181
  catch (err) {
11488
12182
  const message = err instanceof Error ? err.message : String(err);
12183
+ if ((0, index_availability_1.isSqliteBusyMessage)(message)) {
12184
+ return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
12185
+ }
11489
12186
  return this.textResult(`Failed to build project map: ${message}`);
11490
12187
  }
11491
12188
  }
@@ -11493,14 +12190,18 @@ class ToolHandler {
11493
12190
  return this.textResult('No modules found in the project map.');
11494
12191
  }
11495
12192
  const FILE_CAP = 80;
12193
+ const productState = (0, index_availability_1.resolveProductIndexState)(cg);
11496
12194
  const lines = [
11497
- `**Project map** (phase=${map.phase})`,
12195
+ `**Project map** (status=${productState}, phase=${map.phase})`,
11498
12196
  `modules: ${map.modules.length} · files: ${map.fileCount}`,
11499
12197
  '',
11500
12198
  ];
11501
- if (map.phase === 'fast' || map.phase === 'indexing') {
12199
+ if (productState === 'fast' || map.phase === 'fast' || map.phase === 'indexing') {
11502
12200
  lines.push('_Full symbol index still building — this map has modules/files only (no call graph)._', '');
11503
12201
  }
12202
+ if (productState === 'syncing') {
12203
+ lines.push('_Index write in progress — map may be briefly stale._', '');
12204
+ }
11504
12205
  for (const m of map.modules) {
11505
12206
  const rootLabel = m.rootPath || '.';
11506
12207
  lines.push(`### ${m.name} (\`${rootLabel}\`) · ${m.kind} · ${m.fileCount} files`);
@@ -12396,6 +13097,9 @@ class ToolHandler {
12396
13097
  // Line-numbered (cat -n style, like homegraph_explore and Read) so the
12397
13098
  // agent can cite/edit exact lines without re-Reading the file for them.
12398
13099
  const numbered = node.startLine ? numberSourceLines(code, node.startLine) : code;
13100
+ if (node.startLine && process.env.HOMEGRAPH_SOURCE_RECEIPTS === '1') {
13101
+ lines.push('', (0, source_slice_identity_1.sourceSliceIdentity)(node.filePath, node.startLine, code));
13102
+ }
12399
13103
  lines.push('', '```' + node.language, numbered, '```');
12400
13104
  }
12401
13105
  return lines.join('\n');
@@ -12404,7 +13108,7 @@ class ToolHandler {
12404
13108
  return {
12405
13109
  // Single choke point for every tool's text, so no section builder can ship
12406
13110
  // a stop-searching directive on an output that declares itself partial.
12407
- content: [{ type: 'text', text: reconcilePartialAnswerNow(text) }],
13111
+ content: [{ type: 'text', text: (0, evidence_rendering_1.neutralRetrievalGuidance)(reconcilePartialAnswerNow(text)) }],
12408
13112
  };
12409
13113
  }
12410
13114
  /**