import { createHash } from "node:crypto"; import { capabilityId, type PackageExport, type PackageExportId, type DependencyPort, type PackageRevision, } from "./types.js"; import { TypeSubstitution, GenericTypeError, bindTypeParameters, canonicalTypeArgument, type TypeParameter, type ValueTypeExpression, type ObjectTypeExpression, type InterfaceApplicationExpression, type GenericTypeEnvironment, type ClosedTypeArgument, type ValueAliasDefinition, } from "./generics.js"; export type GenericDependencyPort = Omit & { requirement: | { kind: "state"; valueType: ValueTypeExpression; primitives: Extract["primitives"]; } | { kind: "edge"; target: ObjectTypeExpression; cardinality: Extract["cardinality"]; primitives: Extract["primitives"]; } | { kind: "interface"; application: InterfaceApplicationExpression } | { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression }; }; export interface GenericPackageExport { id: PackageExportId; displayName: string; parameters: TypeParameter[]; kind: "operation" | "function"; inputType: ValueTypeExpression; outputType: ValueTypeExpression; eventType?: ValueTypeExpression; mode?: Extract["mode"]; receiverRequirement: | { kind: "any-object" } | { kind: "target"; target: ObjectTypeExpression } | { kind: "interfaces"; interfaces: InterfaceApplicationExpression[] }; dependencyPorts: GenericDependencyPort[]; aliases: ValueAliasDefinition[]; } /** A closed manifest is a Nix build input, never a mutable source checkout. */ export const specializePackageExport = ( pkg: PackageRevision, definition: GenericPackageExport, arguments_: ClosedTypeArgument[], environment: GenericTypeEnvironment, ): PackageExport => { const lexical = { ...environment, aliases: new Map(definition.aliases.map((alias) => [alias.id, alias])) }; const bindings = bindTypeParameters(definition.parameters, arguments_, lexical, definition.displayName); const substitution = new TypeSubstitution({ ...lexical, arguments: bindings }); const digest = createHash("sha256") .update( JSON.stringify([ "quixos-package-specialization-v1", pkg.revisionId, [pkg.source.repository, pkg.source.commit], definition.id, arguments_.map(canonicalTypeArgument), environment.self ?? null, ]), ) .digest("hex"); const ports: DependencyPort[] = definition.dependencyPorts.map((port) => { const requirement = port.requirement; switch (requirement.kind) { case "state": return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } }; case "edge": return { ...port, requirement: { ...requirement, target: substitution.object(requirement.target) } }; case "interface": return { ...port, requirement: { kind: "interface", interfaceRevisionId: substitution.application(requirement.application) }, }; case "constructor": { const target = substitution.object(requirement.target); if (target.kind !== "atom") throw new GenericTypeError( "constructor-target", port.displayName, "Constructor ports require a concrete atom argument, not an interface view", ); return { ...port, requirement: { kind: "constructor", atomId: target.atomId, inputType: substitution.value(requirement.inputType), }, }; } } }); const common = { id: capabilityId.packageExport(`export-application:sha256:${digest}`), displayName: `${definition.displayName}$${digest}`, inputType: substitution.value(definition.inputType), outputType: substitution.value(definition.outputType), dependencyPorts: ports, application: { exportId: definition.id, arguments: structuredClone(arguments_), ...(environment.self ? { self: environment.self } : {}), }, }; if (definition.kind === "function") return { ...common, kind: "function" }; const receiver = definition.receiverRequirement; const target = receiver.kind === "target" ? substitution.object(receiver.target) : undefined; return { ...common, kind: "operation", mode: definition.mode!, ...(definition.eventType ? { eventType: substitution.value(definition.eventType) } : {}), receiverRequirement: target ? target.kind === "atom" ? { kind: "exact-atom", atomId: target.atomId } : { kind: "all-interfaces", interfaceRevisionIds: [target.interfaceRevisionId] } : receiver.kind === "interfaces" ? { kind: "all-interfaces", interfaceRevisionIds: receiver.interfaces.map((value) => substitution.application(value)), } : { kind: "any-object" }, }; };