Skip to content

feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011)#1091

Merged
thymikee merged 2 commits into
mainfrom
feat/adr0011-direct-ios-delegation
Jul 4, 2026
Merged

feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011)#1091
thymikee merged 2 commits into
mainfrom
feat/adr0011-direct-ios-delegation

Conversation

@thymikee

@thymikee thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member

Implements ADR 0011 wave-2 delegation-on-error for the direct iOS selector path. Part of #1081.

What changes

When the direct XCTest runner dispatch fails with a semantic shape, the interaction now falls back to the tree-based runtime path instead of surfacing the bare runner error:

Runner failure Non-maestro dispatch Maestro replay dispatch (allowNonHittableCoordinateFallback)
AMBIGUOUS_MATCH falls back → runtime disambiguation (visible-first/deepest/smallest) resolves or raises the rich ambiguous error errors, unchanged (runner-native shape preserved)
ELEMENT_NOT_FOUND falls back → covered targets raise the occlusion refusal + hint; non-hittable targets are promoted/annotated (targetHittable/hint); true misses get the shared no-match diagnostics errors, unchanged
ELEMENT_OFFSCREEN falls back (unchanged, #1075) falls back (unchanged)
transport COMMAND_FAILED falls back (unchanged) falls back (unchanged)

Maestro gating (explicit)

The maestro replay path flows through the same dispatch, so the flag is threaded into the fallback decision explicitly: isDirectIosSelectorFallbackError(error, { delegateSemanticFailures: selector.allowNonHittableCoordinateFallback !== true }). Replay matching is intentionally runner-native (unique-or-ambiguous scan); its error semantics are byte-for-byte unchanged, and both sides are pinned by unit tests and a provider scenario.

Query-path semantics preserved

selector-runtime.ts's queryDirectIosSelectorOrFallback (wait/get/is) keeps its exact behavior via the untouched allowElementNotFound option: ELEMENT_NOT_FOUND still falls back, AMBIGUOUS_MATCH still surfaces as-is (pinned by the existing get text ... does not snapshot-fallback on ambiguous runner match test). The default was not inverted precisely to avoid changing that caller.

Registry (ADR 0011 Layer 1)

direct-ios-selector errorTaxonomy / nonHittable / occlusion flip from gap: waivers to delegated → runtime-selector cells (with the non-maestro nuance in the cell text); the pinned gap list shrinks 6 → 3.

Success-path parity cells intentionally remain gaps per the ADR: disambiguation and responseIdentity stay waived — delegation-on-error cannot catch the case where XCTest succeeds on a candidate the runtime rules would refuse or rank differently. maestro-non-hittable-fallback/errorTaxonomy also stays (this PR deliberately preserves those shapes).

Tests

  • Unit: fallback-predicate matrix (direct-ios-selector.test.ts), flipped + new maestro-gated dispatch tests (interaction.test.ts).
  • Provider scenarios (interaction-direct-selector-fallback.test.ts, transcript-driven): ambiguous → fallback → disambiguation taps the visible twin; ELEMENT_NOT_FOUND on covered element → occlusion refusal + hint; ELEMENT_NOT_FOUND on non-hittable element → targetHittable: false + hint annotation; maestro dispatch → AMBIGUOUS_MATCH surfaces with zero fallback traffic (single-entry transcript + wire-level flag assertion).

Not done (called out)

The message-sniffing transport checks in isDirectIosSelectorFallbackError stay: the sniffed shapes originate at 4+ scattered throw sites plus raw undici fetch failed TypeErrors that are never AppErrors before this boundary, and isRetryableRunnerError does the same sniffing for retry policy — a typed transport marker needs one coordinated wrapping change, left as Tier-3 cleanup (documented in-code).

Verification

format:check, typecheck, lint, fallow audit --base origin/main (clean; one inherited duplication excluded by gate), vitest run src/daemon src/commands src/contracts (1232 passed), vitest run test/integration (93 passed).

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.5 MB 1.5 MB +171 B
JS gzip 489.3 kB 489.3 kB +65 B
npm tarball 588.4 kB 588.4 kB +54 B
npm unpacked 2.1 MB 2.1 MB +171 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.6 ms 25.8 ms +0.2 ms
CLI --help 49.6 ms 49.1 ms -0.5 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/selector-runtime.js +102 B +39 B
dist/src/interaction.js +69 B +26 B

@thymikee

thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Reviewed by diff (author-gated suites; host busy with the replay suite rerun, CI re-confirms). All three traps from the task spec were handled the right way:

  • Maestro gating: delegateSemanticFailures set from allowNonHittableCoordinateFallback !== true at the single dispatch site, with the replay-semantics rationale on the option itself — and the single-entry transcript scenario proving zero fallback traffic for maestro dispatches is exactly the right kind of negative evidence.
  • selector-runtime:384: not inverting the default and citing the pinning test (does not snapshot-fallback on ambiguous runner match) as preserved-unmodified is precisely how that preservation should be demonstrated. The read-path/interaction-path asymmetry (AMBIGUOUS_MATCH surfaces for get/wait/is, delegates for press/fill) is now explicit in the option docs rather than implicit in call sites.
  • Transport-sniff deferral: the in-code comment enumerating the scattered origins (including the raw undici TypeErrors and the isRetryableRunnerError coupling) turns a skipped cleanup into a documented decision — that's the difference between debt and rot.

Registry flips and the 6→3 pin shrink match the ADR's hybrid strategy exactly (success-path parity cells untouched). LGTM pending CI.

Note for the release: this PR's behavior change (ambiguous/not-found direct-path failures now resolve via runtime instead of erroring) is covered by the react-navigation replay suite currently running against merged main — I'll rerun it against this branch if the maintainer wants belt-and-suspenders before merge.

@thymikee

thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Coordination note: #1092 (contract-suite coverage gate) should merge first; this PR then rebases and must add three coverage-manifest entries for its newly-delegated cells (errorTaxonomy/nonHittable/occlusion) — its own fallback provider scenarios already prove those behaviors, so it's manifest wiring, not new tests. I'll do the rebase when #1092 lands.

…ailures (ADR 0011)

Runner ELEMENT_NOT_FOUND/AMBIGUOUS_MATCH on the direct iOS selector
dispatch now delegate to the tree-based runtime path, which supplies
runtime disambiguation, occlusion refusal, non-hittable
promotion/annotation, and rich selector diagnostics/hints.

Maestro replay dispatches (allowNonHittableCoordinateFallback) keep the
runner-native error shapes: the new delegateSemanticFailures option is
threaded from the dispatch flag, and the query path
(allowElementNotFound at selector-runtime) keeps its exact semantics.

Registry: direct-ios-selector errorTaxonomy/nonHittable/occlusion flip
from gap waivers to delegated cells; the pinned gap list shrinks 6 -> 3.
Success-path parity cells (disambiguation, responseIdentity)
intentionally remain gaps per ADR 0011.

Refs #1081
@thymikee

thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Sequencing blocker: #1092 has now merged, so this branch needs the promised rebase/update before it is ready. The current #1091 diff still does not include the interaction-contract coverage manifest entries for the newly delegated direct-ios-selector cells (errorTaxonomy/nonHittable/occlusion). Please rebase onto current main and wire those coverage-manifest entries, then rerun the relevant contract/provider checks.

The coverage gate demanded scenarios the moment the rebase flipped
errorTaxonomy/nonHittable/occlusion to delegated — as designed. Three
transcript scenarios prove the delegation end to end: runner
ELEMENT_NOT_FOUND falls back to runtime no-match diagnostics, to the
covered refusal, and to an annotated coordinate tap (the transcript
asserts the fallback tap is coordinate-keyed).
@thymikee thymikee force-pushed the feat/adr0011-direct-ios-delegation branch from f689aec to 97cd231 Compare July 4, 2026 14:09
@thymikee

thymikee commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Re-review status: sequencing blocker resolved. The branch now includes the post-#1092 interaction-contract coverage entries for direct-ios-selector errorTaxonomy, nonHittable, and occlusion, with matching contract scenarios. Focused validation passed locally: pnpm exec vitest run src/contracts/tests/interaction-contract-coverage.test.ts test/integration/interaction-contract/direct-ios-selector.contract.test.ts (9 tests). GitHub checks are green (21/21). Added ready-for-human.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 4, 2026
@thymikee thymikee merged commit ad754ac into main Jul 4, 2026
21 checks passed
@thymikee thymikee deleted the feat/adr0011-direct-ios-delegation branch July 4, 2026 14:41
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-04 14:41 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant