Skip to content

fix(rn-fast-runner): honest hittable computation in iOS snapshots (#395)#519

Merged
Lykhoyda merged 18 commits into
mainfrom
fix/395-hittable-computation
Jul 10, 2026
Merged

fix(rn-fast-runner): honest hittable computation in iOS snapshots (#395)#519
Lykhoyda merged 18 commits into
mainfrom
fix/395-hittable-computation

Conversation

@Lykhoyda

@Lykhoyda Lykhoyda commented Jul 9, 2026

Copy link
Copy Markdown
Owner

What & why

Closes #395. The iOS runner's per-node hittable flag now means "enabled and its center is on-screen" (plausibly tappable, half-open viewport bounds) instead of the old occlusion heuristic.

The old computedSnapshotHittable walked later snapshot nodes and marked a node non-hittable if any later node's frame contained its center. On real RN screens this counted trailing transparent full-screen containers (gesture-handler roots, portal hosts, collapsed bottom-sheet wrappers) as occluders and drove nearly every node to hittable=false — poisoning device_find candidate ranking, device_batch's dead-control annotation, and starving the hittable-first screen-rect union (#517) into its all-nodes fallback. Front-most is unrepresentable from XCUIElementSnapshot data anyway: RN modals get their own UIWindow, so occluded content is absent from the XCUI tree entirely.

The refusal half of the original #395 report ("no longer hittable" errors on modal screens) was a stale-ref message already fixed by #396. This PR is the re-scoped remaining half: making the emitted flag honest.

Approach

Two pure, unit-testable predicates land in a new SnapshotPredicates.swift in the RnFastRunnerUITests target:

  • computeSnapshotHittable(enabled:frame:viewport:)enabled ∧ non-empty frame ∧ center-in-viewport, with an explicit half-open [min, max) bounds check (edge policy is deterministic, not delegated to CGRect.contains).
  • shouldIncludeSnapshotNode(...) — snapshot filtering, with no hittable parameter. Under the old always-false flag, filtering was de-facto content/type-based; making that explicit keeps snapshot sizes stable while hittable gains its new meaning.

RnFastRunnerTests+Snapshot.swift is rewired to call both, and the dead occlusion machinery is deleted (computedSnapshotHittable, isOccludingType, flattenedSnapshots, laterSnapshots, plus the flatSnapshots/snapshotRanges context fields). The collapsed-tab fallback's real XCUIElement.isHittable is untouched (out of scope). No wire-shape change (RUNNER_PROTOCOL_VERSION stays 1); host runner copies regenerated via build:host-runtimes.

No TypeScript changes — consumers start receiving meaningful data for free.

Testing

  • Native XCTest (on the booted iPhone 17 sim): 38/38 green, including 16 new cases that exercise the compiled predicates on-device — enabled∧onscreen→true, disabled→false, empty/null frame→false, off-screen center (wizard pane at x=423 on a 402pt viewport)→false, half-open edge policy, and filtering fully independent of hittable. The full bundle compiling after the deletions proves no dangling reference survived.
  • Core TS suite: 2,983/2,983 green. No dist/ churn (no TS sources on this branch). The fix(story-06): post-merge review fixes — hittable-first screen rect + 4 more (#387) #517 hittable-first screen-rect union was written for honest hittable semantics — this PR activates its primary branch on iOS; the story-06-screenrect-system-windows tests stay green.
  • Android parity (code-read, no change): CommandDispatcher.kt:168-178 already maps hittable from UIAutomator's visible-to-user attribute (native visibility, no occlusion heuristic) — consistent with the new iOS semantics.

Device evidence

Checkout-built (new-code) runner, real device (iPhone 17 sim), expo-dev-client launcher — a real screen containing 20 stacked full-screen (402×874) Other containers, the exact pattern the old heuristic penalized: 80/80 nodes hittable, disabled→false invariant holds, no uniform-false pathology.

⚠️ Live re-measurement on an RN modal screen (the documented 0/226 Tasks-screen and 42/45-false TaskWizard baselines from the prior-session investigation recorded in the spec) was blocked this session by the environment: the workspace dev-client would not attach the JS bundle (Metro up on :8081 but 0 Hermes debug targets — confirmed by cdp_status and cdp_restart hardReset), and maestro-runner 1.0.9 could not tap the SpringBoard "Open" confirm. The new predicate producing those ratios is directly pinned by the on-simulator unit tests above.

Docs

Spec: docs/superpowers/specs/2026-07-09-395-hittable-computation-design.md · Plan: docs/superpowers/plans/2026-07-09-395-hittable-computation.md

🤖 Generated with Claude Code

Lykhoyda and others added 17 commits July 9, 2026 20:53
Investigation showed the refusal half of #395 was fixed by #396 (@@ref
double-prefix; the 'no longer hittable' wording comes from the stale-ref
path, not a hittability gate). The remaining defect: the iOS runner's
computedSnapshotHittable occlusion loop counts trailing transparent
full-screen containers as occluders, marking every node hittable=false
on all screens (85/85 on the wizard modal, 226/226 on the Tasks screen,
device-verified). Approved design drops the occlusion term (enabled ∧
non-empty frame ∧ center-in-viewport) and decouples shouldInclude
filtering from the emitted flag.

Refs #395

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…395)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebased fix/395-hittable-computation onto post-#498 main: canonical runner
source is packages/rn-fast-runner (host copies are generated via
build:host-runtimes), TS moved to packages/rn-dev-agent-core, changeset
package is rn-dev-agent-core, and Task 4 gains the PR #517 hittable-first
screen-rect consumer compatibility check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er check

Codex-pair review flagged the CGRect.contains-delegated edge test as
environment-dependent; the predicate now specifies [min, max) bounds
directly and both edge tests assert deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…predicate (#395)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uple filtering (#395)

hittable now means "enabled and its center is on-screen" (half-open
viewport bounds). The later-node occlusion loop counted transparent
full-screen RN containers as occluders and marked every node false on
real screens; real modal occlusion is unrepresentable anyway since RN
modals live in their own UIWindow. Snapshot filtering is decoupled from
hittable (its de-facto behavior under the always-false flag), keeping
snapshot sizes stable. Host runner copies regenerated via
build:host-runtimes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mment-budget note

Snapshot filtering decoupled from hittable can only shrink node counts
(trailing contentless overlay wrappers the old algorithm marked hittable
are no longer included), never grow them — plan + changeset now say so
precisely. Plan comment budget updated to include the reviewer-requested
half-open-bounds note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Android scoped out explicitly (two internally-inconsistent hittable
sources — snapshot visible-to-user vs find isEnabled — filed as #520)
and the rollout question settled: the version gate is env-passed at
spawn so a runner-version-constant bump cannot detect artifact
staleness; per-version cache dirs + release-process artifact evidence
cover rollout instead.

Refs #395, #520

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- spec §4: original Android instruction now defers to its own out-of-scope
  amendment (verify-only; Android changes are a separate follow-up)
- spec §5: mark the non-functional 'bump runner version constant' mitigation
  as must-not-attempt, pointing to the settled amendment
- plan Step 6: note getCachedScreenRect requires the device-wrapper path,
  not direct runIOS — drop or reroute the screenRect assertion accordingly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scoping

- spec §5: new amendment making 'corepack yarn build:host-runtimes' a
  required rollout step (marketplace installs copy the generated host
  runner copies, not packages/rn-fast-runner) — codex-pair HIGH
- spec out-of-scope: no Android change on this branch, unconditionally
- plan Step 6: retitled/rescoped to the client-mapping + ranking path it
  actually exercises; ref-map/screenRect coverage deferred to the wrapper
  path + existing story-06 TS unit tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 1-3)

Honest hittable data (#395) activated three latent consumer assumptions
calibrated to the old constant-false flag:

- screen-rect union (#517 path): a center-on-screen straddling card
  inflated the estimate past the physical viewport (off-screen taps,
  scrollintoview false-pass). Application/Window node extents now cap
  the union on iOS; Android (no window-typed nodes) keeps the uncapped
  union per the #517 CI-Android constraint.
- settle-hash: hittable encoded an UNquantized center-vs-edge bit that
  defeated the 4px bounds quantization (settle timeouts, phantom
  hierarchyChanged suppressing Story 05 re-taps). Dropped from the node
  hash — it is derived from enabled + rect + viewport, both hashed.
- device_find ranking: the +1000 hittable bonus outranked real controls
  with on-screen body text. Type priority is now primary; hittable only
  breaks same-type ties.

2988/2988 unit tests.

Refs #395, #519

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
 finding 4)

A pre-#395 runner artifact passes every staleness gate: the wire protocol
is unchanged, no command verb was added, and runnerVersion is env-passed
at launch — so the version-skew check reports whatever plugin spawned it.
The artifact keeps silently emitting hittable=false for every node.

HONEST_HITTABLE is now compiled into the /health capabilities, making
absence-on-a-healthy-probe the one artifact-truthful staleness signal.
The client queues a warn-once advisory through the existing
pendingFastRunnerArtifactNote channel (surfaced as meta.note by the open
and dispatch paths) — advisory only, never a forced rebuild, honoring
the no-silent-multi-minute-xcodebuild contract.

Host-runtime mirrors regenerated. 2993/2993 unit tests.

Refs #395, #519

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
copyCleanDir copied the entire runner package including build/DerivedData,
then stripped it post-copy. DerivedData's CompilationCache is a
hardlink/APFS-clone-dedup'd store: 127M on disk materialized as 16G+ when
cpSync expanded every link, filling the disk (ENOSPC) before the strip ran.
Exclude build output via cpSync's filter so it is never copied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- changeset: note HONEST_HITTABLE capability + stale-artifact advisory
- reword dead-control language in snapshot predicates (finding 6)
- align iOS fallback behavior with the capability gate (finding 7)
- drop dead occlusion logic left from the old heuristic (finding 8)
- refresh stale comments referencing the removed heuristic (finding 9)

Mirrors regenerated via build:host-runtimes; 2993/2993 unit tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Lykhoyda Lykhoyda merged commit 15def1d into main Jul 10, 2026
14 checks passed
@Lykhoyda Lykhoyda deleted the fix/395-hittable-computation branch July 10, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: iOS modal subtrees report hittable=false → device_batch/find press paths refuse on modal screens

1 participant