mobile-debug-mcp 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +16 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,11 @@
1
+ import type { AndroidInteract } from './android.js'
2
+ import type { ActionTargetResolved, DeviceInfo } from '../types.js'
3
+
4
+ type AdjustmentResult = { success?: boolean; device?: DeviceInfo; error?: string }
5
+ type AdjustmentTarget = { deviceId: string; target: ActionTargetResolved | null; property: string }
6
+
7
+ /** Common native input surface, with optional semantic control capabilities. */
8
+ export type InputService = Pick<AndroidInteract, 'tap' | 'swipe' | 'scrollToElement'> & {
9
+ setAdjustableValue?(input: AdjustmentTarget & { value: number }): Promise<AdjustmentResult>
10
+ adjustAccessibleValue?(input: AdjustmentTarget & { direction: 'increment' | 'decrement' }): Promise<AdjustmentResult>
11
+ }
@@ -0,0 +1,36 @@
1
+ import { systemClock, type Clock } from '../runtime/clock.js'
2
+ import type { ActionTrace, TraceStep } from '../types.js'
3
+ import { createTraceStep, nextActionId } from '../runtime/action-results.js'
4
+
5
+ export function buildObservationTrace({
6
+ actionType,
7
+ stage,
8
+ success,
9
+ attempts,
10
+ metadata
11
+ }: {
12
+ actionType: string
13
+ stage: 'verify' | 'stabilize' | 'resolve'
14
+ success: boolean
15
+ attempts: number
16
+ metadata?: Record<string, unknown>
17
+ }, clock: Clock = systemClock): ActionTrace {
18
+ const now = clock.now()
19
+ const actionId = nextActionId(actionType, now)
20
+ const steps: TraceStep[] = [
21
+ createTraceStep({
22
+ stage,
23
+ timestamp: now,
24
+ result: success ? 'success' : 'failure',
25
+ attemptIndex: 0,
26
+ metadata
27
+ })
28
+ ]
29
+
30
+ return {
31
+ action_id: actionId,
32
+ steps,
33
+ final_outcome: success ? 'success' : 'failure',
34
+ attempts: Math.max(1, Math.floor(attempts || 1))
35
+ }
36
+ }
@@ -0,0 +1,109 @@
1
+ import type { UIElementState, UIElementSemanticMetadata } from '../types.js'
2
+ export interface ScreenFingerprintResponse { fingerprint: string | null }
3
+
4
+ export interface UiElement {
5
+ text?: string | null
6
+ label?: string | null
7
+ value?: string | null
8
+ contentDescription?: string | null
9
+ contentDesc?: string | null
10
+ accessibilityLabel?: string | null
11
+ resourceId?: string | null
12
+ resourceID?: string | null
13
+ id?: string | null
14
+ type?: string | null
15
+ class?: string | null
16
+ bounds?: number[] | null
17
+ clickable?: boolean
18
+ enabled?: boolean
19
+ focusable?: boolean
20
+ visible?: boolean
21
+ parentId?: number | string | null
22
+ children?: number[]
23
+ _index?: number
24
+ _interactable?: boolean
25
+ _sliderLike?: boolean
26
+ state?: UIElementState | null
27
+ stable_id?: string | null
28
+ role?: string | null
29
+ test_tag?: string | null
30
+ selector?: { value: string | null, confidence: { score: number, reason: string } | null } | null
31
+ semantic?: UIElementSemanticMetadata | null
32
+ actions?: string[] | null
33
+ accessibilityActions?: string[] | null
34
+ runtimeActions?: string[] | null
35
+ platformActions?: string[] | null
36
+ }
37
+
38
+ export interface ResolvedUiElementContext {
39
+ elementId: string
40
+ platform: 'android' | 'ios'
41
+ deviceId?: string
42
+ bounds: [number, number, number, number] | null
43
+ index: number
44
+ stable_id?: string | null
45
+ }
46
+
47
+ export interface UiResolution {
48
+ width?: number
49
+ height?: number
50
+ }
51
+
52
+ export interface UiChangeSignatureSet {
53
+ hierarchy: string | null
54
+ text: string | null
55
+ state: string | null
56
+ }
57
+
58
+ export interface UiChangeScopeResolution {
59
+ scope: 'screen' | 'subtree'
60
+ target: string | null
61
+ resolved: boolean
62
+ resolvedIndex: number | null
63
+ resolvedStableId: string | null
64
+ reason: string
65
+ }
66
+
67
+ export interface UiChangeScopeResult {
68
+ elements: UiElement[]
69
+ resolution: UiChangeScopeResolution
70
+ error?: {
71
+ code: 'INVALID_SCOPE' | 'ELEMENT_NOT_FOUND'
72
+ message: string
73
+ }
74
+ }
75
+
76
+ export interface RankedResolutionCandidate {
77
+ el: UiElement
78
+ idx: number
79
+ score: number
80
+ reason: string
81
+ interactable: boolean
82
+ }
83
+
84
+ export interface InferredLabeledSliderState {
85
+ current: number
86
+ min: number
87
+ max: number
88
+ value_range: { min: number; max: number }
89
+ touch_bounds: [number, number, number, number]
90
+ source: 'nearby_labels'
91
+ }
92
+
93
+ export interface FindElementResolutionSummary {
94
+ confidence: number
95
+ reason: string
96
+ fallback_available: boolean
97
+ matched_count: number
98
+ alternates: Array<{
99
+ text: string | null
100
+ resource_id: string | null
101
+ accessibility_id: string | null
102
+ class: string | null
103
+ bounds: { left: number; top: number; right: number; bottom: number } | null
104
+ clickable: boolean
105
+ enabled: boolean
106
+ score: number
107
+ reason: string
108
+ }>
109
+ }
@@ -0,0 +1,7 @@
1
+ import type { GetUITreeResponse } from '../types.js'
2
+ import type { UiElement } from './model.js'
3
+
4
+ export type UiTree = Omit<Partial<GetUITreeResponse>, 'elements'> & { elements?: UiElement[] }
5
+ import type { DeviceTarget } from '../contracts/target.js'
6
+ export type { DeviceTarget } from '../contracts/target.js'
7
+ export type ObserveUI = (target: DeviceTarget) => Promise<UiTree>
@@ -0,0 +1,49 @@
1
+ import type { UiElement, RankedResolutionCandidate, UiResolution } from '../model.js'
2
+ import { _isSemanticActionable } from './element-helpers.js'
3
+ import { nearbyControl } from './nearby-control.js'
4
+
5
+ type Match = { el: UiElement; idx: number }
6
+ function parentOf(elements: UiElement[], parent: number | string) {
7
+ const index = typeof parent === 'number' ? parent : /^\d+$/.test(parent) ? Number(parent) : null
8
+ if (index !== null && elements[index]) return { el: elements[index], idx: index }
9
+ if (typeof parent !== 'string') return null
10
+ const found = elements.findIndex(el => el.resourceId === parent || el.id === parent)
11
+ return found >= 0 && elements[found] ? { el: elements[found], idx: found } : null
12
+ }
13
+ function ancestor(elements: UiElement[], chosen: Match): Match | null {
14
+ if (!Array.isArray(chosen.el.bounds) || chosen.el.parentId == null) return null
15
+ let current = chosen, safety = 0
16
+ while (safety < 20 && !(current.el.clickable || current.el.focusable) && current.el.parentId != null) {
17
+ const parent = parentOf(elements, current.el.parentId)
18
+ if (!parent) break
19
+ current = parent
20
+ if (current.el.clickable || current.el.enabled || current.el.focusable) return current
21
+ safety++
22
+ }
23
+ return null
24
+ }
25
+ function containing(elements: UiElement[], chosen: Match): Match | null {
26
+ const child = chosen.el.bounds
27
+ if (!Array.isArray(child)) return null
28
+ let best: Match | null = null, area = Infinity
29
+ for (let idx = 0; idx < elements.length; idx++) {
30
+ const el = elements[idx]
31
+ if (!el || !(el.clickable || el.focusable) || !Array.isArray(el.bounds) || el.bounds.length < 4) continue
32
+ const [l, t, r, b] = el.bounds
33
+ if (l <= child[0] && t <= child[1] && r >= child[2] && b >= child[3]) {
34
+ const size = (r - l) * (b - t)
35
+ if (size < area) { area = size; best = { el, idx } }
36
+ }
37
+ }
38
+ return best
39
+ }
40
+ export function preferActionable(best: RankedResolutionCandidate, elements: UiElement[], screen: UiResolution | null) {
41
+ const parent = ancestor(elements, best) ?? containing(elements, best)
42
+ if (parent) best = { ...parent, score: Math.min(1, best.score + .02), reason: 'clickable_parent_preferred', interactable: true }
43
+ if (!(best.el.clickable || best.el.focusable || _isSemanticActionable(best.el))) {
44
+ const nearby = nearbyControl(elements, best, screen)
45
+ if (nearby) best = { el: nearby.el, idx: nearby.idx, score: Math.min(1, best.score + .02),
46
+ reason: nearby.sliderLike ? 'slider_track_preferred' : 'nearby_actionable_control', interactable: true }
47
+ }
48
+ return best
49
+ }
@@ -0,0 +1,24 @@
1
+ import type { FindElementResponse, FindElementElement } from '../../types.js'
2
+ import type { RankedResolutionCandidate } from '../model.js'
3
+ import { _summarizeResolutionCandidate } from './element-helpers.js'
4
+
5
+ export function candidateResult(best: RankedResolutionCandidate, candidates: RankedResolutionCandidate[]): FindElementResponse {
6
+ const b = best.el.bounds
7
+ const bounds = Array.isArray(b) ? { left: b[0], top: b[1], right: b[2], bottom: b[3] } : null
8
+ const unique = candidates.filter((candidate, index, array) => index === array.findIndex(other => other.idx === candidate.idx && other.el === candidate.el))
9
+ const alternates = unique.filter(candidate => candidate.idx !== best.idx || candidate.el !== best.el).slice(0, 3).map(_summarizeResolutionCandidate)
10
+ const element: FindElementElement = {
11
+ text: best.el.text ?? null, resourceId: best.el.resourceId ?? null,
12
+ contentDesc: best.el.contentDescription ?? best.el.contentDesc ?? null, class: best.el.type ?? best.el.class ?? null,
13
+ bounds, clickable: !!best.el.clickable, enabled: !!best.el.enabled,
14
+ stable_id: best.el.stable_id ?? null, role: best.el.role ?? null, test_tag: best.el.test_tag ?? null,
15
+ selector: best.el.selector ?? null, semantic: best.el.semantic ?? null,
16
+ tapCoordinates: bounds ? { x: Math.floor((bounds.left + bounds.right) / 2), y: Math.floor((bounds.top + bounds.bottom) / 2) } : null,
17
+ telemetry: { matchedIndex: best.idx ?? null, matchedInteractable: !!best.interactable, sliderLike: best.reason === 'slider_track_preferred' }
18
+ }
19
+ if (best.reason === 'slider_track_preferred') element.interactionHint = { kind: 'slider',
20
+ axis: bounds && (bounds.bottom - bounds.top) > (bounds.right - bounds.left) ? 'vertical' : 'horizontal', trackBounds: bounds }
21
+ const score = Math.min(1, Number(best.score.toFixed(3)))
22
+ return { found: true, element, score, confidence: score, resolution: {
23
+ confidence: score, reason: best.reason, fallback_available: alternates.length > 0, matched_count: unique.length, alternates } }
24
+ }
@@ -0,0 +1,42 @@
1
+ import type { UiElement, RankedResolutionCandidate } from '../model.js'
2
+ import { _normalize, _isSemanticActionable } from './element-helpers.js'
3
+
4
+ type Score = { score: number; reason: string }
5
+ function textScore(el: UiElement, query: string, exact: boolean): Score {
6
+ const text = _normalize(el.text ?? el.label ?? el.value ?? '')
7
+ const content = _normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? '')
8
+ const resource = _normalize(el.resourceId ?? el.resourceID ?? el.id ?? '')
9
+ const type = _normalize(el.type ?? el.class ?? '')
10
+ const choices: Array<[string, boolean, number, string]> = [
11
+ [text, text === query, 1, 'exact_text_match'], [content, content === query, .95, 'exact_content_desc_match'],
12
+ [resource, resource === query, .92, 'exact_resource_id_match']
13
+ ]
14
+ if (!exact) choices.push([text, text.includes(query), .6, 'partial_text_match'], [content, content.includes(query), .55, 'partial_content_desc_match'],
15
+ [resource, resource.includes(query), .7, 'partial_resource_id_match'])
16
+ choices.push([type, exact ? type === query : type.includes(query), .3, exact ? 'exact_class_match' : 'partial_class_match'])
17
+ for (const [value, matched, score, reason] of choices) if (value && matched) return { score, reason }
18
+ return { score: 0, reason: 'best_scoring_candidate' }
19
+ }
20
+ function semanticScore(el: UiElement, query: string, exact: boolean, initial: Score): Score {
21
+ const role = _normalize(el.semantic?.semantic_role ?? '')
22
+ const actions = Array.isArray(el.semantic?.supported_actions) ? el.semantic.supported_actions.map(_normalize).filter(Boolean) : []
23
+ const result = { ...initial }
24
+ if (!result.score && role && (exact ? role === query : role.includes(query))) { result.score = .5; result.reason = 'semantic_role_match' }
25
+ if (actions.some(action => exact ? action === query : action.includes(query))) {
26
+ result.score = Math.max(result.score, result.score > 0 ? .65 : .6); result.reason = 'semantic_action_match'
27
+ }
28
+ if (!exact && result.score === 0 && el.semantic?.adjustable && /slider|stepper|dropdown|segment|control|adjust/.test(query)) {
29
+ result.score = .45; result.reason = 'semantic_control_match'
30
+ }
31
+ return result
32
+ }
33
+ export function scoreCandidate(el: UiElement | null, idx: number, query: string, exact: boolean): RankedResolutionCandidate | null {
34
+ if (!el || !el.visible) return null
35
+ const bounds = el.bounds || [0, 0, 0, 0]
36
+ if (!Array.isArray(bounds) || bounds.length < 4) return null
37
+ if (bounds[2] <= bounds[0] || bounds[3] <= bounds[1]) return null
38
+ const interactable = !!(el.clickable || el.enabled || el.focusable || _isSemanticActionable(el))
39
+ const result = semanticScore(el, query, exact, textScore(el, query, exact))
40
+ if (result.score > 0 && interactable) result.score += .05
41
+ return result.score > 0 ? { el, idx, ...result, interactable } : null
42
+ }
@@ -0,0 +1,243 @@
1
+ import type { FindElementResolutionSummary } from '../model.js'
2
+ import type { ActionFailureCode, ActionTargetResolved } from '../../types.js'
3
+ import type { UiElement, RankedResolutionCandidate } from '../model.js'
4
+ import { createHash } from 'node:crypto'
5
+
6
+ export function _normalize(s: unknown): string {
7
+ if (s === null || s === undefined) return ''
8
+ try { return String(s).toLowerCase().trim() } catch { return '' }
9
+ }
10
+
11
+ export function _normalizeBounds(bounds: unknown): [number, number, number, number] | null {
12
+ if (!Array.isArray(bounds) || bounds.length < 4) return null
13
+ const normalized = bounds.slice(0, 4).map((value: unknown) => Number(value))
14
+ if (normalized.some((value: number) => Number.isNaN(value))) return null
15
+ return normalized as [number, number, number, number]
16
+ }
17
+
18
+ export function _hash(value: unknown): string {
19
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex')
20
+ }
21
+
22
+ function matchesField(value: string, query: string | null | undefined, contains: boolean) {
23
+ if (query === undefined || query === null) return true
24
+ const normalized = _normalize(query)
25
+ return contains ? value.includes(normalized) : value === normalized
26
+ }
27
+ export function _matchesSelector(el: UiElement, selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }): boolean {
28
+ if (!selector) return false
29
+ const contains = !!selector.contains
30
+ return matchesField(_normalize(el.text ?? el.label ?? el.value ?? ''), selector.text, contains) &&
31
+ matchesField(_normalize(el.resourceId ?? el.resourceID ?? el.id ?? ''), selector.resource_id, contains) &&
32
+ matchesField(_normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? el.label ?? ''), selector.accessibility_id, contains)
33
+ }
34
+
35
+ export function _findFirstMatchingElement(
36
+ elements: UiElement[],
37
+ selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }
38
+ ): { el: UiElement, idx: number } | null {
39
+ if (!selector) return null
40
+ for (let i = 0; i < elements.length; i++) {
41
+ const el = elements[i]
42
+ if (!el) continue
43
+ if (_matchesSelector(el, selector)) return { el, idx: i }
44
+ }
45
+ return null
46
+ }
47
+
48
+ export function _resolveParentIndex(elements: UiElement[], parentId: number | string | null | undefined): number | null {
49
+ if (parentId === undefined || parentId === null) return null
50
+
51
+ if (typeof parentId === 'number' && Number.isInteger(parentId) && parentId >= 0 && parentId < elements.length) {
52
+ return parentId
53
+ }
54
+
55
+ if (typeof parentId === 'string') {
56
+ const normalized = _normalize(parentId)
57
+ if (!normalized) return null
58
+
59
+ if (/^\d+$/.test(normalized)) {
60
+ const index = Number(normalized)
61
+ if (index >= 0 && index < elements.length) return index
62
+ }
63
+
64
+ const foundIndex = elements.findIndex((el) => {
65
+ if (!el) return false
66
+ return _normalize(el.resourceId ?? el.resourceID ?? el.id ?? '') === normalized ||
67
+ _normalize(el.stable_id ?? '') === normalized
68
+ })
69
+
70
+ return foundIndex >= 0 ? foundIndex : null
71
+ }
72
+
73
+ return null
74
+ }
75
+
76
+ export function _isVisibleElement(el: UiElement): boolean {
77
+ const bounds = _normalizeBounds(el.bounds)
78
+ return !!el.visible && !!bounds && bounds[2] > bounds[0] && bounds[3] > bounds[1]
79
+ }
80
+
81
+ export function _isTapActionable(
82
+ el: UiElement,
83
+ storedStableId?: string | null,
84
+ platform?: 'android' | 'ios'
85
+ ): { actionable: boolean, failureCode?: ActionFailureCode, reason?: string } {
86
+ if (!_isVisibleElement(el)) {
87
+ return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not visible' }
88
+ }
89
+
90
+ if (el.enabled === false) {
91
+ return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is disabled' }
92
+ }
93
+
94
+ const semanticTapActionable = !!el.semantic && (
95
+ el.semantic.is_clickable ||
96
+ (Array.isArray(el.semantic.supported_actions) && el.semantic.supported_actions.some((action) => _normalize(action) === 'tap'))
97
+ )
98
+
99
+ if (!el.clickable && !(platform === 'ios' && semanticTapActionable)) {
100
+ return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not clickable' }
101
+ }
102
+
103
+ if (storedStableId) {
104
+ if (!el.stable_id || el.stable_id !== storedStableId) {
105
+ return { actionable: false, failureCode: 'STALE_REFERENCE', reason: 'element stable_id changed' }
106
+ }
107
+ }
108
+
109
+ return { actionable: true }
110
+ }
111
+
112
+ export function _isAdjustableActionable(el: UiElement, storedStableId?: string | null): { actionable: boolean, failureCode?: ActionFailureCode, reason?: string } {
113
+ if (!_isVisibleElement(el)) {
114
+ return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is not visible' }
115
+ }
116
+
117
+ if (el.enabled === false) {
118
+ return { actionable: false, failureCode: 'ELEMENT_NOT_INTERACTABLE', reason: 'element is disabled' }
119
+ }
120
+
121
+ if (storedStableId) {
122
+ if (!el.stable_id || el.stable_id !== storedStableId) {
123
+ return { actionable: false, failureCode: 'STALE_REFERENCE', reason: 'element stable_id changed' }
124
+ }
125
+ }
126
+
127
+ return { actionable: true }
128
+ }
129
+
130
+ export function _computeElementId(platform: 'android' | 'ios', deviceId: string | undefined, el: UiElement, index: number): string {
131
+ const identity = {
132
+ platform,
133
+ deviceId: deviceId || '',
134
+ text: _normalize(el.text ?? el.label ?? el.value ?? ''),
135
+ resourceId: _normalize(el.resourceId ?? el.resourceID ?? el.id ?? ''),
136
+ accessibilityId: _normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? el.label ?? ''),
137
+ class: _normalize(el.type ?? el.class ?? ''),
138
+ bounds: _normalizeBounds(el.bounds) ?? [0, 0, 0, 0],
139
+ index
140
+ }
141
+ return `el_${createHash('sha1').update(JSON.stringify(identity)).digest('hex').slice(0, 24)}`
142
+ }
143
+
144
+ export function _resolvedTargetFromElement(
145
+ elementId: string,
146
+ element: UiElement,
147
+ index: number
148
+ ): ActionTargetResolved {
149
+ return {
150
+ elementId,
151
+ text: element.text ?? null,
152
+ resource_id: element.resourceId ?? element.resourceID ?? element.id ?? null,
153
+ accessibility_id: element.contentDescription ?? element.contentDesc ?? element.accessibilityLabel ?? element.label ?? null,
154
+ class: element.type ?? element.class ?? null,
155
+ bounds: _normalizeBounds(element.bounds),
156
+ index,
157
+ state: element.state ?? null,
158
+ stable_id: element.stable_id ?? null,
159
+ role: element.role ?? null,
160
+ test_tag: element.test_tag ?? null,
161
+ selector: element.selector ?? null,
162
+ semantic: element.semantic ?? null
163
+ }
164
+ }
165
+
166
+ export function _summarizeResolutionCandidate(candidate: RankedResolutionCandidate): FindElementResolutionSummary['alternates'][number] {
167
+ const bounds = _normalizeBounds(candidate.el.bounds)
168
+ return {
169
+ text: candidate.el.text ?? null,
170
+ resource_id: candidate.el.resourceId ?? candidate.el.resourceID ?? candidate.el.id ?? null,
171
+ accessibility_id: candidate.el.contentDescription ?? candidate.el.contentDesc ?? candidate.el.accessibilityLabel ?? candidate.el.label ?? null,
172
+ class: candidate.el.type ?? candidate.el.class ?? null,
173
+ bounds: bounds
174
+ ? { left: bounds[0], top: bounds[1], right: bounds[2], bottom: bounds[3] }
175
+ : null,
176
+ clickable: !!candidate.el.clickable,
177
+ enabled: !!candidate.el.enabled,
178
+ score: candidate.score,
179
+ reason: candidate.reason
180
+ }
181
+ }
182
+
183
+ export function _isAdjustableControl(el: UiElement | null): boolean {
184
+ if (!el) return false
185
+ const type = _normalize(el.type ?? el.class ?? '')
186
+ const role = _normalize(el.role ?? '')
187
+ return !!el.state?.value_range || /slider|seekbar|stepper|adjustable|range/.test(type) || /slider|seekbar|stepper|adjustable|range/.test(role)
188
+ }
189
+
190
+ export function _isSemanticActionable(el: UiElement | null): boolean {
191
+ if (!el?.semantic) return false
192
+ if (el.semantic.adjustable) return true
193
+ return Array.isArray(el.semantic.supported_actions) && el.semantic.supported_actions.length > 0
194
+ }
195
+
196
+ export function _resolveActionableAncestor(elements: UiElement[], chosen: { el: UiElement, idx: number } | null): { el: UiElement, idx: number } | null {
197
+ if (!chosen) return null
198
+ if (hasActionableTrait(chosen.el)) return chosen
199
+
200
+ let current = chosen
201
+ let safety = 0
202
+
203
+ while (safety < 20 && !(hasActionableTrait(current.el)) && current.el.parentId !== undefined && current.el.parentId !== null) {
204
+ const parentIndex = _resolveParentIndex(elements, current.el.parentId)
205
+
206
+ if (parentIndex !== null && elements[parentIndex]) {
207
+ current = { el: elements[parentIndex], idx: parentIndex }
208
+ if (hasActionableTrait(current.el)) return current
209
+ } else {
210
+ break
211
+ }
212
+
213
+ safety++
214
+ }
215
+
216
+ const childBounds = _normalizeBounds(chosen.el.bounds)
217
+ if (!childBounds) return null
218
+ const [cl, ct, cr, cb] = childBounds
219
+
220
+ let best: { el: UiElement, idx: number } | null = null
221
+ let bestArea = Infinity
222
+
223
+ for (let i = 0; i < elements.length; i++) {
224
+ const el = elements[i]
225
+ if (!el || !(hasActionableTrait(el))) continue
226
+ const bounds = _normalizeBounds(el.bounds)
227
+ if (!bounds) continue
228
+ const [pl, pt, pr, pb] = bounds
229
+ if (pl <= cl && pt <= ct && pr >= cr && pb >= cb) {
230
+ const area = (pr - pl) * (pb - pt)
231
+ if (area < bestArea) {
232
+ bestArea = area
233
+ best = { el, idx: i }
234
+ }
235
+ }
236
+ }
237
+
238
+ return best
239
+ }
240
+
241
+ function hasActionableTrait(el: UiElement): boolean {
242
+ return !!(el.clickable || el.focusable || _isSemanticActionable(el))
243
+ }
@@ -0,0 +1,47 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { FindElementResponse } from '../../types.js'
3
+ import type { RankedResolutionCandidate } from '../model.js'
4
+ import type { UiTree, ObserveUI, DeviceTarget } from '../observation.js'
5
+ import { _normalize } from './element-helpers.js'
6
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
7
+ import { scoreCandidate } from './candidate-scoring.js'
8
+ import { preferActionable } from './candidate-ancestor.js'
9
+ import { candidateResult } from './candidate-result.js'
10
+
11
+ export interface FindInput extends DeviceTarget { query: string; exact?: boolean; timeoutMs?: number }
12
+ interface Search { best: RankedResolutionCandidate | null; tree: UiTree | null; candidates: RankedResolutionCandidate[]; stop: boolean }
13
+ function scoreTree(search: Search, tree: UiTree, query: string, exact: boolean) {
14
+ if (!tree || !Array.isArray(tree.elements)) return
15
+ const candidates: RankedResolutionCandidate[] = []
16
+ let improved = false
17
+ for (let i = 0; i < tree.elements.length; i++) {
18
+ try {
19
+ const candidate = scoreCandidate(tree.elements[i], i, query, exact)
20
+ if (!candidate) continue
21
+ candidates.push(candidate)
22
+ if (!search.best || candidate.score > search.best.score) {
23
+ search.best = candidate; search.tree = tree; improved = true
24
+ if (candidate.score >= .95) { search.stop = true; break }
25
+ }
26
+ } catch (error) { checkDebuggerGuard(); console.error('Error scoring element:', error) }
27
+ }
28
+ if (improved) search.candidates = candidates.slice()
29
+ }
30
+ export async function findElement({ query, exact = false, timeoutMs = 3000, platform, deviceId }: FindInput, observe: ObserveUI, clock: Clock = systemClock): Promise<FindElementResponse> {
31
+ const start = clock.now(), deadline = start + timeoutMs, normalized = _normalize(query)
32
+ if (!normalized) return { found: false, error: 'Empty query' }
33
+ const search: Search = { best: null, tree: null, candidates: [], stop: false }
34
+ while (clock.now() <= deadline) {
35
+ try { scoreTree(search, await observe({ platform, deviceId }), normalized, exact) }
36
+ catch (error) { checkDebuggerGuard(); console.error('Error fetching UI tree:', error) }
37
+ if (search.stop || clock.now() > deadline) break
38
+ await clock.sleep(100)
39
+ }
40
+ if (!search.best) return { found: false, error: 'Element not found' }
41
+ try {
42
+ const elements = search.tree && Array.isArray(search.tree.elements) ? search.tree.elements : []
43
+ const screen = search.tree?.resolution && typeof search.tree.resolution === 'object' ? search.tree.resolution : null
44
+ search.best = preferActionable(search.best, elements, screen)
45
+ } catch (error) { checkDebuggerGuard(); console.error('Error resolving ancestor:', error) }
46
+ return candidateResult(search.best, search.candidates)
47
+ }
@@ -0,0 +1,46 @@
1
+ import type { UiElement, UiResolution } from '../model.js'
2
+ import { _normalize, _normalizeBounds } from './element-helpers.js'
3
+
4
+ type Match = { el: UiElement; idx: number; sliderLike?: boolean }
5
+ function trackLimits(label: [number, number, number, number], screen?: UiResolution | null) {
6
+ const width = Number(screen?.width) > 0 ? Number(screen?.width) : 0
7
+ const height = Number(screen?.height) > 0 ? Number(screen?.height) : 0
8
+ return { length: Math.max(220, width > 0 ? Math.floor(width * .18) : 0, height > 0 ? Math.floor(height * .18) : 0),
9
+ thickness: Math.max(180, width > 0 ? Math.floor(width * .08) : 0, height > 0 ? Math.floor(height * .08) : 0),
10
+ labelWidth: label[2] - label[0], labelHeight: label[3] - label[1] }
11
+ }
12
+ function nearbyScore(el: UiElement, label: [number, number, number, number], limits: ReturnType<typeof trackLimits>) {
13
+ if (!el || !(el.clickable || el.focusable) || el.visible === false) return null
14
+ const b = _normalizeBounds(el.bounds)
15
+ if (!b) return null
16
+ const width = b[2] - b[0], height = b[3] - b[1], gap = b[1] - label[3]
17
+ if (gap < -32 || gap > 640 || Math.min(label[2], b[2]) - Math.max(label[0], b[0]) < -32) return null
18
+ const horizontal = width >= Math.max(limits.length, Math.floor(limits.labelWidth * 1.5)) && height <= limits.thickness
19
+ const vertical = height >= Math.max(limits.length, Math.floor(limits.labelHeight * 1.5)) && width <= limits.thickness
20
+ const sliderLike = /slider|seek|range/i.test(_normalize(el.type ?? el.class ?? '')) || horizontal || vertical
21
+ return { score: appearanceScore(gap, el, width, limits.labelWidth, sliderLike), sliderLike }
22
+ }
23
+ function appearanceScore(gap: number, el: UiElement, width: number, labelWidth: number, sliderLike: boolean) {
24
+ const text = _normalize(el.text ?? el.label ?? el.value ?? '')
25
+ const content = _normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? '')
26
+ const type = _normalize(el.type ?? el.class ?? '')
27
+ let score = gap
28
+ if (!text && !content) score -= 18
29
+ if (sliderLike) score -= 30
30
+ if (/view|layout|group|frame/i.test(type)) score -= 10
31
+ if (width > labelWidth * 1.5) score -= 8
32
+ if (text || content) score += 20
33
+ return score
34
+ }
35
+ export function nearbyControl(elements: UiElement[], chosen: Match | null, screen?: UiResolution | null): Match | null {
36
+ if (!chosen) return null
37
+ const label = _normalizeBounds(chosen.el.bounds)
38
+ if (!label) return null
39
+ const limits = trackLimits(label, screen)
40
+ let best: Match | null = null, score = Infinity
41
+ for (let i = chosen.idx + 1; i < Math.min(elements.length, chosen.idx + 8); i++) {
42
+ const candidate = nearbyScore(elements[i], label, limits)
43
+ if (candidate && candidate.score < score) { score = candidate.score; best = { el: elements[i], idx: i, sliderLike: candidate.sliderLike } }
44
+ }
45
+ return best
46
+ }
@@ -0,0 +1,20 @@
1
+ import type { ResolvedUiElementContext } from '../model.js'
2
+
3
+ /** Bounded, insertion-ordered handles; repeated observations refresh retention order. */
4
+ export class ResolvedElements {
5
+ private readonly elements = new Map<string, ResolvedUiElementContext>()
6
+ constructor(private readonly maximum = 256) {}
7
+
8
+ get(id: string) { return this.elements.get(id) }
9
+ clear() { this.elements.clear() }
10
+
11
+ remember(id: string, context: ResolvedUiElementContext) {
12
+ if (this.elements.has(id)) this.elements.delete(id)
13
+ this.elements.set(id, context)
14
+ while (this.elements.size > this.maximum) {
15
+ const oldest = this.elements.keys().next().value
16
+ if (!oldest) break
17
+ this.elements.delete(oldest)
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,22 @@
1
+ import type { ActionTargetResolved, ActionFailureCode } from '../../types.js'
2
+ import type { UiElement, ResolvedUiElementContext } from '../model.js'
3
+ import type { UiTree } from '../observation.js'
4
+ import { _isTapActionable, _normalizeBounds } from './element-helpers.js'
5
+ interface TapResolution {
6
+ current(elements: UiElement[], platform: 'android' | 'ios', deviceId: string | undefined, handle: ResolvedUiElementContext): { el: UiElement; index: number } | null
7
+ target(element: UiElement, index: number): ActionTargetResolved
8
+ }
9
+ type TapTarget = { failure: ActionFailureCode; target: ActionTargetResolved | null } | { target: ActionTargetResolved; point: { x: number; y: number } }
10
+ export function resolveTap(tree: UiTree, stored: ResolvedUiElementContext, deps: TapResolution): TapTarget {
11
+ const platform = tree?.device?.platform === 'ios' ? 'ios' : stored.platform
12
+ const deviceId = tree?.device?.id || stored.deviceId
13
+ const elements = Array.isArray(tree?.elements) ? tree.elements : []
14
+ const current = deps.current(elements, platform, deviceId, stored)
15
+ if (!current) return { failure: 'STALE_REFERENCE', target: null }
16
+ const target = deps.target(current.el, current.index)
17
+ const actionable = _isTapActionable(current.el, stored.stable_id, stored.platform)
18
+ if (!actionable.actionable) return { failure: actionable.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', target }
19
+ const bounds = _normalizeBounds(current.el.bounds) ?? stored.bounds
20
+ if (!bounds || bounds[2] <= bounds[0] || bounds[3] <= bounds[1]) return { failure: 'ELEMENT_NOT_INTERACTABLE', target }
21
+ return { target, point: { x: Math.floor((bounds[0] + bounds[2]) / 2), y: Math.floor((bounds[1] + bounds[3]) / 2) } }
22
+ }