Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

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

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

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

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

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

Conditional operations do not inherit that fallback rule. Their absence is valid only when the owner
fact reports them unavailable and parity evidence establishes the required path for that owner cell.
When the fact reports one available, omission of its implementation is a runtime-contract failure;
the handler must not silently continue through a path known to lose the observation.

Family registration and support coverage are separate gates. The immutable registry owns each of the
six canonical families exactly once. Before a command cuts over, an independent parity artifact
freezes its legacy supported/unsupported cells and hints. Runtime-fact scenarios expand canonical
Expand Down Expand Up @@ -677,6 +689,11 @@ a preferred operation requires a recorded measurement of the fast path's benefit
review; the direct-selector fast path is the model. A preferred operation declared without a
measurement is speculative surface and is rejected in review.

Declaring a conditional operation instead requires a parity artifact showing both sides: the owner
cells whose semantic path depends on the operation and the owner cells whose required path remains
complete without it. A conditional declaration justified only by speed belongs under `preferred`;
one justified only by universal correctness belongs under `required`.

### 10. Process-lifetime and cross-cutting surfaces

A cross-cutting facet — one consumed from more than one command's execution path, such as snapshot
Expand Down Expand Up @@ -727,8 +744,8 @@ platform-freedom is structurally enforced rather than measured.
[ADR 0007](0007-remote-device-leases.md): daemon request-policy traits, lease admission, and lock
ordering remain daemon-owned. Binding happens only after their admission requirements are met.
- [ADR 0008](0008-command-descriptor-registry.md): the descriptor registry remains the command root.
Device-command capability buckets evolve into typed required/preferred runtime use joined with
exact runtime facts; inventory commands declare inventory use.
Device-command capability buckets evolve into typed required/preferred/conditional runtime use
joined with exact runtime facts; inventory commands declare inventory use.
- [ADR 0009](0009-apple-platform-consolidation.md): the Apple family and `AppleOS` leaf axis remain.
The shallow `PlatformPlugin` shape is superseded as command units migrate; physical shared
mechanics move only through the legal injected substrate transition or after their last legacy
Expand Down
35 changes: 30 additions & 5 deletions packages/contracts/src/command-platform-execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ describe('command platform execution declaration', () => {
{ kind: 'none' },
{ kind: 'legacy' },
{ kind: 'inventory', use: inventoryUse },
{ kind: 'device-runtime', use: { required: ['capture'], preferred: ['inspect'] } },
{ kind: 'device-runtime', use: { required: ['capture'], preferred: [] } },
{
kind: 'device-runtime',
use: { required: ['capture'], preferred: ['inspect'], conditional: ['observe'] },
},
])('accepts one closed execution shape: %j', (value) => {
expect(() => assertCommandPlatformExecution(value)).not.toThrow();
});
Expand All @@ -19,9 +23,27 @@ describe('command platform execution declaration', () => {
{ kind: 'legacy', use: inventoryUse },
{ kind: 'inventory' },
{ kind: 'inventory', use: inventoryUse, legacy: true },
{ kind: 'device-runtime', use: { required: [], preferred: [] }, inventory: true },
{ kind: 'device-runtime', use: { required: ['capture', 'capture'], preferred: [] } },
{ kind: 'device-runtime', use: { required: ['capture'], preferred: ['capture'] } },
{
kind: 'device-runtime',
use: { required: [], preferred: [] },
inventory: true,
},
{
kind: 'device-runtime',
use: { required: ['capture', 'capture'], preferred: [] },
},
{
kind: 'device-runtime',
use: { required: ['capture'], preferred: ['capture'] },
},
{
kind: 'device-runtime',
use: { required: ['capture'], preferred: [], conditional: ['capture'] },
},
{
kind: 'device-runtime',
use: { required: [], preferred: ['capture'], conditional: ['capture'] },
},
])('rejects neither, mixed, widened, duplicate, or overlapping declarations: %j', (value) => {
expect(() => assertCommandPlatformExecution(value)).toThrow(/exactly one/);
});
Expand All @@ -36,7 +58,10 @@ describe('command platform execution declaration', () => {
{ kind: 'device-runtime', uses: [] },
{ kind: 'device-runtime', use: appLogRuntimePlanUses[0], uses: appLogRuntimePlanUses },
{ kind: 'device-runtime', uses: [appLogRuntimePlanUses[0], appLogRuntimePlanUses[0]] },
{ kind: 'device-runtime', uses: [{ required: ['appLogStart'], preferred: ['appLogStart'] }] },
{
kind: 'device-runtime',
uses: [{ required: ['appLogStart'], preferred: ['appLogStart'] }],
},
])('rejects empty, duplicate, overlapping, or both-form runtime uses: %j', (value) => {
expect(() => assertCommandPlatformExecution(value)).toThrow(/exactly one/);
});
Expand Down
43 changes: 24 additions & 19 deletions packages/contracts/src/command-platform-execution.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { InventoryUse } from './platform-module.ts';
import type { RuntimeUseDeclaration } from './platform-runtime.ts';
import { runtimeUseIdentity } from './platform-runtime-use.ts';

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

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

function runtimeUseIdentity(use: RuntimeUseDeclaration): string {
return JSON.stringify({
required: [...use.required].sort(),
preferred: [...use.preferred].sort(),
});
}

function hasExactInventoryUse(value: unknown): boolean {
if (value === null || typeof value !== 'object') return false;
const use = value as Record<string, unknown>;
Expand All @@ -70,12 +64,28 @@ function hasExactInventoryUse(value: unknown): boolean {
function hasRuntimeUseDeclaration(value: unknown): boolean {
if (value === null || typeof value !== 'object') return false;
const use = value as Record<string, unknown>;
const required = stringArray(use['required']);
const preferred = stringArray(use['preferred']);
if (!required || !preferred) return false;
if (!hasUniqueValues(required) || !hasUniqueValues(preferred)) return false;
if (!areDisjoint(required, preferred)) return false;
return sameKeys(Object.keys(use).sort(), ['preferred', 'required']);
const categories = [
stringArray(use['required']),
stringArray(use['preferred']),
stringArray(use['conditional'] ?? []),
];
if (!hasValidRuntimeUseCategories(categories)) return false;
const operations = categories.flat();
if (new Set(operations).size !== operations.length) return false;
return sameKeys(
Object.keys(use).sort(),
use['conditional'] === undefined
? ['preferred', 'required']
: ['conditional', 'preferred', 'required'],
);
}

function hasValidRuntimeUseCategories(
categories: readonly (string[] | null)[],
): categories is readonly string[][] {
return categories.every(
(category): category is string[] => category !== null && hasUniqueValues(category),
);
}

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

function areDisjoint(left: readonly string[], right: readonly string[]): boolean {
const leftValues = new Set(left);
return right.every((value) => !leftValues.has(value));
}

function sameKeys(actual: readonly string[], expected: readonly string[]): boolean {
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/contracts/src/facades/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,19 @@ export {
resolveSnapshotRuntimePlan,
screenshotRuntimePlanUses,
selectorCaptureRuntimePlanUses,
selectorTextCaptureRuntimePlanUses,
snapshotRuntimePlanUses,
waitSelectorCaptureRuntimePlanUses,
viewportRuntimeUse,
} from '../platform-runtime-operations.ts';
export type {
ScreenshotRuntimePlan,
SelectorCaptureRuntimeIntent,
SelectorCaptureRuntimePlan,
SnapshotRuntimePlan,
} from '../platform-runtime-operations.ts';
export { waitObservesDevice } from '../wait-runtime-plan.ts';
export type { WaitRuntimeTarget } from '../wait-runtime-plan.ts';
export type {
PlatformRuntimeHost,
PlatformRuntimeModule,
Expand Down Expand Up @@ -258,8 +263,21 @@ export type {
export {
bindLocalSnapshotInteractor,
bindProviderSnapshotInteractor,
captureSnapshotSignal,
snapshotRuntimeOperationFacts,
} from '../snapshot-runtime.ts';
export { selectorObservationRuntimeOperationFacts } from '../selector-observation-runtime.ts';
export type {
FindSelectorInput,
FindSelectorResult,
FindSelectorRuntimeOperations,
FindTextInput,
FindTextResult,
FindTextRuntimeOperations,
SelectorObservationRuntimeOperationFacts,
SelectorObservationRuntimeOperations,
SelectorObservationResult,
} from '../selector-observation-runtime.ts';
export type {
CaptureSnapshotInput,
LocalSnapshotInteractorResolver,
Expand Down
16 changes: 16 additions & 0 deletions packages/contracts/src/interactor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,22 @@ export type Interactor = {
point: Point,
options?: { appBundleId?: string; surface?: SessionSurface; signal?: AbortSignal },
): Promise<string | undefined>;
/**
* Native text-presence reading, when the backend has one that does not require a tree capture.
* A `true` answer is authoritative; anything else means "not proven here" and the caller
* consults the canonical tree (see `FindTextResult`).
*/
findText?(
text: string,
options?: { appBundleId?: string; signal?: AbortSignal },
): Promise<{
found: boolean;
}>;
/** Native one-sided observation for a simple selector; false defers to canonical capture. */
findSelector?(
selector: Readonly<{ key: ElementSelectorKey; value: string }>,
options?: { appBundleId?: string; signal?: AbortSignal },
): Promise<{ found: boolean }>;
gestureViewport?(): Promise<Rect>;
back(mode?: BackMode): Promise<void>;
home(): Promise<void>;
Expand Down
9 changes: 1 addition & 8 deletions packages/contracts/src/logs-runtime-cutover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
type CommandPlatformExecution,
} from './command-platform-execution.ts';
import { appLogRuntimePlanUses } from './logs-runtime-plan.ts';
import type { RuntimeUseDeclaration } from './platform-runtime.ts';
import { runtimeUseIdentity } from './platform-runtime-use.ts';

/** Joins the input-dependent logs plans to one exhaustive descriptor declaration. */
export function assertLogsRuntimeExecution(
Expand All @@ -18,13 +18,6 @@ export function assertLogsRuntimeExecution(
}
}

function runtimeUseIdentity(use: RuntimeUseDeclaration): string {
return JSON.stringify({
required: [...use.required].sort(),
preferred: [...use.preferred].sort(),
});
}

function invalidLogsExecution(): TypeError {
return new TypeError(
'Logs runtime execution must declare exactly the distinct uses selected by its seven plans',
Expand Down
10 changes: 8 additions & 2 deletions packages/contracts/src/network-runtime-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ test('normalizes defaults and retains the requested alias and projection', () =>
include: 'headers',
use: networkDumpUse,
});
assert.deepEqual(networkDumpUse, { required: ['networkDump'], preferred: [] });
assert.deepEqual(networkAdmissionUse, { required: [], preferred: ['networkDump'] });
assert.deepEqual(networkDumpUse, {
required: ['networkDump'],
preferred: [],
});
assert.deepEqual(networkAdmissionUse, {
required: [],
preferred: ['networkDump'],
});
});

test('rejects actions and projections outside the frozen cells', () => {
Expand Down
10 changes: 8 additions & 2 deletions packages/contracts/src/platform-runtime-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ test.each([
true,
'custom-actions-active-app',
'captureSnapshotWithCustomActions',
{ required: ['captureSnapshot', 'captureSnapshotWithCustomActions'], preferred: [] },
{
required: ['captureSnapshot', 'captureSnapshotWithCustomActions'],
preferred: [],
},
],
[
false,
false,
'without-active-app',
'captureSnapshotWithoutActiveApp',
{ required: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'], preferred: [] },
{
required: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'],
preferred: [],
},
],
[
true,
Expand Down
Loading
Loading