Skip to content

refactor: native target resolution — one pipeline, per-caller policy as data #1630

Description

@thymikee

From the 2026-08-06 architecture review (claims verified by independent adversarial passes; checked against ADR 0011/0015 + the Selector Capture Reliability Contract).

Problem

Five native consumers of "resolve a selector against the screen" each hand-assemble their own subset of the capture → match → disambiguate → occlusion → off-screen pipeline. The shared core (packages/selectors) owns parse/match/disambiguation only; everything around it is per-caller:

caller ambiguity policy occlusion off-screen poll budget
click/press/fill (resolution.ts:267-351) disambiguate (disambiguateAmbiguous:true) yes yes none (one capture + one immediate interactiveOnly=false re-capture)
is non-exists / get attrs (selector-read.ts:316-321,465-470) fail closed (requireUnique:true, disambiguateAmbiguous:false) none none none
exists / find read actions (selector-read.ts:294-314,437-443) first match, ambiguity-tolerant none none none
wait (selector-wait.ts:213-278) no disambiguation concept (matchedNodes[0]) none none 300ms poll / 10s default
mutating find (daemon/handlers/find.ts) own ranking (preferOnscreenMatches/rankInteractiveMatches/interactiveMatchScore) own check own scoring none

That's at least 4 distinct ambiguity policies declared as inline literals at call sites — no central policy table exists. Occlusion (isSnapshotNodeInteractionBlocked) is consulted by 2 of 5. Mutating find chains two engines per action: its own ranking, then re-dispatch by @ref through resolution.ts's ref path (find.ts:391-402). And resolveSelectorInteractionTarget is module-private, so resolution semantics are only testable through the full-runtime harness with hand-built snapshot fixtures.

Proposed deepening

One native resolution module whose interface takes the target plus a declared policy (ambiguity mode, occlusion on/off, poll budget) as data. Each caller's current semantics are preserved exactly — the win is that the whole policy matrix becomes visible, diffable, and testable through one interface, and a future policy change is a table edit instead of a 4-file hunt.

The ADR pass rated this a restatement of ADR 0011's own thesis: "each guarantee has exactly one home that all TS paths import."

Scope guards

  • The Maestro engine stays fully separate (ADR 0015) — its own matcher, visibility, and poll loops are compatibility-policy-owned and must not be unified into this.
  • is keeps failing closed on ambiguity; no silent fallbacks (ADR 0011 + Selector Capture Reliability Contract). This refactor changes zero semantics.
  • Whether click/tap/is ever get an implicit lookup budget is a deliberately open product decision (2026-07-31) — out of scope here; this deepening only makes it a one-row policy change if ever taken.
  • Direct iOS selector fast-path gating rules (simple one-term selectors, never during pending stabilization) unchanged.

Acceptance criteria

  • One resolution interface; per-caller policy declared as data in one place
  • All five callers routed through it with byte-identical error/result semantics (existing tests unchanged and green)
  • Policy matrix has a direct test suite driving the interface with fixture trees
  • Mutating find no longer chains two resolution engines
  • No disambiguateAmbiguous/requireUnique literals at call sites outside the policy declaration

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions