mobile-debug-mcp 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +23 -2
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +158 -124
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +20 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +79 -0
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/guard.js +30 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +304 -0
- package/dist/debugger/worker.js +176 -0
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +65 -3074
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +42 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +122 -76
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +13 -2
- package/dist/observe/readiness.js +44 -18
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace-android.js +135 -0
- package/dist/observe/system-trace.js +294 -0
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/handlers/debugger-handlers.js +48 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +69 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +160 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/schemas/debugger-schema.js +47 -0
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
- package/dist/server/schemas/system-trace-schema.js +17 -0
- package/dist/server/tool-definitions.js +62 -1134
- package/dist/server-core.js +4 -4
- package/dist/server.js +34 -0
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -438
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/exec.js +2 -0
- package/dist/utils/ios/utils.js +45 -46
- package/dist/utils/java.js +75 -57
- package/dist/utils/operation-budget.js +2 -0
- package/dist/utils/resolve-device.js +6 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +48 -0
- package/docs/README.md +38 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +118 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
- package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
- package/docs/specs/023-android-system-traces/intent.md +41 -0
- package/docs/specs/023-android-system-traces/plan.md +115 -0
- package/docs/specs/023-android-system-traces/spec.md +122 -0
- package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
- package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
- package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/tools/TOOLS.md +31 -16
- package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
- package/docs/tools/debugger.md +180 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +64 -25
- package/docs/tools/system-traces.md +134 -0
- package/docs/tools/system.md +12 -5
- package/package.json +11 -8
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +66 -0
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +31 -0
- package/src/debugger/java/DebuggerWorker.java +166 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +327 -0
- package/src/debugger/worker.ts +135 -0
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +76 -3491
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +65 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +130 -71
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +10 -3
- package/src/observe/readiness.ts +49 -22
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace-android.ts +133 -0
- package/src/observe/system-trace.ts +227 -0
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +39 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +79 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +159 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +43 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
- package/src/server/schemas/system-trace-schema.ts +17 -0
- package/src/server/tool-definitions.ts +69 -1140
- package/src/server-core.ts +4 -4
- package/src/server.ts +25 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -523
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/exec.ts +2 -0
- package/src/utils/ios/utils.ts +57 -61
- package/src/utils/java.ts +71 -51
- package/src/utils/operation-budget.ts +2 -0
- package/src/utils/resolve-device.ts +11 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +219 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +104 -0
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3536 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +67 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/NoLocals.java +7 -0
- package/test/fixtures/debugger-app/README.md +65 -0
- package/test/fixtures/debugger-app/build.ts +39 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/perfetto.ts +59 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +50 -0
- package/test/unit/debugger/native.test.ts +33 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +83 -0
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/readiness.test.ts +3 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/observe/system-trace-android.test.ts +52 -0
- package/test/unit/observe/system-trace.test.ts +166 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/system-trace.test.ts +64 -0
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -893
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/tool-handlers.ts +0 -923
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DebuggerError, classify, diagnostic, atStage } from './diagnostics.js'
|
|
2
|
+
export { DebuggerError } from './diagnostics.js'
|
|
3
|
+
import { parseWorkerMessage, type WorkerEvent, type WorkerReply } from './protocol.js'
|
|
4
|
+
export type { WorkerEvent } from './protocol.js'
|
|
5
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
6
|
+
import { execFile, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
7
|
+
import { promisify } from 'node:util'
|
|
8
|
+
import { readFile, mkdtemp, chmod, rm } from 'node:fs/promises'
|
|
9
|
+
import { createHash } from 'node:crypto'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { detectJavaHome } from '../utils/java.js'
|
|
14
|
+
|
|
15
|
+
const exec = promisify(execFile)
|
|
16
|
+
const operations = new AsyncLocalStorage<{ deadline: number; signal?: AbortSignal }>()
|
|
17
|
+
export function withDebuggerBudget<T>(timeout: number, signal: AbortSignal | undefined, operation: () => Promise<T>) {
|
|
18
|
+
return operations.run({ deadline: performance.now() + timeout, signal }, operation)
|
|
19
|
+
}
|
|
20
|
+
export function debuggerRemaining(fallback: number) {
|
|
21
|
+
const context = operations.getStore()
|
|
22
|
+
if (context?.signal?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
|
|
23
|
+
const remaining = Math.floor((context?.deadline ?? Infinity) - performance.now())
|
|
24
|
+
if (remaining <= 0) throw new DebuggerError('DEBUGGER_TIMEOUT')
|
|
25
|
+
return Math.min(fallback, remaining)
|
|
26
|
+
}
|
|
27
|
+
export function debuggerSignal() { return operations.getStore()?.signal }
|
|
28
|
+
export function nativeError(error: unknown, fallback: string): DebuggerError {
|
|
29
|
+
if (error instanceof DebuggerError) return error
|
|
30
|
+
if (debuggerSignal()?.aborted) return new DebuggerError('DEBUGGER_CANCELLED')
|
|
31
|
+
if (typeof error === 'object' && error !== null && 'killed' in error && error.killed) return new DebuggerError('DEBUGGER_TIMEOUT')
|
|
32
|
+
return new DebuggerError(fallback)
|
|
33
|
+
}
|
|
34
|
+
export const workerSource = fileURLToPath(new URL('../../src/debugger/java/DebuggerWorker.java', import.meta.url))
|
|
35
|
+
let compiled: Promise<{ java: string; directory: string }> | undefined
|
|
36
|
+
export async function prepareWorker() {
|
|
37
|
+
let stage: 'prerequisite' | 'compilation' = 'prerequisite'
|
|
38
|
+
compiled ??= (async () => {
|
|
39
|
+
const home = await detectJavaHome({ quiet: true }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', 'unavailable') })
|
|
40
|
+
const java = home ? path.join(home, 'bin', 'java') : 'java'
|
|
41
|
+
const javac = home ? path.join(home, 'bin', 'javac') : 'javac'
|
|
42
|
+
const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', debuggerSignal()?.aborted ? 'cancelled' : typeof e === 'object' && e !== null && 'killed' in e && e.killed ? 'timeout' : 'unavailable') })
|
|
43
|
+
const major = Number(/version "(\d+)/.exec(version.stderr)?.[1])
|
|
44
|
+
if (!(major >= 17)) throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('prerequisite', 'unsupported'))
|
|
45
|
+
stage = 'compilation'
|
|
46
|
+
const source = await readFile(workerSource).catch(e => { throw classify(e, 'compilation', 'DEBUGGER_ATTACH_FAILED', 'unavailable') })
|
|
47
|
+
const key = createHash('sha256').update(source).update(version.stderr).digest('hex').slice(0, 16)
|
|
48
|
+
const directory = await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`)))
|
|
49
|
+
try { await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => chmod(directory, 0o700)); await exec(javac, ['--release', '17', '--add-modules', 'jdk.jdi', '-d', directory, workerSource], { timeout: debuggerRemaining(15000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 }) }
|
|
50
|
+
catch (e) { await rm(directory, { recursive: true, force: true }); throw classify(nativeError(e, 'DEBUGGER_UNSUPPORTED'), 'compilation', 'DEBUGGER_UNSUPPORTED', 'operation_failed') }
|
|
51
|
+
return { java, directory }
|
|
52
|
+
})().catch(error => { compiled = undefined; throw classify(error, stage, 'DEBUGGER_ATTACH_FAILED') })
|
|
53
|
+
return compiled
|
|
54
|
+
}
|
|
55
|
+
function commandStage(command: string) { return command === 'attach' ? 'attach' as const : command === 'detach' ? 'cleanup' as const : 'command' as const }
|
|
56
|
+
export interface DebugWorker {
|
|
57
|
+
call(command: string, args?: Record<string, unknown>, timeout?: number): Promise<WorkerReply>
|
|
58
|
+
close(): Promise<void>
|
|
59
|
+
}
|
|
60
|
+
type WorkerFailureReason = 'spawn_failed' | 'process_exited' | 'write_failed' | 'invalid_message' | 'message_limit'
|
|
61
|
+
export class JdiWorker implements DebugWorker {
|
|
62
|
+
private child: ChildProcessWithoutNullStreams
|
|
63
|
+
private pending = new Map<number, { resolve(value: WorkerReply): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout>; command: string }>()
|
|
64
|
+
private sequence = 0
|
|
65
|
+
private dead = false
|
|
66
|
+
private failure?: DebuggerError
|
|
67
|
+
private replied = false
|
|
68
|
+
private exited: Promise<void>
|
|
69
|
+
constructor(java: string, directory: string, private onEvent: (event: WorkerEvent) => void) {
|
|
70
|
+
this.child = spawn(java, ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
71
|
+
let buffer = ''
|
|
72
|
+
this.exited = new Promise(resolve => { this.child.once('exit', () => { this.fail('process_exited'); resolve() }); this.child.once('error', () => { this.fail('spawn_failed'); resolve() }) })
|
|
73
|
+
this.child.stdin.on('error', () => this.fail('write_failed'))
|
|
74
|
+
this.child.stderr.resume() // Never copy worker data or VM diagnostics into MCP stdout/logs.
|
|
75
|
+
this.child.stdout.setEncoding('utf8')
|
|
76
|
+
this.child.stdout.on('data', (chunk: string) => {
|
|
77
|
+
buffer += chunk
|
|
78
|
+
if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) { this.fail('message_limit'); this.child.kill('SIGKILL'); return }
|
|
79
|
+
let end: number
|
|
80
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
81
|
+
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1)
|
|
82
|
+
try {
|
|
83
|
+
if (Buffer.byteLength(line) > 1024 * 1024) throw new Error('IPC_LIMIT')
|
|
84
|
+
const msg = parseWorkerMessage(JSON.parse(line))
|
|
85
|
+
if (msg.kind === 'event') onEvent(msg.value)
|
|
86
|
+
else {
|
|
87
|
+
this.replied = true
|
|
88
|
+
const request = this.pending.get(msg.id)
|
|
89
|
+
if (!request) continue
|
|
90
|
+
clearTimeout(request.timer); this.pending.delete(msg.id)
|
|
91
|
+
if (msg.error) request.reject(classify(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED', msg.error.diagnostic), commandStage(request.command), 'DEBUGGER_WORKER_FAILED'))
|
|
92
|
+
else request.resolve(msg.result)
|
|
93
|
+
}
|
|
94
|
+
} catch { this.fail(Buffer.byteLength(line) > 1024 * 1024 ? 'message_limit' : 'invalid_message'); this.child.kill('SIGKILL'); return }
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
private fail(reason: WorkerFailureReason) {
|
|
99
|
+
if (this.dead) return
|
|
100
|
+
this.dead = true
|
|
101
|
+
const stage = reason === 'spawn_failed' || (reason === 'process_exited' && !this.replied) ? 'worker_start' : 'ipc'
|
|
102
|
+
this.failure = new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic(stage, reason))
|
|
103
|
+
for (const request of this.pending.values()) {
|
|
104
|
+
clearTimeout(request.timer)
|
|
105
|
+
request.reject(this.failure)
|
|
106
|
+
}
|
|
107
|
+
this.pending.clear()
|
|
108
|
+
this.onEvent({ event: 'worker_failed', diagnostic: this.failure.diagnostic })
|
|
109
|
+
}
|
|
110
|
+
call(command: string, args: Record<string, unknown> = {}, timeout = 10000): Promise<WorkerReply> {
|
|
111
|
+
if (this.dead) return Promise.reject(this.failure ?? new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'unknown')))
|
|
112
|
+
try { timeout = Math.min(timeout, debuggerRemaining(timeout)) } catch (e) { return Promise.reject(classify(e, commandStage(command), 'DEBUGGER_WORKER_FAILED')) }
|
|
113
|
+
const id = ++this.sequence
|
|
114
|
+
const message = JSON.stringify({ ...args, command, id }) + '\n'
|
|
115
|
+
if (Buffer.byteLength(message) > 1024 * 1024) return Promise.reject(new DebuggerError('INVALID_ARGUMENT'))
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const signal = debuggerSignal()
|
|
118
|
+
const abort = () => { this.pending.delete(id); clearTimeout(timer); done(); reject(new DebuggerError('DEBUGGER_CANCELLED', diagnostic(commandStage(command), 'cancelled'))) }
|
|
119
|
+
const done = () => signal?.removeEventListener('abort', abort)
|
|
120
|
+
const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT', diagnostic(commandStage(command), 'timeout'))) }, timeout)
|
|
121
|
+
this.pending.set(id, { command, resolve: value => { done(); resolve(value) }, reject: error => { done(); reject(error) }, timer })
|
|
122
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
123
|
+
try { this.child.stdin.write(message) } catch { this.fail('write_failed') }
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
async close() {
|
|
127
|
+
this.child.stdin.end()
|
|
128
|
+
const kill = setTimeout(() => this.child.kill('SIGKILL'), 2000)
|
|
129
|
+
try { await this.exited } finally { clearTimeout(kill) }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function createWorker(onEvent: (event: WorkerEvent) => void) {
|
|
133
|
+
const { java, directory } = await prepareWorker()
|
|
134
|
+
return new JdiWorker(java, directory, onEvent)
|
|
135
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { systemClock, type Clock } from '../../runtime/clock.js'
|
|
2
|
+
import type { ActionFailureCode, ActionTargetResolved, AdjustControlResponse, DeviceInfo, FindElementResponse, TraceStep } from '../../types.js'
|
|
3
|
+
import type { UiSelector } from '../../contracts/selector.js'
|
|
4
|
+
import type { UiElement, ResolvedUiElementContext } from '../model.js'
|
|
5
|
+
import type { InputService } from '../input-service.js'
|
|
6
|
+
import type { AdjustmentVerificationDependencies } from './adjustment-verification.js'
|
|
7
|
+
import { createTraceStep, buildActionExecutionResult } from '../../runtime/action-results.js'
|
|
8
|
+
import { completeAdjustment } from './adjustment-result.js'
|
|
9
|
+
import { planAdjustment } from './adjustment-plan.js'
|
|
10
|
+
|
|
11
|
+
export interface AdjustmentInput { selector?: UiSelector; element_id?: string; property?: string; targetValue: number; tolerance?: number; maxAttempts?: number; platform?: 'android' | 'ios'; deviceId?: string }
|
|
12
|
+
export interface ControlTree { elements?: UiElement[]; device?: DeviceInfo }
|
|
13
|
+
export type Match = { el: UiElement; idx: number }
|
|
14
|
+
export interface ControlMatch { tree: ControlTree | null; device?: DeviceInfo; match: Match | null; resolvedTarget: ActionTargetResolved | null }
|
|
15
|
+
export interface ControlWait { status?: string; element?: { elementId?: string | null } | null; device?: DeviceInfo; error?: { code?: string; message?: string } }
|
|
16
|
+
export interface AdjustmentDependencies {
|
|
17
|
+
clock?: Clock
|
|
18
|
+
observe(platform: 'android' | 'ios', deviceId?: string): Promise<ControlTree>
|
|
19
|
+
fingerprint(platform: 'android' | 'ios', deviceId?: string): Promise<string | null>
|
|
20
|
+
getHandle(id: string): ResolvedUiElementContext | undefined
|
|
21
|
+
currentHandle(elements: UiElement[], platform: 'android' | 'ios', deviceId: string | undefined, handle: ResolvedUiElementContext): { el: UiElement; index: number } | null
|
|
22
|
+
target(platform: 'android' | 'ios', deviceId: string | undefined, match: Match): ActionTargetResolved
|
|
23
|
+
wait(input: { selector: UiSelector; condition: 'clickable'; timeout_ms: number; poll_interval_ms: number; platform: 'android' | 'ios'; deviceId?: string }): Promise<ControlWait | undefined>
|
|
24
|
+
find(input: { query: string; exact: boolean; timeoutMs: number; platform: 'android' | 'ios'; deviceId?: string }): Promise<FindElementResponse>
|
|
25
|
+
expectState: AdjustmentVerificationDependencies['expectState']
|
|
26
|
+
service(platform: 'android' | 'ios', deviceId?: string): Promise<{ interact: Pick<InputService, 'setAdjustableValue' | 'adjustAccessibleValue'>; resolved: { id: string } }>
|
|
27
|
+
tap(input: { platform: 'android' | 'ios'; x: number; y: number; deviceId?: string }): Promise<{ success: boolean; device?: DeviceInfo }>
|
|
28
|
+
swipe(input: { platform: 'android' | 'ios'; x1: number; y1: number; x2: number; y2: number; duration: number; deviceId?: string }): Promise<{ success: boolean; device?: DeviceInfo; error?: string }>
|
|
29
|
+
}
|
|
30
|
+
export type ActualState = AdjustControlResponse['actual_state']
|
|
31
|
+
export type Mode = AdjustControlResponse['adjustment_mode']
|
|
32
|
+
export type Verification = { verification: { reason?: string } | null; observedState: ActualState; withinTolerance: boolean }
|
|
33
|
+
export interface AdjustmentState {
|
|
34
|
+
clock: Clock
|
|
35
|
+
input: AdjustmentInput & { property: string }
|
|
36
|
+
platform: 'android' | 'ios'; deviceId?: string; stored: ResolvedUiElementContext | null
|
|
37
|
+
selector: Record<string, unknown> | null; tolerance: number | null; limit: number; before: string | null
|
|
38
|
+
fallback: FindElementResponse['element'] | null; trace: TraceStep[]; sequence: number
|
|
39
|
+
observed: ActualState; mode: Mode; target: ActionTargetResolved | null; device?: DeviceInfo
|
|
40
|
+
attempts: number; cached: Match | null; coordinatePending: boolean
|
|
41
|
+
}
|
|
42
|
+
export type Iteration = ReturnType<typeof planAdjustment> & {
|
|
43
|
+
element: UiElement; index: number; actionDevice?: DeviceInfo; observed: ActualState; verification: Verification
|
|
44
|
+
}
|
|
45
|
+
export type StrategyOutcome = 'next' | 'retry' | AdjustControlResponse
|
|
46
|
+
|
|
47
|
+
export async function createAdjustment(input: AdjustmentInput, deps: AdjustmentDependencies): Promise<AdjustmentState> {
|
|
48
|
+
const platform = input.platform || 'android'
|
|
49
|
+
const tolerance = typeof input.tolerance === 'number' && Number.isFinite(input.tolerance) ? Math.max(0, input.tolerance) : null
|
|
50
|
+
const limit = input.maxAttempts == null ? 5 : Math.max(1, Math.floor(Number(input.maxAttempts) || 1))
|
|
51
|
+
const stored = input.element_id ? deps.getHandle(input.element_id) ?? null : null
|
|
52
|
+
const before = await deps.fingerprint(platform, input.deviceId)
|
|
53
|
+
return { clock: deps.clock ?? systemClock, input: { ...input, property: input.property === undefined ? 'value' : input.property }, platform, deviceId: input.deviceId, stored,
|
|
54
|
+
selector: input.selector ?? (input.element_id ? { elementId: input.element_id } : null), tolerance, limit, before,
|
|
55
|
+
fallback: null, trace: [], sequence: 0, observed: null, mode: 'gesture', target: null,
|
|
56
|
+
attempts: 0, cached: null, coordinatePending: false }
|
|
57
|
+
}
|
|
58
|
+
export function recordAdjustment(s: AdjustmentState, stage: TraceStep['stage'], result: TraceStep['result'], metadata?: Record<string, unknown>) {
|
|
59
|
+
s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }))
|
|
60
|
+
}
|
|
61
|
+
export function failAdjustment(s: AdjustmentState, code: ActionFailureCode, reason: string, retryable = false,
|
|
62
|
+
target = s.target, device = s.device, observed = s.observed, attempts = s.attempts, mode: Mode = s.mode, after: string | null = null): AdjustControlResponse {
|
|
63
|
+
if (!s.trace.some(step => step.stage === 'resolve')) recordAdjustment(s, 'resolve', 'failure', { reason, failure_code: code })
|
|
64
|
+
if (!s.trace.some(step => step.stage === 'recover')) recordAdjustment(s, 'recover', retryable ? 'retry' : 'failure', {
|
|
65
|
+
reason, failure_code: code, retry_allowed: retryable, recovery_attempts: attempts, retry_depth: attempts })
|
|
66
|
+
const tolerance = s.tolerance ?? 0
|
|
67
|
+
const base = buildActionExecutionResult({ actionType: 'adjust_control', sourceModule: 'interact', device,
|
|
68
|
+
selector: s.selector, resolved: target, success: false, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
|
|
69
|
+
failure: { failureCode: code, retryable }, traceSteps: s.trace,
|
|
70
|
+
details: { target_value: s.input.targetValue, tolerance, property: s.input.property, attempts,
|
|
71
|
+
adjustment_mode: mode, actual_state: observed, converged: false, within_tolerance: false, reason } })
|
|
72
|
+
return { ...base, target_state: { property: s.input.property, target_value: s.input.targetValue, tolerance },
|
|
73
|
+
actual_state: observed, within_tolerance: false, converged: false, attempts, adjustment_mode: mode }
|
|
74
|
+
}
|
|
75
|
+
export async function succeedAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies,
|
|
76
|
+
reason = 'control converged to target value', mode = s.mode, device = p.actionDevice ?? s.device) {
|
|
77
|
+
const after = await deps.fingerprint(s.platform, s.deviceId)
|
|
78
|
+
return completeAdjustment({ actionType: 'adjust_control', sourceModule: 'interact', device,
|
|
79
|
+
selector: s.selector, resolved: s.target, success: true, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
|
|
80
|
+
traceSteps: s.trace, details: { target_value: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
|
|
81
|
+
property: s.input.property, attempts: s.attempts, adjustment_mode: mode, actual_state: p.observed,
|
|
82
|
+
converged: true, within_tolerance: true, reason } })
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { _controlAxis, _buildConservativeControlPoint, _buildControlPoint, _buildAdjustmentProbePoints } from './control-geometry.js'
|
|
2
|
+
import { recordAdjustment, succeedAdjustment, failAdjustment, type AdjustmentState, type AdjustmentDependencies, type Iteration, type StrategyOutcome } from './adjustment-context.js'
|
|
3
|
+
import { verifyControl } from './adjustment-semantic.js'
|
|
4
|
+
|
|
5
|
+
function points(p: Iteration) {
|
|
6
|
+
const bounds = p.bounds!, range = p.valueRange!, value = p.currentValue!
|
|
7
|
+
const axis = _controlAxis(p.element, bounds)
|
|
8
|
+
return { axis, target: _buildConservativeControlPoint(bounds, p.normalizedTargetValue, value, range.min, range.max, axis),
|
|
9
|
+
current: _buildControlPoint(bounds, (value - range.min) / (range.max - range.min), axis) }
|
|
10
|
+
}
|
|
11
|
+
export async function coordinateAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
|
|
12
|
+
if (s.coordinatePending) return 'next'
|
|
13
|
+
const geometry = points(p)
|
|
14
|
+
const probes = p.inferredSliderState ? [geometry.target] : _buildAdjustmentProbePoints(p.bounds!, p.normalizedTargetValue, p.currentValue!, p.valueRange!.min, p.valueRange!.max, geometry.axis)
|
|
15
|
+
for (let i = 0; i < probes.length && s.attempts < s.limit; i++) {
|
|
16
|
+
const point = probes[i]
|
|
17
|
+
s.mode = 'coordinate'
|
|
18
|
+
recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'coordinate', point })
|
|
19
|
+
const action = await deps.tap({ platform: s.platform, x: point.x, y: point.y, deviceId: s.deviceId })
|
|
20
|
+
s.attempts++; p.actionDevice = action.device ?? p.actionDevice
|
|
21
|
+
if (!action.success) {
|
|
22
|
+
recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts, mode: 'coordinate', point, success: false }); continue
|
|
23
|
+
}
|
|
24
|
+
await verifyControl(s, p, deps, 'control did not converge yet')
|
|
25
|
+
if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
|
|
26
|
+
s.coordinatePending = true; s.cached = null
|
|
27
|
+
if (s.attempts < s.limit) return 'retry'
|
|
28
|
+
}
|
|
29
|
+
return 'next'
|
|
30
|
+
}
|
|
31
|
+
export async function gestureAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
|
|
32
|
+
if (s.attempts >= s.limit) return 'next'
|
|
33
|
+
const geometry = points(p)
|
|
34
|
+
s.coordinatePending = false; s.mode = 'gesture'
|
|
35
|
+
recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'gesture', start: geometry.current, end: geometry.target })
|
|
36
|
+
const action = await deps.swipe({ platform: s.platform, x1: geometry.current.x, y1: geometry.current.y,
|
|
37
|
+
x2: geometry.target.x, y2: geometry.target.y, duration: p.inferredSliderState ? 520 : 220, deviceId: s.deviceId })
|
|
38
|
+
s.attempts++
|
|
39
|
+
if (!action.success) {
|
|
40
|
+
recordAdjustment(s, 'execute', 'failure', { attempt: s.attempts, mode: 'gesture', start: geometry.current, end: geometry.target, success: false })
|
|
41
|
+
return failAdjustment(s, 'UNKNOWN', action.error ?? 'adjustment gesture failed', false, s.target,
|
|
42
|
+
action.device ?? p.actionDevice, p.observed ?? p.actualState)
|
|
43
|
+
}
|
|
44
|
+
p.actionDevice = action.device ?? p.actionDevice
|
|
45
|
+
await verifyControl(s, p, deps, 'gesture adjustment did not converge yet')
|
|
46
|
+
if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
|
|
47
|
+
return 'next'
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { UiElement } from '../model.js'
|
|
2
|
+
import type { AdjustControlResponse } from '../../types.js'
|
|
3
|
+
import { _normalizeBounds } from '../targeting/element-helpers.js'
|
|
4
|
+
import { _inferLabeledSliderState } from './slider-labels.js'
|
|
5
|
+
import { _readNumericControlRange, _readControlStep, _quantizeTargetValue,
|
|
6
|
+
_defaultAdjustTolerance, _readNumericControlValue } from './control-values.js'
|
|
7
|
+
|
|
8
|
+
export function planAdjustment(currentEl: UiElement, elements: UiElement[], targetValue: number,
|
|
9
|
+
property: string, requestedTolerance: number | null) {
|
|
10
|
+
const inferredSliderState = _inferLabeledSliderState(elements, currentEl)
|
|
11
|
+
const nativeBounds = _normalizeBounds(currentEl.bounds)
|
|
12
|
+
const bounds = inferredSliderState?.touch_bounds ?? nativeBounds
|
|
13
|
+
const valueRange = _readNumericControlRange(currentEl) ?? inferredSliderState?.value_range ?? null
|
|
14
|
+
const step = _readControlStep(currentEl)
|
|
15
|
+
const normalizedTargetValue = valueRange ? _quantizeTargetValue(targetValue, valueRange.min, valueRange.max, step) : targetValue
|
|
16
|
+
const effectiveTolerance = requestedTolerance ?? _defaultAdjustTolerance(valueRange, step)
|
|
17
|
+
const currentValue = _readNumericControlValue(currentEl, property) ?? inferredSliderState?.current ?? null
|
|
18
|
+
const actualState: AdjustControlResponse['actual_state'] = currentValue !== null ? {
|
|
19
|
+
property, value: currentValue,
|
|
20
|
+
raw_value: typeof currentEl.state?.raw_value === 'number' ? currentEl.state.raw_value : inferredSliderState?.current
|
|
21
|
+
} : null
|
|
22
|
+
return { inferredSliderState, bounds, valueRange, normalizedTargetValue, effectiveTolerance, currentValue, actualState }
|
|
23
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { UiSelector } from '../../contracts/selector.js'
|
|
2
|
+
import type { ControlWait } from './adjustment-context.js'
|
|
3
|
+
import type { FindElementResponse } from '../../types.js'
|
|
4
|
+
import type { UiElement } from '../model.js'
|
|
5
|
+
import { _normalizeBounds, _isAdjustableControl, _findFirstMatchingElement, _isAdjustableActionable } from '../targeting/element-helpers.js'
|
|
6
|
+
import { planAdjustment } from './adjustment-plan.js'
|
|
7
|
+
import { failAdjustment, recordAdjustment, succeedAdjustment, type AdjustmentState, type AdjustmentDependencies, type ControlMatch, type Iteration } from './adjustment-context.js'
|
|
8
|
+
|
|
9
|
+
function fallbackBounds(fallback: FindElementResponse['element']) {
|
|
10
|
+
const bounds = fallback?.bounds
|
|
11
|
+
if (Array.isArray(bounds)) return _normalizeBounds(bounds)
|
|
12
|
+
return bounds && typeof bounds === 'object' ? _normalizeBounds([Number(bounds.left), Number(bounds.top), Number(bounds.right), Number(bounds.bottom)]) : null
|
|
13
|
+
}
|
|
14
|
+
function findFallback(elements: UiElement[], fallback: FindElementResponse['element']) {
|
|
15
|
+
const bounds = fallbackBounds(fallback)
|
|
16
|
+
if (!bounds) return null
|
|
17
|
+
let index = elements.findIndex(el => {
|
|
18
|
+
const current = _normalizeBounds(el.bounds)
|
|
19
|
+
return !!current && current.every((value, i) => value === bounds[i])
|
|
20
|
+
})
|
|
21
|
+
if (index === -1) index = nearestControl(elements, bounds)
|
|
22
|
+
return index >= 0 && elements[index] ? { el: elements[index], idx: index } : null
|
|
23
|
+
}
|
|
24
|
+
function nearestControl(elements: UiElement[], bounds: [number, number, number, number]) {
|
|
25
|
+
const x = Math.floor((bounds[0] + bounds[2]) / 2), y = Math.floor((bounds[1] + bounds[3]) / 2)
|
|
26
|
+
let best = Infinity, index = -1
|
|
27
|
+
for (let i = 0; i < elements.length; i++) {
|
|
28
|
+
const el = elements[i]
|
|
29
|
+
if (!_isAdjustableControl(el)) continue
|
|
30
|
+
const current = _normalizeBounds(el.bounds)
|
|
31
|
+
if (!current) continue
|
|
32
|
+
const distance = Math.abs(Math.floor((current[0] + current[2]) / 2) - x) + Math.abs(Math.floor((current[1] + current[3]) / 2) - y)
|
|
33
|
+
if (distance < best) { best = distance; index = i }
|
|
34
|
+
}
|
|
35
|
+
return index
|
|
36
|
+
}
|
|
37
|
+
export async function resolveControl(s: AdjustmentState, deps: AdjustmentDependencies): Promise<ControlMatch | null> {
|
|
38
|
+
const tree = await deps.observe(s.platform, s.deviceId)
|
|
39
|
+
s.platform = tree?.device?.platform === 'ios' ? 'ios' : s.platform
|
|
40
|
+
s.deviceId = tree?.device?.id || s.deviceId
|
|
41
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : []
|
|
42
|
+
let match = null
|
|
43
|
+
if (s.input.element_id) {
|
|
44
|
+
const stored = deps.getHandle(s.input.element_id)
|
|
45
|
+
if (!stored) return null
|
|
46
|
+
const current = deps.currentHandle(elements, s.platform, s.deviceId, stored)
|
|
47
|
+
if (!current) return null
|
|
48
|
+
match = { el: current.el, idx: current.index }
|
|
49
|
+
} else {
|
|
50
|
+
if (s.fallback) match = findFallback(elements, s.fallback)
|
|
51
|
+
if (!match && s.input.selector) match = _findFirstMatchingElement(elements, s.input.selector)
|
|
52
|
+
}
|
|
53
|
+
return match ? { tree, device: tree?.device, match, resolvedTarget: deps.target(s.platform, s.deviceId, match) } : null
|
|
54
|
+
}
|
|
55
|
+
export async function initializeControl(s: AdjustmentState, deps: AdjustmentDependencies) {
|
|
56
|
+
const { selector, element_id } = s.input
|
|
57
|
+
if ((!selector && !element_id) || (selector && element_id)) {
|
|
58
|
+
return failAdjustment(s, 'ELEMENT_NOT_FOUND', 'exactly one of selector or element_id is required', false, null, undefined, null, 0, 'gesture')
|
|
59
|
+
}
|
|
60
|
+
if (!selector || element_id) return null
|
|
61
|
+
const wait = await deps.wait({ selector, condition: 'clickable', timeout_ms: 5000, poll_interval_ms: 300, platform: s.platform, deviceId: s.deviceId })
|
|
62
|
+
if (wait?.status === 'success' && wait?.element?.elementId) {
|
|
63
|
+
s.input.element_id = wait.element.elementId; s.fallback = null; return null
|
|
64
|
+
}
|
|
65
|
+
return initializeFallback(s, deps, wait, selector)
|
|
66
|
+
}
|
|
67
|
+
async function initializeFallback(s: AdjustmentState, deps: AdjustmentDependencies, wait: ControlWait | undefined, selector: UiSelector) {
|
|
68
|
+
const query = selector.text ?? selector.resource_id ?? selector.accessibility_id ?? ''
|
|
69
|
+
if (!query) return failAdjustment(s, wait?.error?.code === 'ELEMENT_NOT_FOUND' ? 'ELEMENT_NOT_FOUND' : 'TIMEOUT',
|
|
70
|
+
wait?.error?.message ?? 'adjustable control not found', wait?.error?.code === 'ELEMENT_NOT_FOUND', null, wait?.device, null, 0, 'gesture')
|
|
71
|
+
const fallback = await deps.find({ query, exact: false, timeoutMs: 3000, platform: s.platform, deviceId: s.deviceId })
|
|
72
|
+
if (!fallback.found || !fallback.element) return failAdjustment(s, 'ELEMENT_NOT_FOUND', wait?.error?.message ?? 'adjustable control not found', true, null, wait?.device, null, 0, 'gesture')
|
|
73
|
+
s.fallback = fallback.element
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
export async function prepareIteration(s: AdjustmentState, deps: AdjustmentDependencies) {
|
|
77
|
+
const resolved = s.cached ? { tree: null, device: s.device, match: s.cached, resolvedTarget: deps.target(s.platform, s.deviceId, s.cached) } : await resolveControl(s, deps)
|
|
78
|
+
if (!resolved?.match || !resolved.resolvedTarget) return failAdjustment(s, 'STALE_REFERENCE', 'adjustable control could not be resolved', true)
|
|
79
|
+
s.device = resolved.device; s.target = resolved.resolvedTarget; s.cached = resolved.match
|
|
80
|
+
const element = resolved.match.el
|
|
81
|
+
const actionable = _isAdjustableActionable(element, s.stored?.stable_id)
|
|
82
|
+
if (!actionable.actionable) return failAdjustment(s, actionable.failureCode ?? 'ELEMENT_NOT_INTERACTABLE', actionable.reason ?? 'adjustable control is not actionable', true)
|
|
83
|
+
const elements = Array.isArray(resolved.tree?.elements) ? resolved.tree.elements : s.cached ? [s.cached.el] : [element]
|
|
84
|
+
const plan = planAdjustment(element, elements, s.input.targetValue, s.input.property, s.tolerance)
|
|
85
|
+
s.observed = plan.actualState
|
|
86
|
+
if (!s.trace.some(step => step.stage === 'resolve')) recordAdjustment(s, 'resolve', 'success', { resolved_target: s.target, current_value: plan.currentValue, adjustment_mode: s.mode })
|
|
87
|
+
const iteration: Iteration = { ...plan, element, index: resolved.match.idx, actionDevice: s.device, observed: plan.actualState,
|
|
88
|
+
verification: { verification: null, observedState: plan.actualState, withinTolerance: false } }
|
|
89
|
+
const invalid = validateControl(s, iteration)
|
|
90
|
+
if (invalid) return invalid
|
|
91
|
+
if (plan.currentValue !== null && Math.abs(plan.currentValue - plan.normalizedTargetValue) <= plan.effectiveTolerance) {
|
|
92
|
+
recordAdjustment(s, 'verify', 'success', { property: s.input.property, target_value: plan.normalizedTargetValue, actual_state: plan.actualState, reason: 'control already within tolerance' })
|
|
93
|
+
return succeedAdjustment(s, iteration, deps, 'control already within tolerance', 'semantic', s.device)
|
|
94
|
+
}
|
|
95
|
+
if (!plan.bounds) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds')
|
|
96
|
+
return iteration
|
|
97
|
+
}
|
|
98
|
+
function validateControl(s: AdjustmentState, p: Iteration) {
|
|
99
|
+
if (s.input.property !== 'value' && s.input.property !== 'raw_value') return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjust_control currently supports numeric value and raw_value properties only')
|
|
100
|
+
if (!_isAdjustableControl(p.element) && !p.inferredSliderState) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'target is not an adjustable control')
|
|
101
|
+
if (p.currentValue === null) return failAdjustment(s, 'UNKNOWN', 'current numeric value is unavailable')
|
|
102
|
+
if (p.valueRange && (s.input.targetValue < p.valueRange.min || s.input.targetValue > p.valueRange.max)) return failAdjustment(s, 'CONTROL_CONVERGENCE_FAILED', `targetValue ${s.input.targetValue} is outside the control range ${p.valueRange.min}..${p.valueRange.max}`)
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AdjustControlResponse } from '../../types.js'
|
|
2
|
+
import { buildActionExecutionResult } from '../../runtime/action-results.js'
|
|
3
|
+
|
|
4
|
+
type SuccessDetails = {
|
|
5
|
+
property: string
|
|
6
|
+
target_value: number
|
|
7
|
+
tolerance: number
|
|
8
|
+
attempts: number
|
|
9
|
+
adjustment_mode: AdjustControlResponse['adjustment_mode']
|
|
10
|
+
actual_state: AdjustControlResponse['actual_state']
|
|
11
|
+
reason: string
|
|
12
|
+
converged: true
|
|
13
|
+
within_tolerance: true
|
|
14
|
+
}
|
|
15
|
+
type AdjustmentSuccess = Parameters<typeof buildActionExecutionResult>[0] & { details: SuccessDetails }
|
|
16
|
+
|
|
17
|
+
/** Keep the action envelope and top-level control result in agreement. */
|
|
18
|
+
export function completeAdjustment(input: AdjustmentSuccess): AdjustControlResponse {
|
|
19
|
+
const base = buildActionExecutionResult(input)
|
|
20
|
+
const details = input.details
|
|
21
|
+
return {
|
|
22
|
+
...base,
|
|
23
|
+
target_state: {
|
|
24
|
+
property: details.property,
|
|
25
|
+
target_value: details.target_value,
|
|
26
|
+
tolerance: details.tolerance
|
|
27
|
+
},
|
|
28
|
+
actual_state: details.actual_state,
|
|
29
|
+
within_tolerance: true,
|
|
30
|
+
converged: true,
|
|
31
|
+
attempts: details.attempts,
|
|
32
|
+
adjustment_mode: details.adjustment_mode
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { _hasDirectAdjustmentAction, _adjustmentDirections } from './control-values.js'
|
|
2
|
+
import { verifyAdjustment } from './adjustment-verification.js'
|
|
3
|
+
import { recordAdjustment, succeedAdjustment, type AdjustmentState, type AdjustmentDependencies, type Iteration, type StrategyOutcome } from './adjustment-context.js'
|
|
4
|
+
|
|
5
|
+
export async function verifyControl(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies, fallback: string) {
|
|
6
|
+
p.verification = await verifyAdjustment({ labeled: !!p.inferredSliderState, currentEl: p.element, actualState: p.actualState,
|
|
7
|
+
property: s.input.property, target: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
|
|
8
|
+
expectation: { element_id: s.target?.elementId ?? s.input.element_id, selector: s.input.selector ?? undefined,
|
|
9
|
+
property: s.input.property, expected: p.normalizedTargetValue, platform: s.platform, deviceId: s.deviceId }
|
|
10
|
+
}, { observe: () => deps.observe(s.platform, s.deviceId), expectState: deps.expectState })
|
|
11
|
+
p.observed = p.verification.observedState; s.observed = p.observed
|
|
12
|
+
recordAdjustment(s, 'verify', p.verification.withinTolerance ? 'success' : 'retry', {
|
|
13
|
+
attempt: s.attempts, property: s.input.property, target_value: p.normalizedTargetValue,
|
|
14
|
+
actual_state: p.observed, reason: p.verification.verification?.reason ?? fallback })
|
|
15
|
+
}
|
|
16
|
+
export async function directAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
|
|
17
|
+
if (!_hasDirectAdjustmentAction(p.element) || s.attempts >= s.limit) return 'next'
|
|
18
|
+
const { interact, resolved } = await deps.service(s.platform, s.deviceId)
|
|
19
|
+
if (typeof interact.setAdjustableValue !== 'function') return 'next'
|
|
20
|
+
s.mode = 'semantic'
|
|
21
|
+
recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'semantic', strategy: 'direct_set_value', target_value: p.normalizedTargetValue })
|
|
22
|
+
const action = await interact.setAdjustableValue({ deviceId: resolved.id, target: s.target, property: s.input.property, value: p.normalizedTargetValue })
|
|
23
|
+
s.attempts++; p.actionDevice = action?.device ?? p.actionDevice
|
|
24
|
+
if (action?.success === false) return 'next'
|
|
25
|
+
await verifyControl(s, p, deps, 'direct adjustment did not converge yet')
|
|
26
|
+
if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
|
|
27
|
+
s.cached = null
|
|
28
|
+
return s.attempts < s.limit ? 'retry' : 'next'
|
|
29
|
+
}
|
|
30
|
+
export async function incrementalAdjustment(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies): Promise<StrategyOutcome> {
|
|
31
|
+
const directions = _adjustmentDirections(p.element)
|
|
32
|
+
if ((!directions.increment && !directions.decrement) || s.attempts >= s.limit) return 'next'
|
|
33
|
+
const { interact, resolved } = await deps.service(s.platform, s.deviceId)
|
|
34
|
+
const adjust = typeof interact.adjustAccessibleValue === 'function' ? interact.adjustAccessibleValue.bind(interact) : null
|
|
35
|
+
while (adjust && s.attempts < s.limit) {
|
|
36
|
+
const direction = p.normalizedTargetValue > (p.observed?.value ?? p.currentValue!) ? 'increment' : 'decrement'
|
|
37
|
+
if (!directionAllowed(direction, directions)) break
|
|
38
|
+
s.mode = 'semantic'
|
|
39
|
+
recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'semantic', strategy: 'accessibility_increment_decrement', direction })
|
|
40
|
+
const action = await adjust({ deviceId: resolved.id, target: s.target, property: s.input.property, direction })
|
|
41
|
+
s.attempts++; p.actionDevice = action?.device ?? p.actionDevice
|
|
42
|
+
if (action?.success === false) break
|
|
43
|
+
await verifyControl(s, p, deps, 'incremental adjustment did not converge yet')
|
|
44
|
+
if (p.verification.withinTolerance) return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value')
|
|
45
|
+
s.cached = null
|
|
46
|
+
if (s.attempts < s.limit) return 'retry'
|
|
47
|
+
}
|
|
48
|
+
return 'next'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function directionAllowed(direction: 'increment' | 'decrement', directions: { increment: boolean; decrement: boolean }) {
|
|
52
|
+
return !((direction === 'increment' && !directions.increment) || (direction === 'decrement' && !directions.decrement))
|
|
53
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AdjustControlResponse, ExpectStateResponse } from '../../types.js'
|
|
2
|
+
import type { UiElement } from '../model.js'
|
|
3
|
+
import type { UiSelector } from '../../contracts/selector.js'
|
|
4
|
+
import { _inferLabeledSliderState } from './slider-labels.js'
|
|
5
|
+
import { _parseNumericControlValue } from './control-values.js'
|
|
6
|
+
|
|
7
|
+
interface StateExpectation {
|
|
8
|
+
element_id?: string
|
|
9
|
+
selector?: UiSelector
|
|
10
|
+
property: string
|
|
11
|
+
expected: number
|
|
12
|
+
platform: 'android' | 'ios'
|
|
13
|
+
deviceId?: string
|
|
14
|
+
}
|
|
15
|
+
export interface AdjustmentVerificationInput {
|
|
16
|
+
labeled: boolean
|
|
17
|
+
currentEl: UiElement
|
|
18
|
+
actualState: AdjustControlResponse['actual_state']
|
|
19
|
+
property: string
|
|
20
|
+
target: number
|
|
21
|
+
tolerance: number
|
|
22
|
+
expectation: StateExpectation
|
|
23
|
+
}
|
|
24
|
+
export interface AdjustmentVerificationDependencies {
|
|
25
|
+
observe(): Promise<{ elements?: UiElement[] } | null>
|
|
26
|
+
expectState(input: StateExpectation): Promise<ExpectStateResponse>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Every verification obtains fresh evidence through its supplied observation boundary. */
|
|
30
|
+
export async function verifyAdjustment(input: AdjustmentVerificationInput, dependencies: AdjustmentVerificationDependencies) {
|
|
31
|
+
if (input.labeled) return verifyLabeledControl(input, dependencies)
|
|
32
|
+
const verification = await dependencies.expectState(input.expectation)
|
|
33
|
+
const observedValue = _parseNumericControlValue(verification?.observed_state?.value) ??
|
|
34
|
+
_parseNumericControlValue(verification?.observed_state?.raw_value)
|
|
35
|
+
const observedState = observedValue !== null ? {
|
|
36
|
+
property: input.property, value: observedValue,
|
|
37
|
+
raw_value: typeof verification?.observed_state?.raw_value === 'number' ? verification.observed_state.raw_value : undefined
|
|
38
|
+
} : input.actualState
|
|
39
|
+
return { verification, observedState, withinTolerance: withinTolerance(observedValue, input) }
|
|
40
|
+
}
|
|
41
|
+
async function verifyLabeledControl(input: AdjustmentVerificationInput, dependencies: AdjustmentVerificationDependencies) {
|
|
42
|
+
const tree = await dependencies.observe()
|
|
43
|
+
const elements = Array.isArray(tree?.elements) ? tree.elements : []
|
|
44
|
+
const refreshed = _inferLabeledSliderState(elements, input.currentEl)
|
|
45
|
+
const observedValue = refreshed?.current ?? null
|
|
46
|
+
const observedState = observedValue !== null ? {
|
|
47
|
+
property: input.property, value: observedValue, raw_value: observedValue
|
|
48
|
+
} : input.actualState
|
|
49
|
+
const matched = withinTolerance(observedValue, input)
|
|
50
|
+
return {
|
|
51
|
+
verification: {
|
|
52
|
+
success: matched, observed_state: observedState,
|
|
53
|
+
reason: observedValue !== null ? 'visible labeled value read from nearby slider labels' : 'visible labeled value unavailable'
|
|
54
|
+
},
|
|
55
|
+
observedState, withinTolerance: matched
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function withinTolerance(value: number | null, input: AdjustmentVerificationInput) {
|
|
59
|
+
return value !== null && Math.abs(value - input.target) <= input.tolerance
|
|
60
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AdjustControlResponse } from '../../types.js'
|
|
2
|
+
import { createAdjustment, failAdjustment, recordAdjustment, succeedAdjustment,
|
|
3
|
+
type AdjustmentInput, type AdjustmentDependencies, type AdjustmentState, type Iteration } from './adjustment-context.js'
|
|
4
|
+
import { initializeControl, prepareIteration } from './adjustment-resolution.js'
|
|
5
|
+
import { directAdjustment, incrementalAdjustment } from './adjustment-semantic.js'
|
|
6
|
+
import { coordinateAdjustment, gestureAdjustment } from './adjustment-gestures.js'
|
|
7
|
+
|
|
8
|
+
function cacheObservation(s: AdjustmentState, p: Iteration) {
|
|
9
|
+
s.cached = p.inferredSliderState ? null : {
|
|
10
|
+
el: { ...p.element, state: { ...(p.element.state ?? null), ...(p.observed ? {
|
|
11
|
+
[p.observed.property]: p.observed.value, raw_value: p.observed.raw_value ?? p.observed.value
|
|
12
|
+
} : {}) } }, idx: p.index
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function attempt(s: AdjustmentState, p: Iteration, deps: AdjustmentDependencies) {
|
|
16
|
+
for (const strategy of [directAdjustment, incrementalAdjustment]) {
|
|
17
|
+
const outcome = await strategy(s, p, deps)
|
|
18
|
+
if (outcome !== 'next') return outcome
|
|
19
|
+
}
|
|
20
|
+
if (!p.bounds) return failAdjustment(s, 'ELEMENT_NOT_INTERACTABLE', 'adjustable control has no bounds')
|
|
21
|
+
if (!p.valueRange) return failAdjustment(s, 'UNKNOWN', 'value_range unavailable')
|
|
22
|
+
for (const strategy of [coordinateAdjustment, gestureAdjustment]) {
|
|
23
|
+
const outcome = await strategy(s, p, deps)
|
|
24
|
+
if (outcome !== 'next') return outcome
|
|
25
|
+
}
|
|
26
|
+
s.observed = p.observed
|
|
27
|
+
if (p.verification.withinTolerance) {
|
|
28
|
+
const reason = p.verification.verification?.reason ?? 'control converged to target value'
|
|
29
|
+
recordAdjustment(s, 'verify', 'success', { attempt: s.attempts, property: s.input.property,
|
|
30
|
+
target_value: p.normalizedTargetValue, actual_state: p.observed, reason })
|
|
31
|
+
return succeedAdjustment(s, p, deps, reason)
|
|
32
|
+
}
|
|
33
|
+
cacheObservation(s, p)
|
|
34
|
+
return 'next'
|
|
35
|
+
}
|
|
36
|
+
export async function adjustControl(input: AdjustmentInput, deps: AdjustmentDependencies): Promise<AdjustControlResponse> {
|
|
37
|
+
const state = await createAdjustment(input, deps)
|
|
38
|
+
const invalid = await initializeControl(state, deps)
|
|
39
|
+
if (invalid) return invalid
|
|
40
|
+
for (let index = 0; index < state.limit && state.attempts < state.limit; index++) {
|
|
41
|
+
const prepared = await prepareIteration(state, deps)
|
|
42
|
+
if ('success' in prepared) return prepared
|
|
43
|
+
const outcome = await attempt(state, prepared, deps)
|
|
44
|
+
if (outcome !== 'retry' && outcome !== 'next') return outcome
|
|
45
|
+
}
|
|
46
|
+
const after = await deps.fingerprint(state.platform, state.deviceId)
|
|
47
|
+
return failAdjustment(state, 'CONTROL_CONVERGENCE_FAILED', 'control did not converge within the allotted attempts', true,
|
|
48
|
+
state.target, state.device, state.observed, state.attempts, state.mode, after)
|
|
49
|
+
}
|