Skip to content

Commit b223f47

Browse files
committed
refactor(contracts): declare each interactor operation once
Second review P1 on #2021. `interactor-operation-catalog.ts` declared the same operation set three times — a name tuple, a complete local binder map, and a complete provider binder map — and each facet carried a mirrored `bindLocal…Interactor`/`bindProvider…Interactor` pair whose only difference was which interactor source to use and which label a refusal names. There is now one row per operation, carrying its facts key, its provider refusal label, and the facet's own executor. The local/provider split lives in the two adapters, which differ by exactly the thing that differs: the interactor source. Adding an operation is adding one row. Deleted: the parallel tuple, both binder maps, 32 mirrored wrappers across ten facet modules, and the per-facet `Local…`/`Provider…InteractorResolver` aliases that existed only to be re-exported. Kept: every facet's typed executor, now exported as its binding surface. Net −563 production lines in `packages/contracts`. Two consumers moved onto the catalog's public entry point rather than keeping a private path to a single operation: the app-event delivery test and the provider scenario fixture, whose two hand-bound keyboard legs are now whichever legs its facts admit. Each facet's tests spell out the composition the retired wrappers performed, so every assertion still exercises one executor reached through one source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RpfS12XApXqasuAWZJaEX
1 parent b50a9c2 commit b223f47

25 files changed

Lines changed: 385 additions & 798 deletions

packages/contracts/src/alert-runtime.test.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { expect, test, vi } from 'vitest';
2-
import {
3-
alertRuntimeOperationFacts,
4-
bindLocalAlertAcceptInteractor,
5-
bindLocalAlertDismissInteractor,
6-
bindLocalAlertReadInteractor,
7-
bindLocalAlertWaitInteractor,
8-
bindProviderAlertAcceptInteractor,
9-
} from './alert-runtime.ts';
2+
import { alertRuntimeOperationFacts, bindAlertLeg } from './alert-runtime.ts';
3+
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
104
import type { AlertInteractorOptions, Interactor } from './interactor-types.ts';
115

126
const device = {
@@ -18,6 +12,39 @@ const device = {
1812
booted: true,
1913
} as const;
2014

15+
// The composition the interactor catalog performs, spelled out so each assertion below
16+
// still exercises one facet executor reached through one interactor source.
17+
const bindLocalAlertReadInteractor = (params: {
18+
device: typeof device;
19+
signal: AbortSignal;
20+
resolveInteractor: any;
21+
}) => bindAlertLeg('readAlert', params.signal, localInteractorSource(params));
22+
const bindLocalAlertWaitInteractor = (params: {
23+
device: typeof device;
24+
signal: AbortSignal;
25+
resolveInteractor: any;
26+
}) => bindAlertLeg('awaitAlert', params.signal, localInteractorSource(params));
27+
const bindLocalAlertAcceptInteractor = (params: {
28+
device: typeof device;
29+
signal: AbortSignal;
30+
resolveInteractor: any;
31+
}) => bindAlertLeg('acceptAlert', params.signal, localInteractorSource(params));
32+
const bindLocalAlertDismissInteractor = (params: {
33+
device: typeof device;
34+
signal: AbortSignal;
35+
resolveInteractor: any;
36+
}) => bindAlertLeg('dismissAlert', params.signal, localInteractorSource(params));
37+
const bindProviderAlertAcceptInteractor = (params: {
38+
device: typeof device;
39+
signal: AbortSignal;
40+
resolveInteractor: any;
41+
}) =>
42+
bindAlertLeg(
43+
'acceptAlert',
44+
params.signal,
45+
providerInteractorSource({ ...params, operation: 'alert accept' }),
46+
);
47+
2148
test('builds the exact alert operation fact catalog', () => {
2249
const read = { available: true } as const;
2350
const wait = { available: false, reason: 'owner-capability-missing' } as const;

packages/contracts/src/alert-runtime.ts

Lines changed: 1 addition & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
import type { DeviceInfo } from '@agent-device/kernel/device';
2-
import {
3-
localInteractorSource,
4-
providerInteractorSource,
5-
type LocalInteractorOperationResolver,
6-
type ProviderInteractorOperationResolver,
7-
} from './interactor-operation-binding.ts';
81
import type { AlertInteractorOptions, Interactor, RunnerContext } from './interactor-types.ts';
92
import type { RuntimeOperationFact } from './platform-runtime.ts';
103
import type { SessionSurface } from './session-surface.ts';
@@ -111,7 +104,7 @@ function alertInteractorOptions(input: AlertRuntimeInput): AlertInteractorOption
111104

112105
type AlertLeg = 'readAlert' | 'awaitAlert' | 'acceptAlert' | 'dismissAlert';
113106

114-
function bindAlertLeg<Leg extends AlertLeg>(
107+
export function bindAlertLeg<Leg extends AlertLeg>(
115108
leg: Leg,
116109
signal: AbortSignal,
117110
resolveInteractor: (runner: RunnerContext) => Promise<Interactor>,
@@ -123,83 +116,3 @@ function bindAlertLeg<Leg extends AlertLeg>(
123116
},
124117
}) as Readonly<Record<Leg, (input: AlertRuntimeInput) => Promise<Record<string, unknown>>>>;
125118
}
126-
127-
export type LocalAlertInteractorResolver = LocalInteractorOperationResolver;
128-
export type ProviderAlertInteractorResolver = ProviderInteractorOperationResolver;
129-
130-
type LocalAlertBinding = Readonly<{
131-
device: DeviceInfo;
132-
signal: AbortSignal;
133-
resolveInteractor: LocalAlertInteractorResolver;
134-
}>;
135-
136-
type ProviderAlertBinding = Readonly<{
137-
device: DeviceInfo;
138-
signal: AbortSignal;
139-
resolveInteractor: ProviderAlertInteractorResolver;
140-
}>;
141-
142-
export function bindLocalAlertReadInteractor(
143-
params: LocalAlertBinding,
144-
): AlertReadRuntimeOperations {
145-
return bindAlertLeg('readAlert', params.signal, localInteractorSource(params));
146-
}
147-
148-
export function bindLocalAlertWaitInteractor(
149-
params: LocalAlertBinding,
150-
): AlertWaitRuntimeOperations {
151-
return bindAlertLeg('awaitAlert', params.signal, localInteractorSource(params));
152-
}
153-
154-
export function bindLocalAlertAcceptInteractor(
155-
params: LocalAlertBinding,
156-
): AlertAcceptRuntimeOperations {
157-
return bindAlertLeg('acceptAlert', params.signal, localInteractorSource(params));
158-
}
159-
160-
export function bindLocalAlertDismissInteractor(
161-
params: LocalAlertBinding,
162-
): AlertDismissRuntimeOperations {
163-
return bindAlertLeg('dismissAlert', params.signal, localInteractorSource(params));
164-
}
165-
166-
/** Provider bindings fail closed when their exact owner no longer exposes its interactor. */
167-
export function bindProviderAlertReadInteractor(
168-
params: ProviderAlertBinding,
169-
): AlertReadRuntimeOperations {
170-
return bindAlertLeg(
171-
'readAlert',
172-
params.signal,
173-
providerInteractorSource({ ...params, operation: 'alert get' }),
174-
);
175-
}
176-
177-
export function bindProviderAlertWaitInteractor(
178-
params: ProviderAlertBinding,
179-
): AlertWaitRuntimeOperations {
180-
return bindAlertLeg(
181-
'awaitAlert',
182-
params.signal,
183-
providerInteractorSource({ ...params, operation: 'alert wait' }),
184-
);
185-
}
186-
187-
export function bindProviderAlertAcceptInteractor(
188-
params: ProviderAlertBinding,
189-
): AlertAcceptRuntimeOperations {
190-
return bindAlertLeg(
191-
'acceptAlert',
192-
params.signal,
193-
providerInteractorSource({ ...params, operation: 'alert accept' }),
194-
);
195-
}
196-
197-
export function bindProviderAlertDismissInteractor(
198-
params: ProviderAlertBinding,
199-
): AlertDismissRuntimeOperations {
200-
return bindAlertLeg(
201-
'dismissAlert',
202-
params.signal,
203-
providerInteractorSource({ ...params, operation: 'alert dismiss' }),
204-
);
205-
}

packages/contracts/src/app-event-runtime.test.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { expect, test, vi } from 'vitest';
2-
import {
3-
appEventRuntimeOperationFacts,
4-
bindLocalAppEventInteractor,
5-
bindProviderAppEventInteractor,
6-
} from './app-event-runtime.ts';
2+
import { appEventRuntimeOperationFacts, bindAppEvent } from './app-event-runtime.ts';
73
import type { Interactor } from './interactor-types.ts';
4+
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
85

96
const device = {
107
platform: 'android',
@@ -14,6 +11,19 @@ const device = {
1411
booted: true,
1512
} as const;
1613

14+
const bindAppEventLocal = (
15+
params: Parameters<typeof localInteractorSource>[0] & { signal: AbortSignal },
16+
) => bindAppEvent(params.signal, localInteractorSource(params));
17+
const bindAppEventProvider = (
18+
params: Parameters<typeof providerInteractorSource>[0] extends infer P
19+
? Omit<P, 'operation'> & { signal: AbortSignal }
20+
: never,
21+
) =>
22+
bindAppEvent(
23+
params.signal,
24+
providerInteractorSource({ ...params, operation: 'trigger-app-event' }),
25+
);
26+
1727
test('builds the exact app-event operation fact catalog', () => {
1828
const triggerAppEvent = { available: true } as const;
1929
expect(appEventRuntimeOperationFacts({ triggerAppEvent })).toEqual({ triggerAppEvent });
@@ -26,7 +36,7 @@ test('a local binding opens the resolved event URL against the session app', asy
2636
const resolveInteractor = vi.fn(async () => ({ open }) as unknown as Interactor);
2737
const signal = new AbortController().signal;
2838

29-
const operations = bindLocalAppEventInteractor({ device, signal, resolveInteractor });
39+
const operations = bindAppEventLocal({ device, signal, resolveInteractor });
3040
await operations.triggerAppEvent({
3141
eventUrl: 'myapp://agent-device/event?name=checkout',
3242
options: { appBundleId: 'com.example.app' },
@@ -45,7 +55,7 @@ test('a local binding opens the resolved event URL against the session app', asy
4555
});
4656

4757
test('a provider binding fails closed when its exact owner exposes no interactor', async () => {
48-
const operations = bindProviderAppEventInteractor({
58+
const operations = bindAppEventProvider({
4959
device,
5060
signal: new AbortController().signal,
5161
resolveInteractor: () => undefined,
@@ -63,7 +73,7 @@ test('an already-cancelled request never resolves an interactor', async () => {
6373
const open = vi.fn(async () => undefined);
6474
const resolveInteractor = vi.fn(async () => ({ open }) as unknown as Interactor);
6575

66-
const operations = bindLocalAppEventInteractor({
76+
const operations = bindAppEventLocal({
6777
device,
6878
signal: controller.signal,
6979
resolveInteractor,

packages/contracts/src/app-event-runtime.ts

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
import type { DeviceInfo } from '@agent-device/kernel/device';
2-
import {
3-
localInteractorSource,
4-
providerInteractorSource,
5-
type LocalInteractorOperationResolver,
6-
type ProviderInteractorOperationResolver,
7-
} from './interactor-operation-binding.ts';
81
import type { Interactor, RunnerContext } from './interactor-types.ts';
92
import type { RuntimeOperationFact } from './platform-runtime.ts';
103
import type { SnapshotRuntimeExecution } from './snapshot-runtime.ts';
@@ -46,7 +39,7 @@ export function appEventRuntimeOperationFacts(
4639
* owner is already chosen by the time a binder is called, so each entry point supplies its own
4740
* resolution and this holds only what both share: the runner context and the delivery itself.
4841
*/
49-
function bindAppEvent(
42+
export function bindAppEvent(
5043
signal: AbortSignal,
5144
resolveInteractor: (runner: RunnerContext) => Promise<Interactor>,
5245
): AppEventRuntimeOperations {
@@ -62,31 +55,3 @@ function bindAppEvent(
6255
},
6356
});
6457
}
65-
66-
export type LocalAppEventInteractorResolver = LocalInteractorOperationResolver;
67-
68-
export function bindLocalAppEventInteractor(
69-
params: Readonly<{
70-
device: DeviceInfo;
71-
signal: AbortSignal;
72-
resolveInteractor: LocalAppEventInteractorResolver;
73-
}>,
74-
): AppEventRuntimeOperations {
75-
return bindAppEvent(params.signal, localInteractorSource(params));
76-
}
77-
78-
export type ProviderAppEventInteractorResolver = ProviderInteractorOperationResolver;
79-
80-
/** Provider bindings fail closed when their exact owner no longer exposes its interactor. */
81-
export function bindProviderAppEventInteractor(
82-
params: Readonly<{
83-
device: DeviceInfo;
84-
signal: AbortSignal;
85-
resolveInteractor: ProviderAppEventInteractorResolver;
86-
}>,
87-
): AppEventRuntimeOperations {
88-
return bindAppEvent(
89-
params.signal,
90-
providerInteractorSource({ ...params, operation: 'trigger-app-event' }),
91-
);
92-
}

packages/contracts/src/app-switcher-runtime.test.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { expect, test, vi } from 'vitest';
2-
import {
3-
appSwitcherRuntimeOperationFacts,
4-
bindLocalAppSwitcherInteractor,
5-
bindProviderAppSwitcherInteractor,
6-
} from './app-switcher-runtime.ts';
2+
import { appSwitcherRuntimeOperationFacts, bindAppSwitcher } from './app-switcher-runtime.ts';
73
import type { Interactor } from './interactor-types.ts';
4+
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
85

96
const device = {
107
platform: 'android',
@@ -14,6 +11,19 @@ const device = {
1411
booted: true,
1512
} as const;
1613

14+
const bindAppSwitcherLocal = (
15+
params: Parameters<typeof localInteractorSource>[0] & { signal: AbortSignal },
16+
) => bindAppSwitcher(params.signal, localInteractorSource(params));
17+
const bindAppSwitcherProvider = (
18+
params: Parameters<typeof providerInteractorSource>[0] extends infer P
19+
? Omit<P, 'operation'> & { signal: AbortSignal }
20+
: never,
21+
) =>
22+
bindAppSwitcher(
23+
params.signal,
24+
providerInteractorSource({ ...params, operation: 'app-switcher' }),
25+
);
26+
1727
test('builds the exact app-switcher operation fact catalog', () => {
1828
const appSwitcher = { available: true } as const;
1929
expect(appSwitcherRuntimeOperationFacts({ appSwitcher })).toEqual({ appSwitcher });
@@ -24,7 +34,7 @@ test('a local binding drives the interactor with the request runner context', as
2434
const resolveInteractor = vi.fn(async () => ({ appSwitcher }) as unknown as Interactor);
2535
const signal = new AbortController().signal;
2636

27-
const operations = bindLocalAppSwitcherInteractor({ device, signal, resolveInteractor });
37+
const operations = bindAppSwitcherLocal({ device, signal, resolveInteractor });
2838
await operations.appSwitcher({
2939
options: { appBundleId: 'com.example.app' },
3040
execution: { logPath: '/tmp/daemon.log', requestId: 'switcher-1' },
@@ -40,7 +50,7 @@ test('a local binding drives the interactor with the request runner context', as
4050
});
4151

4252
test('a provider binding fails closed when its exact owner exposes no interactor', async () => {
43-
const operations = bindProviderAppSwitcherInteractor({
53+
const operations = bindAppSwitcherProvider({
4454
device,
4555
signal: new AbortController().signal,
4656
resolveInteractor: () => undefined,
@@ -58,7 +68,7 @@ test('an already-cancelled request never resolves an interactor', async () => {
5868
const appSwitcher = vi.fn(async () => undefined);
5969
const resolveInteractor = vi.fn(async () => ({ appSwitcher }) as unknown as Interactor);
6070

61-
const operations = bindLocalAppSwitcherInteractor({
71+
const operations = bindAppSwitcherLocal({
6272
device,
6373
signal: controller.signal,
6474
resolveInteractor,
Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
import type { DeviceInfo } from '@agent-device/kernel/device';
2-
import {
3-
localInteractorSource,
4-
providerInteractorSource,
5-
type LocalInteractorOperationResolver,
6-
type ProviderInteractorOperationResolver,
7-
} from './interactor-operation-binding.ts';
81
import type { Interactor, RunnerContext } from './interactor-types.ts';
92
import type { RuntimeOperationFact } from './platform-runtime.ts';
103
import type { SnapshotRuntimeExecution } from './snapshot-runtime.ts';
@@ -39,7 +32,7 @@ export function appSwitcherRuntimeOperationFacts(
3932
* owner is already chosen by the time a binder is called, so each entry point supplies its own
4033
* resolution and this holds only what both share: the runner context and the reveal itself.
4134
*/
42-
function bindAppSwitcher(
35+
export function bindAppSwitcher(
4336
signal: AbortSignal,
4437
resolveInteractor: (runner: RunnerContext) => Promise<Interactor>,
4538
): AppSwitcherRuntimeOperations {
@@ -55,31 +48,3 @@ function bindAppSwitcher(
5548
},
5649
});
5750
}
58-
59-
export type LocalAppSwitcherInteractorResolver = LocalInteractorOperationResolver;
60-
61-
export function bindLocalAppSwitcherInteractor(
62-
params: Readonly<{
63-
device: DeviceInfo;
64-
signal: AbortSignal;
65-
resolveInteractor: LocalAppSwitcherInteractorResolver;
66-
}>,
67-
): AppSwitcherRuntimeOperations {
68-
return bindAppSwitcher(params.signal, localInteractorSource(params));
69-
}
70-
71-
export type ProviderAppSwitcherInteractorResolver = ProviderInteractorOperationResolver;
72-
73-
/** Provider bindings fail closed when their exact owner no longer exposes its interactor. */
74-
export function bindProviderAppSwitcherInteractor(
75-
params: Readonly<{
76-
device: DeviceInfo;
77-
signal: AbortSignal;
78-
resolveInteractor: ProviderAppSwitcherInteractorResolver;
79-
}>,
80-
): AppSwitcherRuntimeOperations {
81-
return bindAppSwitcher(
82-
params.signal,
83-
providerInteractorSource({ ...params, operation: 'app-switcher' }),
84-
);
85-
}

0 commit comments

Comments
 (0)