Make local graph execution optional across composed packages
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from "./plan.js";
|
||||
export * from "./native-backends.js";
|
||||
export * from "./artifacts.js";
|
||||
export * from "./ports.js";
|
||||
export * from "./files.js";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user