import type { InterfaceRevision, PackageRevision, ValueType, DependencyPort, WorkspaceRevision, } from "../capability-model/types.js"; import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js"; import { genericImplementationType } from "./generics.js"; import { capabilityId } from "../capability-model/types.js"; /** Portable generator input. New backends consume this instead of the parser or TS runtime. */ export type BindingSchema = { format: "quixos-bindings"; version: 1; interfaces: InterfaceRevision[]; interfaceTemplates?: InterfaceRevision[]; packages: PackageRevision[]; }; export const bindingSchema = (compiled: Pick): BindingSchema => ({ format: "quixos-bindings", version: 1, interfaceTemplates: compiled.resources.flatMap((node) => node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [], ), interfaces: [ ...new Map( compiled.resources .flatMap((node) => [ ...(node.resource.kind === "interface" ? [node.resource.revision] : []), ...(node.resource.specializations ?? []), ]) .filter((entry) => !entry.template) .map((entry) => [entry.revisionId, entry]), ).values(), ], packages: compiled.resources.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])), }); export const specializeBindingSchema = ( base: BindingSchema, workspace: WorkspaceRevision, revisionId: string, ): BindingSchema => { const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId); if (!pkg) throw new Error(`Missing candidate package ${revisionId}`); const needed = new Set(); const visit = (value: unknown): void => { if (!value || typeof value !== "object") return; if ("interfaceRevisionId" in value && typeof value.interfaceRevisionId === "string") needed.add(value.interfaceRevisionId); Object.values(value).forEach(visit); }; visit(pkg.exports); const interfaces = new Map(base.interfaces.map((entry) => [entry.revisionId, entry])); for (const id of needed) { const iface = workspace.interfaceImports.find((entry) => entry.revisionId === id); if (iface) { interfaces.set(id, iface); visit(iface.members); } } return { ...base, interfaces: [...interfaces.values()], packages: base.packages.map((entry) => { const candidate = workspace.packageImports.find((value) => value.revisionId === entry.revisionId); if (!candidate) throw new Error(`Missing dependency package ${entry.revisionId}`); return candidate; }), }; }; export type TypeScriptBindingOptions = { runtimeModule?: string; /** Each export must implement MessageBinding, providing both TS type and wire codec. */ messages?: Record; }; export function generatePackageDescriptor(schema: BindingSchema, revisionId: string): string { const pkg = schema.packages.find((entry) => entry.revisionId === revisionId); if (!pkg) throw new Error(`Unknown package revision ${revisionId}`); return ( `# Generated from the checked package contract\npackage_id: ${JSON.stringify(pkg.packageId)}\npackage_revision_id: ${JSON.stringify(pkg.revisionId)}\nruntime_protocol_version: "quixos-capabilities-v1"\n` + pkg.exports .map( (entry) => `exports: { export_id: ${JSON.stringify(entry.id)} runtime_symbol: ${JSON.stringify(entry.displayName)} }\n`, ) .join("") ); } const q = JSON.stringify; const object = (entries: [string, string][]) => `{ ${entries.map(([key, value]) => `${q(key)}: ${value}`).join("; ")} }`; const unit = (type: ValueType) => type.kind === "builtin" && type.name === "unit"; export const generateTypeScriptBindings = ( schema: BindingSchema, packageRevisionId: string, options: TypeScriptBindingOptions = {}, ) => { if (schema.format !== "quixos-bindings" || schema.version !== 1) throw new Error("Unsupported binding schema version"); if (schema.interfaces.some((entry) => entry.template)) throw new Error("Package bindings require closed interface applications, not generic definitions"); const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId); if (!pkg) throw new Error(`Unknown package revision ${packageRevisionId}`); const messages = new Map(); const type = (value: ValueType): string => { switch (value.kind) { case "builtin": return value.name === "unit" ? "null" : "QxWatchHandle"; case "scalar": return { bool: "boolean", bytes: "Uint8Array", string: "string", int64: "bigint", uint64: "bigint", double: "number", int32: "number", uint32: "number", }[value.name]; case "object-ref": return `QxObjectRef<${q(value.expectation.kind === "atom" ? `atom:${value.expectation.atomId}` : `interface:${value.expectation.interfaceRevisionId}`)}>`; case "optional": return `(${type(value.value)} | null)`; case "list": return `Array<${type(value.value)}>`; case "record": return `{ ${Object.entries(value.fields) .map(([name, field]) => `${q(name)}: ${type(field)}`) .join("; ")} }`; case "message": { if (!options.messages?.[value.descriptorId]) throw new Error(`Missing TypeScript message binding for ${value.descriptorId}`); if (!messages.has(value.descriptorId)) messages.set(value.descriptorId, `message${messages.size}`); return `BindingValue`; } } }; const ref = (target: { kind: "atom"; atomId: string } | { kind: "interface"; interfaceRevisionId: string }) => `QxObjectRef<${q(target.kind === "atom" ? `atom:${target.atomId}` : `interface:${target.interfaceRevisionId}`)}>`; const params = (input: ValueType) => (unit(input) ? "" : `input: ${type(input)}`); const port = (entry: DependencyPort): { type: string; spec: unknown } => { const requirement = entry.requirement; switch (requirement.kind) { case "state": { const methods = requirement.primitives.map((primitive): [string, string] => { if (primitive === "read") return ["get", `() => Promise<${type(requirement.valueType)}>`]; if (primitive === "write") return ["set", `(value: ${type(requirement.valueType)}) => Promise`]; throw new Error(`State primitive ${primitive} is not supported by the TypeScript runtime binding yet`); }); if (requirement.primitives.includes("read")) methods.push(["live", "() => Promise"]); return { type: object(methods), spec: { ...requirement, id: entry.id } }; } case "edge": { const methods = requirement.primitives.map((primitive): [string, string] => { if (primitive === "resolve") return [primitive, `() => Promise>`]; if (primitive === "connect" || primitive === "disconnect") return [primitive, `(target: ${ref(requirement.target)}) => Promise`]; throw new Error(`Edge primitive ${primitive} is not supported by the TypeScript runtime binding yet`); }); if (requirement.primitives.includes("resolve")) methods.push(["collection", `() => Promise>`]); if ( ["resolve", "connect", "disconnect"].every((primitive) => requirement.primitives.includes(primitive as "resolve"), ) ) methods.push([ "replace", `(entries: RelationshipEntry<${ref(requirement.target)}>[], expectedRevision: bigint) => Promise>`, ]); return { type: object(methods), spec: { kind: "edge", id: entry.id, primitives: requirement.primitives } }; } case "interface": { const contract = schema.interfaces.find( (candidate) => candidate.revisionId === requirement.interfaceRevisionId, ); if (!contract) throw new Error(`Missing imported interface contract ${requirement.interfaceRevisionId}`); // Streaming ports need a future streaming ABI; ordinary calls are fully typed today. const operations = contract.members.flatMap((member) => member.operations .filter((operation) => operation.mode === "call" && operation.scope !== "class") .map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })), ); const methods: [string, string][] = [ ["objectId", ref({ kind: "interface", interfaceRevisionId: requirement.interfaceRevisionId })], [ "live", object( operations.map((operation) => [ operation.name, `(${params(operation.inputType)}) => Promise`, ]), ), ], ...operations.map((operation): [string, string] => [ operation.name, `(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`, ]), ]; return { type: object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]), spec: { kind: "interface", id: entry.id, interfaceRevisionId: requirement.interfaceRevisionId, operations: Object.fromEntries( operations.map(({ name, id, inputType, outputType }) => [name, { id, inputType, outputType }]), ), }, }; } case "constructor": { const input = requirement.inputType; if (!input) throw new Error( `Constructor port ${entry.id} needs an explicit input contract: add 'input TYPE' after ${requirement.atomId} in QX`, ); return { type: object([ ["construct", `(${params(input)}) => Promise<${ref({ kind: "atom", atomId: requirement.atomId })}>`], ]), spec: { kind: "constructor", id: entry.id, inputType: input }, }; } } }; const exports = [...pkg.exports].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); const names = new Set(); const specs: Record = {}; const contexts: [string, string][] = []; const handlers: [string, string][] = []; const results: [string, string][] = []; for (const entry of exports) { if (names.has(entry.displayName)) throw new Error(`Duplicate export name ${entry.displayName}`); names.add(entry.displayName); const ports = entry.dependencyPorts.map((dependency) => ({ name: dependency.displayName, ...port(dependency) })); if (new Set(ports.map((p) => p.name)).size !== ports.length) throw new Error(`Duplicate dependency name in ${entry.displayName}`); const receiver = entry.kind === "constructor" ? ref({ kind: "atom", atomId: entry.constructsAtom }) : entry.kind === "operation" && entry.receiverRequirement.kind === "exact-atom" ? ref({ kind: "atom", atomId: entry.receiverRequirement.atomId }) : entry.kind === "operation" && entry.receiverRequirement.kind === "all-interfaces" ? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>` : "QxObjectRef"; const contextShape = object([ ["conform", "QxConformer"], ...(entry.kind === "function" ? [] : [["objectId", receiver] as [string, string]]), ["input", type(entry.inputType)], ["ports", object(ports.map((port) => [port.name, port.type]))], ]); contexts.push([ entry.displayName, entry.kind === "function" ? `${contextShape} & {signal?: AbortSignal}` : `${contextShape} & QxContextLifecycle<${contextShape} & {signal?: AbortSignal}>`, ]); const event = entry.kind === "operation" ? entry.eventType : undefined; const contextType = `Contexts[${q(entry.displayName)}]`; const outputType = type(event ?? entry.outputType); results.push([entry.displayName, outputType]); // Watch-start handlers produce events through the runtime's derived stream protocol. if (!entry.application) handlers.push([ entry.displayName, event ? `QxDerived<${contextType}, ${outputType}>` : `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`, ]); specs[entry.displayName] = { ...(entry.kind === "function" ? { receiver: "none" } : {}), inputType: entry.inputType, outputType: entry.outputType, ...(event ? { eventType: event } : {}), ports: Object.fromEntries(ports.map((port) => [port.name, port.spec])), }; } for (const definition of pkg.genericExports ?? []) { handlers.push([ definition.displayName, genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type), ]); } // Unused imported interfaces must not introduce new message-codec obligations. // A used port still fails above if its own required codec is missing. const hasCodec = (value: ValueType): boolean => { if (value.kind === "message") return !!options.messages?.[value.descriptorId]; if (value.kind === "record") return Object.values(value.fields).every(hasCodec); if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value); return true; }; const contracts = schema.interfaces .filter((iface) => iface.members.every((member) => member.operations .filter((operation) => operation.mode === "call" && operation.scope !== "class") .every((operation) => hasCodec(operation.inputType) && hasCodec(operation.outputType)), ), ) .map((iface) => { const generated = port({ id: capabilityId.dependencyPort("contract"), displayName: iface.displayName, requirement: { kind: "interface", interfaceRevisionId: iface.revisionId }, }); const spec = generated.spec as { operations: unknown }; return { iface, type: generated.type, code: `defineQxInterfaceContract<${generated.type}>(${q(iface.revisionId)}, ${JSON.stringify(spec.operations)})`, }; }); const imports = [...messages].map(([id, alias]) => { const binding = options.messages![id]!; if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export)) throw new Error(`Invalid message binding export ${binding.export}`); return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`; }); const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${contracts.map((entry) => entry.type).join(" ")}`; const typeImports = [ "BindingValue", "QxObjectRef", "QxWatchHandle", "QxHandler", "QxDerived", "QxContextLifecycle", "QxConformer", "QxInterfaceContract", "QxLiveValue", "RelationshipCollection", "RelationshipEntry", ].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures)); return ( `// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` + `import { ${contracts.length ? "defineQxInterfaceContract, " : ""}${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` + imports.join("\n") + `\nexport const packageRevisionId = ${q(pkg.revisionId)};\n` + `declare const appliedType: unique symbol;\ntype QxApplied = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` + `export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` + `export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` + `export const interfaces = {${contracts .filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1) .map((entry) => `${q(entry.iface.displayName)}: contracts[${q(entry.iface.revisionId)}]`) .join(",\n")}};\n` + `const messages = { ${[...messages].map(([id, alias]) => `${q(id)}: ${alias}`).join(", ")} } satisfies QxMessages;\n` + `const specs = ${JSON.stringify(specs, null, 2)} satisfies Record;\n` + `export const createRuntime = (implementation: Implementation) => ({\n packageRevisionId,\n exports: {\n` + exports .map( (entry) => ` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.application ? pkg.genericExports!.find((definition) => definition.id === entry.application!.exportId)!.displayName : entry.displayName)}], messages),`, ) .join("\n") + `\n },\n});\n` ); };