From c482a99acab14a81d2a643e2dfc15ee2f35c9c7b Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Fri, 25 Sep 2026 12:11:01 -0700 Subject: [PATCH] Package ordinary PresentationMount contracts and typed React hosting --- src/capability-model/presentations.ts | 4 ++-- src/execution-world/client-contracts.ts | 15 ++++++++++++++- src/execution-world/inspection.ts | 4 ++-- src/execution-world/plan.ts | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/capability-model/presentations.ts b/src/capability-model/presentations.ts index aed9822..b454b31 100644 --- a/src/capability-model/presentations.ts +++ b/src/capability-model/presentations.ts @@ -37,8 +37,8 @@ export function validatePresentations(workspace: WorkspaceRevision): CapabilityV ?.modules?.find((entry) => entry.id === presentation.componentId); if (module) { const contract = moduleContract(workspace.interfaceImports, module); - if (contract?.contract !== "org.quixos.react.subject-only/1") - fail(`${path}.componentId`, "Standalone presentation requires a subject-only module contract"); + if (!contract || !["org.quixos.react.subject-only/1", "org.quixos.react.component/2"].includes(contract.contract)) + fail(`${path}.componentId`, "Standalone presentation requires a supported React presentation contract"); if (contract?.subject?.kind !== "interface" || contract.subject.interfaceRevisionId !== presentation.subject) fail(`${path}.subject`, "Component requires its exact declared subject interface"); continue; diff --git a/src/execution-world/client-contracts.ts b/src/execution-world/client-contracts.ts index fe974f3..d96161f 100644 --- a/src/execution-world/client-contracts.ts +++ b/src/execution-world/client-contracts.ts @@ -211,6 +211,19 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi const subjects = Object.fromEntries(components.map((component) => [component.displayName, find(component.subject)])); const props = (iface: string, member: string) => `ExtraModuleProps<${JSON.stringify(JSON.stringify([iface, member]))}>`; + // Preserve closed type arguments for the artifact consumer. Interpretation of + // a particular module contract belongs to that consumer, not this compiler. + const moduleArguments = (iface: InterfaceRevision) => `[${(iface.application?.arguments ?? []).map(argument => { + if (argument.kind === "value") return tsType(argument.type); + const target = argument.target; + const reference = target.kind === "atom" ? `ManagedReference<"atom", ${JSON.stringify(target.atomId)}>` : `ManagedReference<"interface", ${JSON.stringify(target.interfaceRevisionId)}>`; + const fields = target.kind === "interface" ? find(target.interfaceRevisionId).members.flatMap(member => { + if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return []; + const getter = member.operations.find(operation => operation.displayName === (member.kind === "value" ? "get" : "resolve")); + return getter ? [`readonly ${JSON.stringify(member.displayName)}: ${tsType(getter.outputType)}`] : []; + }) : []; + return `{readonly reference: ${reference}; readonly values: {${fields.join(";")}}}`; + }).join(",")}]`; const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react", props); const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react", props) .split("\n") @@ -248,7 +261,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi .map((component) => { const module = modules.find((module) => module.id === component.id)!; const key = JSON.stringify([module.interfaceRevisionId, module.memberId]); - return `${JSON.stringify(component.displayName)}: ComponentType & ExtraModuleProps<${JSON.stringify(key)}>>;`; + return `${JSON.stringify(component.displayName)}: ComponentType & ExtraModuleProps<${JSON.stringify(key)}>>;`; }) .join("\n") + "\n}\n" diff --git a/src/execution-world/inspection.ts b/src/execution-world/inspection.ts index 7667b2f..e75e84d 100644 --- a/src/execution-world/inspection.ts +++ b/src/execution-world/inspection.ts @@ -18,7 +18,7 @@ const meta = () => selection("_camino", { selection: [selection("ref")] }); export const inspectionPackage = "org.quixos.inspection/1"; export interface InspectionTemplate { interface: string; - members: { id: string; name: string; key: string; flag: string; lane: "local" | "server"; many: boolean }[]; + members: { id: string; name: string; key: string; flag: string; lane: "local" | "server"; many: boolean; relationship: boolean }[]; inputs?: PublicQueryContract; view?: PublicQueryContract; server?: PublicQueryContract; @@ -81,7 +81,7 @@ export function compileInspectionTemplates(plan: ExecutionPlan) { // Only native edges promise the relationship traversal used by the interpreter. if (member.kind === "relationship" && providers.some((op) => op!.binding.kind !== "edge")) continue; if (!complete && (lane === "local" || !portable)) - template.members.push({ id: member.id, name: member.displayName, key, flag, lane, many }); + template.members.push({ id: member.id, name: member.displayName, key, flag, lane, many, relationship: member.kind === "relationship" }); inputFields[flag] = scalar("bool"); const field = selection(member.displayName, { key, diff --git a/src/execution-world/plan.ts b/src/execution-world/plan.ts index ae543f3..0e155d3 100644 --- a/src/execution-world/plan.ts +++ b/src/execution-world/plan.ts @@ -82,7 +82,7 @@ function componentDeclarations(workspace: WorkspaceRevision, pkg: WorkspaceRevis ...(pkg.modules ?? []).flatMap((module) => { const contract = moduleContract(workspace.interfaceImports, module)!; if ( - !["org.quixos.react.component/1", "org.quixos.react.subject-only/1"].includes(contract.contract) || + !["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(contract.contract) || contract.subject?.kind !== "interface" ) return [];