Skip to content

Commit 6cfc93e

Browse files
committed
refactor(contracts): name the runtime operation vocabulary below the operations union
The lifecycle execution carries the operations a plan requires, but typing that list with the operations union closed a 36-file type cycle: the operations types depend on the lifecycle types. The vocabulary now lives as a const list below both, proven equal to the union by a type test, so the plan is typed end to end, the Apple host table indexes it without casts, and the daemon narrows descriptor names through a guard instead of a cast.
1 parent c085401 commit 6cfc93e

7 files changed

Lines changed: 144 additions & 15 deletions

File tree

packages/contracts/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@
359359
"types": "./src/platform-runtime-unavailable.ts",
360360
"default": "./src/platform-runtime-unavailable.ts"
361361
},
362+
"./runtime-operation-names": {
363+
"types": "./src/runtime-operation-names.ts",
364+
"default": "./src/runtime-operation-names.ts"
365+
},
362366
"./progress": {
363367
"types": "./src/facades/progress.ts",
364368
"default": "./src/facades/progress.ts"

packages/contracts/src/application-lifecycle-runtime.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { RuntimeOperationFact, RuntimeOperationKey } from './platform-runtime.ts';
2-
import type { PlatformRuntimeOperations } from './platform-runtime-operations.ts';
1+
import type { RuntimeOperationName } from './runtime-operation-names.ts';
2+
import type { RuntimeOperationFact } from './platform-runtime.ts';
33
import type { DeviceInfo } from '@agent-device/kernel/device';
44
import type { Interactor, RunnerContext } from './interactor-types.ts';
55
import type { RunnerLogicalLeaseContext } from './runner-lease-context.ts';
@@ -46,7 +46,7 @@ export type ApplicationLifecycleExecution = Readonly<{
4646
* declared runtime uses. Never a public flag and never on the wire. Absent when the future of the
4747
* session is unknown (a standalone command).
4848
*/
49-
plannedOperations?: readonly RuntimeOperationKey<PlatformRuntimeOperations>[];
49+
plannedOperations?: readonly RuntimeOperationName[];
5050
}>;
5151

5252
/** Semantic target resolution used before an application open. */

packages/contracts/src/platform-runtime-operations.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import assert from 'node:assert/strict';
22
import { test, vi } from 'vitest';
3-
import { localRuntimeOwner, providerRuntimeOwner } from './platform-runtime.ts';
43
import {
4+
localRuntimeOwner,
5+
providerRuntimeOwner,
6+
type RuntimeOperationKey,
7+
} from './platform-runtime.ts';
8+
import {
9+
RUNTIME_OPERATION_NAMES,
10+
isRuntimeOperationName,
11+
type RuntimeOperationName,
12+
} from './runtime-operation-names.ts';
13+
import {
14+
type PlatformRuntimeOperations,
15+
type PlatformRuntimeProviderModule,
516
bootTargetHeadlessUse,
617
bootTargetUse,
718
captureSnapshotUse,
819
resolveDeviceReadinessRuntimePlan,
920
resolveSnapshotRuntimePlan,
10-
type PlatformRuntimeProviderModule,
1121
} from './platform-runtime-operations.ts';
1222

1323
function compileTimeProviderModuleProof(): void {
@@ -99,3 +109,18 @@ test.each([
99109
});
100110
},
101111
);
112+
113+
// The value-level vocabulary must name exactly the operations union: a name missing from the list
114+
// or an extra name both collapse these assignments to a compile error.
115+
type OperationKey = RuntimeOperationKey<PlatformRuntimeOperations>;
116+
type MissingFromList = Exclude<OperationKey, RuntimeOperationName>;
117+
type ExtraInList = Exclude<RuntimeOperationName, OperationKey>;
118+
const noOperationIsMissingFromTheList: [MissingFromList] extends [never] ? true : never = true;
119+
const noListedNameIsUnknown: [ExtraInList] extends [never] ? true : never = true;
120+
121+
test('the runtime operation vocabulary is the operations union, with no duplicates', () => {
122+
assert.equal(noOperationIsMissingFromTheList && noListedNameIsUnknown, true);
123+
assert.equal(new Set(RUNTIME_OPERATION_NAMES).size, RUNTIME_OPERATION_NAMES.length);
124+
assert.equal(isRuntimeOperationName('captureSnapshot'), true);
125+
assert.equal(isRuntimeOperationName('notAnOperation'), false);
126+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* The runtime operation vocabulary as a value, for contracts that name operations without
3+
* importing the operations union that sits above them (the lifecycle execution carries the
4+
* operations a plan requires; the operations types depend on the lifecycle types). The
5+
* `platform-runtime-operations` test proves this list and `RuntimeOperationKey<PlatformRuntimeOperations>`
6+
* name exactly the same members, so adding an operation refuses to pass until it is listed here.
7+
*/
8+
export const RUNTIME_OPERATION_NAMES = [
9+
'acceptAlert',
10+
'appLogCleanup',
11+
'appLogDoctor',
12+
'appLogInspect',
13+
'appLogReattach',
14+
'appLogStart',
15+
'appState',
16+
'appSwitcher',
17+
'applyRuntimeHints',
18+
'audioProbeCleanup',
19+
'audioProbeQuery',
20+
'audioProbeReattach',
21+
'audioProbeStart',
22+
'awaitAlert',
23+
'back',
24+
'bootTarget',
25+
'bootTargetHeadless',
26+
'captureScreenshot',
27+
'captureSnapshot',
28+
'captureSnapshotWithCustomActions',
29+
'captureSnapshotWithoutActiveApp',
30+
'clearRuntimeHints',
31+
'closeApplication',
32+
'configureProviderPortReverse',
33+
'deployApp',
34+
'deployMaterializedApp',
35+
'dismissAlert',
36+
'ensureReady',
37+
'fillPoint',
38+
'fillRef',
39+
'finalizeApplicationClose',
40+
'findSelector',
41+
'findText',
42+
'focusPoint',
43+
'gestureViewport',
44+
'home',
45+
'hoverPoint',
46+
'hoverRef',
47+
'keyboardDismiss',
48+
'keyboardEnter',
49+
'keyboardStatus',
50+
'listApps',
51+
'longPressPoint',
52+
'materializeAppSource',
53+
'networkDump',
54+
'openApplication',
55+
'perfFrames',
56+
'perfMemorySample',
57+
'perfMemorySnapshot',
58+
'perfNativeCaptureCleanup',
59+
'perfNativeCaptureReattach',
60+
'perfNativeCaptureStart',
61+
'perfProfileReport',
62+
'performDirectionalFlingPlan',
63+
'performGesturePlan',
64+
'performMultiTouchGesturePlan',
65+
'performTargetAuthoredDrag',
66+
'prepareAppleRunner',
67+
'prepareApplicationOpen',
68+
'readAlert',
69+
'readClipboard',
70+
'readTextAtPoint',
71+
'resolveOpenTarget',
72+
'screenRecordingCleanup',
73+
'screenRecordingReattach',
74+
'screenRecordingStart',
75+
'scrollDirection',
76+
'sendPushNotification',
77+
'setOrientation',
78+
'setSetting',
79+
'setViewport',
80+
'shutdownTarget',
81+
'tapElementSelector',
82+
'tapPoint',
83+
'tapRef',
84+
'triggerAppEvent',
85+
'tvRemote',
86+
'typeText',
87+
'writeClipboard',
88+
] as const;
89+
90+
export type RuntimeOperationName = (typeof RUNTIME_OPERATION_NAMES)[number];
91+
92+
const runtimeOperationNames: ReadonlySet<string> = new Set(RUNTIME_OPERATION_NAMES);
93+
94+
export function isRuntimeOperationName(value: string): value is RuntimeOperationName {
95+
return runtimeOperationNames.has(value);
96+
}

packages/platform-apple/src/runner-demand.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { OpenApplicationRunnerDemand } from '@agent-device/contracts/application-lifecycle-runtime';
2-
import type { RuntimeOperationKey } from '@agent-device/contracts/platform-runtime';
3-
import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations';
2+
import type { RuntimeOperationName } from '@agent-device/contracts/runtime-operation-names';
43

54
/**
65
* Which host the Apple runtime executes each declared runtime operation through on a local iOS
@@ -9,12 +8,12 @@ import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform
98
*
109
* Bridge-eligible snapshots keep their typed XCTest fallback, but a fallback is a recovery, not a
1110
* plan requirement, so they classify as `simulator`. The record is complete over the runtime
12-
* operation union by construction: a new operation refuses to compile until it is classified.
11+
* operation vocabulary by construction: a new operation refuses to compile until it is classified.
1312
*/
1413
type AppleSimulatorOperationHost = 'runner' | 'simulator';
1514

1615
const APPLE_SIMULATOR_OPERATION_HOSTS: Readonly<
17-
Record<RuntimeOperationKey<PlatformRuntimeOperations>, AppleSimulatorOperationHost>
16+
Record<RuntimeOperationName, AppleSimulatorOperationHost>
1817
> = Object.freeze({
1918
// Application lifecycle: simctl launch/terminate and host readiness.
2019
resolveOpenTarget: 'simulator',
@@ -107,7 +106,7 @@ const APPLE_SIMULATOR_OPERATION_HOSTS: Readonly<
107106
* any runner-served operation makes readiness worth preparing now.
108107
*/
109108
export function resolveAppleSimulatorRunnerDemand(
110-
operations: readonly RuntimeOperationKey<PlatformRuntimeOperations>[] | undefined,
109+
operations: readonly RuntimeOperationName[] | undefined,
111110
): OpenApplicationRunnerDemand {
112111
if (operations === undefined) return 'possible';
113112
return operations.some((operation) => APPLE_SIMULATOR_OPERATION_HOSTS[operation] === 'runner')

scripts/layering/contracts-exports.snapshot.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"@agent-device/contracts/remote",
9494
"@agent-device/contracts/replay",
9595
"@agent-device/contracts/runner-lease-context",
96+
"@agent-device/contracts/runtime-operation-names",
9697
"@agent-device/contracts/screen-recording-runtime",
9798
"@agent-device/contracts/screen-recording-runtime-host",
9899
"@agent-device/contracts/screen-recording-runtime-plan",

src/core/command-descriptor/planned-operations.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { RuntimeUseStep } from '@agent-device/contracts/command-platform-execution';
2-
import type { RuntimeOperationKey } from '@agent-device/contracts/platform-runtime';
3-
import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations';
2+
import {
3+
isRuntimeOperationName,
4+
type RuntimeOperationName,
5+
} from '@agent-device/contracts/runtime-operation-names';
46
import { commandDescriptors } from './registry.ts';
57
import type { CommandDescriptor } from './types.ts';
68

@@ -11,7 +13,7 @@ const descriptorsByName = new Map<string, CommandDescriptor>(
1113
/** One step of a plan as the batch runner holds it: the command plus what its handler will read. */
1214
export type PlannedStep = RuntimeUseStep & Readonly<{ command: string }>;
1315

14-
export type PlannedRuntimeOperation = RuntimeOperationKey<PlatformRuntimeOperations>;
16+
export type PlannedRuntimeOperation = RuntimeOperationName;
1517

1618
/**
1719
* The runtime operations a sequence of steps must execute, read from each command's declared
@@ -34,8 +36,10 @@ export function resolvePlannedRuntimeOperations(
3436
const uses =
3537
'uses' in execution ? (execution.selectUses?.(step) ?? execution.uses) : [execution.use];
3638
for (const use of uses) {
37-
// Every declared use is built with `defineUse`, which admits only operation keys.
38-
for (const operation of use.required as readonly PlannedRuntimeOperation[]) {
39+
for (const operation of use.required) {
40+
// `defineUse` admits only operation keys, so an unknown name means the vocabulary list
41+
// and the operations union drifted; treat the plan as unproven rather than guess.
42+
if (!isRuntimeOperationName(operation)) return undefined;
3943
operations.add(operation);
4044
}
4145
}

0 commit comments

Comments
 (0)