mobile-debug-mcp 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +156 -129
- 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 +16 -6
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
- 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 +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +104 -62
- package/dist/debugger/worker.js +53 -42
- 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 -3090
- 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 +114 -75
- 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 +4 -2
- package/dist/observe/readiness.js +29 -10
- 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.js +22 -13
- 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/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
- 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/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- 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/ios/utils.js +45 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +44 -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/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- 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 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +76 -0
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +9 -1
- 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 +2 -1
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +6 -1
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +314 -155
- package/src/debugger/worker.ts +51 -38
- 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 -3492
- 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 +122 -70
- 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 +5 -3
- package/src/observe/readiness.ts +39 -18
- 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.ts +24 -15
- 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/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +1 -1
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- 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/ios/utils.ts +57 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -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 +19 -3
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- 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 +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -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 +4 -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 +1 -1
- 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/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -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/tool_definitions_parity.test.ts +8 -0
- package/dist/server/tool-handlers.js +0 -1000
- 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/debugger-handlers.ts +0 -29
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/dist/server/{system-trace-schema.js → schemas/system-trace-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
package/docs/tools/manage.md
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# App management
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Documentation](../README.md) · [Tool reference](TOOLS.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use these tools to select a device, build and install an app, and control its
|
|
6
|
+
lifecycle.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
After `start_app` or `restart_app`, [inspect the resulting UI](observe.md) and
|
|
9
|
+
[assert the expected screen or element](interact.md#expect_screen). If the app
|
|
10
|
+
is still loading, use a [readiness check](observe.md#app-readiness) or an
|
|
11
|
+
appropriate [UI wait](interact.md#wait_for_ui) before continuing.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## On this page
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
- [List devices](#list_devices).
|
|
16
|
+
- [Build an app](#build_app).
|
|
17
|
+
- [Install an existing artifact](#install_app).
|
|
18
|
+
- [Launch, stop, restart, or reset an app](#start_app--terminate_app--restart_app--reset_app_data).
|
|
14
19
|
|
|
15
20
|
## list_devices
|
|
16
21
|
Enumerate connected Android devices and iOS simulators.
|
|
@@ -51,50 +56,21 @@ Notes:
|
|
|
51
56
|
- Requires `platform`, `projectType`, and `projectPath`.
|
|
52
57
|
- Android builds prefer the project `gradlew` when present.
|
|
53
58
|
- iOS builds honor environment-based Xcode destination and derived-data settings where configured.
|
|
54
|
-
-
|
|
59
|
+
- Standard toolchains are discovered automatically. Use [path overrides](../../README.md#environment-setup) when discovery needs help. Gradle-backed builds require a suitable Java installation.
|
|
55
60
|
- iOS setup typically needs `XCRUN_PATH` and `MCP_IDB_PATH` or `IDB_PATH` when `idb` is not on `PATH`.
|
|
56
61
|
|
|
57
62
|
---
|
|
58
63
|
|
|
59
|
-
## build_and_install (buildAndInstallHandler)
|
|
60
|
-
Orchestrates build then install and returns streamed NDJSON events plus a final result object.
|
|
61
|
-
|
|
62
|
-
This is a documented repository helper, not part of the main public MCP tool list in `toolDefinitions`.
|
|
63
|
-
|
|
64
|
-
Input:
|
|
65
|
-
|
|
66
|
-
```json
|
|
67
|
-
{ "projectPath": "/path/to/project", "platform": "android", "deviceId": "emulator-5554", "projectType": "kmp" }
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
NDJSON events (example stream):
|
|
71
|
-
|
|
72
|
-
```json
|
|
73
|
-
{"type":"build","status":"started","platform":"android"}
|
|
74
|
-
{"type":"build","status":"finished","artifactPath":"/path/to/app.apk"}
|
|
75
|
-
{"type":"install","status":"started","artifactPath":"/path/to/app.apk","deviceId":"emulator-5554"}
|
|
76
|
-
{"type":"install","status":"finished","artifactPath":"/path/to/app.apk","device":{"platform":"android","id":"emulator-5554"}}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Final result:
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
{ "success": true, "artifactPath": "/path/to/app.apk", "device": { "platform": "android", "id": "emulator-5554" }, "output": "Performing Streamed Install\nSuccess" }
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Notes:
|
|
86
|
-
- If `projectType` === `kmp`, the handler prefers Android by default. Set `platform` explicitly to override.
|
|
87
|
-
- If `MCP_DISABLE_AUTODETECT=1`, callers MUST provide `platform` or `projectType`.
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
64
|
## install_app
|
|
92
|
-
Install
|
|
65
|
+
Install a built artifact, or build and install from a project directory.
|
|
66
|
+
`platform`, `projectType`, and `appPath` are required. `projectType` is one of
|
|
67
|
+
`native`, `kmp`, `react-native`, or `flutter`; acceptance of a project type does
|
|
68
|
+
not establish tested support for every framework.
|
|
93
69
|
|
|
94
70
|
Input:
|
|
95
71
|
|
|
96
72
|
```json
|
|
97
|
-
{ "platform": "android", "appPath": "/path/to/app.apk", "deviceId": "emulator-5554" }
|
|
73
|
+
{ "platform": "android", "projectType": "native", "appPath": "/path/to/app.apk", "deviceId": "emulator-5554" }
|
|
98
74
|
```
|
|
99
75
|
|
|
100
76
|
Response:
|
|
@@ -155,4 +131,9 @@ Notes:
|
|
|
155
131
|
- Use `details.observed_app` as the quick decision signal for what the tool actually saw after launch.
|
|
156
132
|
- Android launch feedback usually includes package/activity matching; iOS launch feedback includes launch output and PID when available.
|
|
157
133
|
- When the landing screen is known, use `expect_screen` as the final verification step.
|
|
158
|
-
- If launch timing is uncertain,
|
|
134
|
+
- If launch timing is uncertain, use a UI wait or readiness check before the final assertion.
|
|
135
|
+
|
|
136
|
+
`terminate_app`, `restart_app`, and `reset_app_data` also require `platform` and
|
|
137
|
+
`appId`, with optional `deviceId`. Resetting app data removes application state;
|
|
138
|
+
use it only when a clean app state is intended. Execution controls apply to
|
|
139
|
+
`start_app` and `restart_app`; see [verification modes](interact.md#interaction-execution-controls-rfc-018).
|
package/docs/tools/observe.md
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Observation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Documentation](../README.md) · [Tool reference](TOOLS.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use observations to understand the current UI and investigate unexpected behaviour.
|
|
6
|
+
Screenshots show layout; UI trees expose labels, identifiers, and control state.
|
|
7
|
+
Logs and debug snapshots provide context when the UI does not explain a failure.
|
|
8
|
+
Use [interaction assertions](interact.md#expect_element_visible) to establish an
|
|
9
|
+
expected outcome.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
- supporting synchronization
|
|
9
|
-
- diagnostics when verification fails
|
|
11
|
+
## On this page
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
- Inspect the UI: [screenshot](#capture_screenshot), [UI tree](#get_ui_tree), [current screen](#get_current_screen), [fingerprint](#get_screen_fingerprint).
|
|
14
|
+
- Investigate a failure: [logs](#get_logs), [debug snapshot](#capture_debug_snapshot), [log streams](#start_log_stream--read_log_stream--stop_log_stream).
|
|
15
|
+
- Check whether the UI has settled: [app readiness](#app-readiness).
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Android performance investigations can use [system traces](system-traces.md)
|
|
17
|
-
to bracket a journey and retrieve a local Perfetto artifact.
|
|
17
|
+
To connect an observation to a specific action, use [evidence
|
|
18
|
+
correlation](correlated-evidence.md). For Android performance questions, collect a
|
|
19
|
+
[system trace](system-traces.md) around the interaction.
|
|
18
20
|
|
|
19
21
|
## get_logs
|
|
20
22
|
|
|
21
|
-
Fetch recent logs as structured entries
|
|
23
|
+
Fetch recent logs as structured entries for diagnosis.
|
|
22
24
|
|
|
23
25
|
Use logs as a debugging aid only. Prefer `expect_*` for verification, and use logs after verification fails or when an error is suspected.
|
|
24
26
|
|
|
25
27
|
Input (example):
|
|
26
28
|
|
|
27
29
|
```json
|
|
28
|
-
{ "platform": "android
|
|
30
|
+
{ "platform": "android", "appId": "com.example.app", "deviceId": "emulator-5554", "pid": 1234, "tag": "MyTag", "level": "ERROR", "contains": "timeout", "since_seconds": 60, "limit": 50 }
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
Defaults:
|
|
@@ -86,10 +88,10 @@ Input:
|
|
|
86
88
|
{ "platform": "android", "deviceId": "emulator-5554" }
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
Response (
|
|
91
|
+
Response (selected fields):
|
|
90
92
|
|
|
91
93
|
```json
|
|
92
|
-
{ "device": { "platform": "android", "id": "emulator-5554" }, "screen": "", "resolution": { "width": 1080, "height": 2400 }, "snapshot_revision": 12, "captured_at_ms": 1710000000123, "loading_state": { "active": true, "signal": "spinner", "source": "ui_tree" }, "elements": [ { "text": "Sign in", "type": "android.widget.Button", "resourceId": "com.example:id/signin", "clickable": true, "bounds": [0,0,100,50], "state": { "enabled": true }, "stable_id": "com.example:id/signin", "role": "button", "test_tag": "com.example:id/signin", "selector": { "value": "com.example:id/signin", "confidence": { "score": 1, "reason": "resource_id" } }, "semantic": { "is_clickable": true, "is_container": false } } ] }
|
|
94
|
+
{ "device": { "platform": "android", "id": "emulator-5554" }, "screen": "", "resolution": { "width": 1080, "height": 2400 }, "snapshot_session_id": "example-server-session", "snapshot_revision": 12, "captured_at_ms": 1710000000123, "loading_state": { "active": true, "signal": "spinner", "source": "ui_tree" }, "elements": [ { "text": "Sign in", "type": "android.widget.Button", "resourceId": "com.example:id/signin", "clickable": true, "bounds": [0,0,100,50], "state": { "enabled": true }, "stable_id": "com.example:id/signin", "role": "button", "test_tag": "com.example:id/signin", "selector": { "value": "com.example:id/signin", "confidence": { "score": 1, "reason": "resource_id" } }, "semantic": { "is_clickable": true, "is_container": false } } ] }
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
Notes:
|
|
@@ -133,8 +135,9 @@ Input:
|
|
|
133
135
|
{
|
|
134
136
|
"reason": "optional string describing why snapshot is taken",
|
|
135
137
|
"includeLogs": true,
|
|
138
|
+
"includeScreenshot": true,
|
|
136
139
|
"logLines": 200,
|
|
137
|
-
"platform": "android
|
|
140
|
+
"platform": "android",
|
|
138
141
|
"appId": "optional package/bundle id to scope logs",
|
|
139
142
|
"deviceId": "optional device serial/udid",
|
|
140
143
|
"sessionId": "optional log stream session id to prefer"
|
|
@@ -152,17 +155,17 @@ Behavior:
|
|
|
152
155
|
- `semantic` is optional, derived from `raw`, and intended for planning only.
|
|
153
156
|
- `raw` now includes `snapshot_revision`, `captured_at_ms`, and `loading_state` when detectable.
|
|
154
157
|
|
|
155
|
-
Response (
|
|
158
|
+
Response (selected fields):
|
|
156
159
|
|
|
157
160
|
```json
|
|
158
161
|
{
|
|
159
162
|
"raw": {
|
|
160
|
-
"timestamp":
|
|
163
|
+
"timestamp": 1710000000000,
|
|
161
164
|
"reason": "Crash after tapping checkout",
|
|
162
165
|
"activity": "CheckoutActivity",
|
|
163
166
|
"fingerprint": "abc123",
|
|
164
167
|
"screenshot": "<base64 PNG string>",
|
|
165
|
-
"ui_tree": {
|
|
168
|
+
"ui_tree": { "elements": [] },
|
|
166
169
|
"logs": [ { "timestamp": "2024-03-09T12:00:00.000Z", "level": "ERROR", "tag": "CheckoutViewModel", "pid": 1234, "message": "NullPointerException at CheckoutViewModel" } ]
|
|
167
170
|
},
|
|
168
171
|
"semantic": {
|
|
@@ -214,12 +217,41 @@ Guidance:
|
|
|
214
217
|
---
|
|
215
218
|
|
|
216
219
|
## start_log_stream / read_log_stream / stop_log_stream
|
|
217
|
-
Start
|
|
220
|
+
Start an app-scoped background stream on Android (`adb logcat`) or an iOS
|
|
221
|
+
simulator (`xcrun simctl ... log stream`). Start with `start_log_stream`:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"platform": "android",
|
|
226
|
+
"deviceId": "emulator-5554",
|
|
227
|
+
"packageName": "com.example.app",
|
|
228
|
+
"sessionId": "app-diagnostics",
|
|
229
|
+
"level": "error"
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`packageName` is required and is also used for an iOS bundle ID. `platform`
|
|
234
|
+
defaults to Android. Android accepts `level`: `error` (default), `warn`, `info`,
|
|
235
|
+
or `debug`. The iOS adapter does not use that level argument. Omitted session IDs
|
|
236
|
+
use `default`; supply an explicit ID to keep diagnostic streams distinct.
|
|
237
|
+
|
|
238
|
+
Pass the same session ID to `read_log_stream` and, when finished,
|
|
239
|
+
`stop_log_stream`:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{ "sessionId": "app-diagnostics" }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Use distinct session IDs across platforms: when an ID exists only on iOS, read
|
|
246
|
+
and stop select iOS; otherwise they select Android. Reads retain the stream's device scope.
|
|
247
|
+
`read_log_stream` also accepts the optional [action reference](correlated-evidence.md)
|
|
248
|
+
for evidence correlation. A read returns accumulated entries and any available
|
|
249
|
+
crash summary; reading does not itself stop the background stream.
|
|
218
250
|
|
|
219
|
-
|
|
251
|
+
Example read response (selected fields):
|
|
220
252
|
|
|
221
253
|
```json
|
|
222
|
-
{ "entries": [ { "timestamp": "2026-03-
|
|
254
|
+
{ "entries": [ { "timestamp": "2026-03-20T12:00:00Z", "level": "ERROR", "tag": "AppTag", "pid": 1234, "message": "FATAL EXCEPTION" } ], "crash_summary": { "crash_detected": true } }
|
|
223
255
|
```
|
|
224
256
|
|
|
225
257
|
## App readiness
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Android system traces
|
|
2
2
|
|
|
3
|
+
[Documentation](../README.md) · [Tool reference](TOOLS.md)
|
|
4
|
+
|
|
3
5
|
Use system traces to collect scheduling and system annotations around a slow
|
|
4
6
|
interaction. Capture is optional, device-wide and Android-only; it does not
|
|
5
7
|
automatically diagnose performance problems. No root or Android CLI installation
|
|
@@ -27,7 +29,7 @@ cleanup reserves its slot until resolved. Terminal records with complete cleanup
|
|
|
27
29
|
expire after 10 minutes or eviction beyond 100 records. Server restart invalidates
|
|
28
30
|
IDs. Repeated terminal operations return the stored outcome.
|
|
29
31
|
|
|
30
|
-
##
|
|
32
|
+
## Capture workflow
|
|
31
33
|
|
|
32
34
|
Trace a reproducible slow or stalled interaction when existing UI timing/state
|
|
33
35
|
and logs leave a performance question unresolved. Check tool discovery first:
|
|
@@ -124,3 +126,9 @@ shutdown attempts owned cleanup for up to 10 seconds. A server crash can leave
|
|
|
124
126
|
a uniquely named bounded device file for manual removal; restart performs no
|
|
125
127
|
broad cleanup sweep. Device suspend can extend Perfetto's own monotonic duration
|
|
126
128
|
limit; the server timer bounds wall-clock lifetime while the server is running.
|
|
129
|
+
|
|
130
|
+
## Related guides
|
|
131
|
+
|
|
132
|
+
- [Interaction](interact.md#interaction-execution-controls-rfc-018): run the journey being measured.
|
|
133
|
+
- [Evidence correlation](correlated-evidence.md): interpret action and observation timing.
|
|
134
|
+
- [Android runtime debugger](debugger.md): inspect paused application state.
|
package/docs/tools/system.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Environment diagnostics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Documentation](../README.md) · [Tool reference](TOOLS.md)
|
|
4
|
+
|
|
5
|
+
Check local toolchains and connected devices before building, installing, or
|
|
6
|
+
interacting with an app. If discovery cannot find a tool, configure the relevant
|
|
7
|
+
[path override](../../README.md#environment-setup), then check again. Once the
|
|
8
|
+
required platform is available, continue with [device selection](manage.md#list_devices).
|
|
4
9
|
|
|
5
10
|
## get_system_status
|
|
6
11
|
A fast, non-throwing healthcheck that inspects key dependencies and connections required for mobile debugging.
|
|
@@ -11,7 +16,7 @@ Input:
|
|
|
11
16
|
{}
|
|
12
17
|
```
|
|
13
18
|
|
|
14
|
-
Response (
|
|
19
|
+
Response (selected fields):
|
|
15
20
|
|
|
16
21
|
```json
|
|
17
22
|
{
|
|
@@ -63,13 +68,15 @@ Behavior notes:
|
|
|
63
68
|
- Always returns structured JSON and never throws; any failures are surfaced in the `issues` array.
|
|
64
69
|
- `status` gives a quick overall gate: `ready`, `degraded`, or `blocked`.
|
|
65
70
|
- `summary.android`, `summary.ios`, and `summary.gradle` provide the fastest path to the actual blocker category.
|
|
66
|
-
-
|
|
71
|
+
- The public tool accepts an empty input object; it exposes no `fastMode` option.
|
|
72
|
+
- Probes are best-effort and can take longer when native tooling is slow.
|
|
67
73
|
- Useful to call at the start of an agent session to gate subsequent actions.
|
|
68
74
|
|
|
69
75
|
Usage guidance:
|
|
70
76
|
- Call before build/install flows to avoid wasted build attempts on misconfigured systems.
|
|
71
77
|
- Call early in a session when device or toolchain availability is uncertain.
|
|
72
|
-
-
|
|
78
|
+
- `success: false` can reflect issues on a platform you are not using. Inspect the
|
|
79
|
+
relevant platform status and blockers before deciding whether to proceed.
|
|
73
80
|
|
|
74
81
|
Recommended setup patterns:
|
|
75
82
|
- Default: let the server discover tools automatically from standard locations and call `get_system_status` first to confirm what is available.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,29 +17,32 @@
|
|
|
17
17
|
"test:integration": "npm run test:device",
|
|
18
18
|
"test:device": "npm run build && tsx test/device/index.ts",
|
|
19
19
|
"test": "npm run test:unit",
|
|
20
|
-
"lint": "eslint src test --quiet",
|
|
21
|
-
"lint:fix": "eslint src test --fix",
|
|
20
|
+
"lint": "eslint src test --quiet && npm run check:quality && npm run check:architecture",
|
|
21
|
+
"lint:fix": "eslint src test --fix && npm run check:quality && npm run check:architecture",
|
|
22
22
|
"sync:agent-plugin": "node scripts/agent-plugin.mjs --sync",
|
|
23
|
+
"check:quality": "node scripts/code-quality.mjs",
|
|
24
|
+
"check:architecture": "node scripts/check-architecture.mjs",
|
|
23
25
|
"check:agent-plugin": "node scripts/agent-plugin.mjs --check",
|
|
24
26
|
"verify": "npm run lint && npm run build && npm run test:unit && npm run check:agent-plugin"
|
|
25
27
|
},
|
|
26
|
-
|
|
27
28
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
29
|
+
"node": ">=22"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
33
|
"fast-xml-parser": "^5.5.1",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
34
|
+
"sharp": "^0.35.5",
|
|
35
|
+
"zod": "^4.6.7"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "^25.4.0",
|
|
38
39
|
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
39
40
|
"@typescript-eslint/parser": "^8.57.1",
|
|
41
|
+
"archunit": "2.5.4",
|
|
40
42
|
"eslint": "^9.39.4",
|
|
41
43
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
42
|
-
"
|
|
44
|
+
"minimist": "^1.2.8",
|
|
45
|
+
"tsx": "^4.23.15",
|
|
43
46
|
"typescript": "^5.9.3"
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { isBuiltin } from 'node:module'
|
|
4
|
+
import { extractGraph } from 'archunit'
|
|
5
|
+
|
|
6
|
+
// ArchUnitTS 2.5.4 extracts static imports. Add re-exports, import types and
|
|
7
|
+
// literal dynamic imports using the same project's TypeScript resolution.
|
|
8
|
+
export async function architectureGraph(configFile = 'tsconfig.json') {
|
|
9
|
+
const configPath = path.resolve(configFile)
|
|
10
|
+
const root = path.dirname(configPath)
|
|
11
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile)
|
|
12
|
+
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
|
|
13
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root)
|
|
14
|
+
if (parsed.errors.length) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
|
|
15
|
+
getCanonicalFileName: file => file, getCurrentDirectory: () => root, getNewLine: () => '\n'
|
|
16
|
+
}))
|
|
17
|
+
const graph = await extractGraph(configPath, { clearCache: true })
|
|
18
|
+
const local = file => path.relative(root, file).split(path.sep).join('/')
|
|
19
|
+
const edges = new Map(graph.map(edge => [JSON.stringify([edge.source, edge.target]), edge]))
|
|
20
|
+
const recorded = new Set()
|
|
21
|
+
for (const file of parsed.fileNames) {
|
|
22
|
+
const code = ts.sys.readFile(file)
|
|
23
|
+
if (code === undefined) throw new Error(`Cannot read architecture source: ${file}`)
|
|
24
|
+
const ast = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true)
|
|
25
|
+
const source = local(file)
|
|
26
|
+
function add(specifier, typeOnly = false) {
|
|
27
|
+
const module = specifier.text
|
|
28
|
+
const resolved = ts.resolveModuleName(module, file, parsed.options, ts.sys).resolvedModule
|
|
29
|
+
if (!resolved && !isBuiltin(module)) throw new Error(`Unresolved dependency: ${source} -> ${module}`)
|
|
30
|
+
const external = !resolved || !!resolved.isExternalLibraryImport
|
|
31
|
+
const target = resolved ? local(resolved.resolvedFileName) : module.replace(/^(?!node:)/, 'node:')
|
|
32
|
+
const key = JSON.stringify([source, target])
|
|
33
|
+
const kind = typeOnly ? 'type' : 'value'
|
|
34
|
+
const previous = edges.get(key)
|
|
35
|
+
// Replace the initial static classification, then union repeated dependencies.
|
|
36
|
+
if (previous && recorded.has(key)) {
|
|
37
|
+
previous.importKinds = [...new Set([...previous.importKinds, kind])]
|
|
38
|
+
} else edges.set(key, { source, target, external, importKinds: [kind] })
|
|
39
|
+
recorded.add(key)
|
|
40
|
+
}
|
|
41
|
+
function visit(node) {
|
|
42
|
+
if (ts.isImportDeclaration(node)) {
|
|
43
|
+
const clause = node.importClause
|
|
44
|
+
const bindings = clause?.namedBindings
|
|
45
|
+
const typeOnly = clause?.isTypeOnly || (!clause?.name && bindings && ts.isNamedImports(bindings) &&
|
|
46
|
+
bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly))
|
|
47
|
+
add(node.moduleSpecifier, !!typeOnly)
|
|
48
|
+
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
49
|
+
const bindings = node.exportClause
|
|
50
|
+
const typeOnly = node.isTypeOnly || (bindings && ts.isNamedExports(bindings) &&
|
|
51
|
+
bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly))
|
|
52
|
+
add(node.moduleSpecifier, !!typeOnly)
|
|
53
|
+
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
54
|
+
if (!node.moduleReference.expression) throw new Error(`Missing import target in ${source}`)
|
|
55
|
+
add(node.moduleReference.expression, node.isTypeOnly)
|
|
56
|
+
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
57
|
+
add(node.argument.literal, true)
|
|
58
|
+
} else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
59
|
+
(ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
|
|
60
|
+
const argument = node.arguments[0]
|
|
61
|
+
if (!argument || !ts.isStringLiteralLike(argument)) throw new Error(`Nonliteral dependency in ${source}; architecture cannot resolve it`)
|
|
62
|
+
add(argument)
|
|
63
|
+
}
|
|
64
|
+
ts.forEachChild(node, visit)
|
|
65
|
+
}
|
|
66
|
+
visit(ast)
|
|
67
|
+
}
|
|
68
|
+
return [...edges.values()]
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { gatherDependOnFileViolations, gatherCycleViolations, projectEdges, perEdge, perInternalEdge } from 'archunit'
|
|
4
|
+
import { architectureGraph } from './architecture-graph.mjs'
|
|
5
|
+
|
|
6
|
+
export const architectureRules = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Shared contracts stay independent of application implementations',
|
|
9
|
+
source: /^src\/contracts\//,
|
|
10
|
+
forbidden: /^src\/(?!contracts\/)/
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Capabilities and shared modules cannot import the MCP server layer',
|
|
14
|
+
source: /^src\/(?!server(?:\/|-core\.ts$|\.ts$))/,
|
|
15
|
+
forbidden: /^src\/server(?:\/|-core\.ts$|\.ts$)/
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Pure interaction calculations cannot import native I/O or service facades',
|
|
19
|
+
source: /^src\/interact\/(?:adjustment\/(?:adjustment-plan|control-values|control-geometry|slider-labels)|targeting\/(?:candidate-scoring|candidate-ancestor|candidate-result|nearby-control|element-helpers|resolved-elements|tap-target|target-scope)|waits\/(?:ui-change-helpers|ui-wait-condition)|assertions\/state-comparison)\.ts$/,
|
|
20
|
+
forbidden: /^(?:node:(?:child_process|fs|net|http|https)(?:\/|$)|src\/(?:utils\/(?:android|ios)\/|(?:interact|observe|manage)\/(?:index|android|ios)\.ts$))/
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
const filter = regExp => ({ regExp, options: { target: 'path', matching: 'partial' } })
|
|
24
|
+
|
|
25
|
+
export function checkArchitecture(graph) {
|
|
26
|
+
const projected = projectEdges(graph, perEdge())
|
|
27
|
+
const results = architectureRules.map(rule => ({
|
|
28
|
+
name: rule.name,
|
|
29
|
+
violations: gatherDependOnFileViolations(projected, [filter(rule.source)], [filter(rule.forbidden)], true)
|
|
30
|
+
}))
|
|
31
|
+
const runtime = graph.filter(edge => edge.importKinds.includes('value') || !edge.importKinds.includes('type'))
|
|
32
|
+
results.push({ name: 'Production runtime dependencies must be cycle-free',
|
|
33
|
+
violations: gatherCycleViolations(projectEdges(runtime, perInternalEdge()), []) })
|
|
34
|
+
return results
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const graph = await architectureGraph(process.argv[2])
|
|
39
|
+
const results = checkArchitecture(graph)
|
|
40
|
+
const failures = results.filter(result => result.violations.length)
|
|
41
|
+
for (const result of failures) console.error(`${result.name}\n${JSON.stringify(result.violations, null, 2)}`)
|
|
42
|
+
if (failures.length) process.exitCode = 1
|
|
43
|
+
else console.log(`Architecture: PASS (${results.length} rules; ${graph.filter(edge => edge.source !== edge.target).length} dependency edges, including type, dynamic and re-export dependencies)`)
|
|
44
|
+
}
|
|
45
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
46
|
+
main().catch(error => { console.error(error); process.exitCode = 1 })
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ESLint } from 'eslint'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
import { parse } from '@typescript-eslint/parser'
|
|
5
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
export const limits = Object.freeze({ 'max-lines': 500, 'class-lines': 300,
|
|
10
|
+
'max-lines-per-function': 80, complexity: 20, 'max-depth': 4, 'explicit-any': 0 })
|
|
11
|
+
const functionKinds = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])
|
|
12
|
+
const measuredRules = new Set(['max-lines', 'max-lines-per-function', 'complexity', 'max-depth'])
|
|
13
|
+
|
|
14
|
+
function children(node) {
|
|
15
|
+
return Object.entries(node).filter(([key]) => !['parent', 'tokens', 'comments', 'loc', 'range'].includes(key))
|
|
16
|
+
.flatMap(([, value]) => Array.isArray(value) ? value : [value])
|
|
17
|
+
.filter(value => value && typeof value === 'object' && typeof value.type === 'string')
|
|
18
|
+
}
|
|
19
|
+
function label(node, parent) {
|
|
20
|
+
if (node.id?.name) return node.id.name
|
|
21
|
+
if (parent?.type === 'MethodDefinition' || parent?.type === 'Property' || parent?.type === 'PropertyDefinition') {
|
|
22
|
+
const name = parent.key.name ?? parent.key.value ?? '[computed]'
|
|
23
|
+
return ['get', 'set'].includes(parent.kind) ? `${parent.kind}:${name}` : name
|
|
24
|
+
}
|
|
25
|
+
if (parent?.type === 'VariableDeclarator') return parent.id.name ?? '[destructured]'
|
|
26
|
+
if (parent?.type === 'CallExpression' || parent?.type === 'NewExpression') {
|
|
27
|
+
const callee = parent.callee.name ?? parent.callee.property?.name ?? '[call]'
|
|
28
|
+
return `${callee}:argument${parent.arguments.indexOf(node)}`
|
|
29
|
+
}
|
|
30
|
+
return `[${node.type}]`
|
|
31
|
+
}
|
|
32
|
+
function syntax(code) {
|
|
33
|
+
const ast = parse(code, { loc: true, range: true, comment: true, sourceType: 'module', ecmaVersion: 'latest' })
|
|
34
|
+
const symbols = [], anys = [], imports = []
|
|
35
|
+
function visit(node, parent, scope = '<module>') {
|
|
36
|
+
if (['TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(node.type)) scope = `${scope}/${node.id.name}`
|
|
37
|
+
if (functionKinds.has(node.type) || node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
|
38
|
+
const anonymous = !node.id?.name && !['MethodDefinition', 'Property', 'PropertyDefinition', 'VariableDeclarator'].includes(parent?.type)
|
|
39
|
+
const suffix = anonymous ? ':' + createHash('sha256').update(code.slice(...node.range).replace(/\s/g, '')).digest('hex').slice(0, 12) : ''
|
|
40
|
+
scope = `${scope}/${label(node, parent)}${suffix}`
|
|
41
|
+
symbols.push({ node, identity: scope })
|
|
42
|
+
}
|
|
43
|
+
if (node.type === 'TSAnyKeyword') anys.push({ node, identity: scope })
|
|
44
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration', 'ImportExpression', 'TSImportType'].includes(node.type) && node.source) {
|
|
45
|
+
imports.push({ node, value: node.source.value })
|
|
46
|
+
}
|
|
47
|
+
if (node.type === 'CallExpression' && node.callee.name === 'require' && node.arguments[0]?.type === 'Literal') {
|
|
48
|
+
imports.push({ node, value: node.arguments[0].value })
|
|
49
|
+
}
|
|
50
|
+
for (const child of children(node)) visit(child, node, scope)
|
|
51
|
+
}
|
|
52
|
+
visit(ast, null)
|
|
53
|
+
return { ast, symbols, anys, imports }
|
|
54
|
+
}
|
|
55
|
+
function lineCount(code, comments, start = 0, end = code.length) {
|
|
56
|
+
const chars = code.split('')
|
|
57
|
+
for (const c of comments) for (let i = c.range[0]; i < c.range[1]; i++) if (chars[i] !== '\n') chars[i] = ' '
|
|
58
|
+
return chars.slice(start, end).join('').split('\n').filter(line => line.trim()).length
|
|
59
|
+
}
|
|
60
|
+
const eslint = new ESLint({ overrideConfig: [{ files: ['**/*.ts'], linterOptions: { noInlineConfig: true }, rules: {
|
|
61
|
+
'max-lines': ['error', { max: limits['max-lines'], skipBlankLines: true, skipComments: true }],
|
|
62
|
+
'max-lines-per-function': ['error', { max: limits['max-lines-per-function'], skipBlankLines: true, skipComments: true }],
|
|
63
|
+
complexity: ['error', { max: limits.complexity, variant: 'classic' }], 'max-depth': ['error', limits['max-depth']]
|
|
64
|
+
} }] })
|
|
65
|
+
|
|
66
|
+
export async function measure(file, code) {
|
|
67
|
+
if (file.endsWith('.java')) {
|
|
68
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, code)
|
|
69
|
+
const comments = []
|
|
70
|
+
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
71
|
+
if ([ts.SyntaxKind.SingleLineCommentTrivia, ts.SyntaxKind.MultiLineCommentTrivia].includes(token)) {
|
|
72
|
+
comments.push({ range: [scanner.getTokenPos(), scanner.getTextPos()] })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const value = lineCount(code, comments)
|
|
76
|
+
return value > limits['max-lines'] ? [{ file, rule: 'max-lines', symbol: '<module>', value, line: 1 }] : []
|
|
77
|
+
}
|
|
78
|
+
const { ast, symbols, anys, imports } = syntax(code)
|
|
79
|
+
const findings = []
|
|
80
|
+
const add = (rule, symbol, value, line) => findings.push({ file, rule, symbol, value, line })
|
|
81
|
+
const identities = new Set()
|
|
82
|
+
for (const { node, identity } of symbols) {
|
|
83
|
+
if (identities.has(identity) && !identity.includes(':argument')) add('duplicate-symbol', identity, 1, node.loc.start.line)
|
|
84
|
+
identities.add(identity)
|
|
85
|
+
if (node.type.startsWith('Class')) {
|
|
86
|
+
const value = lineCount(code, ast.comments, ...node.range)
|
|
87
|
+
if (value > limits['class-lines']) add('class-lines', identity, value, node.loc.start.line)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const anyCounts = new Map()
|
|
91
|
+
for (const { node, identity } of anys) {
|
|
92
|
+
const current = anyCounts.get(identity) ?? { value: 0, line: node.loc.start.line }
|
|
93
|
+
current.value++; anyCounts.set(identity, current)
|
|
94
|
+
}
|
|
95
|
+
for (const [identity, { value, line }] of anyCounts) add('explicit-any', identity, value, line)
|
|
96
|
+
const [result] = await eslint.lintText(code, { filePath: file })
|
|
97
|
+
for (const m of result.messages.filter(m => measuredRules.has(m.ruleId))) {
|
|
98
|
+
const value = Number(/(?:has too many lines \(|complexity of |nested too deeply \()([\d]+)/.exec(m.message)?.[1])
|
|
99
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot read ${m.ruleId} metric: ${m.message}`)
|
|
100
|
+
const candidates = symbols.filter(({ node }) => m.line >= node.loc.start.line && m.line <= node.loc.end.line)
|
|
101
|
+
.sort((a, b) => a.node.range[1] - a.node.range[0] - (b.node.range[1] - b.node.range[0]))
|
|
102
|
+
const symbol = m.ruleId === 'max-lines' ? '<module>' : candidates[0]?.identity ?? '<module>'
|
|
103
|
+
add(m.ruleId, symbol, value, m.line)
|
|
104
|
+
}
|
|
105
|
+
for (const comment of ast.comments) {
|
|
106
|
+
if (/(?:eslint-(?:disable|enable)|^\s*eslint\s)/.test(comment.value) &&
|
|
107
|
+
(!/eslint-(?:disable|enable)(?:-next-line|-line)?\s+(?!-)[\w@]/.test(comment.value) ||
|
|
108
|
+
/max-lines|complexity|max-depth|no-explicit-any|code-quality/.test(comment.value))) {
|
|
109
|
+
add('inline-suppression', '<module>', 1, comment.loc.start.line)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (file.startsWith('src/interact/') || file.startsWith('src/runtime/')) {
|
|
113
|
+
for (const { node, value } of imports) if (typeof value === 'string' && /(?:^|\/)server(?:\/|-core)/.test(value)) {
|
|
114
|
+
add('import-boundary', '<module>', 1, node.loc.start.line)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Multiple nested blocks can report depth in the same function; retain its maximum.
|
|
118
|
+
return [...findings.reduce((out, f) => {
|
|
119
|
+
const key = `${f.rule}:${f.symbol}`
|
|
120
|
+
if (!out.has(key) || out.get(key).value < f.value) out.set(key, f)
|
|
121
|
+
return out
|
|
122
|
+
}, new Map()).values()]
|
|
123
|
+
}
|
|
124
|
+
export function compare(findings, baseline) {
|
|
125
|
+
const errors = [], entries = new Map()
|
|
126
|
+
const key = f => JSON.stringify([f.file, f.rule, f.symbol])
|
|
127
|
+
for (const b of baseline) {
|
|
128
|
+
if (!b.file || !b.rule || !b.symbol || !b.reason || !Number.isInteger(b.value) || b.value < 1) {
|
|
129
|
+
errors.push('Invalid baseline entry'); continue
|
|
130
|
+
}
|
|
131
|
+
if (entries.has(key(b))) errors.push(`Duplicate baseline entry: ${key(b)}`)
|
|
132
|
+
entries.set(key(b), b)
|
|
133
|
+
}
|
|
134
|
+
for (const f of findings) {
|
|
135
|
+
const b = entries.get(key(f))
|
|
136
|
+
if (['duplicate-symbol', 'inline-suppression', 'import-boundary'].includes(f.rule) || !b || f.value > b.value) {
|
|
137
|
+
errors.push(`${f.file}:${f.line}: ${f.rule} ${f.symbol} = ${f.value}; allowed ${b?.value ?? limits[f.rule] ?? 0}`)
|
|
138
|
+
} else if (f.value < b.value) errors.push(`Lower baseline ceiling for ${key(f)}: ${b.value} -> ${f.value}`)
|
|
139
|
+
entries.delete(key(f))
|
|
140
|
+
}
|
|
141
|
+
for (const b of entries.values()) errors.push(`Remove obsolete baseline entry: ${key(b)}`)
|
|
142
|
+
return errors
|
|
143
|
+
}
|
|
144
|
+
export async function collect(root = 'src') {
|
|
145
|
+
const out = []
|
|
146
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
147
|
+
const file = path.posix.join(root, entry.name)
|
|
148
|
+
if (entry.isDirectory()) out.push(...await collect(file))
|
|
149
|
+
else if (/\.(ts|java)$/.test(file)) out.push(...await measure(file, await readFile(file, 'utf8')))
|
|
150
|
+
}
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
async function main() {
|
|
154
|
+
const baseline = JSON.parse(await readFile(new URL('./code-quality-baseline.json', import.meta.url), 'utf8'))
|
|
155
|
+
const errors = compare(await collect(), baseline)
|
|
156
|
+
if (errors.length) { console.error(errors.join('\n')); process.exitCode = 1 }
|
|
157
|
+
else console.log('Code quality: PASS (size, complexity, nesting, types, boundaries and legacy ceilings)')
|
|
158
|
+
}
|
|
159
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
160
|
+
main().catch(error => { console.error(error); process.exitCode = 1 })
|
|
161
|
+
}
|