feat: add selector-targeted drag gestures - #1567
Conversation
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
Review: direction is right, two defects to fix, and one encapsulation themeDirection: yes. A target-authored drag is the correct primitive to add — it belongs at the interaction layer next to
Gates I ran locally on Below: two defects I reproduced, then the encapsulation review you asked for. 1. Recording round-trip corrupts partially-specified drag timings
function compact(values: Array<string | number | undefined>): string[] {
return values.filter((value): value is string | number => value !== undefined).map(String);
}Drag is the first gesture with three independent optional positionals, so a hole in the middle shifts everything after it into the wrong slot. Reproduced against this branch:
The consequence is the bad kind: a recorded script silently replays a different gesture. For the long-press-drag reorder this feature exists to drive, moving 600ms out of Fix is either positional placeholders for drag, or encode the timings as a trailing triple that is all-or-nothing. Worth a round-trip test over each of the 8 present/absent combinations. 2.
|
| Module | Coupling |
|---|---|
packages/contracts/src/gesture-normalization.ts:264,313 |
owns the encoding |
src/daemon/handlers/interaction-gesture.ts:237-238 |
writes positionals[1], positionals[2] |
src/daemon/session-script-writer.ts:345 |
reads positionals.slice(1, 3) |
src/daemon/handlers/session-replay-target-token.ts:8 |
reads positionals[1] |
Reorder drag's positionals and three of these break silently — and defect 1 above is exactly what a hole in that layout already does. The daemon rewrite in particular would read better as a re-encode than an index patch:
gesturePayloadToPositionals({
...input,
source: recording.sourceSelector ?? input.source,
destination: recording.destinationSelector ?? input.destination,
})…which keeps the layout knowledge in the one module that owns it, and stops being wrong the moment placeholders land for defect 1.
The type model doesn't absorb drag, so each layer widens or casts around it:
resolveExecutionProfilereturns'hold-drag'(interaction-gesture.ts:198), butGestureExecutionProfileis'endpoint-hold' | 'timed-pan'andbuildDragGesturePlanstamps the plan'timed-pan'. So the response advertises a profile that is not in the union and is not what executed. The declared return type isstring | undefined, so nothing catches it. Either admit'hold-drag'into the union and carry it onto the plan, or rename the response field so it isn't read as the plan's profile.kind: GestureIntent | 'drag'— an inline widening rather than the exportedGestureCommandInputmodel.capabilities.tstypes its parameterGestureSemanticInput | { intent: 'drag' }even thoughGestureCommandInputis exported from contracts for exactly this. Two spellings of one concept.- Two casts where a discriminated narrow would do:
options.gesture as GestureSemanticInput(gesture-command.ts:85) andresolved as ResolvedInteractionTarget & { point: Point }(line 169). The first is a direct consequence of narrowing via the separateresolvedDragvariable instead of branching onoptions.gesture.intent— which is also why the same block needsdragGesture?.sourceHoldMson a value that cannot be undefined there.
None of these is individually serious. Together they mean the next gesture that carries targets repeats all of it. Branching once on the discriminant, and letting GestureCommandInput be the single spelling, removes most of the casts and the fake-pan shim at the same time.
Smaller notes
- Duration sum is validated too late. Each timing is capped at 10s in
readGesturePayload, but the total is only checked insidebuildDragGesturePlan— aftercaptureGestureViewportand both target resolutions.gesture drag src dst 10000 10000 10000pays a snapshot and two resolutions before failingINVALID_ARGS. The sum check belongs in contracts next to the per-field ones. readNonEmptyStringvalidatesvalue.trim().length > 0but returns the untrimmed string, so' id="x" 'reaches the resolver with padding.sourceHoldMshasmin: 1whiledestinationHoldMshasmin: 0. Presumably deliberate (you must hold to activate; you needn't hold to release) but it's undocumented and will read as a typo.prepareDragTargetreturns{ target }and both call sites immediately unwrap.target.- No corpus coverage.
grepfindsgesture dragnowhere underexamples/ortest/— only inwebsite/docs. Unit coverage is solid, but the repo'stest-app:replay:*corpora are how gestures usually earn their keep, and removing the prototype assets left the new command with no replay fixture. A smalldrag.adagainst a test-app reorder target would also give the timing semantics somewhere to regress.
One thing I could not verify
Drag lowers to executionProfile: 'timed-pan', so it shares the runner's .sampled path with gesture pan. While testing something unrelated on iOS earlier today I saw gesture pan wall-time not scale with its requested durationMs (400 / 1200 / 3000ms all completing in roughly the same time). I could not confirm that: every one of those trials used a pan geometry that was inert, so the fast return may just be an early-out, and a re-test at a moving geometry was blocked by another daemon owning the simulator. Flagging it only because this feature's whole value rests on an 800ms activation hold actually lasting 800ms on device. If it hasn't been checked directly, it's worth asserting the observed contact duration once on each platform rather than inferring it from the plan.
|
Reviewed exact head
CLI/Node/MCP/daemon routing, ref admission/expiry, portable ref rewriting, duration planning, CI, and the reported device runs otherwise look sound. The device evidence is described but not attached/reproducible, so that remains a readiness residual. |
Deep code-quality audit (structural)Follow-up to my earlier comment, which covered two reproduced defects and the positional-layout coupling. This pass is purely about structure: is this the simplest shape this feature can take? I don't think it is, and I think there are two code-judo moves that delete most of the new complexity rather than rearrange it. Not approving on structure yet. Behavior looks right and the gates are green — that isn't the bar here. File-size rule: clean. Nothing crosses 1k because of this PR. 1. Drag logic is in the wrong module, and that's what forces the casts
That work already has a canonical home in the same directory. const resolved = await resolveInteractionTarget(runtime, options, {
action, requireInteractive, promoteToHittableAncestor,
expectedResolvedTarget: options.expectedResolvedTarget,
});
const point = requireResolvedPoint(resolved);
…
return { ...resolved, … }; // disclosure / selectorChain / evidence propagate by spreadThe new code reimplements that spine instead of using it:
Note what that first row means: the cast at :169 exists only because the canonical helper wasn't used. The judo move: put
I'll grant the one real asymmetry: drag has two endpoints and the spread convention carries one. So the destination needs something bespoke. But the source is the recorded, replay-verified identity — it can use the canonical path as-is, and only the destination needs a small addition. That is a much smaller delta than the current 153 new lines in the wrong file. 2.
|
Could this reuse
|
|
Addressed all four review comments in
Red proofs were run for sparse timing corruption, fake-pan capability reporting, skipped destination verification, missing ADR matrix coverage, and late total-duration validation. Final local gate: |
There was a problem hiding this comment.
Pull request overview
Adds a new selector/@ref-targeted gesture drag interaction to agent-device, spanning CLI/client/MCP projection, daemon dispatch + recording, and replay verification. This introduces a dual-endpoint resolution + disclosure model and dual-endpoint replay guards/evidence so drag replays refuse before pointer-down if either endpoint no longer matches.
Changes:
- Introduce target-authored
gesture dragend-to-end (contracts planning + runtime resolution + daemon handler + client/MCP/CLI projection). - Add
targets-v1dual-endpoint recording evidence (targetEvidences) and replay-time verification/guarding for both source and destination. - Extend docs, ADRs, fixtures, and integration/e2e replay scripts to cover the new drag surface.
Reviewed changes
Copilot reviewed 75 out of 75 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| website/docs/docs/commands.md | Document gesture drag CLI usage and behavior. |
| website/docs/docs/client-api.md | Add client.interactions.drag() to the documented API surface. |
| test/integration/ios-simulator-e2e/live-replay-scenarios.ts | Include iOS drag replay script in the live replay fixture suite. |
| test/integration/interaction-contract/target-drag.coverage.ts | Add contract coverage manifest for the target-drag dispatch path. |
| test/integration/interaction-contract/target-drag.contract.test.ts | Add interaction-contract tests for drag resolution/guards/errors and response shape. |
| test/integration/interaction-contract/runtime-harness.ts | Extend contract runtime harness to allow gesture viewport + gesture dispatch overrides. |
| test/integration/interaction-contract/index.ts | Register drag coverage manifest in the global contract coverage aggregation. |
| test/integration/interaction-contract/fixtures.ts | Add a drag endpoints snapshot fixture for contract tests. |
| test/integration/interaction-contract/daemon-harness.ts | Add provider transcript helpers for gesture viewport + gesture dispatch. |
| test/integration/android-emulator-e2e/live-replay-scenarios.ts | Include Android drag replay script in the live replay fixture suite. |
| src/replay/plan-digest.ts | Include targetEvidences in replay plan digest canonicalization. |
| src/replay/tests/plan-digest.test.ts | Assert digest changes when multi-target evidence changes. |
| src/mcp/tests/command-tools.test.ts | Assert MCP gesture tool schema exposes drag endpoints + bounded timing phases. |
| src/daemon/types.ts | Add replayTargetGuards internal dual-endpoint guard channel for drag. |
| src/daemon/session-script-writer.ts | Refuse writing recorded drag actions that still contain unresolved @ref endpoints. |
| src/daemon/session-script-active-publication.ts | Enforce portability + evidence requirements for recorded drag publication. |
| src/daemon/session-action-recorder.ts | Persist targetEvidences (dual-endpoint targets-v1) into recorded actions. |
| src/daemon/handlers/session-replay-target-verification.ts | Refactor verification to support endpoint-specific token verification and role tagging. |
| src/daemon/handlers/session-replay-target-token.ts | Add extraction helpers for drag’s source/destination replay tokens. |
| src/daemon/handlers/session-replay-runtime.ts | Use multi-target verification and thread dual guards into dispatch internals. |
| src/daemon/handlers/session-replay-report-action.ts | Include targetEvidences in replay report action payload. |
| src/daemon/handlers/session-replay-multi-target-verification.ts | New helper to verify and guard both drag endpoints before dispatch. |
| src/daemon/handlers/interaction-ref-policy.ts | Add throwing ref-admission helper for composing multi-ref interactions. |
| src/daemon/handlers/interaction-gesture.ts | Add drag normalization (ref admission + suffix stripping), dual disclosure, and dual recording evidence capture. |
| src/daemon/handlers/interaction-gesture-response.ts | Centralize gesture response shaping, including drag targets disclosure + timing. |
| src/daemon/handlers/interaction-common.ts | Record targets-v1 evidence for multi-target interactions in finalize/recording path. |
| src/daemon/handlers/tests/session-replay-target-verification-runtime.test.ts | Add replay tests for drag dual-endpoint guard threading and mismatch reporting. |
| src/daemon/handlers/tests/session-replay-target-token.test.ts | Add unit test asserting drag evidence binds to the source token for single-target extraction. |
| src/daemon/handlers/tests/interaction-gesture-response.test.ts | Add unit coverage for the new gesture response builder and drag disclosure preservation. |
| src/daemon/handlers/tests/interaction-gesture-drag.test.ts | Add daemon interaction tests for drag ref admission, recording portability, and frame-expiry behavior. |
| src/daemon/tests/session-script-writer.test.ts | Add script-writer tests for refusing unresolved drag refs and emitting targets-v1. |
| src/daemon/tests/session-script-active-publication.test.ts | Add active-publication tests enforcing portable drag endpoints + targets-v1 evidence. |
| src/core/command-descriptor/registry.ts | Mark gesture as targetIdentityVerification: pre-dispatch. |
| src/core/command-descriptor/tests/parity.test.ts | Pin gesture in the evidence-carrying command set parity test. |
| src/core/capabilities.ts | Update gesture capability checks to accept the expanded command input shape (including drag). |
| src/core/tests/gesture-capabilities.test.ts | Add capability-policy tests for drag (treated like one-pointer pan). |
| src/commands/interaction/runtime/resolution.ts | Add targetRole to guard-mismatch details and plumb it through resolution errors. |
| src/commands/interaction/runtime/gestures.ts | Implement runtime dragCommand (dual resolution → build plan → dispatch → disclosure + recording details). |
| src/commands/interaction/runtime/gestures.test.ts | Add runtime tests for drag resolution ordering, dispatch gating, and endpoint role in guard mismatch. |
| src/commands/interaction/runtime/gesture-command.ts | Route drag inputs to dragCommand; share viewport resolution with coordinate gesture command. |
| src/commands/interaction/runtime/tests/test-utils/index.ts | Add drag snapshot fixture for runtime tests. |
| src/commands/interaction/metadata.ts | Extend gesture metadata schema/types to include drag endpoint + timing fields. |
| src/commands/interaction/index.ts | Add CLI projection from gesture drag ... to client interactions.drag(). |
| src/commands/interaction/gesture.test.ts | Add CLI/daemon writer projection test for drag positional parsing and structured input. |
| src/client/client-types.ts | Add interactions.drag() to the typed client interface. |
| src/cli/parser/cli-help.ts | Update top-level CLI help text to include drag and describe its behavior. |
| src/cli/parser/tests/cli-help-topics.test.ts | Update help-topic test to expect drag in gesture usage. |
| src/agent-device-client.ts | Add client implementation for interactions.drag() projecting to gesture structured input. |
| src/tests/test-utils/property-arbitraries.ts | Rename gesture kind set used for property tests to exclude drag from coordinate-only arbitraries. |
| src/tests/client.test.ts | Add client projection test for drag structured input. |
| packages/contracts/src/target-annotation.ts | Add MultiTargetAnnotationV1 contract type for dual-endpoint evidence. |
| packages/contracts/src/session-action.ts | Add targetEvidences field to recorded session action contract (targets-v1). |
| packages/contracts/src/interaction.ts | Update resolution disclosure docstring to include drag endpoints. |
| packages/contracts/src/interaction-guarantees.ts | Add target-drag dispatch path classification + guarantee mapping. |
| packages/contracts/src/gesture-plan.ts | Add buildDragGesturePlan() and drag hold/move/hold planning helpers. |
| packages/contracts/src/gesture-plan.test.ts | Add unit + property tests for drag planning and duration bounds. |
| packages/contracts/src/gesture-plan-types.ts | Add drag command input type and default drag timing constants. |
| packages/contracts/src/gesture-normalization.ts | Add drag positional codec + normalizeGestureCommandInput() for runtime drag inputs. |
| packages/contracts/src/gesture-normalization.test.ts | Add tests for drag positional grammar, recording materialization, and normalization. |
| packages/contracts/src/gesture-input.ts | Add drag payload parsing/validation and total-duration enforcement at the trust boundary. |
| packages/contracts/src/gesture-input.test.ts | Add validation tests for drag endpoint strings and timing phase bounds. |
| packages/contracts/src/client-gesture.ts | Add DragOptions client contract type. |
| packages/ad-script/src/internal/target-annotation-serde.ts | Add targets-v1 serialization/parsing and enforce bounded wrapper payload size. |
| packages/ad-script/src/internal/script.ts | Bind targets-v1 annotations to the immediately-following action line during parsing. |
| packages/ad-script/src/internal/script-formatting.ts | Emit targets-v1 annotation lines when targetEvidences is present. |
| packages/ad-script/src/internal/tests/target-annotation-serde.test.ts | Add tests for targets-v1 round-trip, required endpoints, and size cap enforcement. |
| packages/ad-script/src/internal/tests/script.test.ts | Add parsing test asserting targets-v1 binds to one action as targetEvidences. |
| packages/ad-script/src/index.ts | Re-export multi-target annotation serde helpers and payload cap constant. |
| examples/test-app/src/screens/GestureLab.tsx | Add a drag fixture UI and state bit (drag completed yes/no) for replay verification. |
| examples/test-app/src/screens/gesture-lab-styles.ts | Add styles for the drag fixture endpoints. |
| examples/test-app/replays/drag.ad | New iOS simulator replay script validating gesture drag end-to-end. |
| examples/test-app/replays/drag-android.ad | New Android emulator replay script validating gesture drag end-to-end. |
| docs/adr/0013-unified-gesture-plans.md | Update ADR 0013 to describe target-authored drag and its planning/recording model. |
| docs/adr/0012-interactive-replay.md | Amend ADR 0012 with targets-v1 dual-endpoint evidence semantics and replay verification rules. |
| docs/adr/0011-interaction-guarantee-contract.md | Extend the dispatch-path matrix with the new target-drag path and scope notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 'Run touch gestures: pan <x> <y> <dx> <dy> [durationMs], fling <up|down|left|right> <x> <y> [distance], swipe <left|right|left-edge|right-edge>, pinch <scale> [x] [y], rotate <degrees> [x] [y], or transform <x> <y> <dx> <dy> <scale> <degrees> [durationMs]. For command plans, output only command lines. Android transform verification should use all app-observable effects, for example wait text "pan changed yes", wait text "pinch changed yes", and wait text "rotate changed yes", not exact transform values.', | ||
| summary: 'Run pan, fling, swipe, pinch, rotate, or transform gestures', | ||
| positionalArgs: ['pan|fling|swipe|pinch|rotate|transform', 'args?'], | ||
| 'Run touch gestures: pan <x> <y> <dx> <dy> [durationMs], fling <up|down|left|right> <x> <y> [distance], swipe <left|right|left-edge|right-edge>, pinch <scale> [x] [y], rotate <degrees> [x] [y], transform <x> <y> <dx> <dy> <scale> <degrees> [durationMs], or drag <source-selector> <destination-selector> [sourceHoldMs] [moveMs] [destinationHoldMs]. For command plans, output only command lines. Android transform verification should use all app-observable effects, for example wait text "pan changed yes", wait text "pinch changed yes", and wait text "rotate changed yes", not exact transform values.', |
| portable selector chains are returned to the caller. Recordings replace session-local refs at both | ||
| endpoints with those selector chains; the source additionally carries the action's `target-v1` identity | ||
| evidence because it is the element whose mutation is initiated. Ref admission happens for both endpoints | ||
| before either is dispatched, and the usual mutation boundary expires the frame after the gesture. |
|
Re-review of exact head
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 78 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/commands/interaction/index.ts:124
- The gesture help text says
pinned-reffor drag endpoints, but the ref grammar in this repo allows both bare refs (@e12) and optionally pinned refs (@e12~s3). Usingpinned-refhere can read like the suffix is required.
Consider documenting this as ref to match the actual accepted syntax.
src/cli/parser/tests/cli-help-topics.test.ts:44
- This test asserts the
gesturehelp usespinned-ref, but refs are valid both with and without a~s<generation>suffix. If the help text is updated to sayref, update the regex and test description accordingly so the test matches the actual CLI contract.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 78 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/commands/interaction/runtime/gestures.ts:226
- The error message here is generic to "gesture" even though this code path is specifically for selector-targeted drag. If a backend supports coordinate gestures but not target-authored drag, this message will be misleading to users and makes debugging harder.
|
P1: Android target drag is stationary despite green CI. The branch/docs/guarantee blockers are otherwise resolved, but both drag replay scripts are in |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 81 out of 81 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/daemon/session-script-active-publication.ts:123
- In
assertPortableDragBindings, non-selector endpoint tokens currently fall through (continue) and can be published without error ortargets-v1evidence. Forgesture drag, endpoints are required to be portable selector expressions at publication time; otherwise a published script can be invalid or silently skip identity enforcement.
|
Clean code-review on Remaining merge gates: exact-head CI and the in-progress nightly Android/iOS direct-drag plus full-tier replay evidence must complete green and prove |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 84 out of 84 changed files in this pull request and generated no new comments.
Suppressed comments (1)
test/integration/interaction-contract/fixtures.ts:260
- In this snapshot fixture, node 3 declares
parentIndex: 0(direct child of the Application) butdepth: 2, which is inconsistent with the rest of the snapshot structure. Keepingdepthaligned withparentIndexmakes the fixture more representative and avoids accidental reliance on invalid tree shapes in resolution/ordering logic.
|
Addressed at exact head
Exact-head live evidence: Replay Nightly run 30954741944:
Local gate is also green: 398 test files / 3,764 tests; changed-line coverage 126/133 (94.74%), changed-branch coverage 142/162 (87.65%). |
|
Final exact-head gate update: all regular PR checks are green, and the new direct selector-targeted drag replay passed on both platforms (Android: 8 steps, |
|
Branch blocker update on unchanged head |
14d3ef8 to
7c5b98e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 84 out of 84 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/daemon/session-script-active-publication.ts:125
assertPortableDragBindingscurrently only rejects session-local@refs, but it allows non-@endpoints that are not valid selector expressions (it justcontinues). Sincegesture dragendpoints must be selectors once portable, this can let an invalid/garbage endpoint slip through publication and only fail later at replay-time resolution.
|
Addressed the branch blocker:
Validation on the exact rebased tree:
The resolution preserves the previously reviewed drag production path, so I did not repeat the prior device evidence, per your note. |
|
Re-reviewed exact head |
…-viewport-geometry Resolves src/snapshot/snapshot-occlusion.ts against #1567. That PR added isFullViewportChromeContainer plus a second isViewportRoot call site inside it; both local root checks now use the canonical isViewportRootNode, and #1567's full-viewport-chrome exclusion is preserved unchanged.
Review on #1614 caught this conversion silently narrowing the public surface. The explicit lists were generated against the surface at fork time; #1567 landed 13 exports meanwhile — `DragOptions`, the drag-gesture vocabulary (`COORDINATE_GESTURE_KINDS`, `CoordinateGesturePayload`, the three `DEFAULT_DRAG_*` constants, `DragGestureInput`, `DragGesturePayload`, `GestureCommandInput`, `buildDragGesturePlan`, `dragGesturePayloadFromPositionals`, `normalizeGestureCommandInput`) and `MultiTargetAnnotationV1`. The `export *` barrels had been forwarding all 13 automatically; the rebase dropped every one, and only a human diff caught it. The star-rejection gate could not: it only proves a façade does not WIDEN invisibly. Narrowing is the failure an explicit list newly makes possible, because `export *` could not narrow by construction. So the property the stars gave for free is now asserted directly — every name a re-exported source declares must appear in the façade. Scoped to `packages/*/src/facades/`, the barrels this PR converted. A hand-curated package `index.ts` is a different thing: `ad-replay` deliberately publishes two values out of a much larger `internal/`, and forcing exhaustiveness there would widen a surface its owner narrowed on purpose (#1555). A source that itself carries a bare `export *` is skipped — unknowable from that file alone, and reachable because the façade re-exports the starred module directly too, which IS checked. Red evidence: dropping `MultiTargetAnnotationV1` from facades/replay.ts — one of the 13 the old gate was blind to — fails with the file, the source and the symbol named. 13 pass / 0 fail once restored.
Review on #1614 caught this conversion silently narrowing the public surface. The explicit lists were generated against the surface at fork time; #1567 landed 13 exports meanwhile — `DragOptions`, the drag-gesture vocabulary (`COORDINATE_GESTURE_KINDS`, `CoordinateGesturePayload`, the three `DEFAULT_DRAG_*` constants, `DragGestureInput`, `DragGesturePayload`, `GestureCommandInput`, `buildDragGesturePlan`, `dragGesturePayloadFromPositionals`, `normalizeGestureCommandInput`) and `MultiTargetAnnotationV1`. The `export *` barrels had been forwarding all 13 automatically; the rebase dropped every one, and only a human diff caught it. The star-rejection gate could not: it only proves a façade does not WIDEN invisibly. Narrowing is the failure an explicit list newly makes possible, because `export *` could not narrow by construction. So the property the stars gave for free is now asserted directly — every name a re-exported source declares must appear in the façade. Scoped to `packages/*/src/facades/`, the barrels this PR converted. A hand-curated package `index.ts` is a different thing: `ad-replay` deliberately publishes two values out of a much larger `internal/`, and forcing exhaustiveness there would widen a surface its owner narrowed on purpose (#1555). A source that itself carries a bare `export *` is skipped — unknowable from that file alone, and reachable because the façade re-exports the starred module directly too, which IS checked. Red evidence: dropping `MultiTargetAnnotationV1` from facades/replay.ts — one of the 13 the old gate was blind to — fails with the file, the source and the symbol named. 13 pass / 0 fail once restored.
Two review findings, plus a third the gate caught on itself. P1 — the three `DEFAULT_DRAG_*` constants join the existing public-façade suppression, alongside `COORDINATE_GESTURE_KINDS` and `normalizePublicGesture` which the same conversion surfaced. All five are #1567's drag vocabulary, made individually visible to `--production` analysis for the first time because a bare star used to hide them from that exact check. Kept rather than narrowed, for the reason the existing entry already states: the façade's surface stays byte-identical to what the retired pin table asserted, and narrowing is a follow-up with its own review. P2 — the exhaustiveness gate skipped any source carrying a bare `export *`, which dropped that module's DIRECT exports from the check too. `gesture-plan.ts` stars `gesture-plan-types.ts`, so removing `buildDragGesturePlan` from the façade narrowed the public surface and still passed. `readDirectNamedExports` now reads exactly the names a module declares or re-exports BY NAME and ignores the star, so direct exports are checked while the starred set stays covered by the façade's own direct re-export of that module. Red evidence: removing `buildDragGesturePlan` from facades/interaction.ts now fails naming file, source and symbol; 13 pass / 0 fail restored. Third, and the reason the gate is worth having: rebasing onto main after #1612 merged silently dropped `TEXT_ENTRY_ROUTES`, `TextEntryRoute` and `TypeTextBackendResult` from the interaction façade — the same narrowing class as the #1567 one review caught by hand, one merge later. The gate failed on it before CI did. Restored.
…n table (#1614) * refactor(contracts): name façade exports explicitly and retire the pin table Thirteen of the fourteen `@agent-device/contracts` façades were bare `export *` barrels. `facades/snapshot.ts`, added by #1582, was the one exception — explicit named re-exports — and that is now the rule. Everything #1574 built to cope with `export *` goes with them: scripts/layering/facade-symbols.ts -980 (816 pinned names) scripts/layering/facade-exports.ts -192 (readFacadeExports) scripts/layering/facade-exports.test.ts -234 (star semantics) scripts/layering/package-boundaries.test.ts -55 `readFacadeExports` re-implemented ESM `GetExportedNames`/`ResolveExport` — star-chain resolution, ambiguity rejection, diamond binding identity, cycle guards, spec-accurate `default` filtering at the star rather than the source. All of it existed to enumerate what `export *` hides. 523 of the 816 pinned names belonged to contracts, i.e. to those thirteen files. Once a façade names its exports, the façade file IS the pin, and it is visible in the diff of the file that widened rather than in a separate table a reviewer has to cross-check. `readNamedExports` (20 lines) stays and is enough: it already throws on bare `export *` and on `export default`. The pin is replaced by one structural gate — no façade may contain a bare star — which reuses that rejection rather than adding a regex. Surface equivalence verified independently, not asserted: main's own `readFacadeExports` run over the new façades, compared against main's own `FACADE_SYMBOLS` table — 31 subpaths, 0 added, 0 removed. Red evidence for the new gate: planting `export * from '../request-progress.ts'` back into facades/progress.ts fails it with the file named and the reason quoted; 12 pass / 0 fail once reverted. Not included: the `lowerAndroidTouchPlan` tuple-assertion drive-by. It needs `sampleGestureOffsets` to carry a min-arity tuple through `.map()`, which TypeScript will not infer without a typed helper — a real change to the gesture-plan contract rather than a drive-by, so it stays out. * test(layering): assert façades stay exhaustive over their sources Review on #1614 caught this conversion silently narrowing the public surface. The explicit lists were generated against the surface at fork time; #1567 landed 13 exports meanwhile — `DragOptions`, the drag-gesture vocabulary (`COORDINATE_GESTURE_KINDS`, `CoordinateGesturePayload`, the three `DEFAULT_DRAG_*` constants, `DragGestureInput`, `DragGesturePayload`, `GestureCommandInput`, `buildDragGesturePlan`, `dragGesturePayloadFromPositionals`, `normalizeGestureCommandInput`) and `MultiTargetAnnotationV1`. The `export *` barrels had been forwarding all 13 automatically; the rebase dropped every one, and only a human diff caught it. The star-rejection gate could not: it only proves a façade does not WIDEN invisibly. Narrowing is the failure an explicit list newly makes possible, because `export *` could not narrow by construction. So the property the stars gave for free is now asserted directly — every name a re-exported source declares must appear in the façade. Scoped to `packages/*/src/facades/`, the barrels this PR converted. A hand-curated package `index.ts` is a different thing: `ad-replay` deliberately publishes two values out of a much larger `internal/`, and forcing exhaustiveness there would widen a surface its owner narrowed on purpose (#1555). A source that itself carries a bare `export *` is skipped — unknowable from that file alone, and reachable because the façade re-exports the starred module directly too, which IS checked. Red evidence: dropping `MultiTargetAnnotationV1` from facades/replay.ts — one of the 13 the old gate was blind to — fails with the file, the source and the symbol named. 13 pass / 0 fail once restored. * fix(layering): close the exhaustiveness gate's starred-source hole Two review findings, plus a third the gate caught on itself. P1 — the three `DEFAULT_DRAG_*` constants join the existing public-façade suppression, alongside `COORDINATE_GESTURE_KINDS` and `normalizePublicGesture` which the same conversion surfaced. All five are #1567's drag vocabulary, made individually visible to `--production` analysis for the first time because a bare star used to hide them from that exact check. Kept rather than narrowed, for the reason the existing entry already states: the façade's surface stays byte-identical to what the retired pin table asserted, and narrowing is a follow-up with its own review. P2 — the exhaustiveness gate skipped any source carrying a bare `export *`, which dropped that module's DIRECT exports from the check too. `gesture-plan.ts` stars `gesture-plan-types.ts`, so removing `buildDragGesturePlan` from the façade narrowed the public surface and still passed. `readDirectNamedExports` now reads exactly the names a module declares or re-exports BY NAME and ignores the star, so direct exports are checked while the starred set stays covered by the façade's own direct re-export of that module. Red evidence: removing `buildDragGesturePlan` from facades/interaction.ts now fails naming file, source and symbol; 13 pass / 0 fail restored. Third, and the reason the gate is worth having: rebasing onto main after #1612 merged silently dropped `TEXT_ENTRY_ROUTES`, `TextEntryRoute` and `TypeTextBackendResult` from the interaction façade — the same narrowing class as the #1567 one review caught by hand, one merge later. The gate failed on it before CI did. Restored.
Summary
Adds a generic selector/ref-targeted drag gesture across the CLI, Node client, MCP, recording, and replay surfaces.
targets-v1annotation containing source and destination identity evidence; replay verifies both endpoints before pointer-down and attributes a later guard mismatch to the correct endpoint.Public API
CLI:
Node:
MCP:
{ "kind": "drag", "source": "id=\"drag-source\"", "destination": "id=\"drop-target\"", "sourceHoldMs": 700, "moveMs": 600, "destinationHoldMs": 200 }Defaults are 800 ms source hold, 500 ms movement, and 0 ms destination hold. The combined plan is capped at 10 seconds.
Recording and replay
A recorded drag stores one annotation of this shape:
Snapshot refs are materialized to portable selector chains. Strict publication refuses either endpoint when it remains a session-local ref or when dual identity evidence is missing. Replay resolves and verifies source and destination sequentially before dispatch, then threads both verified guards through the daemon boundary.
Validation
pnpm check:affected --runmain.drag completed yes.Video evidence
native-ios.mp4