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; versions: typeof executionVersions; bindings: BindingContractSchema; queries: PublicQueryContract[]; component?: string; moduleInterfaces: { revisionId: string; definitionId?: string; members: string[] }[]; diagnostics: string[]; }; digest: string; } const normalize = (value: T): T => JSON.parse(canonicalJson(value)) as T; const indexSchema = (schema: BindingSchema) => ({ interfaces: new Map( [...schema.interfaces, ...(schema.interfaceTemplates ?? [])].map((entry) => [entry.revisionId, entry]), ), packages: new Map(schema.packages.map((entry) => [entry.revisionId, entry])), }); export function packageBuildContexts( schema: BindingSchema, ): Record { 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, ): 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) => [...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(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(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("; "), ); }