From e5544dde21fca8b2c224a495c1b46b1611a1851e Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Mon, 21 Sep 2026 18:31:43 -0700 Subject: [PATCH] Prove cross-package application composition through public capabilities (WI-15) --- src/bindings/portable.ts | 4 +- src/capability-model/execution.ts | 9 ++-- test/emit-controllers.ts | 18 +++++++- test/fixtures/controller-device/Viewer.qx | 7 ++++ .../fixtures/controller-device/viewer.graphql | 1 + test/reference-dependencies.test.ts | 42 ++++++++++++++++++- 6 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/controller-device/Viewer.qx create mode 100644 test/fixtures/controller-device/viewer.graphql diff --git a/src/bindings/portable.ts b/src/bindings/portable.ts index 9fb4b42..53f2ecb 100644 --- a/src/bindings/portable.ts +++ b/src/bindings/portable.ts @@ -192,8 +192,8 @@ function methods( `Selected operation is not an object call available in portable target ${target}; select a portable subset or move this handler to remote execution`, ); if (execution.kind === "transaction" && !remote) writable(); - if (req.receiver === "input" && (remote || execution.kind !== "snapshot")) - throw new PortableBindingError(path, "Reference-selected dependencies require portable snapshot calls"); + if (req.receiver === "input" && remote) + throw new PortableBindingError(path, "Reference-selected dependencies require portable execution"); return method( `${member.displayName}.${op.displayName}`, { kind: "interface", operationId: op.id }, diff --git a/src/capability-model/execution.ts b/src/capability-model/execution.ts index f0def04..0af462e 100644 --- a/src/capability-model/execution.ts +++ b/src/capability-model/execution.ts @@ -68,14 +68,11 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi const selected = requirement.operations; if (requirement.receiver === "input") { if (!entry.execution || entry.execution.kind === "remote" || !selected?.length) - fail( - portPath, - "Reference-selected reads require a portable snapshot or transaction and an explicit selection", - ); + fail(portPath, "Reference-selected calls require portable execution and an explicit selection"); for (const id of selected ?? []) { const operation = contract && operations(id, contract); - if (operation?.execution?.kind !== "snapshot" || operation.scope === "class") - fail(portPath, "Reference-selected dependencies currently support object snapshot operations only"); + if (!operation?.execution || operation.execution.kind === "remote" || operation.scope === "class") + fail(portPath, "Reference-selected dependencies require portable object operations"); } } if (selected && (!selected.length || new Set(selected).size !== selected.length)) diff --git a/test/emit-controllers.ts b/test/emit-controllers.ts index 4f79820..1a8a0a7 100644 --- a/test/emit-controllers.ts +++ b/test/emit-controllers.ts @@ -46,11 +46,25 @@ pkg.controllers = parseControllerCatalog( [iface, controllerInterface], pkg.checkedQueries, ); +const viewer = compileCapabilityResourceSource(await read("Viewer.qx"), { + source: { ...source, repository: "https://example.test/device-viewer.git" }, + environment, +}); +assert.ok(viewer.ok && viewer.resource.kind === "package", JSON.stringify(viewer.diagnostics)); +const viewerPackage = viewer.resource.revision; +viewerPackage.checkedQueries = await Promise.all( + viewerPackage.queries!.map((query) => compileQuery(query, [iface], read)), +); for (const variant of ["controller", "controller-next"]) { - const extra = variant.endsWith("next") ? 'atom Dashboard id "dashboard";' : ""; + const extra = variant.endsWith("next") + ? 'atom Dashboard id "dashboard"; import package DeviceViewer; presentation DeviceStatus id "device:status" for Device using DeviceViewer.Status;' + : ""; const result = compileCapabilitySource((await read("workspace.qx")).replace(/}\s*$/, extra + "\n}"), "workspace.qx", { ...environment, - packages: new Map([["Driver", pkg]]), + packages: new Map([ + ["Driver", pkg], + ...(variant.endsWith("next") ? [["DeviceViewer", viewerPackage] as const] : []), + ]), }); assert.ok(result.ok, JSON.stringify(result.diagnostics)); const plan = compileExecutionPlan(result.workspace), diff --git a/test/fixtures/controller-device/Viewer.qx b/test/fixtures/controller-device/Viewer.qx new file mode 100644 index 0000000..71714eb --- /dev/null +++ b/test/fixtures/controller-device/Viewer.qx @@ -0,0 +1,7 @@ +import interface Device; +external atom Display id "device:display"; +package DeviceViewer id "device-viewer" revision "device-viewer@1" { + component Status id "component:device:status" subject Device runtime react; + query Inputs id "device-viewer:inputs" root Device document "viewer.graphql" operation "Status" {replica;} + query View id "device-viewer:view" root Device document "viewer.graphql" operation "Status" {local from Inputs;watch;} +} diff --git a/test/fixtures/controller-device/viewer.graphql b/test/fixtures/controller-device/viewer.graphql new file mode 100644 index 0000000..8511345 --- /dev/null +++ b/test/fixtures/controller-device/viewer.graphql @@ -0,0 +1 @@ +query Status {root {enabled requested {text version} observed {text version}}} diff --git a/test/reference-dependencies.test.ts b/test/reference-dependencies.test.ts index d80b2b0..e8f3531 100644 --- a/test/reference-dependencies.test.ts +++ b/test/reference-dependencies.test.ts @@ -56,7 +56,7 @@ test("reference-selected dependencies round trip and do not require a recursive assert.ok(generated.includes('ManagedReference<"interface", "data@1">')); assert.ok(generated.includes('"receiver"')); }); -test("both sides explicitly grant reference selection and only object snapshots are permitted", () => { +test("both sides explicitly grant reference selection and remote execution is excluded", () => { assert.equal(compile(pkg, workspace.replace("Data from input", "Data")).ok, false); assert.equal(compile(pkg.replace(" from input", ""), workspace).ok, false); assert.equal(compile(pkg.replace("execution snapshot targets [server]", "execution remote"), workspace).ok, false); @@ -88,3 +88,43 @@ test("portable input-selected reads require the dependency's declared target sup const result = compile(portable, workspace, iface.replaceAll("targets [server]", "targets [browser, server]")); assert.ok(result.ok, JSON.stringify(result.diagnostics)); }); + +test("input-selected transactions compose only within a compatible transaction contract", () => { + const txInterface = iface + .replace('get id "content:get" execution snapshot', 'get id "content:get" execution transaction') + .replace('call id "peek:call" execution snapshot', 'call id "peek:call" execution transaction'); + const txPackage = pkg.replace("execution snapshot targets [server]", "execution transaction targets [server]"); + // Use an operation for the selected write; value getters themselves must remain snapshots. + const contract = txInterface.replace( + 'value content id "content" : string { get id "content:get" execution transaction targets [server]; }', + 'operation update id "content" : unit -> string { call id "content:get" execution transaction targets [server]; }', + ); + const implementation = txPackage.replace("using [content.get]", "using [update.call]"); + const model = workspace.replace( + "bind content.get to state Content.read;", + "bind update.call to package Runtime.update with { data to state Content; };", + ); + const sourcePackage = implementation.replace( + /\n}$/, + '\n operation update id "runtime:update" : unit -> string mode call execution transaction targets [server] receiver atom Item requires {state data id "data" : string [read,write];};\n}', + ); + const result = compile(sourcePackage, model, contract); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); + const plan = compileExecutionPlan(result.workspace); + const generated = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", { + target: "server", + }); + assert.ok(generated.includes('"receiver"')); + assert.ok(generated.includes('"update.call"')); + assert.equal( + compile( + sourcePackage.replace( + 'operation peek id "runtime:peek" : interface-ref -> string mode call execution transaction', + 'operation peek id "runtime:peek" : interface-ref -> string mode call execution snapshot', + ), + model, + contract, + ).ok, + false, + ); +});