Files
quixos-protocol/src/execution-world/build-contract.ts
T

225 lines
9.5 KiB
TypeScript
Raw Normal View History

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("; "),
);
}