Skip to content

Commit c4d517f

Browse files
committed
fix(selectors): keep the moving-target reason in the vocabulary the adapters read (#3471)
The eager-closure gate (ADR 0027, `scripts/__tests__/eager-closure-budgets.test.ts`) holds every façade that reads an interaction reason to "no more modules than the merge-base evaluated". Importing `TARGET_STILL_MOVING_REASON` from `@agent-device/contracts/moving-target-gate` pulled that module and its kernel dependencies into six closures, `packages/maestro/src/daemon-port/daemon-runtime-port.ts` and `packages/replay-port/src/daemon-port/session-replay-maestro-runtime.ts` among them. The reason is a literal in `INTERACTION_ERROR_REASONS` again, the one vocabulary every adapter already reads, so the leaf imports nothing; the hint stays in contracts, which is on no façade path.
1 parent 113ae5e commit c4d517f

4 files changed

Lines changed: 22 additions & 19 deletions

File tree

‎packages/contracts/src/moving-target-gate.test.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
MOVING_TARGET_GATE_RECT_TOLERANCE_PX,
55
MOVING_TARGET_GATE_SAMPLE_GAP_MS,
66
MOVING_TARGET_GATE_WINDOW_MS,
7-
TARGET_STILL_MOVING_REASON,
87
movingTargetGateDecision,
98
movingTargetRectsStable,
109
targetStillMovingHint,
@@ -163,10 +162,6 @@ test('the stability comparison is symmetric', () => {
163162

164163
// --- the refusal's published recovery ---
165164

166-
test('the reason is the one shared constant, not a per-producer literal', () => {
167-
assert.equal(TARGET_STILL_MOVING_REASON, 'target_still_moving');
168-
});
169-
170165
test('a refused selector press names the wait that recovers it', () => {
171166
const hint = targetStillMovingHint({ selector: 'label="Slide target"' });
172167

‎packages/contracts/src/moving-target-gate.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,13 @@ export function movingTargetRectsStable(
112112
);
113113
}
114114

115-
/** The one reason a press refuses because it cannot name a position its target is at rest in. */
116-
export const TARGET_STILL_MOVING_REASON = 'target_still_moving';
117-
118115
/**
119-
* The hint published beside every {@link TARGET_STILL_MOVING_REASON} refusal — the recovery that
120-
* works, owned here so no producer can publish the reason without it.
116+
* The hint published beside a `target_still_moving` refusal — the recovery that works, with its
117+
* wording owned here so every producer publishes one text. Pairing the two is a convention rather
118+
* than something this module enforces: the reason is a literal in the vocabulary every interaction
119+
* adapter reads (`INTERACTION_ERROR_REASONS.targetStillMoving` in
120+
* `packages/selectors/src/interaction-error.ts`), so a producer could raise it without this hint.
121+
* The runtime moving-target gate is the only producer today and publishes both together.
121122
*
122123
* A selector re-resolves against a fresh capture and can be retried as-is. An `@ref` cannot survive
123124
* the wait: the capture a `wait` takes retires the ref frame (ADR 0014), which is the same reason

‎packages/selectors/src/interaction-error.ts‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { TARGET_STILL_MOVING_REASON } from '@agent-device/contracts/moving-target-gate';
21
import type { Rect, SnapshotCoveringNode, SnapshotNode } from '@agent-device/kernel/snapshot';
32

4-
/** Machine-readable `error.details.reason` values shared by interaction producers and adapters. */
3+
/**
4+
* Machine-readable `error.details.reason` values shared by interaction producers and adapters.
5+
*
6+
* Every value is a literal, and this module imports nothing: these codes are the vocabulary the
7+
* Maestro, replay, and host adapters read, and the eager-closure gate (`scripts/__tests__/
8+
* eager-closure-budgets.test.ts`) holds this leaf at the size it has — a shared constant imported
9+
* from a heavier package makes every façade that reads a reason evaluate that package.
10+
*/
511
export const INTERACTION_ERROR_REASONS = {
612
selectorNotFound: 'selector_not_found',
713
predicateFailed: 'predicate_failed',
@@ -26,12 +32,13 @@ export const INTERACTION_ERROR_REASONS = {
2632
/**
2733
* A press or click refused because its target's rect was still changing when the transition window
2834
* after the session's last mutating action ran out. `details.selector` or `details.ref` names the
29-
* target, `details.rect` where it was resolved and `details.movedToRect` where the re-observation
30-
* found it. Branch on this reason, not the message: the recovery is to wait for the surface and
31-
* retry, never to retry the same point immediately. The owner is `moving-target-gate.ts` in this
32-
* package, which raises the reason beside the hint that names that recovery.
35+
* target, `details.rect` where the command resolved it and `details.movedToRect` where the last
36+
* re-observation found it. Branch on this reason, not the message: the recovery is to wait for the
37+
* surface and retry, never to retry the same point immediately. The owner is
38+
* `src/commands/interaction/runtime/moving-target-gate.ts`, which raises the reason beside the
39+
* hint that names that recovery.
3340
*/
34-
targetStillMoving: TARGET_STILL_MOVING_REASON,
41+
targetStillMoving: 'target_still_moving',
3542
} as const;
3643

3744
/** The part of a covered node a refusal reports. */

‎src/commands/interaction/runtime/moving-target-gate.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
22
MOVING_TARGET_GATE_SAMPLE_GAP_MS,
3-
TARGET_STILL_MOVING_REASON,
43
movingTargetGateDecision,
54
movingTargetRectsStable,
65
targetStillMovingHint,
76
type MovingTargetGateDecision,
87
} from '@agent-device/contracts/moving-target-gate';
8+
import { INTERACTION_ERROR_REASONS } from '@agent-device/selectors/interaction-error';
99
import type { ResolvedInteractionTarget } from '@agent-device/contracts/interaction';
1010
import type { Rect, SnapshotNode } from '@agent-device/kernel/snapshot';
1111
import { AppError, discloseDispatch } from '@agent-device/kernel/errors';
@@ -208,7 +208,7 @@ function motionRefusal(
208208
? `Selector ${selector} is still moving, so no resting position could be confirmed to press`
209209
: `Ref ${ref} is still moving, so no resting position could be confirmed to press`,
210210
{
211-
reason: TARGET_STILL_MOVING_REASON,
211+
reason: INTERACTION_ERROR_REASONS.targetStillMoving,
212212
...(selector !== undefined ? { selector } : { ref }),
213213
...(resolved.node?.rect ? { rect: resolved.node.rect } : {}),
214214
...(disagreement?.secondRect ? { movedToRect: disagreement.secondRect } : {}),

0 commit comments

Comments
 (0)