mobile-debug-mcp 0.34.0 → 0.36.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 (500) 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/.cursor-plugin/marketplace.json +1 -1
  11. package/.github/copilot-instructions.md +14 -36
  12. package/.github/workflows/ci.yml +4 -4
  13. package/AGENTS.md +12 -3
  14. package/CONTRIBUTING.md +79 -0
  15. package/README.md +218 -129
  16. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  17. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  18. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  19. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  20. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  21. package/agent-plugin/.mcp.json +1 -1
  22. package/agent-plugin/README.md +50 -11
  23. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  24. package/agent-plugin/cursor/LICENSE +201 -0
  25. package/agent-plugin/cursor/README.md +9 -0
  26. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  27. package/agent-plugin/mcp.json +1 -1
  28. package/agent-plugin/mcp_config.json +13 -0
  29. package/agent-plugin/plugin.json +1 -1
  30. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
  31. package/dist/contracts/execution-controls.js +24 -0
  32. package/dist/contracts/selector.js +8 -0
  33. package/dist/contracts/target.js +15 -0
  34. package/dist/contracts/ui.js +1 -0
  35. package/dist/debugger/android.js +2 -1
  36. package/dist/debugger/diagnostics.js +54 -0
  37. package/dist/debugger/guard.js +4 -4
  38. package/dist/debugger/ios-worker.js +62 -0
  39. package/dist/debugger/ios.js +57 -0
  40. package/dist/debugger/protocol.js +33 -0
  41. package/dist/debugger/sessions.js +133 -74
  42. package/dist/debugger/worker.js +59 -46
  43. package/dist/interact/adjustment/adjustment-context.js +41 -0
  44. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  45. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  46. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  47. package/dist/interact/adjustment/adjustment-result.js +19 -0
  48. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  49. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  50. package/dist/interact/adjustment/adjustment.js +52 -0
  51. package/dist/interact/adjustment/control-geometry.js +87 -0
  52. package/dist/interact/adjustment/control-values.js +136 -0
  53. package/dist/interact/adjustment/slider-labels.js +84 -0
  54. package/dist/interact/assertions/screen-assertion.js +37 -0
  55. package/dist/interact/assertions/state-assertion-context.js +35 -0
  56. package/dist/interact/assertions/state-assertion.js +76 -0
  57. package/dist/interact/assertions/state-comparison.js +141 -0
  58. package/dist/interact/assertions/visibility-assertion.js +34 -0
  59. package/dist/interact/conditional-action.js +12 -7
  60. package/dist/interact/index.js +77 -3092
  61. package/dist/interact/input-service.js +1 -0
  62. package/dist/interact/interaction-trace.js +21 -0
  63. package/dist/interact/model.js +1 -0
  64. package/dist/interact/observation.js +1 -0
  65. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  66. package/dist/interact/targeting/candidate-result.js +23 -0
  67. package/dist/interact/targeting/candidate-scoring.js +50 -0
  68. package/dist/interact/targeting/element-helpers.js +216 -0
  69. package/dist/interact/targeting/find-element.js +65 -0
  70. package/dist/interact/targeting/nearby-control.js +56 -0
  71. package/dist/interact/targeting/resolved-elements.js +21 -0
  72. package/dist/interact/targeting/tap-target.js +17 -0
  73. package/dist/interact/targeting/target-scope.js +104 -0
  74. package/dist/interact/waits/legacy-log-wait.js +38 -0
  75. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  76. package/dist/interact/waits/legacy-wait-state.js +34 -0
  77. package/dist/interact/waits/legacy-wait.js +48 -0
  78. package/dist/interact/waits/screen-wait.js +49 -0
  79. package/dist/interact/waits/ui-change-helpers.js +157 -0
  80. package/dist/interact/waits/ui-change-state.js +18 -0
  81. package/dist/interact/waits/ui-change-wait.js +77 -0
  82. package/dist/interact/waits/ui-wait-condition.js +56 -0
  83. package/dist/interact/waits/ui-wait-state.js +38 -0
  84. package/dist/interact/waits/ui-wait.js +106 -0
  85. package/dist/manage/android.js +55 -51
  86. package/dist/manage/index.js +38 -75
  87. package/dist/manage/ios/build-types.js +1 -0
  88. package/dist/manage/ios/build.js +210 -0
  89. package/dist/manage/ios/project-discovery.js +29 -0
  90. package/dist/manage/ios/scheme.js +18 -0
  91. package/dist/manage/ios.js +44 -256
  92. package/dist/observe/android/context.js +1 -0
  93. package/dist/observe/android/current-screen.js +55 -0
  94. package/dist/observe/android/fingerprint.js +13 -0
  95. package/dist/observe/android/log-stream.js +132 -0
  96. package/dist/observe/android/logs.js +107 -0
  97. package/dist/observe/android/screenshot.js +70 -0
  98. package/dist/observe/android/ui-tree.js +107 -0
  99. package/dist/observe/android.js +16 -520
  100. package/dist/observe/evidence.js +114 -75
  101. package/dist/observe/index.js +16 -263
  102. package/dist/observe/ios/context.js +16 -0
  103. package/dist/observe/ios/fingerprint.js +12 -0
  104. package/dist/observe/ios/log-stream.js +105 -0
  105. package/dist/observe/ios/logs.js +168 -0
  106. package/dist/observe/ios/screenshot.js +22 -0
  107. package/dist/observe/ios/ui-elements.js +241 -0
  108. package/dist/observe/ios/ui-tree.js +137 -0
  109. package/dist/observe/ios-trace/artifact.js +63 -0
  110. package/dist/observe/ios-trace/capabilities.js +86 -0
  111. package/dist/observe/ios-trace/native.js +93 -0
  112. package/dist/observe/ios-trace/notification.js +35 -0
  113. package/dist/observe/ios-trace/process.js +98 -0
  114. package/dist/observe/ios-trace/sessions.js +244 -0
  115. package/dist/observe/ios.js +17 -715
  116. package/dist/observe/logs/model.js +1 -0
  117. package/dist/{network → observe/network}/index.js +2 -2
  118. package/dist/observe/readiness-service.js +5 -3
  119. package/dist/observe/readiness.js +33 -14
  120. package/dist/observe/screenshot-encoding.js +41 -0
  121. package/dist/observe/snapshot-collection.js +79 -0
  122. package/dist/observe/snapshot-logs.js +30 -0
  123. package/dist/observe/snapshot-metadata.js +43 -31
  124. package/dist/observe/snapshot-semantic.js +115 -0
  125. package/dist/observe/system-trace-error.js +7 -0
  126. package/dist/observe/system-trace-router.js +81 -0
  127. package/dist/observe/system-trace.js +26 -21
  128. package/dist/runtime/action-results.js +270 -0
  129. package/dist/runtime/clock.js +4 -0
  130. package/dist/server/common.js +1 -256
  131. package/dist/server/definitions/assert.js +162 -0
  132. package/dist/server/definitions/diagnostic.js +118 -0
  133. package/dist/server/definitions/input.js +274 -0
  134. package/dist/server/definitions/manage.js +194 -0
  135. package/dist/server/definitions/observe.js +149 -0
  136. package/dist/server/definitions/target.js +138 -0
  137. package/dist/server/definitions/wait.js +105 -0
  138. package/dist/server/handlers/action-execution.js +163 -0
  139. package/dist/server/handlers/correlation.js +23 -0
  140. package/dist/server/handlers/debugger-handlers.js +67 -0
  141. package/dist/server/handlers/debugger-response.js +9 -0
  142. package/dist/server/handlers/diagnostic.js +77 -0
  143. package/dist/server/handlers/handler-types.js +1 -0
  144. package/dist/server/handlers/input.js +154 -0
  145. package/dist/server/handlers/interact.js +113 -0
  146. package/dist/server/handlers/journey-assertions.js +35 -0
  147. package/dist/server/handlers/journey.js +147 -0
  148. package/dist/server/handlers/manage.js +119 -0
  149. package/dist/server/handlers/observe.js +95 -0
  150. package/dist/server/handlers/payload.js +4 -0
  151. package/dist/server/handlers/tool-handlers.js +170 -0
  152. package/dist/server/schemas/condition-schema.js +2 -0
  153. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
  154. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  155. package/dist/server/schemas/system-trace-schema.js +22 -0
  156. package/dist/server/tool-definitions.js +58 -1135
  157. package/dist/server-core.js +2 -2
  158. package/dist/server.js +4 -4
  159. package/dist/system/android-status.js +101 -0
  160. package/dist/system/android.js +37 -27
  161. package/dist/system/index.js +13 -439
  162. package/dist/system/ios-status.js +132 -0
  163. package/dist/system/ios.js +11 -8
  164. package/dist/system/model.js +1 -0
  165. package/dist/system/probes.js +105 -0
  166. package/dist/system/status-summary.js +43 -0
  167. package/dist/system/unsupported-status.js +52 -0
  168. package/dist/utils/android/commands.js +89 -0
  169. package/dist/utils/android/gradle.js +139 -0
  170. package/dist/utils/android/logs.js +103 -0
  171. package/dist/utils/android/ui-tree.js +233 -0
  172. package/dist/utils/android/utils.js +6 -564
  173. package/dist/utils/cli/idb/idb-helper.js +4 -6
  174. package/dist/utils/cli/idb/install-idb.js +13 -9
  175. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  176. package/dist/utils/ios/utils.js +48 -46
  177. package/dist/utils/java.js +65 -49
  178. package/dist/utils/resolve-device.js +4 -2
  179. package/dist/utils/ui/index.js +26 -26
  180. package/docs/CHANGELOG.md +67 -1
  181. package/docs/README.md +38 -0
  182. package/docs/agent-plugin-installation-verification.md +39 -0
  183. package/docs/benchmarks/README.md +18 -0
  184. package/docs/benchmarks/interaction-latency/README.md +14 -0
  185. package/docs/benchmarks/interaction-latency/final.md +4 -1
  186. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  187. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  188. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  189. package/docs/code-structure.md +120 -0
  190. package/docs/specs/001-state-verification/spec.md +16 -14
  191. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  192. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  193. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  194. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  195. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  196. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  197. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  198. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  199. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  200. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  201. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  202. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  203. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  204. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  205. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  206. package/docs/specs/016-environment-tools/spec.md +20 -20
  207. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  208. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  209. package/docs/specs/020-conditional-actions/intent.md +5 -3
  210. package/docs/specs/020-conditional-actions/plan.md +8 -9
  211. package/docs/specs/020-conditional-actions/spec.md +4 -2
  212. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  213. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  214. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  215. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  216. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  217. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  218. package/docs/specs/023-android-system-traces/intent.md +3 -2
  219. package/docs/specs/023-android-system-traces/plan.md +5 -7
  220. package/docs/specs/023-android-system-traces/spec.md +3 -1
  221. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  222. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  223. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  224. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  225. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  226. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  227. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  228. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  229. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  230. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  231. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  232. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  233. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  234. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  235. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  236. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  237. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  238. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  239. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  240. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  241. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  242. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  243. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  244. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  245. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  246. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  247. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  248. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  249. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  250. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  251. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  252. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  253. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  254. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  255. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  256. package/docs/tools/TOOLS.md +31 -18
  257. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  258. package/docs/tools/debugger.md +120 -4
  259. package/docs/tools/interact.md +42 -20
  260. package/docs/tools/manage.md +25 -44
  261. package/docs/tools/observe.md +55 -23
  262. package/docs/tools/system-traces.md +89 -14
  263. package/docs/tools/system.md +12 -5
  264. package/package.json +12 -8
  265. package/scripts/agent-plugin.mjs +55 -5
  266. package/scripts/architecture-graph.mjs +69 -0
  267. package/scripts/check-architecture.mjs +47 -0
  268. package/scripts/code-quality-baseline.json +1 -0
  269. package/scripts/code-quality.mjs +161 -0
  270. package/src/contracts/execution-controls.ts +26 -0
  271. package/src/contracts/selector.ts +17 -0
  272. package/src/contracts/target.ts +19 -0
  273. package/src/contracts/ui.ts +94 -0
  274. package/src/debugger/android.ts +3 -2
  275. package/src/debugger/diagnostics.ts +43 -0
  276. package/src/debugger/guard.ts +10 -5
  277. package/src/debugger/ios-worker.ts +49 -0
  278. package/src/debugger/ios.ts +45 -0
  279. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  280. package/src/debugger/lldb/breakpoints.py +51 -0
  281. package/src/debugger/lldb/inspection.py +109 -0
  282. package/src/debugger/lldb/worker.py +203 -0
  283. package/src/debugger/protocol.ts +37 -0
  284. package/src/debugger/sessions.ts +326 -155
  285. package/src/debugger/worker.ts +57 -41
  286. package/src/interact/adjustment/adjustment-context.ts +83 -0
  287. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  288. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  289. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  290. package/src/interact/adjustment/adjustment-result.ts +34 -0
  291. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  292. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  293. package/src/interact/adjustment/adjustment.ts +49 -0
  294. package/src/interact/adjustment/control-geometry.ts +120 -0
  295. package/src/interact/adjustment/control-values.ts +141 -0
  296. package/src/interact/adjustment/slider-labels.ts +84 -0
  297. package/src/interact/assertions/screen-assertion.ts +39 -0
  298. package/src/interact/assertions/state-assertion-context.ts +48 -0
  299. package/src/interact/assertions/state-assertion.ts +64 -0
  300. package/src/interact/assertions/state-comparison.ts +168 -0
  301. package/src/interact/assertions/visibility-assertion.ts +41 -0
  302. package/src/interact/conditional-action.ts +18 -12
  303. package/src/interact/index.ts +89 -3494
  304. package/src/interact/input-service.ts +11 -0
  305. package/src/interact/interaction-trace.ts +36 -0
  306. package/src/interact/model.ts +109 -0
  307. package/src/interact/observation.ts +7 -0
  308. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  309. package/src/interact/targeting/candidate-result.ts +24 -0
  310. package/src/interact/targeting/candidate-scoring.ts +42 -0
  311. package/src/interact/targeting/element-helpers.ts +243 -0
  312. package/src/interact/targeting/find-element.ts +47 -0
  313. package/src/interact/targeting/nearby-control.ts +46 -0
  314. package/src/interact/targeting/resolved-elements.ts +20 -0
  315. package/src/interact/targeting/tap-target.ts +22 -0
  316. package/src/interact/targeting/target-scope.ts +121 -0
  317. package/src/interact/waits/legacy-log-wait.ts +30 -0
  318. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  319. package/src/interact/waits/legacy-wait-state.ts +48 -0
  320. package/src/interact/waits/legacy-wait.ts +34 -0
  321. package/src/interact/waits/screen-wait.ts +37 -0
  322. package/src/interact/waits/ui-change-helpers.ts +184 -0
  323. package/src/interact/waits/ui-change-state.ts +26 -0
  324. package/src/interact/waits/ui-change-wait.ts +59 -0
  325. package/src/interact/waits/ui-wait-condition.ts +52 -0
  326. package/src/interact/waits/ui-wait-state.ts +55 -0
  327. package/src/interact/waits/ui-wait.ts +84 -0
  328. package/src/manage/android.ts +56 -50
  329. package/src/manage/index.ts +63 -93
  330. package/src/manage/ios/build-types.ts +14 -0
  331. package/src/manage/ios/build.ts +230 -0
  332. package/src/manage/ios/project-discovery.ts +26 -0
  333. package/src/manage/ios/scheme.ts +16 -0
  334. package/src/manage/ios.ts +67 -298
  335. package/src/observe/android/context.ts +1 -0
  336. package/src/observe/android/current-screen.ts +63 -0
  337. package/src/observe/android/fingerprint.ts +13 -0
  338. package/src/observe/android/log-stream.ts +127 -0
  339. package/src/observe/android/logs.ts +110 -0
  340. package/src/observe/android/screenshot.ts +79 -0
  341. package/src/observe/android/ui-tree.ts +113 -0
  342. package/src/observe/android.ts +17 -531
  343. package/src/observe/evidence.ts +122 -70
  344. package/src/observe/index.ts +24 -286
  345. package/src/observe/ios/context.ts +22 -0
  346. package/src/observe/ios/fingerprint.ts +12 -0
  347. package/src/observe/ios/log-stream.ts +103 -0
  348. package/src/observe/ios/logs.ts +179 -0
  349. package/src/observe/ios/screenshot.ts +26 -0
  350. package/src/observe/ios/ui-elements.ts +266 -0
  351. package/src/observe/ios/ui-tree.ts +143 -0
  352. package/src/observe/ios-trace/artifact.ts +55 -0
  353. package/src/observe/ios-trace/capabilities.ts +74 -0
  354. package/src/observe/ios-trace/native.ts +72 -0
  355. package/src/observe/ios-trace/notification.ts +30 -0
  356. package/src/observe/ios-trace/process.ts +82 -0
  357. package/src/observe/ios-trace/sessions.ts +204 -0
  358. package/src/observe/ios.ts +18 -749
  359. package/src/observe/logs/model.ts +11 -0
  360. package/src/{network → observe/network}/index.ts +2 -2
  361. package/src/observe/readiness-service.ts +6 -4
  362. package/src/observe/readiness.ts +42 -21
  363. package/src/observe/screenshot-encoding.ts +32 -0
  364. package/src/observe/snapshot-collection.ts +69 -0
  365. package/src/observe/snapshot-logs.ts +26 -0
  366. package/src/observe/snapshot-metadata.ts +51 -31
  367. package/src/observe/snapshot-semantic.ts +153 -0
  368. package/src/observe/system-trace-error.ts +3 -0
  369. package/src/observe/system-trace-router.ts +61 -0
  370. package/src/observe/system-trace.ts +28 -19
  371. package/src/runtime/action-results.ts +376 -0
  372. package/src/runtime/clock.ts +9 -0
  373. package/src/server/common.ts +9 -359
  374. package/src/server/definitions/assert.ts +164 -0
  375. package/src/server/definitions/diagnostic.ts +120 -0
  376. package/src/server/definitions/input.ts +276 -0
  377. package/src/server/definitions/manage.ts +196 -0
  378. package/src/server/definitions/observe.ts +151 -0
  379. package/src/server/definitions/target.ts +140 -0
  380. package/src/server/definitions/wait.ts +107 -0
  381. package/src/server/handlers/action-execution.ts +164 -0
  382. package/src/server/handlers/correlation.ts +20 -0
  383. package/src/server/handlers/debugger-handlers.ts +58 -0
  384. package/src/server/handlers/debugger-response.ts +7 -0
  385. package/src/server/handlers/diagnostic.ts +86 -0
  386. package/src/server/handlers/handler-types.ts +7 -0
  387. package/src/server/handlers/input.ts +173 -0
  388. package/src/server/handlers/interact.ts +135 -0
  389. package/src/server/handlers/journey-assertions.ts +39 -0
  390. package/src/server/handlers/journey.ts +132 -0
  391. package/src/server/handlers/manage.ts +141 -0
  392. package/src/server/handlers/observe.ts +116 -0
  393. package/src/server/handlers/payload.ts +36 -0
  394. package/src/server/handlers/tool-handlers.ts +170 -0
  395. package/src/server/schemas/condition-schema.ts +2 -0
  396. package/src/server/schemas/debugger-schema.ts +46 -0
  397. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  398. package/src/server/schemas/system-trace-schema.ts +23 -0
  399. package/src/server/tool-definitions.ts +67 -1143
  400. package/src/server-core.ts +2 -2
  401. package/src/server.ts +4 -4
  402. package/src/system/android-status.ts +124 -0
  403. package/src/system/android.ts +33 -21
  404. package/src/system/index.ts +21 -525
  405. package/src/system/ios-status.ts +147 -0
  406. package/src/system/ios.ts +14 -10
  407. package/src/system/model.ts +41 -0
  408. package/src/system/probes.ts +114 -0
  409. package/src/system/status-summary.ts +47 -0
  410. package/src/system/unsupported-status.ts +55 -0
  411. package/src/types.ts +16 -106
  412. package/src/utils/android/commands.ts +81 -0
  413. package/src/utils/android/gradle.ts +132 -0
  414. package/src/utils/android/logs.ts +103 -0
  415. package/src/utils/android/ui-tree.ts +239 -0
  416. package/src/utils/android/utils.ts +16 -556
  417. package/src/utils/cli/idb/check-idb.ts +4 -4
  418. package/src/utils/cli/idb/idb-helper.ts +6 -8
  419. package/src/utils/cli/idb/install-idb.ts +15 -9
  420. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  421. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  422. package/src/utils/ios/utils.ts +60 -61
  423. package/src/utils/java.ts +62 -43
  424. package/src/utils/resolve-device.ts +9 -6
  425. package/src/utils/ui/index.ts +28 -28
  426. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  427. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  428. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  429. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  430. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  431. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  432. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  433. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  434. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  435. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  436. package/test/fixtures/contracts/tool-definitions.json +3696 -0
  437. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  438. package/test/fixtures/coroutine-app/README.md +48 -0
  439. package/test/fixtures/coroutine-app/build.ts +35 -0
  440. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  441. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  442. package/test/fixtures/debugger-app/README.md +52 -0
  443. package/test/fixtures/debugger-app/build.ts +25 -15
  444. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  445. package/test/fixtures/ios-debugger-app/README.md +27 -0
  446. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  447. package/test/fixtures/latency-journey-app/README.md +2 -0
  448. package/test/fixtures/readiness-app/README.md +2 -0
  449. package/test/helpers/ios-trace-shutdown.ts +29 -0
  450. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  451. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  452. package/test/unit/debugger/diagnostics.test.ts +80 -0
  453. package/test/unit/debugger/guards.test.ts +4 -0
  454. package/test/unit/debugger/handle-guards.test.ts +88 -0
  455. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  456. package/test/unit/debugger/ios.test.ts +104 -0
  457. package/test/unit/debugger/lldb-worker.py +166 -0
  458. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  459. package/test/unit/debugger/preparation.test.ts +32 -0
  460. package/test/unit/debugger/protocol.test.ts +15 -0
  461. package/test/unit/debugger/sessions.test.ts +1 -1
  462. package/test/unit/interact/adjustment_service.test.ts +42 -0
  463. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  464. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  465. package/test/unit/interact/element_helpers.test.ts +19 -0
  466. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  467. package/test/unit/interact/state_comparison.test.ts +20 -0
  468. package/test/unit/interact/target_scope.test.ts +35 -0
  469. package/test/unit/network/get_network_activity.test.ts +1 -1
  470. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  471. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  472. package/test/unit/observe/readiness.test.ts +1 -1
  473. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  474. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  475. package/test/unit/observe/system-trace-router.test.ts +49 -0
  476. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  477. package/test/unit/quality/architecture.test.ts +60 -0
  478. package/test/unit/quality/code-quality.test.ts +58 -0
  479. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  480. package/test/unit/server/conditional_actions.test.ts +1 -1
  481. package/test/unit/server/conditional_schema.test.ts +2 -2
  482. package/test/unit/server/debugger-response.test.ts +63 -0
  483. package/test/unit/server/system-trace.test.ts +12 -3
  484. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  485. package/dist/server/debugger-handlers.js +0 -38
  486. package/dist/server/system-trace-schema.js +0 -17
  487. package/dist/server/tool-handlers.js +0 -1000
  488. package/docs/ROADMAP.md +0 -646
  489. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  490. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  491. package/docs/specs/baseline-spec-v0.md +0 -312
  492. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  493. package/src/server/debugger-handlers.ts +0 -29
  494. package/src/server/debugger-schema.ts +0 -37
  495. package/src/server/system-trace-schema.ts +0 -17
  496. package/src/server/tool-handlers.ts +0 -1008
  497. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  498. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  499. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  500. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -1,3 +1,7 @@
1
+ import { DebuggerError, classify, diagnostic, atStage } from './diagnostics.js'
2
+ export { DebuggerError } from './diagnostics.js'
3
+ import { parseWorkerMessage, type WorkerEvent, type WorkerReply } from './protocol.js'
4
+ export type { WorkerEvent } from './protocol.js'
1
5
  import { AsyncLocalStorage } from 'node:async_hooks'
2
6
  import { execFile, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
3
7
  import { promisify } from 'node:util'
@@ -21,93 +25,102 @@ export function debuggerRemaining(fallback: number) {
21
25
  return Math.min(fallback, remaining)
22
26
  }
23
27
  export function debuggerSignal() { return operations.getStore()?.signal }
24
- export function nativeError(error: any, fallback: string): DebuggerError {
28
+ export function nativeError(error: unknown, fallback: string): DebuggerError {
25
29
  if (error instanceof DebuggerError) return error
26
30
  if (debuggerSignal()?.aborted) return new DebuggerError('DEBUGGER_CANCELLED')
27
- if (error?.killed) return new DebuggerError('DEBUGGER_TIMEOUT')
31
+ if (typeof error === 'object' && error !== null && 'killed' in error && error.killed) return new DebuggerError('DEBUGGER_TIMEOUT')
28
32
  return new DebuggerError(fallback)
29
33
  }
30
- export class DebuggerError extends Error { constructor(public code: string) { super(code) } }
31
- export const workerSource = fileURLToPath(new URL('../../worker/DebuggerWorker.java', import.meta.url))
34
+ export const workerSource = fileURLToPath(new URL('../../src/debugger/java/DebuggerWorker.java', import.meta.url))
32
35
  let compiled: Promise<{ java: string; directory: string }> | undefined
33
36
  export async function prepareWorker() {
37
+ let stage: 'prerequisite' | 'compilation' = 'prerequisite'
34
38
  compiled ??= (async () => {
35
- const home = await detectJavaHome({ quiet: true })
39
+ const home = await detectJavaHome({ quiet: true }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', 'unavailable') })
36
40
  const java = home ? path.join(home, 'bin', 'java') : 'java'
37
41
  const javac = home ? path.join(home, 'bin', 'javac') : 'javac'
38
- const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() })
42
+ const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', debuggerSignal()?.aborted ? 'cancelled' : typeof e === 'object' && e !== null && 'killed' in e && e.killed ? 'timeout' : 'unavailable') })
39
43
  const major = Number(/version "(\d+)/.exec(version.stderr)?.[1])
40
- if (!(major >= 17)) throw new DebuggerError('DEBUGGER_UNSUPPORTED')
41
- const source = await readFile(workerSource)
44
+ if (!(major >= 17)) throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('prerequisite', 'unsupported'))
45
+ stage = 'compilation'
46
+ const source = await readFile(workerSource).catch(e => { throw classify(e, 'compilation', 'DEBUGGER_ATTACH_FAILED', 'unavailable') })
42
47
  const key = createHash('sha256').update(source).update(version.stderr).digest('hex').slice(0, 16)
43
- const directory = await mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`))
44
- await chmod(directory, 0o700)
45
- try { await exec(javac, ['--release', '17', '--add-modules', 'jdk.jdi', '-d', directory, workerSource], { timeout: debuggerRemaining(15000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 }) }
46
- catch (e) { await rm(directory, { recursive: true, force: true }); throw nativeError(e, 'DEBUGGER_UNSUPPORTED') }
48
+ const directory = await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`)))
49
+ try { await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => chmod(directory, 0o700)); await exec(javac, ['--release', '17', '--add-modules', 'jdk.jdi', '-d', directory, workerSource], { timeout: debuggerRemaining(15000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 }) }
50
+ catch (e) { await rm(directory, { recursive: true, force: true }); throw classify(nativeError(e, 'DEBUGGER_UNSUPPORTED'), 'compilation', 'DEBUGGER_UNSUPPORTED', 'operation_failed') }
47
51
  return { java, directory }
48
- })().catch(error => { compiled = undefined; throw error })
52
+ })().catch(error => { compiled = undefined; throw classify(error, stage, 'DEBUGGER_ATTACH_FAILED') })
49
53
  return compiled
50
54
  }
51
- export type WorkerEvent = Record<string, any>
55
+ function commandStage(command: string) { return command === 'attach' ? 'attach' as const : command === 'detach' ? 'cleanup' as const : 'command' as const }
52
56
  export interface DebugWorker {
53
- call(command: string, args?: Record<string, unknown>, timeout?: number): Promise<any>
57
+ call(command: string, args?: Record<string, unknown>, timeout?: number): Promise<WorkerReply>
54
58
  close(): Promise<void>
55
59
  }
56
- export class JdiWorker implements DebugWorker {
60
+ type WorkerFailureReason = 'spawn_failed' | 'process_exited' | 'write_failed' | 'invalid_message' | 'message_limit'
61
+ export class DebuggerProcess implements DebugWorker {
57
62
  private child: ChildProcessWithoutNullStreams
58
- private pending = new Map<number, { resolve(value: any): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout> }>()
63
+ private pending = new Map<number, { resolve(value: WorkerReply): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout>; command: string }>()
59
64
  private sequence = 0
60
65
  private dead = false
66
+ private failure?: DebuggerError
67
+ private replied = false
61
68
  private exited: Promise<void>
62
- constructor(java: string, directory: string, onEvent: (event: WorkerEvent) => void) {
63
- this.child = spawn(java, ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'] })
69
+ constructor(java: string, directory: string, private onEvent: (event: WorkerEvent) => void, launch?: { args: string[]; env: NodeJS.ProcessEnv }) {
70
+ this.child = spawn(java, launch?.args ?? ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'], ...(launch ? { env: launch.env } : {}) })
64
71
  let buffer = ''
65
- const fail = () => {
66
- if (this.dead) return
67
- this.dead = true
68
- for (const request of this.pending.values()) { clearTimeout(request.timer); request.reject(new DebuggerError('DEBUGGER_WORKER_FAILED')) }
69
- this.pending.clear()
70
- onEvent({ event: 'worker_failed' })
71
- }
72
- this.exited = new Promise(resolve => { this.child.once('exit', () => { fail(); resolve() }); this.child.once('error', () => { fail(); resolve() }) })
73
- this.child.stdin.on('error', fail)
72
+ this.exited = new Promise(resolve => { this.child.once('exit', () => { this.fail('process_exited'); resolve() }); this.child.once('error', () => { this.fail('spawn_failed'); resolve() }) })
73
+ this.child.stdin.on('error', () => this.fail('write_failed'))
74
74
  this.child.stderr.resume() // Never copy worker data or VM diagnostics into MCP stdout/logs.
75
75
  this.child.stdout.setEncoding('utf8')
76
76
  this.child.stdout.on('data', (chunk: string) => {
77
77
  buffer += chunk
78
- if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) { fail(); this.child.kill('SIGKILL'); return }
78
+ if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) { this.fail('message_limit'); this.child.kill('SIGKILL'); return }
79
79
  let end: number
80
80
  while ((end = buffer.indexOf('\n')) >= 0) {
81
81
  const line = buffer.slice(0, end); buffer = buffer.slice(end + 1)
82
82
  try {
83
83
  if (Buffer.byteLength(line) > 1024 * 1024) throw new Error('IPC_LIMIT')
84
- const msg = JSON.parse(line)
85
- if (msg.event) onEvent(msg)
84
+ const msg = parseWorkerMessage(JSON.parse(line))
85
+ if (msg.kind === 'event') onEvent(msg.value)
86
86
  else {
87
+ this.replied = true
87
88
  const request = this.pending.get(msg.id)
88
89
  if (!request) continue
89
90
  clearTimeout(request.timer); this.pending.delete(msg.id)
90
- if (msg.error) request.reject(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED'))
91
+ if (msg.error) request.reject(classify(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED', msg.error.diagnostic), commandStage(request.command), 'DEBUGGER_WORKER_FAILED'))
91
92
  else request.resolve(msg.result)
92
93
  }
93
- } catch { fail(); this.child.kill('SIGKILL'); return }
94
+ } catch { this.fail(Buffer.byteLength(line) > 1024 * 1024 ? 'message_limit' : 'invalid_message'); this.child.kill('SIGKILL'); return }
94
95
  }
95
96
  })
96
97
  }
97
- call(command: string, args: Record<string, unknown> = {}, timeout = 10000): Promise<any> {
98
- if (this.dead) return Promise.reject(new DebuggerError('DEBUGGER_WORKER_FAILED'))
99
- try { timeout = Math.min(timeout, debuggerRemaining(timeout)) } catch (e) { return Promise.reject(e) }
98
+ private fail(reason: WorkerFailureReason) {
99
+ if (this.dead) return
100
+ this.dead = true
101
+ const stage = reason === 'spawn_failed' || (reason === 'process_exited' && !this.replied) ? 'worker_start' : 'ipc'
102
+ this.failure = new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic(stage, reason))
103
+ for (const request of this.pending.values()) {
104
+ clearTimeout(request.timer)
105
+ request.reject(this.failure)
106
+ }
107
+ this.pending.clear()
108
+ this.onEvent({ event: 'worker_failed', diagnostic: this.failure.diagnostic })
109
+ }
110
+ call(command: string, args: Record<string, unknown> = {}, timeout = 10000): Promise<WorkerReply> {
111
+ if (this.dead) return Promise.reject(this.failure ?? new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'unknown')))
112
+ try { timeout = Math.min(timeout, debuggerRemaining(timeout)) } catch (e) { return Promise.reject(classify(e, commandStage(command), 'DEBUGGER_WORKER_FAILED')) }
100
113
  const id = ++this.sequence
101
114
  const message = JSON.stringify({ ...args, command, id }) + '\n'
102
115
  if (Buffer.byteLength(message) > 1024 * 1024) return Promise.reject(new DebuggerError('INVALID_ARGUMENT'))
103
116
  return new Promise((resolve, reject) => {
104
117
  const signal = debuggerSignal()
105
- const abort = () => { this.pending.delete(id); clearTimeout(timer); reject(new DebuggerError('DEBUGGER_CANCELLED')) }
118
+ const abort = () => { this.pending.delete(id); clearTimeout(timer); done(); reject(new DebuggerError('DEBUGGER_CANCELLED', diagnostic(commandStage(command), 'cancelled'))) }
106
119
  const done = () => signal?.removeEventListener('abort', abort)
107
- const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT')) }, timeout)
108
- this.pending.set(id, { resolve: value => { done(); resolve(value) }, reject: error => { done(); reject(error) }, timer })
120
+ const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT', diagnostic(commandStage(command), 'timeout'))) }, timeout)
121
+ this.pending.set(id, { command, resolve: value => { done(); resolve(value) }, reject: error => { done(); reject(error) }, timer })
109
122
  signal?.addEventListener('abort', abort, { once: true })
110
- this.child.stdin.write(message)
123
+ try { this.child.stdin.write(message) } catch { this.fail('write_failed') }
111
124
  })
112
125
  }
113
126
  async close() {
@@ -118,5 +131,8 @@ export class JdiWorker implements DebugWorker {
118
131
  }
119
132
  export async function createWorker(onEvent: (event: WorkerEvent) => void) {
120
133
  const { java, directory } = await prepareWorker()
121
- return new JdiWorker(java, directory, onEvent)
134
+ return new DebuggerProcess(java, directory, onEvent)
122
135
  }
136
+
137
+ // Compatibility export for existing JDI transport callers/tests.
138
+ export { DebuggerProcess as JdiWorker }
@@ -0,0 +1,83 @@
1
+ import { systemClock, type Clock } from '../../runtime/clock.js'
2
+ import type { ActionFailureCode, ActionTargetResolved, AdjustControlResponse, DeviceInfo, FindElementResponse, TraceStep } from '../../types.js'
3
+ import type { UiSelector } from '../../contracts/selector.js'
4
+ import type { UiElement, ResolvedUiElementContext } from '../model.js'
5
+ import type { InputService } from '../input-service.js'
6
+ import type { AdjustmentVerificationDependencies } from './adjustment-verification.js'
7
+ import { createTraceStep, buildActionExecutionResult } from '../../runtime/action-results.js'
8
+ import { completeAdjustment } from './adjustment-result.js'
9
+ import { planAdjustment } from './adjustment-plan.js'
10
+
11
+ export interface AdjustmentInput { selector?: UiSelector; element_id?: string; property?: string; targetValue: number; tolerance?: number; maxAttempts?: number; platform?: 'android' | 'ios'; deviceId?: string }
12
+ export interface ControlTree { elements?: UiElement[]; device?: DeviceInfo }
13
+ export type Match = { el: UiElement; idx: number }
14
+ export interface ControlMatch { tree: ControlTree | null; device?: DeviceInfo; match: Match | null; resolvedTarget: ActionTargetResolved | null }
15
+ export interface ControlWait { status?: string; element?: { elementId?: string | null } | null; device?: DeviceInfo; error?: { code?: string; message?: string } }
16
+ export interface AdjustmentDependencies {
17
+ clock?: Clock
18
+ observe(platform: 'android' | 'ios', deviceId?: string): Promise<ControlTree>
19
+ fingerprint(platform: 'android' | 'ios', deviceId?: string): Promise<string | null>
20
+ getHandle(id: string): ResolvedUiElementContext | undefined
21
+ currentHandle(elements: UiElement[], platform: 'android' | 'ios', deviceId: string | undefined, handle: ResolvedUiElementContext): { el: UiElement; index: number } | null
22
+ target(platform: 'android' | 'ios', deviceId: string | undefined, match: Match): ActionTargetResolved
23
+ wait(input: { selector: UiSelector; condition: 'clickable'; timeout_ms: number; poll_interval_ms: number; platform: 'android' | 'ios'; deviceId?: string }): Promise<ControlWait | undefined>
24
+ find(input: { query: string; exact: boolean; timeoutMs: number; platform: 'android' | 'ios'; deviceId?: string }): Promise<FindElementResponse>
25
+ expectState: AdjustmentVerificationDependencies['expectState']
26
+ service(platform: 'android' | 'ios', deviceId?: string): Promise<{ interact: Pick<InputService, 'setAdjustableValue' | 'adjustAccessibleValue'>; resolved: { id: string } }>
27
+ tap(input: { platform: 'android' | 'ios'; x: number; y: number; deviceId?: string }): Promise<{ success: boolean; device?: DeviceInfo }>
28
+ swipe(input: { platform: 'android' | 'ios'; x1: number; y1: number; x2: number; y2: number; duration: number; deviceId?: string }): Promise<{ success: boolean; device?: DeviceInfo; error?: string }>
29
+ }
30
+ export type ActualState = AdjustControlResponse['actual_state']
31
+ export type Mode = AdjustControlResponse['adjustment_mode']
32
+ export type Verification = { verification: { reason?: string } | null; observedState: ActualState; withinTolerance: boolean }
33
+ export interface AdjustmentState {
34
+ clock: Clock
35
+ input: AdjustmentInput & { property: string }
36
+ platform: 'android' | 'ios'; deviceId?: string; stored: ResolvedUiElementContext | null
37
+ selector: Record<string, unknown> | null; tolerance: number | null; limit: number; before: string | null
38
+ fallback: FindElementResponse['element'] | null; trace: TraceStep[]; sequence: number
39
+ observed: ActualState; mode: Mode; target: ActionTargetResolved | null; device?: DeviceInfo
40
+ attempts: number; cached: Match | null; coordinatePending: boolean
41
+ }
42
+ export type Iteration = ReturnType<typeof planAdjustment> & {
43
+ element: UiElement; index: number; actionDevice?: DeviceInfo; observed: ActualState; verification: Verification
44
+ }
45
+ export type StrategyOutcome = 'next' | 'retry' | AdjustControlResponse
46
+
47
+ export async function createAdjustment(input: AdjustmentInput, deps: AdjustmentDependencies): Promise<AdjustmentState> {
48
+ const platform = input.platform || 'android'
49
+ const tolerance = typeof input.tolerance === 'number' && Number.isFinite(input.tolerance) ? Math.max(0, input.tolerance) : null
50
+ const limit = input.maxAttempts == null ? 5 : Math.max(1, Math.floor(Number(input.maxAttempts) || 1))
51
+ const stored = input.element_id ? deps.getHandle(input.element_id) ?? null : null
52
+ const before = await deps.fingerprint(platform, input.deviceId)
53
+ return { clock: deps.clock ?? systemClock, input: { ...input, property: input.property === undefined ? 'value' : input.property }, platform, deviceId: input.deviceId, stored,
54
+ selector: input.selector ?? (input.element_id ? { elementId: input.element_id } : null), tolerance, limit, before,
55
+ fallback: null, trace: [], sequence: 0, observed: null, mode: 'gesture', target: null,
56
+ attempts: 0, cached: null, coordinatePending: false }
57
+ }
58
+ export function recordAdjustment(s: AdjustmentState, stage: TraceStep['stage'], result: TraceStep['result'], metadata?: Record<string, unknown>) {
59
+ s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }))
60
+ }
61
+ export function failAdjustment(s: AdjustmentState, code: ActionFailureCode, reason: string, retryable = false,
62
+ target = s.target, device = s.device, observed = s.observed, attempts = s.attempts, mode: Mode = s.mode, after: string | null = null): AdjustControlResponse {
63
+ if (!s.trace.some(step => step.stage === 'resolve')) recordAdjustment(s, 'resolve', 'failure', { reason, failure_code: code })
64
+ if (!s.trace.some(step => step.stage === 'recover')) recordAdjustment(s, 'recover', retryable ? 'retry' : 'failure', {
65
+ reason, failure_code: code, retry_allowed: retryable, recovery_attempts: attempts, retry_depth: attempts })
66
+ const tolerance = s.tolerance ?? 0
67
+ const base = buildActionExecutionResult({ actionType: 'adjust_control', sourceModule: 'interact', device,
68
+ selector: s.selector, resolved: target, success: false, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
69
+ failure: { failureCode: code, retryable }, traceSteps: s.trace,
70
+ details: { target_value: s.input.targetValue, tolerance, property: s.input.property, attempts,
71
+ adjustment_mode: mode, actual_state: observed, converged: false, within_tolerance: false, reason } })
72
+ return { ...base, target_state: { property: s.input.property, target_value: s.input.targetValue, tolerance },
73
+ actual_state: observed, within_tolerance: false, converged: false, attempts, adjustment_mode: mode }
74
+ }
75
+ export async function succeedAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies,
76
+ reason = 'control converged to target value', mode = s.mode, device = p.actionDevice ?? s.device) {
77
+ const after = await deps.fingerprint(s.platform, s.deviceId)
78
+ return completeAdjustment({ actionType: 'adjust_control', sourceModule: 'interact', device,
79
+ selector: s.selector, resolved: s.target, success: true, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
80
+ traceSteps: s.trace, details: { target_value: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
81
+ property: s.input.property, attempts: s.attempts, adjustment_mode: mode, actual_state: p.observed,
82
+ converged: true, within_tolerance: true, reason } })
83
+ }
@@ -0,0 +1,48 @@
1
+ import { _controlAxis, _buildConservativeControlPoint, _buildControlPoint, _buildAdjustmentProbePoints } from './control-geometry.js'
2
+ import { recordAdjustment, succeedAdjustment, failAdjustment, type AdjustmentState, type AdjustmentDependencies, type Iteration, type StrategyOutcome } from './adjustment-context.js'
3
+ import { verifyControl } from './adjustment-semantic.js'
4
+
5
+ function points(p: Iteration) {
6
+ const bounds = p.bounds!, range = p.valueRange!, value = p.currentValue!
7
+ const axis = _controlAxis(p.element, bounds)
8
+ return { axis, target: _buildConservativeControlPoint(bounds, p.normalizedTargetValue, value, range.min, range.max, axis),
9
+ current: _buildControlPoint(bounds, (value - range.min) / (range.max - range.min), axis) }
10
+ }
11
+ export async function coordinateAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
12
+ if (s.coordinatePending) return 'next'
13
+ const geometry = points(p)
14
+ const probes = p.inferredSliderState ? [geometry.target] : _buildAdjustmentProbePoints(p.bounds!, p.normalizedTargetValue, p.currentValue!, p.valueRange!.min, p.valueRange!.max, geometry.axis)
15
+ for (let i = 0; i < probes.length && s.attempts < s.limit; i++) {
16
+ const point = probes[i]
17
+ s.mode = 'coordinate'
18
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'coordinate', point })
19
+ const action = await deps.tap({ platform: s.platform, x: point.x, y: point.y, deviceId: s.deviceId })
20
+ s.attempts++; p.actionDevice = action.device ?? p.actionDevice
21
+ if (!action.success) {
22
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts, mode: 'coordinate', point, success: false }); continue
23
+ }
24
+ await verifyControl(s, p, deps, 'control did not converge yet')
25
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
26
+ s.coordinatePending = true; s.cached = null
27
+ if (s.attempts < s.limit) return 'retry'
28
+ }
29
+ return 'next'
30
+ }
31
+ export async function gestureAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
32
+ if (s.attempts >= s.limit) return 'next'
33
+ const geometry = points(p)
34
+ s.coordinatePending = false; s.mode = 'gesture'
35
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'gesture', start: geometry.current, end: geometry.target })
36
+ const action = await deps.swipe({ platform: s.platform, x1: geometry.current.x, y1: geometry.current.y,
37
+ x2: geometry.target.x, y2: geometry.target.y, duration: p.inferredSliderState ? 520 : 220, deviceId: s.deviceId })
38
+ s.attempts++
39
+ if (!action.success) {
40
+ recordAdjustment(s, 'execute', 'failure', { attempt: s.attempts, mode: 'gesture', start: geometry.current, end: geometry.target, success: false })
41
+ return failAdjustment(s, 'UNKNOWN', action.error ?? 'adjustment gesture failed', false, s.target,
42
+ action.device ?? p.actionDevice, p.observed ?? p.actualState)
43
+ }
44
+ p.actionDevice = action.device ?? p.actionDevice
45
+ await verifyControl(s, p, deps, 'gesture adjustment did not converge yet')
46
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
47
+ return 'next'
48
+ }
@@ -0,0 +1,23 @@
1
+ import type { UiElement } from '../model.js'
2
+ import type { AdjustControlResponse } from '../../types.js'
3
+ import { _normalizeBounds } from '../targeting/element-helpers.js'
4
+ import { _inferLabeledSliderState } from './slider-labels.js'
5
+ import { _readNumericControlRange, _readControlStep, _quantizeTargetValue,
6
+ _defaultAdjustTolerance, _readNumericControlValue } from './control-values.js'
7
+
8
+ export function planAdjustment(currentEl: UiElement, elements: UiElement[], targetValue: number,
9
+ property: string, requestedTolerance: number | null) {
10
+ const inferredSliderState = _inferLabeledSliderState(elements, currentEl)
11
+ const nativeBounds = _normalizeBounds(currentEl.bounds)
12
+ const bounds = inferredSliderState?.touch_bounds ?? nativeBounds
13
+ const valueRange = _readNumericControlRange(currentEl) ?? inferredSliderState?.value_range ?? null
14
+ const step = _readControlStep(currentEl)
15
+ const normalizedTargetValue = valueRange ? _quantizeTargetValue(targetValue, valueRange.min, valueRange.max, step) : targetValue
16
+ const effectiveTolerance = requestedTolerance ?? _defaultAdjustTolerance(valueRange, step)
17
+ const currentValue = _readNumericControlValue(currentEl, property) ?? inferredSliderState?.current ?? null
18
+ const actualState: AdjustControlResponse['actual_state'] = currentValue !== null ? {
19
+ property, value: currentValue,
20
+ raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : inferredSliderState?.current
21
+ } : null
22
+ return { inferredSliderState, bounds, valueRange, normalizedTargetValue, effectiveTolerance, currentValue, actualState }
23
+ }
@@ -0,0 +1,104 @@
1
+ import type { UiSelector } from '../../contracts/selector.js'
2
+ import type { ControlWait } from './adjustment-context.js'
3
+ import type { FindElementResponse } from '../../types.js'
4
+ import type { UiElement } from '../model.js'
5
+ import { _normalizeBounds, _isAdjustableControl, _findFirstMatchingElement, _isAdjustableActionable } from '../targeting/element-helpers.js'
6
+ import { planAdjustment } from './adjustment-plan.js'
7
+ import { failAdjustment, recordAdjustment, succeedAdjustment, type AdjustmentState, type AdjustmentDependencies, type ControlMatch, type Iteration } from './adjustment-context.js'
8
+
9
+ function fallbackBounds(fallback: FindElementResponse['element']) {
10
+ const bounds = fallback?.bounds
11
+ if (Array.isArray(bounds)) return _normalizeBounds(bounds)
12
+ return bounds && typeof bounds === 'object' ? _normalizeBounds([Number(bounds.left), Number(bounds.top), Number(bounds.right), Number(bounds.bottom)]) : null
13
+ }
14
+ function findFallback(elements: UiElement[], fallback: FindElementResponse['element']) {
15
+ const bounds = fallbackBounds(fallback)
16
+ if (!bounds) return null
17
+ let index = elements.findIndex(el => {
18
+ const current = _normalizeBounds(el.bounds)
19
+ return !!current && current.every((value, i) => value === bounds[i])
20
+ })
21
+ if (index === -1) index = nearestControl(elements, bounds)
22
+ return index >= 0 && elements[index] ? { el: elements[index], idx: index } : null
23
+ }
24
+ function nearestControl(elements: UiElement[], bounds: [number, number, number, number]) {
25
+ const x = Math.floor((bounds[0] + bounds[2]) / 2), y = Math.floor((bounds[1] + bounds[3]) / 2)
26
+ let best = Infinity, index = -1
27
+ for (let i = 0; i < elements.length; i++) {
28
+ const el = elements[i]
29
+ if (!_isAdjustableControl(el)) continue
30
+ const current = _normalizeBounds(el.bounds)
31
+ if (!current) continue
32
+ const distance = Math.abs(Math.floor((current[0] + current[2]) / 2) - x) + Math.abs(Math.floor((current[1] + current[3]) / 2) - y)
33
+ if (distance < best) { best = distance; index = i }
34
+ }
35
+ return index
36
+ }
37
+ export async function resolveControl(s: AdjustmentState, deps: AdjustmentDependencies): Promise<ControlMatch | null> {
38
+ const tree = await deps.observe(s.platform, s.deviceId)
39
+ s.platform = tree?.device?.platform === 'ios' ? 'ios' : s.platform
40
+ s.deviceId = tree?.device?.id || s.deviceId
41
+ const elements = Array.isArray(tree?.elements) ? tree.elements : []
42
+ let match = null
43
+ if (s.input.element_id) {
44
+ const stored = deps.getHandle(s.input.element_id)
45
+ if (!stored) return null
46
+ const current = deps.currentHandle(elements, s.platform, s.deviceId, stored)
47
+ if (!current) return null
48
+ match = { el: current.el, idx: current.index }
49
+ } else {
50
+ if (s.fallback) match = findFallback(elements, s.fallback)
51
+ if (!match && s.input.selector) match = _findFirstMatchingElement(elements, s.input.selector)
52
+ }
53
+ return match ? { tree, device: tree?.device, match, resolvedTarget: deps.target(s.platform, s.deviceId, match) } : null
54
+ }
55
+ export async function initializeControl(s: AdjustmentState, deps: AdjustmentDependencies) {
56
+ const { selector, element_id } = s.input
57
+ if ((!selector && !element_id) || (selector && element_id)) {
58
+ return failAdjustment(s, 'ELEMENT_NOT_FOUND', 'exactly one of selector or element_id is required', false, null, undefined, null, 0, 'gesture')
59
+ }
60
+ if (!selector || element_id) return null
61
+ const wait = await deps.wait({ selector, condition: 'clickable', timeout_ms: 5000, poll_interval_ms: 300, platform: s.platform, deviceId: s.deviceId })
62
+ if (wait?.status === 'success' && wait?.element?.elementId) {
63
+ s.input.element_id = wait.element.elementId; s.fallback = null; return null
64
+ }
65
+ return initializeFallback(s, deps, wait, selector)
66
+ }
67
+ async function initializeFallback(s: AdjustmentState, deps: AdjustmentDependencies, wait: ControlWait | undefined, selector: UiSelector) {
68
+ const query = selector.text ?? selector.resource_id ?? selector.accessibility_id ?? ''
69
+ if (!query) return failAdjustment(s, wait?.error?.code === 'ELEMENT_NOT_FOUND' ? 'ELEMENT_NOT_FOUND' : 'TIMEOUT',
70
+ wait?.error?.message ?? 'adjustable control not found', wait?.error?.code === 'ELEMENT_NOT_FOUND', null, wait?.device, null, 0, 'gesture')
71
+ const fallback = await deps.find({ query, exact: false, timeoutMs: 3000, platform: s.platform, deviceId: s.deviceId })
72
+ if (!fallback.found || !fallback.element) return failAdjustment(s, 'ELEMENT_NOT_FOUND', wait?.error?.message ?? 'adjustable control not found', true, null, wait?.device, null, 0, 'gesture')
73
+ s.fallback = fallback.element
74
+ return null
75
+ }
76
+ export async function prepareIteration(s: AdjustmentState, deps: AdjustmentDependencies) {
77
+ const resolved = s.cached ? { tree: null, device: s.device, match: s.cached, resolvedTarget: deps.target(s.platform, s.deviceId, s.cached) } : await resolveControl(s, deps)
78
+ if (!resolved?.match || !resolved.resolvedTarget) return failAdjustment(s, 'STALE_REFERENCE', 'adjustable control could not be resolved', true)
79
+ s.device = resolved.device; s.target = resolved.resolvedTarget; s.cached = resolved.match
80
+ const element = resolved.match.el
81
+ const actionable = _isAdjustableActionable(element, s.stored?.stable_id)
82
+ if (!actionable.actionable) return failAdjustment(s, actionable.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', actionable.reason ?? 'adjustable control is not actionable', true)
83
+ const elements = Array.isArray(resolved.tree?.elements) ? resolved.tree.elements : s.cached ? [s.cached.el] : [element]
84
+ const plan = planAdjustment(element, elements, s.input.targetValue, s.input.property, s.tolerance)
85
+ s.observed = plan.actualState
86
+ if (!s.trace.some(step => step.stage === 'resolve')) recordAdjustment(s, 'resolve', 'success', { resolved_target: s.target, current_value: plan.currentValue, adjustment_mode: s.mode })
87
+ const iteration: Iteration = { ...plan, element, index: resolved.match.idx, actionDevice: s.device, observed: plan.actualState,
88
+ verification: { verification: null, observedState: plan.actualState, withinTolerance: false } }
89
+ const invalid = validateControl(s, iteration)
90
+ if (invalid) return invalid
91
+ if (plan.currentValue !== null && Math.abs(plan.currentValue - plan.normalizedTargetValue) <= plan.effectiveTolerance) {
92
+ recordAdjustment(s, 'verify', 'success', { property: s.input.property, target_value: plan.normalizedTargetValue, actual_state: plan.actualState, reason: 'control already within tolerance' })
93
+ return succeedAdjustment(s, iteration, deps, 'control already within tolerance', 'semantic', s.device)
94
+ }
95
+ if (!plan.bounds) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds')
96
+ return iteration
97
+ }
98
+ function validateControl(s: AdjustmentState, p: Iteration) {
99
+ if (s.input.property !== 'value' && s.input.property !== 'raw_value') return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only')
100
+ if (!_isAdjustableControl(p.element) && !p.inferredSliderState) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control')
101
+ if (p.currentValue === null) return failAdjustment(s, 'UNKNOWN', 'current numeric value is unavailable')
102
+ if (p.valueRange && (s.input.targetValue < p.valueRange.min || s.input.targetValue > p.valueRange.max)) return failAdjustment(s, 'CONTROL_CONVERGENCE_FAILED', `targetValue ${s.input.targetValue} is outside the control range ${p.valueRange.min}..${p.valueRange.max}`)
103
+ return null
104
+ }
@@ -0,0 +1,34 @@
1
+ import type { AdjustControlResponse } from '../../types.js'
2
+ import { buildActionExecutionResult } from '../../runtime/action-results.js'
3
+
4
+ type SuccessDetails = {
5
+ property: string
6
+ target_value: number
7
+ tolerance: number
8
+ attempts: number
9
+ adjustment_mode: AdjustControlResponse['adjustment_mode']
10
+ actual_state: AdjustControlResponse['actual_state']
11
+ reason: string
12
+ converged: true
13
+ within_tolerance: true
14
+ }
15
+ type AdjustmentSuccess = Parameters<typeof buildActionExecutionResult>[0] & { details: SuccessDetails }
16
+
17
+ /** Keep the action envelope and top-level control result in agreement. */
18
+ export function completeAdjustment(input: AdjustmentSuccess): AdjustControlResponse {
19
+ const base = buildActionExecutionResult(input)
20
+ const details = input.details
21
+ return {
22
+ ...base,
23
+ target_state: {
24
+ property: details.property,
25
+ target_value: details.target_value,
26
+ tolerance: details.tolerance
27
+ },
28
+ actual_state: details.actual_state,
29
+ within_tolerance: true,
30
+ converged: true,
31
+ attempts: details.attempts,
32
+ adjustment_mode: details.adjustment_mode
33
+ }
34
+ }
@@ -0,0 +1,53 @@
1
+ import { _hasDirectAdjustmentAction, _adjustmentDirections } from './control-values.js'
2
+ import { verifyAdjustment } from './adjustment-verification.js'
3
+ import { recordAdjustment, succeedAdjustment, type AdjustmentState, type AdjustmentDependencies, type Iteration, type StrategyOutcome } from './adjustment-context.js'
4
+
5
+ export async function verifyControl(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies, fallback: string) {
6
+ p.verification = await verifyAdjustment({ labeled: !!p.inferredSliderState, currentEl: p.element, actualState: p.actualState,
7
+ property: s.input.property, target: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
8
+ expectation: { element_id: s.target?.elementId ?? s.input.element_id, selector: s.input.selector ?? undefined,
9
+ property: s.input.property, expected: p.normalizedTargetValue, platform: s.platform, deviceId: s.deviceId }
10
+ }, { observe: () => deps.observe(s.platform, s.deviceId), expectState: deps.expectState })
11
+ p.observed = p.verification.observedState; s.observed = p.observed
12
+ recordAdjustment(s, 'verify', p.verification.withinTolerance ? 'success' : 'retry', {
13
+ attempt: s.attempts, property: s.input.property, target_value: p.normalizedTargetValue,
14
+ actual_state: p.observed, reason: p.verification.verification?.reason ?? fallback })
15
+ }
16
+ export async function directAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
17
+ if (!_hasDirectAdjustmentAction(p.element) || s.attempts >= s.limit) return 'next'
18
+ const { interact, resolved } = await deps.service(s.platform, s.deviceId)
19
+ if (typeof interact.setAdjustableValue !== 'function') return 'next'
20
+ s.mode = 'semantic'
21
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'semantic', strategy: 'direct_set_value', target_value: p.normalizedTargetValue })
22
+ const action = await interact.setAdjustableValue({ deviceId: resolved.id, target: s.target, property: s.input.property, value: p.normalizedTargetValue })
23
+ s.attempts++; p.actionDevice = action?.device ?? p.actionDevice
24
+ if (action?.success === false) return 'next'
25
+ await verifyControl(s, p, deps, 'direct adjustment did not converge yet')
26
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
27
+ s.cached = null
28
+ return s.attempts < s.limit ? 'retry' : 'next'
29
+ }
30
+ export async function incrementalAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
31
+ const directions = _adjustmentDirections(p.element)
32
+ if ((!directions.increment && !directions.decrement) || s.attempts >= s.limit) return 'next'
33
+ const { interact, resolved } = await deps.service(s.platform, s.deviceId)
34
+ const adjust = typeof interact.adjustAccessibleValue === 'function' ? interact.adjustAccessibleValue.bind(interact) : null
35
+ while (adjust && s.attempts < s.limit) {
36
+ const direction = p.normalizedTargetValue > (p.observed?.value ?? p.currentValue!) ? 'increment' : 'decrement'
37
+ if (!directionAllowed(direction, directions)) break
38
+ s.mode = 'semantic'
39
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'semantic', strategy: 'accessibility_increment_decrement', direction })
40
+ const action = await adjust({ deviceId: resolved.id, target: s.target, property: s.input.property, direction })
41
+ s.attempts++; p.actionDevice = action?.device ?? p.actionDevice
42
+ if (action?.success === false) break
43
+ await verifyControl(s, p, deps, 'incremental adjustment did not converge yet')
44
+ if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
45
+ s.cached = null
46
+ if (s.attempts < s.limit) return 'retry'
47
+ }
48
+ return 'next'
49
+ }
50
+
51
+ function directionAllowed(direction: 'increment' | 'decrement', directions: { increment: boolean; decrement: boolean }) {
52
+ return !((direction === 'increment' && !directions.increment) || (direction === 'decrement' && !directions.decrement))
53
+ }
@@ -0,0 +1,60 @@
1
+ import type { AdjustControlResponse, ExpectStateResponse } from '../../types.js'
2
+ import type { UiElement } from '../model.js'
3
+ import type { UiSelector } from '../../contracts/selector.js'
4
+ import { _inferLabeledSliderState } from './slider-labels.js'
5
+ import { _parseNumericControlValue } from './control-values.js'
6
+
7
+ interface StateExpectation {
8
+ element_id?: string
9
+ selector?: UiSelector
10
+ property: string
11
+ expected: number
12
+ platform: 'android' | 'ios'
13
+ deviceId?: string
14
+ }
15
+ export interface AdjustmentVerificationInput {
16
+ labeled: boolean
17
+ currentEl: UiElement
18
+ actualState: AdjustControlResponse['actual_state']
19
+ property: string
20
+ target: number
21
+ tolerance: number
22
+ expectation: StateExpectation
23
+ }
24
+ export interface AdjustmentVerificationDependencies {
25
+ observe(): Promise<{ elements?: UiElement[] } | null>
26
+ expectState(input: StateExpectation): Promise<ExpectStateResponse>
27
+ }
28
+
29
+ /** Every verification obtains fresh evidence through its supplied observation boundary. */
30
+ export async function verifyAdjustment(input: AdjustmentVerificationInput, dependencies: AdjustmentVerificationDependencies) {
31
+ if (input.labeled) return verifyLabeledControl(input, dependencies)
32
+ const verification = await dependencies.expectState(input.expectation)
33
+ const observedValue = _parseNumericControlValue(verification?.observed_state?.value) ??
34
+ _parseNumericControlValue(verification?.observed_state?.raw_value)
35
+ const observedState = observedValue !== null ? {
36
+ property: input.property, value: observedValue,
37
+ raw_value: typeof verification?.observed_state?.raw_value === 'number' ? verification.observed_state.raw_value : undefined
38
+ } : input.actualState
39
+ return { verification, observedState, withinTolerance: withinTolerance(observedValue, input) }
40
+ }
41
+ async function verifyLabeledControl(input: AdjustmentVerificationInput, dependencies: AdjustmentVerificationDependencies) {
42
+ const tree = await dependencies.observe()
43
+ const elements = Array.isArray(tree?.elements) ? tree.elements : []
44
+ const refreshed = _inferLabeledSliderState(elements, input.currentEl)
45
+ const observedValue = refreshed?.current ?? null
46
+ const observedState = observedValue !== null ? {
47
+ property: input.property, value: observedValue, raw_value: observedValue
48
+ } : input.actualState
49
+ const matched = withinTolerance(observedValue, input)
50
+ return {
51
+ verification: {
52
+ success: matched, observed_state: observedState,
53
+ reason: observedValue !== null ? 'visible labeled value read from nearby slider labels' : 'visible labeled value unavailable'
54
+ },
55
+ observedState, withinTolerance: matched
56
+ }
57
+ }
58
+ function withinTolerance(value: number | null, input: AdjustmentVerificationInput) {
59
+ return value !== null && Math.abs(value - input.target) <= input.tolerance
60
+ }
@@ -0,0 +1,49 @@
1
+ import type { AdjustControlResponse } from '../../types.js'
2
+ import { createAdjustment, failAdjustment, recordAdjustment, succeedAdjustment,
3
+ type AdjustmentInput, type AdjustmentDependencies, type AdjustmentState, type Iteration } from './adjustment-context.js'
4
+ import { initializeControl, prepareIteration } from './adjustment-resolution.js'
5
+ import { directAdjustment, incrementalAdjustment } from './adjustment-semantic.js'
6
+ import { coordinateAdjustment, gestureAdjustment } from './adjustment-gestures.js'
7
+
8
+ function cacheObservation(s: AdjustmentState, p: Iteration) {
9
+ s.cached = p.inferredSliderState ? null : {
10
+ el: { ...p.element, state: { ...(p.element.state ?? null), ...(p.observed ? {
11
+ [p.observed.property]: p.observed.value, raw_value: p.observed.raw_value ?? p.observed.value
12
+ } : {}) } }, idx: p.index
13
+ }
14
+ }
15
+ async function attempt(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies) {
16
+ for (const strategy of [directAdjustment, incrementalAdjustment]) {
17
+ const outcome = await strategy(s, p, deps)
18
+ if (outcome !== 'next') return outcome
19
+ }
20
+ if (!p.bounds) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds')
21
+ if (!p.valueRange) return failAdjustment(s, 'UNKNOWN', 'value_range unavailable')
22
+ for (const strategy of [coordinateAdjustment, gestureAdjustment]) {
23
+ const outcome = await strategy(s, p, deps)
24
+ if (outcome !== 'next') return outcome
25
+ }
26
+ s.observed = p.observed
27
+ if (p.verification.withinTolerance) {
28
+ const reason = p.verification.verification?.reason ?? 'control converged to target value'
29
+ recordAdjustment(s, 'verify', 'success', { attempt: s.attempts, property: s.input.property,
30
+ target_value: p.normalizedTargetValue, actual_state: p.observed, reason })
31
+ return succeedAdjustment(s, p, deps, reason)
32
+ }
33
+ cacheObservation(s, p)
34
+ return 'next'
35
+ }
36
+ export async function adjustControl(input: AdjustmentInput, deps: AdjustmentDependencies): Promise<AdjustControlResponse> {
37
+ const state = await createAdjustment(input, deps)
38
+ const invalid = await initializeControl(state, deps)
39
+ if (invalid) return invalid
40
+ for (let index = 0; index < state.limit && state.attempts < state.limit; index++) {
41
+ const prepared = await prepareIteration(state, deps)
42
+ if ('success' in prepared) return prepared
43
+ const outcome = await attempt(state, prepared, deps)
44
+ if (outcome !== 'retry' && outcome !== 'next') return outcome
45
+ }
46
+ const after = await deps.fingerprint(state.platform, state.deviceId)
47
+ return failAdjustment(state, 'CONTROL_CONVERGENCE_FAILED', 'control did not converge within the allotted attempts', true,
48
+ state.target, state.device, state.observed, state.attempts, state.mode, after)
49
+ }