Skip to content

Commit 17bdca7

Browse files
authored
refactor: migrate wait to request-bound runtime (#1875)
* refactor: migrate wait to request-bound runtime * fix: preserve native selector wait observation * fix: classify wait observations as conditional * refactor: compact conditional runtime declarations * fix: isolate selector runtime intents
1 parent 494eb52 commit 17bdca7

76 files changed

Lines changed: 2507 additions & 841 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/adr/0019-request-bound-platform-runtime.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ those descriptors or authorize a second local/provider chooser.
3838
tested for the exact platform leaf, device kind/backend, and provider mode; family ownership never
3939
implies uniform leaf support.
4040
- Command descriptors declare one typed execution shape: inventory use, or platform-neutral required
41-
device operations with separately declared preferred fast paths. Runtime owners report
41+
device operations with separately declared preferred optimizations and fact-conditional semantic
42+
operations. Runtime owners report
4243
device-specific facts and expose behavior-bearing facets; platform and provider implementations
4344
never name commands.
4445
- `RequestExecutionScope.bindDevice(device, use)` resolves provider ownership, validates the facts
@@ -67,8 +68,9 @@ those descriptors or authorize a second local/provider chooser.
6768
- Evidence is tiered by what a unit imports: request-scoped device units prove facts, operations,
6869
and parity cells; only durable-resource units carry the section 4–5 lifecycle evidence.
6970
- A handler binds once with its execution use. Admission, `capabilities`, and doctor questions use
70-
side-effect-free facts inspection; required-only declarations are the default and a preferred
71-
operation requires a recorded measurement.
71+
side-effect-free facts inspection; required-only declarations are the default, a preferred
72+
operation requires a recorded measurement, and a conditional operation requires parity evidence
73+
explaining why correctness depends on the owner fact.
7274
- Cross-cutting facets land with their first consuming command unit. Daemon startup recovery is
7375
evidence-gated, daemon shutdown is two-phase (detach, then stop), and session-teardown steps
7476
belong to their owning domains — there is no generic lifecycle-hook API.
@@ -156,11 +158,15 @@ procedure.
156158
### 2. Runtime use joins facts and narrows the bound runtime
157159

158160
`CommandDescriptor` remains the command declaration root. Its runtime-use declaration has a typed set
159-
of required platform-neutral operations and may separately name preferred optimizations. Commands
160-
whose use depends on normalized input first produce a discriminated execution plan that retains
161-
literal required/preferred types. Required and preferred operation keys are disjoint, and the
162-
required-only path is semantically complete; preferred operations may improve execution but are
163-
never necessary for command correctness.
161+
of required platform-neutral operations and may separately name preferred optimizations or
162+
fact-conditional semantic operations. Commands whose use depends on normalized input first produce a
163+
discriminated execution plan that retains literal required/preferred/conditional types. The three
164+
operation sets are pairwise disjoint. Preferred operations may improve execution but are never
165+
necessary for command correctness. A conditional operation is different: an owner that advertises
166+
it must provide it because that owner's semantically complete path depends on the observation; an
167+
owner that reports it unavailable must have a parity-proven complete path through the required
168+
operations. This prevents a correctness-bearing owner variation from being mislabeled as an
169+
optimization or made unconditionally required across owners without that semantic source.
164170

165171
Inventory commands have a separate `inventoryUse` declaration. `devices` calls the composed
166172
`DeviceInventoryGateway`, which selects canonical family sources and provider-owned inventory sources
@@ -180,22 +186,23 @@ operations from the same facet.
180186
`RequestExecutionScope.bindDevice(device, use)` is the trust choke point. It:
181187

182188
1. resolves the exact local or provider runtime owner;
183-
2. checks every required operation and classifies each preferred operation against facts for the
184-
platform leaf, device kind/backend, and provider mode;
189+
2. checks every required operation and classifies each preferred and conditional operation against
190+
facts for the platform leaf, device kind/backend, and provider mode;
185191
3. creates or reuses one request binding for that ownership-qualified device;
186-
4. verifies that every required operation and every preferred operation advertised as available has
187-
a concrete facet implementation; an advertised operation with no implementation is a
188-
runtime-contract error; and
192+
4. verifies that every required operation and every preferred or conditional operation advertised as
193+
available has a concrete facet implementation; an advertised operation with no implementation is
194+
a runtime-contract error; and
189195
5. returns a selected operation projection: required operations are non-optional, declared preferred
190-
operations are optional and present only when available, and undeclared operations are inaccessible.
196+
and conditional operations are optional and present only when available, and undeclared operations
197+
are inaccessible.
191198

192199
The cached broad runtime remains private to `RequestExecutionScope`; narrowing does not intersect a
193200
wide optional aggregate that would still expose undeclared facets. The descriptor and its specialized
194201
handler share one non-widened declaration, and a widened generic descriptor carries no static proof.
195202
A compile-time contract test proves the selected projection. A structural
196203
**runtime-facet-narrowing gate** covers every runtime-migrated handler owner and rejects attempts to
197204
manufacture required-operation proof with assertions or optional admission. Optional access is
198-
permitted only for descriptor-declared preferred operations. The tracking issue owns the gate
205+
permitted only for descriptor-declared preferred or conditional operations. The tracking issue owns the gate
199206
implementation and its required planted violation.
200207

201208
Absence or failure of a preferred path may change optimization/path disclosure, not whether the
@@ -204,6 +211,11 @@ reason and an explicit descriptor/ADR 0011 path classification; it is never a ge
204211
fallback. Helper/session reuse hidden inside one required operation remains that facet's implementation
205212
detail and follows ADR 0002 rather than becoming a daemon-visible preferred operation.
206213

214+
Conditional operations do not inherit that fallback rule. Their absence is valid only when the owner
215+
fact reports them unavailable and parity evidence establishes the required path for that owner cell.
216+
When the fact reports one available, omission of its implementation is a runtime-contract failure;
217+
the handler must not silently continue through a path known to lose the observation.
218+
207219
Family registration and support coverage are separate gates. The immutable registry owns each of the
208220
six canonical families exactly once. Before a command cuts over, an independent parity artifact
209221
freezes its legacy supported/unsupported cells and hints. Runtime-fact scenarios expand canonical
@@ -677,6 +689,11 @@ a preferred operation requires a recorded measurement of the fast path's benefit
677689
review; the direct-selector fast path is the model. A preferred operation declared without a
678690
measurement is speculative surface and is rejected in review.
679691

692+
Declaring a conditional operation instead requires a parity artifact showing both sides: the owner
693+
cells whose semantic path depends on the operation and the owner cells whose required path remains
694+
complete without it. A conditional declaration justified only by speed belongs under `preferred`;
695+
one justified only by universal correctness belongs under `required`.
696+
680697
### 10. Process-lifetime and cross-cutting surfaces
681698

682699
A cross-cutting facet — one consumed from more than one command's execution path, such as snapshot
@@ -727,8 +744,8 @@ platform-freedom is structurally enforced rather than measured.
727744
[ADR 0007](0007-remote-device-leases.md): daemon request-policy traits, lease admission, and lock
728745
ordering remain daemon-owned. Binding happens only after their admission requirements are met.
729746
- [ADR 0008](0008-command-descriptor-registry.md): the descriptor registry remains the command root.
730-
Device-command capability buckets evolve into typed required/preferred runtime use joined with
731-
exact runtime facts; inventory commands declare inventory use.
747+
Device-command capability buckets evolve into typed required/preferred/conditional runtime use
748+
joined with exact runtime facts; inventory commands declare inventory use.
732749
- [ADR 0009](0009-apple-platform-consolidation.md): the Apple family and `AppleOS` leaf axis remain.
733750
The shallow `PlatformPlugin` shape is superseded as command units migrate; physical shared
734751
mechanics move only through the legal injected substrate transition or after their last legacy

packages/contracts/src/command-platform-execution.test.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ describe('command platform execution declaration', () => {
88
{ kind: 'none' },
99
{ kind: 'legacy' },
1010
{ kind: 'inventory', use: inventoryUse },
11-
{ kind: 'device-runtime', use: { required: ['capture'], preferred: ['inspect'] } },
11+
{ kind: 'device-runtime', use: { required: ['capture'], preferred: [] } },
12+
{
13+
kind: 'device-runtime',
14+
use: { required: ['capture'], preferred: ['inspect'], conditional: ['observe'] },
15+
},
1216
])('accepts one closed execution shape: %j', (value) => {
1317
expect(() => assertCommandPlatformExecution(value)).not.toThrow();
1418
});
@@ -19,9 +23,27 @@ describe('command platform execution declaration', () => {
1923
{ kind: 'legacy', use: inventoryUse },
2024
{ kind: 'inventory' },
2125
{ kind: 'inventory', use: inventoryUse, legacy: true },
22-
{ kind: 'device-runtime', use: { required: [], preferred: [] }, inventory: true },
23-
{ kind: 'device-runtime', use: { required: ['capture', 'capture'], preferred: [] } },
24-
{ kind: 'device-runtime', use: { required: ['capture'], preferred: ['capture'] } },
26+
{
27+
kind: 'device-runtime',
28+
use: { required: [], preferred: [] },
29+
inventory: true,
30+
},
31+
{
32+
kind: 'device-runtime',
33+
use: { required: ['capture', 'capture'], preferred: [] },
34+
},
35+
{
36+
kind: 'device-runtime',
37+
use: { required: ['capture'], preferred: ['capture'] },
38+
},
39+
{
40+
kind: 'device-runtime',
41+
use: { required: ['capture'], preferred: [], conditional: ['capture'] },
42+
},
43+
{
44+
kind: 'device-runtime',
45+
use: { required: [], preferred: ['capture'], conditional: ['capture'] },
46+
},
2547
])('rejects neither, mixed, widened, duplicate, or overlapping declarations: %j', (value) => {
2648
expect(() => assertCommandPlatformExecution(value)).toThrow(/exactly one/);
2749
});
@@ -36,7 +58,10 @@ describe('command platform execution declaration', () => {
3658
{ kind: 'device-runtime', uses: [] },
3759
{ kind: 'device-runtime', use: appLogRuntimePlanUses[0], uses: appLogRuntimePlanUses },
3860
{ kind: 'device-runtime', uses: [appLogRuntimePlanUses[0], appLogRuntimePlanUses[0]] },
39-
{ kind: 'device-runtime', uses: [{ required: ['appLogStart'], preferred: ['appLogStart'] }] },
61+
{
62+
kind: 'device-runtime',
63+
uses: [{ required: ['appLogStart'], preferred: ['appLogStart'] }],
64+
},
4065
])('rejects empty, duplicate, overlapping, or both-form runtime uses: %j', (value) => {
4166
expect(() => assertCommandPlatformExecution(value)).toThrow(/exactly one/);
4267
});

packages/contracts/src/command-platform-execution.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { InventoryUse } from './platform-module.ts';
22
import type { RuntimeUseDeclaration } from './platform-runtime.ts';
3+
import { runtimeUseIdentity } from './platform-runtime-use.ts';
34

45
export type CommandPlatformExecution =
56
| Readonly<{ kind: 'none' }>
@@ -11,7 +12,7 @@ export type CommandPlatformExecution =
1112
uses: readonly [RuntimeUseDeclaration, ...RuntimeUseDeclaration[]];
1213
}>;
1314

14-
// The discriminated union cannot prove uniqueness or required/preferred disjointness inside
15+
// The discriminated union cannot prove uniqueness or operation-category disjointness inside
1516
// readonly arrays. Validate those declaration invariants where descriptors enter the registry.
1617
export function assertCommandPlatformExecution(
1718
value: unknown,
@@ -54,13 +55,6 @@ function hasRuntimeUseDeclarations(
5455
return new Set(identities).size === identities.length;
5556
}
5657

57-
function runtimeUseIdentity(use: RuntimeUseDeclaration): string {
58-
return JSON.stringify({
59-
required: [...use.required].sort(),
60-
preferred: [...use.preferred].sort(),
61-
});
62-
}
63-
6458
function hasExactInventoryUse(value: unknown): boolean {
6559
if (value === null || typeof value !== 'object') return false;
6660
const use = value as Record<string, unknown>;
@@ -70,12 +64,28 @@ function hasExactInventoryUse(value: unknown): boolean {
7064
function hasRuntimeUseDeclaration(value: unknown): boolean {
7165
if (value === null || typeof value !== 'object') return false;
7266
const use = value as Record<string, unknown>;
73-
const required = stringArray(use['required']);
74-
const preferred = stringArray(use['preferred']);
75-
if (!required || !preferred) return false;
76-
if (!hasUniqueValues(required) || !hasUniqueValues(preferred)) return false;
77-
if (!areDisjoint(required, preferred)) return false;
78-
return sameKeys(Object.keys(use).sort(), ['preferred', 'required']);
67+
const categories = [
68+
stringArray(use['required']),
69+
stringArray(use['preferred']),
70+
stringArray(use['conditional'] ?? []),
71+
];
72+
if (!hasValidRuntimeUseCategories(categories)) return false;
73+
const operations = categories.flat();
74+
if (new Set(operations).size !== operations.length) return false;
75+
return sameKeys(
76+
Object.keys(use).sort(),
77+
use['conditional'] === undefined
78+
? ['preferred', 'required']
79+
: ['conditional', 'preferred', 'required'],
80+
);
81+
}
82+
83+
function hasValidRuntimeUseCategories(
84+
categories: readonly (string[] | null)[],
85+
): categories is readonly string[][] {
86+
return categories.every(
87+
(category): category is string[] => category !== null && hasUniqueValues(category),
88+
);
7989
}
8090

8191
function stringArray(value: unknown): string[] | null {
@@ -87,11 +97,6 @@ function hasUniqueValues(values: readonly string[]): boolean {
8797
return new Set(values).size === values.length;
8898
}
8999

90-
function areDisjoint(left: readonly string[], right: readonly string[]): boolean {
91-
const leftValues = new Set(left);
92-
return right.every((value) => !leftValues.has(value));
93-
}
94-
95100
function sameKeys(actual: readonly string[], expected: readonly string[]): boolean {
96101
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
97102
}

packages/contracts/src/facades/platform.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,19 @@ export {
218218
resolveSnapshotRuntimePlan,
219219
screenshotRuntimePlanUses,
220220
selectorCaptureRuntimePlanUses,
221+
selectorTextCaptureRuntimePlanUses,
221222
snapshotRuntimePlanUses,
223+
waitSelectorCaptureRuntimePlanUses,
222224
viewportRuntimeUse,
223225
} from '../platform-runtime-operations.ts';
224226
export type {
225227
ScreenshotRuntimePlan,
228+
SelectorCaptureRuntimeIntent,
226229
SelectorCaptureRuntimePlan,
227230
SnapshotRuntimePlan,
228231
} from '../platform-runtime-operations.ts';
232+
export { waitObservesDevice } from '../wait-runtime-plan.ts';
233+
export type { WaitRuntimeTarget } from '../wait-runtime-plan.ts';
229234
export type {
230235
PlatformRuntimeHost,
231236
PlatformRuntimeModule,
@@ -258,8 +263,21 @@ export type {
258263
export {
259264
bindLocalSnapshotInteractor,
260265
bindProviderSnapshotInteractor,
266+
captureSnapshotSignal,
261267
snapshotRuntimeOperationFacts,
262268
} from '../snapshot-runtime.ts';
269+
export { selectorObservationRuntimeOperationFacts } from '../selector-observation-runtime.ts';
270+
export type {
271+
FindSelectorInput,
272+
FindSelectorResult,
273+
FindSelectorRuntimeOperations,
274+
FindTextInput,
275+
FindTextResult,
276+
FindTextRuntimeOperations,
277+
SelectorObservationRuntimeOperationFacts,
278+
SelectorObservationRuntimeOperations,
279+
SelectorObservationResult,
280+
} from '../selector-observation-runtime.ts';
263281
export type {
264282
CaptureSnapshotInput,
265283
LocalSnapshotInteractorResolver,

packages/contracts/src/interactor-types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,22 @@ export type Interactor = {
232232
point: Point,
233233
options?: { appBundleId?: string; surface?: SessionSurface; signal?: AbortSignal },
234234
): Promise<string | undefined>;
235+
/**
236+
* Native text-presence reading, when the backend has one that does not require a tree capture.
237+
* A `true` answer is authoritative; anything else means "not proven here" and the caller
238+
* consults the canonical tree (see `FindTextResult`).
239+
*/
240+
findText?(
241+
text: string,
242+
options?: { appBundleId?: string; signal?: AbortSignal },
243+
): Promise<{
244+
found: boolean;
245+
}>;
246+
/** Native one-sided observation for a simple selector; false defers to canonical capture. */
247+
findSelector?(
248+
selector: Readonly<{ key: ElementSelectorKey; value: string }>,
249+
options?: { appBundleId?: string; signal?: AbortSignal },
250+
): Promise<{ found: boolean }>;
235251
gestureViewport?(): Promise<Rect>;
236252
back(mode?: BackMode): Promise<void>;
237253
home(): Promise<void>;

packages/contracts/src/logs-runtime-cutover.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
type CommandPlatformExecution,
44
} from './command-platform-execution.ts';
55
import { appLogRuntimePlanUses } from './logs-runtime-plan.ts';
6-
import type { RuntimeUseDeclaration } from './platform-runtime.ts';
6+
import { runtimeUseIdentity } from './platform-runtime-use.ts';
77

88
/** Joins the input-dependent logs plans to one exhaustive descriptor declaration. */
99
export function assertLogsRuntimeExecution(
@@ -18,13 +18,6 @@ export function assertLogsRuntimeExecution(
1818
}
1919
}
2020

21-
function runtimeUseIdentity(use: RuntimeUseDeclaration): string {
22-
return JSON.stringify({
23-
required: [...use.required].sort(),
24-
preferred: [...use.preferred].sort(),
25-
});
26-
}
27-
2821
function invalidLogsExecution(): TypeError {
2922
return new TypeError(
3023
'Logs runtime execution must declare exactly the distinct uses selected by its seven plans',

packages/contracts/src/network-runtime-plan.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,14 @@ test('normalizes defaults and retains the requested alias and projection', () =>
4040
include: 'headers',
4141
use: networkDumpUse,
4242
});
43-
assert.deepEqual(networkDumpUse, { required: ['networkDump'], preferred: [] });
44-
assert.deepEqual(networkAdmissionUse, { required: [], preferred: ['networkDump'] });
43+
assert.deepEqual(networkDumpUse, {
44+
required: ['networkDump'],
45+
preferred: [],
46+
});
47+
assert.deepEqual(networkAdmissionUse, {
48+
required: [],
49+
preferred: ['networkDump'],
50+
});
4551
});
4652

4753
test('rejects actions and projections outside the frozen cells', () => {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,20 @@ test.each([
6060
true,
6161
'custom-actions-active-app',
6262
'captureSnapshotWithCustomActions',
63-
{ required: ['captureSnapshot', 'captureSnapshotWithCustomActions'], preferred: [] },
63+
{
64+
required: ['captureSnapshot', 'captureSnapshotWithCustomActions'],
65+
preferred: [],
66+
},
6467
],
6568
[
6669
false,
6770
false,
6871
'without-active-app',
6972
'captureSnapshotWithoutActiveApp',
70-
{ required: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'], preferred: [] },
73+
{
74+
required: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'],
75+
preferred: [],
76+
},
7177
],
7278
[
7379
true,

0 commit comments

Comments
 (0)