From e0e8563224b67cb182dfe4746ca1ca03e733b076 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Fri, 25 Sep 2026 11:30:37 -0700 Subject: [PATCH] Expose closed module types and define stable presentation occurrences --- src/execution-world/client-contracts.ts | 3 ++- src/execution-world/installation.ts | 4 ++++ src/execution-world/module-contracts.ts | 17 +++++++++++++++++ src/execution-world/plan.ts | 6 ++++++ test/modules.test.ts | 25 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/execution-world/module-contracts.ts diff --git a/src/execution-world/client-contracts.ts b/src/execution-world/client-contracts.ts index bbcbb67..fe974f3 100644 --- a/src/execution-world/client-contracts.ts +++ b/src/execution-world/client-contracts.ts @@ -7,6 +7,7 @@ import type { import { canonicalJson } from "../capability-model/evolution.js"; import { portableValueSchema, tsType } from "../bindings/portable.js"; import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js"; +import { moduleTypeContext } from "./module-contracts.js"; /** Public shape only: no slot inventory, implementation binding, or workspace identity. */ export interface PublicOperationContract { @@ -111,7 +112,7 @@ export function generateInterfaceClientBindings( const members = iface.members.map((member) => { if (member.kind === "module" && member.subject?.kind === "interface") { const subject = member.subject.interfaceRevisionId; - return `${JSON.stringify(member.displayName)}: defineModuleMember<${JSON.stringify(subject)}, ${moduleProps?.(iface.revisionId, member.id) ?? "{}"}>(${JSON.stringify({ interface: iface.revisionId, member: member.id, subject, contract: member.contract })})`; + return `${JSON.stringify(member.displayName)}: defineModuleMember<${JSON.stringify(subject)}, ${moduleProps?.(iface.revisionId, member.id) ?? "{}"}>(${JSON.stringify({ interface: iface.revisionId, member: member.id, subject, contract: member.contract, ...moduleTypeContext(iface) })})`; } const operations = member.operations .filter((op) => op.mode === "call") diff --git a/src/execution-world/installation.ts b/src/execution-world/installation.ts index 5d41ec4..34d9f5e 100644 --- a/src/execution-world/installation.ts +++ b/src/execution-world/installation.ts @@ -128,6 +128,8 @@ export interface InstalledComponent { readonly member: string; readonly subject: string; readonly contract: string; + readonly typeArguments?: readonly unknown[]; + readonly requiredInterfaces?: readonly string[]; }; readonly packageRevision: string; readonly id: string; @@ -215,6 +217,8 @@ function assembleExecutionInstallation( member: module.memberId, subject: component.subject, contract: module.contract, + ...(module.typeArguments ? { typeArguments: module.typeArguments } : {}), + ...(module.requiredInterfaces ? { requiredInterfaces: module.requiredInterfaces } : {}), }; })(), packageRevision: revision, diff --git a/src/execution-world/module-contracts.ts b/src/execution-world/module-contracts.ts new file mode 100644 index 0000000..2105713 --- /dev/null +++ b/src/execution-world/module-contracts.ts @@ -0,0 +1,17 @@ +import type { InterfaceRevision } from "../capability-model/types.js"; + +/** Public, checked type information for artifact consumers. It describes the + * nominal role, without exposing implementations or synthesizing any storage. */ +export function moduleTypeContext(iface: InterfaceRevision): { + typeArguments?: readonly unknown[]; + requiredInterfaces?: readonly string[]; +} { + return { + ...(iface.application?.arguments.length + ? { + typeArguments: structuredClone(iface.application.arguments), + } + : {}), + ...(iface.requiredInterfaces?.length ? { requiredInterfaces: [...iface.requiredInterfaces].sort() } : {}), + }; +} diff --git a/src/execution-world/plan.ts b/src/execution-world/plan.ts index e72aa42..ae543f3 100644 --- a/src/execution-world/plan.ts +++ b/src/execution-world/plan.ts @@ -16,6 +16,7 @@ import { resolveForwardedBinding } from "../capability-model/forwarding.js"; import { linkQueries, type LinkedQuery } from "../query/link.js"; import { type BindingSchema } from "../bindings/index.js"; import { unsupportedQueryOperator } from "./query-operators.js"; +import { moduleTypeContext } from "./module-contracts.js"; export class ExecutionPlanError extends Error { constructor( @@ -62,6 +63,8 @@ export interface PlanPackage { modules?: (NonNullable[number] & { contract: string; subject?: import("../capability-model/types.js").ObjectExpectation; + typeArguments?: readonly unknown[]; + requiredInterfaces?: readonly string[]; })[]; controllers?: NonNullable; components?: { id: string; displayName: string; subject: InterfaceRevisionId; runtime: "react" }[]; @@ -504,6 +507,9 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan { ...module, contract: contract.contract, ...(contract.subject ? { subject: contract.subject } : {}), + ...moduleTypeContext( + workspace.interfaceImports.find((iface) => iface.revisionId === module.interfaceRevisionId)!, + ), }; }), } diff --git a/test/modules.test.ts b/test/modules.test.ts index 3611120..12d0516 100644 --- a/test/modules.test.ts +++ b/test/modules.test.ts @@ -211,3 +211,28 @@ test("unrecognized module consumers fail before artifact execution, not during i /No artifact consumer.*example.device.firmware/, ); }); + +test("artifact consumers receive checked closed role arguments and prerequisites", async () => { + const { moduleTypeContext } = await import("../src/execution-world/module-contracts.js"); + const { generateInterfaceClientBindings } = await import("../src/execution-world/client-contracts.js"); + const result = compile(); + assert.ok(result.ok, JSON.stringify(result.diagnostics)); + const role = result.workspace.interfaceImports.find((iface) => iface.application)!; + const expected = [{ kind: "object", target: { kind: "interface", interfaceRevisionId: "interface:data@1" } }]; + assert.deepEqual(moduleTypeContext(role), { typeArguments: expected }); + const plan = compileExecutionPlan(result.workspace); + assert.deepEqual( + plan.definition.packages[0]!.modules!.find((module) => module.displayName === "Editor")!.typeArguments, + expected, + ); + assert.match(generateInterfaceClientBindings({ Editor: role }), /"typeArguments":\[/); + assert.deepEqual(moduleTypeContext(interfaces.get("Row")!), {}); + // Prerequisites are public nominal contracts, never private storage inventory. + assert.deepEqual( + moduleTypeContext({ + ...role, + requiredInterfaces: [capabilityId.interfaceRevision("z"), capabilityId.interfaceRevision("a")], + }).requiredInterfaces, + ["a", "z"], + ); +});