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,120 @@
1
+ import type { UiElement } from '../model.js'
2
+ import { _normalize } from '../targeting/element-helpers.js'
3
+
4
+ export function _buildControlPoint(bounds: [number, number, number, number], ratio: number, axis: 'horizontal' | 'vertical') {
5
+ const clampedRatio = Math.max(0, Math.min(1, ratio))
6
+ const [left, top, right, bottom] = bounds
7
+ const width = Math.max(1, right - left)
8
+ const height = Math.max(1, bottom - top)
9
+ const insetX = Math.max(8, Math.floor(width * 0.08))
10
+ const insetY = Math.max(8, Math.floor(height * 0.08))
11
+ if (axis === 'vertical') {
12
+ const usableHeight = Math.max(1, height - (insetY * 2))
13
+ return {
14
+ x: Math.floor((left + right) / 2),
15
+ y: Math.floor(bottom - insetY - (usableHeight * clampedRatio))
16
+ }
17
+ }
18
+ const usableWidth = Math.max(1, width - (insetX * 2))
19
+ return {
20
+ x: Math.floor(left + insetX + (usableWidth * clampedRatio)),
21
+ y: Math.floor((top + bottom) / 2)
22
+ }
23
+ }
24
+
25
+ export function _buildConservativeControlPoint(
26
+ bounds: [number, number, number, number],
27
+ targetValue: number,
28
+ currentValue: number | null,
29
+ min: number,
30
+ max: number,
31
+ axis: 'horizontal' | 'vertical'
32
+ ) {
33
+ const range = Math.max(1, max - min)
34
+ const targetRatio = (targetValue - min) / range
35
+ const stepRatio = 1 / range
36
+ const centerBias = stepRatio / 2
37
+ const direction = currentValue === null ? 0 : Math.sign(targetValue - currentValue)
38
+ const controlLengthPx = axis === 'vertical' ? Math.max(1, bounds[3] - bounds[1]) : Math.max(1, bounds[2] - bounds[0])
39
+ const edgeWindow = Math.max(3, Math.floor(range * 0.1))
40
+ const isNearLowEdge = targetValue - min <= edgeWindow
41
+ const isNearHighEdge = max - targetValue <= edgeWindow
42
+ const directionBias = direction > 0
43
+ ? -stepRatio * 0.15
44
+ : direction < 0
45
+ ? stepRatio * 0.65
46
+ : 0
47
+ const pixelBasedMargin = Math.min(0.03, Math.max(0.005, 2 / controlLengthPx))
48
+ const endpointMargin = Math.max(stepRatio * 0.5, pixelBasedMargin)
49
+ const edgeBias = isNearLowEdge
50
+ ? endpointMargin
51
+ : isNearHighEdge
52
+ ? Math.max(stepRatio * 0.4, endpointMargin * 0.75)
53
+ : 0
54
+ const safeRatio = Math.min(
55
+ 1 - (endpointMargin * 0.25),
56
+ Math.max(endpointMargin, targetRatio + centerBias + directionBias + edgeBias)
57
+ )
58
+ return _buildControlPoint(bounds, safeRatio, axis)
59
+ }
60
+
61
+ export function _buildAdjustmentProbePoints(
62
+ bounds: [number, number, number, number],
63
+ targetValue: number,
64
+ currentValue: number | null,
65
+ min: number,
66
+ max: number,
67
+ axis: 'horizontal' | 'vertical'
68
+ ) {
69
+ const targetPoint = _buildConservativeControlPoint(bounds, targetValue, currentValue, min, max, axis)
70
+ const currentPoint = currentValue !== null
71
+ ? _buildControlPoint(bounds, (currentValue - min) / (max - min), axis)
72
+ : _buildControlPoint(bounds, 0.5, axis)
73
+
74
+ const [left, top, right, bottom] = bounds
75
+ const width = Math.max(1, right - left)
76
+ const height = Math.max(1, bottom - top)
77
+ const crossAxisBumps = axis === 'horizontal'
78
+ ? [Math.max(24, Math.floor(height * 0.75)), Math.max(40, Math.floor(height * 1.5))]
79
+ : [Math.max(24, Math.floor(width * 0.75)), Math.max(40, Math.floor(width * 1.5))]
80
+
81
+ const clampPoint = (point: { x: number, y: number }) => ({
82
+ x: axis === 'horizontal'
83
+ ? Math.max(left, Math.min(right, point.x))
84
+ : Math.max(left, Math.min(right + Math.max(width, height), point.x)),
85
+ y: axis === 'vertical'
86
+ ? Math.max(top, Math.min(bottom, point.y))
87
+ : Math.max(top, Math.min(bottom + Math.max(height, width), point.y))
88
+ })
89
+
90
+ const probes = [targetPoint, currentPoint]
91
+ for (const bump of crossAxisBumps) {
92
+ if (axis === 'horizontal') {
93
+ probes.push(
94
+ { x: targetPoint.x, y: bottom + bump },
95
+ { x: currentPoint.x, y: bottom + bump }
96
+ )
97
+ } else {
98
+ probes.push(
99
+ { x: right + bump, y: targetPoint.y },
100
+ { x: right + bump, y: currentPoint.y }
101
+ )
102
+ }
103
+ }
104
+
105
+ return Array.from(
106
+ new Map(
107
+ probes
108
+ .map(clampPoint)
109
+ .map((point) => [`${point.x}:${point.y}`, point] as const)
110
+ ).values()
111
+ )
112
+ }
113
+
114
+ export function _controlAxis(el: UiElement, bounds: [number, number, number, number]): 'horizontal' | 'vertical' {
115
+ const type = _normalize(el.type ?? el.class ?? '')
116
+ const role = _normalize(el.role ?? '')
117
+ if (/vertical/.test(type) || /vertical/.test(role)) return 'vertical'
118
+ if (/horizontal/.test(type) || /horizontal/.test(role)) return 'horizontal'
119
+ return (bounds[3] - bounds[1]) > (bounds[2] - bounds[0]) ? 'vertical' : 'horizontal'
120
+ }
@@ -0,0 +1,141 @@
1
+ import type { UIElementState } from '../../types.js'
2
+ import type { UiElement } from '../model.js'
3
+ import { _normalize } from '../targeting/element-helpers.js'
4
+
5
+ export function _readNumericControlValue(el: UiElement | null, property: string): number | null {
6
+ if (!el?.state) return null
7
+ const stateValue = el.state[property as keyof UIElementState]
8
+ if (typeof stateValue === 'number' && Number.isFinite(stateValue)) return stateValue
9
+ const parsedStateValue = _parseNumericControlValue(stateValue)
10
+ if (parsedStateValue !== null) return parsedStateValue
11
+ if (property === 'value' || property === 'raw_value') {
12
+ const fallback = el.state.raw_value ?? el.state.value
13
+ if (typeof fallback === 'number' && Number.isFinite(fallback)) return fallback
14
+ return _parseNumericControlValue(fallback)
15
+ }
16
+ return null
17
+ }
18
+
19
+ export function _parseNumericControlValue(value: unknown): number | null {
20
+ if (typeof value === 'number' && Number.isFinite(value)) return value
21
+ if (typeof value !== 'string') return null
22
+ const trimmed = value.trim()
23
+ if (!trimmed) return null
24
+
25
+ const percent = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s*%$/)
26
+ if (percent) return Number(percent[1])
27
+
28
+ const fraction = trimmed.match(/^([-+]?\d+(?:\.\d+)?)\s+of\s+[-+]?\d+(?:\.\d+)?$/i)
29
+ if (fraction) return Number(fraction[1])
30
+
31
+ const plain = Number(trimmed)
32
+ if (Number.isFinite(plain)) return plain
33
+
34
+ const trailing = trimmed.match(/,\s*([-+]?\d+(?:\.\d+)?)$/)
35
+ if (trailing) return Number(trailing[1])
36
+
37
+ return null
38
+ }
39
+
40
+ export function _readNumericControlRange(el: UiElement | null): { min: number; max: number } | null {
41
+ const range = el?.state?.value_range
42
+ const min = typeof range?.min === 'number' ? range.min : null
43
+ const max = typeof range?.max === 'number' ? range.max : null
44
+ if (min !== null && max !== null && Number.isFinite(min) && Number.isFinite(max) && max > min) {
45
+ return { min, max }
46
+ }
47
+
48
+ return rangeFromFraction(el?.state?.value ?? el?.state?.raw_value)
49
+ }
50
+
51
+ function rangeFromFraction(value: unknown): { min: number; max: number } | null {
52
+ if (typeof value === 'string') {
53
+ const fraction = value.trim().match(/^[-+]?\d+(?:\.\d+)?\s+of\s+([-+]?\d+(?:\.\d+)?)$/i)
54
+ if (fraction) {
55
+ const parsedMax = Number(fraction[1])
56
+ if (Number.isFinite(parsedMax) && parsedMax > 0) return { min: 0, max: parsedMax }
57
+ }
58
+ }
59
+
60
+ return null
61
+ }
62
+
63
+ export function _readControlStep(el: UiElement | null): number | null {
64
+ const candidates = [
65
+ el?.state?.step,
66
+ el?.state?.value_range?.step
67
+ ]
68
+ for (const candidate of candidates) {
69
+ if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) return candidate
70
+ }
71
+ return null
72
+ }
73
+
74
+ export function _quantizeTargetValue(targetValue: number, min: number, max: number, step: number | null): number {
75
+ if (!step) return targetValue
76
+ const quantized = min + (Math.round((targetValue - min) / step) * step)
77
+ const bounded = Math.max(min, Math.min(max, quantized))
78
+ const precision = _decimalPrecisionForStep(step)
79
+ return Number(bounded.toFixed(precision))
80
+ }
81
+
82
+ export function _decimalPrecisionForStep(step: number): number {
83
+ const normalized = String(step).toLowerCase()
84
+ const [coefficient, exponentPart] = normalized.split('e')
85
+ const coefficientDecimals = coefficient.includes('.')
86
+ ? coefficient.split('.')[1]?.length ?? 0
87
+ : 0
88
+ const exponent = exponentPart !== undefined ? Number(exponentPart) : 0
89
+ const precision = exponent < 0
90
+ ? coefficientDecimals + Math.abs(exponent)
91
+ : Math.max(0, coefficientDecimals - exponent)
92
+ return Math.max(0, Math.min(12, precision))
93
+ }
94
+
95
+ export function _defaultAdjustTolerance(range: { min: number; max: number } | null, step: number | null): number {
96
+ if (step) return 0
97
+ if (!range) return 0
98
+ return Math.max(0, 0.01 * Math.abs(range.max - range.min))
99
+ }
100
+
101
+ export function _runtimeActionSet(el: UiElement | null): Set<string> {
102
+ const values = [
103
+ ...(Array.isArray(el?.actions) ? el!.actions! : []),
104
+ ...(Array.isArray(el?.accessibilityActions) ? el!.accessibilityActions! : []),
105
+ ...(Array.isArray(el?.runtimeActions) ? el!.runtimeActions! : []),
106
+ ...(Array.isArray(el?.platformActions) ? el!.platformActions! : [])
107
+ ]
108
+ return new Set(values.map((value) => _normalize(value)).filter(Boolean))
109
+ }
110
+
111
+ export function _hasDirectAdjustmentAction(el: UiElement | null): boolean {
112
+ const actions = _runtimeActionSet(el)
113
+ return [
114
+ 'action_set_progress',
115
+ 'set_progress',
116
+ 'setprogress',
117
+ 'semanticsactions.setprogress',
118
+ 'set_value',
119
+ 'setvalue',
120
+ 'direct_set_value'
121
+ ].some((action) => actions.has(action))
122
+ }
123
+
124
+ export function _adjustmentDirections(el: UiElement | null): { increment: boolean; decrement: boolean } {
125
+ const actions = _runtimeActionSet(el)
126
+ return {
127
+ increment: ['increment', 'accessibility_increment', 'action_increment'].some((action) => actions.has(action)),
128
+ decrement: ['decrement', 'accessibility_decrement', 'action_decrement'].some((action) => actions.has(action))
129
+ }
130
+ }
131
+
132
+ export function _textFromElement(el: UiElement | null): string {
133
+ return String(el?.text ?? el?.contentDescription ?? el?.contentDesc ?? el?.accessibilityLabel ?? el?.value ?? '').trim()
134
+ }
135
+
136
+ export function _parseLeadingNumber(text: string): number | null {
137
+ const match = text.trim().match(/^([-+]?\d+(?:\.\d+)?)(?:\s*[a-zA-Z%]+)?$/)
138
+ if (!match) return null
139
+ const value = Number(match[1])
140
+ return Number.isFinite(value) ? value : null
141
+ }
@@ -0,0 +1,84 @@
1
+ import type { UiElement, InferredLabeledSliderState } from '../model.js'
2
+ import { _normalize, _normalizeBounds } from '../targeting/element-helpers.js'
3
+ import { _textFromElement, _parseLeadingNumber } from './control-values.js'
4
+
5
+ export function _inferLabeledSliderState(elements: UiElement[], track: UiElement | null): InferredLabeledSliderState | null {
6
+ const trackBounds = _normalizeBounds(track?.bounds)
7
+ if (!track || !trackBounds) return null
8
+ const type = _normalize(track.type ?? track.class ?? '')
9
+ const role = _normalize(track.role ?? '')
10
+ const width = trackBounds[2] - trackBounds[0]
11
+ const height = trackBounds[3] - trackBounds[1]
12
+ const wideTrackLike = width >= 240 && height <= 220
13
+ const knownTrackLike = /slider|seekbar|range/.test(type) || /slider|range/.test(role)
14
+ if (!wideTrackLike && !knownTrackLike) return null
15
+
16
+ const trackTop = trackBounds[1]
17
+ const trackBottom = trackBounds[3]
18
+ const trackCenterX = (trackBounds[0] + trackBounds[2]) / 2
19
+ const labels = elements
20
+ .map((el, index) => {
21
+ const bounds = _normalizeBounds(el.bounds)
22
+ const text = _textFromElement(el)
23
+ const value = _parseLeadingNumber(text)
24
+ return bounds && text && value !== null
25
+ ? {
26
+ index,
27
+ el,
28
+ text,
29
+ value,
30
+ bounds,
31
+ centerX: (bounds[0] + bounds[2]) / 2,
32
+ centerY: (bounds[1] + bounds[3]) / 2,
33
+ hasUnit: /[a-zA-Z%]/.test(text)
34
+ }
35
+ : null
36
+ })
37
+ .filter((label): label is NonNullable<typeof label> => !!label)
38
+
39
+ const rangeLabels = labels
40
+ .filter((label) => {
41
+ const verticallyNearTrack = label.centerY >= trackTop - 520 && label.centerY <= trackBottom + 260
42
+ const horizontallyNearTrack = label.centerX >= trackBounds[0] - 120 && label.centerX <= trackBounds[2] + 120
43
+ return verticallyNearTrack && horizontallyNearTrack && label.hasUnit
44
+ })
45
+ .sort((a, b) => a.centerX - b.centerX)
46
+
47
+ if (rangeLabels.length < 2) return null
48
+ const minLabel = rangeLabels[0]
49
+ const maxLabel = rangeLabels[rangeLabels.length - 1]
50
+ if (!minLabel || !maxLabel || maxLabel.value <= minLabel.value) return null
51
+
52
+ const currentCandidates = labels
53
+ .filter((label) => {
54
+ if (label.index === minLabel.index || label.index === maxLabel.index) return false
55
+ if (label.value < minLabel.value || label.value > maxLabel.value) return false
56
+ if (label.centerY < trackTop - 950 || label.centerY > minLabel.centerY - 20) return false
57
+ return label.centerX >= trackCenterX - 80
58
+ })
59
+ .sort((a, b) => {
60
+ const yDistance = Math.abs((trackTop - 420) - a.centerY) - Math.abs((trackTop - 420) - b.centerY)
61
+ if (yDistance !== 0) return yDistance
62
+ return b.centerX - a.centerX
63
+ })
64
+
65
+ const currentLabel = currentCandidates[0]
66
+ if (!currentLabel) return null
67
+
68
+ const candidateBelowRangeLabels = minLabel.centerY < trackTop && maxLabel.centerY < trackTop
69
+ const labelDerivedBounds: [number, number, number, number] = [
70
+ Math.min(minLabel.bounds[0], trackBounds[0]),
71
+ Math.max(currentLabel.bounds[3] + 12, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 160),
72
+ Math.max(maxLabel.bounds[2], trackBounds[2]),
73
+ Math.max(currentLabel.bounds[3] + 80, Math.min(minLabel.bounds[1], maxLabel.bounds[1]) - 32)
74
+ ]
75
+
76
+ return {
77
+ current: currentLabel.value,
78
+ min: minLabel.value,
79
+ max: maxLabel.value,
80
+ value_range: { min: minLabel.value, max: maxLabel.value },
81
+ touch_bounds: candidateBelowRangeLabels ? labelDerivedBounds : trackBounds,
82
+ source: 'nearby_labels'
83
+ }
84
+ }
@@ -0,0 +1,39 @@
1
+ import type { ExpectScreenResponse } from '../../types.js'
2
+ import type { DeviceTarget } from '../observation.js'
3
+ import { buildObservationTrace } from '../interaction-trace.js'
4
+ export interface ScreenExpectation extends DeviceTarget { fingerprint?: string; screen?: string }
5
+ export interface ScreenAccess {
6
+ fingerprint(target: DeviceTarget): Promise<{ fingerprint: string | null; activity?: string | null } | null>
7
+ current(target: { deviceId?: string }): Promise<{ activity?: string; shortActivity?: string } | null>
8
+ }
9
+ function compareScreen(input: ScreenExpectation, observed: { fingerprint: string | null; screen: string | null }, label: string | null) {
10
+ const { fingerprint, screen } = input
11
+ if (fingerprint) {
12
+ const success = observed.fingerprint === fingerprint
13
+ return { success, basis: 'fingerprint' as const, reason: success ? `observed fingerprint matches expected fingerprint ${fingerprint}` : `expected fingerprint ${fingerprint} but observed ${observed.fingerprint ?? 'null'}` }
14
+ }
15
+ if (screen) {
16
+ const candidates = new Set<string>()
17
+ if (observed.screen) candidates.add(observed.screen)
18
+ if (label) candidates.add(label)
19
+ const success = candidates.has(screen)
20
+ return { success, basis: 'screen' as const, reason: success ? `observed screen matches expected screen ${screen}` : `expected screen ${screen} but observed ${label ?? observed.screen ?? 'null'}` }
21
+ }
22
+ return { success: false, basis: 'none' as const, reason: 'No fingerprint or screen expectation provided' }
23
+ }
24
+ export async function expectScreen(input: ScreenExpectation, deps: ScreenAccess): Promise<ExpectScreenResponse> {
25
+ const observed = await deps.fingerprint({ platform: input.platform, deviceId: input.deviceId })
26
+ const screen = { fingerprint: observed?.fingerprint ?? null, screen: observed?.activity ?? null }
27
+ let label = screen.screen
28
+ if (!input.fingerprint && input.screen && input.platform !== 'ios') {
29
+ try { const current = await deps.current({ deviceId: input.deviceId }); label = current?.shortActivity || current?.activity || label }
30
+ catch { /* Keep the fingerprint-derived activity if current-screen lookup is unavailable. */ }
31
+ }
32
+ const comparison = compareScreen(input, screen, label)
33
+ const expected = { fingerprint: input.fingerprint ?? null, screen: input.screen ?? null }
34
+ const observedScreen = { fingerprint: screen.fingerprint, screen: label }
35
+ return { success: comparison.success, observed_screen: observedScreen, expected_screen: expected,
36
+ confidence: comparison.success ? 1 : 0, comparison: { basis: comparison.basis, matched: comparison.success, reason: comparison.reason },
37
+ trace: buildObservationTrace({ actionType: 'expect_screen', stage: 'verify', success: comparison.success, attempts: 1,
38
+ metadata: { expected_screen: expected, observed_screen: observedScreen, comparison: { basis: comparison.basis, matched: comparison.success } } }) }
39
+ }
@@ -0,0 +1,48 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { ActionTargetResolved, ExpectStateResponse, TraceStep, UIElementState } from '../../types.js'
3
+ import type { DeviceTarget, ObserveUI } from '../observation.js'
4
+ import type { UiSelector } from '../../contracts/selector.js'
5
+ import type { AdjustmentDependencies } from '../adjustment/adjustment-context.js'
6
+ import { createTraceStep, nextActionId } from '../../runtime/action-results.js'
7
+ import type { StateComparison } from './state-comparison.js'
8
+ export interface StateInput extends DeviceTarget {
9
+ selector?: UiSelector; element_id?: string; property: string; expected: ExpectStateResponse['expected_state']['expected']
10
+ stabilization_window_ms?: number; stable_observation_count?: number; snapshot_stale_threshold_ms?: number; poll_interval_ms?: number; exact_budget?: boolean
11
+ }
12
+ export type StateDependencies = Pick<AdjustmentDependencies, 'getHandle' | 'currentHandle' | 'target'> & { observe: ObserveUI; clock?: Clock }
13
+ export function createStateAssertion({ selector, element_id, property, expected, platform, deviceId,
14
+ stabilization_window_ms = 1000, stable_observation_count = 2, snapshot_stale_threshold_ms = 500,
15
+ poll_interval_ms = 150, exact_budget = false }: StateInput, clock: Clock = systemClock) {
16
+ const start = clock.now()
17
+ return { clock, input: { selector, element_id, property, expected, platform, deviceId }, start,
18
+ deadline: start + (exact_budget ? stabilization_window_ms : Math.max(500, stabilization_window_ms)),
19
+ stableTarget: Math.max(1, Math.floor(stable_observation_count || 2)),
20
+ delay: exact_budget ? Math.max(50, Math.min(poll_interval_ms || 150, 1000)) : Math.max(100, Math.min(poll_interval_ms || 150, 200)),
21
+ stale: Math.max(300, Math.min(snapshot_stale_threshold_ms || 500, 800)), attempts: 0, stable: 0, reason: 'element not found',
22
+ failure: 'ELEMENT_NOT_FOUND' as 'ELEMENT_NOT_FOUND' | 'UNKNOWN', element: null as (ActionTargetResolved & { state?: UIElementState | null }) | null,
23
+ value: null as StateComparison['observedValue'], raw: null as StateComparison['rawValue'], id: element_id ?? null,
24
+ trace: [] as TraceStep[], sequence: 0, resolveRecorded: false }
25
+ }
26
+ export type StateAssertion = ReturnType<typeof createStateAssertion>
27
+ export function recordState(s: StateAssertion, stage: TraceStep['stage'], result: TraceStep['result'], metadata: Record<string, unknown>) {
28
+ s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }))
29
+ }
30
+ export function stabilizeState(s: StateAssertion, result: 'success' | 'retry', age: number | null, reason: string) {
31
+ recordState(s, 'stabilize', result, { stabilization_attempts: s.attempts, stable_observation_count: s.stable, snapshot_freshness_ms: age, reason })
32
+ }
33
+ function trace(s: StateAssertion, outcome: 'success' | 'failure') {
34
+ return { action_id: nextActionId('expect_state', s.clock.now()), steps: s.trace, final_outcome: outcome, attempts: s.attempts }
35
+ }
36
+ export function stateSuccess(s: StateAssertion, comparison: StateComparison, age: number | null): ExpectStateResponse {
37
+ return { success: true, selector: s.input.selector, element_id: s.id,
38
+ expected_state: { property: s.input.property, expected: s.input.expected }, element: s.element,
39
+ observed_state: { property: s.input.property, value: comparison.observedValue, ...(comparison.rawValue !== null ? { raw_value: comparison.rawValue } : {}) },
40
+ reason: comparison.reason, stabilization_attempts: s.attempts, stabilization_window_ms: s.clock.now() - s.start,
41
+ stable_observation_count: s.stable, snapshot_freshness_ms: age ?? undefined, trace: trace(s, 'success') }
42
+ }
43
+ export function stateFailure(s: StateAssertion): ExpectStateResponse {
44
+ return { success: false, selector: s.input.selector, element_id: s.id,
45
+ expected_state: { property: s.input.property, expected: s.input.expected }, element: s.element,
46
+ observed_state: { property: s.input.property, value: s.value, ...(s.raw !== null ? { raw_value: s.raw } : {}) },
47
+ reason: s.reason, failure_code: s.failure, retryable: true, trace: trace(s, 'failure') }
48
+ }
@@ -0,0 +1,64 @@
1
+ import type { UiTree } from '../observation.js'
2
+ import type { ExpectStateResponse } from '../../types.js'
3
+ import { _findFirstMatchingElement } from '../targeting/element-helpers.js'
4
+ import { compareReadableState } from './state-comparison.js'
5
+ import { createStateAssertion, recordState, stabilizeState, stateSuccess, stateFailure,
6
+ type StateInput, type StateDependencies, type StateAssertion } from './state-assertion-context.js'
7
+
8
+ function matchState(s: StateAssertion, tree: UiTree, deps: StateDependencies) {
9
+ const elements = Array.isArray(tree?.elements) ? tree.elements : []
10
+ const platform = tree?.device?.platform === 'ios' ? 'ios' : s.input.platform || 'android'
11
+ const deviceId = tree?.device?.id || s.input.deviceId
12
+ const age = typeof tree?.captured_at_ms === 'number' ? s.clock.now() - tree.captured_at_ms : null
13
+ let matched = null
14
+ if (s.input.element_id) {
15
+ const handle = deps.getHandle(s.input.element_id)
16
+ if (handle) {
17
+ const current = deps.currentHandle(elements, platform, deviceId, handle)
18
+ if (current) matched = { el: current.el, idx: current.index }
19
+ }
20
+ }
21
+ if (!matched && s.input.selector) matched = _findFirstMatchingElement(elements, s.input.selector)
22
+ return { matched, platform, deviceId, age }
23
+ }
24
+ function retryResolution(s: StateAssertion, matched: boolean, age: number | null) {
25
+ s.reason = matched ? 'stale snapshot' : 'element not found'
26
+ s.failure = matched ? 'UNKNOWN' : 'ELEMENT_NOT_FOUND'; s.stable = 0
27
+ recordState(s, 'resolve', 'retry', { selector: s.input.selector ?? null, element_id: s.id, matched, reason: s.reason, attempt: s.attempts })
28
+ stabilizeState(s, 'retry', age, s.reason)
29
+ }
30
+ async function sample(s: StateAssertion, deps: StateDependencies): Promise<ExpectStateResponse | null> {
31
+ s.attempts++
32
+ const tree = await deps.observe({ platform: s.input.platform, deviceId: s.input.deviceId })
33
+ const { matched, platform, deviceId, age } = matchState(s, tree, deps)
34
+ if (!matched) { retryResolution(s, false, age); return null }
35
+ const element = deps.target(platform, deviceId, matched)
36
+ s.id = element.elementId; s.element = { ...element, state: matched.el.state ?? null }
37
+ if (age !== null && age > s.stale) { retryResolution(s, true, age); return null }
38
+ if (!s.resolveRecorded) {
39
+ recordState(s, 'resolve', 'success', { selector: s.input.selector ?? null, element_id: s.id, matched: true, reason: 'element resolved' })
40
+ s.resolveRecorded = true
41
+ }
42
+ const comparison = compareReadableState(s.input.property, s.input.expected, matched.el.state ?? null)
43
+ if (comparison.success) {
44
+ s.stable++
45
+ stabilizeState(s, 'success', age, comparison.reason)
46
+ recordState(s, 'verify', 'success', { property: s.input.property, expected: s.input.expected, observed_value: comparison.observedValue, reason: comparison.reason })
47
+ if (s.stable >= s.stableTarget) return stateSuccess(s, comparison, age)
48
+ } else {
49
+ s.stable = 0; s.reason = comparison.reason || s.reason; s.failure = 'UNKNOWN'
50
+ stabilizeState(s, 'retry', age, s.reason)
51
+ recordState(s, 'verify', 'retry', { property: s.input.property, expected: s.input.expected, observed_value: comparison.observedValue, reason: s.reason })
52
+ s.value = comparison.observedValue; s.raw = comparison.rawValue
53
+ }
54
+ return null
55
+ }
56
+ export async function expectState(input: StateInput, deps: StateDependencies): Promise<ExpectStateResponse> {
57
+ const state = createStateAssertion(input, deps.clock)
58
+ while (state.clock.now() <= state.deadline) {
59
+ const result = await sample(state, deps)
60
+ if (result) return result
61
+ await state.clock.sleep(state.delay)
62
+ }
63
+ return stateFailure(state)
64
+ }
@@ -0,0 +1,168 @@
1
+ import type { UIElementState, ExpectStateResponse } from '../../types.js';
2
+ type Expected = ExpectStateResponse['expected_state']['expected'];
3
+ type Observed = NonNullable<ExpectStateResponse['observed_state']>['value'];
4
+ export interface StateComparison {
5
+ success: boolean;
6
+ reason: string;
7
+ rawValue: boolean | number | string | null;
8
+ observedValue: Observed;
9
+ }
10
+ interface ComparisonInput {
11
+ property: string;
12
+ expected: Expected;
13
+ actual: Observed;
14
+ observedState: UIElementState | null;
15
+ }
16
+ const compareBoolean = (value: unknown) => typeof value === 'boolean' ? value : null;
17
+ const compareString = (value: unknown) => typeof value === 'string' ? value : null;
18
+ const compareNumber = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : null;
19
+ export function compareReadableState(property: string, expected: Expected, observedState: UIElementState | null): StateComparison {
20
+ const input = { property, expected, observedState, actual: observedState?.[property as keyof UIElementState] ?? null };
21
+ if (['checked', 'focused', 'expanded', 'enabled'].includes(property))
22
+ return compareBooleanState(input);
23
+ if (['value', 'raw_value'].includes(property))
24
+ return compareValueState(input);
25
+ if (property === 'selected')
26
+ return compareSelectedState(input);
27
+ if (property === 'text_value')
28
+ return compareTextState(input);
29
+ return compareOtherState(input);
30
+ }
31
+ function compareBooleanState({ property, expected, actual }: ComparisonInput): StateComparison {
32
+ let success = false;
33
+ let reason = '';
34
+ let rawValue: StateComparison['rawValue'] = null;
35
+ let observedValue: Observed = actual;
36
+ const expectedBool = compareBoolean(expected);
37
+ const actualBool = compareBoolean(actual);
38
+ if (expectedBool === null) {
39
+ reason = `expected ${property} must be boolean`;
40
+ }
41
+ else if (actualBool === null) {
42
+ reason = `${property} state unavailable`;
43
+ }
44
+ else {
45
+ rawValue = actualBool;
46
+ success = actualBool === expectedBool;
47
+ reason = success ? `${property} matches expected value` : `expected ${property}=${expectedBool} but observed ${actualBool}`;
48
+ }
49
+ observedValue = actualBool;
50
+ return { success, reason, rawValue, observedValue };
51
+ }
52
+ function compareValueState({ expected, actual }: ComparisonInput): StateComparison {
53
+ let success = false;
54
+ let reason = '';
55
+ let rawValue: StateComparison['rawValue'] = null;
56
+ let observedValue: Observed = actual;
57
+ const expectedNumber = compareNumber(expected);
58
+ const actualNumber = compareNumber(actual);
59
+ if (expectedNumber !== null && actualNumber !== null) {
60
+ success = actualNumber === expectedNumber;
61
+ rawValue = actualNumber;
62
+ observedValue = actualNumber;
63
+ reason = success ? 'value matches expected value' : `expected value=${expectedNumber} but observed ${actualNumber}`;
64
+ return { success, reason, rawValue, observedValue };
65
+ }
66
+ const expectedString = typeof expected === 'string' ? expected : null;
67
+ const actualString = compareString(actual);
68
+ if (expectedString !== null && actualString !== null) {
69
+ success = actualString === expectedString;
70
+ rawValue = actualString;
71
+ observedValue = actualString;
72
+ reason = success ? 'value matches expected value' : `expected value=${expectedString} but observed ${actualString}`;
73
+ }
74
+ else {
75
+ reason = 'value state unavailable';
76
+ }
77
+ return { success, reason, rawValue, observedValue };
78
+ }
79
+ function compareSelectedState({ expected, actual, observedState }: ComparisonInput): StateComparison {
80
+ let success = false;
81
+ let reason = '';
82
+ let rawValue: StateComparison['rawValue'] = null;
83
+ let observedValue: Observed = actual;
84
+ const expectedBool = typeof expected === 'boolean' ? expected : null;
85
+ const expectedString = typeof expected === 'string'
86
+ ? expected
87
+ : expected && typeof expected === 'object'
88
+ ? String((expected as {
89
+ id?: unknown;
90
+ label?: unknown;
91
+ }).id ?? (expected as {
92
+ id?: unknown;
93
+ label?: unknown;
94
+ }).label ?? '')
95
+ : null;
96
+ if (!observedState || observedState.selected === undefined || observedState.selected === null) {
97
+ reason = 'selected state unavailable';
98
+ return { success, reason, rawValue, observedValue };
99
+ }
100
+ if (expectedBool !== null) {
101
+ const actualBool = typeof observedState.selected === 'boolean' ? observedState.selected : null;
102
+ if (actualBool === null) {
103
+ reason = 'selected state is not boolean';
104
+ return { success, reason, rawValue, observedValue };
105
+ }
106
+ rawValue = actualBool;
107
+ observedValue = actualBool;
108
+ success = actualBool === expectedBool;
109
+ reason = success ? 'selected matches expected value' : `expected selected=${expectedBool} but observed ${actualBool}`;
110
+ return { success, reason, rawValue, observedValue };
111
+ }
112
+ const actualSelected = typeof observedState.selected === 'object' && observedState.selected !== null
113
+ ? String((observedState.selected as {
114
+ id?: unknown;
115
+ label?: unknown;
116
+ }).id ?? (observedState.selected as {
117
+ id?: unknown;
118
+ label?: unknown;
119
+ }).label ?? '')
120
+ : String(observedState.selected);
121
+ const actualString = actualSelected.trim();
122
+ if (!expectedString) {
123
+ reason = 'expected selected must be boolean, string, or object with id/label';
124
+ return { success, reason, rawValue, observedValue };
125
+ }
126
+ rawValue = actualString;
127
+ observedValue = actualString;
128
+ success = actualString === expectedString;
129
+ reason = success ? 'selected matches expected value' : `expected selected=${expectedString} but observed ${actualString}`;
130
+ return { success, reason, rawValue, observedValue };
131
+ }
132
+ function compareTextState({ expected, actual }: ComparisonInput): StateComparison {
133
+ let success = false;
134
+ let reason = '';
135
+ let rawValue: StateComparison['rawValue'] = null;
136
+ let observedValue: Observed = actual;
137
+ const expectedString = typeof expected === 'string' ? expected : null;
138
+ const actualString = compareString(actual);
139
+ if (!expectedString) {
140
+ reason = 'expected text_value must be string';
141
+ }
142
+ else if (!actualString) {
143
+ reason = 'text_value state unavailable';
144
+ }
145
+ else {
146
+ success = actualString === expectedString;
147
+ rawValue = actualString;
148
+ observedValue = actualString;
149
+ reason = success ? 'text_value matches expected value' : `expected text_value=${expectedString} but observed ${actualString}`;
150
+ }
151
+ return { success, reason, rawValue, observedValue };
152
+ }
153
+ function compareOtherState({ property, expected, actual }: ComparisonInput): StateComparison {
154
+ let success = false;
155
+ let reason = '';
156
+ let rawValue: StateComparison['rawValue'] = null;
157
+ let observedValue: Observed = actual;
158
+ if (actual !== null && actual !== undefined) {
159
+ success = actual === expected;
160
+ observedValue = actual;
161
+ rawValue = typeof actual === 'string' || typeof actual === 'number' || typeof actual === 'boolean' ? actual : null;
162
+ reason = success ? `${property} matches expected value` : `expected ${property} to match but observed ${String(actual)}`;
163
+ }
164
+ else {
165
+ reason = `unsupported or unavailable state property: ${property}`;
166
+ }
167
+ return { success, reason, rawValue, observedValue };
168
+ }