feat(resolver): Phase 1 — RNTL-style discovery resolver (byRole/byText/byPlaceholder + selector bundle)#362
Conversation
…(design) Two-phase design: an RNTL-style discovery ladder (byRole/byText/byPlaceholder) on the live fiber tree emitting a selector bundle, projected to Maestro YAML via a fail-closed, CONTAINS-aware, TS-owned-gated fallback ladder, with bundle-aware self-heal. Grounded by a Codex adversarial debate (approach selection) + a 6-agent hardening pass across rn-dev-agent / RNTL / maestro-runner. Captures the verified CONTAINS-not-EXACT native-match verdict and two Section-1 corrections: the maestro-runner selector gate is advisory-only (so the hard gate is re-homed into the TS projector), and the RNTL helpers must be ported against a fiber->host adapter rather than copied verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9 bite-sized TDD tasks: Task 0 vm test harness, then __match / __hostKind / __role / __accessibleName / __hidden ports, fail-closed truncation, resolveLadder + interact() routing, and anchor capture — implementing the precision half of the selector-resolver design spec. Each task is failing-test (vm + buildFiber) -> ES5 port inside the INJECTED_HELPERS IIFE -> passing test -> commit, with source-drift guards. Drafted via an 8-agent grounding + drafting workflow against real RNTL v14 and injected-helpers source; task-number cross-refs and the bounds-null caveat reconciled in self-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-matcher form ({value,exact?} | {regexSource,regexFlags?}); trim+collapse
normalizer that does NOT lowercase (case-insensitivity lives in the non-exact
compare). Bumps HELPERS_VERSION 26->27.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port RNTL host-component-names (isHostText/TextInput/Image/Switch/ ScrollView/Modal) into a single hostKind(fiber) that maps a host name (string fiber.type or fiber.type.displayName/name) to one of text|textinput|image|switch|scrollview|modal|null. Name lists widened to native view names (RCTSinglelineTextInputView, RCTImageView, RCTModalHostView, ...) since live fibers expose the platform view name. Placed at IIFE top level (not inside getTree); returns null for Views, user components, text nodes, and null types. HELPERS_VERSION already at 27 from Task 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 bumps HELPERS_VERSION past 26; the two #321 guards pinned it to exactly 26 and broke on the first bump. Assert the >=26 baseline instead so feature branches bump freely without false regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add normalizeRole + __role (explicit role → accessibilityRole image→img → host Text → none), reusing __hostKind from Task 2. Does not reuse the digest inferRole (which defaults Pressable to button); divergence pinned by test. Bump HELPERS_VERSION 26→27. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port RNTL computeAccessibleName + computeAriaLabel + getAriaLabelledByIds + joinAccessibleNameParts to ES5 on __RN_AGENT. labelledBy nativeID refs are resolved to the referenced node's plain TEXT CONTENT (port of RNTL getTextContent via __refTextContent), NOT by recursively computing its accessible name — this matches RNTL's computeAriaLabel and makes a malformed labelledBy cycle (A->B->A) safe (no stack overflow). The normal child-name recursion in computeAccessibleName is retained. Inline host-text parts join with '' (so 'Sign'+'In' -> 'SignIn'), otherwise ' '. TextInput placeholder is the name only at root. HELPERS_VERSION unchanged (27). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Climbs fiber.return, flattens memoizedProps.style arrays manually (no StyleSheet.flatten in-page), and treats aria-hidden / accessibilityElementsHidden / importantForAccessibility=no-hide-descendants / display:none / aria-modal host siblings as hidden. opacity:0 is not hidden. HELPERS_VERSION stays at 27 (already bumped on this branch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the silent `if (findCount > 8000) return;` cap with a
rootsSeeded-scaled node budget (Math.min(40000, 8000*roots)) plus a 3s
wall-clock guard, mirroring the salient-digest budget. On trip, set a
findTruncated flag and short-circuit interact() to return
{error:"Resolution truncated", truncated:true, scanned:<n>, hint:...}
BEFORE any tier[0] pick, "Component not found" branch, or onPress fire —
so a partial scan can never trigger a false action. HELPERS_VERSION stays
at 27 (single branch bump).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add __RN_AGENT.resolveLadder(specJson) composing __match/__role/ __accessibleName/__hidden/hostKind into byRole/byText/byPlaceholder predicates. Collect-all across renderers: 0 -> Component not found, >1 -> Ambiguous component match (count + descriptors), 1 -> bundle. Hidden excluded unless includeHidden; bundle.bounds is null in Phase 1. interact() routes role/name/text/placeholder specs (no testID/ accessibilityLabel) through the resolver and presses the found fiber or its nearest onPress ancestor, via the fiber-returning twin __resolveLadderFiber. Legacy testID/accessibilityLabel paths and Task 6 fail-closed truncation untouched. HELPERS_VERSION stays 27 (no bump). Also widen the find-active-renderer-migration B145 guard window (3000 -> 8000 chars): a pre-existing brittle source-position assertion that Task 6 already pushed out of range; the guarded findFiber-in- forEachRootFiber behavior is intact, only its byte offset moved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ixes; HELPERS_VERSION single-branch-bump policy) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add __collectAnchors — a bounded (depth 8) fiber.return ancestor walk
mirroring the setFieldValue ancestor walk — recording nearest-first
{testID, text, relation: childOf, depth, provenance} entries for any
ancestor with a testID/nativeID or explicit accessibility label
(__ariaLabel only, no recursive child traversal so bare host Text
nodes are skipped). provenance is authored-testID for testID/nativeID
ancestors, else text. Populate bundle.anchors in resolveLadder.
Expose __collectAnchors on the __RN_AGENT public surface.
Full suite: 2437/2437 pass, 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fix) Task 8 changed src/injected-helpers.ts but did not rebuild the tracked dist/injected-helpers.js. dist is the shipped artifact (imported by cdp/setup.ts; no build hook), so the committed resolver silently omitted bundle.anchors; CI masked it by rebuilding before tests. Regenerated dist so the committed artifact matches src. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f3e4ebf3e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… (live-device bug) Live-simulator testing revealed every RN text/input element matched TWICE in resolveLadder — once as the composite fiber (Text/TextInput) and once as its host child (RCTText/RCTSinglelineTextInputView), both passing hostKind — so byText/byPlaceholder always fail-closed as Ambiguous on a real device. The vm tests missed it because buildFiber made one node per element. Add __deepestOnly (RNTL matchDeepestOnly parity): drop any match that is an ancestor of another match, keeping the deepest; distinct siblings stay ambiguous. Applied in both resolveLadder and __resolveLadderFiber. Bump HELPERS_VERSION 27->28. Verified live on the running test app: byText "Go to Dashboard" and byPlaceholder "Add a task..." now resolve uniquely with correct anchors. Full suite 2495/2495. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validated live on the simulator (real device fibers)Injected the built Found + fixed a device-only bug ( After the fix (live results):
Full suite 2495/2495.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e53471e0e1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 557c4527b5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ation, byText content, non-press guard PR #362 review (chatgpt-codex-connector) — 4 P2 findings, all verified + fixed: - #1 cdp_interact now accepts the ladder selectors (role / text / placeholder / exact / includeHidden): relax the host guard, forward the fields, add them to the zod schema. The resolver is reachable via the supported tool, not only via raw eval. - #2 resolveLadder / __resolveLadderFiber had their own silent 8000-fiber cap; a duplicate past the cap could leave matched.length===1 and press the wrong element. Now fail-closed (truncated:true) with a rootsSeeded-scaled budget + wall-clock guard, matching the legacy findFiber path. - #3 byText matched __accessibleName (accessibilityLabel precedence) instead of the visible text content — now uses __refTextContent (getTextContent port); accessible names stay for byRole/name; bundle.text is text content too. - #4 the ladder interact() branch pressed regardless of the requested action — now fails closed for any action other than press. Verified live on the running test app; full suite 2502/2502. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed all four findings in
Thanks — all four were accurate. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab9f8936cb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… testID matcher PR #362 (chatgpt-codex-connector) — 2 P2 findings: - #5 __hostKind now recognizes Android's `AndroidTextInput` host name; without it byPlaceholder/byText returned null for every TextInput in Android sessions. - #6 resolveLadder / __resolveLadderFiber now match a `testID` spec — the spec already accepted testID but had no matcher, so resolveLadder({testID:'x'}) returned Component not found for mounted ids. Bump HELPERS_VERSION 28->29 (the injected surface changed across the review fixes; a same-version re-inject is skipped by the __v freshness guard). Verified live (testID + byText resolve on the running app) + full suite 2507/2507. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round 2 addressed in the latest commit (full suite 2507/2507, testID resolution verified live):
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b9b84b4028
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…element duplicates #7: __deepestOnly blanket-dropped every ancestor match, so two DISTINCT nested components both matching a selector (e.g. an outer card button + an inner button both named "Settings") collapsed to the inner one and got silently pressed instead of failing closed as Ambiguous. Now drop a match's nearest matching ancestor only when they are the SAME element: (1) composite+host pair (the composite wrapper of a host match), or (2) the same testID/nativeID (one element whose id propagates across nested fibers, e.g. a tab button). Distinct nested matches stay Ambiguous. The same-testID arm also keeps resolveLadder({testID}) at found:1 (a tab's id spans ~7 fibers on-device). Bump HELPERS_VERSION 29->30. Verified live (testID / byText / byPlaceholder resolve; nested stays ambiguous); full suite 2509/2509. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round 3 (
|
…opt-out + role normalization) #8: byRole now respects the explicit accessible={false} opt-out (RNTL isAccessibilityElement) — an opted-out duplicate (e.g. an offscreen Pressable kept mounted) no longer causes false ambiguity or gets pressed. #9: the REQUESTED role is now normalized (normalizeRole(spec.role)) like the element side, so byRole({role:'image'}) matches an element whose accessibilityRole 'image' normalizes to 'img' — callers pass the RN prop value, not 'img'. Bump HELPERS_VERSION 30->31. Full suite 2513/2513; byRole verified live (buttons still resolve, no accessible-exclusion regression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round 4 addressed in the latest commit (full suite 2513/2513; byRole verified live):
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d689212a91
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…cessibility-element predicate
The earlier accessible={false} exclusion still let a plain
<View accessibilityRole="button"> with accessible undefined match byRole, even
though RNTL / screen readers would not expose it. byRole now requires the full
isAccessibilityElement predicate (__isA11yElement): Text / TextInput / Switch and
Image+alt are accessibility elements by default; everything else must opt in
with accessible={true}. Applied in both isCandidate and __resolveLadderFiber.
Verified live: real RN buttons expose accessible:true in their fiber
memoizedProps (40/48 role=button fibers — host RCTView + inner Pressable layers),
so byRole({role:'button'}) still resolves them (count unchanged) — only plain-View
role props are now excluded. Updated synthetic byRole tests to model real a11y
elements (accessible:true). Bump HELPERS_VERSION 31->32. Full suite 2518/2518.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round 5 addressed in the latest commit (full suite 2518/2518; verified live on the running app):
I probed the live fiber tree before committing: real RN buttons expose |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17c5b8847f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var target = matched[0]; | ||
| var tprops = target.memoizedProps || {}; | ||
| var bundle = { | ||
| testID: tprops.testID, |
There was a problem hiding this comment.
Preserve nativeID-backed IDs in selector bundles
When the resolver matches an element by nativeID (the predicate above accepts tprops.testID === spec.testID || tprops.nativeID === spec.testID, and placeholder/text matches can also land on a nativeID-only element), this bundle drops the durable id because it only copies tprops.testID. In that scenario resolveLadder({testID:'email-native'}) reports found:true but returns no bundle.testID, so the projector/self-heal path loses the strongest selector and falls back to weaker text/name fields; mirror the matcher/anchor logic here with tprops.testID || tprops.nativeID.
Useful? React with 👍 / 👎.
Phase 1: Discovery resolver (precision half of the selector-resolver design)
Adds an RNTL-style discovery ladder to the in-app fiber matcher, ported against a fiber→host adapter, emitting a fail-closed selector bundle.
docs/superpowers/specs/2026-06-19-rnt-selector-resolver-projector-design.mddocs/superpowers/plans/2026-06-19-rnt-selector-resolver-phase1.mdWhat's added (all ES5, inside the
INJECTED_HELPERSIIFE)__match(matcher + normalizer),__hostKind(fiber host classifier),__role(getRole),__accessibleName(computeAccessibleName;labelledByresolved via text-content, cycle-safe),__hidden(isHiddenFromAccessibility).interact()'sfindFiber— the old silent 8000-node cap could silent-pick or mis-report "not found".resolveLadder— composes the helpers intobyRole(+name)/byText/byPlaceholder, with fail-closed multiplicity (Ambiguous), hidden-exclusion by default, and a selector bundle.interact()routing through the ladder — provably gated so it cannot alter thetestID/accessibilityLabel/ truncation paths.bundle.anchorsancestor walk for durable re-selection.test/unit/helpers/inject-harness.js).Quality
dist/that omitted Task 8's anchors; rebuilt infix(build)).labelledBycycle is proven bounded.Scope / follow-ups (Phase 2/3 — not in this PR)
resolveLadder/__resolveLadderFiberpredicate; documentmatchescap-at-10 vs truecount;__refTextContentjoin''fidelity fix.git diff --exit-code -- scripts/cdp-bridge/dist/after build — the committeddistis the shipped artifact (no build hook), and this class of stale-mirror bug already bit once.🤖 Generated with Claude Code