From e1ad15c62015e3173208be3d34b8cd5750d979d2 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Mon, 21 Sep 2026 16:01:48 -0700 Subject: [PATCH] Implement Studio root and placement capabilities on the replica graph (WI-10) --- src/bindings/portable.ts | 36 +++++++++++++++++++++++-- src/capability-model/execution.ts | 12 ++++----- src/capability-model/validation.ts | 5 +++- src/execution-world/execution-groups.ts | 6 ++++- src/execution-world/kernel.ts | 15 +++++++++++ src/execution-world/ports.ts | 5 +++- src/execution-world/reflection.ts | 28 +++++++++++++++++++ test/portable-bindings.test.ts | 2 +- test/reference-dependencies.test.ts | 13 ++++----- 9 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 src/execution-world/reflection.ts diff --git a/src/bindings/portable.ts b/src/bindings/portable.ts index 5872fbe..9fb4b42 100644 --- a/src/bindings/portable.ts +++ b/src/bindings/portable.ts @@ -1,3 +1,4 @@ +import { portableReflectionType } from "../execution-world/reflection.js"; import type { BindingSchema } from "./index.js"; import type { ValueType, @@ -98,7 +99,7 @@ interface Method { name: string; portId: string; portName: string; - action: { kind: string; primitive?: string; operationId?: string }; + action: { kind: string; primitive?: string; operationId?: string; selector?: string }; input: ValueType; output: ValueType; keyed: boolean; @@ -126,6 +127,15 @@ function methods( throw new PortableBindingError(path, "Snapshot context cannot expose mutation ports"); }; switch (req.kind) { + case "reflection": + if (remote) + throw new PortableBindingError( + path, + "Use an ordinary portable snapshot capability to read installation metadata", + ); + return req.selectors.map((selector) => + method(selector, { kind: "reflection", selector }, unit, portableReflectionType(selector)), + ); case "creation": writable(); return [ @@ -301,5 +311,27 @@ function generateBindings( methods: all.map((m) => ({ ...m, input: portableValueSchema(m.input), output: portableValueSchema(m.output) })), }); } - return `// Generated ${remote ? "remote" : "portable"} bindings. Regenerate from the checked capability schema; do not edit.\nimport { define${mode}Registry, type ${mode}Export, type PreparedChange, type ManagedReference, type ExactReference, type ${remote ? "ServerMutationOptions" : "MutationKey"} } from ${q(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const target = ${q(options.target)};\nexport type Contexts = { ${contexts.join(";\n")} };\nexport type ${mode}Implementation = { ${implementations.join(";\n")} };\nexport const declarations = ${JSON.stringify(descriptors, null, 2)} as const satisfies readonly ${mode}Export[];\nexport function createRegistry(handlers: ${mode}Implementation) { const installed = { ...handlers }; return define${mode}Registry(declarations, { ${adapters.join(",\n")} }); }\n`; + const groups = new Map(); + for (const entry of entries) { + if (!entry.application) continue; + const template = pkg.genericExports?.find((item) => item.id === entry.application!.exportId); + if (!template) throw new PortableBindingError(entry.id, "Missing generic export template"); + groups.set(template.displayName, [...(groups.get(template.displayName) ?? []), entry]); + } + const generic = [...groups].map(([name, applications]) => { + contexts.push( + `readonly ${q(name)}: ${applications.map((entry) => `Contexts[${q(entry.displayName)}]`).join(" | ")}`, + ); + const type = applications + .map( + (entry) => + `((context: Contexts[${q(entry.displayName)}]) => ${tsType(entry.outputType)} | Promise<${tsType(entry.outputType)}>)`, + ) + .join(" & "); + const handlers = applications + .map((entry) => `${q(entry.displayName)}: (context: Contexts[${q(entry.displayName)}]) => handler(context)`) + .join(","); + return `${q(name)}: (handler: ${type}) => ({${handlers}})`; + }); + return `// Generated ${remote ? "remote" : "portable"} bindings. Regenerate from the checked capability schema; do not edit.\nimport { define${mode}Registry, type ${mode}Export, type PreparedChange, type ManagedReference, type ExactReference, type ${remote ? "ServerMutationOptions" : "MutationKey"} } from ${q(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const target = ${q(options.target)};\nexport type Contexts = { ${contexts.join(";\n")} };\nexport type ${mode}Implementation = { ${implementations.join(";\n")} };\nexport const declarations = ${JSON.stringify(descriptors, null, 2)} as const satisfies readonly ${mode}Export[];\nexport const generic = { ${generic.join(",\n")} };\nexport function createRegistry(handlers: ${mode}Implementation) { const installed = { ...handlers }; return define${mode}Registry(declarations, { ${adapters.join(",\n")} }); }\n`; } diff --git a/src/capability-model/execution.ts b/src/capability-model/execution.ts index 830cc0c..f0def04 100644 --- a/src/capability-model/execution.ts +++ b/src/capability-model/execution.ts @@ -67,15 +67,10 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi ); const selected = requirement.operations; if (requirement.receiver === "input") { - if ( - entry.execution?.kind !== "snapshot" || - entry.execution.targets.length !== 1 || - entry.execution.targets[0] !== "server" || - !selected?.length - ) + if (!entry.execution || entry.execution.kind === "remote" || !selected?.length) fail( portPath, - "Reference-selected reads currently require a server-only snapshot and an explicit selection; local acquisition of dynamic inputs is not implemented", + "Reference-selected reads require a portable snapshot or transaction and an explicit selection", ); for (const id of selected ?? []) { const operation = contract && operations(id, contract); @@ -108,6 +103,9 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi fail(portPath, "Portable operations require a local query dependency"); } else if (requirement.kind === "creation") { if (entry.execution.kind !== "transaction") fail(portPath, "Object creation requires transaction execution"); + } else if (requirement.kind === "reflection") { + if (requirement.selectors.some((selector) => selector !== "presentations")) + fail(portPath, "Only checked presentation metadata is portable"); } else if (requirement.kind !== "interface") fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`); } diff --git a/src/capability-model/validation.ts b/src/capability-model/validation.ts index 22682c9..99563e0 100644 --- a/src/capability-model/validation.ts +++ b/src/capability-model/validation.ts @@ -1065,7 +1065,10 @@ const validatePackages = ( !port.requirement.selectors.length || new Set(port.requirement.selectors).size !== port.requirement.selectors.length || port.requirement.selectors.some( - (kind) => !["active", "atoms", "interfaces", "conformances", "queries", "indexes"].includes(kind), + (kind) => + !["active", "atoms", "interfaces", "conformances", "queries", "indexes", "presentations"].includes( + kind, + ), ) ) issue(issues, "invalid-dependency-binding", portPath, "Reflection requires distinct supported selectors"); diff --git a/src/execution-world/execution-groups.ts b/src/execution-world/execution-groups.ts index 6edf079..2b07126 100644 --- a/src/execution-world/execution-groups.ts +++ b/src/execution-world/execution-groups.ts @@ -1,3 +1,4 @@ +import { portableReflectionValue } from "./reflection.js"; import type { BoundDependency, DependencyBinding, @@ -152,7 +153,10 @@ export function compileExecutionGroups( const target = bound.binding, port = entry.dependencyPorts.find((p) => p.id === bound.portId)!; if ("via" in target && target.via) storage(target.via.edgeTypeId); - if (target.kind === "state") storage(target.slotId); + if (target.kind === "reflection") { + for (const selector of target.selectors) + remember("reflection:" + selector, portableReflectionValue(workspace, selector)); + } else if (target.kind === "state") storage(target.slotId); else if (target.kind === "edge") storage(target.edgeTypeId); else if (target.kind === "query") query(target.packageRevisionId, target.queryId); else if (target.kind === "interface" && port.requirement.kind === "interface") { diff --git a/src/execution-world/kernel.ts b/src/execution-world/kernel.ts index 87cc5dc..fd338b7 100644 --- a/src/execution-world/kernel.ts +++ b/src/execution-world/kernel.ts @@ -1,3 +1,4 @@ +import { portableReflectionType, portableReflectionValue } from "./reflection.js"; import type { ValueType, InitializationField, @@ -119,6 +120,20 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { const requirement = entry.dependencyPorts.find((p) => p.id === dependency.portId)!.requirement; const bound = dependency.binding; const common = { id: dependency.portId, ...via(bound) }; + if (bound.kind === "reflection" && requirement.kind === "reflection") + return { + ...common, + kind: "reflection", + values: Object.fromEntries( + bound.selectors.map((selector) => [ + selector, + { + schema: portableValueSchema(portableReflectionType(selector)), + value: portableReflectionValue(workspace, selector), + }, + ]), + ), + }; if (bound.kind === "state" && requirement.kind === "state") return { ...common, diff --git a/src/execution-world/ports.ts b/src/execution-world/ports.ts index 7896193..d26e5a3 100644 --- a/src/execution-world/ports.ts +++ b/src/execution-world/ports.ts @@ -2,6 +2,7 @@ * operation/port schema by the coordinator, not trusted because this header parsed. */ export const transactionPortAbi = "quixos-transaction-ports-v1"; export type PortAction = + | { kind: "reflection"; selector: string } | { kind: "state"; primitive: "read" | "write" | "edit" } | { kind: "edge"; primitive: "resolve" | "disconnect" } | { kind: "edge"; primitive: "connect"; key: string } @@ -75,7 +76,9 @@ export function checkPortInvocation(value: unknown): PortInvocation { identifier(action.operationId) : action.kind === "creation" ? exact(["kind", "key"]) && identifier(action.key) - : action.kind === "query" && exact(["kind"]); + : action.kind === "reflection" + ? exact(["kind", "selector"]) && identifier(action.selector) + : action.kind === "query" && exact(["kind"]); if (!valid) throw new Error("Invalid port action"); return { ...fields, action: { ...action } } as unknown as PortInvocation; } diff --git a/src/execution-world/reflection.ts b/src/execution-world/reflection.ts new file mode 100644 index 0000000..d513ebe --- /dev/null +++ b/src/execution-world/reflection.ts @@ -0,0 +1,28 @@ +import type { ValueType, WorkspaceRevision } from "../capability-model/types.js"; +import { ExecutionPlanError } from "./plan.js"; +const string: ValueType = { kind: "scalar", name: "string" }; +/** Narrow immutable installation metadata. No graph enumeration or live reflection. */ +export function portableReflectionType(selector: string): ValueType { + if (selector !== "presentations") + throw new ExecutionPlanError( + "UNSUPPORTED_PORTABLE_REFLECTION", + selector, + "Only checked presentation metadata is portable", + ); + return { + kind: "list", + value: { kind: "record", fields: { id: string, subject: string, atoms: { kind: "list", value: string } } }, + }; +} +export function portableReflectionValue(workspace: WorkspaceRevision, selector: string) { + portableReflectionType(selector); + return (workspace.presentations ?? []) + .map((p) => ({ + id: p.id, + subject: p.subject, + atoms: [ + ...new Set(workspace.conformances.filter((c) => c.interfaceRevisionId === p.subject).map((c) => c.atomId)), + ].sort(), + })) + .sort((a, b) => a.id.localeCompare(b.id)); +} diff --git a/test/portable-bindings.test.ts b/test/portable-bindings.test.ts index 181967f..1f8b458 100644 --- a/test/portable-bindings.test.ts +++ b/test/portable-bindings.test.ts @@ -50,7 +50,7 @@ test("generation rejects remote dependencies, wrong effects and unsupported shap assert.throws(generate, /not an object call available in portable target browser/); getter.execution = { kind: "snapshot", targets: ["browser", "server"] }; entry.dependencyPorts[0]!.requirement = { kind: "reflection", selectors: ["active"] }; - assert.throws(generate, /Unsupported reflection port/); + assert.throws(generate, /Only checked presentation metadata is portable/); }); test("closed generic portable handlers have distinct concrete codecs and no universal casts", async () => { diff --git a/test/reference-dependencies.test.ts b/test/reference-dependencies.test.ts index cb050be..d80b2b0 100644 --- a/test/reference-dependencies.test.ts +++ b/test/reference-dependencies.test.ts @@ -23,8 +23,8 @@ const workspace = `workspace Test id "workspace:test" revision "workspace:test@1 bind peek.call to package Runtime.peek with { peers to interface Data from input; }; } }`; -function compile(packageSource = pkg, workspaceSource = workspace) { - const contract = compileCapabilityResourceSource(iface, { source }); +function compile(packageSource = pkg, workspaceSource = workspace, interfaceSource = iface) { + const contract = compileCapabilityResourceSource(interfaceSource, { source }); assert.ok(contract.ok && contract.resource.kind === "interface", JSON.stringify(contract.diagnostics)); const interfaces = new Map([["Data", contract.resource.revision]]); const runtime = compileCapabilityResourceSource(packageSource, { source, environment: { interfaces } }); @@ -82,8 +82,9 @@ test("closed generic specialization preserves reference-selected interface ports ); }); -test("dynamic reference reads do not promise an unimplemented client input closure", () => { - const result = compile(pkg.replace("targets [server]", "targets [browser, server]")); - assert.equal(result.ok, false); - assert.ok(result.diagnostics.some((diagnostic) => diagnostic.message.includes("local acquisition"))); +test("portable input-selected reads require the dependency's declared target support", () => { + const portable = pkg.replaceAll("targets [server]", "targets [browser, server]"); + assert.equal(compile(portable).ok, false); + const result = compile(portable, workspace, iface.replaceAll("targets [server]", "targets [browser, server]")); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); });