Skip to content

refactor(ios): decide bridge-failure scope and warning once - #3914

Merged
thymikee merged 2 commits into
mainfrom
refactor/bridge-failure-disposition
Oct 22, 2026
Merged

thymikee merged 2 commits into
mainfrom
refactor/bridge-failure-disposition

Conversation

@thymikee

@thymikee thymikee commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

One classifier, bridgeFailureDisposition, now decides both whether a bridge failure opens the generation's circuit and which fallback-warning sentence the capture carries. Previously two owners classified the same failure — opensGenerationCircuit for scope, string comparisons against duplicated constants for wording — and they could drift apart (#3874 and #3895 each had to teach both owners the same lesson). Behavior is unchanged for every reachable failure; unknown guest codes keep the conservative generation default.

Touched files: 3. No scope expansion. Production lines: snapshot-route.ts 622 → 599, new policy module 54 — net +31 against the plan's aspirational 30–70-line reduction; the classification data moved out of the route without shrinking, in exchange for one owner of the decision. Gross diff 246 lines including tests.

Validation

Tested SHA: 0a58d5bdab. pnpm typecheck, pnpm lint, pnpm format pass; snapshot-route.test.ts unchanged, all route/policy/observability cases pass. check:affected --run --head receipt recorded per head in comments.

  • Red/green: the capture-scoped second-capture cases FAIL on 989804f11f^ (pre-fix(ios): scope bridge ownership refusals to the capture #3874), the window case FAILs on ba953a8066^ (pre-feat(ios): route iPad Simulator snapshots through the host AX bridge #3895), all pass at head.
  • Mutation: forcing the foreground consequence to generation scope fails exactly the route's second-capture assertions and policy tests.
  • Live, owned sims: iPhone — alert refusal gave the capture-scoped warning with XCTest serving the alert; after dismissal the same generation captured bridge-served with warnings: null. iPad Stage Manager windowed — the refusal warning repeated identically across 6+ captures, zero circuit-disabled in the session log.

CI obligations: standard lanes. Unresolved risks: none known.

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

The capture-scoped bridge refusals were classified twice: once to decide
whether the app generation's circuit opens, and again to pick the fallback
warning sentence. A new refusal could keep the circuit and still publish a
generation-wide warning, or the reverse (#3874, #3895). One classifier in
snapshot-route-failure-policy.ts now returns a single disposition that the
route consumes for both decisions; the standalone constants, the refusal
set, and the duplicated warning branches are deleted.
The guest failure code is an open string. An object-literal table would answer a
code that names an inherited property, so the lookup uses a map and every
unknown code keeps the conservative generation default.
@thymikee

Copy link
Copy Markdown
Member Author

Validation receipts:

  • 147400df72: check:affected --run --head 147400df72841978abb4b2b0fdbf2e319d548aa0 → all runnable checks passed, exactCommit=true.
  • 0a58d5bdab: check:affected --run --head 0a58d5bdab52372df1bf7b11b921910d33967af2 → all runnable checks passed, exactCommit=true (receipt run-A4a5DE).

@github-actions

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.56 MB 5.56 MB -43 B
Package (unpacked) 5.56 MB 5.56 MB -43 B
Package (download) 1.68 MB 1.68 MB +9 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 39.7 ms 39.7 ms +0.0 ms
CLI --help 81.2 ms 82.1 ms +0.8 ms

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready to merge. I reviewed 0a58d5b and found nothing that needs to change. All 16 checks are green at that commit, and there are no conflicts. The live simulator runs in the PR body have no attached logs, but the unchanged route tests cover this behavior-preserving refactor, so that is enough. I did not run the tests locally.

Not blocking, and you can take or leave these: snapshot-route-failure-policy.test.ts re-asserts through the helper what the route tests in snapshot-route.test.ts already cover, so only the unknown-code case is new and the rest could go; FallbackReason mixes the disposition object with bare string reasons, which forces the two typeof reason === 'string' checks in fallbackWarning, so giving route reasons the same shape would leave one discriminant to switch on; and the four doc blocks in snapshot-route-failure-policy.ts restate one rationale and could collapse into the one on bridgeFailureDisposition.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 22, 2026
@thymikee
thymikee merged commit 118551c into main Oct 22, 2026
16 checks passed
@thymikee
thymikee deleted the refactor/bridge-failure-disposition branch October 22, 2026 19:50
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-22 19:51 UTC

thymikee added a commit that referenced this pull request Oct 22, 2026
The refusal wording belongs to the snapshot-route failure policy, not to
this fix.

Refs #3899
thymikee added a commit that referenced this pull request Oct 24, 2026
* origin/main:
  fix(android): bound the dumpsys reads behind dialog and foreground checks (#3936)
  feat(android): detect Fire OS devices as Android TV targets and document Fire TV support (#3923)
  docs(macos): document Electron/Chromium app automation through the accessibility tree (#3932)
  test(capture-kit): encode solid test PNGs unfiltered to stay inside the unit budget (#3933)
  fix: guard bounded lifecycle and claim recovery (#3911)
  fix(ios): clear a long text value wherever the caret lands (#3908)
  fix(snapshot): stop iOS 27 floating-bar containers from covering content (#3912)
  ci: cache and bound the Linux desktop dependency install (#3919)
  fix: refuse resource operations in bounded composites (#3910)
  refactor(ios): decide bridge-failure scope and warning once (#3914)
  refactor(capture-kit): encode post-gesture stabilization baseline provenance (#3915)
  perf: expect the persistent session on Android snapshot-raw (#3913)
  test(ios): hold the fill read-count pin to the route it measures (#3909)
  perf(ios): admit tap text-input probes from fresh target evidence (#3881)
  feat(perf): time every Android command against the fixture app (#3902)
  chore: mark 0.21.31 as released
thymikee added a commit that referenced this pull request Oct 25, 2026
* fix(ios): publish a windowed iPad app's capture in screen space (#3899)

An iPad window that does not fill the screen reports its own screen box
while its subtree arrives anchored at its own top-left. The capture
published a screen-space viewport around window-relative rects: the fold
clipped content to (w - x) x (h - y), a rect named no screenshot
position, and a synthesized tap landed at the point minus the window
origin.

SnapshotContentTranslation reads the one vector from a carrier's
(0,0)-anchored copy of the viewport box to the viewport itself, and
SnapshotGeometrySpace.normalized translates the subtree it declares in
the same pass that turns a quarter-turned subtree.
CoordinateSpaceRotation now turns axes only; the digitizer origin stays
where the display put it. Replayed from
contracts/fixtures/window-coordinate-space.json in both languages.

Refs #3899

* revert: keep the snapshot-route warning owned by #3914

The refusal wording belongs to the snapshot-route failure policy, not to
this fix.

Refs #3899

* fix(ios): read a placed window's snapshot frames at their screen place (#3899)

XCUIElement.frame reports a placed window's content in screen space, but
XCUIElementSnapshot.frame reports it in the window's own content space,
so the text-input probe saw two spaces on an iPad Windowed Apps window
and refused a fill whose point was on the screen. The probe, the focus
tap, and the typing reader now read the same SnapshotContentTranslation
vector the capture reads, from the two live boxes that name it.

Refs #3899

* fix(ios): keep caret repositioning and keyboard evidence decision stable on a placed window [ #3899 ]

* fix(ios): translate only snapshot frames and pin the unmeasured landscape case [ #3899 ]

* refactor(ios): keep the window-content translation with the capture that runs it [ #3899 ]

* refactor(ios): make ResolvedTextEntryInput own the probe-side screen frame (#3899)

The readiness retap re-read the input as a bare XCUIElementSnapshot and tapped
its raw center through tapAt, which takes screen points, so on a windowed iPad
the retap landed outside the window. The live window lookup also ran whole-app
queries four to six times per fill on every iOS device.

Every snapshot frame that becomes a dispatched point now passes through the one
content translation, held on ResolvedTextEntryInput.frameOnScreen. The vector is
read once per command and stored on TextEntryTarget.contentTranslation; the probe
matching, focus tap, refresh point, caret tap and readiness retap all read that
one property. The threaded translation: parameters and the per-read lookups are
gone, and the once-per-command keyboard read in the typing command is reverted to
the base behavior with the retap fixed.

* docs(ios): name the probe-side owner and the screen-space dispatch basis (#3899)

Amends ADR 0004 to name ResolvedTextEntryInput as the probe-side owner of the
content vector, cites the measured Add-button rect at (659,321), records that the
runner republishes a windowed capture in screen space where the bridge-refusal
warning said it was offset, and names screen space plus the app.frame origin
assumption in the interactionCoordinate comment.

* refactor(ios): cut the screen-space comments and the duplicated frame offset (#3899)

The vector on the target replaces the offset arithmetic in
textEntryRefreshPoint, which read the snapshot frame and added the vector
itself. One owner now: ResolvedTextEntryInput.frameOnScreen.

The comments that restated the capture's rule at every call site are cut to
the constraint the code cannot show. SnapshotCoordinateSpace.swift ships in
the npm package without comment stripping, so its prose lives in ADR 0004 and
the golden table instead; the same rule text is removed from the TypeScript
decoder's foot note.

* refactor(runner): make the text-entry content vector a non-defaulted tri-state

frameOnScreen becomes optional and carries the #3060 rule: it names no box
without a snapshot or under an unknown content space, so the four consumer
guards collapse into one nil check. liveContentTranslation returns
TextEntryContentTranslation (known/none/unknown) instead of CGSize?, so a
failed window read can no longer look like full screen and send the
readiness retap to a raw window-local point. The vector now rides on
TextInputAtPoint, cutting one parameter from the probe signatures. retapAt
skips the point on unknown; probe matching and the focus tap fall back to
element.frame. Adds the unknown-space retap unit test.

* feat(runner): log the readiness retap point and its skips

* fix(runner): take the probe vector from the resolved window box alone

Reply to review 5642729478:
- liveContentTranslation builds the zero-origin reported box from the
  resolved window's size, so an app.frame that unions transformed
  subtrees cannot silently cost a placed window its vector (P1 thread 1).
- Synthesized dispatch turns about the display's oriented box, never a
  placed window's smaller box; landscape under Windowed Apps stays
  unmeasured and the ADR says so (P1 thread 2).
- requireTextInputAtPoint passes the live vector (P2 thread 3).
- ADR 0004 distinguishes the refusing bridge from the converting runner
  and states the focus-tap/retap refusal rule (P2 threads 4-5).
- The initial focus tap dispatches the caller's requested point when the
  read names no screen box; the live element.frame is never dispatched,
  and focusTapPoint is pinned by a unit test (P2 thread 7).

* refactor(runner): shrink the windowed-iPad screen-space change to its rule

Keep the accepted design (one SnapshotContentTranslation rule in
normalized, ResolvedTextEntryInput.frameOnScreen, a non-defaulted
known/none/unknown vector read once per command, rotation about the
sole display) and cut what did not serve it: cosmetic renames and
reformatting, comments that restated code, the one-caller focusTapPoint
helper, TextInputAtPoint's copy of the target's vector, the duplicated
sole-display math, duplicate unit tests and fixture rows, and most of
the ADR 0004 amendment prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(snapshot): pin translated hittability and the offset rule's empty-box guards

Review follow-ups to the shrink: assert a translated node's hittability,
add the two fixture rows that discriminate the positive-finite guards,
keep windowContentCases names unique, read the display size from the
fixed coordinate space, and correct ADR 0004's vector-read and gap
wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runner): restore the discriminating window-content rows and the retap skip reason

Put back the moved-window, keyboard, and SpringBoard rows in
windowContentCases, and log one skip line carrying its reason when the
readiness retap names no screen point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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