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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- 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)
|