mobile-debug-mcp 0.33.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 (448) 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 +23 -2
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +158 -124
  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 +20 -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 +113 -4
  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 +79 -0
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/guard.js +30 -0
  30. package/dist/debugger/protocol.js +33 -0
  31. package/dist/debugger/sessions.js +304 -0
  32. package/dist/debugger/worker.js +176 -0
  33. package/dist/interact/adjustment/adjustment-context.js +41 -0
  34. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  35. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  36. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  37. package/dist/interact/adjustment/adjustment-result.js +19 -0
  38. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  39. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  40. package/dist/interact/adjustment/adjustment.js +52 -0
  41. package/dist/interact/adjustment/control-geometry.js +87 -0
  42. package/dist/interact/adjustment/control-values.js +136 -0
  43. package/dist/interact/adjustment/slider-labels.js +84 -0
  44. package/dist/interact/assertions/screen-assertion.js +37 -0
  45. package/dist/interact/assertions/state-assertion-context.js +35 -0
  46. package/dist/interact/assertions/state-assertion.js +76 -0
  47. package/dist/interact/assertions/state-comparison.js +141 -0
  48. package/dist/interact/assertions/visibility-assertion.js +34 -0
  49. package/dist/interact/conditional-action.js +12 -7
  50. package/dist/interact/index.js +65 -3074
  51. package/dist/interact/input-service.js +1 -0
  52. package/dist/interact/interaction-trace.js +21 -0
  53. package/dist/interact/model.js +1 -0
  54. package/dist/interact/observation.js +1 -0
  55. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  56. package/dist/interact/targeting/candidate-result.js +23 -0
  57. package/dist/interact/targeting/candidate-scoring.js +50 -0
  58. package/dist/interact/targeting/element-helpers.js +216 -0
  59. package/dist/interact/targeting/find-element.js +65 -0
  60. package/dist/interact/targeting/nearby-control.js +56 -0
  61. package/dist/interact/targeting/resolved-elements.js +21 -0
  62. package/dist/interact/targeting/tap-target.js +17 -0
  63. package/dist/interact/targeting/target-scope.js +104 -0
  64. package/dist/interact/waits/legacy-log-wait.js +38 -0
  65. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  66. package/dist/interact/waits/legacy-wait-state.js +34 -0
  67. package/dist/interact/waits/legacy-wait.js +48 -0
  68. package/dist/interact/waits/screen-wait.js +49 -0
  69. package/dist/interact/waits/ui-change-helpers.js +157 -0
  70. package/dist/interact/waits/ui-change-state.js +18 -0
  71. package/dist/interact/waits/ui-change-wait.js +77 -0
  72. package/dist/interact/waits/ui-wait-condition.js +56 -0
  73. package/dist/interact/waits/ui-wait-state.js +38 -0
  74. package/dist/interact/waits/ui-wait.js +106 -0
  75. package/dist/manage/android.js +55 -51
  76. package/dist/manage/index.js +38 -75
  77. package/dist/manage/ios/build-types.js +1 -0
  78. package/dist/manage/ios/build.js +210 -0
  79. package/dist/manage/ios/project-discovery.js +29 -0
  80. package/dist/manage/ios/scheme.js +18 -0
  81. package/dist/manage/ios.js +42 -256
  82. package/dist/observe/android/context.js +1 -0
  83. package/dist/observe/android/current-screen.js +55 -0
  84. package/dist/observe/android/fingerprint.js +13 -0
  85. package/dist/observe/android/log-stream.js +132 -0
  86. package/dist/observe/android/logs.js +107 -0
  87. package/dist/observe/android/screenshot.js +70 -0
  88. package/dist/observe/android/ui-tree.js +107 -0
  89. package/dist/observe/android.js +16 -520
  90. package/dist/observe/evidence.js +122 -76
  91. package/dist/observe/index.js +16 -263
  92. package/dist/observe/ios/context.js +16 -0
  93. package/dist/observe/ios/fingerprint.js +12 -0
  94. package/dist/observe/ios/log-stream.js +105 -0
  95. package/dist/observe/ios/logs.js +168 -0
  96. package/dist/observe/ios/screenshot.js +22 -0
  97. package/dist/observe/ios/ui-elements.js +241 -0
  98. package/dist/observe/ios/ui-tree.js +137 -0
  99. package/dist/observe/ios.js +17 -715
  100. package/dist/observe/logs/model.js +1 -0
  101. package/dist/{network → observe/network}/index.js +2 -2
  102. package/dist/observe/readiness-service.js +13 -2
  103. package/dist/observe/readiness.js +44 -18
  104. package/dist/observe/screenshot-encoding.js +41 -0
  105. package/dist/observe/snapshot-collection.js +79 -0
  106. package/dist/observe/snapshot-logs.js +30 -0
  107. package/dist/observe/snapshot-metadata.js +43 -31
  108. package/dist/observe/snapshot-semantic.js +115 -0
  109. package/dist/observe/system-trace-android.js +135 -0
  110. package/dist/observe/system-trace.js +294 -0
  111. package/dist/runtime/action-results.js +270 -0
  112. package/dist/runtime/clock.js +4 -0
  113. package/dist/server/common.js +1 -256
  114. package/dist/server/definitions/assert.js +162 -0
  115. package/dist/server/definitions/diagnostic.js +118 -0
  116. package/dist/server/definitions/input.js +274 -0
  117. package/dist/server/definitions/manage.js +194 -0
  118. package/dist/server/definitions/observe.js +149 -0
  119. package/dist/server/definitions/target.js +138 -0
  120. package/dist/server/definitions/wait.js +105 -0
  121. package/dist/server/handlers/action-execution.js +163 -0
  122. package/dist/server/handlers/correlation.js +23 -0
  123. package/dist/server/handlers/debugger-handlers.js +48 -0
  124. package/dist/server/handlers/debugger-response.js +9 -0
  125. package/dist/server/handlers/diagnostic.js +69 -0
  126. package/dist/server/handlers/handler-types.js +1 -0
  127. package/dist/server/handlers/input.js +154 -0
  128. package/dist/server/handlers/interact.js +113 -0
  129. package/dist/server/handlers/journey-assertions.js +35 -0
  130. package/dist/server/handlers/journey.js +147 -0
  131. package/dist/server/handlers/manage.js +119 -0
  132. package/dist/server/handlers/observe.js +95 -0
  133. package/dist/server/handlers/payload.js +4 -0
  134. package/dist/server/handlers/tool-handlers.js +160 -0
  135. package/dist/server/schemas/condition-schema.js +2 -0
  136. package/dist/server/schemas/debugger-schema.js +47 -0
  137. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
  138. package/dist/server/schemas/system-trace-schema.js +17 -0
  139. package/dist/server/tool-definitions.js +62 -1134
  140. package/dist/server-core.js +4 -4
  141. package/dist/server.js +34 -0
  142. package/dist/system/android-status.js +101 -0
  143. package/dist/system/android.js +37 -27
  144. package/dist/system/index.js +13 -438
  145. package/dist/system/ios-status.js +132 -0
  146. package/dist/system/ios.js +11 -8
  147. package/dist/system/model.js +1 -0
  148. package/dist/system/probes.js +105 -0
  149. package/dist/system/status-summary.js +43 -0
  150. package/dist/system/unsupported-status.js +52 -0
  151. package/dist/utils/android/commands.js +89 -0
  152. package/dist/utils/android/gradle.js +139 -0
  153. package/dist/utils/android/logs.js +103 -0
  154. package/dist/utils/android/ui-tree.js +233 -0
  155. package/dist/utils/android/utils.js +6 -564
  156. package/dist/utils/cli/idb/idb-helper.js +4 -6
  157. package/dist/utils/cli/idb/install-idb.js +13 -9
  158. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  159. package/dist/utils/exec.js +2 -0
  160. package/dist/utils/ios/utils.js +45 -46
  161. package/dist/utils/java.js +75 -57
  162. package/dist/utils/operation-budget.js +2 -0
  163. package/dist/utils/resolve-device.js +6 -2
  164. package/dist/utils/ui/index.js +26 -26
  165. package/docs/CHANGELOG.md +48 -0
  166. package/docs/README.md +38 -0
  167. package/docs/benchmarks/README.md +18 -0
  168. package/docs/benchmarks/interaction-latency/README.md +14 -0
  169. package/docs/benchmarks/interaction-latency/final.md +4 -1
  170. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  171. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  172. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  173. package/docs/code-structure.md +118 -0
  174. package/docs/specs/001-state-verification/spec.md +16 -14
  175. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  176. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  177. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  178. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  179. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  180. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  181. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  182. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  183. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  184. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  185. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  186. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  187. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  188. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  189. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  190. package/docs/specs/016-environment-tools/spec.md +20 -20
  191. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  192. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  193. package/docs/specs/020-conditional-actions/intent.md +5 -3
  194. package/docs/specs/020-conditional-actions/plan.md +8 -9
  195. package/docs/specs/020-conditional-actions/spec.md +4 -2
  196. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  197. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  198. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  199. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  200. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  201. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  202. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  203. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  204. package/docs/specs/023-android-system-traces/intent.md +41 -0
  205. package/docs/specs/023-android-system-traces/plan.md +115 -0
  206. package/docs/specs/023-android-system-traces/spec.md +122 -0
  207. package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
  208. package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
  209. package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
  210. package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
  211. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  212. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  213. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  214. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  215. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  216. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  217. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  218. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  219. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  220. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  221. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  222. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  223. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  224. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  225. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  226. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  227. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  228. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  229. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  230. package/docs/tools/TOOLS.md +31 -16
  231. package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
  232. package/docs/tools/debugger.md +180 -0
  233. package/docs/tools/interact.md +42 -20
  234. package/docs/tools/manage.md +25 -44
  235. package/docs/tools/observe.md +64 -25
  236. package/docs/tools/system-traces.md +134 -0
  237. package/docs/tools/system.md +12 -5
  238. package/package.json +11 -8
  239. package/scripts/architecture-graph.mjs +69 -0
  240. package/scripts/check-architecture.mjs +47 -0
  241. package/scripts/code-quality-baseline.json +1 -0
  242. package/scripts/code-quality.mjs +161 -0
  243. package/src/contracts/execution-controls.ts +26 -0
  244. package/src/contracts/selector.ts +17 -0
  245. package/src/contracts/target.ts +19 -0
  246. package/src/contracts/ui.ts +94 -0
  247. package/src/debugger/android.ts +66 -0
  248. package/src/debugger/diagnostics.ts +43 -0
  249. package/src/debugger/guard.ts +31 -0
  250. package/src/debugger/java/DebuggerWorker.java +166 -0
  251. package/src/debugger/protocol.ts +37 -0
  252. package/src/debugger/sessions.ts +327 -0
  253. package/src/debugger/worker.ts +135 -0
  254. package/src/interact/adjustment/adjustment-context.ts +83 -0
  255. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  256. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  257. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  258. package/src/interact/adjustment/adjustment-result.ts +34 -0
  259. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  260. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  261. package/src/interact/adjustment/adjustment.ts +49 -0
  262. package/src/interact/adjustment/control-geometry.ts +120 -0
  263. package/src/interact/adjustment/control-values.ts +141 -0
  264. package/src/interact/adjustment/slider-labels.ts +84 -0
  265. package/src/interact/assertions/screen-assertion.ts +39 -0
  266. package/src/interact/assertions/state-assertion-context.ts +48 -0
  267. package/src/interact/assertions/state-assertion.ts +64 -0
  268. package/src/interact/assertions/state-comparison.ts +168 -0
  269. package/src/interact/assertions/visibility-assertion.ts +41 -0
  270. package/src/interact/conditional-action.ts +18 -12
  271. package/src/interact/index.ts +76 -3491
  272. package/src/interact/input-service.ts +11 -0
  273. package/src/interact/interaction-trace.ts +36 -0
  274. package/src/interact/model.ts +109 -0
  275. package/src/interact/observation.ts +7 -0
  276. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  277. package/src/interact/targeting/candidate-result.ts +24 -0
  278. package/src/interact/targeting/candidate-scoring.ts +42 -0
  279. package/src/interact/targeting/element-helpers.ts +243 -0
  280. package/src/interact/targeting/find-element.ts +47 -0
  281. package/src/interact/targeting/nearby-control.ts +46 -0
  282. package/src/interact/targeting/resolved-elements.ts +20 -0
  283. package/src/interact/targeting/tap-target.ts +22 -0
  284. package/src/interact/targeting/target-scope.ts +121 -0
  285. package/src/interact/waits/legacy-log-wait.ts +30 -0
  286. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  287. package/src/interact/waits/legacy-wait-state.ts +48 -0
  288. package/src/interact/waits/legacy-wait.ts +34 -0
  289. package/src/interact/waits/screen-wait.ts +37 -0
  290. package/src/interact/waits/ui-change-helpers.ts +184 -0
  291. package/src/interact/waits/ui-change-state.ts +26 -0
  292. package/src/interact/waits/ui-change-wait.ts +59 -0
  293. package/src/interact/waits/ui-wait-condition.ts +52 -0
  294. package/src/interact/waits/ui-wait-state.ts +55 -0
  295. package/src/interact/waits/ui-wait.ts +84 -0
  296. package/src/manage/android.ts +56 -50
  297. package/src/manage/index.ts +63 -93
  298. package/src/manage/ios/build-types.ts +14 -0
  299. package/src/manage/ios/build.ts +230 -0
  300. package/src/manage/ios/project-discovery.ts +26 -0
  301. package/src/manage/ios/scheme.ts +16 -0
  302. package/src/manage/ios.ts +65 -298
  303. package/src/observe/android/context.ts +1 -0
  304. package/src/observe/android/current-screen.ts +63 -0
  305. package/src/observe/android/fingerprint.ts +13 -0
  306. package/src/observe/android/log-stream.ts +127 -0
  307. package/src/observe/android/logs.ts +110 -0
  308. package/src/observe/android/screenshot.ts +79 -0
  309. package/src/observe/android/ui-tree.ts +113 -0
  310. package/src/observe/android.ts +17 -531
  311. package/src/observe/evidence.ts +130 -71
  312. package/src/observe/index.ts +24 -286
  313. package/src/observe/ios/context.ts +22 -0
  314. package/src/observe/ios/fingerprint.ts +12 -0
  315. package/src/observe/ios/log-stream.ts +103 -0
  316. package/src/observe/ios/logs.ts +179 -0
  317. package/src/observe/ios/screenshot.ts +26 -0
  318. package/src/observe/ios/ui-elements.ts +266 -0
  319. package/src/observe/ios/ui-tree.ts +143 -0
  320. package/src/observe/ios.ts +18 -749
  321. package/src/observe/logs/model.ts +11 -0
  322. package/src/{network → observe/network}/index.ts +2 -2
  323. package/src/observe/readiness-service.ts +10 -3
  324. package/src/observe/readiness.ts +49 -22
  325. package/src/observe/screenshot-encoding.ts +32 -0
  326. package/src/observe/snapshot-collection.ts +69 -0
  327. package/src/observe/snapshot-logs.ts +26 -0
  328. package/src/observe/snapshot-metadata.ts +51 -31
  329. package/src/observe/snapshot-semantic.ts +153 -0
  330. package/src/observe/system-trace-android.ts +133 -0
  331. package/src/observe/system-trace.ts +227 -0
  332. package/src/runtime/action-results.ts +376 -0
  333. package/src/runtime/clock.ts +9 -0
  334. package/src/server/common.ts +9 -359
  335. package/src/server/definitions/assert.ts +164 -0
  336. package/src/server/definitions/diagnostic.ts +120 -0
  337. package/src/server/definitions/input.ts +276 -0
  338. package/src/server/definitions/manage.ts +196 -0
  339. package/src/server/definitions/observe.ts +151 -0
  340. package/src/server/definitions/target.ts +140 -0
  341. package/src/server/definitions/wait.ts +107 -0
  342. package/src/server/handlers/action-execution.ts +164 -0
  343. package/src/server/handlers/correlation.ts +20 -0
  344. package/src/server/handlers/debugger-handlers.ts +39 -0
  345. package/src/server/handlers/debugger-response.ts +7 -0
  346. package/src/server/handlers/diagnostic.ts +79 -0
  347. package/src/server/handlers/handler-types.ts +7 -0
  348. package/src/server/handlers/input.ts +173 -0
  349. package/src/server/handlers/interact.ts +135 -0
  350. package/src/server/handlers/journey-assertions.ts +39 -0
  351. package/src/server/handlers/journey.ts +132 -0
  352. package/src/server/handlers/manage.ts +141 -0
  353. package/src/server/handlers/observe.ts +116 -0
  354. package/src/server/handlers/payload.ts +36 -0
  355. package/src/server/handlers/tool-handlers.ts +159 -0
  356. package/src/server/schemas/condition-schema.ts +2 -0
  357. package/src/server/schemas/debugger-schema.ts +43 -0
  358. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
  359. package/src/server/schemas/system-trace-schema.ts +17 -0
  360. package/src/server/tool-definitions.ts +69 -1140
  361. package/src/server-core.ts +4 -4
  362. package/src/server.ts +25 -1
  363. package/src/system/android-status.ts +124 -0
  364. package/src/system/android.ts +33 -21
  365. package/src/system/index.ts +21 -523
  366. package/src/system/ios-status.ts +147 -0
  367. package/src/system/ios.ts +14 -10
  368. package/src/system/model.ts +41 -0
  369. package/src/system/probes.ts +114 -0
  370. package/src/system/status-summary.ts +47 -0
  371. package/src/system/unsupported-status.ts +55 -0
  372. package/src/types.ts +16 -106
  373. package/src/utils/android/commands.ts +81 -0
  374. package/src/utils/android/gradle.ts +132 -0
  375. package/src/utils/android/logs.ts +103 -0
  376. package/src/utils/android/ui-tree.ts +239 -0
  377. package/src/utils/android/utils.ts +16 -556
  378. package/src/utils/cli/idb/check-idb.ts +4 -4
  379. package/src/utils/cli/idb/idb-helper.ts +6 -8
  380. package/src/utils/cli/idb/install-idb.ts +15 -9
  381. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  382. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  383. package/src/utils/exec.ts +2 -0
  384. package/src/utils/ios/utils.ts +57 -61
  385. package/src/utils/java.ts +71 -51
  386. package/src/utils/operation-budget.ts +2 -0
  387. package/src/utils/resolve-device.ts +11 -6
  388. package/src/utils/ui/index.ts +28 -28
  389. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  390. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  391. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  392. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  393. package/test/device/automated/debugger/runtime.android.test.ts +219 -0
  394. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  395. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  396. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  397. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  398. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  399. package/test/fixtures/coroutine-app/README.md +48 -0
  400. package/test/fixtures/coroutine-app/build.ts +35 -0
  401. package/test/fixtures/debugger-app/MainActivity.java +67 -0
  402. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  403. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  404. package/test/fixtures/debugger-app/README.md +65 -0
  405. package/test/fixtures/debugger-app/build.ts +39 -0
  406. package/test/fixtures/latency-journey-app/README.md +2 -0
  407. package/test/fixtures/readiness-app/README.md +2 -0
  408. package/test/helpers/perfetto.ts +59 -0
  409. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  410. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  411. package/test/unit/debugger/diagnostics.test.ts +80 -0
  412. package/test/unit/debugger/guards.test.ts +50 -0
  413. package/test/unit/debugger/native.test.ts +33 -0
  414. package/test/unit/debugger/preparation.test.ts +32 -0
  415. package/test/unit/debugger/protocol.test.ts +15 -0
  416. package/test/unit/debugger/sessions.test.ts +83 -0
  417. package/test/unit/interact/adjustment_service.test.ts +42 -0
  418. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  419. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  420. package/test/unit/interact/element_helpers.test.ts +19 -0
  421. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  422. package/test/unit/interact/state_comparison.test.ts +20 -0
  423. package/test/unit/interact/target_scope.test.ts +35 -0
  424. package/test/unit/network/get_network_activity.test.ts +1 -1
  425. package/test/unit/observe/readiness.test.ts +3 -1
  426. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  427. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  428. package/test/unit/observe/system-trace-android.test.ts +52 -0
  429. package/test/unit/observe/system-trace.test.ts +166 -0
  430. package/test/unit/quality/architecture.test.ts +60 -0
  431. package/test/unit/quality/code-quality.test.ts +58 -0
  432. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  433. package/test/unit/server/conditional_actions.test.ts +1 -1
  434. package/test/unit/server/conditional_schema.test.ts +2 -2
  435. package/test/unit/server/debugger-response.test.ts +63 -0
  436. package/test/unit/server/system-trace.test.ts +64 -0
  437. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  438. package/dist/server/tool-handlers.js +0 -893
  439. package/docs/ROADMAP.md +0 -646
  440. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  441. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  442. package/docs/specs/baseline-spec-v0.md +0 -312
  443. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  444. package/src/server/tool-handlers.ts +0 -923
  445. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  446. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  447. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  448. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -25,6 +25,7 @@ interface Acquisition {
25
25
  startSequence: number; endSequence: number; overlapping: boolean; reused: boolean
26
26
  }
27
27
  interface EvidenceContext {
28
+ systemTraceId?: string
28
29
  correlationId: string; action: ActionRecord | null; ref?: ActionRef
29
30
  device: EvidenceDevice | null; start: Stamp; end: Stamp | null; verification: Stamp | null
30
31
  observations: Acquisition[]; isAction: boolean; children: EvidenceContext[]
@@ -37,7 +38,13 @@ const orders = new Map<string, DeviceOrder>()
37
38
  const stamp = (): Stamp => ({ wall: Date.now(), mono: performance.now() })
38
39
  const deviceKey = (d: EvidenceDevice) => `${d.platform}:${d.id}`
39
40
  const sameDevice = (a: EvidenceDevice | null, b: EvidenceDevice | null) => !!a && !!b && deviceKey(a) === deviceKey(b)
40
- 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
41
48
  return d && ['android', 'ios'].includes(d.platform) && typeof d.id === 'string' && d.id && !['default', 'booted'].includes(d.id)
42
49
  ? { platform: d.platform, id: d.id } : null
43
50
  }
@@ -67,11 +74,17 @@ function prune() {
67
74
  export function validateActionRef(value: unknown): ActionRef | undefined {
68
75
  if (value === undefined) return undefined
69
76
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
70
- 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')
71
78
  const ref = value as ActionRef
72
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')
73
80
  return { sessionId: ref.sessionId, actionId: ref.actionId }
74
81
  }
82
+ export function evidenceSessionId() { return sessionId }
83
+ export function currentEvidenceCorrelationId() { return context.getStore()?.correlationId }
84
+ export function bindSystemTrace(traceId: string) {
85
+ const current = context.getStore()
86
+ if (current) current.systemTraceId = traceId
87
+ }
75
88
  export function currentEvidenceActionId() { return context.getStore()?.action?.id }
76
89
  export function bindEvidenceDevice(device: { platform: string; id: string }) {
77
90
  const current = context.getStore()
@@ -107,11 +120,13 @@ export function omitEvidence(kind: Kind) {
107
120
  const current = context.getStore()
108
121
  if (!current) return
109
122
  const start = stamp()
110
- current.observations.push({ value: {
111
- evidence_id: randomUUID(), kind, status: 'omitted', capture_started_at_ms: null,
112
- capture_completed_at_ms: null, source_captured_at_ms: null, age_ms: null,
113
- freshness: kind === 'logs' ? 'not_applicable' : 'unknown', freshness_reasons: [], device: current.device
114
- }, 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
+ })
115
130
  }
116
131
  export async function captureEvidence<T>(kind: Kind, device: EvidenceDevice | null, operation: () => Promise<T>): Promise<T> {
117
132
  const current = context.getStore()
@@ -120,42 +135,19 @@ export async function captureEvidence<T>(kind: Kind, device: EvidenceDevice | nu
120
135
  if (target) bindEvidenceDevice(target)
121
136
  const order = orderFor(target)
122
137
  const start = stamp()
123
- const acquisition: Acquisition = { value: {
124
- evidence_id: randomUUID(), kind, status: 'failed', capture_started_at_ms: start.wall,
125
- capture_completed_at_ms: null, source_captured_at_ms: null, age_ms: null,
126
- freshness: 'unknown', freshness_reasons: [], device: target
127
- }, start, end: null, order, startSequence: order?.sequence ?? 0,
128
- 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
+ }
129
146
  current.observations.push(acquisition)
130
147
  try {
131
- const result = await operation() as any
132
- const value = acquisition.value
133
- const resolved = validDevice(result?.device) ?? target
134
- value.device = resolved
135
- if (resolved) bindEvidenceDevice(resolved)
136
- const failure = result?.error ?? result?.meta?.error
137
- value.status = failure || result?.success === false || (kind === 'fingerprint' && !result?.fingerprint) ? 'failed' : 'captured'
138
- if (failure) value.error = failure
139
- const source = result?.captured_at_ms
140
- if (Number.isFinite(source) && source > 0) {
141
- value.source_captured_at_ms = source
142
- acquisition.reused = source < start.wall
143
- }
144
- if (kind === 'ui_tree') {
145
- value.snapshot_session_id = result?.snapshot_session_id
146
- value.snapshot_revision = result?.snapshot_revision
147
- }
148
- if (kind === 'logs') {
149
- const entries = result?.logs ?? result?.entries ?? []
150
- const times = entries.map((e: any) => {
151
- const t = e?.timestamp ?? e?._iso
152
- return typeof t === 'number' ? t : typeof t === 'string' ? Date.parse(t) : NaN
153
- }).filter((t: number) => Number.isFinite(t)) as number[]
154
- value.clock = 'device_or_unknown'; value.event_time_comparable = false
155
- value.earliest_entry_at_ms = times.length ? times.reduce((a, b) => Math.min(a, b)) : null
156
- value.latest_entry_at_ms = times.length ? times.reduce((a, b) => Math.max(a, b)) : null
157
- }
158
- return result
148
+ const original = await operation()
149
+ recordObservationResult(acquisition, original as EvidencePayload | null, kind, target, start)
150
+ return original
159
151
  } catch (error) {
160
152
  acquisition.value.error = error instanceof Error ? error.message : String(error)
161
153
  throw error
@@ -181,46 +173,28 @@ function assemble(current: EvidenceContext) {
181
173
  const link = association(current)
182
174
  const action = link.association === 'unavailable' ? null : current.action
183
175
  const clockChanged = end.wall < current.start.wall
184
- const observations = current.observations.map(a => {
185
- const v = { ...a.value, freshness_reasons: [] as string[] }
186
- if (v.status === 'omitted') return v
187
- const componentAction = sameDevice(v.device, action?.device ?? null) ? action : null
188
- const sourceMono = a.end && v.source_captured_at_ms !== null ? a.end.mono + v.source_captured_at_ms - a.end.wall : a.end?.mono
189
- const reason = (freshness: Freshness, code: string) => { v.freshness = freshness; v.freshness_reasons.push(code) }
190
- if (v.kind === 'logs') reason('unknown', 'DEVICE_CLOCK_UNVERIFIED')
191
- else if (clockChanged || (a.end && a.end.wall < a.start.wall)) reason('unknown', 'CLOCK_DISCONTINUITY')
192
- else if (v.source_captured_at_ms !== null && a.end && v.source_captured_at_ms > a.end.wall) reason('unknown', 'CLOCK_DISCONTINUITY')
193
- else if (!v.device || !a.order || orders.get(deviceKey(v.device)) !== a.order || v.status !== 'captured' || !a.end) reason('unknown', 'CAPTURE_TIME_UNKNOWN')
194
- else if (componentAction?.dispatchStart && sourceMono !== undefined && sourceMono < componentAction.dispatchStart.mono) reason('stale', 'BEFORE_ACTION')
195
- else if (a.order && (a.order.sequence > a.endSequence || (a.reused && sourceMono !== undefined && sourceMono < a.order.lastStarted))) reason('stale', 'SUPERSEDED_BY_ACTION')
196
- else if (a.overlapping) reason('unknown', 'OVERLAPPING_ACTION')
197
- else if (a.reused) reason('unknown', 'REUSED_OBSERVATION')
198
- else { v.freshness = 'fresh' }
199
- if (!clockChanged && a.end && a.end.wall >= a.start.wall && v.source_captured_at_ms !== null && v.source_captured_at_ms <= end.wall) {
200
- v.age_ms = Math.max(0, end.mono - a.end.mono + a.end.wall - v.source_captured_at_ms)
201
- }
202
- return { ...v, session_id: sessionId, correlation_id: componentAction?.correlationId ?? current.correlationId,
203
- action_id: componentAction?.id ?? null, association: componentAction ? 'internal' : link.association }
204
- })
176
+ const observations = current.observations.map(a => assembleObservation(a, action, current, link, end, clockChanged))
205
177
  return {
206
178
  session_id: sessionId, correlation_id: action?.correlationId ?? current.correlationId,
207
179
  action_id: action?.id ?? null, device: current.device, ...link,
208
180
  ...(current.ref ? { requested_action_ref: current.ref } : {}),
209
181
  operation_started_at_ms: current.start.wall, operation_completed_at_ms: current.end?.wall ?? end.wall,
210
- dispatch_started_at_ms: action?.dispatchStart?.wall ?? null,
211
- dispatch_completed_at_ms: action?.dispatchEnd?.wall ?? null,
212
- verification_completed_at_ms: current.verification?.wall ?? action?.verification?.wall ?? null,
213
- observations, trace_context: { supported: false, trace_id: null, markers: [] }
182
+ ...actionTimes(action, current),
183
+ observations, trace_context: { supported: !!current.systemTraceId, trace_id: current.systemTraceId ?? null, markers: [] }
214
184
  }
215
185
  }
216
186
  export async function withEvidence<T>(options: { action?: boolean; journey?: boolean; ref?: ActionRef }, operation: () => Promise<T>): Promise<{ result: T; evidence: ReturnType<typeof assemble> }> {
217
187
  prune()
218
188
  const parent = context.getStore()
219
189
  const id = randomUUID()
220
- const record: ActionRecord | null = options.action ? { id, correlationId: parent?.correlationId ?? id, device: null,
221
- dispatchStart: null, dispatchEnd: null, verification: null, completed: null } : options.ref?.sessionId === sessionId ? active.get(options.ref.actionId) ?? history.get(options.ref.actionId) ?? null : null
222
- const current: EvidenceContext = { correlationId: options.action ? record!.correlationId : options.journey ? id : randomUUID(),
223
- 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
+ }
224
198
  if (options.action && record) active.set(id, record)
225
199
  if (parent && options.action) parent.children.push(current)
226
200
  try {
@@ -236,7 +210,7 @@ export async function withEvidence<T>(options: { action?: boolean; journey?: boo
236
210
  }
237
211
  }
238
212
 
239
- export function refreshJourneyEvidence(steps: Array<{ result?: { evidence?: unknown } }>) {
213
+ export function refreshJourneyEvidence(steps: Array<{ result?: { evidence?: unknown } | null }>) {
240
214
  const current = context.getStore()
241
215
  if (!current) return
242
216
  for (const step of steps) {
@@ -253,3 +227,88 @@ export function completeJourneyVerification(actionId: string) {
253
227
  child.action.verification = child.verification
254
228
  }
255
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
+ }