Expose closed module types and define stable presentation occurrences

This commit is contained in:
Timothy J. Aveni
2026-09-25 11:37:27 -07:00
parent a83239892c
commit e0e8563224
5 changed files with 54 additions and 1 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ import type {
import { canonicalJson } from "../capability-model/evolution.js"; import { canonicalJson } from "../capability-model/evolution.js";
import { portableValueSchema, tsType } from "../bindings/portable.js"; import { portableValueSchema, tsType } from "../bindings/portable.js";
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.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. */ /** Public shape only: no slot inventory, implementation binding, or workspace identity. */
export interface PublicOperationContract { export interface PublicOperationContract {
@@ -111,7 +112,7 @@ export function generateInterfaceClientBindings(
const members = iface.members.map((member) => { const members = iface.members.map((member) => {
if (member.kind === "module" && member.subject?.kind === "interface") { if (member.kind === "module" && member.subject?.kind === "interface") {
const subject = member.subject.interfaceRevisionId; 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 const operations = member.operations
.filter((op) => op.mode === "call") .filter((op) => op.mode === "call")
+4
View File
@@ -128,6 +128,8 @@ export interface InstalledComponent {
readonly member: string; readonly member: string;
readonly subject: string; readonly subject: string;
readonly contract: string; readonly contract: string;
readonly typeArguments?: readonly unknown[];
readonly requiredInterfaces?: readonly string[];
}; };
readonly packageRevision: string; readonly packageRevision: string;
readonly id: string; readonly id: string;
@@ -215,6 +217,8 @@ function assembleExecutionInstallation(
member: module.memberId, member: module.memberId,
subject: component.subject, subject: component.subject,
contract: module.contract, contract: module.contract,
...(module.typeArguments ? { typeArguments: module.typeArguments } : {}),
...(module.requiredInterfaces ? { requiredInterfaces: module.requiredInterfaces } : {}),
}; };
})(), })(),
packageRevision: revision, packageRevision: revision,
+17
View File
@@ -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() } : {}),
};
}
+6
View File
@@ -16,6 +16,7 @@ import { resolveForwardedBinding } from "../capability-model/forwarding.js";
import { linkQueries, type LinkedQuery } from "../query/link.js"; import { linkQueries, type LinkedQuery } from "../query/link.js";
import { type BindingSchema } from "../bindings/index.js"; import { type BindingSchema } from "../bindings/index.js";
import { unsupportedQueryOperator } from "./query-operators.js"; import { unsupportedQueryOperator } from "./query-operators.js";
import { moduleTypeContext } from "./module-contracts.js";
export class ExecutionPlanError extends Error { export class ExecutionPlanError extends Error {
constructor( constructor(
@@ -62,6 +63,8 @@ export interface PlanPackage {
modules?: (NonNullable<WorkspaceRevision["packageImports"][number]["modules"]>[number] & { modules?: (NonNullable<WorkspaceRevision["packageImports"][number]["modules"]>[number] & {
contract: string; contract: string;
subject?: import("../capability-model/types.js").ObjectExpectation; subject?: import("../capability-model/types.js").ObjectExpectation;
typeArguments?: readonly unknown[];
requiredInterfaces?: readonly string[];
})[]; })[];
controllers?: NonNullable<WorkspaceRevision["packageImports"][number]["controllers"]>; controllers?: NonNullable<WorkspaceRevision["packageImports"][number]["controllers"]>;
components?: { id: string; displayName: string; subject: InterfaceRevisionId; runtime: "react" }[]; components?: { id: string; displayName: string; subject: InterfaceRevisionId; runtime: "react" }[];
@@ -504,6 +507,9 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
...module, ...module,
contract: contract.contract, contract: contract.contract,
...(contract.subject ? { subject: contract.subject } : {}), ...(contract.subject ? { subject: contract.subject } : {}),
...moduleTypeContext(
workspace.interfaceImports.find((iface) => iface.revisionId === module.interfaceRevisionId)!,
),
}; };
}), }),
} }
+25
View File
@@ -211,3 +211,28 @@ test("unrecognized module consumers fail before artifact execution, not during i
/No artifact consumer.*example.device.firmware/, /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"],
);
});