@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +5914 -0
  2. package/LICENSE +21 -0
  3. package/README.md +345 -0
  4. package/dist/cjs-fallback.cjs +15 -0
  5. package/dist/column.d.ts +197 -0
  6. package/dist/column.js +306 -0
  7. package/dist/errors.d.ts +22 -0
  8. package/dist/errors.js +25 -0
  9. package/dist/graph.d.ts +89 -0
  10. package/dist/graph.js +133 -0
  11. package/dist/index.d.ts +59 -0
  12. package/dist/index.js +45 -0
  13. package/dist/node.d.ts +151 -0
  14. package/dist/node.js +268 -0
  15. package/dist/plan/builder.d.ts +138 -0
  16. package/dist/plan/builder.js +166 -0
  17. package/dist/plan/fluent.d.ts +93 -0
  18. package/dist/plan/fluent.js +140 -0
  19. package/dist/plan/folds.d.ts +25 -0
  20. package/dist/plan/folds.js +190 -0
  21. package/dist/plan/graph.d.ts +171 -0
  22. package/dist/plan/graph.js +658 -0
  23. package/dist/plan/history.d.ts +61 -0
  24. package/dist/plan/history.js +82 -0
  25. package/dist/plan/host.d.ts +173 -0
  26. package/dist/plan/host.js +234 -0
  27. package/dist/plan/identity.d.ts +81 -0
  28. package/dist/plan/identity.js +158 -0
  29. package/dist/plan/params.d.ts +15 -0
  30. package/dist/plan/params.js +26 -0
  31. package/dist/plan/registry.d.ts +162 -0
  32. package/dist/plan/registry.js +422 -0
  33. package/dist/plan/run.d.ts +211 -0
  34. package/dist/plan/run.js +360 -0
  35. package/dist/plan/slots.d.ts +65 -0
  36. package/dist/plan/slots.js +114 -0
  37. package/dist/plan/source.d.ts +49 -0
  38. package/dist/plan/source.js +54 -0
  39. package/dist/plan/types.d.ts +376 -0
  40. package/dist/plan/types.js +20 -0
  41. package/dist/pool/index.d.ts +15 -0
  42. package/dist/pool/index.js +12 -0
  43. package/dist/pool/pool.d.ts +92 -0
  44. package/dist/pool/pool.js +237 -0
  45. package/dist/pool/protocol.d.ts +48 -0
  46. package/dist/pool/protocol.js +9 -0
  47. package/dist/pool/wire.d.ts +52 -0
  48. package/dist/pool/wire.js +95 -0
  49. package/dist/pool/worker.d.ts +22 -0
  50. package/dist/pool/worker.js +80 -0
  51. package/dist/port.d.ts +79 -0
  52. package/dist/port.js +222 -0
  53. package/dist/source.d.ts +161 -0
  54. package/dist/source.js +182 -0
  55. package/dist/types.d.ts +77 -0
  56. package/dist/types.js +26 -0
  57. 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