Adds a definition and retains its literal shape in the return type.
Runtime callers still validate through this registry. The accumulated type exists for the programmable fluent authoring layer, where it turns op names, params, input roles and output suffixes into compile-time facts.
The entry as a fold, or undefined if it produces columns.
Declared outputs, empty for a fold.
Every caller that used to reach for op.outputs went through here
once folds existed, because a fact has none and the alternative was
an optional-chain at each of the nine call sites.
Applies defaults, then validates every declared param.
The tool contract: ops as a discriminated union of param objects.
The spec schema is recursive — an input is a column name or another spec — which is what lets a caller express EMA of SMA of px from the schema alone, without being taught a nesting concept. That recursion is the single most load-bearing thing here, and getting it to travel took three attempts.
It lives in $defs, and the recursion goes through
#/$defs/<name>. That is the only shape that is actually portable:
#/items, the original, dangles the moment the projection is
nested inside a larger schema, because a $ref resolves against
the document root. Silently — nothing requires a $ref to
resolve ([PND-PROCREG], M2).#/properties/process/items, a pointer into the host document,
fixes that and passes local validators — including OpenAI's own
toStrictJsonSchema — but the API rejects it: "reference can
only point to definitions defined at the top level of the
schema" ([PND-PROCSCHEMA], M5).So a caller embedding this must lift $defs to its own root, where
#/$defs/<name> resolves from anywhere:
const plan = registry.toJsonSchema({ defs: 'spec' });
const { $defs, ...body } = plan;
const schema = {
type: 'object',
$defs, // hoisted to the root
properties: { process: body },
};
$schema is emitted only at the root — a nested subschema declaring
its own dialect is not what a caller means.
Two more things learned by calling a real API rather than reading a spec, both cases where a client-side strict validator accepted what the server refused:
anyOf, not oneOf. Both branch sets here are
disjoint — the op union is discriminated by a const, and an
input is a string or an object, never both — so they are
equivalent in meaning, and anyOf is the one tool APIs accept
("'oneOf' is not permitted").const carries its type alongside. Redundant to a validator,
and required by the same API ("schema must have a 'type' key").
Grouped for a picker.