feat(ios): read modal surfaces from a modal-scope pair
PR 2 of 3 for modal-scoped alert on iOS. Stacks on #4124. Host-only: no runner, alert, or snapshot behavior change.
ModalSurface.read pairs a plain and an honor-modal tree. A surface is the children the honor-modal tree keeps under a node where it removes others. When a kept child holds its own surface, that inner surface replaces the outer one, so an underlying modal's controls stay out. Keyboard branches offer no actions. The outcome is modal(surfaces), noExclusion, or unknown(reason). An identical pair never reads as absent.
SnapshotPresentedSurface.classify takes the reading as presenting evidence. The golden table gains modalScopeExpected on two corpus-derived cases. The TS rule is unchanged.
| Recording | Surface root | Actions offered | Excluded |
|---|---|---|---|
| SpringBoard alert, folded and unfolded | Alert (innermost of 3 levels) | Cancel, Open | none |
| Safari prompt, folded | SFDialogView's container | Cancel, Open | Back, bookmark, add, Tabs, refresh |
| Safari prompt, unfolded | same | Cancel, Open | 7 toolbar buttons |
| Settings Clear History sheet | sheet container | dismiss popup, both close buttons, checkmark, 5 cells, Close All Tabs, Clear History | none |
| SpringBoard home, Settings root | noExclusion | none | ā |
Remaining gaps (not recorded yet): iPad popovers, React Native Modal, SFSafariViewController, physical devices.
Checks: swift test, TS parity tests, check:affected. Tests catch mutations to nested replacement, keyboard skip, dismiss marker, and modal evidence.
š¤ Generated with Claude Code