/** * Materialize the `functions` map (`defineRemoteFunction` entries) into the two * halves a standalone-function DOMAIN MEMBER contributes — mirroring how a class * method splits across compile + serialize: * * - the CONTRACT half — a `func({ input, output })` per slug, declared in the * schema's `functions` group so the compiled domain carries `function.` * on `refs.functions` (the kernel owns the ref/layout/of_domain mapping). * - `FunctionSchema[]` + per-slug `FunctionBinding` — the IMPL/BINDING half, * fed to `serialize` (url-stamped). The worker route is * `//`, DECOUPLED from the graph layout — so * `functionsFolder` only names the URL segment, not the node position. * * A standalone function is NOT a core/instance node: it is attached directly to * the Domain via `of_domain` by the kernel-core serializer, so this module never * touches `Core`. */ import type { FunctionBinding } from '@astrale-os/kernel-api/routed' import type { FunctionSchema } from '@astrale-os/kernel-core/domain' import type { Schema } from '@astrale-os/kernel-dsl' import { zodToJsonSchema } from '@astrale-os/kernel-core/domain' import type { AnyRemoteFunctionDef } from '../define/remote-function.js' import { functionBindingForManifest } from '../service/functions.js' import { resolveBinding } from './binding.js' /** * Default URL segment for a function's worker route (`/functions/`). * Names the HTTP route only — the graph layout (`//functions/`) is * a fixed kernel convention, independent of this. */ export const DEFAULT_FUNCTIONS_FOLDER = 'functions' const SLUG_RE = /^[a-z][a-z0-9-]*$/ function assertValidSlugs(functions: Record): void { for (const slug of Object.keys(functions)) { if (!SLUG_RE.test(slug)) { throw new Error( `defineRemoteDomain: invalid function slug "${slug}" — must match ${SLUG_RE}.`, ) } } } /** * Contract half — since "functions as first-class defineSchema members", a * standalone function's CONTRACT is declared in the SCHEMA (`defineSchema(..., * { functions: { seed: func({ input, output }) } })`) and `compileDomain` builds * the member refs from it; the SDK map supplies the HANDLER half. This check * pins the two halves together at define time: every schema-declared function * needs a handler, every handler a declaration — a drifted key fails loudly at * authoring, never as a boot-time missing-identity or a dangling graph member. */ export function assertFunctionsMatchSchema( schema: Schema, functions: Record | undefined, ): void { if (functions) assertValidSlugs(functions) const declared = Object.keys(schema.functions ?? {}) const handled = Object.keys(functions ?? {}) const missingHandler = declared.filter((slug) => !handled.includes(slug)) const missingDecl = handled.filter((slug) => !declared.includes(slug)) if (missingHandler.length === 0 && missingDecl.length === 0) return const parts: string[] = [] if (missingHandler.length > 0) { parts.push( `schema declares function(s) with no handler: ${missingHandler.map((s) => `"${s}"`).join(', ')}`, ) } if (missingDecl.length > 0) { parts.push( `handler(s) with no schema declaration: ${missingDecl.map((s) => `"${s}"`).join(', ')} ` + `(declare them in defineSchema's \`functions\` group via \`func({ input, output })\`)`, ) } throw new Error(`defineRemoteDomain: functions/schema mismatch — ${parts.join('; ')}.`) } /** * Impl/binding half — `FunctionSchema[]` for `serialize` plus the per-slug * `FunctionBinding` map the worker mounts routes from. `ref` is the canonical * `function.` member ref (matching `compiled.$.refs.functions`), so the * serializer's per-member impl lookup finds it. */ export function buildFunctionSchemas( functions: Record, url: string, functionsFolder: string, ): { schemas: FunctionSchema[]; bindings: Record } { const schemas: FunctionSchema[] = [] const bindings: Record = {} for (const [slug, def] of Object.entries(functions)) { const binding = functionBindingForManifest( resolveBinding(def.binding, url, functionsFolder, slug), def.auth, ) bindings[slug] = binding schemas.push({ ref: `function.${slug}`, inputSchema: zodToJsonSchema(def.inputSchema), outputSchema: zodToJsonSchema(def.outputSchema), output: 'value', binding, }) } return { schemas, bindings } }