Separate graph execution identity from installation provenance
This commit is contained in:
@@ -25,7 +25,9 @@ const checked = await compiler.loadCheckedArtifactWorld(plan, roots, generator);
|
|||||||
const installation = await compiler.loadExecutionInstallation(plan, roots);
|
const installation = await compiler.loadExecutionInstallation(plan, roots);
|
||||||
const kernel = compiler.compileKernelProgram(plan),
|
const kernel = compiler.compileKernelProgram(plan),
|
||||||
queries = compiler.compileQueryProgram(plan);
|
queries = compiler.compileQueryProgram(plan);
|
||||||
|
const graph = compiler.graphExecutionRepresentation(plan, installation, kernel, queries);
|
||||||
for (const [file, value] of [
|
for (const [file, value] of [
|
||||||
|
["graph-execution.json", graph],
|
||||||
["execution-world.json", checked.world],
|
["execution-world.json", checked.world],
|
||||||
["installation.json", installation],
|
["installation.json", installation],
|
||||||
["kernel.json", kernel],
|
["kernel.json", kernel],
|
||||||
@@ -37,6 +39,7 @@ for (const [file, value] of [
|
|||||||
generator,
|
generator,
|
||||||
roots,
|
roots,
|
||||||
manifests: checked.manifests,
|
manifests: checked.manifests,
|
||||||
|
execution: compiler.hexDigest(graph),
|
||||||
programs: { kernel: compiler.hexDigest(kernel), queries: compiler.hexDigest(queries) },
|
programs: { kernel: compiler.hexDigest(kernel), queries: compiler.hexDigest(queries) },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { compileKernelProgram } from "../execution-world/kernel.js";
|
||||||
import { hexDigest, type ExecutionPlan } from "../execution-world/plan.js";
|
import { hexDigest, type ExecutionPlan } from "../execution-world/plan.js";
|
||||||
import { resultInterfaces } from "../execution-world/projections.js";
|
import { resultInterfaces } from "../execution-world/projections.js";
|
||||||
import { portableValueSchema, tsType, PortableBindingError } from "./portable.js";
|
import { portableValueSchema, tsType, PortableBindingError } from "./portable.js";
|
||||||
@@ -10,6 +11,7 @@ export function generatePortableClientBindings(
|
|||||||
options: { target: string; runtimeModule?: string },
|
options: { target: string; runtimeModule?: string },
|
||||||
): string {
|
): string {
|
||||||
if (hexDigest(plan.definition) !== plan.digest) throw new PortableBindingError("client", "Invalid plan digest");
|
if (hexDigest(plan.definition) !== plan.digest) throw new PortableBindingError("client", "Invalid plan digest");
|
||||||
|
const executionPlan = (compileKernelProgram(plan) as { plan: string }).plan;
|
||||||
const workspace = plan.definition.workspace,
|
const workspace = plan.definition.workspace,
|
||||||
atoms = new Map<string, string[]>(),
|
atoms = new Map<string, string[]>(),
|
||||||
unavailable: { key: string; reason: string }[] = [];
|
unavailable: { key: string; reason: string }[] = [];
|
||||||
@@ -39,7 +41,7 @@ export function generatePortableClientBindings(
|
|||||||
const roots = resultInterfaces(plan, operation.key);
|
const roots = resultInterfaces(plan, operation.key);
|
||||||
const descriptor = {
|
const descriptor = {
|
||||||
key: operation.key,
|
key: operation.key,
|
||||||
plan: plan.digest,
|
plan: executionPlan,
|
||||||
effect: operation.execution.kind,
|
effect: operation.execution.kind,
|
||||||
input: portableValueSchema(operation.input),
|
input: portableValueSchema(operation.input),
|
||||||
output: portableValueSchema(operation.output),
|
output: portableValueSchema(operation.output),
|
||||||
@@ -91,7 +93,7 @@ export function generatePortableClientBindings(
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
kind: "server-projection",
|
kind: "server-projection",
|
||||||
id: query.id,
|
id: query.id,
|
||||||
plan: plan.digest,
|
plan: executionPlan,
|
||||||
root: declaration.root.interface,
|
root: declaration.root.interface,
|
||||||
rootKind: declaration.root.kind,
|
rootKind: declaration.root.kind,
|
||||||
}) +
|
}) +
|
||||||
@@ -108,7 +110,7 @@ export function generatePortableClientBindings(
|
|||||||
if (declaration.placement.kind === "local") {
|
if (declaration.placement.kind === "local") {
|
||||||
const list = views.get(pkg.displayName) ?? [];
|
const list = views.get(pkg.displayName) ?? [];
|
||||||
list.push(
|
list.push(
|
||||||
`${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: plan.digest, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`,
|
`${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: executionPlan, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`,
|
||||||
);
|
);
|
||||||
views.set(pkg.displayName, list);
|
views.set(pkg.displayName, list);
|
||||||
addResult();
|
addResult();
|
||||||
@@ -116,11 +118,11 @@ export function generatePortableClientBindings(
|
|||||||
}
|
}
|
||||||
const list = queries.get(pkg.displayName) ?? [];
|
const list = queries.get(pkg.displayName) ?? [];
|
||||||
list.push(
|
list.push(
|
||||||
`${JSON.stringify(declaration.displayName)}: { id: ${JSON.stringify(query.id)}, plan: ${JSON.stringify(plan.digest)}, root: ${JSON.stringify(declaration.root.interface)}, rootKind: ${JSON.stringify(declaration.root.kind)} } as const satisfies ReplicaProjection<${JSON.stringify(declaration.root.interface)}, ${JSON.stringify(declaration.root.kind)}>`,
|
`${JSON.stringify(declaration.displayName)}: { id: ${JSON.stringify(query.id)}, plan: ${JSON.stringify(executionPlan)}, root: ${JSON.stringify(declaration.root.interface)}, rootKind: ${JSON.stringify(declaration.root.kind)} } as const satisfies ReplicaProjection<${JSON.stringify(declaration.root.interface)}, ${JSON.stringify(declaration.root.kind)}>`,
|
||||||
);
|
);
|
||||||
queries.set(pkg.displayName, list);
|
queries.set(pkg.displayName, list);
|
||||||
}
|
}
|
||||||
const groups = (entries: Map<string, string[]>) =>
|
const groups = (entries: Map<string, string[]>) =>
|
||||||
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n");
|
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n");
|
||||||
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, decodeArgument, workspaceId, type Json, type PreparedChange, type ManagedReference, type ReplicaProjection, type ServerProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(plan.digest)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const serverProjections = {${groups(serverProjections)}} as const;\nexport const results = {${groups(results)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
|
return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, decodeArgument, workspaceId, type Json, type PreparedChange, type ManagedReference, type ReplicaProjection, type ServerProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(executionPlan)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const serverProjections = {${groups(serverProjections)}} as const;\nexport const results = {${groups(results)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,9 +75,8 @@ export const specializePackageExport = (
|
|||||||
const digest = createHash("sha256")
|
const digest = createHash("sha256")
|
||||||
.update(
|
.update(
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
"quixos-package-specialization-v1",
|
"quixos-package-specialization-v2",
|
||||||
pkg.revisionId,
|
pkg.revisionId,
|
||||||
[pkg.source.repository, pkg.source.commit],
|
|
||||||
definition.id,
|
definition.id,
|
||||||
arguments_.map(canonicalTypeArgument),
|
arguments_.map(canonicalTypeArgument),
|
||||||
environment.self ?? null,
|
environment.self ?? null,
|
||||||
|
|||||||
@@ -258,10 +258,8 @@ export interface AppliedInterfaceIdentity {
|
|||||||
|
|
||||||
export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => {
|
export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => {
|
||||||
const encoding = JSON.stringify([
|
const encoding = JSON.stringify([
|
||||||
"quixos-applied-interface-v1",
|
"quixos-applied-interface-v2",
|
||||||
application.definitionId,
|
application.definitionId,
|
||||||
application.source.repository,
|
|
||||||
application.source.commit.toLowerCase(),
|
|
||||||
application.arguments.map(canonicalTypeArgument),
|
application.arguments.map(canonicalTypeArgument),
|
||||||
application.self ?? null,
|
application.self ?? null,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { graphExecutionRepresentation } from "./graph-execution.js";
|
||||||
import { open, lstat } from "node:fs/promises";
|
import { open, lstat } from "node:fs/promises";
|
||||||
import { constants } from "node:fs";
|
import { constants } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -24,7 +25,7 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
) {
|
) {
|
||||||
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
|
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
|
||||||
throw Error("Candidate must be an immutable Nix output directory");
|
throw Error("Candidate must be an immutable Nix output directory");
|
||||||
const [candidate, plan, artifacts, source, entryRequest, kernel, queries] = await Promise.all([
|
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = await Promise.all([
|
||||||
readJson(root, "candidate.json"),
|
readJson(root, "candidate.json"),
|
||||||
readJson(root, "execution-plan.json"),
|
readJson(root, "execution-plan.json"),
|
||||||
readJson(root, "execution-artifacts.json"),
|
readJson(root, "execution-artifacts.json"),
|
||||||
@@ -32,6 +33,7 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
readJson(root, "entry.json"),
|
readJson(root, "entry.json"),
|
||||||
readJson(root, "kernel.json"),
|
readJson(root, "kernel.json"),
|
||||||
readJson(root, "queries.json"),
|
readJson(root, "queries.json"),
|
||||||
|
readJson(root, "graph-execution.json"),
|
||||||
]);
|
]);
|
||||||
const { executionContracts: _legacyContracts, ...workspace } = candidate;
|
const { executionContracts: _legacyContracts, ...workspace } = candidate;
|
||||||
if (
|
if (
|
||||||
@@ -81,12 +83,19 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
["kernel", kernel, "camino-kernel-program-v1"],
|
["kernel", kernel, "camino-kernel-program-v1"],
|
||||||
["queries", queries, "camino-query-program-v1"],
|
["queries", queries, "camino-query-program-v1"],
|
||||||
] as const)
|
] as const)
|
||||||
if (program?.codec !== codec || program.plan !== plan.digest || hexDigest(program) !== artifacts.programs?.[name])
|
if (program?.codec !== codec || program.plan !== kernel.plan || hexDigest(program) !== artifacts.programs?.[name])
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"CANDIDATE_PROGRAM_MISMATCH",
|
"CANDIDATE_PROGRAM_MISMATCH",
|
||||||
name,
|
name,
|
||||||
"Stored interpreter input must match the checked build and plan",
|
"Stored interpreter input must match the checked build and plan",
|
||||||
);
|
);
|
||||||
|
const expectedGraph = graphExecutionRepresentation(plan, installation, kernel, queries);
|
||||||
|
if (hexDigest(graph) !== hexDigest(expectedGraph) || artifacts.execution !== hexDigest(graph))
|
||||||
|
throw new ExecutionPlanError(
|
||||||
|
"CANDIDATE_EXECUTION_MISMATCH",
|
||||||
|
root,
|
||||||
|
"Stored graph execution differs from checked inputs",
|
||||||
|
);
|
||||||
const provenance = {
|
const provenance = {
|
||||||
workspaceRevision: workspace.id as string,
|
workspaceRevision: workspace.id as string,
|
||||||
source: { repository: source.repository as string, commit: source.commit as string },
|
source: { repository: source.repository as string, commit: source.commit as string },
|
||||||
@@ -110,8 +119,9 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
return {
|
return {
|
||||||
reference,
|
reference,
|
||||||
workspace: plan.definition.workspace.workspaceId,
|
workspace: plan.definition.workspace.workspaceId,
|
||||||
world: installation.world,
|
world: hexDigest(graph),
|
||||||
plan: plan.digest,
|
plan: kernel.plan as string,
|
||||||
|
graphExecution: graph,
|
||||||
provenance,
|
provenance,
|
||||||
entry,
|
entry,
|
||||||
kernel,
|
kernel,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||||
|
import type { ExecutionInstallation } from "./installation.js";
|
||||||
|
|
||||||
|
/** Execution inputs, not build evidence. Nominal revision IDs remain dispatch
|
||||||
|
* keys: replacing a contract with a structurally similar one is not continuity.
|
||||||
|
* Host placement/policy/credentials are compared separately by execution groups. */
|
||||||
|
export function graphExecutionRepresentation(
|
||||||
|
plan: ExecutionPlan,
|
||||||
|
installation: ExecutionInstallation,
|
||||||
|
kernel: unknown,
|
||||||
|
queries: unknown,
|
||||||
|
) {
|
||||||
|
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
||||||
|
throw new ExecutionPlanError("GRAPH_PLAN_MISMATCH", "graph", "Expected one checked installation");
|
||||||
|
if (
|
||||||
|
!kernel ||
|
||||||
|
typeof kernel !== "object" ||
|
||||||
|
Array.isArray(kernel) ||
|
||||||
|
!queries ||
|
||||||
|
typeof queries !== "object" ||
|
||||||
|
Array.isArray(queries)
|
||||||
|
)
|
||||||
|
throw new ExecutionPlanError("GRAPH_PROGRAM_MISMATCH", "graph", "Expected interpreter program records");
|
||||||
|
const input = kernel as Record<string, unknown>,
|
||||||
|
queryInput = queries as Record<string, unknown>;
|
||||||
|
const { plan: key, ...program } = input;
|
||||||
|
if (
|
||||||
|
input.codec !== "camino-kernel-program-v1" ||
|
||||||
|
queryInput.codec !== "camino-query-program-v1" ||
|
||||||
|
queryInput.plan !== key ||
|
||||||
|
key !== hexDigest({ versions: plan.definition.versions, program })
|
||||||
|
)
|
||||||
|
throw new ExecutionPlanError("GRAPH_PROGRAM_MISMATCH", "graph", "Interpreter inputs do not match execution key");
|
||||||
|
return {
|
||||||
|
codec: "camino-graph-execution-v1",
|
||||||
|
versions: plan.definition.versions,
|
||||||
|
kernel,
|
||||||
|
queries,
|
||||||
|
// Include every selected implementation, including acceptance/remote code
|
||||||
|
// and the browser dependency closure, not merely the public export names.
|
||||||
|
packages: installation.packages.map((pkg) => ({
|
||||||
|
revision: pkg.revision,
|
||||||
|
service: pkg.service ?? null,
|
||||||
|
module: pkg.module ?? null,
|
||||||
|
browser: pkg.browserImplementation ?? null,
|
||||||
|
})),
|
||||||
|
controllers: plan.definition.packages.map((pkg) => ({
|
||||||
|
revision: pkg.revision,
|
||||||
|
controllers: pkg.controllers ?? [],
|
||||||
|
})),
|
||||||
|
shared: {
|
||||||
|
core: installation.coreURL ?? null,
|
||||||
|
engine: installation.engineURL ?? null,
|
||||||
|
components: [
|
||||||
|
...new Map(installation.components.flatMap((component) => Object.entries(component.sharedModules))).entries(),
|
||||||
|
].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -19,3 +19,5 @@ export * from "./candidate.js";
|
|||||||
export { reviewSemanticMajors } from "../capability-model/evolution.js";
|
export { reviewSemanticMajors } from "../capability-model/evolution.js";
|
||||||
|
|
||||||
export * from "./public-reflection.js";
|
export * from "./public-reflection.js";
|
||||||
|
|
||||||
|
export * from "./graph-execution.js";
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export interface ExecutionInstallation {
|
|||||||
readonly root: string;
|
readonly root: string;
|
||||||
readonly service?: ArtifactImplementation;
|
readonly service?: ArtifactImplementation;
|
||||||
readonly module?: ArtifactImplementation;
|
readonly module?: ArtifactImplementation;
|
||||||
|
readonly browserImplementation?: ArtifactImplementation;
|
||||||
readonly browserURL?: string;
|
readonly browserURL?: string;
|
||||||
readonly browserRegistry?: string;
|
readonly browserRegistry?: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -270,6 +271,7 @@ function assembleExecutionInstallation(
|
|||||||
root,
|
root,
|
||||||
...(service ? { service } : {}),
|
...(service ? { service } : {}),
|
||||||
...(module ? { module } : {}),
|
...(module ? { module } : {}),
|
||||||
|
browserImplementation: browser,
|
||||||
browserURL: contentArtifactUrl(entry),
|
browserURL: contentArtifactUrl(entry),
|
||||||
browserRegistry: browser.registryExport,
|
browserRegistry: browser.registryExport,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const ownerBySlot = new Map<string, string>();
|
const ownerBySlot = new Map<string, string>();
|
||||||
const edges: unknown[] = [];
|
const edges: { id: string; owner: string; a: unknown; b: unknown }[] = [];
|
||||||
const owners = plan.definition.owners.map((owner) => {
|
const owners = plan.definition.owners.map((owner) => {
|
||||||
const slots: unknown[] = [];
|
const slots: unknown[] = [];
|
||||||
for (const storage of owner.storage as PersistentAttachment[]) {
|
for (const storage of owner.storage as PersistentAttachment[]) {
|
||||||
@@ -166,7 +166,19 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
kind: "creation",
|
kind: "creation",
|
||||||
atom: bound.atomId,
|
atom: bound.atomId,
|
||||||
schema: portableValueSchema(requirement.inputType),
|
schema: portableValueSchema(requirement.inputType),
|
||||||
recipe: hexDigest({ plan: plan.digest, operation: operation.key, port: dependency.portId }),
|
recipe: hexDigest({
|
||||||
|
codec: "camino-creation-recipe-v1",
|
||||||
|
versions: plan.definition.versions,
|
||||||
|
operation: operation.key,
|
||||||
|
export: entry.id,
|
||||||
|
port: dependency.portId,
|
||||||
|
requirement,
|
||||||
|
binding: bound,
|
||||||
|
owners: owners.filter((owner) => owner.atom === bound.atomId),
|
||||||
|
edges: edges.filter((edge) =>
|
||||||
|
owners.some((owner) => owner.atom === bound.atomId && owner.id === edge.owner),
|
||||||
|
),
|
||||||
|
}),
|
||||||
owners: bound.owners.map((seed) => {
|
owners: bound.owners.map((seed) => {
|
||||||
const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId);
|
const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId);
|
||||||
if (!owner)
|
if (!owner)
|
||||||
@@ -230,11 +242,12 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
output: schema(entry.outputType),
|
output: schema(entry.outputType),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
return {
|
const body = {
|
||||||
codec: "camino-kernel-program-v1",
|
codec: "camino-kernel-program-v1",
|
||||||
plan: plan.digest,
|
|
||||||
model: { workspace: workspace.workspaceId, atoms: workspace.atoms.map((atom) => atom.id), owners, edges },
|
model: { workspace: workspace.workspaceId, atoms: workspace.atoms.map((atom) => atom.id), owners, edges },
|
||||||
operations,
|
operations,
|
||||||
packages,
|
packages,
|
||||||
};
|
};
|
||||||
|
// The interpreter's plan key covers its real input, not source/build evidence.
|
||||||
|
return { ...body, plan: hexDigest({ versions: plan.definition.versions, program: body }) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { compileKernelProgram } from "./kernel.js";
|
||||||
import { compileInspectionTemplates } from "./inspection.js";
|
import { compileInspectionTemplates } from "./inspection.js";
|
||||||
import { portableValueSchema } from "../bindings/portable.js";
|
import { portableValueSchema } from "../bindings/portable.js";
|
||||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||||
@@ -26,7 +27,7 @@ export function compileQueryProgram(plan: ExecutionPlan): unknown {
|
|||||||
const interfaces = plan.definition.workspace.interfaceImports;
|
const interfaces = plan.definition.workspace.interfaceImports;
|
||||||
return {
|
return {
|
||||||
codec: "camino-query-program-v1",
|
codec: "camino-query-program-v1",
|
||||||
plan: plan.digest,
|
plan: (compileKernelProgram(plan) as { plan: string }).plan,
|
||||||
queries: [
|
queries: [
|
||||||
...compileInspectionTemplates(plan).queries,
|
...compileInspectionTemplates(plan).queries,
|
||||||
...plan.definition.queries.map(({ id, query, admittedOperations }) => {
|
...plan.definition.queries.map(({ id, query, admittedOperations }) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { specializePackageExport } from "../src/capability-model/generic-packages.js";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
@@ -297,3 +298,25 @@ test("typed presentation and factory consumers preserve distinct closed applicat
|
|||||||
await fs.rm(directory, { recursive: true, force: true });
|
await fs.rm(directory, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("specialized dispatch keys retain nominal arguments, not build provenance", () => {
|
||||||
|
const pkg = definition();
|
||||||
|
const generic = pkg.genericExports![0]!;
|
||||||
|
const environment = {
|
||||||
|
arguments: new Map(),
|
||||||
|
applyInterface: () => {
|
||||||
|
throw Error("unused");
|
||||||
|
},
|
||||||
|
implementsInterface: () => false,
|
||||||
|
};
|
||||||
|
const arguments_ = [{ kind: "value" as const, type: { kind: "scalar" as const, name: "string" as const } }];
|
||||||
|
const original = specializePackageExport(pkg, generic, arguments_, environment);
|
||||||
|
const rebuilt = structuredClone(pkg);
|
||||||
|
rebuilt.source.commit = "f".repeat(40);
|
||||||
|
assert.deepEqual(specializePackageExport(rebuilt, generic, arguments_, environment), original);
|
||||||
|
assert.notEqual(
|
||||||
|
specializePackageExport(rebuilt, generic, [{ kind: "value", type: { kind: "scalar", name: "bool" } }], environment)
|
||||||
|
.id,
|
||||||
|
original.id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ test("bounds require evidence and storable constraints inspect nested values", (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("closed applications canonicalize records without erasing nominal identity or provenance", () => {
|
test("closed applications preserve nominal identity while separating source provenance", () => {
|
||||||
const a: ClosedTypeArgument = {
|
const a: ClosedTypeArgument = {
|
||||||
kind: "value",
|
kind: "value",
|
||||||
type: { kind: "record", fields: { b: valueType.int64, a: valueType.string } },
|
type: { kind: "record", fields: { b: valueType.int64, a: valueType.string } },
|
||||||
@@ -183,6 +183,10 @@ test("closed applications canonicalize records without erasing nominal identity
|
|||||||
assert.equal(appliedInterfaceId(application), appliedInterfaceId({ ...application, arguments: [b] }));
|
assert.equal(appliedInterfaceId(application), appliedInterfaceId({ ...application, arguments: [b] }));
|
||||||
assert.notEqual(appliedInterfaceId(application), appliedInterfaceId({ ...application, self: note }));
|
assert.notEqual(appliedInterfaceId(application), appliedInterfaceId({ ...application, self: note }));
|
||||||
assert.notEqual(
|
assert.notEqual(
|
||||||
|
appliedInterfaceId(application),
|
||||||
|
appliedInterfaceId({ ...application, definitionId: capabilityId.interfaceRevision("other@1") }),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
appliedInterfaceId(application),
|
appliedInterfaceId(application),
|
||||||
appliedInterfaceId({ ...application, source: { ...application.source, commit: "b".repeat(40) } }),
|
appliedInterfaceId({ ...application, source: { ...application.source, commit: "b".repeat(40) } }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { loadReaderWorkspace } from "./reader-fixture.js";
|
||||||
|
import { buildArtifactFixture } from "./artifact-fixture.js";
|
||||||
|
import {
|
||||||
|
generatePortableClientBindings,
|
||||||
|
compileExecutionPlan,
|
||||||
|
compileKernelProgram,
|
||||||
|
compileQueryProgram,
|
||||||
|
graphExecutionRepresentation,
|
||||||
|
hexDigest,
|
||||||
|
type ExecutionPlan,
|
||||||
|
type ExecutionInstallation,
|
||||||
|
} from "../src/execution-world/index.js";
|
||||||
|
|
||||||
|
function prepare(plan: ExecutionPlan) {
|
||||||
|
const installation: ExecutionInstallation = {
|
||||||
|
codec: "camino-execution-installation-v1",
|
||||||
|
plan: plan.digest,
|
||||||
|
world: "0".repeat(64),
|
||||||
|
routes: {},
|
||||||
|
components: [],
|
||||||
|
packages: plan.definition.packages.map((pkg) => {
|
||||||
|
const { manifest } = buildArtifactFixture(plan, pkg.revision);
|
||||||
|
return {
|
||||||
|
revision: pkg.revision,
|
||||||
|
root: "/build/provenance",
|
||||||
|
service: manifest.implementations[0]!,
|
||||||
|
browserImplementation: manifest.implementations[2]!,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const kernel = compileKernelProgram(plan) as any,
|
||||||
|
queries = compileQueryProgram(plan) as any;
|
||||||
|
return {
|
||||||
|
installation,
|
||||||
|
kernel,
|
||||||
|
queries,
|
||||||
|
graph: () => graphExecutionRepresentation(plan, installation, kernel, queries),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("actual interpreter inputs and creation recipes exclude source provenance", async () => {
|
||||||
|
const workspace = await loadReaderWorkspace();
|
||||||
|
const originalPlan = compileExecutionPlan(workspace);
|
||||||
|
const before = prepare(originalPlan);
|
||||||
|
workspace.sourceRootCommit = "f".repeat(40);
|
||||||
|
for (const pkg of workspace.packageImports) pkg.source.commit = "e".repeat(40);
|
||||||
|
const nextPlan = compileExecutionPlan(workspace);
|
||||||
|
const after = prepare(nextPlan);
|
||||||
|
assert.equal(
|
||||||
|
generatePortableClientBindings(originalPlan, { target: "browser" }),
|
||||||
|
generatePortableClientBindings(nextPlan, { target: "browser" }),
|
||||||
|
);
|
||||||
|
assert.notEqual(before.installation.plan, after.installation.plan);
|
||||||
|
assert.deepEqual(before.kernel, after.kernel);
|
||||||
|
assert.deepEqual(before.queries, after.queries);
|
||||||
|
assert.deepEqual(before.graph(), after.graph());
|
||||||
|
assert.ok(JSON.stringify(before.kernel).includes('"recipe"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("graph identity covers executable closures, query semantics and real interpreter input", async () => {
|
||||||
|
const plan = compileExecutionPlan(await loadReaderWorkspace());
|
||||||
|
const fixture = prepare(plan),
|
||||||
|
baseline = hexDigest(fixture.graph());
|
||||||
|
fixture.installation.packages[0]!.service!.files[0]!.digest = "c".repeat(64);
|
||||||
|
assert.notEqual(hexDigest(fixture.graph()), baseline);
|
||||||
|
const query = prepare(plan);
|
||||||
|
query.queries.queries[0].budgets.rows++;
|
||||||
|
assert.notEqual(hexDigest(query.graph()), baseline);
|
||||||
|
query.kernel.operations[0].output = { kind: "scalar", name: "string" };
|
||||||
|
assert.throws(() => query.graph(), /GRAPH_PROGRAM_MISMATCH/);
|
||||||
|
const shared = prepare(plan);
|
||||||
|
(shared.installation as any).engineURL = "/changed-engine.mjs";
|
||||||
|
assert.notEqual(hexDigest(shared.graph()), baseline);
|
||||||
|
const future = prepare(plan);
|
||||||
|
future.kernel.codec = "future-kernel";
|
||||||
|
assert.throws(() => future.graph(), /GRAPH_PROGRAM_MISMATCH/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("initialization, storage, nominal contracts and bound ports affect executed kernel identity", async () => {
|
||||||
|
const workspace = await loadReaderWorkspace(),
|
||||||
|
plan = compileExecutionPlan(workspace);
|
||||||
|
const original = prepare(plan);
|
||||||
|
for (const change of [
|
||||||
|
(p: ExecutionPlan) => {
|
||||||
|
(p.definition.owners[0]!.initialization as any[]).push({
|
||||||
|
input: "extra",
|
||||||
|
binding: { kind: "state", slotId: "extra" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(p: ExecutionPlan) => {
|
||||||
|
p.definition.owners[0]!.id += ":different";
|
||||||
|
},
|
||||||
|
(p: ExecutionPlan) => {
|
||||||
|
p.definition.operations[0]!.operationId += ":different";
|
||||||
|
},
|
||||||
|
(p: ExecutionPlan) => {
|
||||||
|
p.definition.versions.crdt = "future-genesis" as any;
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const changed = structuredClone(plan);
|
||||||
|
change(changed);
|
||||||
|
changed.digest = hexDigest(changed.definition);
|
||||||
|
assert.notEqual((compileKernelProgram(changed) as any).plan, original.kernel.plan);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user