Make local graph execution optional across composed packages

This commit is contained in:
Timothy J. Aveni
2026-09-28 21:05:42 -07:00
parent 1e72713ae2
commit e9c15fe40f
87 changed files with 3703 additions and 3400 deletions
+1 -4
View File
@@ -32,10 +32,7 @@ export function publicOperationContract(
const execution = operation.execution ?? { kind: "remote" };
const member = iface.members.find((m) => m.operations.includes(operation))!;
const fieldPolicy =
member.kind === "value" &&
operation.scope !== "class" &&
execution.kind === "transaction" &&
execution.targets.includes("browser")
member.kind === "value" && operation.scope !== "class" && execution.kind === "transaction"
? operation.displayName === "edit"
? "crdt"
: member.nativeField && operation.displayName === "set"
+5 -1
View File
@@ -117,7 +117,11 @@ export class BrowserArtifactGateway {
const entries: BrowserArtifact[] = [];
for (const manifest of loaded.manifests)
for (const implementation of manifest.implementations) {
if (implementation.target !== "browser" || implementation.compatibility !== "quixos-js-module-v1") continue;
if (
implementation.target !== "target:org.quixos.execution.javascript@1" ||
implementation.compatibility !== "quixos-js-module-v1"
)
continue;
const prefix = `/__quixos/artifacts/${loaded.digest}/${implementation.contentDigest}/`;
for (const file of implementation.files) {
if (!/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(file.mediaType)) fail(file.path, "Invalid asset MIME type");
+1
View File
@@ -1,4 +1,5 @@
export * from "./plan.js";
export * from "./native-backends.js";
export * from "./artifacts.js";
export * from "./ports.js";
export * from "./files.js";
+3 -1
View File
@@ -81,7 +81,9 @@ export function compileInspectionTemplates(plan: ExecutionPlan) {
),
);
if (providers.some((op) => !op || op.unsupported || !["snapshot", "remote"].includes(op.effect))) continue;
const portable = providers.every((op) => op!.effect === "snapshot" && op!.targets.includes("browser"));
const portable = providers.every(
(op) => op!.effect === "snapshot" && op!.targets.includes("target:org.quixos.execution.javascript@1"),
);
if (lane === "local" && !portable) continue;
const key = "m" + index,
flag = "s" + index;
+4 -2
View File
@@ -250,7 +250,9 @@ function assembleExecutionInstallation(
})
: undefined;
const browserExports = exports.filter(
(entry) => entry.execution.kind !== "remote" && entry.execution.targets.includes("browser"),
(entry) =>
entry.execution.kind !== "remote" &&
entry.execution.targets.includes("target:org.quixos.execution.javascript@1"),
);
if (!browserExports.length) {
if (selection.browser) fail(revision, "Package has no browser exports to select");
@@ -259,7 +261,7 @@ function assembleExecutionInstallation(
}
const browser = selectArtifactImplementation(manifest, {
...selection.browser,
target: "browser",
target: "target:org.quixos.execution.javascript@1",
compatibility: "quixos-js-module-v1",
exports: browserExports.map((entry) => entry.id),
});
+21
View File
@@ -0,0 +1,21 @@
import type { Binding } from "../capability-model/types.js";
/** Nominal library identity. Backends implement primitives; target names carry no
* built-in semantics in the graph planner. The JS backend is installed by the
* current toolchain, alongside the authoritative server executor. */
export const javascriptTarget = "target:org.quixos.execution.javascript@1";
export interface NativeExecutionBackend {
readonly target: string;
readonly supports: (binding: Exclude<Binding, { kind: "interface" }>) => boolean;
}
export const standardNativeBackends: readonly NativeExecutionBackend[] = [
{ target: "server", supports: () => true },
{
target: javascriptTarget,
supports: (binding) =>
binding.kind === "state" ||
binding.kind === "edge" ||
binding.kind === "journal" ||
(binding.kind === "subscription" && binding.primitive === "status"),
},
];
+19 -14
View File
@@ -1,3 +1,4 @@
import { standardNativeBackends, type NativeExecutionBackend } from "./native-backends.js";
import type {
WorkspaceRevision,
ExecutionContract,
@@ -45,7 +46,7 @@ export interface PlannedOperation {
operationId: string;
scope: "object" | "class";
execution: ExecutionContract;
/** Installed implementation closure; the public execution contract above remains required. */
/** Targets available for this complete installed implementation/dependency closure. */
availableTargets: string[];
input: ValueType;
output: ValueType;
@@ -171,7 +172,10 @@ function checkValue(type: ValueType, path: string, allowChange = false): void {
}
/** Compile declarations into a closed dispatch/dependency graph before artifact selection. */
export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
export function compileExecutionPlan(
input: WorkspaceRevision,
backends: readonly NativeExecutionBackend[] = standardNativeBackends,
): ExecutionPlan {
const workspace = structuredClone(input);
const issues = validateWorkspaceRevision(workspace);
if (issues.length)
@@ -311,13 +315,6 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
path.join(" -> "),
"Execution effect is not allowed by the root contract",
);
for (const target of requested.targets)
if (!node.execution.targets.includes(target))
throw new ExecutionPlanError(
"MISSING_EXECUTION_TARGET",
path.join(" -> "),
`Dependency does not provide ${target}`,
);
for (const dependency of node.dependencies) {
if (dependency.kind === "operation")
walk(dependency.operation, [...path, dependency.port, dependency.operation]);
@@ -352,9 +349,10 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
const provider =
binding.kind === "package"
? packages.get(binding.packageRevisionId)!.exports.find((entry) => entry.id === binding.exportId)!.execution
: binding.kind === "lifecycle" || (binding.kind === "subscription" && binding.primitive !== "status")
? { kind: "transaction" as const, targets: ["server" as const] }
: { kind: node.execution.kind, targets: ["browser" as const, "server" as const] };
: {
kind: node.execution.kind,
targets: backends.filter((backend) => backend.supports(binding)).map((backend) => backend.target),
};
if (provider && provider.kind !== "remote") node.availableTargets = [...provider.targets].sort();
}
let targetsChanged = true;
@@ -382,6 +380,13 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
}
}
}
for (const node of operationMap.values())
if (node.execution.kind !== "remote" && !node.availableTargets.includes("server"))
throw new ExecutionPlanError(
"MISSING_SERVER_IMPLEMENTATION",
node.key,
"Every graph operation requires an authoritative implementation across its dependency closure",
);
const plannedQueries: PlannedQuery[] = queries.map((query) => ({
id: queryKey(query.packageRevisionId, query.checked.declaration.id),
query,
@@ -515,11 +520,11 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
source.admittedOperations.push(...query.operations);
for (const operation of query.operations) {
const node = operationMap.get(operation)!;
if (node.execution.kind !== "snapshot" || !node.execution.targets.includes("browser"))
if (node.execution.kind !== "snapshot" || !node.availableTargets.some((target) => target !== "server"))
throw new ExecutionPlanError(
"NONPORTABLE_QUERY_FIELD",
`${query.id}/${operation}`,
"Local query getter must provide browser snapshot execution",
"Local query getter needs an installed local snapshot implementation",
);
}
}
+3 -2
View File
@@ -15,6 +15,8 @@ export interface ArtifactReceipt {
generated: { target: string; path: string; digest: string; bytes: number }[];
}
const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
/** Nominal identities never become filesystem paths. */
export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`;
/** Deterministic expected receipt. A trusted build must also typecheck and bundle these exact sources.
* This is consistency evidence inside a trusted Nix build path, not remote build attestation. */
export function expectedArtifactReceipt(
@@ -56,8 +58,7 @@ export function expectedArtifactReceipt(
.sort();
const sources = new Map<string, Uint8Array>();
const generated = targets.map((target) => {
if (!/^[a-zA-Z0-9_-]+$/.test(target)) throw new Error("Unsupported generated target filename");
const path = `share/quixos/generated/${target}.ts`;
const path = generatedBindingPath(target);
const bytes = new TextEncoder().encode(
generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }),
);