You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
find <locator> focus and find <locator> type <text> dispatch the focus themselves, so they never pass the shared pipeline door where the #2589 keyboard guard runs. A match whose tap point sits behind the visible keyboard is focused anyway, activating a key instead of the target — the exact misfire #2589 was filed for.
Found while reviewing #2602 (src/daemon/interaction/internal/find.ts#L340-L367, at 69c67c7). It is outside the ADR 0011 matrix rows the PR covers, and the layering scan blocks the one-line fix, so the seam is tracked here.
rejectCoveredFindMatch — the same-window occlusion refusal, so this seam does carry one pre-action guard;
centerOfRect(match.resolvedNode.rect) — the point it then dispatches;
expireRefFrame(session) — the ADR 0014 side-effect seam;
executeFocusPoint(...).
assertTapTargetClearOfVisibleKeyboard (src/commands/interaction/runtime/keyboard-occlusion.ts) is called from runInteractionPipelineStages (src/commands/interaction/runtime/resolution.ts:678) and the native-ref preflight only. find click / find fill re-enter the interaction leaf and are covered; find focus / find type do not.
A keyboard is invisible to step 1 by construction (its own system surface, never a covering sibling) and passes the viewport rule, so nothing between the match and the device refuses this point.
Required behavior
The tap point this seam dispatches is refused before the frame is expired, with the shared decision and the shared failure:
same decision as the acting paths: resolveKeyboardTapOcclusion against match.nodes (the tree find matched against) and the point from step 2;
same failure: reason tap_keyboard_occludes_target, ref, rect, keyboardFrame details, and the passive-verb message the acting guards use for focus;
the refusal stays ahead of expireRefFrame, so a refused find focus leaves the captured frame pinnable, as the covered refusal already does;
one shared implementation, reached through a dependency direction the layering scan accepts (see below) — not a second copy of the band reading or the error shape.
Why the obvious patch is rejected
Calling the guard from find.ts is four lines and passes every test:
It fails R2 commands-floor: daemon/ must not import commands/ … if two zones need the same rule, put the rule below both of them. A rule with two zone callers belongs at the shared Node stage door, so this issue owes a placement decision rather than a guard.
Completion conditions
find <locator> focus and find <locator> type <text> refuse a target whose tap point sits behind a docked keyboard, quoting the keyboard frame, and never reach focusPoint.
A refused seam leaves refFrameState(session) at active.
The refusal is produced by the same code the acting paths call; rg -n "behind the visible keyboard" src/ names one construction site.
pnpm run check:layering passes.
A matrix row exists for this seam and every cell is classified — see below.
pnpm check:affected --run is green on the commit being pushed, per docs/agents/pull-requests.md.
Matrix row
packages/contracts/src/interaction-guarantees.ts has no path id for this seam: runtime-ref lists press, click, fill, longpress, hover, and INTERACTION_PATH_IDS has nothing for a mutating find that dispatches on its own. Adding a path means classifying all InteractionGuarantee cells (compile-enforced), so the row is part of the work, not a formality. Starting points to verify rather than copy:
Guarantee
Candidate cell
occlusion
runtime — src/daemon/interaction/internal/find.ts#rejectCoveredFindMatch, served from runNodePipelineStages(SELECTOR_PIPELINE_POLICIES.findAct, …)
keyboardOcclusion
runtime — whatever shared symbol this issue lands
parentOwnedTouchPoint, offscreen, nonHittable
read SELECTOR_PIPELINE_POLICIES.findAct and classify what that policy actually runs
this seam returns executeFocusPoint's shape plus recordFindAction; classify what it can and cannot echo
Placement options
Shared Node stage — make the guard a stage of the node pipeline in @agent-device/selectors, next to selector-pipeline.ts#runNodePipelineStages and interaction-touch-point.ts, and declare it in SELECTOR_PIPELINE_POLICIES.findAct. Then both find and the interaction leaf get it at the door they already run through, and the matrix cell reads like occlusion's. Costs: runNodePipelineStages stages must carry the action, the caller's label, and the resolved tap point, which is a wider stage contract than any current stage needs.
Move the rule below both zones — relocate keyboard-occlusion.ts and the action vocabulary it needs (InteractionAction + interactionVerb, currently in the commands zone) into a package both zones already import. Costs: keyboard-occlusion.test.ts currently drives the guard through commands/interaction/runtime/__tests__/test-utils, so the test splits between the shared unit assertions and the pipeline-level ones.
Both stay within the interaction family; neither is a rename-only move.
Notes
The unit and seam tests for the patch above are written and pass with the guard wired in (two cases in src/daemon/interaction/internal/__tests__/find.test.ts, using the captured iPhone 17 Pro keyboard tree from test/integration/interaction-contract/fixtures.ts#keyboardCoveredTabBarSnapshot). They are recoverable from this issue's patch snippet plus that fixture.
Reproduction without a unit test: iOS Simulator, keyboard up, then find <label of an element inside the keyboard band> focus on a target that is not itself a keyboard control.
Purpose
find <locator> focusandfind <locator> type <text>dispatch the focus themselves, so they never pass the shared pipeline door where the #2589 keyboard guard runs. A match whose tap point sits behind the visible keyboard is focused anyway, activating a key instead of the target — the exact misfire #2589 was filed for.Found while reviewing #2602 (
src/daemon/interaction/internal/find.ts#L340-L367, at69c67c7). It is outside the ADR 0011 matrix rows the PR covers, and the layering scan blocks the one-line fix, so the seam is tracked here.Current behavior
dispatchFocusForFindMatch(src/daemon/interaction/internal/find.ts:340) runs:rejectCoveredFindMatch— the same-window occlusion refusal, so this seam does carry one pre-action guard;centerOfRect(match.resolvedNode.rect)— the point it then dispatches;expireRefFrame(session)— the ADR 0014 side-effect seam;executeFocusPoint(...).assertTapTargetClearOfVisibleKeyboard(src/commands/interaction/runtime/keyboard-occlusion.ts) is called fromrunInteractionPipelineStages(src/commands/interaction/runtime/resolution.ts:678) and the native-ref preflight only.find click/find fillre-enter the interaction leaf and are covered;find focus/find typedo not.A keyboard is invisible to step 1 by construction (its own system surface, never a covering sibling) and passes the viewport rule, so nothing between the match and the device refuses this point.
Required behavior
The tap point this seam dispatches is refused before the frame is expired, with the shared decision and the shared failure:
resolveKeyboardTapOcclusionagainstmatch.nodes(the tree find matched against) and the point from step 2;tap_keyboard_occludes_target,ref,rect,keyboardFramedetails, and the passive-verb message the acting guards use forfocus;expireRefFrame, so a refusedfind focusleaves the captured frame pinnable, as the covered refusal already does;Why the obvious patch is rejected
Calling the guard from
find.tsis four lines and passes every test:It fails
R2 commands-floor:daemon/ must not import commands/ … if two zones need the same rule, put the rule below both of them. A rule with two zone callers belongs at the shared Node stage door, so this issue owes a placement decision rather than a guard.Completion conditions
find <locator> focusandfind <locator> type <text>refuse a target whose tap point sits behind a docked keyboard, quoting the keyboard frame, and never reachfocusPoint.refFrameState(session)atactive.rg -n "behind the visible keyboard" src/names one construction site.pnpm run check:layeringpasses.pnpm check:affected --runis green on the commit being pushed, perdocs/agents/pull-requests.md.Matrix row
packages/contracts/src/interaction-guarantees.tshas no path id for this seam:runtime-reflistspress, click, fill, longpress, hover, andINTERACTION_PATH_IDShas nothing for a mutatingfindthat dispatches on its own. Adding a path means classifying allInteractionGuaranteecells (compile-enforced), so the row is part of the work, not a formality. Starting points to verify rather than copy:occlusionruntime—src/daemon/interaction/internal/find.ts#rejectCoveredFindMatch, served fromrunNodePipelineStages(SELECTOR_PIPELINE_POLICIES.findAct, …)keyboardOcclusionruntime— whatever shared symbol this issue landsparentOwnedTouchPoint,offscreen,nonHittableSELECTOR_PIPELINE_POLICIES.findActand classify what that policy actually runsdisambiguation,errorTaxonomy,resolutionDisclosureresolveFindMatch, not the runtime rowsresponseConstruction,responseIdentity,verifyEvidence,settleObservationexecuteFocusPoint's shape plusrecordFindAction; classify what it can and cannot echoPlacement options
@agent-device/selectors, next toselector-pipeline.ts#runNodePipelineStagesandinteraction-touch-point.ts, and declare it inSELECTOR_PIPELINE_POLICIES.findAct. Then bothfindand the interaction leaf get it at the door they already run through, and the matrix cell reads likeocclusion's. Costs:runNodePipelineStagesstages must carry the action, the caller's label, and the resolved tap point, which is a wider stage contract than any current stage needs.keyboard-occlusion.tsand the action vocabulary it needs (InteractionAction+interactionVerb, currently in the commands zone) into a package both zones already import. Costs:keyboard-occlusion.test.tscurrently drives the guard throughcommands/interaction/runtime/__tests__/test-utils, so the test splits between the shared unit assertions and the pipeline-level ones.Both stay within the
interactionfamily; neither is a rename-only move.Notes
src/daemon/interaction/internal/__tests__/find.test.ts, using the captured iPhone 17 Pro keyboard tree fromtest/integration/interaction-contract/fixtures.ts#keyboardCoveredTabBarSnapshot). They are recoverable from this issue's patch snippet plus that fixture.find <label of an element inside the keyboard band> focuson a target that is not itself a keyboard control.packages/contracts/src/tap-keyboard-occlusion.ts) provides the decision this seam must call.