Skip to main content

Rover deep coverage on hardened canvas-rendered apps

This is the design record for the fix/rover-canvas-coverage effort — extending Rover's crawl coverage on hardened, canvas-rendered apps whose tap targets carry no text, description, or resource-id. It formalizes a live PhonePe real-device session (2026-07-21) into a diagnosis, a measurement instrument, and a ranked set of fixes. It is the companion to the rover module context (backend/.context/modules/rover.md).

Why this doc exists​

PhonePe (com.phonepe.app) is the reference hard case. Across nine real-device scans on a physical Pixel, Rover mapped 17 distinct screens with zero destructive actions — Home, Alerts, Transaction History and Details, Credit Score, Offers Hub and Details, Support Chat, Country Selection, Insurance Hub, Monthly Summary, Contact Picker, Calculator, Payment Request, Payment Chat, and the Share Sheet — and the nav-edge audit confirms taps into Recharge and Bills, Loans, and Insure Now. The frontier then exhausts at roughly 13 screens per scan (17 cumulative, deduped).

The remaining unmapped surface splits cleanly into two categories that must never be conflated:

  • Forbidden (out of scope): payment, transfer, and recharge completion (Pay / Confirm / PIN). PhonePe's Pay buttons are unlabeled canvas (class="View", blank text and description), so no text denylist can reliably identify them. The crawl must not enter completion flows. See Absolute safety.
  • Fixable non-destructive gaps (this effort): Search, QR, Profile and Settings, deep home sections (Mutual Funds / Gold / Rewards info), and Recharge or biller selection screens.

Root-cause findings​

These come from the 2026-07-21 diagnosis; they are recorded so they are not re-derived.

  1. Tap coordinate is bounds-center. MaestroDriver.resolvePoint taps bounds.center(). The crawl navigated fine from ~21 taps, so bounds-center is not broadly broken. Canvas tap-fidelity — the center of a large container missing the real handler — is a hypothesis to measure, not a confirmed cause. This is exactly what P0 measures.
  2. Search and QR bottom-nav never produced navigates_to edges. Search almost certainly expands an in-place overlay (same activity, a search bar) that the crawl does not model as a distinct screen; QR launches the camera (a permission or degenerate-capture dead end).
  3. Profile is a top-left-of-home tap. Confirmed verbatim by PhonePe's in-app terms: "Go to Profile Page by clicking on the top left of the home page." The avatar is likely an unlabeled clickable that the frontier-seeding logic does not enqueue, or a small target whose clickable ancestor is the whole top bar (so a bounds-center tap misses it).
  4. The scroll no-progress guard is already bounds-aware for unlabeled nodes (PR #521, scrollSignature). Deep-section discovery pairs with it.

The measurement instrument (P0) — shipped​

Before fixing anything, measure. Every one of the fixable gaps above reduces to the same question: when Rover taps a target, does anything happen? P0 answers it directly.

For every resolved component tap, ScanRunner now records whether the normalized screen changed afterwards. The ratio of taps that produced no navigation to all component taps is the no-nav rate.

  • Signal: ScanRunner.screenChanged(before, after) — the route differs or the normalized StructuralSignature differs.
  • Anchored on the screen the tap was actually issued against (TapOutcome.tappedOnScreen: the current screen for a same-screen sibling, or the re-converged screen after bounded re-resolution unwound one or more back()s), not the top-of-iteration screen — so a re-resolution back() is never misattributed to the tap.
  • Both screens are already normalizeScreen output, so OS-chrome churn is invisible and the comparison aligns with the deduper's own notion of "same screen".
  • Only resolved component taps count (TapOutcome.tappedNode != null — never SCROLL or back).

Where it surfaces:

  • CoverageReport.componentTaps (denominator), CoverageReport.noNavTaps (numerator), and the derived CoverageReport.noNavRate (null when there were no taps). Carried as counts, not a pre-divided rate, so cross-segment totals sum before the ratio is taken.

  • Threaded SegmentResult → workflow totalComponentTaps / totalNoNavTaps → markCompleted → the componentTaps / noNavTaps / noNavRate keys in the rover.scans.summary JSON. This mirrors the existing unreachedTargets thread exactly.

  • Per-tap telemetry is a DEBUG log line:

    rover tap-outcome scan=<id> label=<label> route=<route> nav=<true|false>

Reading the result: a high no-nav rate on a canvas-rendered app is the primary evidence for a tap-fidelity problem (bounds-center missing the real handler). A low rate means the taps are landing and the unmapped surface is genuinely non-navigating or gated (forbidden completion flows, camera, in-place overlays). This single number decides how much of the remaining gap is tap-fidelity versus in-place-overlay versus dedup, and it is a generally-useful crawl-health metric for any app.

Shipped in PR #526 (feat(rover): tap-outcome instrumentation + no-nav rate).

Prioritized fixes​

Leverage against safety-of-implementation. P0 is the diagnostic that ranks the rest.

#FixStatusLeverageRisk
P0Tap-outcome instrumentation + no-nav rateShipped (PR #526)Diagnostic for all belowLow
P1Profile-avatar captureMeasurement-gated on P01 screen + Settings subtreeLow–medium
P2In-place-overlay-as-navigable (Search)Design below; founder reviewSearch + any overlay-driven appMedium–high
P3Recharge / biller selection reachabilityMeasurement-gated on P0Several info screensMedium
P4Deep-section discovery (Mutual Funds / Gold)Measurement-gated on P0Info screens below the foldMedium

Why P1, P3, P4 are measurement-gated​

All three reduce to the tap-fidelity question P0 measures. Writing them before the no-nav rate is known would mean guessing at PhonePe's node tree and hard-coding positional heuristics (for example "tap the top-left corner"), which violates the standing no-hard-coded-heuristics rule and risks regressing every other app. The disciplined sequence is: land P0, run one PhonePe scan, read the no-nav rate and the per-tap DEBUG lines, then implement the fix the evidence points to.

  • If the no-nav rate is high on the home tiles and the profile avatar, the fix is tap-fidelity: resolve a large clickable container to a more specific child target (or the labelled leaf) rather than tapping the container's geometric center. P1 (profile avatar) is the smallest instance.
  • If the no-nav rate is low but sections are still unmapped, the gap is structural: in-place overlays (P2), a WebView-degenerate biller-selection screen that dedups onto its hub (P3), or scroll depth versus the no-progress guard (P4 — builds on PR #521).

P1 — profile-avatar capture (design)​

The profile entry is a top-left tap on Home. Two shapes are possible and P0's per-tap DEBUG output tells them apart:

  1. The avatar node is itself clickable. Then it is already enqueued (any tappable node is), and the gap is tap-fidelity — its bounds-center should hit it, so a no-nav here points at a resolver or overlay issue, not enqueueing.
  2. The avatar is an unlabeled leaf whose clickable ancestor is the whole top bar. Then the frontier enqueues the top-bar container, and its bounds-center is the middle of the bar — not the avatar. The fix is to resolve the container to the specific corner child, or enqueue the leaf as its own Target so the driver taps the leaf's center.

The fix must stay principled: broaden target resolution on a structural signal (a distinct interactive leaf inside a larger clickable container), never on a hard-coded screen position. Unit tested via FakeDeviceDriver with a container-plus-leaf fixture; verified live by the profile screen appearing in the map and the no-nav rate on that tap dropping to zero.

P2 — in-place-overlay-as-navigable (design; founder review required)​

Founder review required before implementation. P2 redefines what counts as a "screen." It is the biggest lever and the riskiest change — it affects every app, not just PhonePe. Do not implement without sign-off.

The gap​

PhonePe's Search (and QR's search-like surfaces) expand an overlay in the same activity: a search bar and results panel appear over Home without a navigation event. Rover's deduper treats the result as a structural variation of the same screen — no navigates_to edge, no distinct node — so the entire search surface is invisible to the map. The same shape appears in any app that drives major UI through in-place overlays (bottom sheets, expanding panels, modal drawers) rather than navigation.

The proposal​

Model a same-activity screen-signature delta as a navigable state: when a tap leaves the route unchanged but changes the normalized structural signature by more than a threshold (a new input field plus a results container appearing), mint a distinct overlay-state screen and wire a navigates_to edge to it, tagged as an in-place transition.

P0's screenChanged already computes the exact signal this needs (route-same, structure-changed) — so the overlay taps that P2 would capture are precisely the component taps P0 counts as navigated (structure changed) rather than no-nav. P0 therefore both motivates P2 and measures how many overlay transitions exist before any code is written.

Why it needs review​

  • It changes the screen-count contract. Every app with hover states, tooltips, expanding rows, or transient panels could see its screen count inflate. The threshold that separates "a real overlay state worth mapping" from "a trivial UI twitch" is a product decision, not a mechanical one.
  • It needs a per-app eval bar. Before enabling it broadly, we would want a golden set (like the SPEC-055 identity eval harness) that scores overlay-minting precision and recall so a change can be proven not to inflate counts on the existing mapped apps.
  • It interacts with dedup and identity. An overlay state that is minted here must also be re-recognized on a later visit, which touches the identity resolver — the highest-blast-radius part of the crawl.

Recommendation: gate it behind a flag, ship the eval harness first, and measure the overlay-transition count (via P0's navigated-tap breakdown) on PhonePe and two control apps before turning it on.

P3 — biller / recharge selection reachability (design)​

Recharge and Bills is reached (the nav-edge audit confirms the tap), but the biller selection screens do not appear as distinct nodes. Two candidate causes, distinguished by P0 plus a read-only capture:

  • WebView-degenerate: the selection list is a WebView whose rows report clickable=false (the SPEC-057 WebRegionOutliner case). If so, the fix is ancestor-context promotion of the rows, already the pattern for beans' route list.
  • Dedup collapse: the selection screen is structurally close enough to its hub that the deduper calls it SAME. If so, the fix is on the identity side, not the crawl.

Hard constraint: this fix maps the selection surface only. It must never advance into the pay step. The measurement and any live verification stay read-only or verify-before-tap on known-safe screens.

P4 — deep-section discovery (design)​

Mutual Funds, Gold, and Rewards info screens sit below the fold on Home and in sub-hubs. P4 builds on the bounds-aware scroll no-progress guard (PR #521). The open question — scroll depth versus the no-progress guard versus tap-but-no-nav — is answered by P0: if the tiles are reached but tapping them is no-nav, it is tap-fidelity (P1's fix generalizes); if they are never reached, it is scroll depth (extend the guard).

Method and safety​

  • Unit-test-driven. Every fix is exercised through the scripted FakeDeviceDriver so it does not depend on live-device access; new tests are auto-discovered by RoverTestSuite (BazelJUnit5AutoSuite) and verified via the started-count in test.log. bazel test //backend/rover/... must be green.
  • Read-only live diagnosis. Any live-device work is read-only (uiautomator dump, screencap, logs) or verify-before-tap navigation to known-safe info screens only — never open-ended autonomous crawling that could reach a payment completion. When unsure, do not tap.
  • Small PRs. Each fix is its own small PR off origin/main; humans merge.

Absolute safety​

The reference device runs a real-money PhonePe account on a physical Pixel. The following are non-negotiable:

  • Never tap Pay, Confirm, Send, a UPI-PIN pad, delete, unlink, or save-card.
  • Pay buttons are unlabeled canvas — there is no reliable text guard for them, so staying out of completion is enforced by not running open-ended autonomous crawls into payment flows, plus the multi-step-plus-PIN structure of those flows.
  • Device driving, if any, is verify-before-tap to known-safe info screens only. Prefer read-only diagnosis and unit tests. When unsure, do not tap.