* 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>
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 —opensGenerationCircuitfor 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.ts622 → 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 formatpass;snapshot-route.test.tsunchanged, all route/policy/observability cases pass.check:affected --run --headreceipt recorded per head in comments.989804f11f^(pre-fix(ios): scope bridge ownership refusals to the capture #3874), the window case FAILs onba953a8066^(pre-feat(ios): route iPad Simulator snapshots through the host AX bridge #3895), all pass at head.warnings: null. iPad Stage Manager windowed — the refusal warning repeated identically across 6+ captures, zerocircuit-disabledin the session log.CI obligations: standard lanes. Unresolved risks: none known.
🤖 Generated with Claude Code