@pond-ts/process 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5914 -0
- package/LICENSE +21 -0
- package/README.md +345 -0
- package/dist/cjs-fallback.cjs +15 -0
- package/dist/column.d.ts +197 -0
- package/dist/column.js +306 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +25 -0
- package/dist/graph.d.ts +89 -0
- package/dist/graph.js +133 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +45 -0
- package/dist/node.d.ts +151 -0
- package/dist/node.js +268 -0
- package/dist/plan/builder.d.ts +138 -0
- package/dist/plan/builder.js +166 -0
- package/dist/plan/fluent.d.ts +93 -0
- package/dist/plan/fluent.js +140 -0
- package/dist/plan/folds.d.ts +25 -0
- package/dist/plan/folds.js +190 -0
- package/dist/plan/graph.d.ts +171 -0
- package/dist/plan/graph.js +658 -0
- package/dist/plan/history.d.ts +61 -0
- package/dist/plan/history.js +82 -0
- package/dist/plan/host.d.ts +173 -0
- package/dist/plan/host.js +234 -0
- package/dist/plan/identity.d.ts +81 -0
- package/dist/plan/identity.js +158 -0
- package/dist/plan/params.d.ts +15 -0
- package/dist/plan/params.js +26 -0
- package/dist/plan/registry.d.ts +162 -0
- package/dist/plan/registry.js +422 -0
- package/dist/plan/run.d.ts +211 -0
- package/dist/plan/run.js +360 -0
- package/dist/plan/slots.d.ts +65 -0
- package/dist/plan/slots.js +114 -0
- package/dist/plan/source.d.ts +49 -0
- package/dist/plan/source.js +54 -0
- package/dist/plan/types.d.ts +376 -0
- package/dist/plan/types.js +20 -0
- package/dist/pool/index.d.ts +15 -0
- package/dist/pool/index.js +12 -0
- package/dist/pool/pool.d.ts +92 -0
- package/dist/pool/pool.js +237 -0
- package/dist/pool/protocol.d.ts +48 -0
- package/dist/pool/protocol.js +9 -0
- package/dist/pool/wire.d.ts +52 -0
- package/dist/pool/wire.js +95 -0
- package/dist/pool/worker.d.ts +22 -0
- package/dist/pool/worker.js +80 -0
- package/dist/port.d.ts +79 -0
- package/dist/port.js +222 -0
- package/dist/source.d.ts +161 -0
- package/dist/source.js +182 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +26 -0
- package/package.json +50 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry, and the param vocabulary it is declared in.
|
|
3
|
+
*
|
|
4
|
+
* One declaration, four readers: **param validation**, a **JSON Schema
|
|
5
|
+
* projection** so a tool caller can compose plans, a **UI picker**
|
|
6
|
+
* (family + params + defaults is exactly a grouped menu), and **unit
|
|
7
|
+
* propagation**.
|
|
8
|
+
*
|
|
9
|
+
* The internal param spec is the source of truth and the validator reads
|
|
10
|
+
* it; JSON Schema is a *projection* emitted for callers, not the
|
|
11
|
+
* authority ([PND-PROCREG]). Adopting JSON Schema as the source would
|
|
12
|
+
* mean taking on a schema-validator dependency to do work a dozen lines
|
|
13
|
+
* already do.
|
|
14
|
+
*/
|
|
15
|
+
import { ProcessError } from '../errors.js';
|
|
16
|
+
import { STANDARD_FOLDS } from './folds.js';
|
|
17
|
+
export { int, num, choice, flag } from './params.js';
|
|
18
|
+
import { isFold } from './types.js';
|
|
19
|
+
/** Thrown when a plan names an op the registry does not have. */
|
|
20
|
+
export class UnknownOpError extends ProcessError {
|
|
21
|
+
}
|
|
22
|
+
/** Thrown when a param is missing, mistyped, or out of range. */
|
|
23
|
+
export class ParamError extends ProcessError {
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates one param and returns it.
|
|
27
|
+
*
|
|
28
|
+
* Reports what it actually received, including the type. A caller
|
|
29
|
+
* composing JSON is the audience least able to debug `must be an
|
|
30
|
+
* integer, got 20` when it sent `"20"`.
|
|
31
|
+
*/
|
|
32
|
+
function checkParam(op, key, def, v) {
|
|
33
|
+
const got = `${JSON.stringify(v)} (${typeof v})`;
|
|
34
|
+
if (def.kind === 'integer' || def.kind === 'number') {
|
|
35
|
+
if (typeof v !== 'number' || Number.isNaN(v)) {
|
|
36
|
+
const article = def.kind === 'integer' ? 'an' : 'a';
|
|
37
|
+
throw new ParamError(`${op}.${key} must be ${article} ${def.kind}, got ${got}`);
|
|
38
|
+
}
|
|
39
|
+
if (def.kind === 'integer' && !Number.isInteger(v)) {
|
|
40
|
+
throw new ParamError(`${op}.${key} must be an integer, got ${got}`);
|
|
41
|
+
}
|
|
42
|
+
if (def.min !== undefined && v < def.min) {
|
|
43
|
+
throw new ParamError(`${op}.${key}=${v} is below minimum ${def.min}`);
|
|
44
|
+
}
|
|
45
|
+
if (def.max !== undefined && v > def.max) {
|
|
46
|
+
throw new ParamError(`${op}.${key}=${v} is above maximum ${def.max}`);
|
|
47
|
+
}
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
if (def.kind === 'enum') {
|
|
51
|
+
if (typeof v !== 'string' || !def.of.includes(v)) {
|
|
52
|
+
throw new ParamError(`${op}.${key} must be one of ${def.of.map((o) => `'${o}'`).join(', ')}, got ${got}`);
|
|
53
|
+
}
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
if (typeof v !== 'boolean') {
|
|
57
|
+
throw new ParamError(`${op}.${key} must be a boolean, got ${got}`);
|
|
58
|
+
}
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks a `suggest` range at declaration time, not at call time.
|
|
63
|
+
*
|
|
64
|
+
* A suggestion never rejects a value, so a malformed one would otherwise
|
|
65
|
+
* go unnoticed until a control was drawn backwards. This is a mistake in
|
|
66
|
+
* the op's own source, and the op's author is the one who can fix it —
|
|
67
|
+
* so it fires when the op is defined, in front of them.
|
|
68
|
+
*/
|
|
69
|
+
function checkSuggest(op, key, d) {
|
|
70
|
+
if (d.kind === 'enum' || d.kind === 'boolean' || d.suggest === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const [lo, hi] = d.suggest;
|
|
74
|
+
const bad = lo > hi
|
|
75
|
+
? `is inverted`
|
|
76
|
+
: (d.min !== undefined && lo < d.min) ||
|
|
77
|
+
(d.max !== undefined && hi > d.max)
|
|
78
|
+
? `escapes the legal range [${d.min ?? '-∞'}, ${d.max ?? '∞'}]`
|
|
79
|
+
: undefined;
|
|
80
|
+
if (bad !== undefined) {
|
|
81
|
+
throw new ProcessError(`${op}.${key} suggest [${lo}, ${hi}] ${bad}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class Registry {
|
|
85
|
+
#ops = new Map();
|
|
86
|
+
/**
|
|
87
|
+
* Adds a definition and retains its literal shape in the return type.
|
|
88
|
+
*
|
|
89
|
+
* Runtime callers still validate through this registry. The accumulated
|
|
90
|
+
* type exists for the programmable fluent authoring layer, where it turns
|
|
91
|
+
* op names, params, input roles and output suffixes into compile-time facts.
|
|
92
|
+
*/
|
|
93
|
+
define(def) {
|
|
94
|
+
if (Object.hasOwn(def.params, 'as')) {
|
|
95
|
+
throw new ProcessError(`definition '${def.name}' uses reserved param 'as' — fluent plans use it for the node slot`);
|
|
96
|
+
}
|
|
97
|
+
if (def.inputs.some((input) => input.role === 'as')) {
|
|
98
|
+
throw new ProcessError(`definition '${def.name}' uses reserved input role 'as' — fluent plans use it for the node slot`);
|
|
99
|
+
}
|
|
100
|
+
// Duplicates are checked at definition time because at run time they
|
|
101
|
+
// do not fail — they collapse. Inputs resolve by role, so a repeated
|
|
102
|
+
// role makes every reader see the last input; outputs key node
|
|
103
|
+
// outlets by id, so a repeated id silently discards the earlier
|
|
104
|
+
// column. Both are mistakes in the definition's own source, surfaced
|
|
105
|
+
// in front of the author who can fix them.
|
|
106
|
+
const roles = new Set();
|
|
107
|
+
for (const input of def.inputs) {
|
|
108
|
+
if (roles.has(input.role)) {
|
|
109
|
+
throw new ProcessError(`definition '${def.name}' declares input role '${input.role}' twice`);
|
|
110
|
+
}
|
|
111
|
+
roles.add(input.role);
|
|
112
|
+
}
|
|
113
|
+
if (!isFold(def)) {
|
|
114
|
+
if (def.outputs.length === 0) {
|
|
115
|
+
throw new ProcessError(`op '${def.name}' declares no outputs`);
|
|
116
|
+
}
|
|
117
|
+
if (def.outputs.length > 1 && def.outputs.some((o) => o.id === '')) {
|
|
118
|
+
throw new ProcessError(`op '${def.name}' is multi-output, so every output needs a suffix — '' would collide with the spec id`);
|
|
119
|
+
}
|
|
120
|
+
const outputIds = new Set();
|
|
121
|
+
for (const output of def.outputs) {
|
|
122
|
+
if (outputIds.has(output.id)) {
|
|
123
|
+
throw new ProcessError(`op '${def.name}' declares output '${output.id}' twice`);
|
|
124
|
+
}
|
|
125
|
+
outputIds.add(output.id);
|
|
126
|
+
for (const key of output.dependsOn ?? []) {
|
|
127
|
+
if (!Object.hasOwn(def.params, key)) {
|
|
128
|
+
throw new ProcessError(`op '${def.name}' output '${output.id}' dependsOn unknown param '${key}'`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const [key, d] of Object.entries(def.params)) {
|
|
134
|
+
checkSuggest(def.name, key, d);
|
|
135
|
+
// A bad default fails every spec that omits the param — checked
|
|
136
|
+
// here so it fails the one caller who wrote it instead.
|
|
137
|
+
try {
|
|
138
|
+
checkParam(def.name, key, d, d.default);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
throw new ProcessError(`definition '${def.name}' has an invalid default for '${key}': ${e instanceof Error ? e.message : String(e)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.#ops.set(def.name, def);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
has(name) {
|
|
148
|
+
return this.#ops.has(name);
|
|
149
|
+
}
|
|
150
|
+
/** @throws {UnknownOpError} naming what is available, so an agent can retry. */
|
|
151
|
+
get(name) {
|
|
152
|
+
const op = this.#ops.get(name);
|
|
153
|
+
if (op === undefined) {
|
|
154
|
+
throw new UnknownOpError(`unknown op '${name}' — have ${[...this.#ops.keys()].map((k) => `'${k}'`).join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
return op;
|
|
157
|
+
}
|
|
158
|
+
/** The entry as a fold, or `undefined` if it produces columns. */
|
|
159
|
+
foldFor(name) {
|
|
160
|
+
const def = this.#ops.get(name);
|
|
161
|
+
return def !== undefined && isFold(def) ? def : undefined;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Declared outputs, empty for a fold.
|
|
165
|
+
*
|
|
166
|
+
* Every caller that used to reach for `op.outputs` went through here
|
|
167
|
+
* once folds existed, because a fact has none and the alternative was
|
|
168
|
+
* an optional-chain at each of the nine call sites.
|
|
169
|
+
*/
|
|
170
|
+
outputsOf(def) {
|
|
171
|
+
return isFold(def) ? [] : def.outputs;
|
|
172
|
+
}
|
|
173
|
+
/** Applies defaults, then validates every declared param. */
|
|
174
|
+
resolveParams(op, given = {}) {
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [key, def] of Object.entries(op.params)) {
|
|
177
|
+
const raw = Object.hasOwn(given, key) ? given[key] : def.default;
|
|
178
|
+
out[key] = checkParam(op.name, key, def, raw);
|
|
179
|
+
}
|
|
180
|
+
for (const key of Object.keys(given)) {
|
|
181
|
+
if (!Object.hasOwn(op.params, key)) {
|
|
182
|
+
throw new ParamError(`${op.name} has no param '${key}' — takes ${Object.keys(op.params)
|
|
183
|
+
.map((k) => `'${k}'`)
|
|
184
|
+
.join(', ') || 'none'}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/** Grouped for a picker. */
|
|
190
|
+
byFamily() {
|
|
191
|
+
const out = new Map();
|
|
192
|
+
for (const d of this.describe()) {
|
|
193
|
+
const list = out.get(d.family) ?? [];
|
|
194
|
+
list.push(d);
|
|
195
|
+
out.set(d.family, list);
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
describe() {
|
|
200
|
+
return [...this.#ops.values()].map((op) => ({
|
|
201
|
+
name: op.name,
|
|
202
|
+
family: op.family,
|
|
203
|
+
summary: op.summary,
|
|
204
|
+
params: op.params,
|
|
205
|
+
inputs: op.inputs,
|
|
206
|
+
kind: isFold(op) ? 'fold' : 'op',
|
|
207
|
+
outputs: this.outputsOf(op).map((o) => ({ suffix: o.id, unit: o.unit })),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The tool contract: ops as a discriminated union of param objects.
|
|
212
|
+
*
|
|
213
|
+
* The spec schema is **recursive** — an input is a column name *or*
|
|
214
|
+
* another spec — which is what lets a caller express *EMA of SMA of
|
|
215
|
+
* px* from the schema alone, without being taught a nesting concept.
|
|
216
|
+
* That recursion is the single most load-bearing thing here, and
|
|
217
|
+
* getting it to travel took three attempts.
|
|
218
|
+
*
|
|
219
|
+
* It lives in `$defs`, and the recursion goes through
|
|
220
|
+
* `#/$defs/<name>`. That is the only shape that is actually portable:
|
|
221
|
+
*
|
|
222
|
+
* - `#/items`, the original, dangles the moment the projection is
|
|
223
|
+
* nested inside a larger schema, because a `$ref` resolves against
|
|
224
|
+
* the **document root**. Silently — nothing requires a `$ref` to
|
|
225
|
+
* resolve ([PND-PROCREG], M2).
|
|
226
|
+
* - `#/properties/process/items`, a pointer *into* the host document,
|
|
227
|
+
* fixes that and passes local validators — including OpenAI's own
|
|
228
|
+
* `toStrictJsonSchema` — but the API rejects it: *"reference can
|
|
229
|
+
* only point to definitions defined at the top level of the
|
|
230
|
+
* schema"* ([PND-PROCSCHEMA], M5).
|
|
231
|
+
*
|
|
232
|
+
* So a caller embedding this must lift `$defs` to its own root, where
|
|
233
|
+
* `#/$defs/<name>` resolves from anywhere:
|
|
234
|
+
*
|
|
235
|
+
* ```ts
|
|
236
|
+
* const plan = registry.toJsonSchema({ defs: 'spec' });
|
|
237
|
+
* const { $defs, ...body } = plan;
|
|
238
|
+
* const schema = {
|
|
239
|
+
* type: 'object',
|
|
240
|
+
* $defs, // hoisted to the root
|
|
241
|
+
* properties: { process: body },
|
|
242
|
+
* };
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* `$schema` is emitted only at the root — a nested subschema declaring
|
|
246
|
+
* its own dialect is not what a caller means.
|
|
247
|
+
*
|
|
248
|
+
* Two more things learned by calling a real API rather than reading a
|
|
249
|
+
* spec, both cases where a **client-side** strict validator accepted
|
|
250
|
+
* what the server refused:
|
|
251
|
+
*
|
|
252
|
+
* - Unions are `anyOf`, not `oneOf`. Both branch sets here are
|
|
253
|
+
* disjoint — the op union is discriminated by a `const`, and an
|
|
254
|
+
* input is a string or an object, never both — so they are
|
|
255
|
+
* equivalent in meaning, and `anyOf` is the one tool APIs accept
|
|
256
|
+
* (*"'oneOf' is not permitted"*).
|
|
257
|
+
* - A `const` carries its `type` alongside. Redundant to a validator,
|
|
258
|
+
* and required by the same API (*"schema must have a 'type' key"*).
|
|
259
|
+
*/
|
|
260
|
+
toJsonSchema(options = {}) {
|
|
261
|
+
if (options.shape === 'slots') {
|
|
262
|
+
return slotSchemaFor([...this.#ops.values()]);
|
|
263
|
+
}
|
|
264
|
+
const name = options.defs ?? 'spec';
|
|
265
|
+
const ref = `#/$defs/${name}`;
|
|
266
|
+
return {
|
|
267
|
+
...(options.root !== false && {
|
|
268
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
269
|
+
}),
|
|
270
|
+
title: 'Plan',
|
|
271
|
+
type: 'array',
|
|
272
|
+
items: { $ref: ref },
|
|
273
|
+
$defs: {
|
|
274
|
+
[name]: {
|
|
275
|
+
anyOf: [...this.#ops.values()].map((op) => ({
|
|
276
|
+
title: op.name,
|
|
277
|
+
description: op.summary,
|
|
278
|
+
type: 'object',
|
|
279
|
+
required: ['op', 'inputs'],
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
properties: {
|
|
282
|
+
op: { type: 'string', const: op.name },
|
|
283
|
+
inputs: {
|
|
284
|
+
type: 'array',
|
|
285
|
+
minItems: op.inputs.length,
|
|
286
|
+
maxItems: op.inputs.length,
|
|
287
|
+
items: {
|
|
288
|
+
anyOf: [
|
|
289
|
+
{ type: 'string' },
|
|
290
|
+
{ $ref: ref },
|
|
291
|
+
// The picked-output form — one named output of a
|
|
292
|
+
// nested spec. Without this branch the public
|
|
293
|
+
// `PickedOutput` input was legal to the resolver but
|
|
294
|
+
// inexpressible to a caller composing against the
|
|
295
|
+
// schema, so a band's Lower output was unreachable
|
|
296
|
+
// from exactly the callers the projection exists for.
|
|
297
|
+
{
|
|
298
|
+
title: 'picked output',
|
|
299
|
+
description: 'One named output of a nested spec — e.g. the Lower column of a multi-output op.',
|
|
300
|
+
type: 'object',
|
|
301
|
+
required: ['from', 'output'],
|
|
302
|
+
additionalProperties: false,
|
|
303
|
+
properties: {
|
|
304
|
+
from: { $ref: ref },
|
|
305
|
+
output: { type: 'string' },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
params: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
additionalProperties: false,
|
|
314
|
+
properties: Object.fromEntries(Object.entries(op.params).map(([k, d]) => [
|
|
315
|
+
k,
|
|
316
|
+
jsonSchemaForParam(d),
|
|
317
|
+
])),
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
})),
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The slot projection — [PND-PROCSLOT].
|
|
328
|
+
*
|
|
329
|
+
* Worth noticing what is *not* here. The nested form's single most
|
|
330
|
+
* load-bearing line is a recursive `$ref`, because an input may be
|
|
331
|
+
* another spec — and making that portable took three rounds against a
|
|
332
|
+
* live API: `oneOf` refused, every node needing an explicit `type`, and
|
|
333
|
+
* a body pointer rejected in favour of a top-level `$defs`.
|
|
334
|
+
*
|
|
335
|
+
* With slots an input is a **string** — a column name, or another slot —
|
|
336
|
+
* so the recursion is gone, and every one of those problems with it.
|
|
337
|
+
* Flat, no `$defs`, no `$ref`, nothing to rebase when embedded.
|
|
338
|
+
*
|
|
339
|
+
* `nodes` projects as an **array** rather than an object keyed by slot
|
|
340
|
+
* name: a caller-chosen key cannot be declared in `properties`, and
|
|
341
|
+
* strict structured outputs require `additionalProperties: false`. The
|
|
342
|
+
* array carries the name as a field instead, and the caller keys by it.
|
|
343
|
+
*/
|
|
344
|
+
function slotSchemaFor(ops) {
|
|
345
|
+
return {
|
|
346
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
347
|
+
title: 'Nodes',
|
|
348
|
+
type: 'array',
|
|
349
|
+
items: {
|
|
350
|
+
anyOf: ops.map((op) => ({
|
|
351
|
+
title: op.name,
|
|
352
|
+
description: isFold(op)
|
|
353
|
+
? `${op.summary} Terminal: produces a fact, so nothing can take it as an input — surface it in outputs instead.`
|
|
354
|
+
: op.summary,
|
|
355
|
+
type: 'object',
|
|
356
|
+
required: ['slot', 'op', 'in'],
|
|
357
|
+
additionalProperties: false,
|
|
358
|
+
properties: {
|
|
359
|
+
slot: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'A short name you choose for this node, unique within the request. Used to wire it into other nodes and to name it in outputs. It must not be the name of a source column.',
|
|
362
|
+
},
|
|
363
|
+
op: { type: 'string', const: op.name },
|
|
364
|
+
in: {
|
|
365
|
+
type: 'array',
|
|
366
|
+
minItems: op.inputs.length,
|
|
367
|
+
maxItems: op.inputs.length,
|
|
368
|
+
description: 'Inputs in order. Each is a source column name, or the slot of another node in this request. To read one named output of a multi-output node, write "slot#Output" — e.g. "bb#Upper".',
|
|
369
|
+
items: { type: 'string' },
|
|
370
|
+
},
|
|
371
|
+
params: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
additionalProperties: false,
|
|
374
|
+
properties: Object.fromEntries(Object.entries(op.params).map(([k, d]) => [
|
|
375
|
+
k,
|
|
376
|
+
jsonSchemaForParam(d),
|
|
377
|
+
])),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
})),
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function jsonSchemaForParam(d) {
|
|
385
|
+
if (d.kind === 'enum') {
|
|
386
|
+
return { type: 'string', enum: [...d.of], default: d.default };
|
|
387
|
+
}
|
|
388
|
+
if (d.kind === 'boolean') {
|
|
389
|
+
return { type: 'boolean', default: d.default };
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
type: d.kind === 'integer' ? 'integer' : 'number',
|
|
393
|
+
default: d.default,
|
|
394
|
+
...(d.min !== undefined && { minimum: d.min }),
|
|
395
|
+
...(d.max !== undefined && { maximum: d.max }),
|
|
396
|
+
// Carried as prose, not as an `x-suggest` keyword. A composing model
|
|
397
|
+
// is a reader of this range too — knowing 5000 is legal but absurd is
|
|
398
|
+
// what stops it being picked — and getting three portability rounds
|
|
399
|
+
// out of this projection already ([PND-PROCSCHEMA]) is enough to
|
|
400
|
+
// distrust a custom keyword. `description` every validator allows.
|
|
401
|
+
...(d.suggest !== undefined && {
|
|
402
|
+
description: `Typically ${d.suggest[0]}–${d.suggest[1]}. Values outside that are legal but unusual.`,
|
|
403
|
+
}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* A registry with the standard folds already in it.
|
|
408
|
+
*
|
|
409
|
+
* Pre-registered because `last`, `extremes`, `percentileRank` and `shape`
|
|
410
|
+
* apply to any numeric series and every consumer wants them — not because
|
|
411
|
+
* they are privileged. They are plain defs: `define` over a name to
|
|
412
|
+
* replace one.
|
|
413
|
+
*/
|
|
414
|
+
export function createRegistry(options) {
|
|
415
|
+
const registry = new Registry();
|
|
416
|
+
if (options?.folds !== false) {
|
|
417
|
+
for (const fold of STANDARD_FOLDS)
|
|
418
|
+
registry.define(fold);
|
|
419
|
+
}
|
|
420
|
+
return registry;
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run` — one entry point, one response. [PND-DEMOM0], [PND-PROCTERM].
|
|
3
|
+
*
|
|
4
|
+
* The request carries a plan **and** what it wants back. A renderer asks
|
|
5
|
+
* for columns; an agent asks for facts; a legend chip is a fact riding
|
|
6
|
+
* alongside columns in the same pass. Collapsing the request rather than
|
|
7
|
+
* forking the terminal is what makes that one call.
|
|
8
|
+
*
|
|
9
|
+
* Two things measured earlier are load-bearing here:
|
|
10
|
+
*
|
|
11
|
+
* - **Facts read node values directly.** Assembling a `TimeSeries` so a
|
|
12
|
+
* reduction has a column to read cost 52× more, and 441× once facts
|
|
13
|
+
* memoize. Assembly happens only when `columns` is asked for.
|
|
14
|
+
* - **Assembly resolves a closure.** "Needed" is not "selected with
|
|
15
|
+
* `columns`": a reduction reads a column too, and `crossings`'s
|
|
16
|
+
* `against` names a second one. Assembling only the column-selectors
|
|
17
|
+
* produced a fact with *no value* rather than an error — silent, which
|
|
18
|
+
* is worse than a throw.
|
|
19
|
+
*/
|
|
20
|
+
import type { Column, SeriesSchema, TimeSeries } from 'pond-ts';
|
|
21
|
+
import type { BoundGraph } from './graph.js';
|
|
22
|
+
import { type Slots } from './slots.js';
|
|
23
|
+
import type { Input, Plan, SpecRef } from './types.js';
|
|
24
|
+
/** What to do when a spec or a selector fails. Covers both, not just resolution. */
|
|
25
|
+
export type ErrorPolicy = 'throw' | 'skip' | 'collect';
|
|
26
|
+
/**
|
|
27
|
+
* A selector says **what to surface**, not what to compute.
|
|
28
|
+
*
|
|
29
|
+
* It used to say both: `{ on, reduce: 'percentileRank' }` named a node
|
|
30
|
+
* *and* a fold to run over it, from an enum this file owned. The fold is
|
|
31
|
+
* a node now ([PND-PROCFOLD]), so a selector's whole job is to point at
|
|
32
|
+
* one and name it — and what comes back is whatever that node produces:
|
|
33
|
+
* a fact from a fold, columns from anything else.
|
|
34
|
+
*
|
|
35
|
+
* That is the simplification the change was for. There is no `reduce`,
|
|
36
|
+
* no `points`, and no `columns: true`; asking for a bounded sample means
|
|
37
|
+
* pointing at a `shape` node, which is a thing with an id that caches.
|
|
38
|
+
*/
|
|
39
|
+
export interface Select {
|
|
40
|
+
readonly on: SpecRef;
|
|
41
|
+
/**
|
|
42
|
+
* Which output of a multi-output op. Defaults to all of them.
|
|
43
|
+
*
|
|
44
|
+
* Only meaningful for a column-producing node — a fact has one shape.
|
|
45
|
+
*/
|
|
46
|
+
readonly output?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The caller's own name for this output.
|
|
49
|
+
*
|
|
50
|
+
* Set from the key when a request uses `outputs`. It rides back on the
|
|
51
|
+
* {@link Fact} and {@link OutputInfo} so a consumer reads the name it
|
|
52
|
+
* chose rather than parsing a derived id ([PND-PROCSLOT]).
|
|
53
|
+
*/
|
|
54
|
+
readonly name?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Options common to both request forms. */
|
|
57
|
+
export interface RunOptions {
|
|
58
|
+
readonly onError?: ErrorPolicy;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a `columns` selector also assembles a widened `TimeSeries`.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to `true`, which is right for an **in-process** renderer:
|
|
63
|
+
* the chart layers take a series plus a column name, and assembling
|
|
64
|
+
* once is cheaper than making every consumer do it.
|
|
65
|
+
*
|
|
66
|
+
* Pass `false` when the consumer is across a wire. Assembly there is
|
|
67
|
+
* pure waste — the series cannot be serialized, and the columns can,
|
|
68
|
+
* so the receiving side rebuilds one with `TimeSeries.fromColumns`,
|
|
69
|
+
* which **adopts a `Float64Array` zero-copy**. Measured at 1M rows,
|
|
70
|
+
* `appendColumn` costs 7.6 ms for a gapless column and 22.4 ms for a
|
|
71
|
+
* gapped one — and every rolling study is gapped ([PND-PROCCOL]).
|
|
72
|
+
*/
|
|
73
|
+
readonly assemble?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** A request written as nested specs — the original form. */
|
|
76
|
+
export interface PlanRequest extends RunOptions {
|
|
77
|
+
readonly plan: Plan;
|
|
78
|
+
readonly select?: readonly Select[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A request written as **slots** — [PND-PROCSLOT].
|
|
82
|
+
*
|
|
83
|
+
* `nodes` is keyed by caller-assigned names, and `outputs` is keyed by
|
|
84
|
+
* the caller's name for each surfaced result. Both names ride back on
|
|
85
|
+
* the response, so a consumer keys its UI on a slot that survives a
|
|
86
|
+
* param edit and its cache reasoning on the derived id.
|
|
87
|
+
*
|
|
88
|
+
* Every slot becomes a plan entry, so a slot nothing selects is still
|
|
89
|
+
* reported in {@link RunResult.nodes} — a pipeline view draws the graph
|
|
90
|
+
* that was described, not the subset one selector reached.
|
|
91
|
+
*/
|
|
92
|
+
export interface SlotRequest extends RunOptions {
|
|
93
|
+
readonly nodes: Slots;
|
|
94
|
+
/**
|
|
95
|
+
* Which slots to surface, under the caller's own names.
|
|
96
|
+
*
|
|
97
|
+
* Purely a projection of {@link nodes}: every result here is produced
|
|
98
|
+
* by a node the request declared, so nothing is computed that the plan
|
|
99
|
+
* does not already describe.
|
|
100
|
+
*/
|
|
101
|
+
readonly outputs?: Readonly<Record<string, Select>>;
|
|
102
|
+
}
|
|
103
|
+
export type RunRequest = PlanRequest | SlotRequest;
|
|
104
|
+
export interface OutputInfo {
|
|
105
|
+
readonly column: string;
|
|
106
|
+
readonly unit: string | null;
|
|
107
|
+
/** The caller's name for this output, when it gave one. */
|
|
108
|
+
readonly name?: string;
|
|
109
|
+
}
|
|
110
|
+
export interface Fact {
|
|
111
|
+
readonly id: string;
|
|
112
|
+
/** The caller's name for this output, when it gave one. */
|
|
113
|
+
readonly name?: string;
|
|
114
|
+
/** The fold that produced it — a registry name, not a fixed enum. */
|
|
115
|
+
readonly op: string;
|
|
116
|
+
readonly unit: string | null;
|
|
117
|
+
readonly [k: string]: unknown;
|
|
118
|
+
}
|
|
119
|
+
/** One node's contribution to a request — the per-node badge. */
|
|
120
|
+
export interface NodeTiming {
|
|
121
|
+
readonly id: string;
|
|
122
|
+
/**
|
|
123
|
+
* The caller's name for this node's position, when the request was
|
|
124
|
+
* written with slots. Stable across a param edit, unlike {@link id} —
|
|
125
|
+
* which is the whole point of having both ([PND-PROCSLOT]).
|
|
126
|
+
*/
|
|
127
|
+
readonly slot?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Whether this request actually read the node's value.
|
|
130
|
+
*
|
|
131
|
+
* A plan may resolve specs nothing selects. They are compiled and they
|
|
132
|
+
* are part of the pipeline, but no value was pulled through them, so
|
|
133
|
+
* {@link ms} is zero and says nothing. Reporting only the pulled subset
|
|
134
|
+
* made `nodes` a half-truth — the M4 pipeline view drew a plan with
|
|
135
|
+
* whole branches missing, because the request had not asked for them.
|
|
136
|
+
*/
|
|
137
|
+
readonly pulled: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* False when the value was produced this call.
|
|
140
|
+
*
|
|
141
|
+
* Still meaningful when {@link pulled} is false: a node left clean by
|
|
142
|
+
* an earlier request genuinely holds a cached value, this request just
|
|
143
|
+
* had no reason to read it.
|
|
144
|
+
*/
|
|
145
|
+
readonly cached: boolean;
|
|
146
|
+
/** Milliseconds attributable to this node, to 3 decimal places. Zero when not pulled. */
|
|
147
|
+
readonly ms: number;
|
|
148
|
+
/**
|
|
149
|
+
* Upstream node ids, in the op's declared input order. Raw source
|
|
150
|
+
* columns are named by column, so an entry here is either an id in
|
|
151
|
+
* {@link RunResult.nodes} or a column of the bound series.
|
|
152
|
+
*
|
|
153
|
+
* This is what makes the response a **graph** rather than a list.
|
|
154
|
+
* A caller cannot derive it: the edges live in the specs, and turning
|
|
155
|
+
* a spec into an id means reimplementing `specId`'s canonicalization —
|
|
156
|
+
* which is exactly why a selector takes an inline spec rather than an
|
|
157
|
+
* id string. Added for M4's pipeline view.
|
|
158
|
+
*/
|
|
159
|
+
readonly inputs: readonly string[];
|
|
160
|
+
}
|
|
161
|
+
export interface Skipped {
|
|
162
|
+
/**
|
|
163
|
+
* The spec that failed, echoed back — including `inputs`, because a
|
|
164
|
+
* plan may hold two specs of the same op and a caller retrying needs
|
|
165
|
+
* to know which one it was.
|
|
166
|
+
*/
|
|
167
|
+
readonly spec?: {
|
|
168
|
+
op: string;
|
|
169
|
+
params: Record<string, unknown>;
|
|
170
|
+
inputs: readonly Input[];
|
|
171
|
+
};
|
|
172
|
+
readonly select?: Select;
|
|
173
|
+
readonly reason: string;
|
|
174
|
+
}
|
|
175
|
+
export interface RunResult {
|
|
176
|
+
/** Present only when a `columns` selector asked for it, and `assemble`. */
|
|
177
|
+
readonly series?: TimeSeries<SeriesSchema>;
|
|
178
|
+
/**
|
|
179
|
+
* The resolved columns a `columns` selector asked for, keyed by the
|
|
180
|
+
* name they carry in {@link outputs} — present whenever one did.
|
|
181
|
+
*
|
|
182
|
+
* This is the wire-shaped answer, and the one M3 settled on: a column
|
|
183
|
+
* is a `Float64Array` plus a validity bitmap, so it encodes compactly
|
|
184
|
+
* and the consumer reassembles for free. {@link series} is the
|
|
185
|
+
* in-process convenience over the top of it.
|
|
186
|
+
*/
|
|
187
|
+
readonly columns?: Readonly<Record<string, Column>>;
|
|
188
|
+
readonly outputs: Readonly<Record<string, readonly OutputInfo[]>>;
|
|
189
|
+
readonly facts: readonly Fact[];
|
|
190
|
+
/**
|
|
191
|
+
* Lineage per id, never hand-built. Covers every id in {@link nodes}
|
|
192
|
+
* as well as the plan's own entries, so a caller labelling a node
|
|
193
|
+
* always has a string for it.
|
|
194
|
+
*/
|
|
195
|
+
readonly explain: Readonly<Record<string, string>>;
|
|
196
|
+
readonly skipped: readonly Skipped[];
|
|
197
|
+
/**
|
|
198
|
+
* Every node the plan resolved, in dependency order, each with its
|
|
199
|
+
* upstream ids and whether this request pulled it — and if so, whether
|
|
200
|
+
* it was computed or served warm, and how long it took.
|
|
201
|
+
*
|
|
202
|
+
* Two things at once, and deliberately: it is the demo's explaining
|
|
203
|
+
* device ([PND-DEMOM1]), without which the caching is true but
|
|
204
|
+
* invisible, *and* it is the pipeline's shape ([PND-DEMOM4]), which no
|
|
205
|
+
* caller can derive because turning a spec into an id means
|
|
206
|
+
* reimplementing `specId`.
|
|
207
|
+
*/
|
|
208
|
+
readonly nodes: readonly NodeTiming[];
|
|
209
|
+
}
|
|
210
|
+
export declare function run(graph: BoundGraph, request: RunRequest): RunResult;
|
|
211
|
+
//# sourceMappingURL=run.d.ts.map
|