Implement checked workspace integration contracts (WI-01)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import type {
|
||||
InterfaceRevision,
|
||||
InterfaceOperation,
|
||||
ExecutionContract,
|
||||
PackageRevision,
|
||||
} from "../capability-model/types.js";
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { portableValueSchema, tsType } from "../bindings/portable.js";
|
||||
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
|
||||
|
||||
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */
|
||||
export interface PublicOperationContract {
|
||||
interface: string;
|
||||
operation: string;
|
||||
scope: "object" | "class";
|
||||
execution: ExecutionContract;
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
}
|
||||
export function publicOperationContract(
|
||||
iface: InterfaceRevision,
|
||||
operation: InterfaceOperation,
|
||||
): PublicOperationContract {
|
||||
if (iface.template || operation.mode !== "call" || !iface.members.some((m) => m.operations.includes(operation)))
|
||||
throw new ExecutionPlanError(
|
||||
"UNSUPPORTED_CLIENT_CONTRACT",
|
||||
iface.revisionId,
|
||||
"Expected a closed interface call operation",
|
||||
);
|
||||
const execution = operation.execution ?? { kind: "remote" };
|
||||
return {
|
||||
interface: iface.revisionId,
|
||||
operation: operation.id,
|
||||
scope: operation.scope ?? "object",
|
||||
execution: execution.kind === "remote" ? execution : { ...execution, targets: [...execution.targets].sort() },
|
||||
input: portableValueSchema(operation.inputType),
|
||||
output: portableValueSchema(operation.outputType),
|
||||
};
|
||||
}
|
||||
/** Resolve a public contract to internal dispatch. This does not authorize the receiver. */
|
||||
export function linkInterfaceOperation(plan: ExecutionPlan, atom: string, contract: PublicOperationContract) {
|
||||
if (hexDigest(plan.definition) !== plan.digest)
|
||||
throw new ExecutionPlanError("INVALID_PLAN", atom, "Invalid execution plan digest");
|
||||
const iface = plan.definition.workspace.interfaceImports.find((i) => i.revisionId === contract.interface);
|
||||
const operation = iface?.members.flatMap((m) => m.operations).find((o) => o.id === contract.operation);
|
||||
if (!iface || !operation || canonicalJson(publicOperationContract(iface, operation)) !== canonicalJson(contract))
|
||||
throw new ExecutionPlanError(
|
||||
"CLIENT_CONTRACT_MISMATCH",
|
||||
`${contract.interface}/${contract.operation}`,
|
||||
"Installed interface differs from the imported operation contract",
|
||||
);
|
||||
return resolvePlannedOperation(plan, atom, contract.interface, contract.operation);
|
||||
}
|
||||
|
||||
export interface PublicQueryContract {
|
||||
package: string;
|
||||
query: string;
|
||||
definitionDigest: string;
|
||||
root: { kind: "object" | "workspace-interface"; interface: string };
|
||||
placement: "server" | "replica" | "local";
|
||||
sourceQuery?: string;
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
}
|
||||
export function publicQueryContract(pkg: PackageRevision, queryId: string): PublicQueryContract {
|
||||
const query = pkg.checkedQueries?.find((q) => q.declaration.id === queryId);
|
||||
if (!query)
|
||||
throw new ExecutionPlanError("UNCHECKED_QUERY", `${pkg.revisionId}/${queryId}`, "Expected a checked package query");
|
||||
const placement = query.declaration.placement;
|
||||
return {
|
||||
package: pkg.revisionId,
|
||||
query: queryId,
|
||||
definitionDigest: query.definitionDigest,
|
||||
root: query.declaration.root,
|
||||
placement: placement?.kind ?? "server",
|
||||
...(placement?.kind === "local" ? { sourceQuery: placement.sourceQueryId } : {}),
|
||||
input: portableValueSchema(query.variables),
|
||||
output: portableValueSchema(query.output),
|
||||
};
|
||||
}
|
||||
export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContract) {
|
||||
if (hexDigest(plan.definition) !== plan.digest)
|
||||
throw new ExecutionPlanError("INVALID_PLAN", contract.query, "Invalid execution plan digest");
|
||||
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === contract.package);
|
||||
if (!pkg || canonicalJson(publicQueryContract(pkg, contract.query)) !== canonicalJson(contract))
|
||||
throw new ExecutionPlanError(
|
||||
"CLIENT_QUERY_MISMATCH",
|
||||
`${contract.package}/${contract.query}`,
|
||||
"Installed query differs from the imported query contract",
|
||||
);
|
||||
const query = plan.definition.queries.find(
|
||||
(q) => q.query.packageRevisionId === contract.package && q.query.checked.declaration.id === contract.query,
|
||||
);
|
||||
if (!query) throw new ExecutionPlanError("UNINSTALLED_QUERY", contract.query, "No installed query binding");
|
||||
return query;
|
||||
}
|
||||
|
||||
/** Generate only the explicitly imported interfaces. No concrete atom inventory is needed. */
|
||||
export function generateInterfaceClientBindings(
|
||||
imports: Readonly<Record<string, InterfaceRevision>>,
|
||||
runtimeModule = "@quixos/camino-replica-core",
|
||||
): string {
|
||||
const entries = Object.entries(imports).map(([alias, iface]) => {
|
||||
if (iface.template)
|
||||
throw new ExecutionPlanError(
|
||||
"OPEN_CLIENT_CONTRACT",
|
||||
alias,
|
||||
"Specialize the interface before generating a client",
|
||||
);
|
||||
const members = iface.members.map((member) => {
|
||||
const operations = member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.map(
|
||||
(op) =>
|
||||
`${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}>(${JSON.stringify(publicOperationContract(iface, op))})`,
|
||||
);
|
||||
return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`;
|
||||
});
|
||||
return `${JSON.stringify(alias)}: {${members.join(",\n")}}`;
|
||||
});
|
||||
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
|
||||
}
|
||||
Reference in New Issue
Block a user