Conversation
Size Report
Startup median (7 runs, lower is better):
|
|
At 38fb5ed the refusal sits in one place, The check and the tap use different points. A ref tap after the keyboard goes away may still be refused. The ref path reads the stored tree, and the keyboard commands do not appear to refresh it. So #2589 says coordinate-only taps must be guarded too, but this PR only warns on them and says Smaller: A smaller-design question: could one guard take the dispatched touch point and reuse the #1542 re-check hook, instead of adding help text for the recovery? The live run covers iOS only; Android IME detection still needs a device run. CI is green, and this PR is stacked on #2601. |
38fb5ed to
09856dc
Compare
|
Rebased on the updated #2601 and force-pushed as The check and the tap used different points (finding 1). Fixed, and now one point is in play per acting tap. The new case pins the dangerous direction from both sides: a target whose center stays 3 pt above the key plane while its interactive child pushes the dispatched aim below it is refused, and the same tree with the keypad lifted off the bottom edge shows the tap aiming under the plane. So the assertion is about the aim, not about the center. A ref tap after the keyboard goes away (finding 2). The double-check cannot be spent here, because there is nothing to spend it on. Coordinate-only stance (finding 3). The warning is the stance, so I said so on #2589 rather than leaving the issue and the code telling different stories: #2589 (comment). One line of why: a raw coordinate names a point and not an element, it is the only way to tap a keyboard's own control, and that path never captures the tree the band is measured against — refusing on an arbitrarily stale capture is a guess, and a wrong guess there costs a user a tap they cannot re-explain. The disclosure carries the same typed reason and the band it measured. Smaller items. Could one guard take the dispatched point and reuse the #1542 hook? It takes the dispatched point now, which is the half that was missing. The hook is a different question with a different owner: it re-confirms the target's rect through a tree-independent read to rescue an off-screen refusal, and its rescue contract is a live rect to tap. Here the target's rect is fine — the thing that moved is what is covering it, and no owner can answer that live on Apple platforms. Adding help text is the honest remainder. Android device run. Ran on a Pixel 7 review emulator (Android 16) with the real Gboard, What that run does not do: it does not exercise an end-to-end refusal on Android, because nothing app-owned sat under the band on the screen I used (Settings search keeps its content above the keyboard). The acting-path wiring is what the ADR 0011 Gate on this branch: Every stage |
… its top The width rule took a second span over the surface rects while the band was already about to be built from the anchor span. Measured trees place the dock's buttons inside the key columns, and the landscape read the rule exists to catch — key plane 162 x 327 — fails on either span, so the extra Math.min/max pair only widened the distance between what is checked and what is returned. The module doc stopped calling the point rule "the center rule", which is a description of the version #2602 no longer ships, and one case name gets its apostrophe.
95031db to
34088bc
Compare
|
Holistic pass over what the two review rounds added; pushed as
Post-refactor checks: Device spot-check on iPhone 17 Pro after the shape change, because the point now travels through a different signature: covered |
|
Reviewed at 34088bc. Every acting path now guards the point it dispatches: the selector, One small thing: CI is green on 34088bc, and there are no conflicts. This PR is stacked on #2601, which still has open findings, so it waits on that one. |
34088bc to
314879e
Compare
314879e to
69c67c7
Compare
|
Fixed, pushed as This head also carries two things the last round of #2601 needed from here:
Fallow is clean on the stack now (the complexity finding on |
|
Reviewed at 69c67c7. The fast-path finding from 34088bc is fixed, and one acting path still skips the keyboard guard.
The Smoke Tests iOS runner job failed in There are no conflicts. This PR is stacked on #2601, so it lands after that one. The next step is to settle the find focus/type path and rerun Smoke Tests. |
… its top The width rule took a second span over the surface rects while the band was already about to be built from the anchor span. Measured trees place the dock's buttons inside the key columns, and the landscape read the rule exists to catch — key plane 162 x 327 — fails on either span, so the extra Math.min/max pair only widened the distance between what is checked and what is returned. The module doc stopped calling the point rule "the center rule", which is a description of the version #2602 no longer ships, and one case name gets its apostrophe.
69c67c7 to
ae5a4a1
Compare
|
Both items landed as asked — one deferred with the reason on record.
The four-line guard is real and passes: refuse before It fails the layering scan:
Say the word if you would rather have the relocation in this stack. Smoke Tests — The head moved twice since your look: rebased on |
|
Reviewed at ae5a4a1, as a follow-up to the review at 69c67c7. The earlier findings are fixed, and I found nothing new. The iOS and Android device runs in the PR are author-reported; they still apply because the logical patch is unchanged from the head where they were reported. The find focus/type guard stays with #2622. Typecheck & Package fails with TS2353 on Next: this stays stacked on #2601. |
|
Smoke rerun, as promised: the iOS lane on The same lane on #2601 at What is still red on both heads is |
…yboard An acting target whose rect center lands inside the visible keyboard's band is refused with `tap_keyboard_occludes_target`, because the touch activates a key instead of the named element (#2589). Neither sibling guarantee reached it: the keyboard is a separate system surface, so it is never a covering sibling for `occlusion`, and a covered tab bar stays inside the app's own window rect for `offscreen`. The guard runs at the shared pipeline door, so selector, `@ref`, the native-ref preflight, and both drag endpoints read one decision, and a backend that "succeeds" cannot do so on a target the shared rules refuse. Raw coordinates keep their escape hatch and disclose the same reason in the response warning instead: they name a point rather than an element, they are how a keyboard's own control is tapped, and that path never captures the tree the band is measured against. The keyboard's own keys and dock items stay tappable, an element whose center stays above the key plane keeps pressing, and an unmeasurable keyboard fails open. Versioned help and the iOS runner hint no longer claim the keyboard usually does not block interactions. Closes #2589
…d guess at it The guard measured the target's rect center while the acting paths dispatch whatever `resolveNodeTouchPoint` resolves, and those two points are not the same node geometry: a parent whose interactive descendants own its center has its aim pushed into a child-free region. Center-first could wave through a tap whose real aim had moved into the key plane, and refuse one whose real aim had moved clear of it. The stage owner now resolves the aim once, measures that, and returns it for the dispatch, so one point is in play per acting tap. The native-ref fast path keeps the rect center and says why: it hands the element to the backend and dispatches by ref, so there is no coordinate to re-derive. The coordinate path's two disclosures shared their viewport lookup, which the keyboard warning had been resolving a second time for the same point. The runtime cases now build on the ADR 0011 fixture's #2589 tree instead of restating its rects, and one case pins the point rule from both sides: a target whose center stays above the key plane while its dispatched aim falls below it is refused, and the same tree with the keypad lifted off the bottom edge shows the tap aiming under the key plane.
…'s type The stage door carried two overload declarations and two parameter aliases to say that a path handing in a resolver returning `Point` gets a `Point` back and a path without one gets `null`. One constrained generic — `TPoint extends Point | null` on a resolver every path must pass — says exactly that, and the native-ref fast path reads its nullability off the rect center it asks for rather than into a signature above the function it calls. No cast, no branch that cannot be reached. Also removes ceremony the previous commit left behind: a session-lookup helper with one caller, a guard that rebound `params.tapPoint` to a local for one use, and runtime cases that lifted the keyboard off the bottom edge twice, once through a translation knob with one caller and once inline with the offset spelled out again.
Five of the runner-side snapshot payloads here have no consumer anywhere in the tree, and fallow reports them as dead exports because this stack changes the file. Deleted rather than suppressed or de-exported into unused locals: the two that remain are the ones the provider-transcript scenarios actually read.
ae5a4a1 to
73a7aec
Compare
* feat(contracts): name the keyboard band that owns a tap point A software keyboard is its own system surface: it is never a covering sibling of app content, so the same-window occlusion classifier cannot see it, and it leaves the app window rect intact, so the viewport rule passes a covered tab bar. Nothing in the vocabulary said when a tap point belongs to the keyboard rather than to the app behind it (#2589). Derive the band from the captured tree -- topmost keyboard node down to the viewport bottom, across the keyboard's own columns -- and report one decision with four outcomes, failing open whenever the keyboard cannot be measured. Ownership reads accessibility type, role, and Android input-method provenance, never a locale-dependent label. Derivation and outcomes are pinned by contracts/fixtures/tap-keyboard-occlusion-policy.json. * chore(gates): export the keyboard tap-occlusion contract and pin its surface * fix(contracts): dock the keyboard band to the bottom edge, not the midline Band eligibility asked whether the topmost keyboard rect sat in the lower half of the viewport — a height claim dressed as a docking claim. Measured on iPhone 17 Pro (iOS 26.2) with Safari raising the system keyboard, portrait reports the key plane at 583..816 of an 874 pt viewport and landscape at 75..402 of a 402 pt one. The landscape keyboard therefore starts above that orientation's midline at 201, so the old rule declined to measure it and the guard was silently off in landscape: the same silent misfire #2589 reports, in the second orientation. Eligibility is now the measured question. Keyboard geometry has to come within 80 pt of the viewport's bottom edge to own the band down to it. Portrait's 58 pt home-indicator strip passes with 22 pt of slack, landscape passes exactly on the edge, and a surface that stops further up — an iPad floating or split keyboard, which the height heuristic could also misread as a system band — leaves the band unmeasured so the tap fails open rather than refusing on geometry that says nothing about the bottom of the screen. Docking cannot be read off height, which is what the golden table now pins: the same keyboard is 233 pt of an 874 pt viewport in portrait and 327 pt of a 402 pt one in landscape. Six cases gained the docked bottom row a real tree reports beside its top row, and five new cases pin docking itself plus the boundary it accepts: a bottom-anchored app keypad wearing the keyboard types owns its band. `resolveVisibleKeyboardSurface` is module-private. The table derives its band through `expected.frame`, so nothing outside the module needed it, and the surface derivation assertions that only that file could see now live in the table. * fix(contracts): say which tree the keyboard verdict read, and how to re-check it A refusal after `keyboard dismiss` reads like a lie: the keyboard is gone, and the band that refused the tap came from the tree the command measured against. The off-screen guard can spend one runner round trip confirming its target before it refuses (#1542); the keyboard guard cannot spend anything, because there is nothing to spend it on. `keyboard status` and `keyboard get` are Android-only — Apple never had a live keyboard read, and its cells refuse both — and the double-check hook answers whether the target itself is on screen, which a covering keyboard leaves true. So the refusal stops implying a probe it does not have: the hint names the snapshot as the source of the verdict and `snapshot -i` as the re-measurement that clears it. Same words from the coordinate disclosure, which has the same stale-tree exposure and no probe either. * fix(contracts): refuse to measure keyboard geometry that arrived rotated Verified on iPhone 17 Pro with the landscape keyboard up: the screenshot shows the keyboard full width across the bottom 327 pt of an 874 x 402 viewport, and both the regular and the interactive projection report the key plane as 162 x 327 with the QWERTY columns running down the short axis and the dock's globe button at y 8. iOS hands over the landscape keyboard's rects in the keyboard's own rotated space, so a band derived from them would cover a 141 pt strip on the left edge from y 77 down — refusing a headline the keyboard is nowhere near while letting a tab-bar item at (264, 360) tap straight through into a key. A docked keyboard is wide in both orientations, 402 x 233 in measured portrait and 874 x 327 in measured landscape, so geometry that comes back taller than it is wide is refused measurement and the tap fails open, which is the same stance an unresolvable viewport gets. Landscape coverage is not lost, only iOS's rotated read of it: the table now pins the measured rotated tree as `undetermined` and pins a wide, docked landscape keyboard as measurable against the landscape viewport's own bottom edge. Recording the misfire rather than hiding it: `#2589`'s silent tap-into-a-key stays open on landscape iOS until the Apple capture reports keyboard rects in the app's orientation space. That is a capture-side fix, not a classifier rule, and gets its own issue. * refactor(contracts): measure the band's width from the span that sets its top The width rule took a second span over the surface rects while the band was already about to be built from the anchor span. Measured trees place the dock's buttons inside the key columns, and the landscape read the rule exists to catch — key plane 162 x 327 — fails on either span, so the extra Math.min/max pair only widened the distance between what is checked and what is returned. The module doc stopped calling the point rule "the center rule", which is a description of the version #2602 no longer ships, and one case name gets its apostrophe. * fix(contracts): refuse a band whose keys do not tile the width it claims A split iPad keyboard docks both clusters at the bottom edge, and its surface container still reports the whole screen width, so the docking and width rules passed and the band ran across the middle of the screen — where the app content between the clusters is visible and tappable. The container says where the platform thinks the keyboard is; only the keys say where its controls are, so the band now measures whether the keys form one unbroken run of columns before trusting any of it. A gap is not something the band can be built around: nothing in the tree says where either cluster ends, so the geometry stays unmeasured and the tap fails open, as it does for a rotated keyboard or a floating one. Measured, not assumed. iPhone 17 Pro (26.2) reports a key row adjoining across 395 pt, iPad Pro 11-inch (M4) across 743.5 pt, and Gboard on Pixel 7 across 1070 px, none with a gap above a point, so the allowance is for projection rounding rather than layout — and the reduced two-key keyboards the table had been modelling now carry the measured row, which is what makes the new rule bite on a modelled split cluster instead of on a reporting seam. The same pass measured a three-button navigation bar on Android (126 px at 2274..2400 inside Gboard's reported region, which runs to the physical display bottom) and a docked iPad keyboard stopping 3 pt short of its viewport bottom, both now table rows. The rule is blunt in both directions and the table pins both costs: a sparse layout such as the iOS emoji panel reports corner keys over a wide container, does not tile, and reads undetermined, because choosing which gaps are app content is exactly the invention that produced the split-keyboard bug; and a projection reporting the container with no keys has nothing left to check, so the band is the container's own claim — every captured iOS and Android keyboard so far reports its keys, so that shape is the harness's rather than a captured one. Cases that reach a band also owe their whole shape now: the frame the refusal and the disclosure quote, and the control rects, which is where plane exclusion shows up. Previously the frame was compared only on occluded verdicts and the control rects never, so a regression in plane exclusion or in a clear verdict's band passed silently. The measurement rules are each one named function now, and the two ancestry walks the module carried — one asking whether a node belongs to the keyboard, one asking which nodes have keyboard nodes below them — read the projection's parent links through one primitive. * test(android): drop the retired installArgs from the install-timeout fixture `2cafab3ad0` typed this fixture as `AndroidSnapshotHelperManifest` while carrying `installArgs`, a field the manifest no longer has: #2603 retired the `-t` flag the array used to spell, the helper now installs with a fixed `adb install -r`, and the parser ignores the key on older blobs — which is what `snapshot-helper.test.ts` pins. The literal therefore failed `pnpm typecheck` with TS2353 on every branch, reding `Typecheck & Package` on `origin/main` itself. Nothing read the key. The install path builds its adb arguments from the provider's own `replace` option, and the assertion this fixture serves is about the timeout hint derived from the adb failure.
|
Summary
An acting target whose dispatched tap point lands inside the visible keyboard's band is now refused with
tap_keyboard_occludes_targetinstead of reporting a tap that a key ate (#2589). The guard measures the point the interaction actually sends — the touch point a parent with interactive descendants resolves, or the rect center where the platform aims when it is handed an element by name — so a row whose center sits above the key plane cannot have its aim land on a key without a refusal. The guard runs at the shared pipeline door, so selector,@ref, the native-ref preflight and both drag endpoints read one decision. Raw coordinates keep their escape hatch and disclose the same reason inwarning, because a point names no element, is how a keyboard's own control is tapped, and is checked against a last-known tree. The keyboard's keys and dock items stay tappable; an element whose center stays above the key plane keeps pressing; an unmeasurable keyboard fails open. Versioned help and the iOS runner hint no longer say the keyboard usually does not block interactions.Closes #2589. Base PR #2601 supplies the classifier. 17 files, +642/-164.
Before the change the first line read
Tapped @e21 (199, 822)while Catalog stayed selected;fillinto a field with the keyboard up was re-checked and still works.Validation
Tested at
73a7aec338on top of #2601 atdafab14379, rebased onorigin/main2cafab3ad0.pnpm typecheck— clean. TheinstallArgsbreakage that red this stack andorigin/mainis repaired on the base branch (test(android): drop the retired installArgs from the install-timeout fixture), one dead key out of a typed literal in a test fixture.pnpm check:affected --run— results posted as they land.lint,check:layering,check:di-seams,check:fallow --base origin/main,build,check:package,check:command-docs,test:integration:node,test:integration:macos-coverage,check:packaged-runner-swift— clean.check:production-exportsreports 68 findings here and the identical 68 on detachedorigin/mainin this worktree;mutation-model's ownership assertion fails identically there too. Neither is caused here.runtime-selector,runtime-ref,native-ref,target-drag) plus their coverage manifests; thecoordinatecell is waived with an "Intentional" reason rather than claimed.keyboard dismissrefused on this keyboard, so the rewordedUNSUPPORTED_OPERATIONhint was exercised on device as well.open --no-test-ime):keyboard statusreports visible withcom.google.android.inputmethod.latin, the capture carries 129 nodes with that package as provenance, and the classifier derives the bandx 0, y 1517, 1080 x 883from that real tree — a point at (540, 1800) reads occluded, (540, 1400) reads clear.Landscape iPhone is fail-open, not covered: Apple reports the landscape keyboard's rects in the keyboard's own rotated space (key plane 162 x 327 for a keyboard drawn full width across the bottom 327 pt), so there is no band to measure. Tracked in #2612.
find <locator> focus/find typedispatch the focus themselves and still miss this guard. The four-line call passes every test and failsR2 commands-floor, so it belongs below both zones; #2622 carries the patch, the matrix row and the two placement options.Residual false positive: an app-drawn bottom keypad wearing the
Keyaccessibility type reads as the keyboard, so content behind it is refused rather than silently misfired.