Expose closed module types and define stable presentation occurrences
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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)!,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user