Build Camino packages from explicit contract inputs
Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies. Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior. Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
This commit is contained in:
@@ -20,15 +20,19 @@ export interface ArtifactImplementation {
|
||||
files: ArtifactFile[];
|
||||
contentDigest: string;
|
||||
}
|
||||
export interface PackageArtifacts {
|
||||
export interface ArtifactGroup {
|
||||
schemaVersion: 1;
|
||||
packageRevisionId: string;
|
||||
bindingDigest: string;
|
||||
sourceDigest: string;
|
||||
versions: ExecutionPlan["definition"]["versions"];
|
||||
implementations: ArtifactImplementation[];
|
||||
components?: ComponentArtifact[];
|
||||
}
|
||||
/** Installation evidence binds a reusable group to the selected exact source.
|
||||
* Compilation receipts never claim that a cache hit was built at a new pin. */
|
||||
export interface PackageArtifacts extends ArtifactGroup {
|
||||
sourceDigest: string;
|
||||
}
|
||||
export const componentSharedModules = [
|
||||
"react",
|
||||
"react-dom",
|
||||
@@ -199,11 +203,11 @@ function parseComponent(value: unknown, index: number): ComponentArtifact {
|
||||
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
|
||||
return component;
|
||||
}
|
||||
export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
export function parseArtifactGroup(value: unknown): ArtifactGroup {
|
||||
const record = object(value, "manifest");
|
||||
keys(
|
||||
record,
|
||||
["schemaVersion", "packageRevisionId", "bindingDigest", "sourceDigest", "versions", "implementations"],
|
||||
["schemaVersion", "packageRevisionId", "bindingDigest", "versions", "implementations"],
|
||||
["components"],
|
||||
"manifest",
|
||||
);
|
||||
@@ -266,13 +270,19 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
|
||||
bindingDigest: digest(record.bindingDigest, "bindingDigest"),
|
||||
sourceDigest: digest(record.sourceDigest, "sourceDigest"),
|
||||
versions: versions as unknown as PackageArtifacts["versions"],
|
||||
implementations,
|
||||
...(components ? { components } : {}),
|
||||
};
|
||||
}
|
||||
function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void {
|
||||
export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
const { sourceDigest, ...group } = object(value, "manifest");
|
||||
return { ...parseArtifactGroup(group), sourceDigest: digest(sourceDigest, "sourceDigest") };
|
||||
}
|
||||
export function checkPackageCoverage(
|
||||
expected: Pick<PlanPackage, "revision" | "modules" | "components" | "exports" | "bindingDigest" | "sourceDigest">,
|
||||
manifest: PackageArtifacts,
|
||||
): void {
|
||||
for (const module of expected.modules ?? [])
|
||||
if (!expected.components?.some((component) => component.id === module.id))
|
||||
fail(module.id, `No artifact consumer for module contract ${module.contract} and its subject`);
|
||||
@@ -315,6 +325,21 @@ function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void
|
||||
}
|
||||
}
|
||||
export type ArtifactReader = (packageRevisionId: string, path: string, maxBytes: number) => Promise<Uint8Array>;
|
||||
/** Shared by standalone package checks and complete world installation. */
|
||||
export async function checkArtifactFiles(manifest: ArtifactGroup, read: ArtifactReader): Promise<void> {
|
||||
const files = new Map<string, ArtifactFile>();
|
||||
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])])
|
||||
for (const file of artifact.files) {
|
||||
const existing = files.get(file.path);
|
||||
if (existing && canonicalJson(existing) !== canonicalJson(file)) fail(file.path, "Conflicting file descriptors");
|
||||
files.set(file.path, file);
|
||||
}
|
||||
for (const file of files.values()) {
|
||||
const bytes = await read(manifest.packageRevisionId, file.path, file.bytes);
|
||||
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
|
||||
fail(file.path, "Artifact bytes do not match the checked content digest");
|
||||
}
|
||||
}
|
||||
/** Caller supplies a reader confined to the resolved immutable package output, including symlink checks. */
|
||||
export async function checkExecutionWorld(
|
||||
plan: ExecutionPlan,
|
||||
@@ -335,17 +360,11 @@ export async function checkExecutionWorld(
|
||||
const manifest =
|
||||
manifests.find((m) => m.packageRevisionId === expected.revision) ??
|
||||
fail(expected.revision, "Missing package artifact");
|
||||
checkCoverage(expected, manifest);
|
||||
checkPackageCoverage(expected, manifest);
|
||||
if (canonicalJson(manifest.versions) !== canonicalJson(plan.definition.versions))
|
||||
fail(expected.revision, "Incompatible runtime codecs");
|
||||
const files = new Map<string, ArtifactFile>();
|
||||
await checkArtifactFiles(manifest, read);
|
||||
for (const implementation of manifest.implementations) {
|
||||
for (const file of implementation.files) {
|
||||
const existing = files.get(file.path);
|
||||
if (existing && canonicalJson(existing) !== canonicalJson(file))
|
||||
fail(file.path, "Conflicting file descriptors");
|
||||
files.set(file.path, file);
|
||||
}
|
||||
artifacts.push({
|
||||
binding: JSON.stringify([expected.revision, implementation.id]),
|
||||
target: implementation.target,
|
||||
@@ -353,23 +372,12 @@ export async function checkExecutionWorld(
|
||||
});
|
||||
}
|
||||
for (const component of manifest.components ?? []) {
|
||||
for (const file of component.files) {
|
||||
const existing = files.get(file.path);
|
||||
if (existing && canonicalJson(existing) !== canonicalJson(file))
|
||||
fail(file.path, "Conflicting file descriptors");
|
||||
files.set(file.path, file);
|
||||
}
|
||||
artifacts.push({
|
||||
binding: JSON.stringify([expected.revision, "component", component.id]),
|
||||
target: "browser-renderer",
|
||||
digest: component.contentDigest,
|
||||
});
|
||||
}
|
||||
for (const file of files.values()) {
|
||||
const bytes = await read(expected.revision, file.path, file.bytes);
|
||||
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
|
||||
fail(file.path, "Artifact bytes do not match the checked content digest");
|
||||
}
|
||||
}
|
||||
return {
|
||||
codec: "camino-world-v1",
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import type { BindingSchema, BindingContractSchema } from "../bindings/index.js";
|
||||
import type { InterfaceRevision, PackageRevision } from "../capability-model/types.js";
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { moduleContract } from "../capability-model/modules.js";
|
||||
import { generatePackageComponentBindings, publicQueryContract, type PublicQueryContract } from "./client-contracts.js";
|
||||
import { moduleTypeContext } from "./module-contracts.js";
|
||||
import { executionVersions, hexDigest, ExecutionPlanError, type PlanPackage } from "./plan.js";
|
||||
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
|
||||
|
||||
/** Inputs to compilation, independent of source pins and installed providers.
|
||||
* Declared ports select interface contracts; the React consumer projects its
|
||||
* imported interfaces, public queries and module inputs. Ordinary code
|
||||
* and richer language type companions remain separate, explicit Nix inputs. */
|
||||
export interface PackageBuildContract {
|
||||
definition: {
|
||||
codec: "camino-package-build-v1";
|
||||
package: Omit<PlanPackage, "source" | "sourceDigest" | "bindingDigest">;
|
||||
versions: typeof executionVersions;
|
||||
bindings: BindingContractSchema;
|
||||
queries: PublicQueryContract[];
|
||||
component?: string;
|
||||
moduleInterfaces: { revisionId: string; definitionId?: string; members: string[] }[];
|
||||
diagnostics: string[];
|
||||
};
|
||||
digest: string;
|
||||
}
|
||||
|
||||
const normalize = <T>(value: T): T => JSON.parse(canonicalJson(value)) as T;
|
||||
const indexSchema = (schema: BindingSchema) => ({
|
||||
interfaces: new Map<string, InterfaceRevision>(
|
||||
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])].map((entry) => [entry.revisionId, entry]),
|
||||
),
|
||||
packages: new Map<string, PackageRevision>(schema.packages.map((entry) => [entry.revisionId, entry])),
|
||||
});
|
||||
export function packageBuildContexts(
|
||||
schema: BindingSchema,
|
||||
): Record<string, { contract: PackageBuildContract; sourceDigest: string }> {
|
||||
const index = indexSchema(schema);
|
||||
return Object.fromEntries(
|
||||
schema.packages.map((pkg) => [
|
||||
pkg.revisionId,
|
||||
{
|
||||
contract: projectPackageContract(schema, pkg.revisionId, index),
|
||||
sourceDigest: hexDigest({ revision: pkg.revisionId, source: pkg.source }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
export function packageBuildContract(schema: BindingSchema, revision: string): PackageBuildContract {
|
||||
return projectPackageContract(schema, revision, indexSchema(schema));
|
||||
}
|
||||
function projectPackageContract(
|
||||
schema: BindingSchema,
|
||||
revision: string,
|
||||
index: ReturnType<typeof indexSchema>,
|
||||
): PackageBuildContract {
|
||||
const declared = index.packages.get(revision);
|
||||
if (!declared) throw new ExecutionPlanError("UNKNOWN_BUILD_PACKAGE", revision, "Missing package declaration");
|
||||
const pkg = normalize(declared);
|
||||
const interfaces = (ids: Iterable<string>) =>
|
||||
[...ids].map((id) => {
|
||||
const iface = index.interfaces.get(id);
|
||||
if (!iface) throw new ExecutionPlanError("UNKNOWN_BUILD_INTERFACE", id, "Missing declared interface");
|
||||
return normalize(iface);
|
||||
});
|
||||
const modules = [...(pkg.modules ?? [])]
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.map((module) => {
|
||||
const contract = moduleContract(interfaces([module.interfaceRevisionId]), module);
|
||||
if (!contract) throw new ExecutionPlanError("UNKNOWN_MODULE_CONTRACT", module.id, "Missing module contract");
|
||||
return {
|
||||
...module,
|
||||
contract: contract.contract,
|
||||
...(contract.subject ? { subject: contract.subject } : {}),
|
||||
...moduleTypeContext(index.interfaces.get(module.interfaceRevisionId)!),
|
||||
};
|
||||
});
|
||||
const components = modules.flatMap((module) =>
|
||||
["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
|
||||
module.contract,
|
||||
) && module.subject?.kind === "interface"
|
||||
? [
|
||||
{
|
||||
id: module.id,
|
||||
displayName: module.displayName,
|
||||
subject: module.subject.interfaceRevisionId,
|
||||
runtime: "react" as const,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
const shape: PackageBuildContract["definition"]["package"] = {
|
||||
revision,
|
||||
exports: [...pkg.exports]
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.map((entry) => ({ id: entry.id, execution: entry.execution ?? { kind: "remote" } })),
|
||||
...(modules.length ? { modules } : {}),
|
||||
...(components.length ? { components } : {}),
|
||||
...(pkg.controllers?.length ? { controllers: [...pkg.controllers].sort((a, b) => a.id.localeCompare(b.id)) } : {}),
|
||||
};
|
||||
const portInterfaces = new Set(
|
||||
pkg.exports.flatMap((entry) =>
|
||||
entry.dependencyPorts.flatMap((port) =>
|
||||
port.requirement.kind === "interface" ? [port.requirement.interfaceRevisionId] : [],
|
||||
),
|
||||
),
|
||||
);
|
||||
const bindings: BindingContractSchema = {
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
packages: [
|
||||
{
|
||||
revisionId: pkg.revisionId,
|
||||
exports: [...pkg.exports].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
...(pkg.genericExports ? { genericExports: pkg.genericExports } : {}),
|
||||
},
|
||||
],
|
||||
interfaces: interfaces(portInterfaces)
|
||||
.map(({ source, ...contract }) => contract)
|
||||
.sort((a, b) => a.revisionId.localeCompare(b.revisionId)),
|
||||
};
|
||||
const preparation = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
|
||||
const diagnostics = components.flatMap((component) => {
|
||||
const input = preparation?.modules.find((entry) => entry.module === component.id);
|
||||
return input && pkg.queryCompositions?.some((entry) => entry.id === input.composition)
|
||||
? []
|
||||
: [`${component.id}: declare checked inputs in quixos.react.json`];
|
||||
});
|
||||
// Only interfaces whose modules are exposed by these bindings need language
|
||||
// type companions. Reference types themselves do not recursively import the
|
||||
// referenced object's implementation or its conformance inventory.
|
||||
const used = new Set<string>(Object.values(pkg.clientInterfaces ?? {}));
|
||||
for (const module of modules) used.add(module.interfaceRevisionId);
|
||||
for (const component of components) used.add(component.subject);
|
||||
for (const entry of preparation?.modules ?? [])
|
||||
for (const child of entry.children) if (child.role) used.add(child.role.interface);
|
||||
const moduleInterfaces = interfaces(used)
|
||||
.flatMap((iface: InterfaceRevision) => {
|
||||
const members = iface.members
|
||||
.filter((member) => member.kind === "module")
|
||||
.map((member) => member.id)
|
||||
.sort();
|
||||
return members.length
|
||||
? [
|
||||
{
|
||||
revisionId: iface.revisionId,
|
||||
...(iface.application ? { definitionId: iface.application.definitionId } : {}),
|
||||
members,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
})
|
||||
.sort((a, b) => a.revisionId.localeCompare(b.revisionId));
|
||||
for (const module of modules)
|
||||
for (const argument of index.interfaces.get(module.interfaceRevisionId)?.application?.arguments ?? [])
|
||||
if (argument.kind === "object" && argument.target.kind === "interface")
|
||||
used.add(argument.target.interfaceRevisionId);
|
||||
const providers = new Set<string>(Object.values(pkg.clientPackages ?? {}));
|
||||
for (const query of pkg.queries ?? [])
|
||||
for (const imported of query.importedFragments ?? []) providers.add(imported.packageRevisionId);
|
||||
providers.delete(pkg.revisionId);
|
||||
const componentSchema: BindingSchema = {
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: interfaces(used),
|
||||
packages: [
|
||||
pkg,
|
||||
...[...providers].map((id) => {
|
||||
const provider = index.packages.get(id);
|
||||
if (!provider) throw new ExecutionPlanError("UNKNOWN_BUILD_PROVIDER", id, "Missing declared package provider");
|
||||
return normalize(provider);
|
||||
}),
|
||||
],
|
||||
};
|
||||
const definition: PackageBuildContract["definition"] = {
|
||||
codec: "camino-package-build-v1",
|
||||
package: shape,
|
||||
versions: { ...executionVersions },
|
||||
bindings,
|
||||
queries: [
|
||||
...new Map(
|
||||
pkg.exports.flatMap((entry) =>
|
||||
entry.dependencyPorts.flatMap((port) => {
|
||||
if (port.requirement.kind !== "query") return [];
|
||||
const requirement = port.requirement;
|
||||
const provider = index.packages.get(requirement.packageRevisionId);
|
||||
if (!provider)
|
||||
throw new ExecutionPlanError(
|
||||
"UNKNOWN_QUERY_PROVIDER",
|
||||
requirement.packageRevisionId,
|
||||
"Missing declared query provider",
|
||||
);
|
||||
return [
|
||||
[
|
||||
JSON.stringify([requirement.packageRevisionId, requirement.queryId]),
|
||||
publicQueryContract(provider, requirement.queryId),
|
||||
] as const,
|
||||
];
|
||||
}),
|
||||
),
|
||||
).entries(),
|
||||
]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([, query]) => query),
|
||||
...(components.length && !diagnostics.length
|
||||
? { component: generatePackageComponentBindings(componentSchema, shape) }
|
||||
: {}),
|
||||
moduleInterfaces,
|
||||
diagnostics,
|
||||
};
|
||||
const normalized = JSON.parse(canonicalJson(definition)) as typeof definition;
|
||||
return { definition: normalized, digest: hexDigest(normalized) };
|
||||
}
|
||||
|
||||
export function checkPackageBuildContract(contract: PackageBuildContract): void {
|
||||
if (contract.definition.codec !== "camino-package-build-v1" || hexDigest(contract.definition) !== contract.digest)
|
||||
throw new ExecutionPlanError("INVALID_BUILD_CONTRACT", contract.digest, "Package contract digest mismatch");
|
||||
if (contract.definition.diagnostics.length)
|
||||
throw new ExecutionPlanError(
|
||||
"INCOMPLETE_BUILD_CONTRACT",
|
||||
contract.definition.package.revision,
|
||||
contract.definition.diagnostics.join("; "),
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import type {
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { portableValueSchema, tsType } from "../bindings/portable.js";
|
||||
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
|
||||
import type { BindingSchema } from "../bindings/index.js";
|
||||
import type { PackageBuildContract } from "./build-contract.js";
|
||||
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
|
||||
import { moduleTypeContext } from "./module-contracts.js";
|
||||
|
||||
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */
|
||||
@@ -145,6 +148,13 @@ export function generatePackageQueryClientBindings(
|
||||
runtimeModule = "@quixos/camino-replica-core",
|
||||
packages: readonly PackageRevision[] = [pkg],
|
||||
): string {
|
||||
const internal =
|
||||
(pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined)?.internalQueries ?? [];
|
||||
pkg = {
|
||||
...pkg,
|
||||
queries: pkg.queries?.filter((query) => !internal.includes(query.id)),
|
||||
checkedQueries: pkg.checkedQueries?.filter((query) => !internal.includes(query.declaration.id)),
|
||||
};
|
||||
const queries = (pkg.checkedQueries ?? [])
|
||||
.filter((query) => !query.declaration.fragment)
|
||||
.map(
|
||||
@@ -206,12 +216,21 @@ export function generatePackageQueryClientBindings(
|
||||
/** Receipt-covered bindings expose the package's imports, component subjects and
|
||||
* declared queries, never the workspace's complete atom/interface inventory. */
|
||||
export function generateComponentClientBindings(plan: ExecutionPlan, packageRevision: string): string {
|
||||
const pkg = plan.definition.workspace.packageImports.find((pkg) => pkg.revisionId === packageRevision);
|
||||
const components = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.components;
|
||||
const modules = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.modules ?? [];
|
||||
const shape = plan.definition.packages.find((pkg) => pkg.revision === packageRevision);
|
||||
if (!shape) throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Unknown package");
|
||||
return generatePackageComponentBindings(plan.definition.bindingSchema, shape);
|
||||
}
|
||||
export function generatePackageComponentBindings(
|
||||
schema: BindingSchema,
|
||||
shape: PackageBuildContract["definition"]["package"],
|
||||
): string {
|
||||
const packageRevision = shape.revision;
|
||||
const pkg = schema.packages.find((pkg) => pkg.revisionId === packageRevision);
|
||||
const components = shape.components;
|
||||
const modules = shape.modules ?? [];
|
||||
if (!pkg || !components?.length)
|
||||
throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Package declares no component entries");
|
||||
const byId = new Map(plan.definition.workspace.interfaceImports.map((iface) => [iface.revisionId as string, iface]));
|
||||
const byId = new Map(schema.interfaces.map((iface) => [iface.revisionId as string, iface]));
|
||||
const find = (id: string) => {
|
||||
const contract = byId.get(id);
|
||||
if (!contract || contract.template)
|
||||
@@ -254,11 +273,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
.filter((line) => !line.startsWith("import "))
|
||||
.join("\n")
|
||||
.replace("export const interfaces =", "export const subjects =");
|
||||
const queries = generatePackageQueryClientBindings(
|
||||
pkg,
|
||||
"@quixos/camino-react",
|
||||
plan.definition.workspace.packageImports,
|
||||
)
|
||||
const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react", schema.packages)
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith("import "))
|
||||
.join("\n");
|
||||
@@ -270,7 +285,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
queries +
|
||||
`\nexport const providers = {${Object.entries(pkg.clientPackages ?? {})
|
||||
.map(([alias, id]) => {
|
||||
const imported = plan.definition.workspace.packageImports.find((entry) => entry.revisionId === id);
|
||||
const imported = schema.packages.find((entry) => entry.revisionId === id);
|
||||
if (!imported) throw new ExecutionPlanError("UNINSTALLED_PACKAGE", id, "Missing imported module provider");
|
||||
return `${JSON.stringify(alias)}:{${(imported.modules ?? []).map((module) => `${JSON.stringify(module.displayName)}:${JSON.stringify({ packageRevision: id, id: module.id })}`).join(",")}}`;
|
||||
})
|
||||
@@ -304,7 +319,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`;
|
||||
return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`;
|
||||
});
|
||||
const input = `${tsType(composition.output)} & {readonly children: {${children.join(";")}}}`;
|
||||
const input = `${tsType(preparation.input ?? composition.output)} & {readonly children: {${children.join(";")}}}`;
|
||||
return `${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}, ${JSON.stringify(module.contract)}, ${moduleArguments(find(module.interfaceRevisionId))}, ${input}> & ExtraModuleProps<${JSON.stringify(key)}>>;`;
|
||||
})
|
||||
.join("\n") +
|
||||
|
||||
@@ -22,3 +22,5 @@ export { reviewSemanticMajors } from "../capability-model/evolution.js";
|
||||
export * from "./public-reflection.js";
|
||||
|
||||
export * from "./graph-execution.js";
|
||||
|
||||
export * from "./build-contract.js";
|
||||
|
||||
+11
-52
@@ -11,13 +11,12 @@ import type {
|
||||
OperationId,
|
||||
} from "../capability-model/types.js";
|
||||
import { validateWorkspaceRevision } from "../capability-model/validation.js";
|
||||
import { moduleContract } from "../capability-model/modules.js";
|
||||
import { canonicalJson, contentDigest } from "../capability-model/evolution.js";
|
||||
import { resolveForwardedBinding } from "../capability-model/forwarding.js";
|
||||
import { linkQueries, type LinkedQuery } from "../query/link.js";
|
||||
import { type BindingSchema } from "../bindings/index.js";
|
||||
import { unsupportedQueryOperator } from "./query-operators.js";
|
||||
import { moduleTypeContext } from "./module-contracts.js";
|
||||
import { packageBuildContexts } from "./build-contract.js";
|
||||
|
||||
export class ExecutionPlanError extends Error {
|
||||
constructor(
|
||||
@@ -78,30 +77,6 @@ export interface PlanPackage {
|
||||
exports: { id: string; execution: ExecutionContract }[];
|
||||
}
|
||||
|
||||
/** React consumes these module contracts; other contracts remain metadata until
|
||||
* an artifact consumer exists. This does not make modules callable operations. */
|
||||
function componentDeclarations(workspace: WorkspaceRevision, pkg: WorkspaceRevision["packageImports"][number]) {
|
||||
return [
|
||||
...(pkg.modules ?? []).flatMap((module) => {
|
||||
const contract = moduleContract(workspace.interfaceImports, module)!;
|
||||
if (
|
||||
!["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
|
||||
contract.contract,
|
||||
) ||
|
||||
contract.subject?.kind !== "interface"
|
||||
)
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
id: module.id,
|
||||
displayName: module.displayName,
|
||||
subject: contract.subject.interfaceRevisionId,
|
||||
runtime: "react" as const,
|
||||
},
|
||||
];
|
||||
}),
|
||||
];
|
||||
}
|
||||
export interface ExecutionPlanDefinition {
|
||||
presentations?: NonNullable<WorkspaceRevision["presentations"]>;
|
||||
codec: "camino-execution-plan-v1";
|
||||
@@ -543,37 +518,21 @@ export function compileExecutionPlan(
|
||||
packages: sorted(workspace.packageImports, (p) => p.revisionId),
|
||||
};
|
||||
const normalizedSchema = JSON.parse(canonicalJson(schema)) as BindingSchema;
|
||||
const buildContexts = packageBuildContexts(normalizedSchema);
|
||||
const definition: ExecutionPlanDefinition = {
|
||||
codec: "camino-execution-plan-v1",
|
||||
workspace,
|
||||
bindingSchema: normalizedSchema,
|
||||
...(workspace.presentations?.length ? { presentations: sorted(workspace.presentations, (p) => p.id) } : {}),
|
||||
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => ({
|
||||
revision: p.revisionId,
|
||||
source: p.source,
|
||||
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
|
||||
bindingDigest: hexDigest(normalizedSchema),
|
||||
...(p.modules?.length
|
||||
? {
|
||||
modules: sorted(p.modules, (module) => module.id).map((module) => {
|
||||
const contract = moduleContract(workspace.interfaceImports, module)!;
|
||||
return {
|
||||
...module,
|
||||
contract: contract.contract,
|
||||
...(contract.subject ? { subject: contract.subject } : {}),
|
||||
...moduleTypeContext(
|
||||
workspace.interfaceImports.find((iface) => iface.revisionId === module.interfaceRevisionId)!,
|
||||
),
|
||||
};
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(p.controllers?.length ? { controllers: sorted(p.controllers, (controller) => controller.id) } : {}),
|
||||
...(componentDeclarations(workspace, p).length
|
||||
? { components: sorted(componentDeclarations(workspace, p), (component) => component.id) }
|
||||
: {}),
|
||||
exports: sorted(p.exports, (e) => e.id).map((e) => ({ id: e.id, execution: e.execution ?? { kind: "remote" } })),
|
||||
})),
|
||||
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => {
|
||||
const contract = buildContexts[p.revisionId]!.contract;
|
||||
return {
|
||||
...contract.definition.package,
|
||||
source: p.source,
|
||||
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
|
||||
bindingDigest: contract.digest,
|
||||
};
|
||||
}),
|
||||
owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => {
|
||||
if (!c.id)
|
||||
throw new ExecutionPlanError(
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js";
|
||||
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
|
||||
import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js";
|
||||
import { type ArtifactReader, type PackageArtifacts, type ArtifactGroup } from "./artifacts.js";
|
||||
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
||||
import { generateComponentClientBindings, componentAssetDeclarations } from "./client-contracts.js";
|
||||
import { componentAssetDeclarations } from "./client-contracts.js";
|
||||
|
||||
export interface ArtifactReceipt {
|
||||
import { checkPackageBuildContract, packageBuildContract, type PackageBuildContract } from "./build-contract.js";
|
||||
|
||||
export interface GroupReceipt {
|
||||
schemaVersion: 1;
|
||||
packageRevisionId: string;
|
||||
bindingDigest: string;
|
||||
sourceDigest: string;
|
||||
manifestDigest: string;
|
||||
generatorDigest: string;
|
||||
generated: { target: string; path: string; digest: string; bytes: number }[];
|
||||
}
|
||||
export interface ArtifactReceipt extends GroupReceipt {
|
||||
sourceDigest: string;
|
||||
}
|
||||
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`;
|
||||
@@ -24,20 +28,42 @@ export function expectedArtifactReceipt(
|
||||
manifest: PackageArtifacts,
|
||||
generatorDigest: string,
|
||||
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
|
||||
if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest");
|
||||
const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId);
|
||||
const schemaPackage = plan.definition.bindingSchema.packages.find((p) => p.revisionId === manifest.packageRevisionId);
|
||||
if (!pkg || manifest.sourceDigest !== pkg.sourceDigest)
|
||||
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/source mismatch");
|
||||
return expectedPackageReceipt(
|
||||
packageBuildContract(plan.definition.bindingSchema, pkg.revision),
|
||||
manifest,
|
||||
pkg.sourceDigest,
|
||||
generatorDigest,
|
||||
);
|
||||
}
|
||||
export function expectedPackageReceipt(
|
||||
contract: PackageBuildContract,
|
||||
manifest: PackageArtifacts,
|
||||
sourceDigest: string,
|
||||
generatorDigest: string,
|
||||
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
|
||||
if (manifest.sourceDigest !== sourceDigest)
|
||||
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt source mismatch");
|
||||
const result = expectedGroupReceipt(contract, manifest, generatorDigest);
|
||||
return { ...result, receipt: { ...result.receipt, sourceDigest } };
|
||||
}
|
||||
export function expectedGroupReceipt(
|
||||
contract: PackageBuildContract,
|
||||
manifest: ArtifactGroup,
|
||||
generatorDigest: string,
|
||||
): { receipt: GroupReceipt; sources: Map<string, Uint8Array> } {
|
||||
checkPackageBuildContract(contract);
|
||||
if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest");
|
||||
const pkg = contract.definition.package;
|
||||
const schemaPackage = contract.definition.bindings.packages.find((p) => p.revisionId === pkg.revision)!;
|
||||
if (
|
||||
!pkg ||
|
||||
!schemaPackage ||
|
||||
manifest.bindingDigest !== pkg.bindingDigest ||
|
||||
manifest.sourceDigest !== pkg.sourceDigest
|
||||
manifest.packageRevisionId !== pkg.revision ||
|
||||
manifest.bindingDigest !== contract.digest ||
|
||||
hexDigest(manifest.versions) !== hexDigest(contract.definition.versions)
|
||||
)
|
||||
throw new ExecutionPlanError(
|
||||
"INVALID_RECEIPT",
|
||||
manifest.packageRevisionId,
|
||||
"Receipt package/schema/source mismatch",
|
||||
);
|
||||
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/contract mismatch");
|
||||
// A process service may implement transactions in any language. Only the
|
||||
// checked JS module path promises generated TypeScript adapter evidence.
|
||||
const available = new Set(
|
||||
@@ -60,14 +86,14 @@ export function expectedArtifactReceipt(
|
||||
const generated = targets.map((target) => {
|
||||
const path = generatedBindingPath(target);
|
||||
const bytes = new TextEncoder().encode(
|
||||
generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }),
|
||||
generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, { target }),
|
||||
);
|
||||
sources.set(path, bytes);
|
||||
return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
|
||||
});
|
||||
if (manifest.components?.length) {
|
||||
for (const [path, source] of [
|
||||
["share/quixos/generated/client.ts", generateComponentClientBindings(plan, pkg.revision)],
|
||||
["share/quixos/generated/client.ts", contract.definition.component!],
|
||||
["share/quixos/generated/component-assets.d.ts", componentAssetDeclarations()],
|
||||
] as const) {
|
||||
const bytes = new TextEncoder().encode(source);
|
||||
@@ -88,7 +114,7 @@ export function expectedArtifactReceipt(
|
||||
) {
|
||||
const path = "share/quixos/generated/remote.ts";
|
||||
const bytes = new TextEncoder().encode(
|
||||
generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision),
|
||||
generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision),
|
||||
);
|
||||
sources.set(path, bytes);
|
||||
generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length });
|
||||
@@ -97,8 +123,7 @@ export function expectedArtifactReceipt(
|
||||
receipt: {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: pkg.revision,
|
||||
bindingDigest: pkg.bindingDigest,
|
||||
sourceDigest: pkg.sourceDigest,
|
||||
bindingDigest: contract.digest,
|
||||
manifestDigest: hexDigest(manifest),
|
||||
generatorDigest,
|
||||
generated,
|
||||
|
||||
Reference in New Issue
Block a user