Add local-first execution contracts to the capability language

This commit is contained in:
Timothy J. Aveni
2026-09-19 22:09:40 -07:00
parent 757531bb63
commit 03ac7f8557
24 changed files with 3872 additions and 2760 deletions
+139
View File
@@ -0,0 +1,139 @@
import type { ExecutionContract, InterfaceOperation, WorkspaceRevision } from "./types.js";
import type { CapabilityValidationIssue } from "./validation.js";
import { resolveForwardedBinding } from "./forwarding.js";
/** Validate declared effects and targets; this is not a JavaScript purity proof. */
export function validateExecutionContracts(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [];
const fail = (path: string, message: string) => issues.push({ code: "invalid-execution-contract", path, message });
const interfaces = new Map(workspace.interfaceImports.map((i) => [i.revisionId, i]));
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId, p]));
const operations = (id: InterfaceOperation["id"], revision: WorkspaceRevision["interfaceImports"][number]) =>
(revision.template?.members ?? revision.members).flatMap((m) => m.operations).find((o) => o.id === id);
const check = (contract: ExecutionContract | undefined, path: string) => {
if (!contract || contract.kind === "remote") return;
if (
!["snapshot", "transaction"].includes(contract.kind) ||
!Array.isArray(contract.targets) ||
!contract.targets.length ||
contract.targets.some((t) => typeof t !== "string" || !t.trim()) ||
new Set(contract.targets).size !== contract.targets.length
)
fail(path, "Portable execution requires a nonempty set of distinct target names");
};
const supports = (
requested: ExecutionContract | undefined,
provided: ExecutionContract | undefined,
path: string,
) => {
if (!requested || requested.kind === "remote") return;
if (!provided || provided.kind === "remote") {
fail(path, "Portable execution depends on a remote-only operation");
return;
}
if (requested.kind === "snapshot" && provided.kind !== "snapshot")
fail(path, "Snapshot execution cannot perform a transaction");
for (const target of requested.targets ?? [])
if (!provided.targets?.includes(target)) fail(path, `Dependency does not provide execution target ${target}`);
};
for (const revision of workspace.interfaceImports)
for (const member of revision.template?.members ?? revision.members) {
for (const operation of member.operations) {
check(operation.execution, `${revision.revisionId}/${operation.id}`);
if (operation.execution && operation.execution.kind !== "remote" && operation.mode !== "call")
fail(
`${revision.revisionId}/${operation.id}`,
"Watch lifecycle operations are remote; portable reads use snapshots",
);
}
if (member.kind !== "operation" && member.queryRead?.execution === "portable") {
const getter = member.operations.find((o) => o.displayName === (member.kind === "value" ? "get" : "resolve"));
if (getter?.execution?.kind !== "snapshot")
fail(`${revision.revisionId}/${member.id}`, "Queryable portable members require a snapshot getter");
}
}
for (const revision of workspace.packageImports) {
for (const entry of [...revision.exports, ...(revision.genericExports ?? [])]) {
const path = `${revision.revisionId}/${entry.id}`;
check(entry.execution, path);
for (const port of entry.dependencyPorts) {
const requirement = port.requirement;
const portPath = `${path}/${port.id}`;
if (requirement.kind === "interface") {
const contract = interfaces.get(
"interfaceRevisionId" in requirement
? requirement.interfaceRevisionId
: requirement.application.definitionId,
);
const selected = requirement.operations;
if (selected && (!selected.length || new Set(selected).size !== selected.length))
fail(portPath, "Selected interface operations must be nonempty and distinct");
for (const id of selected ?? [])
if (contract && !operations(id, contract)) fail(portPath, `Unknown selected operation ${id}`);
if (entry.execution && entry.execution.kind !== "remote") {
if (!selected) fail(portPath, "Portable interface ports must select their required operations");
for (const id of selected ?? [])
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath);
}
}
if (!entry.execution || entry.execution.kind === "remote") continue;
if (requirement.kind === "state" || requirement.kind === "edge") {
const read = requirement.kind === "state" ? "read" : "resolve";
for (const primitive of requirement.primitives) {
if (primitive.startsWith("watch"))
fail(portPath, "Portable operations cannot acquire watch lifecycle primitives");
else if (entry.execution.kind === "snapshot" && primitive !== read)
fail(portPath, "Snapshot operations cannot acquire write primitives");
}
} else if (requirement.kind === "query") {
const query = packages.get(requirement.packageRevisionId)?.queries?.find((q) => q.id === requirement.queryId);
if (query?.placement?.kind !== "local")
fail(portPath, "Portable operations require a local query dependency");
} else if (requirement.kind !== "interface")
fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`);
}
}
for (const query of revision.queries ?? [])
if (query.placement?.kind === "local") {
const sourceQueryId = query.placement.sourceQueryId;
const source = revision.queries?.find((q) => q.id === sourceQueryId);
if (source?.placement?.kind !== "replica")
fail(`${revision.revisionId}/${query.id}`, "Local queries must name a replica query in the same package");
}
}
for (const conformance of workspace.conformances) {
const revision = interfaces.get(conformance.interfaceRevisionId);
if (!revision) continue;
for (const entry of conformance.operationBindings) {
const operation = operations(entry.operationId, revision);
if (!operation?.execution || operation.execution.kind === "remote") continue;
const path = `${conformance.atomId}/${revision.revisionId}/${entry.operationId}`;
let binding;
try {
binding = resolveForwardedBinding(
workspace,
conformance.atomId,
revision.revisionId,
entry.operationId,
).binding;
} catch {
continue;
} // Ordinary binding validation reports unresolved references and cycles.
if (entry.binding.kind === "interface") {
const target = interfaces.get(entry.binding.interfaceRevisionId);
if (target) supports(operation.execution, operations(entry.binding.operationId, target)?.execution, path);
}
if (binding.kind === "package") {
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
supports(operation.execution, provider?.execution, path);
} else {
const read = binding.kind === "state" ? "read" : "resolve";
if (binding.primitive.startsWith("watch"))
fail(path, "Portable operations cannot bind watch lifecycle primitives");
else if (operation.execution.kind === "snapshot" && binding.primitive !== read)
fail(path, "Snapshot operations cannot bind write primitives");
}
}
}
return issues;
}
+12 -2
View File
@@ -34,11 +34,16 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
}
| { kind: "interface"; application: InterfaceApplicationExpression }
| {
kind: "interface";
application: InterfaceApplicationExpression;
operations?: import("./types.js").OperationId[];
}
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
};
export interface GenericPackageExport {
execution?: import("./types.js").ExecutionContract;
id: PackageExportId;
displayName: string;
parameters: TypeParameter[];
@@ -90,7 +95,11 @@ export const specializePackageExport = (
case "interface":
return {
...port,
requirement: { kind: "interface", interfaceRevisionId: substitution.application(requirement.application) },
requirement: {
kind: "interface",
interfaceRevisionId: substitution.application(requirement.application),
...(requirement.operations ? { operations: requirement.operations } : {}),
},
};
case "constructor": {
const target = substitution.object(requirement.target);
@@ -112,6 +121,7 @@ export const specializePackageExport = (
}
});
const common = {
...(definition.execution ? { execution: structuredClone(definition.execution) } : {}),
id: capabilityId.packageExport(`export-application:sha256:${digest}`),
displayName: `${definition.displayName}$${digest}`,
inputType: substitution.value(definition.inputType),
+8 -1
View File
@@ -104,7 +104,11 @@ export interface AtomDefinition {
export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe";
/** Omission has remote semantics; availability never strengthens an imported contract. */
export type ExecutionContract = { kind: "remote" } | { kind: "snapshot" | "transaction"; targets: string[] };
export interface InterfaceOperation<Type = ValueType> {
execution?: ExecutionContract;
id: OperationId;
displayName: string;
inputType: Type;
@@ -116,7 +120,7 @@ export interface InterfaceOperation<Type = ValueType> {
eventType?: Type;
}
export type QueryReadContract = { execution: "native" | "rpc-permitted" };
export type QueryReadContract = { execution: "native" | "rpc-permitted" | "portable" };
interface InterfaceMemberBase<Type = ValueType> {
id: MemberId;
@@ -239,6 +243,8 @@ export type DependencyPortRequirement =
| {
kind: "interface";
interfaceRevisionId: InterfaceRevisionId;
/** Exact public operation identities, not implementation export names. */
operations?: OperationId[];
}
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
@@ -249,6 +255,7 @@ export interface DependencyPort {
}
interface PackageExportBase {
execution?: ExecutionContract;
id: PackageExportId;
displayName: string;
inputType: ValueType;
+5 -2
View File
@@ -1,3 +1,4 @@
import { validateExecutionContracts } from "./execution.js";
import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js";
import type {
AtomConstructorBinding,
@@ -41,6 +42,7 @@ import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode =
| "invalid-execution-contract"
| "invalid-query-contract"
| "query-provider-reason-required"
| "invalid-semantic-major"
@@ -521,7 +523,7 @@ const collectIdentityIndexes = (
if (member.kind !== "operation" && member.queryRead) {
const contract = member.queryRead.execution;
const getter = member.kind === "value" ? "get" : "resolve";
if (contract !== "native" && contract !== "rpc-permitted")
if (contract !== "native" && contract !== "rpc-permitted" && contract !== "portable")
issue(issues, "invalid-query-contract", memberPath, "Unknown query execution contract");
if (member.operations.filter((operation) => operation.displayName === getter).length !== 1)
issue(issues, "invalid-query-contract", memberPath, `Queryable members require exactly one ${getter}`);
@@ -1590,7 +1592,7 @@ const validateConformances = (
bindingPath,
"This queryable contract requires a native read binding; a justification cannot weaken it",
);
else if (!native && !entry.queryReason?.trim())
else if (!native && member.queryRead.execution === "rpc-permitted" && !entry.queryReason?.trim())
issue(
issues,
"query-provider-reason-required",
@@ -2082,6 +2084,7 @@ export const validateWorkspaceRevision = (workspace: WorkspaceRevision): Capabil
validatePackages(workspace, issues, indexes);
validateConformances(workspace, issues, indexes);
validateConstructors(workspace, issues, indexes);
issues.push(...validateExecutionContracts(workspace));
return issues;
};