/** * SDK-side spec builder: CompiledDomain → serialize → Graph. * * Serializes BoundMethod[] into FunctionSchema[], generates entries for * inherited methods not covered by the domain's handlers, then delegates * to serialize() for graph blueprint generation. * * Domain-side Methods are remote-bound: dispatch routes through * `binding.remoteUrl` and never reaches the kernel sandbox. We therefore * do NOT stamp `code` on emitted FunctionSchema entries — there is no * sandbox-eval'able body. Only the kernel domain (compiled in-process) * carries `code` strings. * * The emitted graph is absolute — the Tree produced by `serialize()` is * already mounted at `/`, so `tree.toGraph()` lifts every internal * relative path into its host-graph absolute form. Wire serialization is the * caller's responsibility (`graph.toWire()`). */ import type { Graph, WireGraph } from '@astrale-os/kernel-core' import type { BoundMethod, FunctionSchema, ViewSchema } from '@astrale-os/kernel-core/domain' import type { Schema } from '@astrale-os/kernel-dsl' import { AbsolutePath } from '@astrale-os/kernel-core' import { hashInstallGraph, serialize, zodToJsonSchema } from '@astrale-os/kernel-core/domain' import type { AnyRemoteHandler } from '../method/single.js' import type { RemoteDomain } from './define.js' import { extractBinding } from './contract.js' import { materializeRemoteDomain } from './define.js' /** * Build the install-ready wire graph for a domain served at `url`: materialize * the domain at its real serving URL (so every binding carries the live URL — * the define-time compile carries none), serialize, and emit the wire form. * This is the graph the kernel installs and re-hashes to verify. There is no * url-less variant: an install graph without bindings is uninstallable (the * kernel install guard rejects it). */ export function buildInstallGraph( domain: RemoteDomain, url: string, ): WireGraph { const { compiled, auxiliary } = materializeRemoteDomain(domain, url) // Two-axis placement: `parent` = the folder the Domain node physically lives // under. `path` names the FULL node path (basename === origin); // unset defaults to /domains/ — the "/domains" placement policy lives // HERE (the kernel default is Root → /). `mount` is deliberately NOT // passed: installed_in + typed addresses stay root-mounted (kernel default). const parent = domain.path ? AbsolutePath.parse(domain.path).parent() : AbsolutePath.from('domains') return buildSpecInternal( compiled, domain.methods, url, auxiliary?.functionSchemas ?? [], auxiliary?.viewSchemas ?? [], parent, ).toWire() as WireGraph } /** * Content hash of the install graph. Delegates the canonical hash to kernel-core's * `hashInstallGraph` — the same algorithm the kernel re-runs on install to verify * the received graph against the signed hash. */ export function buildInstallGraphHash( domain: RemoteDomain, url: string, ): Promise { return hashInstallGraph(buildInstallGraph(domain, url)) } // ── Internal ──────────────────────────────────────────────── function buildSpecInternal( compiled: RemoteDomain['compiled'], methods: BoundMethod[], url: string, functionSchemas: FunctionSchema[], viewSchemas: ViewSchema[], parent: AbsolutePath, ): Graph { const serialized = serializeMethodsWithStubs(compiled, methods, url) // Method impls + standalone-function-member impls travel in the same callable // array (distinct refs: `class.X.method.y` vs `function.`); view-member // impls travel in `options.views` (a view differs structurally — handshake, // view_for, View class). The serializer emits method nodes from the IR, // function/view members from `compiled.$.refs.{functions,views}`, pulling each // impl by ref. const tree = serialize(compiled, [...serialized, ...functionSchemas], { views: viewSchemas, parent, // An install graph is the domain's signed desired state. Reinstalling it // must converge schema and core material exactly, including removing // properties that disappeared from a newer domain version. onConflict: 'replace', }) return tree.toGraph() } /** * Serialize BoundMethod[] and generate stubs for methods in compiled.$.methods * that have no corresponding BoundMethod (inherited-sealed, inherited-default). * * Every Method's binding defaults its `remoteUrl` to the serving URL: domain * methods are remote-bound — the kernel dispatches them by redirecting to the * worker — so a method node without a remote binding is uninstallable (the * kernel install guard rejects it). An explicit per-method `remoteUrl` wins. */ function serializeMethodsWithStubs( compiled: RemoteDomain['compiled'], methods: BoundMethod[], url: string, ): FunctionSchema[] { const bound = new Set(methods.map((m) => m.ref)) const result = methods.map((m) => serializeFromBoundMethod(m, url)) // Generate entries for unbound methods (replaces fillMissingCallables). // No `code` is stamped — domain-side Methods are binding-only. The `ref` MUST // be the QUALIFIED `ResolvedMethod.ref` (`..method.`) — the exact // form `serialize`'s method-node lookup uses; re-deriving a bare `Type.method` // is the F11 bug (interface methods then never resolve → MISSING_HANDLER). for (const classMethods of Object.values(compiled.$.methods)) { for (const rm of Object.values(classMethods)) { const ref = rm.ref if (bound.has(ref)) continue result.push({ ref, inputSchema: { type: 'object' }, outputSchema: {}, output: 'value', binding: { remoteUrl: url }, }) } } return result } function serializeFromBoundMethod( method: BoundMethod, url: string, ): FunctionSchema { const binding = extractBinding(method.handler) return { ref: method.ref, inputSchema: zodToJsonSchema(method.inputSchema), outputSchema: zodToJsonSchema(method.outputSchema), isStatic: method.isStatic || undefined, output: method.output, binding: binding?.remoteUrl ? binding : { ...binding, remoteUrl: url }, } }