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
@@ -38,7 +38,13 @@ const orders = new Map<string, DeviceOrder>()
38
38
  const stamp = (): Stamp => ({ wall: Date.now(), mono: performance.now() })
39
39
  const deviceKey = (d: EvidenceDevice) => `${d.platform}:${d.id}`
40
40
  const sameDevice = (a: EvidenceDevice | null, b: EvidenceDevice | null) => !!a && !!b && deviceKey(a) === deviceKey(b)
41
- function validDevice(d: any): EvidenceDevice | null {
41
+ interface EvidencePayload {
42
+ device?: unknown; error?: string; meta?: { error?: string }; success?: boolean; fingerprint?: string | null
43
+ captured_at_ms?: number; snapshot_session_id?: string; snapshot_revision?: number
44
+ logs?: Array<{ timestamp?: unknown; _iso?: unknown }>; entries?: Array<{ timestamp?: unknown; _iso?: unknown }>
45
+ }
46
+ function validDevice(value: unknown): EvidenceDevice | null {
47
+ const d = value as EvidenceDevice | null
42
48
  return d && ['android', 'ios'].includes(d.platform) && typeof d.id === 'string' && d.id && !['default', 'booted'].includes(d.id)
43
49
  ? { platform: d.platform, id: d.id } : null
44
50
  }
@@ -68,7 +74,7 @@ function prune() {
68
74
  export function validateActionRef(value: unknown): ActionRef | undefined {
69
75
  if (value === undefined) return undefined
70
76
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
71
- Object.keys(value).some(key => !['sessionId', 'actionId'].includes(key))) throw new Error('INVALID_ARGUMENT: invalid actionRef')
77
+ Object.keys(value).some(key => !['sessionId', 'actionId'].includes(key))) throw new Error('INVALID_ARGUMENT: invalid actionRef')
72
78
  const ref = value as ActionRef
73
79
  for (const v of [ref.sessionId, ref.actionId]) if (typeof v !== 'string' || v.length < 1 || v.length > 128) throw new Error('INVALID_ARGUMENT: invalid actionRef')
74
80
  return { sessionId: ref.sessionId, actionId: ref.actionId }
@@ -114,11 +120,13 @@ export function omitEvidence(kind: Kind) {
114
120
  const current = context.getStore()
115
121
  if (!current) return
116
122
  const start = stamp()
117
- current.observations.push({ value: {
118
- evidence_id: randomUUID(), kind, status: 'omitted', capture_started_at_ms: null,
119
- capture_completed_at_ms: null, source_captured_at_ms: null, age_ms: null,
120
- freshness: kind === 'logs' ? 'not_applicable' : 'unknown', freshness_reasons: [], device: current.device
121
- }, start, end: null, order: null, startSequence: 0, endSequence: 0, overlapping: false, reused: false })
123
+ current.observations.push({
124
+ value: {
125
+ evidence_id: randomUUID(), kind, status: 'omitted', capture_started_at_ms: null,
126
+ capture_completed_at_ms: null, source_captured_at_ms: null, age_ms: null,
127
+ freshness: kind === 'logs' ? 'not_applicable' : 'unknown', freshness_reasons: [], device: current.device
128
+ }, start, end: null, order: null, startSequence: 0, endSequence: 0, overlapping: false, reused: false
129
+ })
122
130
  }
123
131
  export async function captureEvidence<T>(kind: Kind, device: EvidenceDevice | null, operation: () => Promise<T>): Promise<T> {
124
132
  const current = context.getStore()
@@ -127,42 +135,19 @@ export async function captureEvidence<T>(kind: Kind, device: EvidenceDevice | nu
127
135
  if (target) bindEvidenceDevice(target)
128
136
  const order = orderFor(target)
129
137
  const start = stamp()
130
- const acquisition: Acquisition = { value: {
131
- evidence_id: randomUUID(), kind, status: 'failed', capture_started_at_ms: start.wall,
132
- capture_completed_at_ms: null, source_captured_at_ms: null, age_ms: null,
133
- freshness: 'unknown', freshness_reasons: [], device: target
134
- }, start, end: null, order, startSequence: order?.sequence ?? 0,
135
- endSequence: 0, overlapping: !!order?.active, reused: false }
138
+ const acquisition: Acquisition = {
139
+ value: {
140
+ evidence_id: randomUUID(), kind, status: 'failed', capture_started_at_ms: start.wall,
141
+ capture_completed_at_ms: null, source_captured_at_ms: null, age_ms: null,
142
+ freshness: 'unknown', freshness_reasons: [], device: target
143
+ }, start, end: null, order, startSequence: order?.sequence ?? 0,
144
+ endSequence: 0, overlapping: !!order?.active, reused: false
145
+ }
136
146
  current.observations.push(acquisition)
137
147
  try {
138
- const result = await operation() as any
139
- const value = acquisition.value
140
- const resolved = validDevice(result?.device) ?? target
141
- value.device = resolved
142
- if (resolved) bindEvidenceDevice(resolved)
143
- const failure = result?.error ?? result?.meta?.error
144
- value.status = failure || result?.success === false || (kind === 'fingerprint' && !result?.fingerprint) ? 'failed' : 'captured'
145
- if (failure) value.error = failure
146
- const source = result?.captured_at_ms
147
- if (Number.isFinite(source) && source > 0) {
148
- value.source_captured_at_ms = source
149
- acquisition.reused = source < start.wall
150
- }
151
- if (kind === 'ui_tree') {
152
- value.snapshot_session_id = result?.snapshot_session_id
153
- value.snapshot_revision = result?.snapshot_revision
154
- }
155
- if (kind === 'logs') {
156
- const entries = result?.logs ?? result?.entries ?? []
157
- const times = entries.map((e: any) => {
158
- const t = e?.timestamp ?? e?._iso
159
- return typeof t === 'number' ? t : typeof t === 'string' ? Date.parse(t) : NaN
160
- }).filter((t: number) => Number.isFinite(t)) as number[]
161
- value.clock = 'device_or_unknown'; value.event_time_comparable = false
162
- value.earliest_entry_at_ms = times.length ? times.reduce((a, b) => Math.min(a, b)) : null
163
- value.latest_entry_at_ms = times.length ? times.reduce((a, b) => Math.max(a, b)) : null
164
- }
165
- return result
148
+ const original = await operation()
149
+ recordObservationResult(acquisition, original as EvidencePayload | null, kind, target, start)
150
+ return original
166
151
  } catch (error) {
167
152
  acquisition.value.error = error instanceof Error ? error.message : String(error)
168
153
  throw error
@@ -188,35 +173,13 @@ function assemble(current: EvidenceContext) {
188
173
  const link = association(current)
189
174
  const action = link.association === 'unavailable' ? null : current.action
190
175
  const clockChanged = end.wall < current.start.wall
191
- const observations = current.observations.map(a => {
192
- const v = { ...a.value, freshness_reasons: [] as string[] }
193
- if (v.status === 'omitted') return v
194
- const componentAction = sameDevice(v.device, action?.device ?? null) ? action : null
195
- const sourceMono = a.end && v.source_captured_at_ms !== null ? a.end.mono + v.source_captured_at_ms - a.end.wall : a.end?.mono
196
- const reason = (freshness: Freshness, code: string) => { v.freshness = freshness; v.freshness_reasons.push(code) }
197
- if (v.kind === 'logs') reason('unknown', 'DEVICE_CLOCK_UNVERIFIED')
198
- else if (clockChanged || (a.end && a.end.wall < a.start.wall)) reason('unknown', 'CLOCK_DISCONTINUITY')
199
- else if (v.source_captured_at_ms !== null && a.end && v.source_captured_at_ms > a.end.wall) reason('unknown', 'CLOCK_DISCONTINUITY')
200
- else if (!v.device || !a.order || orders.get(deviceKey(v.device)) !== a.order || v.status !== 'captured' || !a.end) reason('unknown', 'CAPTURE_TIME_UNKNOWN')
201
- else if (componentAction?.dispatchStart && sourceMono !== undefined && sourceMono < componentAction.dispatchStart.mono) reason('stale', 'BEFORE_ACTION')
202
- else if (a.order && (a.order.sequence > a.endSequence || (a.reused && sourceMono !== undefined && sourceMono < a.order.lastStarted))) reason('stale', 'SUPERSEDED_BY_ACTION')
203
- else if (a.overlapping) reason('unknown', 'OVERLAPPING_ACTION')
204
- else if (a.reused) reason('unknown', 'REUSED_OBSERVATION')
205
- else { v.freshness = 'fresh' }
206
- if (!clockChanged && a.end && a.end.wall >= a.start.wall && v.source_captured_at_ms !== null && v.source_captured_at_ms <= end.wall) {
207
- v.age_ms = Math.max(0, end.mono - a.end.mono + a.end.wall - v.source_captured_at_ms)
208
- }
209
- return { ...v, session_id: sessionId, correlation_id: componentAction?.correlationId ?? current.correlationId,
210
- action_id: componentAction?.id ?? null, association: componentAction ? 'internal' : link.association }
211
- })
176
+ const observations = current.observations.map(a => assembleObservation(a, action, current, link, end, clockChanged))
212
177
  return {
213
178
  session_id: sessionId, correlation_id: action?.correlationId ?? current.correlationId,
214
179
  action_id: action?.id ?? null, device: current.device, ...link,
215
180
  ...(current.ref ? { requested_action_ref: current.ref } : {}),
216
181
  operation_started_at_ms: current.start.wall, operation_completed_at_ms: current.end?.wall ?? end.wall,
217
- dispatch_started_at_ms: action?.dispatchStart?.wall ?? null,
218
- dispatch_completed_at_ms: action?.dispatchEnd?.wall ?? null,
219
- verification_completed_at_ms: current.verification?.wall ?? action?.verification?.wall ?? null,
182
+ ...actionTimes(action, current),
220
183
  observations, trace_context: { supported: !!current.systemTraceId, trace_id: current.systemTraceId ?? null, markers: [] }
221
184
  }
222
185
  }
@@ -224,10 +187,14 @@ export async function withEvidence<T>(options: { action?: boolean; journey?: boo
224
187
  prune()
225
188
  const parent = context.getStore()
226
189
  const id = randomUUID()
227
- const record: ActionRecord | null = options.action ? { id, correlationId: parent?.correlationId ?? id, device: null,
228
- dispatchStart: null, dispatchEnd: null, verification: null, completed: null } : options.ref?.sessionId === sessionId ? active.get(options.ref.actionId) ?? history.get(options.ref.actionId) ?? null : null
229
- const current: EvidenceContext = { systemTraceId: parent?.systemTraceId, correlationId: options.action ? record!.correlationId : options.journey ? id : randomUUID(),
230
- action: record, ref: options.ref, device: null, start: stamp(), end: null, verification: null, observations: [], children: [], isAction: !!options.action }
190
+ const record: ActionRecord | null = options.action ? {
191
+ id, correlationId: parent?.correlationId ?? id, device: null,
192
+ dispatchStart: null, dispatchEnd: null, verification: null, completed: null
193
+ } : options.ref?.sessionId === sessionId ? active.get(options.ref.actionId) ?? history.get(options.ref.actionId) ?? null : null
194
+ const current: EvidenceContext = {
195
+ systemTraceId: parent?.systemTraceId, correlationId: options.action ? record!.correlationId : options.journey ? id : randomUUID(),
196
+ action: record, ref: options.ref, device: null, start: stamp(), end: null, verification: null, observations: [], children: [], isAction: !!options.action
197
+ }
231
198
  if (options.action && record) active.set(id, record)
232
199
  if (parent && options.action) parent.children.push(current)
233
200
  try {
@@ -243,7 +210,7 @@ export async function withEvidence<T>(options: { action?: boolean; journey?: boo
243
210
  }
244
211
  }
245
212
 
246
- export function refreshJourneyEvidence(steps: Array<{ result?: { evidence?: unknown } }>) {
213
+ export function refreshJourneyEvidence(steps: Array<{ result?: { evidence?: unknown } | null }>) {
247
214
  const current = context.getStore()
248
215
  if (!current) return
249
216
  for (const step of steps) {
@@ -260,3 +227,88 @@ export function completeJourneyVerification(actionId: string) {
260
227
  child.action.verification = child.verification
261
228
  }
262
229
  }
230
+
231
+ function recordObservationResult(acquisition: Acquisition, result: EvidencePayload | null, kind: Kind, target: EvidenceDevice | null, start: Stamp) {
232
+ const value = acquisition.value
233
+ const resolved = validDevice(result?.device) ?? target
234
+ value.device = resolved
235
+ if (resolved) bindEvidenceDevice(resolved)
236
+ const failure = result?.error ?? result?.meta?.error
237
+ value.status = failure || result?.success === false || (kind === 'fingerprint' && !result?.fingerprint) ? 'failed' : 'captured'
238
+ if (failure) value.error = failure
239
+ recordCaptureTime(acquisition, result, start)
240
+ if (kind === 'ui_tree') {
241
+ value.snapshot_session_id = result?.snapshot_session_id
242
+ value.snapshot_revision = result?.snapshot_revision
243
+ }
244
+ if (kind === 'logs') recordLogWindow(value, result)
245
+
246
+ }
247
+
248
+ function recordLogWindow(value: Component, result: EvidencePayload | null) {
249
+ const entries = result?.logs ?? result?.entries ?? []
250
+ const times = entries.map((e: { timestamp?: unknown; _iso?: unknown } | null) => {
251
+ const t = e?.timestamp ?? e?._iso
252
+ return typeof t === 'number' ? t : typeof t === 'string' ? Date.parse(t) : NaN
253
+ }).filter((t: number) => Number.isFinite(t)) as number[]
254
+ value.clock = 'device_or_unknown'; value.event_time_comparable = false
255
+ value.earliest_entry_at_ms = times.length ? times.reduce((a, b) => Math.min(a, b)) : null
256
+ value.latest_entry_at_ms = times.length ? times.reduce((a, b) => Math.max(a, b)) : null
257
+ }
258
+
259
+ function assembleObservation(a: Acquisition, action: ActionRecord | null, current: EvidenceContext, link: ReturnType<typeof association>, end: Stamp, clockChanged: boolean) {
260
+
261
+ const v = { ...a.value, freshness_reasons: [] as string[] }
262
+ if (v.status === 'omitted') return v
263
+ const componentAction = sameDevice(v.device, action?.device ?? null) ? action : null
264
+ const sourceMono = a.end && v.source_captured_at_ms !== null ? a.end.mono + v.source_captured_at_ms - a.end.wall : a.end?.mono
265
+ const freshness = observationFreshness(a, componentAction, sourceMono, clockChanged)
266
+ if (freshness) { v.freshness = freshness[0]; v.freshness_reasons.push(freshness[1]) }
267
+ else v.freshness = 'fresh'
268
+
269
+ if (!clockChanged && a.end && a.end.wall >= a.start.wall && v.source_captured_at_ms !== null && v.source_captured_at_ms <= end.wall) {
270
+ v.age_ms = Math.max(0, end.mono - a.end.mono + a.end.wall - v.source_captured_at_ms)
271
+ }
272
+ return {
273
+ ...v, session_id: sessionId, correlation_id: componentAction?.correlationId ?? current.correlationId,
274
+ action_id: componentAction?.id ?? null, association: componentAction ? 'internal' : link.association
275
+ }
276
+
277
+ }
278
+
279
+ function observationFreshness(a: Acquisition, action: ActionRecord | null, sourceMono: number | undefined, clockChanged: boolean): [Freshness, string] | null {
280
+ const v = a.value
281
+ if (v.kind === 'logs') return ['unknown', 'DEVICE_CLOCK_UNVERIFIED']
282
+ if (clockDiscontinuity(a, clockChanged)) return ['unknown', 'CLOCK_DISCONTINUITY']
283
+ if (!knownCapture(a)) return ['unknown', 'CAPTURE_TIME_UNKNOWN']
284
+ if (action?.dispatchStart && sourceMono !== undefined && sourceMono < action.dispatchStart.mono) return ['stale', 'BEFORE_ACTION']
285
+ if (superseded(a, sourceMono)) return ['stale', 'SUPERSEDED_BY_ACTION']
286
+ if (a.overlapping) return ['unknown', 'OVERLAPPING_ACTION']
287
+ if (a.reused) return ['unknown', 'REUSED_OBSERVATION']
288
+ return null
289
+ }
290
+ function clockDiscontinuity(a: Acquisition, changed: boolean) {
291
+ return changed || !!(a.end && a.end.wall < a.start.wall) || !!(a.value.source_captured_at_ms !== null && a.end && a.value.source_captured_at_ms > a.end.wall)
292
+ }
293
+ function knownCapture(a: Acquisition) {
294
+ return !!a.value.device && !!a.order && orders.get(deviceKey(a.value.device)) === a.order && a.value.status === 'captured' && !!a.end
295
+ }
296
+ function superseded(a: Acquisition, sourceMono: number | undefined) {
297
+ return !!a.order && (a.order.sequence > a.endSequence || (a.reused && sourceMono !== undefined && sourceMono < a.order.lastStarted))
298
+ }
299
+
300
+ function actionTimes(action: ActionRecord | null, current: EvidenceContext) {
301
+ return {
302
+ dispatch_started_at_ms: action?.dispatchStart?.wall ?? null,
303
+ dispatch_completed_at_ms: action?.dispatchEnd?.wall ?? null,
304
+ verification_completed_at_ms: current.verification?.wall ?? action?.verification?.wall ?? null,
305
+ }
306
+ }
307
+
308
+ function recordCaptureTime(acquisition: Acquisition, result: EvidencePayload | null, start: Stamp) {
309
+ const source = result?.captured_at_ms
310
+ if (typeof source === 'number' && Number.isFinite(source) && source > 0) {
311
+ acquisition.value.source_captured_at_ms = source
312
+ acquisition.reused = source < start.wall
313
+ }
314
+ }
@@ -1,156 +1,18 @@
1
- import { omitEvidence } from './evidence.js'
1
+ import type { GetUITreeResponse, StateDelta } from '../types.js'
2
2
  import { withResolvedDevice } from '../utils/resolve-device.js'
3
- import { measure } from '../utils/timing.js'
4
3
  import { ReadinessTracker } from './readiness.js'
5
4
  import { resolveTargetDevice } from '../utils/resolve-device.js'
6
5
  import { AndroidObserve } from './android.js'
7
6
  import { iOSObserve } from './ios.js'
8
- import type {
9
- CaptureDebugSnapshotRawResponse,
10
- SnapshotSemanticResponse
11
- } from '../types.js'
12
- import { deriveSnapshotMetadata, getLatestStateDelta, getStateDelta } from './snapshot-metadata.js'
7
+
8
+ import { getLatestStateDelta, getStateDelta } from './snapshot-metadata.js'
13
9
 
14
10
  export { AndroidObserve } from './android.js'
15
11
  export { iOSObserve } from './ios.js'
16
12
 
17
- interface SnapshotTreeElementLike {
18
- text?: string | null
19
- contentDescription?: string | null
20
- contentDesc?: string | null
21
- accessibilityLabel?: string | null
22
- resourceId?: string | null
23
- id?: string | null
24
- type?: string | null
25
- class?: string | null
26
- clickable?: boolean
27
- enabled?: boolean
28
- visible?: boolean
29
- state?: unknown
30
- stable_id?: string | null
31
- role?: string | null
32
- test_tag?: string | null
33
- selector?: unknown
34
- semantic?: unknown
35
- }
36
-
37
- interface SnapshotTreeLike {
38
- screen?: string | null
39
- elements?: SnapshotTreeElementLike[]
40
- }
41
-
42
- function normalizeHint(value: unknown): string {
43
- if (value === null || value === undefined) return ''
44
- return String(value).trim().replace(/\s+/g, ' ').toLowerCase()
45
- }
46
-
47
- function titleCase(value: string): string {
48
- return value
49
- .replace(/[_-]+/g, ' ')
50
- .replace(/\s+/g, ' ')
51
- .trim()
52
- .replace(/\b\w/g, (match) => match.toUpperCase())
53
- }
54
-
55
- function shortActivityName(activity: string | null | undefined): string | null {
56
- if (!activity) return null
57
- const trimmed = String(activity).trim()
58
- if (!trimmed) return null
59
- const lastSegment = trimmed.split('.').pop() || trimmed
60
- const withoutSuffix = lastSegment.replace(/Activity$/, '')
61
- return withoutSuffix ? titleCase(withoutSuffix) : titleCase(lastSegment)
62
- }
63
-
64
- function collectSnapshotTexts(tree: SnapshotTreeLike | null | undefined) {
65
- const elements = Array.isArray(tree?.elements) ? tree!.elements! : []
66
- const texts: string[] = []
67
- const actionables: string[] = []
68
-
69
- for (const element of elements) {
70
- const rawText = element?.text ?? element?.contentDescription ?? element?.contentDesc ?? element?.accessibilityLabel ?? element?.resourceId ?? element?.id ?? ''
71
- const text = normalizeHint(rawText)
72
- if (text) texts.push(text)
73
- if (element?.clickable && element?.enabled !== false && text) {
74
- actionables.push(text)
75
- }
76
- }
77
-
78
- return {
79
- texts: Array.from(new Set(texts)),
80
- actionables: Array.from(new Set(actionables))
81
- }
82
- }
83
-
84
- function inferSnapshotScreen(raw: CaptureDebugSnapshotRawResponse): string | null {
85
- const tree = raw.ui_tree as SnapshotTreeLike | null | undefined
86
- const treeScreen = normalizeHint(tree?.screen)
87
- if (treeScreen) return titleCase(treeScreen)
88
-
89
- const activity = shortActivityName(raw.activity)
90
- if (activity) return activity
91
-
92
- const { texts } = collectSnapshotTexts(tree)
93
- if (texts.length > 0) return titleCase(texts[0])
94
-
95
- return null
96
- }
97
-
98
- function deriveSnapshotSemantic(raw: CaptureDebugSnapshotRawResponse): SnapshotSemanticResponse | null {
99
- const tree = raw.ui_tree as SnapshotTreeLike | null | undefined
100
- const { texts, actionables } = collectSnapshotTexts(tree)
101
- const screenFromTree = normalizeHint(tree?.screen)
102
- const activityHint = normalizeHint(raw.activity)
103
- const screen = inferSnapshotScreen(raw)
104
-
105
- if (!screen && !activityHint && texts.length === 0 && !raw.logs.length) return null
106
-
107
- const hasErrorLogs = raw.logs.some((entry) => /error|fatal exception|exception|failed/i.test(entry.message))
108
- const hasLoadingSignals = texts.some((text) => /loading|please wait|spinner|progress/i.test(text))
109
- const hasPrimaryText = texts.some((text) => /sign in|log in|login|home|checkout|settings|menu|profile|search/i.test(text))
110
- const hasScreenshot = typeof raw.screenshot === 'string' && raw.screenshot.length > 0
111
- const hasUiTree = !!tree && Array.isArray(tree.elements)
112
-
113
- const signals: Record<string, string | number | boolean> = {
114
- has_activity: !!activityHint,
115
- has_ui_tree: hasUiTree,
116
- has_screenshot: hasScreenshot,
117
- has_visible_text: texts.length > 0,
118
- has_clickable_elements: actionables.length > 0,
119
- has_error_logs: hasErrorLogs,
120
- has_loading_signals: hasLoadingSignals,
121
- has_primary_text: hasPrimaryText
122
- }
123
-
124
- const warnings: string[] = []
125
- if (screenFromTree && activityHint && screenFromTree !== activityHint) {
126
- warnings.push('ui_tree.screen and activity hints differ')
127
- }
128
- if (!hasUiTree) warnings.push('ui tree unavailable')
129
- if (!activityHint) warnings.push('activity unavailable')
130
- if (hasErrorLogs) warnings.push('error signals present in logs')
131
-
132
- const evidenceScore =
133
- (hasUiTree ? 0.35 : 0) +
134
- (screen ? 0.2 : 0) +
135
- (activityHint ? 0.15 : 0) +
136
- (actionables.length > 0 ? 0.15 : 0) +
137
- (texts.length > 0 ? 0.1 : 0) +
138
- (hasScreenshot ? 0.05 : 0) +
139
- (hasErrorLogs ? -0.15 : 0) +
140
- (hasLoadingSignals ? -0.05 : 0)
141
-
142
- const confidence = Math.max(0, Math.min(1, Number(evidenceScore.toFixed(2))))
143
-
144
- if (!screen && confidence < 0.3) return null
145
-
146
- return {
147
- screen,
148
- signals,
149
- actions_available: actionables.length > 0 ? actionables.slice(0, 10) : null,
150
- confidence,
151
- warnings
152
- }
153
- }
13
+ import { collectDebugSnapshot } from './snapshot-collection.js'
14
+ interface TreeInput { platform?: 'android' | 'ios'; deviceId?: string; responseMode?: 'compact' | 'debug'; sinceSnapshotRevision?: number }
15
+ interface CompactTree extends Partial<GetUITreeResponse> { delta_available: boolean; state_delta?: StateDelta; reason?: string }
154
16
 
155
17
  export class ToolsObserve {
156
18
  static getLatestStateDelta(platform: 'android' | 'ios', deviceId: string) {
@@ -177,10 +39,12 @@ export class ToolsObserve {
177
39
  }
178
40
  }
179
41
 
42
+ static getUITreeHandler(input: TreeInput & { responseMode?: 'debug' }): Promise<GetUITreeResponse>
43
+ static getUITreeHandler(input: TreeInput): Promise<GetUITreeResponse | CompactTree>
180
44
  static async getUITreeHandler({ platform, deviceId, responseMode = 'debug', sinceSnapshotRevision }: { platform?: 'android' | 'ios', deviceId?: string, responseMode?: 'compact' | 'debug', sinceSnapshotRevision?: number }) {
181
45
  const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId)
182
46
  const acquisitionStarted = Date.now()
183
- const tree = await observe.getUITree(resolved.id) as any
47
+ const tree = await observe.getUITree(resolved.id)
184
48
  tree.readiness = new ReadinessTracker(resolved.platform, resolved.id).observe(tree, acquisitionStarted)
185
49
  if (responseMode !== 'compact') return tree
186
50
  const revision = tree?.snapshot_revision ?? null
@@ -210,25 +74,16 @@ export class ToolsObserve {
210
74
  const filters = { appId, deviceId: resolved.id, pid, tag, level, contains, since_seconds, limit: limit ?? lines }
211
75
 
212
76
  // Validate filters
213
- if (level && !['VERBOSE','DEBUG','INFO','WARN','ERROR'].includes(level.toString().toUpperCase())) {
214
- return { device: resolved, logs: [], crashLines: [], logCount: 0, error: { code: 'INVALID_FILTER', message: `Unsupported level filter: ${level}` } } as any
77
+ if (level && !['VERBOSE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].includes(level.toString().toUpperCase())) {
78
+ return { device: resolved, logs: [], crashLines: [], logCount: 0, error: { code: 'INVALID_FILTER', message: `Unsupported level filter: ${level}` } }
215
79
  }
216
80
 
217
- if (observe instanceof AndroidObserve) {
218
- const response = await observe.getLogs(filters)
219
- const logs = Array.isArray(response.logs) ? response.logs : []
220
- const crashLines = logs.filter(entry => /FATAL EXCEPTION/i.test(entry.message))
221
- const anyFilterApplied = !!(appId || pid || tag || level || contains || since_seconds)
222
- if (anyFilterApplied && logs.length === 0) return { device: response.device, logs: [], crashLines: [], logCount: 0, source: response.source, meta: response.meta, error: { code: 'LOGS_UNAVAILABLE', message: 'No logs match filters' } } as any
223
- return { device: response.device, logs, crashLines, logCount: response.logCount, source: response.source, meta: response.meta }
224
- } else {
225
- const resp = await (observe as iOSObserve).getLogs(filters)
226
- const logs = Array.isArray(resp.logs) ? resp.logs : []
227
- const crashLines = logs.filter(entry => /FATAL EXCEPTION/i.test(entry.message))
228
- const anyFilterApplied = !!(appId || pid || tag || level || contains || since_seconds)
229
- if (anyFilterApplied && logs.length === 0) return { device: resp.device, logs: [], crashLines: [], logCount: 0, source: resp.source, meta: resp.meta, error: { code: 'LOGS_UNAVAILABLE', message: 'No logs match filters' } } as any
230
- return { device: resp.device, logs, crashLines, logCount: resp.logCount, source: resp.source, meta: resp.meta }
231
- }
81
+ const response = await observe.getLogs(filters)
82
+ const logs = Array.isArray(response.logs) ? response.logs : []
83
+ const crashLines = logs.filter(entry => /FATAL EXCEPTION/i.test(entry.message))
84
+ const anyFilterApplied = !!(appId || pid || tag || level || contains || since_seconds)
85
+ if (anyFilterApplied && logs.length === 0) return { device: response.device, logs: [], crashLines: [], logCount: 0, source: response.source, meta: response.meta, error: { code: 'LOGS_UNAVAILABLE', message: 'No logs match filters' } }
86
+ return { device: response.device, logs, crashLines, logCount: response.logCount, source: response.source, meta: response.meta }
232
87
  }
233
88
 
234
89
  static async startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }: { platform?: 'android' | 'ios', packageName: string, level?: 'error' | 'warn' | 'info' | 'debug', sessionId?: string, deviceId?: string }) {
@@ -273,135 +128,18 @@ export class ToolsObserve {
273
128
  static async getScreenFingerprintHandler({ platform, deviceId }: { platform?: 'android' | 'ios', deviceId?: string } = {}) {
274
129
  const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId)
275
130
  // Both observes implement getScreenFingerprint
276
- return await (observe as any).getScreenFingerprint(resolved.id)
131
+ return await (observe).getScreenFingerprint(resolved.id)
277
132
  }
278
133
 
279
134
  static async captureDebugSnapshotHandler({ reason, includeLogs = true, includeScreenshot = true, logLines = 200, platform, appId, deviceId, sessionId }: { reason?: string; includeLogs?: boolean; includeScreenshot?: boolean; logLines?: number; platform?: 'android' | 'ios'; appId?: string; deviceId?: string; sessionId?: string } = {}) {
280
135
  const { resolved } = await ToolsObserve.resolveObserve(platform, deviceId, appId)
281
- return withResolvedDevice(resolved, () => ToolsObserve.collectDebugSnapshot({ reason, includeLogs, includeScreenshot, logLines,
282
- platform: resolved.platform as 'android' | 'ios', appId, deviceId: resolved.id, sessionId }))
136
+ return withResolvedDevice(resolved, () => ToolsObserve.collectDebugSnapshot({
137
+ reason, includeLogs, includeScreenshot, logLines,
138
+ platform: resolved.platform as 'android' | 'ios', appId, deviceId: resolved.id, sessionId
139
+ }))
283
140
  }
284
141
 
285
- private static async collectDebugSnapshot({ reason, includeLogs = true, includeScreenshot = true, logLines = 200, platform, appId, deviceId, sessionId }: { reason?: string; includeLogs?: boolean; includeScreenshot?: boolean; logLines?: number; platform?: 'android' | 'ios'; appId?: string; deviceId?: string; sessionId?: string } = {}) {
286
- if (!includeScreenshot) omitEvidence('screenshot')
287
- if (!includeLogs) omitEvidence('logs')
288
- const timestamp = Date.now()
289
- const raw: CaptureDebugSnapshotRawResponse = {
290
- timestamp,
291
- snapshot_revision: 0,
292
- captured_at_ms: timestamp,
293
- reason: reason || '',
294
- activity: null,
295
- fingerprint: null,
296
- screenshot: null,
297
- ui_tree: null,
298
- logs: []
299
- }
300
-
301
- // Parallel fetches for performance: screenshot, current screen, fingerprint, ui tree, and log stream/get logs
302
- const sid = sessionId || 'default'
303
- const tasks: Record<string, Promise<any>> = {
304
- screenshot: includeScreenshot ? measure('screenshot_ms', () => ToolsObserve.captureScreenshotHandler({ platform, deviceId })) : Promise.resolve(null),
305
- currentScreen: (!platform || platform === 'android') ? ToolsObserve.getCurrentScreenHandler({ deviceId }) : Promise.resolve(null),
306
- fingerprint: ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }),
307
- uiTree: ToolsObserve.getUITreeHandler({ platform, deviceId }),
308
- readLogStream: includeLogs ? ToolsObserve.readLogStreamHandler({ platform, deviceId, sessionId: sid, limit: logLines }) : Promise.resolve({ entries: [] }),
309
- }
310
-
311
- const results = await Promise.allSettled(Object.values(tasks))
312
- const keys = Object.keys(tasks)
313
-
314
- // Map results back to keys
315
- for (let i = 0; i < results.length; i++) {
316
- const key = keys[i]
317
- const res = results[i] as PromiseSettledResult<any>
318
- if (res.status === 'fulfilled') {
319
- const val = res.value
320
- if (key === 'screenshot') {
321
- raw.screenshot = val && val.screenshot ? val.screenshot : null
322
- } else if (key === 'currentScreen') {
323
- raw.activity = val && ((val.activity || val.shortActivity)) ? (val.activity || val.shortActivity) : raw.activity || ''
324
- } else if (key === 'fingerprint') {
325
- if (val && val.fingerprint) raw.fingerprint = val.fingerprint
326
- if (val && val.activity) raw.activity = raw.activity || val.activity
327
- if (val && val.error) raw.fingerprint_error = val.error
328
- } else if (key === 'uiTree') {
329
- raw.ui_tree = val
330
- if (val && val.error) raw.ui_tree_error = val.error
331
- } else if (key === 'readLogStream') {
332
- // handle below after evaluating fallback
333
- // temporarily attach to out._streamEntries
334
- raw.logs = Array.isArray(val?.entries) ? val.entries : []
335
- }
336
- } else {
337
- const errMsg = res.reason instanceof Error ? res.reason.message : String(res.reason)
338
- if (key === 'screenshot') raw.screenshot_error = errMsg
339
- if (key === 'currentScreen') raw.activity_error = errMsg
340
- if (key === 'fingerprint') { raw.fingerprint = null; raw.fingerprint_error = errMsg }
341
- if (key === 'uiTree') { raw.ui_tree = null; raw.ui_tree_error = errMsg }
342
- if (key === 'readLogStream') { raw.logs = []; raw.logs_error = errMsg }
343
- }
344
- }
345
-
346
- // Logs: prefer stream entries, fallback to snapshot logs when empty
347
- if (includeLogs) {
348
- try {
349
- let entries: any[] = Array.isArray(raw.logs) ? raw.logs : []
350
- if (!entries || entries.length === 0) {
351
- const gl = await ToolsObserve.getLogsHandler({ platform, appId, deviceId, lines: logLines })
352
- const snapshotLogs: any[] = (gl && (gl as any).logs) ? (gl as any).logs : []
353
- // raw may be structured entries or strings
354
- entries = snapshotLogs.slice(-Math.max(0, logLines)).map(item => {
355
- if (!item) return { timestamp: null, level: 'INFO', message: '' }
356
- if (typeof item === 'string') {
357
- const level = /\b(FATAL EXCEPTION|ERROR| E )\b/i.test(item) ? 'ERROR' : /\b(WARN| W )\b/i.test(item) ? 'WARN' : 'INFO'
358
- return { timestamp: null, level, message: item }
359
- }
360
- const msg = item.message || item.msg || JSON.stringify(item)
361
- const levelRaw = item.level || item.levelName || item._level || ''
362
- const level = (levelRaw && String(levelRaw)).toUpperCase() || (/\bERROR\b/i.test(msg) ? 'ERROR' : /\bWARN\b/i.test(msg) ? 'WARN' : 'INFO')
363
- const ts = item.timestamp || item._iso || null
364
- const tsNum = (ts && typeof ts === 'string') ? (isNaN(new Date(ts).getTime()) ? null : new Date(ts).getTime()) : (typeof ts === 'number' ? ts : null)
365
- return { timestamp: tsNum, level, message: msg }
366
- })
367
- } else {
368
- entries = entries.map(ent => {
369
- const msg = (ent && (ent.message || ent.msg)) ? (ent.message || ent.msg) : (typeof ent === 'string' ? ent : JSON.stringify(ent))
370
- const levelRaw = (ent && (ent.level || ent.levelName || ent._level)) ? (ent.level || ent.levelName || ent._level) : ''
371
- const level = (levelRaw && String(levelRaw)).toString().toUpperCase() || (/\bERROR\b/i.test(msg) ? 'ERROR' : /\bWARN\b/i.test(msg) ? 'WARN' : 'INFO')
372
- let tsNum: number | null = null
373
- const maybeIso = ent && ((ent._iso || ent.timestamp) as any)
374
- if (maybeIso && typeof maybeIso === 'string') {
375
- const d = new Date(maybeIso)
376
- if (!isNaN(d.getTime())) tsNum = d.getTime()
377
- }
378
- return { timestamp: tsNum, level, message: msg }
379
- })
380
- }
381
-
382
- raw.logs = entries
383
- } catch (e) {
384
- raw.logs = []
385
- raw.logs_error = e instanceof Error ? e.message : String(e)
386
- }
387
- }
388
-
389
- const snapshotDeviceKey = raw.ui_tree?.device
390
- ? `${raw.ui_tree.device.platform}:${raw.ui_tree.device.id}`
391
- : `${platform || 'unknown'}:${deviceId || 'default'}`
392
- const snapshotMetadata = deriveSnapshotMetadata(
393
- snapshotDeviceKey,
394
- raw.ui_tree,
395
- 'snapshot',
396
- raw.ui_tree?.snapshot_revision ? null : (raw.fingerprint || raw.activity || null)
397
- )
398
-
399
- raw.snapshot_revision = raw.ui_tree?.snapshot_revision ?? snapshotMetadata.snapshot_revision
400
- raw.captured_at_ms = raw.ui_tree?.captured_at_ms ?? snapshotMetadata.captured_at_ms
401
- raw.snapshot_delta = raw.ui_tree?.snapshot_delta ?? snapshotMetadata.snapshot_delta ?? null
402
- raw.loading_state = raw.ui_tree?.loading_state ?? snapshotMetadata.loading_state
403
-
404
- const semantic = deriveSnapshotSemantic(raw)
405
- return semantic ? { raw, semantic } : { raw }
142
+ private static collectDebugSnapshot(input: import('./snapshot-collection.js').SnapshotInput = {}) {
143
+ return collectDebugSnapshot(input, ToolsObserve)
406
144
  }
407
145
  }
@@ -0,0 +1,22 @@
1
+ import { execCommand } from "../../utils/ios/utils.js"
2
+ export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
3
+
4
+ export let iosExecCommand = execCommand
5
+
6
+ export function _setIOSExecCommandForTests(fn: typeof execCommand) {
7
+ iosExecCommand = fn
8
+ }
9
+
10
+ export function _resetIOSExecCommandForTests() {
11
+ iosExecCommand = execCommand
12
+ }
13
+
14
+ export const iosActiveLogStreams: Map<string, { proc: Pick<import('node:child_process').ChildProcess, 'kill'>, file: string, device?: { platform: 'android' | 'ios'; id: string } }> = new Map()
15
+
16
+ export function _setIOSActiveLogStream(sessionId: string, file: string, device?: { platform: 'android' | 'ios'; id: string }) {
17
+ iosActiveLogStreams.set(sessionId, { proc: { kill: () => false }, file, device })
18
+ }
19
+
20
+ export function _clearIOSActiveLogStream(sessionId: string) {
21
+ iosActiveLogStreams.delete(sessionId)
22
+ }
@@ -0,0 +1,12 @@
1
+ import { computeScreenFingerprint } from '../../utils/ui/index.js'
2
+ import type { iOSObserve } from '../ios.js'
3
+ export async function collectGetScreenFingerprint(source: Pick<iOSObserve, 'getUITree'>, deviceId: string = 'booted'): Promise<{ fingerprint: string | null; activity?: string; error?: string; captured_at_ms?: number }> {
4
+ try {
5
+ const tree = await source.getUITree(deviceId)
6
+ if (!tree || tree.error) return { fingerprint: null, error: tree && tree.error }
7
+
8
+ return { ...computeScreenFingerprint(tree, null, 'ios', 50), captured_at_ms: tree.captured_at_ms }
9
+ } catch (e) {
10
+ return { fingerprint: null, error: e instanceof Error ? e.message : String(e) }
11
+ }
12
+ }