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,121 @@
1
+ import type { UiElement, UiChangeScopeResult, ResolvedUiElementContext } from '../model.js'
2
+ import { _normalize, _computeElementId } from './element-helpers.js'
3
+ import { _collectSubtreeIndices } from '../waits/ui-change-helpers.js'
4
+
5
+ export interface ScopeTree { elements?: UiElement[]; device?: { platform?: string; id?: string } }
6
+ type StoreReader = { get(id: string): ResolvedUiElementContext | undefined }
7
+ type ScopedMatch = { index: number; stableId: string | null; reason: string }
8
+
9
+ export function findScopedElement(tree: ScopeTree | null | undefined, targetElementId: string, store: StoreReader): { index: number, stableId: string | null, reason: string } | null {
10
+ const elements = Array.isArray(tree?.elements) ? tree.elements as UiElement[] : []
11
+ const platform = tree?.device?.platform === 'ios' ? 'ios' : 'android'
12
+ const deviceId = tree?.device?.id ?? undefined
13
+ const normalizedTarget = _normalize(targetElementId)
14
+
15
+ for (let i = 0; i < elements.length; i++) {
16
+ const el = elements[i]
17
+ if (!el) continue
18
+
19
+ const computedElementId = _computeElementId(platform, deviceId, el, i)
20
+ if (computedElementId === targetElementId) {
21
+ return {
22
+ index: i,
23
+ stableId: el.stable_id ?? null,
24
+ reason: 'element_id_match'
25
+ }
26
+ }
27
+ }
28
+
29
+ const directStable = findStableElement(elements, normalizedTarget, 'stable_id_match')
30
+ if (directStable) return directStable
31
+ const storedContext = store.get(targetElementId)
32
+ return storedContext?.stable_id
33
+ ? findStableElement(elements, _normalize(storedContext.stable_id), 'stored_stable_id_match')
34
+ : null
35
+ }
36
+
37
+ export function resolveUiChangeScope(
38
+ tree: ScopeTree | null | undefined,
39
+ scope: 'screen' | 'subtree' | undefined,
40
+ target: string | null | undefined,
41
+ find: (tree: ScopeTree | null | undefined, id: string) => ScopedMatch | null
42
+ ): UiChangeScopeResult {
43
+ const elements = Array.isArray(tree?.elements) ? tree.elements as UiElement[] : []
44
+ const normalizedScope = scope === 'subtree' ? 'subtree' : 'screen'
45
+
46
+ if (normalizedScope === 'screen') {
47
+ return {
48
+ elements,
49
+ resolution: {
50
+ scope: 'screen',
51
+ target: null,
52
+ resolved: true,
53
+ resolvedIndex: null,
54
+ resolvedStableId: null,
55
+ reason: 'screen scope'
56
+ }
57
+ }
58
+ }
59
+
60
+ const requestedTarget = typeof target === 'string' && target.trim().length > 0 ? target.trim() : null
61
+ if (!requestedTarget) {
62
+ return {
63
+ elements: [],
64
+ resolution: {
65
+ scope: 'subtree',
66
+ target: null,
67
+ resolved: false,
68
+ resolvedIndex: null,
69
+ resolvedStableId: null,
70
+ reason: 'subtree scope requires a target element id'
71
+ },
72
+ error: {
73
+ code: 'INVALID_SCOPE',
74
+ message: 'scope=subtree requires a target element_id'
75
+ }
76
+ }
77
+ }
78
+
79
+ const resolved = find(tree, requestedTarget)
80
+ if (!resolved) {
81
+ return {
82
+ elements: [],
83
+ resolution: {
84
+ scope: 'subtree',
85
+ target: requestedTarget,
86
+ resolved: false,
87
+ resolvedIndex: null,
88
+ resolvedStableId: null,
89
+ reason: 'target element could not be resolved'
90
+ },
91
+ error: {
92
+ code: 'ELEMENT_NOT_FOUND',
93
+ message: `Target element ${requestedTarget} could not be resolved for subtree scope`
94
+ }
95
+ }
96
+ }
97
+
98
+ const subtreeIndices = _collectSubtreeIndices(elements, resolved.index)
99
+ const scopedElements = subtreeIndices.map((index) => elements[index]).filter((element): element is UiElement => !!element)
100
+
101
+ return {
102
+ elements: scopedElements,
103
+ resolution: {
104
+ scope: 'subtree',
105
+ target: requestedTarget,
106
+ resolved: true,
107
+ resolvedIndex: resolved.index,
108
+ resolvedStableId: resolved.stableId,
109
+ reason: resolved.reason
110
+ }
111
+ }
112
+ }
113
+
114
+ function findStableElement(elements: UiElement[], normalizedTarget: string, reason: string): ScopedMatch | null {
115
+ for (let i = 0; i < elements.length; i++) {
116
+ const el = elements[i]
117
+ if (!el?.stable_id) continue
118
+ if (_normalize(el.stable_id) === normalizedTarget) return { index: i, stableId: el.stable_id, reason }
119
+ }
120
+ return null
121
+ }
@@ -0,0 +1,30 @@
1
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
2
+ import { logMessages, legacySuccess, type LegacyWait, type LegacyWaitDependencies } from './legacy-wait-state.js'
3
+ function streamMessage(entry: unknown) {
4
+ if (!entry) return ''
5
+ if (typeof entry === 'object') { const fields = entry as { message?: unknown; msg?: unknown }; return fields.message || fields.msg || entry }
6
+ return entry
7
+ }
8
+ export async function legacyLog(s: LegacyWait, deps: LegacyWaitDependencies) {
9
+ try {
10
+ const stream = await deps.stream({ platform: s.target.platform, sessionId: 'default', limit: 200 })
11
+ for (const entry of stream && Array.isArray(stream.entries) ? stream.entries : []) {
12
+ const message = streamMessage(entry)
13
+ if (s.query && String(message).includes(s.query)) {
14
+ const result = legacySuccess(s, 'log', 'log-stream', 0, { matchedLog: { message, raw: entry } })
15
+ return { ...result, observed_state: true }
16
+ }
17
+ }
18
+ const logs = await deps.logs({ ...s.target, lines: 200 })
19
+ const messages = logMessages(Array.isArray(logs && logs.logs) ? logs!.logs! : [])
20
+ const index = s.baseline ? messages.lastIndexOf(s.baseline) : -1
21
+ for (let i = index >= 0 ? index + 1 : 0; i < messages.length; i++) {
22
+ const message = messages[i]
23
+ if (s.query && String(message).includes(s.query)) {
24
+ const result = legacySuccess(s, 'log', 'log-snapshot', 0, { matchedLog: { message } })
25
+ return { ...result, observed_state: true }
26
+ }
27
+ }
28
+ } catch (error) { checkDebuggerGuard(); console.error('waitForUI(log) error:', error) }
29
+ return null
30
+ }
@@ -0,0 +1,55 @@
1
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
2
+ import { logMessages, legacySuccess, type LegacyWait, type LegacyWaitDependencies } from './legacy-wait-state.js'
3
+ async function withTimeout<T>(promise: Promise<T>, ms: number, clock: import('../../runtime/clock.js').Clock): Promise<T | null> {
4
+ return Promise.race([promise, clock.sleep(ms).then(() => null)])
5
+ }
6
+ export async function legacyBaseline(s: LegacyWait, deps: LegacyWaitDependencies) {
7
+ try {
8
+ const fingerprint = deps.fingerprint(s.target), logs = deps.logs({ ...s.target, lines: 200 })
9
+ const [fp, result] = await Promise.all([withTimeout(fingerprint, 300, s.clock), withTimeout(logs, 500, s.clock)])
10
+ if (fp && typeof fp === 'object') s.initial = fp.fingerprint ?? null
11
+ if (result) { const messages = logMessages(Array.isArray(result.logs) ? result.logs : []); s.baseline = messages.length ? messages[messages.length - 1] : null }
12
+ } catch (error) {
13
+ checkDebuggerGuard()
14
+ try { console.warn('waitForUI: failed to get baseline data (non-fatal):', error instanceof Error ? error.message : String(error)) } catch { /* Logging is best-effort. */ }
15
+ }
16
+ }
17
+ export async function legacyUI(s: LegacyWait, deps: LegacyWaitDependencies) {
18
+ try {
19
+ try {
20
+ const found = await deps.find({ query: s.query, exact: false, timeoutMs: Math.min(500, s.interval), ...s.target })
21
+ const present = !!(found && found.found), matched = s.match === 'present' ? present : !present
22
+ if (matched) {
23
+ if (s.matched === null) s.matched = s.clock.now()
24
+ s.duration = s.clock.now() - s.matched; s.observed = true
25
+ if (s.duration >= s.stability_ms) return legacySuccess(s, 'ui', 'ui-find', s.duration, { matchedElement: present ? found.element : null }, s.match)
26
+ } else { s.matched = null; s.duration = 0; s.observed = false }
27
+ } catch (error) { checkDebuggerGuard(); console.error('waitForUI(ui) find error:', error) }
28
+ } catch (error) { checkDebuggerGuard(); console.error('waitForUI(ui) outer error:', error) }
29
+ return null
30
+ }
31
+ export async function legacyScreen(s: LegacyWait, deps: LegacyWaitDependencies, now: number) {
32
+ try {
33
+ const fp = (await deps.fingerprint(s.target))?.fingerprint ?? null
34
+ if (fp !== null && fp !== undefined && fp !== s.initial) {
35
+ if (s.matched === null) s.matched = now
36
+ const confirm = (await deps.fingerprint(s.target))?.fingerprint ?? null
37
+ if (confirm === fp) {
38
+ s.duration = s.clock.now() - s.matched; s.observed = true
39
+ if (s.duration >= s.stability_ms) return legacySuccess(s, 'screen', 'screen-fingerprint', s.duration, { newFingerprint: fp })
40
+ } else { s.matched = null; s.duration = 0; s.observed = false }
41
+ }
42
+ } catch (error) { checkDebuggerGuard(); console.error('waitForUI(screen) error:', error) }
43
+ return null
44
+ }
45
+ export async function legacyIdle(s: LegacyWait, deps: LegacyWaitDependencies) {
46
+ try {
47
+ const fp = (await deps.fingerprint(s.target))?.fingerprint ?? null
48
+ if (fp !== s.previous) { s.previous = fp; s.lastChange = s.clock.now(); s.matched = null; s.duration = 0; s.observed = false }
49
+ else {
50
+ const idle = s.clock.now() - s.lastChange; s.observed = true
51
+ if (idle >= s.stability_ms) return legacySuccess(s, 'idle', 'idle-stable', idle)
52
+ }
53
+ } catch (error) { checkDebuggerGuard(); console.error('waitForUI(idle) error:', error) }
54
+ return null
55
+ }
@@ -0,0 +1,48 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { FindElementResponse, FindElementElement } from '../../types.js'
3
+ import type { FindInput } from '../targeting/find-element.js'
4
+ import type { DeviceTarget } from '../observation.js'
5
+ export interface LegacyWaitInput extends DeviceTarget {
6
+ type?: 'ui' | 'log' | 'screen' | 'idle'; query?: string; timeoutMs?: number; pollIntervalMs?: number
7
+ includeSnapshotOnFailure?: boolean; match?: 'present' | 'absent'; stability_ms?: number; observationDelayMs?: number
8
+ }
9
+ export interface LegacyWaitDependencies {
10
+ clock?: Clock
11
+ fingerprint(target: DeviceTarget): Promise<{ fingerprint: string | null } | null>
12
+ logs(target: DeviceTarget & { lines: number }): Promise<{ logs?: unknown[] } | null>
13
+ stream(target: DeviceTarget & { sessionId: string; limit: number }): Promise<{ entries?: unknown[] } | null>
14
+ find(input: FindInput): Promise<FindElementResponse>
15
+ snapshot(input: DeviceTarget & { reason: string; includeLogs: boolean }): Promise<unknown>
16
+ }
17
+ export function createLegacyWait({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300,
18
+ includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }: LegacyWaitInput, clock: Clock = systemClock) {
19
+ const start = clock.now()
20
+ return { clock, type, query: query == null ? '' : String(query), start, deadline: start + (timeoutMs || 0),
21
+ interval: Math.max(250, Math.min(pollIntervalMs || 300, 500)), includeSnapshotOnFailure, match, stability_ms, observationDelayMs,
22
+ target: { platform, deviceId }, initial: null as string | null, baseline: null as unknown,
23
+ lastChange: 0, previous: null as string | null, polls: 0, matched: null as number | null,
24
+ observed: null as boolean | null, duration: 0 }
25
+ }
26
+ export type LegacyWait = ReturnType<typeof createLegacyWait>
27
+ export function logMessages(entries: unknown[]): unknown[] {
28
+ if (!Array.isArray(entries)) return []
29
+ return entries.map(entry => {
30
+ if (typeof entry === 'string') return entry
31
+ if (entry && typeof entry === 'object') {
32
+ const fields = entry as { message?: unknown; msg?: unknown }
33
+ if (fields.message || fields.msg) return fields.message || fields.msg
34
+ }
35
+ try { return JSON.stringify(entry) } catch { return String(entry) }
36
+ })
37
+ }
38
+ interface LegacyExtras {
39
+ matchedElement?: FindElementElement | null
40
+ matchedLog?: { message: unknown; raw?: unknown }
41
+ newFingerprint?: string | null
42
+ }
43
+ export function legacySuccess(s: LegacyWait, type: string, matchSource: string, stable: number, extras: LegacyExtras = {}, condition = 'present') {
44
+ const now = s.clock.now()
45
+ return { success: true, condition, query: s.query, poll_count: s.polls, duration_ms: now - s.start,
46
+ stable_duration_ms: stable, ...extras, matchSource, timestamp: now, type, observed_state: s.observed ?? null }
47
+ }
48
+ export const legacySleep = (ms: number, clock: Clock = systemClock) => clock.sleep(ms)
@@ -0,0 +1,34 @@
1
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
2
+ import { createLegacyWait, legacySleep, type LegacyWaitInput, type LegacyWaitDependencies, type LegacyWait } from './legacy-wait-state.js'
3
+ import { legacyBaseline, legacyUI, legacyScreen, legacyIdle } from './legacy-wait-observations.js'
4
+ import { legacyLog } from './legacy-log-wait.js'
5
+ async function sample(s: LegacyWait, deps: LegacyWaitDependencies) {
6
+ s.polls++
7
+ const now = s.clock.now()
8
+ if (s.type === 'ui') return legacyUI(s, deps)
9
+ if (s.type === 'log') return legacyLog(s, deps)
10
+ if (s.type === 'screen') return legacyScreen(s, deps, now)
11
+ if (s.type === 'idle') return legacyIdle(s, deps)
12
+ return null
13
+ }
14
+ export async function waitForUICore(input: LegacyWaitInput, deps: LegacyWaitDependencies) {
15
+ const state = createLegacyWait(input, deps.clock)
16
+ await legacyBaseline(state, deps)
17
+ state.lastChange = state.clock.now(); state.previous = state.initial
18
+ if (typeof state.observationDelayMs === 'number' && state.observationDelayMs > 0) {
19
+ try { console.log(`waitForUI: delaying observation for ${state.observationDelayMs}ms`) } catch { /* Best effort. */ }
20
+ await legacySleep(state.observationDelayMs, state.clock)
21
+ }
22
+ while (state.clock.now() <= state.deadline) {
23
+ const result = await sample(state, deps)
24
+ if (result) return result
25
+ await legacySleep(state.interval, state.clock)
26
+ }
27
+ let snapshot: unknown = null
28
+ if (state.includeSnapshotOnFailure) {
29
+ try { snapshot = await deps.snapshot({ reason: `wait_for_ui timeout for ${state.type}`, includeLogs: true, ...state.target }) }
30
+ catch (error) { checkDebuggerGuard(); snapshot = { error: error instanceof Error ? error.message : String(error) } }
31
+ }
32
+ return { success: false, condition: state.match, query: state.query, poll_count: state.polls,
33
+ duration_ms: state.clock.now() - state.start, stable_duration_ms: state.duration, error: 'Timeout waiting for condition', snapshot, observed_state: state.observed ?? null }
34
+ }
@@ -0,0 +1,37 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
3
+ import type { ScreenAccess } from '../assertions/screen-assertion.js'
4
+ import type { DeviceTarget } from '../observation.js'
5
+ export interface ScreenWaitInput extends DeviceTarget { previousFingerprint: string; timeoutMs?: number; pollIntervalMs?: number }
6
+ interface ScreenState { clock: Clock; start: number; fingerprint: string | null; activity: string | null }
7
+ async function sample(s: ScreenState, input: ScreenWaitInput, interval: number, deps: Pick<ScreenAccess, 'fingerprint'> & { clock?: Clock }) {
8
+ const response = await deps.fingerprint({ platform: input.platform, deviceId: input.deviceId })
9
+ const fingerprint = response?.fingerprint ?? null
10
+ s.activity = response?.activity ?? s.activity
11
+ if (fingerprint == null) { s.fingerprint = null; await s.clock.sleep(interval); return 'continue' as const }
12
+ s.fingerprint = fingerprint
13
+ if (fingerprint === input.previousFingerprint) return null
14
+ await s.clock.sleep(interval)
15
+ try {
16
+ const confirm = await deps.fingerprint({ platform: input.platform, deviceId: input.deviceId })
17
+ const confirmed = confirm?.fingerprint ?? null
18
+ s.activity = confirm?.activity ?? s.activity
19
+ if (confirmed === fingerprint) return { success: true, previousFingerprint: input.previousFingerprint, newFingerprint: fingerprint,
20
+ elapsedMs: s.clock.now() - s.start, observed_screen: { fingerprint, activity: s.activity } }
21
+ s.fingerprint = confirmed
22
+ } catch (error) { checkDebuggerGuard(); console.error('Error confirming fingerprint:', error) }
23
+ return 'continue' as const
24
+ }
25
+ export async function waitForScreenChange({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }: ScreenWaitInput, deps: Pick<ScreenAccess, 'fingerprint'> & { clock?: Clock }) {
26
+ const state: ScreenState = { clock: deps.clock ?? systemClock, start: (deps.clock ?? systemClock).now(), fingerprint: null, activity: null }
27
+ while (state.clock.now() - state.start < timeoutMs) {
28
+ try {
29
+ const result = await sample(state, { platform, previousFingerprint, deviceId }, pollIntervalMs, deps)
30
+ if (result === 'continue') continue
31
+ if (result) return result
32
+ } catch (error) { checkDebuggerGuard(); console.error('Error getting screen fingerprint:', error) }
33
+ await state.clock.sleep(pollIntervalMs)
34
+ }
35
+ return { success: false, reason: 'timeout', previousFingerprint, lastFingerprint: state.fingerprint,
36
+ elapsedMs: state.clock.now() - state.start, observed_screen: { fingerprint: state.fingerprint, activity: state.activity } }
37
+ }
@@ -0,0 +1,184 @@
1
+ import { computeSnapshotSignature } from '../../observe/snapshot-metadata.js'
2
+ import type { UIElementState } from '../../types.js'
3
+ import type { UiElement, UiChangeSignatureSet } from '../model.js'
4
+ const _uiChangeKinds: Array<'hierarchy_diff' | 'text_change' | 'state_change'> = ['hierarchy_diff', 'text_change', 'state_change']
5
+ import { _normalize, _normalizeBounds, _hash, _resolveParentIndex } from '../targeting/element-helpers.js'
6
+
7
+ export function _collectSubtreeIndices(elements: UiElement[], rootIndex: number): number[] {
8
+ if (!Array.isArray(elements) || rootIndex < 0 || rootIndex >= elements.length) return []
9
+
10
+ const visited = new Set<number>()
11
+ const stack = [rootIndex]
12
+ const result: number[] = []
13
+
14
+ while (stack.length > 0) {
15
+ const index = stack.pop()
16
+ if (index === undefined || visited.has(index) || index < 0 || index >= elements.length) continue
17
+ visited.add(index)
18
+ result.push(index)
19
+
20
+ const element = elements[index]
21
+ if (!element) continue
22
+
23
+ const directChildren = childIndices(elements, element, index)
24
+
25
+ for (const childIndex of directChildren) {
26
+ if (!visited.has(childIndex)) stack.push(childIndex)
27
+ }
28
+ }
29
+
30
+ return result.sort((left, right) => left - right)
31
+ }
32
+
33
+ export function _changeIdentityForElement(el: UiElement, index: number): string {
34
+ const stableId = _normalize(el.stable_id)
35
+ if (stableId) return `stable:${stableId}`
36
+
37
+ return `fallback:${_hash({
38
+ text: _normalize(el.text ?? el.label ?? el.value ?? ''),
39
+ contentDescription: _normalize(el.contentDescription ?? el.contentDesc ?? el.accessibilityLabel ?? ''),
40
+ resourceId: _normalize(el.resourceId ?? el.resourceID ?? el.id ?? ''),
41
+ type: _normalize(el.type ?? el.class ?? ''),
42
+ bounds: _normalizeBounds(el.bounds) ?? [0, 0, 0, 0],
43
+ index
44
+ })}`
45
+ }
46
+
47
+ export function _summarizeUiChangeDelta(initialElements: UiElement[], currentElements: UiElement[]) {
48
+ const buildMap = (elements: UiElement[]) => {
49
+ const map = new Map<string, string>()
50
+ for (let i = 0; i < elements.length; i++) {
51
+ const element = elements[i]
52
+ if (!element) continue
53
+ const key = _changeIdentityForElement(element, i)
54
+ map.set(key, _hash({
55
+ text: _normalize(element.text ?? element.label ?? element.value ?? ''),
56
+ contentDescription: _normalize(element.contentDescription ?? element.contentDesc ?? element.accessibilityLabel ?? ''),
57
+ resourceId: _normalize(element.resourceId ?? element.resourceID ?? element.id ?? ''),
58
+ type: _normalize(element.type ?? element.class ?? ''),
59
+ bounds: _normalizeBounds(element.bounds) ?? [0, 0, 0, 0],
60
+ state: element.state ?? null,
61
+ visible: !!element.visible,
62
+ enabled: !!element.enabled,
63
+ clickable: !!element.clickable
64
+ }))
65
+ }
66
+ return map
67
+ }
68
+
69
+ const initialMap = buildMap(initialElements)
70
+ const currentMap = buildMap(currentElements)
71
+ let added = 0
72
+ let removed = 0
73
+ let mutated = 0
74
+
75
+ for (const [key, value] of currentMap.entries()) {
76
+ if (!initialMap.has(key)) {
77
+ added++
78
+ } else if (initialMap.get(key) !== value) {
79
+ mutated++
80
+ }
81
+ }
82
+
83
+ for (const key of initialMap.keys()) {
84
+ if (!currentMap.has(key)) removed++
85
+ }
86
+
87
+ return {
88
+ total_elements: currentElements.length,
89
+ added_elements: added,
90
+ removed_elements: removed,
91
+ mutated_elements: mutated
92
+ }
93
+ }
94
+
95
+ export function _buildUiChangeSignatures(tree: Parameters<typeof computeSnapshotSignature>[0]): UiChangeSignatureSet {
96
+ const elements = Array.isArray(tree?.elements) ? tree.elements as UiElement[] : []
97
+ const textPayload: Array<{ text: string, contentDescription: string, resourceId: string }> = []
98
+ const statePayload: Array<{
99
+ checked: boolean | null
100
+ selected: boolean | string | { id: string; label?: string } | null
101
+ focused: boolean | null
102
+ expanded: boolean | null
103
+ enabled: boolean | null
104
+ text_value: string | null
105
+ value: number | string | null
106
+ raw_value: number | string | null
107
+ value_range: UIElementState['value_range']
108
+ }> = []
109
+
110
+ for (const el of elements) {
111
+ textPayload.push(textSignature(el))
112
+ statePayload.push(stateSignature(el))
113
+ }
114
+
115
+ return {
116
+ hierarchy: computeSnapshotSignature(tree),
117
+ text: _hash({
118
+ screen: _normalize(tree?.screen),
119
+ elements: textPayload
120
+ }),
121
+ state: _hash({
122
+ screen: _normalize(tree?.screen),
123
+ elements: statePayload
124
+ })
125
+ }
126
+ }
127
+
128
+ export function _matchesUiChange(expected: 'hierarchy_diff' | 'text_change' | 'state_change' | undefined, initial: UiChangeSignatureSet, current: UiChangeSignatureSet): 'hierarchy_diff' | 'text_change' | 'state_change' | null {
129
+ const candidates = expected ? [expected] : _uiChangeKinds
130
+
131
+ for (const changeKind of candidates) {
132
+ if (changeKind === 'hierarchy_diff' && initial.hierarchy !== current.hierarchy) return changeKind
133
+ if (changeKind === 'text_change' && initial.text !== current.text) return changeKind
134
+ if (changeKind === 'state_change' && initial.state !== current.state) return changeKind
135
+ }
136
+
137
+ return null
138
+ }
139
+
140
+ export function _uiChangeSignaturesEqual(left: UiChangeSignatureSet, right: UiChangeSignatureSet): boolean {
141
+ return left.hierarchy === right.hierarchy && left.text === right.text && left.state === right.state
142
+ }
143
+
144
+ function childIndices(elements: UiElement[], element: UiElement, index: number) {
145
+ const directChildren = new Set<number>()
146
+ if (Array.isArray(element.children)) {
147
+ for (const childIndex of element.children) {
148
+ if (typeof childIndex === 'number' && Number.isInteger(childIndex) && childIndex >= 0 && childIndex < elements.length) {
149
+ directChildren.add(childIndex)
150
+ }
151
+ }
152
+ }
153
+
154
+ for (let i = 0; i < elements.length; i++) {
155
+ if (_resolveParentIndex(elements, elements[i]?.parentId) === index) {
156
+ directChildren.add(i)
157
+ }
158
+ }
159
+
160
+
161
+ return directChildren
162
+ }
163
+
164
+ function textSignature(el: UiElement | null) {
165
+ return {
166
+ text: _normalize(el?.text ?? el?.label ?? el?.value ?? ''),
167
+ contentDescription: _normalize(el?.contentDescription ?? el?.contentDesc ?? el?.accessibilityLabel ?? ''),
168
+ resourceId: _normalize(el?.resourceId ?? el?.resourceID ?? el?.id ?? '')
169
+ }
170
+ }
171
+ function stateSignature(el: UiElement | null) {
172
+ const state = el?.state ?? {}
173
+ return {
174
+ checked: state.checked ?? null,
175
+ selected: state.selected ?? null,
176
+ focused: state.focused ?? null,
177
+ expanded: state.expanded ?? null,
178
+ enabled: state.enabled ?? null,
179
+ text_value: state.text_value ?? null,
180
+ value: state.value ?? null,
181
+ raw_value: state.raw_value ?? null,
182
+ value_range: state.value_range ?? null
183
+ }
184
+ }
@@ -0,0 +1,26 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { LoadingState, WaitForUIChangeResponse } from '../../types.js'
3
+ import type { DeviceTarget } from '../observation.js'
4
+ import type { UiChangeSignatureSet, UiChangeScopeResult, UiChangeScopeResolution } from '../model.js'
5
+ import { _summarizeUiChangeDelta } from './ui-change-helpers.js'
6
+ import { currentReadinessWait } from '../../observe/readiness.js'
7
+ export interface ChangeInput extends DeviceTarget {
8
+ timeout_ms?: number; stability_window_ms?: number; expected_change?: 'hierarchy_diff' | 'text_change' | 'state_change'; scope?: 'screen' | 'subtree'; target?: string | null
9
+ }
10
+ export function createChangeWait({ platform, deviceId, timeout_ms = 60000, stability_window_ms = 300, expected_change, scope = 'screen', target = null }: ChangeInput, clock: Clock = systemClock) {
11
+ const tracker = currentReadinessWait(), start = clock.now()
12
+ return { clock, input: { platform, deviceId, timeout_ms, expected_change, scope, target }, tracker, start,
13
+ stability: Math.max(0, typeof stability_window_ms === 'number' ? stability_window_ms : 300), baseline: null as UiChangeSignatureSet | null,
14
+ baselineScope: null as UiChangeScopeResult | null, revision: null as number | null, loading: null as LoadingState | null,
15
+ age: null as number | null, candidate: null as UiChangeSignatureSet | null, change: null as WaitForUIChangeResponse['observed_change'],
16
+ since: null as number | null, summary: null as ReturnType<typeof _summarizeUiChangeDelta> | null,
17
+ scope: { scope: scope === 'subtree' ? 'subtree' : 'screen', target: target && typeof target === 'string' ? target : null,
18
+ resolved: scope !== 'subtree', resolvedIndex: null, resolvedStableId: null, reason: scope === 'subtree' ? 'target not resolved yet' : 'screen scope' } as UiChangeScopeResolution }
19
+ }
20
+ export type ChangeWait = ReturnType<typeof createChangeWait>
21
+ export function changeResult(s: ChangeWait, success: boolean, reason: string): WaitForUIChangeResponse {
22
+ return { success, observed_change: success ? s.change : null, snapshot_revision: s.revision ?? undefined,
23
+ snapshot_freshness_ms: s.age ?? null, timeout: !success, elapsed_ms: s.clock.now() - s.start,
24
+ expected_change: s.input.expected_change, loading_state: s.loading ?? null, scope: s.scope.scope, target: s.scope.target,
25
+ stability_state: success ? 'stable' : 'transient', change_summary: s.summary, reason }
26
+ }
@@ -0,0 +1,59 @@
1
+ import { systemClock } from '../../runtime/clock.js'
2
+ import type { WaitForUIChangeResponse } from '../../types.js'
3
+ import type { ObserveUI, UiTree } from '../observation.js'
4
+ import type { ScopeTree } from '../targeting/target-scope.js'
5
+ import type { UiChangeScopeResult, UiChangeSignatureSet } from '../model.js'
6
+ import { remainingBudget } from '../../utils/operation-budget.js'
7
+ import { checkDebuggerGuard } from '../../debugger/guard.js'
8
+ import { observeReadinessWait, isDeviceUnavailable } from '../../observe/readiness.js'
9
+ import { readinessDelay } from '../../observe/readiness-service.js'
10
+ import { _buildUiChangeSignatures, _summarizeUiChangeDelta, _matchesUiChange, _uiChangeSignaturesEqual } from './ui-change-helpers.js'
11
+ import { createChangeWait, changeResult, type ChangeInput, type ChangeWait } from './ui-change-state.js'
12
+ export interface ChangeDependencies { clock?: import('../../runtime/clock.js').Clock; observe: ObserveUI; scope(tree: ScopeTree, scope: 'screen' | 'subtree', target: string | null): UiChangeScopeResult }
13
+ function scopeFailure(s: ChangeWait, tree: UiTree, scoped: UiChangeScopeResult): WaitForUIChangeResponse {
14
+ s.scope = scoped.resolution
15
+ return { success: false, observed_change: null, snapshot_revision: typeof tree?.snapshot_revision === 'number' ? tree.snapshot_revision : s.revision ?? undefined,
16
+ snapshot_freshness_ms: typeof tree?.captured_at_ms === 'number' ? Math.max(0, s.clock.now() - tree.captured_at_ms) : s.age ?? null,
17
+ timeout: true, elapsed_ms: s.clock.now() - s.start, expected_change: s.input.expected_change, loading_state: tree?.loading_state ?? s.loading ?? null,
18
+ scope: scoped.resolution.scope, target: scoped.resolution.target, stability_state: 'transient', change_summary: s.summary, reason: scoped.error!.message, error: scoped.error }
19
+ }
20
+ function changed(s: ChangeWait, signatures: UiChangeSignatureSet) {
21
+ const change = _matchesUiChange(s.input.expected_change, s.baseline!, signatures)
22
+ if (!change) { s.candidate = null; s.change = null; s.since = null; return false }
23
+ if (!s.candidate || !_uiChangeSignaturesEqual(s.candidate, signatures) || s.change !== change) {
24
+ s.candidate = signatures; s.change = change; s.since = s.clock.now()
25
+ }
26
+ const stable = s.since === null ? 0 : s.clock.now() - s.since
27
+ return (s.stability === 0 || stable >= s.stability) && (!s.tracker || s.tracker.summary.state === 'settled')
28
+ }
29
+ async function sample(s: ChangeWait, deps: ChangeDependencies) {
30
+ if (s.tracker) remainingBudget()
31
+ const started = s.clock.now(), a = s.input
32
+ const tree = await deps.observe({ platform: a.platform, deviceId: a.deviceId })
33
+ observeReadinessWait(tree, started)
34
+ const scoped = deps.scope(tree, a.scope, a.target)
35
+ if (scoped.error) return scopeFailure(s, tree, scoped)
36
+ const signatures = _buildUiChangeSignatures({ ...tree, elements: scoped.elements })
37
+ s.revision = typeof tree?.snapshot_revision === 'number' ? tree.snapshot_revision : s.revision
38
+ s.loading = tree?.loading_state ?? s.loading
39
+ s.age = typeof tree?.captured_at_ms === 'number' ? Math.max(0, s.clock.now() - tree.captured_at_ms) : s.age
40
+ s.summary = s.baseline ? _summarizeUiChangeDelta(s.baselineScope?.elements ?? [], scoped.elements) : s.summary
41
+ s.scope = scoped.resolution; s.baselineScope = s.baselineScope ?? scoped
42
+ if (!s.baseline) { s.baseline = signatures; s.baselineScope = scoped }
43
+ else if (changed(s, signatures)) return changeResult(s, true, 'UI change observed')
44
+ return null
45
+ }
46
+ export async function waitForUIChange(input: ChangeInput, deps: ChangeDependencies): Promise<WaitForUIChangeResponse> {
47
+ const state = createChangeWait(input, deps.clock)
48
+ while (state.clock.now() - state.start < state.input.timeout_ms) {
49
+ try { const result = await sample(state, deps); if (result) return result }
50
+ catch (error) {
51
+ checkDebuggerGuard()
52
+ if (state.tracker && !String(error).includes('READINESS_')) state.tracker.unavailable()
53
+ if (state.tracker && isDeviceUnavailable(error)) throw error
54
+ if (state.tracker && String(error).includes('ACTION_TIMEOUT')) { state.tracker.deadline(); break }
55
+ }
56
+ await (state.tracker && state.clock === systemClock ? readinessDelay(300) : state.clock.sleep(300))
57
+ }
58
+ return changeResult(state, false, 'timeout')
59
+ }
@@ -0,0 +1,52 @@
1
+ import type { UiSelector } from '../../contracts/selector.js'
2
+ import type { UiElement } from '../model.js'
3
+ import { _normalize, _isVisibleElement, _resolveActionableAncestor } from '../targeting/element-helpers.js'
4
+
5
+ export type UICondition = 'exists' | 'not_exists' | 'visible' | 'clickable'
6
+ export type ElementMatch = { el: UiElement; idx: number }
7
+ function matchesField(value: unknown, query: string | null | undefined, contains: boolean) {
8
+ if (query === undefined || query === null) return true
9
+ return contains ? _normalize(value).includes(_normalize(query)) : _normalize(value) === _normalize(query)
10
+ }
11
+ export function matchingElements(elements: UiElement[], selector: UiSelector): ElementMatch[] {
12
+ const matches: ElementMatch[] = []
13
+ const contains = !!selector.contains
14
+ for (let idx = 0; idx < elements.length; idx++) {
15
+ const el = elements[idx]
16
+ if (!matchesField(el.text || el.label || el.value || '', selector.text, contains)) continue
17
+ if (!matchesField(el.resourceId || el.resourceID || el.id || '', selector.resource_id, contains)) continue
18
+ if (!matchesField(el.contentDescription || el.contentDesc || el.accessibilityLabel || el.label || '', selector.accessibility_id, contains)) continue
19
+ matches.push({ el, idx })
20
+ }
21
+ return matches
22
+ }
23
+ function selectedMatch(matches: ElementMatch[], index?: number) {
24
+ if (!matches.length) return null
25
+ return index === undefined ? matches[0] : index >= 0 && index < matches.length ? matches[index] : null
26
+ }
27
+ function clickTarget(elements: UiElement[], chosen: ElementMatch | null, tap: boolean) {
28
+ if (!chosen) return null
29
+ if (tap && (!_isVisibleElement(chosen.el) || chosen.el.enabled === false)) return chosen
30
+ return _resolveActionableAncestor(elements, chosen) || chosen
31
+ }
32
+ export function evaluateCondition(elements: UiElement[], matches: ElementMatch[], condition: UICondition, index?: number, tap = false) {
33
+ const chosen = selectedMatch(matches, index)
34
+ let element = chosen
35
+ let met = false
36
+ if (condition === 'exists') met = index !== undefined ? chosen !== null : matches.length >= 1
37
+ else if (condition === 'not_exists') met = index !== undefined ? chosen === null : matches.length === 0
38
+ else if (condition === 'visible') met = !!chosen && waitVisible(chosen.el)
39
+ else if (condition === 'clickable') {
40
+ element = clickTarget(elements, chosen, tap)
41
+ met = !!element && waitVisible(element.el) && !!element.el.enabled && !!(element.el.clickable || element.el._interactable || element.el.focusable)
42
+ }
43
+ if (tap && met && element && _isVisibleElement(element.el) && element.el.enabled !== false) {
44
+ element = _resolveActionableAncestor(elements, element) ?? element
45
+ }
46
+ return { met, element }
47
+ }
48
+
49
+ function waitVisible(el: UiElement) {
50
+ const b = el.bounds
51
+ return !!el.visible && Array.isArray(b) && b.length >= 4 && b[2] > b[0] && b[3] > b[1]
52
+ }