mobster-cli 0.4.3__py3-none-any.whl

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 (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,981 @@
1
+ """What the task contract reads off a screen: the words that name an act, a screen's title and recipient, receipts,
2
+ amounts, and the contract's items (Item, Gate, Control, Pending).
3
+
4
+ Contract (contract.py) proves the items; contract_asks.py decides which commits ask the person. Elements are duck-typed
5
+ (label, role, rect as screen fractions, editable, value): state.Element in the agent, recorded rows in the offline
6
+ replay.
7
+ """
8
+
9
+ import re
10
+ from dataclasses import dataclass, field
11
+
12
+ from .task_policy import COMMIT_CONTROL_WORDS, GUARD_WORDS, UNSEND
13
+
14
+ KINDS = ("READ", "WRITE", "COMMIT", "REPORT", "FORBID")
15
+ # Long reads whose notebook proves their summary item (Contract.notebook): a chain's REPORT items keep screen proof.
16
+ NOTEBOOK_KINDS = ("feed", "survey", "browse")
17
+ SUMMARY_ITEM = re.compile(r"\b(learn(?:ed|t|ing)?|found|summar(?:y|ies|i[sz]e)|shortlist|most promising|"
18
+ r"takeaways?|insights?|what (?:you|it) (?:saw|found|learned))\b", re.I)
19
+ MESSAGE = frozenset({"send", "post", "publish", "reply", "comment", "tweet", "submit"})
20
+ # Act -> the commit verbs a control may carry to perform it.
21
+ FAMILIES = {
22
+ "send_message": MESSAGE, "post": MESSAGE, "comment": MESSAGE, "reply": MESSAGE,
23
+ "react": frozenset({"like", "react"}),
24
+ "pay": frozenset({"pay", "send", "transfer", "confirm", "submit"}),
25
+ "request_money": frozenset({"request", "send", "confirm", "submit"}),
26
+ "transfer": frozenset({"transfer", "send", "confirm", "submit"}),
27
+ "order": frozenset({"order", "place order", "buy", "purchase", "checkout", "pay", "confirm", "submit", "tip"}),
28
+ # "Request CityRideX · $8.00" books a ride (cityride-001, required twice in the F1 runs).
29
+ "book": frozenset({"book", "reserve", "confirm", "submit", "request"}),
30
+ "follow": frozenset({"follow", "subscribe", "sign up"}),
31
+ "unfollow": frozenset({"unfollow", "unsubscribe"}),
32
+ "share": frozenset({"share", "send"}),
33
+ "call": frozenset({"call", "dial", "facetime"}),
34
+ "delete": frozenset({"delete", "erase", "remove"}),
35
+ "save": frozenset({"save"}),
36
+ "archive": frozenset({"archive"}),
37
+ "cancel": frozenset({"cancel", "confirm", "submit"}),
38
+ # An app from the App Store (C3): its Get, Install, price or Redownload button, only in the App Store.
39
+ "install": frozenset({"get", "install", "buy", "purchase", "redownload"}),
40
+ "other": frozenset(), "none": frozenset()}
41
+ ACTS = tuple(FAMILIES)
42
+ MESSAGE_ACTS = frozenset({"send_message", "post", "comment", "reply"})
43
+ MONEY_ACTS = frozenset({"pay", "transfer", "request_money", "order"})
44
+ # Commits bound to a named object: its name must be on screen (feed Follow buttons of other people).
45
+ OBJECT_ACTS = frozenset({"follow", "unfollow", "share", "delete", "save", "archive", "react", "call", "cancel",
46
+ "install"})
47
+ # A FORBID that rules out only the acts the request doesn't name ("Delete these three notes. Delete nothing else."):
48
+ # its words exclude others, so a declared COMMIT of the same act still goes to its own checks and the approval card.
49
+ # Any other FORBID ("do not delete anything") refuses every act of its family, a declared COMMIT or not.
50
+ OTHERS = re.compile(r"\b(?:else|other|others|besides|except|apart from|aside from|but (?:the|these|those|that|this))\b",
51
+ re.I)
52
+ # Acts that turn a control on (a like, a follow). Only that control turning on after the agent's own tap proves one:
53
+ # selected, its words for "on", or its count up by one. MobsterBench Real (11 Oct): "Like the first post" ended done
54
+ # after a tap on a point and a banner closing; any change on screen, or the post still being there, proved it.
55
+ STATE_ACTS = frozenset({"react", "follow"})
56
+ # A control's words once its act is on ("Liked", "Unlike"; "Following", "Requested"), in its label or value.
57
+ STATE_ON = {"react": re.compile(r"\b(liked|unlike|reacted|remove (?:like|reaction)|undo like)\b", re.I),
58
+ "follow": re.compile(r"^(following|followed|unfollow|requested|subscribed)\b", re.I)}
59
+ OFF_VALUES, ON_VALUES = frozenset({"0", "off", "false"}), frozenset({"1", "on", "true"})
60
+ TOGGLE_ROLES = frozenset({"Button", "Toggle", "Switch"})
61
+ # A count on or beside a control: "7", "1,204", "8 likes"; a rounded one ("1.2K") never moves by one.
62
+ COUNT = re.compile(r"^(\d{1,3}(?:,\d{3})+|\d+)(?:\s+(?:likes?|reactions?|hearts?|followers?))?$", re.I)
63
+ APP_STORE = "com.apple.AppStore"
64
+ # The App Store's commit buttons, read only there and only as a label's first word ("Get", "Redownload"), or a
65
+ # price ("$4.99"): "Get Directions" elsewhere is not a purchase.
66
+ STORE_VERBS = {"get": "get", "install": "install", "buy": "buy", "purchase": "purchase", "redownload": "redownload",
67
+ "re-download": "redownload", "download": "redownload"}
68
+ PRICE = re.compile(r"^\s*(?:buy,?\s*)?\$\s?\d")
69
+ # A message's delivery mark under its bubble (O2): "Delivered", "Read 10:42 AM", "Sent as Text Message".
70
+ DELIVERY = re.compile(r"^(delivered|read|sent)\b")
71
+ # Screen titles that name a list or a blank composer, not the person a thread is with.
72
+ # A mail sheet's title is its subject ("Re: Dinner plans"), never the person it goes to.
73
+ SUBJECT = re.compile(r"^\s*(re|fwd?|aw|sv|tr)\s*:", re.I)
74
+ MAIL_APPS = re.compile(r"\b(mail|gmail|outlook|spark|superhuman|proton ?mail|hey)\b")
75
+ GENERIC_TITLES = frozenset({"messages", "new message", "imessage", "chats", "inbox", "conversations", "details", "info",
76
+ "edit", "mail", "whatsapp", "signal"})
77
+ # The last word of a view's or a field's developer name ("ChatView", "chat_compose_field"): words of a UI's
78
+ # structure, which no person's or place's name ends with.
79
+ UI_WORDS = frozenset({"view", "views", "controller", "vc", "bar", "navbar", "navigationbar", "toolbar", "tabbar",
80
+ "field", "textfield", "textview", "button", "btn", "cell", "label", "header", "footer", "screen",
81
+ "container", "sheet", "modal", "input", "window", "panel", "identifier", "nav"})
82
+ # The first part of a bundle identifier ("com.apple.MobileSMS").
83
+ BUNDLE_ROOTS = frozenset({"com", "org", "net", "io", "app", "co", "dev", "me", "ai", "us", "uk", "de", "fr", "jp"})
84
+ # A To field's token that is not a party yet: Messages' "No Name, Searching" while it looks the name up.
85
+ UNRESOLVED = re.compile(r"\b(?:no name|searching|loading)\b", re.I)
86
+ # Controls that sit in a To field's row and name no one.
87
+ TO_ROW_CONTROLS = frozenset({"add", "add contact", "contact add", "add recipient", "add recipients", "to", "to:",
88
+ "cc/bcc, from:", "cc", "bcc", "clear", "remove", "more", "info"})
89
+
90
+ # The taint check (Contract.screen_text_in): a run of this many characters of a message that a screen showed.
91
+ TAINT_CHARS = 16
92
+ # A row's state iOS appends ("Buy oat milk, Incomplete") and a row's parts joined by commas: content, not a control.
93
+ ROW_ROLES_CONTENT = frozenset({"Cell", "StaticText"})
94
+ # The words F1 found ungated (request, cancel, unfollow, archive, save, like, react, unsubscribe). They are common
95
+ # in prose ("Feels Like", "Save on rides", "Your requests"), so they count only as a label's first word, where a
96
+ # control names its act.
97
+ FIRST_WORD_ONLY = frozenset({"request", "cancel", "unfollow", "unsubscribe", "archive", "save", "like", "react"})
98
+ # The verbs of task_policy.COMMIT_CONTROL plus FIRST_WORD_ONLY.
99
+ COMMIT_WORDS = re.compile(r"\b(" + "|".join((*COMMIT_CONTROL_WORDS, *sorted(FIRST_WORD_ONLY))) + r")\b", re.I)
100
+ # Controls that only open a sheet, a thread or a comment list (whose committing control is gated in
101
+ # turn), and the bare Cancel that closes one.
102
+ OPENERS = frozenset({"share", "share…", "share...", "comment", "comments", "comment left", "reply",
103
+ "reply in thread", "cancel"})
104
+ # SF Symbol names used as labels ("pencil.tip" is a drawing tool, not a tip).
105
+ SF_SYMBOL = re.compile(r"[a-z0-9]+(?:\.[a-z0-9]+)+")
106
+ # A delete no delete word heads: the trash glyph an unlabelled swipe action reads as (its SF Symbol name, "trash",
107
+ # "trash.fill", "xmark.bin"), "Move to Trash", and (deletes, with its frame) a narrow button named Trash or Bin.
108
+ DELETE_GLYPH = re.compile(r"(?:trash|xmark\.bin|bin\.xmark)(?:\.[a-z0-9]+)*")
109
+ MOVE_TO_TRASH = re.compile(r"\s*(?:move|send) (?:it |them |this |these )?to (?:the )?(?:trash|bin)\b", re.I)
110
+ TRASH_WORD = re.compile(r"(?:trash|bin)", re.I)
111
+ STREET = re.compile(r"\s+(st|street|ave|avenue|rd|road|blvd|way|dr|drive|ln|lane|ct|pl|plaza|sq)\b\.?", re.I)
112
+ SWITCH_ROLES = frozenset({"Switch", "Toggle"})
113
+
114
+ MONEY = re.compile(r"\$\s?(\d[\d,]*(?:\.\d+)?)")
115
+ NUMBER = re.compile(r"\d[\d,]*(?:\.\d+)?")
116
+ BOUND = re.compile(r"(under|below|less than|at most|up to|no more than|over|above|more than|at least)\s+"
117
+ r"\$\s?(\d[\d,]*(?:\.\d+)?)")
118
+ SEARCHY = re.compile(r"search|find|query|filter|destination|where to|recipient|phone, name|@username|"
119
+ r"\bto:|location|address|\bcity\b|zip", re.I)
120
+ COMPOSER = re.compile(r"compose|message|chat|comment|reply|write|tweet|caption|say something|on your mind|"
121
+ r"imessage|\bpost\b", re.I)
122
+ # What a new row must say to receipt a commit, by act (rows already on screen before the commit do not count:
123
+ # a checkout page's "saved card" once closed an order that was never placed).
124
+ RECEIPTS = {
125
+ "order": re.compile(r"\border (placed|confirmed|received|is on|submitted)|thank you for your order|\border #|"
126
+ r"order number|on (its|the) way|being prepared|estimated (delivery|arrival)|\barriving\b",
127
+ re.I),
128
+ "book": re.compile(r"\b(confirmed|confirmation|reserved|booked|you.re all set|ride requested|finding (you a|your) "
129
+ r"driver|driver (is|on)|arriving|on the way)\b|\bRSV[- ]?\w+|\b[A-Z]{2,4}-?\d{4,}\b", re.I),
130
+ "pay": re.compile(r"\b(paid|payment (sent|complete|successful)|you paid|money sent|sent \$|completed)\b", re.I),
131
+ "transfer": re.compile(r"\b(transfer(red)? (complete|sent|scheduled|successful)|transferred|completed|sent \$)\b",
132
+ re.I),
133
+ "request_money": re.compile(r"\b(requested|request sent|you requested|request (is )?pending)\b", re.I),
134
+ "follow": re.compile(r"^(following|unfollow|followed|subscribed)\b", re.I),
135
+ "unfollow": re.compile(r"^(follow|unfollowed|subscribe)\b", re.I),
136
+ # An install has begun or finished: the button turned to its progress ring or to Open. Apple's own sheet
137
+ # ("Double Click to Install", Face ID, the Apple ID password) changes the screen too, and is not one: the
138
+ # app is not installed until the user confirms there.
139
+ "install": re.compile(r"^(open|downloading|installing|waiting|redownloading|stop)\b|\b(stop|pause|cancel) "
140
+ r"download", re.I),
141
+ }
142
+ # Verbs that end many acts ("Confirm", "Submit", "Send"): a control that names only these says nothing about which
143
+ # act it performs, so it adds no other act's receipts.
144
+ SHARED_VERBS = frozenset({"confirm", "submit", "send"})
145
+ NO_RESULTS = re.compile(r"\bno (results|matches|items|messages|emails|mail|orders|transactions|events|"
146
+ r"trips|notes|documents|files|conversations|contacts)\b|nothing found|0 results|"
147
+ r"couldn.t find|did not match|didn.t match|no .{1,30} found", re.I)
148
+ # A screen that says it is empty: an absence answer ("No events tomorrow") is proven by it as by an empty search
149
+ # (B6: cal-tomorrow searched Calendar for a date to satisfy the gate, and still ended "Check the answer").
150
+ EMPTY_STATE = re.compile(r"^(no (upcoming )?(events|reminders|alarms|appointments|notifications|photos|videos|"
151
+ r"recents|favorites|bookmarks|downloads|rides|bookings|reservations|tasks|items|results|"
152
+ r"messages|emails|mail|orders|transactions|trips|notes|documents|files|contacts)"
153
+ r"( (today|tomorrow|this week|scheduled|found|yet))?|nothing (scheduled|planned|here)"
154
+ r"( (today|tomorrow))?|you have no \w+|all done|inbox zero)\b", re.I)
155
+ NONE_VALUE = re.compile(r"^(none|no\b|nothing|not found|zero\b|0 results|no one|nobody|there (are|were) no)", re.I)
156
+ CONFIRM_ITEM = re.compile(r"\bconfirm|confirmation|\b(was|were|been|is|are) (added|created|sent|saved|updated|"
157
+ r"posted|booked|made|completed|followed|placed|paid|requested|set|done)\b", re.I)
158
+ # A detail the agent works out rather than reads (its numbers are not on any one screen).
159
+ COMPUTED_ITEM = re.compile(r"\b(calculat\w*|total\w*|tally|sum|net|average|avg|project\w*|estimat\w*|how many|"
160
+ r"count|difference|surplus|deficit|budget|compar\w*|rate|percent\w*|per|breakdown|"
161
+ r"summar\w*|categori[sz]\w*|pattern|trend|frequency|typical|recommend\w*|vs|versus|"
162
+ r"progress|remaining|left|weekly|monthly|annual\w*|habits?|routine|analy[sz]\w*|"
163
+ r"plan|schedule|itinerary|preferences?|insights?|takeaways?|should|best|cheapest|"
164
+ r"closest|most|least)\b", re.I)
165
+ ATOM = re.compile(r"\$\s?\d[\d,]*(?:\.\d+)?|\d+(?:\.\d+)?%|\b[A-Z]{2,}-?\d{3,}\b|\b\d{1,2}:\d{2}(?::\d{2})?\b"
166
+ r"|\b\d[\d,]*(?:\.\d+)?\b")
167
+ FOLDERS = ("sent", "archive", "drafts", "trash", "junk", "inbox", "outbox")
168
+ NAME_STOP = frozenset({"pay", "paid", "request", "requested", "confirm", "cancel", "payment", "send", "you", "your",
169
+ "the", "a", "an", "to", "from", "for", "using", "balance", "decline", "done", "and", "of"})
170
+ # Capitalized words that name no particular object ("New post", "Reply to Rachel").
171
+ GENERIC = frozenset({"the", "a", "an", "my", "i", "new", "post", "job", "company", "reply", "comment", "message",
172
+ "request", "requests", "payment", "order", "reservation", "ride", "note", "document"})
173
+ CONFIRM_WORDS = frozenset({"confirm", "confirmation", "confirmed", "that", "were", "have", "been", "added", "created",
174
+ "sent", "updated", "completed", "done", "made", "with", "both", "all", "successfully"})
175
+ # A WRITE that adds (a log entry, an item to a list or a cart), and the words of its target that name no object.
176
+ ADDS = re.compile(r"\b(log(?:s|ged|ging)?|add(?:s|ed|ing)?|creat(?:e|es|ed)|new|insert|append|record)\b", re.I)
177
+ OBJECT_STOP = frozenset("""log logs logged new add set create entry entries item items list lists note notes post
178
+ posts message messages body text section cell value values document documents file files added adding update
179
+ updated created reminder reminders alarm alarms timer timers event events page today today's tomorrow morning
180
+ evening night with from into onto about this that your their summary details detail info information""".split())
181
+ LOW_TURNS = 3
182
+ # Reopens an item may cost before it is marked unproven: one (B6: the gate twice sent cal-tomorrow, weather and
183
+ # appstore-duolingo back for proof of a "no" that no screen could quote).
184
+ MAX_REOPENS = 1
185
+ MAX_ITEMS = 24
186
+
187
+
188
+ def plain(text):
189
+ """``text`` for containment checks: case, quote style and spacing ignored."""
190
+ return " ".join(str(text or "").casefold().replace("’", "'").replace("‘", "'").replace("“", '"')
191
+ .replace("”", '"').replace(" ", " ").split())
192
+
193
+
194
+ def as_float(number):
195
+ try:
196
+ return float(number.replace(",", ""))
197
+ except ValueError:
198
+ return None
199
+
200
+
201
+ # ---------------------------------------------------------------- commit verbs
202
+
203
+ def label_head(label):
204
+ """The first GUARD_WORDS words of ``label``; an identifier label split into words
205
+ ("mybank.checkout.confirmToggle" -> "mybank checkout confirm Toggle"); "" for an SF Symbol name."""
206
+ text = " ".join((label or "").split()[:GUARD_WORDS])
207
+ if SF_SYMBOL.fullmatch(text):
208
+ return ""
209
+ if " " not in text and re.search(r"[a-z][A-Z]|\.", text):
210
+ text = re.sub(r"(?<=[a-z])(?=[A-Z])", " ", text).replace(".", " ")
211
+ return text
212
+
213
+
214
+ def quoted(text):
215
+ """Names in quotes ('Budget Tracker', "Gym"); an apostrophe inside a word ("Anand's") opens none."""
216
+ return [a or b for a, b in re.findall(r"(?<![\w])'([^']{2,}?)'(?![\w])|\"([^\"]{2,}?)\"", text or "")]
217
+
218
+
219
+ # A control that acts on several things at once: "Delete All", "Delete 3 Notes", "Remove (2)".
220
+ MORE_THAN_ONE = re.compile(r"\b(?:all|everything|every)\b|\(\s*[2-9]\d*\s*\)|\b(?:[2-9]|\d{2,}) "
221
+ r"(?:items|notes|messages|photos|videos|files|emails|conversations|contacts|reminders|"
222
+ r"events)\b", re.I)
223
+
224
+
225
+ def row_of(element, elements):
226
+ """The list row ``element`` belongs to: the smallest Cell holding its centre, or (a swipe action revealed beside
227
+ it) a Cell on the same line just left of it. None for a control outside any row (a toolbar's Delete)."""
228
+ if element is None or getattr(element, "role", None) == "Cell":
229
+ return None
230
+ x, y, w, h = element.rect
231
+ cx, cy = x + w / 2, y + h / 2
232
+ holders = [e for e in elements if e is not element and e.role == "Cell"
233
+ and e.rect[0] <= cx <= e.rect[0] + e.rect[2] and e.rect[1] <= cy <= e.rect[1] + e.rect[3]]
234
+ if holders:
235
+ return min(holders, key=lambda e: e.rect[2] * e.rect[3])
236
+ beside = [e for e in elements if e is not element and e.role == "Cell" and abs(center_y(e) - cy) <= e.rect[3] / 2
237
+ and e.rect[0] + e.rect[2] <= x + .02 and e.rect[3] <= .2]
238
+ return max(beside, key=lambda e: e.rect[0] + e.rect[2]) if beside else None
239
+
240
+
241
+ # A reaction control's label, which a feed repeats on every post's action line ("Like", "Liked", "React").
242
+ REACTION_CONTROL = re.compile(r"(?:like|liked|unlike|react|reacted|reaction)\b")
243
+ # How far a feed's action lines may sit from one column (screen fractions), and how far below its line a post starts.
244
+ ACTION_COLUMN, ACTION_LINE = .03, .02
245
+
246
+
247
+ def card_of(element, elements):
248
+ """The post a reaction control closes, on a feed whose posts are not Cells: the elements from just below the
249
+ previous reaction control in its column (the action line of the post above) down to this control's line, or from
250
+ the screen's top. iOSWorld multi-092 (18 Oct, LockedIn): the Like at y .84 closes Devi Anand's card, from her name
251
+ at .42; the Like at .35 closes Rohan's post above it. A reactions summary's "Like" icon is a picture, not a post's
252
+ action line (the same run, on the post's own page)."""
253
+ x, middle = element.rect[0], center_y(element)
254
+ above = [center_y(other) for other in elements if other is not element and other.role != "Image"
255
+ and abs(other.rect[0] - x) <= ACTION_COLUMN and center_y(other) < middle - ACTION_LINE
256
+ and REACTION_CONTROL.match(plain(other.label))]
257
+ top = max(above) + ACTION_LINE if above else -1.0
258
+ return [other for other in elements if top < center_y(other) <= middle + ACTION_LINE / 4]
259
+
260
+
261
+ def is_opener(label):
262
+ return " ".join((label or "").split()).casefold() in OPENERS
263
+
264
+
265
+ def deletes(label, role=None, rect=None):
266
+ """Whether a control deletes though no delete word heads its label: the trash glyph a swipe action reads as when
267
+ it has no label of its own ("trash", "trash.fill"), "Move to Trash", or, given its frame, a narrow button named
268
+ Trash or Bin (a row's swipe action). A cell, a text or a full-width row named Trash is the folder, not a delete.
269
+ MobsterBench Real, 12 Oct: a Clock timer's swipe action "trash" went through Smart's commit check; a "Delete" one
270
+ never did."""
271
+ text = " ".join(str(label or "").split())
272
+ if DELETE_GLYPH.fullmatch(text) or MOVE_TO_TRASH.match(text):
273
+ return True
274
+ return bool(rect and TRASH_WORD.fullmatch(text) and role not in ("Cell", "StaticText", "Link", "Image")
275
+ and rect[2] <= .35)
276
+
277
+
278
+ def label_verbs(label, role="Button"):
279
+ """The commit verbs a control's label names (set, empty when it names none)."""
280
+ if (label or "").strip().casefold() == "following":
281
+ return {"unfollow"} # a Follow button after the tap: tapping it again unfollows
282
+ if deletes(label):
283
+ return {"delete"} # the trash glyph, or Move to Trash
284
+ if UNSEND.search(label or ""):
285
+ return {"delete"}
286
+ if role in ROW_ROLES_CONTENT and ("," in (label or "") or len((label or "").split()) > 6):
287
+ return set() # a row's words say what it shows ("Like a Prayer, Madonna", "Call the dentist, Incomplete")
288
+ head = label_head(label)
289
+ if not head or is_opener(label):
290
+ return set()
291
+ out = set()
292
+ for match in COMMIT_WORDS.finditer(head):
293
+ verb = " ".join(match.group(0).casefold().split())
294
+ verb = "sign up" if verb.replace("-", "").replace(" ", "") == "signup" else verb
295
+ if STREET.match(head, match.end()):
296
+ continue # "490 Post St": an address
297
+ at_start = head[:match.start()].strip(" \t,.:;!-•·") == ""
298
+ if verb in FIRST_WORD_ONLY and not at_start:
299
+ continue
300
+ if role in SWITCH_ROLES and not at_start and " " in (label or "").strip():
301
+ continue # a switch's label names its setting: the alarm "9:30, Client call, Weekdays"
302
+ out.add(verb)
303
+ return out
304
+
305
+
306
+ def internal_name(text):
307
+ """Whether ``text`` is a developer's name for a view, not words the screen shows a person: a class name
308
+ ("CKComposeChat", "UINavigationBar", "ComposeChatViewController"), an accessibility identifier
309
+ ("composeTextField", "chat_compose_field") or a bundle-like string ("com.apple.MobileSMS"). XCUITest reports a
310
+ view's identifier as its label when it has none: a Messages compose sheet's bar read "CKComposeChat", and the
311
+ approval card named it as who the message went to (MobsterBench Real, 11 Oct). Never one: text with a space,
312
+ a phone number, an email address, a handle ("sam_rivera", "jo.bloggs") or a one-hump name ("McDonald",
313
+ "iPhone")."""
314
+ text = " ".join(str(text or "").split())
315
+ if not text or not re.fullmatch(r"[A-Za-z_][\w.-]*", text):
316
+ return False
317
+ if text.startswith("_"):
318
+ return True # a Swift-mangled or private name ("_TtC4Chat8ChatView")
319
+ parts = [p for p in re.split(r"[._-]", text) if p]
320
+ camel = any(re.search(r"[a-z][A-Z]", p) for p in parts)
321
+ if len(parts) > 1:
322
+ # Dotted, snake or kebab: a bundle ("com.apple.MobileSMS"), a camel-cased part, or a UI word last.
323
+ return (camel or "." in text and parts[0].casefold() in BUNDLE_ROOTS and len(parts) >= 3
324
+ or parts[-1].casefold() in UI_WORDS)
325
+ humps = re.findall(r"[A-Z][a-z0-9]+", text)
326
+ last = humps[-1].casefold() if humps else ""
327
+ prefix = re.match(r"[A-Z]{2,}(?=[A-Z][a-z])", text)
328
+ if prefix:
329
+ # An Objective-C class prefix ("CK", "UI", "PU") before two words or a UI word ("CKComposeChat").
330
+ return len(re.findall(r"[A-Z][a-z0-9]+", text[prefix.end():])) >= 2 or last in UI_WORDS
331
+ if re.fullmatch(r"[a-z]+(?:[A-Z][a-z0-9]*)+", text):
332
+ return len(humps) >= 2 or last in UI_WORDS # lowerCamel: "composeTextField", "chatView"; not "iPhone"
333
+ return len(humps) >= 3 or len(humps) == 2 and last in UI_WORDS # "ComposeChatView", "ChatView"
334
+
335
+
336
+ def _bar_text(bar, elements):
337
+ """The centred text of a navigation bar's top row (its title when the bar is named by a class), or None."""
338
+ x, y, w, h = bar.rect
339
+ inside = [e for e in elements if e.role in ("StaticText", "Button") and (e.label or "").strip()
340
+ and y <= e.rect[1] + e.rect[3] / 2 <= y + min(h, .07) and x + .2 * w <= e.rect[0] + e.rect[2] / 2
341
+ <= x + .8 * w]
342
+ if not inside:
343
+ return None
344
+ centred = min(inside, key=lambda e: abs(e.rect[0] + e.rect[2] / 2 - (x + w / 2)))
345
+ return re.sub(r",\s*Actions Menu$", "", " ".join(centred.label.split()), flags=re.I) or None
346
+
347
+
348
+ def screen_title(snapshot):
349
+ """The screen's title as shown (its navigation bar's label), or None: a bar named by a class name
350
+ ("FullDocumentManagerViewControllerNavigationBar") has the title inside it instead."""
351
+ elements = list(getattr(snapshot, "elements", ()) or ())
352
+ bar = next((e for e in elements if e.role == "NavigationBar" and (e.label or "").strip()), None)
353
+ if bar is None:
354
+ return None
355
+ label = " ".join(bar.label.split())
356
+ if not re.fullmatch(r"[A-Za-z_][\w.]*", label) or len(label) <= 20:
357
+ return re.sub(r",\s*Actions Menu$", "", label, flags=re.I) or None
358
+ return _bar_text(bar, elements)
359
+
360
+
361
+ def thread_title(snapshot, app=None):
362
+ """The person or place a message screen is with, as its title shows it ("+1 (555) 564-8583"), or None for a
363
+ list's or a blank composer's title, and for mail: a reply's sheet is titled with its subject, and "Send this
364
+ reply to Re: Dinner plans?" names no one. A bar named by a class ("CKComposeChat") names no one either: its
365
+ centred text stands for it ("New Message", a blank composer's)."""
366
+ title = screen_title(snapshot)
367
+ if title and internal_name(title):
368
+ bar = next((e for e in getattr(snapshot, "elements", ()) or () if e.role == "NavigationBar"
369
+ and (e.label or "").strip()), None)
370
+ title = _bar_text(bar, list(snapshot.elements)) if bar is not None else None
371
+ if not title or internal_name(title):
372
+ return None
373
+ folded = plain(title)
374
+ if folded in GENERIC_TITLES or app and folded == plain(app):
375
+ return None
376
+ if SUBJECT.match(title) or app and MAIL_APPS.search(plain(app)):
377
+ return None
378
+ return title
379
+
380
+
381
+ def addressed_to(snapshot):
382
+ """Who a composer's To field addresses, as its tokens show them ("Jordan Lee"; "Sam Rivera, Alex Kim"), or None
383
+ when it holds none. Messages reads the field as "To:\\ufffcJordan Lee" (each token an attachment mark) and draws
384
+ each token as a button on the field's row ("Jordan Lee, +15550100000"); a token still being looked up ("No Name,
385
+ Searching") names no one yet."""
386
+ elements = list(getattr(snapshot, "elements", ()) or ())
387
+ for to_field in elements:
388
+ if not getattr(to_field, "editable", False) or not re.match(r"^\s*to\b(?![-'’])", to_field.label or "", re.I):
389
+ continue
390
+ value = getattr(to_field, "value", "") or ""
391
+ if value and value == (getattr(to_field, "placeholder", "") or None):
392
+ value = ""
393
+ value = re.sub(r"^\s*to\s*:", "", value, flags=re.I)
394
+ names = [" ".join(part.split()).strip(" ,;") for part in re.split(r"[\ufffc,;\n]", value)]
395
+ names = [n for n in names if n and not UNRESOLVED.search(n) and not internal_name(n)]
396
+ if not names:
397
+ # The tokens as buttons on the field's row, each "name, address".
398
+ x, y, w, h = to_field.rect
399
+ for e in elements:
400
+ label = " ".join((e.label or "").split())
401
+ cx, cy = e.rect[0] + e.rect[2] / 2, e.rect[1] + e.rect[3] / 2
402
+ if (e is to_field or e.role != "Button" or not label or not (x <= cx <= x + w and y <= cy <= y + h)
403
+ or plain(label) in TO_ROW_CONTROLS or SF_SYMBOL.fullmatch(label) or UNRESOLVED.search(label)):
404
+ continue
405
+ name = label.split(",")[0].strip()
406
+ if name and not internal_name(name):
407
+ names.append(name)
408
+ names = list(dict.fromkeys(names))
409
+ if names:
410
+ return ", ".join(names[:3]) + (f" and {len(names) - 3} more" if len(names) > 3 else "")
411
+ return None
412
+
413
+
414
+ _TITLE_ROLES = frozenset({"NavigationBar", "Header", "Heading", "TabBar", "Toolbar", "Key", "Keyboard"})
415
+
416
+
417
+ def _screen_titles(snapshot):
418
+ """What names the screen or the app: navigation titles and the app's own name (never proof)."""
419
+ names = {" ".join(e.label.split()).casefold() for e in snapshot.elements if e.role in _TITLE_ROLES
420
+ and e.label.strip()}
421
+ bundle = (snapshot.bundle_id or "").rsplit(".", 1)[-1]
422
+ if bundle:
423
+ names.add(bundle.casefold())
424
+ try:
425
+ from . import harness_api
426
+ apps = harness_api.hooks().app_catalog() # () in the open-source engine: no app names beyond the bundle's
427
+ names.update(" ".join(app["name"].split()).casefold() for app in apps
428
+ if app.get("bundleId") == snapshot.bundle_id)
429
+ except Exception:
430
+ pass
431
+ return names
432
+
433
+
434
+ def header_recipient(snapshot, target=None):
435
+ """Who a message goes to, from a filled To field's tokens or the conversation's header (its title, or the
436
+ contact label near the top); None when the screen does not say, or names more than one candidate. A
437
+ developer's name for a view ("CKComposeChat", a bar's identifier) is never one (contract.internal_name)."""
438
+ if snapshot is None:
439
+ return None
440
+ to = addressed_to(snapshot)
441
+ if to and len(to) <= 60:
442
+ return to
443
+ generic = _screen_titles(snapshot) | {"messages", "back", "contact photo", "details", "info", "edit", "cancel",
444
+ "done", "facetime", "video", "call", "audio call", "video call", "search",
445
+ "new message", "chats", "inbox", "compose", "more", "menu", "options"}
446
+ above = target.rect[1] if target is not None else 1
447
+ # The conversation header is the navigation bar when there is one: "iMessage · Encrypted" and the
448
+ # day's timestamp sit just under it and are not who the message goes to.
449
+ bars = [e.rect for e in snapshot.elements if e.role == "NavigationBar" and e.rect[1] < .22]
450
+ top, bottom = (min(r[1] for r in bars), max(r[1] + r[3] for r in bars)) if bars else (0, .22)
451
+ candidates = []
452
+ for element in snapshot.elements:
453
+ label = " ".join((element.label or "").split())
454
+ x, y, w, h = element.rect
455
+ if (element.editable or not label or len(label) > 40 or " ".join(label.split()).casefold() in generic
456
+ or internal_name(label)
457
+ or not top <= y + h / 2 <= bottom or y >= above
458
+ or element.role in {"Image", "Icon", "Key", "Switch", "Toggle"}
459
+ or re.fullmatch(r"[\d:. ]+(?:AM|PM)?|Today|Yesterday", label, re.I)):
460
+ continue
461
+ candidates.append(label)
462
+ unique = list(dict.fromkeys(candidates))
463
+ return unique[0] if len(unique) == 1 else None
464
+
465
+
466
+ def visible_recipient(snapshot, app=None, target=None):
467
+ """Who the screen shows a message going to: the To field's tokens, else the thread's title (thread_title), else
468
+ the one name atop a thread whose bar has no title ("Sam" above the messages: header_recipient; ``target``: the
469
+ control about to be tapped, below it), else None. Never a developer's name for a view (internal_name), and never a
470
+ mail's top line, which is its subject."""
471
+ found = addressed_to(snapshot) or thread_title(snapshot, app)
472
+ if found or app and MAIL_APPS.search(plain(app)) or not getattr(snapshot, "elements", None):
473
+ return found
474
+ name = header_recipient(snapshot, target)
475
+ return None if not name or SUBJECT.match(name) or internal_name(name) else name
476
+
477
+
478
+ # An address typed whole: a phone number or an email. Return in a To field makes it a recipient of its own, as asked.
479
+ ADDRESS = re.compile(r"^\s*(?:\+?[\d\s().-]{7,}|[^@\s]+@[^@\s]+\.[^@\s]+)\s*$")
480
+ TO_FIELD = re.compile(r"^\s*(?:to|recipients?)\b(?![-'’])", re.I)
481
+ ROW_ROLES = frozenset({"Cell", "Button", "StaticText", "Other", "Link"})
482
+
483
+
484
+ def squash(text):
485
+ return re.sub(r"[^0-9a-z]", "", plain(text))
486
+
487
+
488
+ def name_match(row, name):
489
+ """How a row's name matches the name a request gave: "exact" (the same words, case and spacing aside), "close" (a
490
+ handle made of it, "jordanlee" for "Jordan Lee"; a longer or shorter name holding it; a shared word), or None."""
491
+ r, n = plain(row), plain(name)
492
+ if not r or not n:
493
+ return None
494
+ if r == n:
495
+ return "exact"
496
+ rs, ns = squash(row), squash(name)
497
+ if rs and ns and min(len(rs), len(ns)) >= 3 and (ns in rs or rs in ns):
498
+ return "close"
499
+ if set(re.findall(r"[a-z0-9]{3,}", r)) & set(re.findall(r"[a-z0-9]{3,}", n)):
500
+ return "close"
501
+ return None
502
+
503
+
504
+ def row_name(label):
505
+ """A row's name: its label's first part ("Jordan Lee, (555) 010-0000, mobile" -> "Jordan Lee")."""
506
+ return " ".join(str(label or "").split(",")[0].split())
507
+
508
+
509
+ def typed_query(element):
510
+ """What a field holds as typed text, without its "To:" and its tokens (each an attachment mark)."""
511
+ value = getattr(element, "text", None)
512
+ value = value if isinstance(value, str) else (getattr(element, "value", "") or "")
513
+ if value and value == (getattr(element, "placeholder", "") or None):
514
+ value = ""
515
+ value = re.sub(r"^\s*(?:to|recipients?)\s*:", "", value, flags=re.I)
516
+ return " ".join(value.split("\ufffc")[-1].split()).strip(" ,;")
517
+
518
+
519
+ def same_party(a, b):
520
+ """Whether two names of a party agree: one contains the other, or the same phone digits."""
521
+ a, b = plain(a), plain(b)
522
+ if not a or not b:
523
+ return False
524
+ digits_a, digits_b = re.sub(r"\D", "", a), re.sub(r"\D", "", b)
525
+ if len(digits_a) >= 7 and len(digits_b) >= 7:
526
+ return digits_a[-10:] == digits_b[-10:]
527
+ return a in b or b in a
528
+
529
+
530
+ def store_verbs(label):
531
+ """The App Store's commit verbs a control's label names (only meant for com.apple.AppStore)."""
532
+ text = " ".join((label or "").split())
533
+ if PRICE.match(text):
534
+ return {"buy"}
535
+ first = text.split(" ", 1)[0].strip(",.:;").casefold() if text else ""
536
+ return {STORE_VERBS[first]} if first in STORE_VERBS else set()
537
+
538
+
539
+ def element_text(element):
540
+ label, value = (element.label or ""), (getattr(element, "value", "") or "")
541
+ return (label + (" " + value if value and value != label else "")).strip()
542
+
543
+
544
+ def is_search_field(element):
545
+ return element.role == "SearchField" or bool(SEARCHY.search(element.label or ""))
546
+
547
+
548
+ def composer_field(element, elements, typed_texts=()):
549
+ """Whether Return in ``element`` would send a message: a non-search editable field named like a
550
+ composer ("chat_compose_field", placeholder "Add a comment"), or one beside a Send/Post button."""
551
+ if element is None or not getattr(element, "editable", False) or is_search_field(element):
552
+ return False
553
+ value = getattr(element, "value", "") or ""
554
+ placeholder = value if value and not any(overlaps(value, t) for t in typed_texts) else ""
555
+ if SEARCHY.search(placeholder):
556
+ return False
557
+ if COMPOSER.search((element.label or "") + " " + placeholder):
558
+ return True
559
+ y = element.rect[1] + element.rect[3] / 2
560
+ for other in elements:
561
+ if other is element or getattr(other, "editable", False) or is_opener(other.label):
562
+ continue
563
+ verbs = label_verbs(other.label, other.role)
564
+ if verbs and verbs <= MESSAGE and abs(other.rect[1] + other.rect[3] / 2 - y) <= .12:
565
+ return True
566
+ return False
567
+
568
+
569
+ DURATION = re.compile(r"(\d+(?:\.\d+)?)\s*(hours?|hrs?|h|minutes?|mins?|m|seconds?|secs?|s)\b")
570
+ CLOCK = re.compile(r"(?<![\d:])(\d{1,2}):(\d{2})(?::(\d{2}))?(?![\d:])")
571
+ TIME_OF_DAY = re.compile(r"(?<![\d:])(\d{1,2})(?::(\d{2}))?\s*([ap])\.?\s?m\b")
572
+ UNIT_SECONDS = {"h": 3600, "m": 60, "s": 1}
573
+
574
+
575
+ def _durations(text):
576
+ """Durations a text states, in seconds: "1 hour", "90 min", and clock readings "1:00:00" or "59:58"."""
577
+ out = {round(float(n) * UNIT_SECONDS[u[0]]) for n, u in DURATION.findall(text)}
578
+ for h, m, sec in CLOCK.findall(text):
579
+ out.add(int(h) * 3600 + int(m) * 60 + int(sec) if sec else int(h) * 60 + int(m))
580
+ return out
581
+
582
+
583
+ def _times(text):
584
+ """Times of day a text states, as minutes past midnight modulo 12 hours: "7 PM", "7:00 pm", "19:00", "6:45"."""
585
+ out = {(int(h) % 12) * 60 + int(m or 0) for h, m, _ in TIME_OF_DAY.findall(text)}
586
+ out |= {(int(h) % 12) * 60 + int(m) for h, m, sec in CLOCK.findall(text) if not sec}
587
+ return out
588
+
589
+
590
+ def _token(atom, text):
591
+ """``atom`` (a number or a time) in ``text`` as a whole token, not inside a longer number or time."""
592
+ return re.search(rf"(?<![\d.:,]){re.escape(atom)}(?![\d:]|[.,]\d)", text) is not None
593
+
594
+
595
+ def shows(key, text):
596
+ """Whether a row's ``text`` shows the short value ``key``: whole, or its numbers and times with most of
597
+ its words ("6:45 AM" in "6:45, gym on"; "Home (410 Brannan St)" in "410 brannan st").
598
+
599
+ Numbers count only as whole tokens: the "1" in "+10:30HRS" does not show "1 hour" (10 Oct, iOSWorld: a World
600
+ Clock row proved a 1-hour timer that was never set). A duration also matches an equal one in another unit or a
601
+ running clock within a minute or 5% ("1 hour": "1 hr", "60 min", "1:00:00", "59:58"), and a time of day its
602
+ other spellings ("7 PM": "7:00 pm", "19:00")."""
603
+ if len(key) >= 40:
604
+ return key in text
605
+ if not re.search(r"\d", key):
606
+ if key in text:
607
+ return True
608
+ words = [w for w in re.findall(r"[a-z]{3,}", key) if w not in ("the", "and", "for")]
609
+ return bool(words) and all(w in text for w in words)
610
+ durations = _durations(key) if DURATION.search(key) else set()
611
+ if durations:
612
+ return any(abs(seen - want) <= max(60, .05 * want) for seen in _durations(text) for want in durations)
613
+ rest = key
614
+ for match in TIME_OF_DAY.finditer(key):
615
+ want = (int(match.group(1)) % 12) * 60 + int(match.group(2) or 0)
616
+ if want not in _times(text):
617
+ return False
618
+ rest = rest.replace(match.group(0), " ")
619
+ atoms = re.findall(r"\d{1,2}:\d{2}|\d[\d,.]*\d|\d", rest)
620
+ # set by pickers and taps: "6 people" shows as "6 guests"
621
+ return all(_token(atom, text) for atom in atoms)
622
+
623
+
624
+ # A tap on a control that names an act writes or commits (the first write in its app counts from it).
625
+ ACT_LABEL = re.compile(r"\b(save|send|add|book|confirm|delete|remove|post|pay|submit|create|reserve|request|buy|order|"
626
+ r"checkout|check out|start|apply|update|rename|accept|decline|join|follow|like|archive|mark|"
627
+ r"done|set|transfer|split|place|schedule|share|upload|reply|rsvp|log|clear|favorite|wishlist|"
628
+ r"enable|disable|turn on|turn off)\b", re.I)
629
+ WRITE_OPS = frozenset({"TYPE", "TYPE_SUBMIT", "SET_TEXT", "SUBMIT"})
630
+ ALREADY = re.compile(r"^already\b\W*(.+)$", re.I)
631
+
632
+
633
+ def receipt_patterns(act, verbs=frozenset()):
634
+ """The rows that receipt a commit of ``act`` made by a control naming ``verbs``: the act's own, and those of
635
+ every act the control's own verb names (an act's family holds it), since the control is what ran. "Request
636
+ CityRideX · $12.80" books a ride whatever act the contract compiled for it: a ride compiled as request_money
637
+ was requested ("Driver arriving") and never counted as done (ac2, 11 Oct). An act with no receipt row of its
638
+ own keeps "the screen changed"; a control naming only SHARED_VERBS adds nothing."""
639
+ own = RECEIPTS.get(act)
640
+ if own is None:
641
+ return []
642
+ named = frozenset(verbs) - SHARED_VERBS
643
+ others = [RECEIPTS[other] for other, family in FAMILIES.items()
644
+ if other != act and other in RECEIPTS and named and named <= family]
645
+ return [own] + others
646
+
647
+
648
+ def list_rows(elements, rows):
649
+ """The folded rows of ``elements`` that sit in a list: a Cell, or an element whose centre is inside a Cell."""
650
+ cells = [e.rect for e in elements if e.role == "Cell" and e.rect[2] * e.rect[3] > 0]
651
+ if not cells:
652
+ return frozenset()
653
+
654
+ def inside(element):
655
+ x, y = element.rect[0] + element.rect[2] / 2, element.rect[1] + element.rect[3] / 2
656
+ return any(cx <= x <= cx + cw and cy <= y <= cy + ch for cx, cy, cw, ch in cells)
657
+ return frozenset(text for element, (text, editable, role) in zip(elements, rows)
658
+ if text and not editable and (role == "Cell" or inside(element)))
659
+
660
+
661
+ def message_key(payload):
662
+ """What identifies a sent message on screen: its first non-empty line, folded, cut to 40 characters."""
663
+ return plain(next((line for line in str(payload or "").split("\n") if line.strip()), payload or ""))[:40]
664
+
665
+
666
+ def overlaps(a, b):
667
+ a, b = plain(a), plain(b)
668
+ if len(a) < 3 or len(b) < 3:
669
+ return a == b and bool(a)
670
+ return a[:30] in b or b[:30] in a
671
+
672
+
673
+ NAME = re.compile(r"^(?:[A-Z][\w'’.&-]*)(?: [A-Z][\w'’.&-]*){0,2}$")
674
+
675
+
676
+ def recipient(item):
677
+ """The person or place a commit is for, when ``what`` names one ("message to Sam" -> "Sam", "Sam" ->
678
+ "Sam"), else None. Never a developer's name for a view (internal_name)."""
679
+ what = " ".join(str(item.what or "").split())
680
+ found = re.search(r"\b(?:to|for|with)\s+((?:[A-Z][\w'’.&-]*)(?: [A-Z][\w'’.&-]*){0,2})", what)
681
+ names = quoted(what)
682
+ who = found.group(1) if found else names[0] if names else (
683
+ what if NAME.fullmatch(what) and plain(what) not in GENERIC else None)
684
+ return None if internal_name(who) else who
685
+
686
+
687
+ def point_in(rect, x, y):
688
+ ex, ey, ew, eh = rect
689
+ return ex <= x <= ex + ew and ey <= y <= ey + eh
690
+
691
+
692
+ # ---------------------------------------------------------------- amounts
693
+
694
+ def bounds(item):
695
+ """(low, high) money bounds from the item's condition and quote ("under $50" -> (None, 49.99...)). A
696
+ money act that names one amount and no bound moves exactly that ("requests ... for $15")."""
697
+ low = high = None
698
+ text = plain(item.condition + " " + item.quote)
699
+ if item.act in ("pay", "transfer", "request_money") and not BOUND.search(text):
700
+ amounts = {as_float(m) for m in MONEY.findall(text)} - {None}
701
+ if len(amounts) == 1:
702
+ value = amounts.pop()
703
+ return value - .005, value + .005
704
+ for match in BOUND.finditer(text):
705
+ value = as_float(match.group(2))
706
+ if value is None:
707
+ continue
708
+ word = match.group(1)
709
+ if word in ("under", "below", "less than"):
710
+ high = value - 1e-9
711
+ elif word in ("at most", "up to", "no more than"):
712
+ high = value
713
+ elif word in ("over", "above", "more than"):
714
+ low = value + 1e-9
715
+ else:
716
+ low = value
717
+ return low, high
718
+
719
+
720
+ def center_y(element):
721
+ return element.rect[1] + element.rect[3] / 2
722
+
723
+
724
+ def screen_amounts(elements):
725
+ """[(amount, y, element)] of the money shown on screen; a "$" drawn apart from its digits
726
+ (SplitPay's keypad amount: "$" then "52") is joined."""
727
+ out = []
728
+ ordered = [e for e in elements if not getattr(e, "editable", False) or MONEY.search(e.value or "")]
729
+ for index, element in enumerate(ordered):
730
+ text = element_text(element)
731
+ for match in MONEY.finditer(text):
732
+ value = as_float(match.group(1))
733
+ if value is not None:
734
+ out.append((value, center_y(element), element))
735
+ if text.strip() == "$":
736
+ for other in ordered[index + 1:index + 3]:
737
+ digits = element_text(other).strip()
738
+ if re.fullmatch(r"\d[\d,]*(?:\.\d+)?", digits) and abs(center_y(other) - center_y(element)) < .06:
739
+ value = as_float(digits)
740
+ if value is not None:
741
+ out.append((value, center_y(other), other))
742
+ break
743
+ return out
744
+
745
+
746
+ AMOUNT_FIELD = re.compile(r"[$€£]?\s?(\d[\d,]*(?:\.\d+)?)")
747
+
748
+
749
+ def typed_amount(element, elements, typed):
750
+ """The amount in a field the run typed into, nearest the control: what a form's Send or Pay moves. ``typed``:
751
+ (the field's label, the text) for each thing the run typed, the label None for text typed earlier in this model
752
+ call's chain, which counts only where a field holds exactly that text. The field must be named Amount and its whole
753
+ value must be an amount ("50", "$50.00"): a numeric memo or account number is not one. None when no such field is on
754
+ screen. A bank's form shows the account's balance beside the amount field ("Available $15,621.00"), and the nearest
755
+ money is that, not the 50 that was typed (iOSWorld mybank-003, 18 Oct)."""
756
+ found = []
757
+ for field_ in elements:
758
+ value = (getattr(field_, "value", "") or "").strip()
759
+ if not getattr(field_, "editable", False) or not value or value == (getattr(field_, "placeholder", "") or ""):
760
+ continue
761
+ if plain(field_.label) != "amount":
762
+ continue
763
+ whole = AMOUNT_FIELD.fullmatch(value)
764
+ amount = as_float(whole.group(1)) if whole else None
765
+ mine = any(label == field_.label if label is not None else plain(text) == plain(value)
766
+ for label, text in typed)
767
+ if amount is not None and mine:
768
+ found.append((abs(center_y(field_) - center_y(element)), amount))
769
+ return min(found)[1] if found else None
770
+
771
+
772
+ # The words a bank puts beside money an account holds. "Balance due", "statement balance", an amount or a total are
773
+ # what a payment moves, and are not these.
774
+ HELD = re.compile(r"(?:available (?:balance|credit|funds|cash|to spend)|current balance)")
775
+ HELD_SECTION = re.compile(r"(?:bank accounts|credit cards)(?: \(\d+\))?")
776
+
777
+
778
+ def held_figures(elements):
779
+ """Labelled account balances on a non-editable dashboard, including an account section's aligned total.
780
+ A bare payment can equal one balance or their sum: only its labels and row establish what the figure means.
781
+ Unknown figures remain checked as payment amounts; any editable field keeps the normal form checks."""
782
+ sections = [e for e in elements if HELD_SECTION.fullmatch(plain(element_text(e)))]
783
+ if not sections or any(getattr(e, "editable", False) for e in elements):
784
+ return set()
785
+ held, bare = set(), []
786
+ for element in elements:
787
+ text = element_text(element)
788
+ if MONEY.fullmatch(text.strip()):
789
+ bare.append((element, as_float(MONEY.fullmatch(text.strip()).group(1))))
790
+ continue
791
+ parts = [p.strip() for p in re.split(r",\s+|\n", text)]
792
+ for index, part in enumerate(parts):
793
+ for match in MONEY.finditer(part):
794
+ beside = [plain(part.replace(match.group(0), "")).strip(" :,"), *(
795
+ plain(parts[i]) for i in (index - 1, index + 1) if 0 <= i < len(parts))]
796
+ if any(HELD.fullmatch(words) or HELD_SECTION.fullmatch(words) for words in beside):
797
+ held.add((id(element), as_float(match.group(1))))
798
+ for element, value in bare:
799
+ if value is None:
800
+ continue
801
+ for section in sections:
802
+ tolerance = min(element.rect[3], section.rect[3]) / 4
803
+ if tolerance > 0 and abs(center_y(element) - center_y(section)) <= tolerance:
804
+ held.add((id(element), value))
805
+ break
806
+ return held
807
+
808
+
809
+ def commit_amount(element, elements, typed=()):
810
+ """The amount a commit control moves: in its label, else in a field the run typed it into (``typed``:
811
+ ``typed_amount``), else the money shown nearest to it that isn't money an account holds (``held_figures``)."""
812
+ match = MONEY.search(element.label or "")
813
+ if match:
814
+ return as_float(match.group(1))
815
+ entered = typed_amount(element, elements, typed)
816
+ if entered is not None:
817
+ return entered
818
+ held = held_figures(elements)
819
+ shown = [(abs(y - center_y(element)), value) for value, y, e in screen_amounts(elements)
820
+ if e is not element and (id(e), value) not in held]
821
+ return min(shown)[1] if shown else None
822
+
823
+
824
+ def counterparty(element, elements):
825
+ """Name tokens of the person a money commit goes to: the nearest row holding two capitalized words."""
826
+ best = None
827
+ for other in list(elements) + [element]:
828
+ text = element_text(other)
829
+ words = [w for w in re.findall(r"[A-Z][a-z]+", text) if w.casefold() not in NAME_STOP]
830
+ if len(words) < 2 or not re.search(r"[A-Z][a-z]+ [A-Z][a-z]+", text):
831
+ continue
832
+ distance = 0 if other is element else abs(center_y(other) - center_y(element))
833
+ if best is None or distance < best[0]:
834
+ best = (distance, {w.casefold() for w in words})
835
+ return best[1] if best and best[0] <= .4 else set()
836
+
837
+
838
+ # ---------------------------------------------------------------- items
839
+
840
+ @dataclass
841
+ class Item:
842
+ id: int
843
+ kind: str
844
+ app: str = ""
845
+ what: str = ""
846
+ payload: str = ""
847
+ act: str = "none"
848
+ count: str = ""
849
+ condition: str = ""
850
+ quote: str = ""
851
+ status: str = "open" # open | closed (commits) | found | missing (reports)
852
+ value: str = None # a REPORT's recorded value
853
+ value_step: int = None
854
+ claim: str = None # the model's own mark: done | missing
855
+ receipts: list = field(default_factory=list)
856
+ executions: int = 0
857
+ reopens: int = 0
858
+ unproven: bool = False
859
+ reason: str = ""
860
+ already: tuple = None # (step, row, whole) the model cited as showing a WRITE was already so
861
+
862
+ def family(self):
863
+ fam = set(FAMILIES.get(self.act, ()))
864
+ if self.act == "other":
865
+ # A commit the families do not name ("reset my filters", "save it") allows the verbs its
866
+ # own verbatim quote uses.
867
+ fam |= {v.casefold() for v in COMMIT_WORDS.findall(self.quote)}
868
+ elif self.kind == "COMMIT":
869
+ # The request's own word for the act names it too: "Request a CityRideX" compiled as an order
870
+ # met a "Request CityRideX · $12.80" button the order family refused 5-21 times a run (iOSWorld
871
+ # A/A, 10 Oct: 6 of 37 failures). The quote is verbatim in the request (from_items drops one that
872
+ # is not), and only its first commit word is taken: the word that asks for this act, not one of a
873
+ # later clause the quote ran into.
874
+ first = COMMIT_WORDS.search(self.quote)
875
+ if first:
876
+ fam.add(" ".join(first.group(1).casefold().split()))
877
+ return fam
878
+
879
+ def needed(self):
880
+ """Receipts that close the item: None for "each" (closed only by the model's mark)."""
881
+ count = str(self.count or "1").strip().casefold()
882
+ if count in ("each", "all", "any", "every"):
883
+ return None
884
+ return int(count) if count.isdigit() and int(count) > 0 else 1
885
+
886
+ def label(self):
887
+ where = f" ({self.app})" if self.app else ""
888
+ act = f" {self.act}" if self.kind in ("COMMIT", "FORBID") and self.act not in ("none", "") else ""
889
+ extra = (f" [{self.count}]" if self.kind == "COMMIT" and str(self.count).strip() not in ("", "1") else "") \
890
+ + (f" if {self.condition}" if self.condition else "") \
891
+ + (f' = "{self.payload}"' if self.payload else "")
892
+ return f"{self.kind}{act}{where}: {self.what}{extra}"
893
+
894
+
895
+ @dataclass
896
+ class Gate:
897
+ """A commit check's outcome. ``item`` is the declared commit it serves (None: not a commit)."""
898
+ allowed: bool
899
+ item: Item = None
900
+ reason: str = ""
901
+ verbs: frozenset = frozenset()
902
+ amount: float = None
903
+ names: frozenset = frozenset()
904
+ payload: str = None
905
+
906
+
907
+ @dataclass(frozen=True)
908
+ class Control:
909
+ """A control as one screen showed it: where it was, what it said, whether it was on, and its count."""
910
+ role: str
911
+ rect: tuple
912
+ label: str
913
+ value: str
914
+ selected: bool
915
+ count: int = None
916
+
917
+
918
+ def shown_count(element, elements, rect=None):
919
+ """The count a control shows (``rect``: where it was): its own label's or value's last number ("Like, 7"), else
920
+ the nearest count just right of it in its row ("7" beside a heart), else None."""
921
+ for text in (getattr(element, "value", "") or "", element.label or "") if element is not None else ():
922
+ found = re.search(r"(?:^|[\s,])(\d{1,3}(?:,\d{3})+|\d+)(?:\s+(?:likes?|reactions?|hearts?|followers?))?\s*$",
923
+ " ".join(text.split()), re.I)
924
+ if found:
925
+ return int(found.group(1).replace(",", ""))
926
+ x, y, w, h = rect or element.rect
927
+ beside = []
928
+ for other in elements:
929
+ if other is element or getattr(other, "editable", False):
930
+ continue
931
+ ox, oy, ow, oh = other.rect
932
+ text = " ".join((other.label or getattr(other, "value", "") or "").split())
933
+ found = COUNT.match(text)
934
+ if found and abs(oy + oh / 2 - (y + h / 2)) <= max(h / 2, .015) and x + w - .01 <= ox <= x + w + .08:
935
+ beside.append((ox - (x + w), int(found.group(1).replace(",", ""))))
936
+ return min(beside)[1] if beside else None
937
+
938
+
939
+ def control_state(element, elements):
940
+ return Control(element.role, tuple(element.rect), " ".join((element.label or "").split()),
941
+ " ".join((getattr(element, "value", "") or "").split()), bool(getattr(element, "selected", False)),
942
+ shown_count(element, elements))
943
+
944
+
945
+ def is_on(act, element):
946
+ """Whether a like's or a follow's control already shows its act on: selected, or its words say so."""
947
+ on = STATE_ON.get(act)
948
+ return bool(getattr(element, "selected", False) or on and (on.search(element.label or "") or on.search(
949
+ getattr(element, "value", "") or "")))
950
+
951
+
952
+ def same_control(control, elements):
953
+ """The elements of a later screen where ``control`` was: its role (or another toggle's), its centre within half
954
+ its size (at least .03 across, .02 down) of where it was."""
955
+ x, y, w, h = control.rect
956
+ cx, cy = x + w / 2, y + h / 2
957
+ out = []
958
+ for e in elements:
959
+ ex, ey, ew, eh = e.rect
960
+ if (e.role == control.role or e.role in TOGGLE_ROLES and control.role in TOGGLE_ROLES) and not getattr(
961
+ e, "editable", False) and abs(ex + ew / 2 - cx) <= max(w / 2, .03) and abs(ey + eh / 2 - cy) <= max(
962
+ h / 2, .02):
963
+ out.append(e)
964
+ return out
965
+
966
+
967
+ @dataclass
968
+ class Pending:
969
+ item: Item
970
+ label: str
971
+ app: str
972
+ step: int
973
+ before: frozenset
974
+ payload: str = None
975
+ amount: float = None
976
+ names: frozenset = frozenset()
977
+ title: str = None # the screen's title at the commit: a message is proven in that thread (O2)
978
+ composer: str = "" # the composer's own label (folded): only that field holding the text is unsent
979
+ hits: int = 0 # rows outside the composer that held the text before the commit
980
+ verbs: frozenset = frozenset() # the commit verbs the tapped control named ("request" in "Request CityRideX")
981
+ control: Control = None # a state act's tapped control as it was before the tap (STATE_ACTS)