From 20e9f4b056b68f1461beb2e5286e0d9fc50f92b7 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Tue, 22 Sep 2026 21:57:35 -0700 Subject: [PATCH] Restore the workspace canvas and atomic Studio creation workflow --- src/bindings/portable.ts | 47 ++++++++++++++++++------------ src/capability-model/validation.ts | 31 ++++++++++++++++++-- src/execution-world/kernel.ts | 7 +++++ test/portable-bindings.test.ts | 17 +++++++++++ 4 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/bindings/portable.ts b/src/bindings/portable.ts index 53f2ecb..4ca3634 100644 --- a/src/bindings/portable.ts +++ b/src/bindings/portable.ts @@ -103,6 +103,7 @@ interface Method { input: ValueType; output: ValueType; keyed: boolean; + explicitInput?: boolean; } function methods( schema: BindingSchema, @@ -185,7 +186,7 @@ function methods( if ( (execution.kind === "remote" ? !remote : !execution.targets.includes(target)) || op.mode !== "call" || - op.scope === "class" + (op.scope === "class" && (remote || req.receiver === "input")) ) throw new PortableBindingError( `${path}.${member.displayName}.${op.displayName}`, @@ -194,24 +195,27 @@ function methods( if (execution.kind === "transaction" && !remote) writable(); 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 }, - req.receiver === "input" - ? { - kind: "record", - fields: { - receiver: { - kind: "object-ref", - expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId }, + return { + ...method( + `${member.displayName}.${op.displayName}`, + { kind: "interface", operationId: op.id }, + req.receiver === "input" + ? { + kind: "record", + fields: { + receiver: { + kind: "object-ref", + expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId }, + }, + input: op.inputType, }, - input: op.inputType, - }, - } - : op.inputType, - op.outputType, - execution.kind === "transaction", - ); + } + : op.inputType, + op.outputType, + execution.kind === "transaction", + ), + ...(op.scope === "class" ? { explicitInput: true } : {}), + }; }); } default: @@ -276,7 +280,7 @@ function generateBindings( throw new PortableBindingError(port.id, "Duplicate method name"); all.push(...selected); facets.push( - `readonly ${q(port.displayName)}: { ${selected.map((m) => `readonly ${q(m.name)}: (${[...(isUnit(m.input) ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? [remote ? "options: ServerMutationOptions" : "options: MutationKey"] : [])].join(", ")}) => Promise<${tsType(m.output)}>`).join("; ")} }`, + `readonly ${q(port.displayName)}: { ${selected.map((m) => `readonly ${q(m.name)}: (${[...(isUnit(m.input) && !m.explicitInput ? [] : [`input: ${tsType(m.input)}`]), ...(m.keyed ? [remote ? "options: ServerMutationOptions" : "options: MutationKey"] : [])].join(", ")}) => Promise<${tsType(m.output)}>`).join("; ")} }`, ); } const receiver = @@ -312,6 +316,10 @@ function generateBindings( }); } const groups = new Map(); + for (const template of pkg.genericExports ?? []) { + if (template.execution?.kind !== "remote" && template.execution?.targets.includes(options.target)) + groups.set(template.displayName, []); + } for (const entry of entries) { if (!entry.application) continue; const template = pkg.genericExports?.find((item) => item.id === entry.application!.exportId); @@ -319,6 +327,7 @@ function generateBindings( groups.set(template.displayName, [...(groups.get(template.displayName) ?? []), entry]); } const generic = [...groups].map(([name, applications]) => { + if (!applications.length) return `${q(name)}: (_handler: unknown) => ({})`; contexts.push( `readonly ${q(name)}: ${applications.map((entry) => `Contexts[${q(entry.displayName)}]`).join(" | ")}`, ); diff --git a/src/capability-model/validation.ts b/src/capability-model/validation.ts index bbf3b46..46008e6 100644 --- a/src/capability-model/validation.ts +++ b/src/capability-model/validation.ts @@ -1458,6 +1458,21 @@ const validateBoundDependencies = ( ); break; } + const selected = requirement.operations?.map( + (id) => params.indexes.interfaces.get(requirement.interfaceRevisionId)?.operations.get(id)?.operation, + ); + if ( + selected?.some((op) => op?.scope === "class") && + (interfaceBinding.via || interfaceBinding.receiver || selected.some((op) => op?.scope !== "class")) + ) { + issue( + issues, + "invalid-dependency-binding", + path, + "Static interface dependencies require a receiverless selection of class operations on the bound atom", + ); + break; + } // The reference supplies its own checked witness at invocation time. // This is not a requirement that the current receiver implements the interface. if (interfaceBinding.receiver === "input") break; @@ -1921,8 +1936,20 @@ const validateConformances = ( }); if ( operation.scope === "class" && - (binding.dependencies.some((entry) => !["constructor", "creation"].includes(entry.binding.kind)) || - packageExport.dependencyPorts.some((entry) => !["constructor", "creation"].includes(entry.requirement.kind))) + packageExport.dependencyPorts.some(({ requirement }) => { + if (["constructor", "creation", "reflection"].includes(requirement.kind)) return false; + if (requirement.kind !== "interface") return true; + if (requirement.receiver === "input") return false; + const contract = indexes.interfaces.get(requirement.interfaceRevisionId); + return ( + !requirement.operations?.length || + !requirement.operations.every((id) => + contract?.revision.members + .flatMap((m) => m.operations) + .some((op) => op.id === id && op.scope === "class"), + ) + ); + }) ) { issue( issues, diff --git a/src/execution-world/kernel.ts b/src/execution-world/kernel.ts index df82847..ca9db0e 100644 --- a/src/execution-world/kernel.ts +++ b/src/execution-world/kernel.ts @@ -156,6 +156,13 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { ...common, kind: "interface", interface: bound.interfaceRevisionId, + ...(!bound.receiver && + !bound.via && + (requirement.operations ?? iface.members.flatMap(m=>m.operations.map(o=>o.id))).every( + (id) => iface.members.flatMap((m) => m.operations).find((o) => o.id === id)?.scope === "class", + ) + ? { staticAtom: operation.atomId } + : {}), ...(bound.receiver ? { receiver: bound.receiver } : {}), operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)), }; diff --git a/test/portable-bindings.test.ts b/test/portable-bindings.test.ts index 9e11724..e7b7ed8 100644 --- a/test/portable-bindings.test.ts +++ b/test/portable-bindings.test.ts @@ -114,3 +114,20 @@ test("server projection descriptors are distinct from canonical inputs and have /@live is retired/, ); }); + +test("static interface ports keep unit arguments explicit for generic factory composition", async () => { + const schema = compileExecutionPlan(await loadReaderWorkspace()).definition.bindingSchema; + const entry = schema.packages.flatMap((p) => p.exports).find((e) => e.displayName === "addHighlight")!; + const port = entry.dependencyPorts.find((p) => p.requirement.kind === "interface")!; + if (port.requirement.kind !== "interface") throw Error("interface required"); + const requirement = port.requirement; + const iface = schema.interfaces.find((i) => i.revisionId === requirement.interfaceRevisionId)!; + const op = iface.members.flatMap((m) => m.operations).find((o) => o.id === requirement.operations![0])!; + op.scope = "class"; + op.inputType = { kind: "builtin", name: "unit" }; + port.requirement.operations = [op.id]; + delete port.requirement.receiver; + const generated = generatePortableTypeScriptBindings(schema, "package:reader-runtime@1", { target: "browser" }); + assert.match(generated, /"explicitInput": true/); + assert.match(generated, /input: null/); +});