@pond-ts/process 0.68.0 → 0.70.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/AGENTS.md +12 -3
- package/API.md +104 -100
- package/CHANGELOG.md +147 -1
- package/README.md +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plan/graph.d.ts +2 -2
- package/dist/plan/graph.js +3 -5
- package/dist/plan/identity.d.ts +7 -5
- package/dist/plan/identity.js +115 -27
- package/dist/plan/registry.d.ts +24 -0
- package/dist/plan/registry.js +32 -0
- package/dist/plan/run.d.ts +14 -6
- package/dist/plan/run.js +1 -5
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -60,8 +60,9 @@ const s = TimeSeries.fromJSON({
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Time cells accept ms-since-epoch numbers, `Date`s, or ISO strings **with an
|
|
63
|
-
offset** (`…Z`, `…+01:00`). A wall-clock string with no offset
|
|
64
|
-
you pass `parse: { timeZone: 'America/New_York' }
|
|
63
|
+
offset** (`…Z`, `…+01:00`). A wall-clock string with no offset is read as
|
|
64
|
+
**UTC** unless you pass `parse: { timeZone: 'America/New_York' }` — it does
|
|
65
|
+
not throw, it silently shifts by the zone's offset.
|
|
65
66
|
|
|
66
67
|
Other doors: `TimeSeries.fromPoints(points)` for wide `{ ts, a, b }` rows,
|
|
67
68
|
`fromColumns` for struct-of-arrays / `Float64Array`, `fromArrow` for an Arrow
|
|
@@ -214,7 +215,8 @@ them; `import { STUDIES } from '@pond-ts/financial/catalog'` lists them at runti
|
|
|
214
215
|
event); `align` puts rows on a grid without reducing.
|
|
215
216
|
3. **`Sequence.every('1M')` for months.** Not fixed-length → use
|
|
216
217
|
`Sequence.calendar('month', { timeZone })`.
|
|
217
|
-
4. **Wall-clock strings without a zone.** `'2025-01-01T09:00'`
|
|
218
|
+
4. **Wall-clock strings without a zone.** `'2025-01-01T09:00'` is read as
|
|
219
|
+
UTC — no error, every instant shifted by your offset. Add
|
|
218
220
|
`parse: { timeZone }` or use offset strings / ms numbers.
|
|
219
221
|
5. **Unsorted rows.** The constructor throws and names the row; pass
|
|
220
222
|
`sort: true` rather than sorting by hand.
|
|
@@ -227,6 +229,13 @@ them; `import { STUDIES } from '@pond-ts/financial/catalog'` lists them at runti
|
|
|
227
229
|
9. **Reaching for a chart-library adapter first.** If the project uses React,
|
|
228
230
|
`@pond-ts/charts` consumes the series with no adapter; `toPoints()` is the
|
|
229
231
|
bridge for other libraries.
|
|
232
|
+
10. **Aggregating in one zone and charting in another.** `Sequence.calendar`
|
|
233
|
+
defaults to **UTC**; a chart's time axis defaults to the **viewer's**
|
|
234
|
+
zone. Pass the same `timeZone` to both — `Sequence.calendar('day', {
|
|
235
|
+
timeZone })` and `<ChartContainer timeZone={timeZone}>` — and a bucket
|
|
236
|
+
edge and the tick that labels it are one instant. A
|
|
237
|
+
`TradingCalendar.fromRules` carries its zone; `calendar={cal}` renders in
|
|
238
|
+
it with no further wiring.
|
|
230
239
|
|
|
231
240
|
## Where to read next
|
|
232
241
|
|
package/API.md
CHANGED
|
@@ -45,7 +45,7 @@ next door is the point.
|
|
|
45
45
|
| `TimeSeries` | Immutable time-indexed collection, columnar storage | `packages/core/src/batch/time-series.ts` |
|
|
46
46
|
| `ValueSeries` | Series keyed by a monotonic non-time value axis | `packages/core/src/batch/value-series.ts` |
|
|
47
47
|
| `PartitionedTimeSeries` | Scoped view for per-partition stateful transforms; `<S, K, By>` — `By` is the partition column names, carried into `aggregate` / `rolling` result types | `packages/core/src/batch/partitioned-time-series.ts` |
|
|
48
|
-
| `Sequence` | Infinite grid of time buckets (
|
|
48
|
+
| `Sequence` | Infinite grid of time buckets: fixed-step (hourly, every N) or calendar (day/week/month/quarter/year in an IANA zone, default UTC) | `packages/core/src/sequence/sequence.ts` |
|
|
49
49
|
| `BoundedSequence` | Finite ordered list of explicit interval buckets | `packages/core/src/sequence/bounded-sequence.ts` |
|
|
50
50
|
|
|
51
51
|
Static constructors on `TimeSeries`: `fromJSON()` (row tuples/objects),
|
|
@@ -99,13 +99,14 @@ Value-axis wire types
|
|
|
99
99
|
|
|
100
100
|
### Temporal keys & events
|
|
101
101
|
|
|
102
|
-
| Export | Purpose
|
|
103
|
-
| ------------- |
|
|
104
|
-
| `Time` | Point-in-time event key
|
|
105
|
-
| `TimeRange` | Interval event key (start/end)
|
|
106
|
-
| `Interval` | Labeled time-interval event key
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
102
|
+
| Export | Purpose | Source |
|
|
103
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
104
|
+
| `Time` | Point-in-time event key | `packages/core/src/core/time.ts` |
|
|
105
|
+
| `TimeRange` | Interval event key (start/end) | `packages/core/src/core/time-range.ts` |
|
|
106
|
+
| `Interval` | Labeled time-interval event key | `packages/core/src/core/interval.ts` |
|
|
107
|
+
| `TimeZone` | IANA zone as a calendar: `startOf` / `next` / `parts` / `instant` / `offsetAt` / `abbreviation`; interned, transition-cached; what `Sequence.calendar` buckets with | `packages/core/src/core/time-zone.ts` |
|
|
108
|
+
| `Event` | Immutable event: temporal key + typed payload | `packages/core/src/core/event.ts` |
|
|
109
|
+
| `toTimeRange` | Coerce temporal values to `TimeRange` | `packages/core/src/core/time-range.ts` |
|
|
109
110
|
|
|
110
111
|
### TimeSeries methods (all in `packages/core/src/batch/time-series.ts`)
|
|
111
112
|
|
|
@@ -158,14 +159,14 @@ Deliberately small — the ordering-based slice of the algebra, no calendar ops
|
|
|
158
159
|
|
|
159
160
|
### Key exported types (batch)
|
|
160
161
|
|
|
161
|
-
| Type group | Names
|
|
162
|
-
| ----------------- |
|
|
163
|
-
| Schema contract | `SeriesSchema`, `RowForSchema`, `EventForSchema`, `EventDataForSchema`, `EventKeyForSchema`, `TimeSeriesInput`, `TimeSeriesJsonInput`
|
|
164
|
-
| Aggregation specs | `AggregateReducer`, `AggregateMap`, `AggregateOutputMap`, `AggregateSchema`, `BinReducerName`, `BinOutput`
|
|
165
|
-
| Operation schemas | `RollingSchema`, `RollingAlignment`, `AlignSchema`, `DiffSchema`, `SmoothSchema`, `SmoothMethod`, `FillStrategy`, `FillMapping`
|
|
166
|
-
| Column/data kinds | `Column`, `KeyColumn`, `ColumnKind`, `ScalarKind`, `ScalarValue`, `ColumnValue`, `ArrayValue`, `ValidityBitmap`
|
|
167
|
-
| JSON wire format | `JsonRowFormat`, `JsonRowForSchema`, `JsonObjectRowForSchema`, `JsonValueForKind`, `JsonTimestampInput`, `JsonTimeRangeInput`, `JsonIntervalInput`
|
|
168
|
-
| Temporal utility | `TemporalLike`, `DurationInput`, `CalendarUnit`, `TimeZoneOptions`, `KeyLike`, `BatchSampleStrategy`, `SequenceSample`, `SequenceCoverage`
|
|
162
|
+
| Type group | Names | Source |
|
|
163
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
164
|
+
| Schema contract | `SeriesSchema`, `RowForSchema`, `EventForSchema`, `EventDataForSchema`, `EventKeyForSchema`, `TimeSeriesInput`, `TimeSeriesJsonInput` | `packages/core/src/schema/index.ts` |
|
|
165
|
+
| Aggregation specs | `AggregateReducer`, `AggregateMap`, `AggregateOutputMap`, `AggregateSchema`, `BinReducerName`, `BinOutput` | `packages/core/src/schema/index.ts`, `packages/core/src/column.ts` |
|
|
166
|
+
| Operation schemas | `RollingSchema`, `RollingAlignment`, `AlignSchema`, `DiffSchema`, `SmoothSchema`, `SmoothMethod`, `FillStrategy`, `FillMapping` | `packages/core/src/schema/index.ts` |
|
|
167
|
+
| Column/data kinds | `Column`, `KeyColumn`, `ColumnKind`, `ScalarKind`, `ScalarValue`, `ColumnValue`, `ArrayValue`, `ValidityBitmap` | `packages/core/src/columnar/` |
|
|
168
|
+
| JSON wire format | `JsonRowFormat`, `JsonRowForSchema`, `JsonObjectRowForSchema`, `JsonValueForKind`, `JsonTimestampInput`, `JsonTimeRangeInput`, `JsonIntervalInput` | `packages/core/src/schema/index.ts` |
|
|
169
|
+
| Temporal utility | `TemporalLike`, `DurationInput`, `CalendarUnit`, `TimeZoneOptions`, `Disambiguation`, `ZonedParts`, `ZonedPartsInput`, `StartOfOptions`, `KeyLike`, `BatchSampleStrategy`, `SequenceSample`, `SequenceCoverage` | `packages/core/src/core/`, `packages/core/src/sequence/` |
|
|
169
170
|
|
|
170
171
|
The `pond-ts/types` subpath re-exports the schema-as-contract types with zero
|
|
171
172
|
runtime (`packages/core/src/schema/public.ts`).
|
|
@@ -250,17 +251,17 @@ Types: `UseSnapshotOptions`, `SnapshotSource` (structural — covers
|
|
|
250
251
|
|
|
251
252
|
### Components — layout & axes
|
|
252
253
|
|
|
253
|
-
| Component | Key props
|
|
254
|
-
| --------------------------- |
|
|
255
|
-
| `ChartContainer` | `width`, `range?`, `theme?`, `cursor?`, `panZoom?`, `xScale?`, `bounds?`, `showAxis?`, `calendar?`, `origin?`, `maxBandWidth?`/`bandAlign?`, `onTrackerChanged?`, `onDrawStats?` | Root: shared x-scale, interactions, annotations
|
|
256
|
-
| `ChartRow` | `height`, `cursor?` (deprecated — mount a cursor in the row)
|
|
257
|
-
| `Layers` | children
|
|
258
|
-
| `YAxis` | `id` (req), `side?`, `scale?` (`'linear'` \| `'log'` \| `'symlog'`), `linearWindow?`, `min?`/`max?`, `format?`, `width?`, `hide?`
|
|
259
|
-
| `XAxis` | `side?`, `label?`, `format?`, `ticks?`, `transform?`, `dateStyle?`
|
|
260
|
-
| `TimeAxis` / `CategoryAxis` | (XAxis props)
|
|
261
|
-
| `Canvas` | `width`, `height`, `draw`
|
|
262
|
-
| `Selector` | `enabled?` (default `true`), `selected?` (mark \| set), `hovered?`, `onSelect?`, `onHover?`, `children?`
|
|
263
|
-
| `MultiSelector` | `enabled?`, `selected?`, `hovered?`, `sequence?`, `onSelect?`, `onHover?`, `children?`
|
|
254
|
+
| Component | Key props | Purpose | Source |
|
|
255
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
256
|
+
| `ChartContainer` | `width`, `range?`, `theme?`, `cursor?`, `panZoom?`, `xScale?`, `bounds?`, `showAxis?`, `calendar?`, `timeZone?`, `origin?`, `maxBandWidth?`/`bandAlign?`, `onTrackerChanged?`, `onDrawStats?` | Root: shared x-scale, interactions, annotations; `timeZone` renders the time axis in an IANA zone (default: viewer-local; a `calendar.timeZone` supplies the default) | `packages/charts/src/ChartContainer.tsx` |
|
|
257
|
+
| `ChartRow` | `height`, `cursor?` (deprecated — mount a cursor in the row) | One stacked plot band; owns its y-axes | `packages/charts/src/ChartRow.tsx` |
|
|
258
|
+
| `Layers` | children | Mandatory z-stack inside a row (back-to-front) | `packages/charts/src/Layers.tsx` |
|
|
259
|
+
| `YAxis` | `id` (req), `side?`, `scale?` (`'linear'` \| `'log'` \| `'symlog'`), `linearWindow?`, `min?`/`max?`, `format?`, `width?`, `hide?` | Y-axis gutter; layers bind via their `axis` prop | `packages/charts/src/YAxis.tsx` |
|
|
260
|
+
| `XAxis` | `side?`, `label?`, `format?`, `ticks?`, `transform?`, `dateStyle?`, `timeZone?` (this strip in another IANA zone) | Placeable x-axis strip; kind inferred from data | `packages/charts/src/XAxis.tsx` |
|
|
261
|
+
| `TimeAxis` / `CategoryAxis` | (XAxis props) | Thin `XAxis` presets | `packages/charts/src/TimeAxis.tsx`, `CategoryAxis.tsx` |
|
|
262
|
+
| `Canvas` | `width`, `height`, `draw` | Low-level DPR-aware canvas primitive | `packages/charts/src/Canvas.tsx` |
|
|
263
|
+
| `Selector` | `enabled?` (default `true`), `selected?` (mark \| set), `hovered?`, `onSelect?`, `onHover?`, `children?` | Wraps its scope; mounting enables click-select and owns the state it drives (RFC A10) | `packages/charts/src/selectors.tsx` |
|
|
264
|
+
| `MultiSelector` | `enabled?`, `selected?`, `hovered?`, `sequence?`, `onSelect?`, `onHover?`, `children?` | Sweep-select superset of `Selector`: drag sweeps marks, release reports `(hits, modifiers, spans)` — plural, one per swept layer (RFC A5.2) | `packages/charts/src/selectors.tsx` |
|
|
264
265
|
|
|
265
266
|
### Components — draw layers
|
|
266
267
|
|
|
@@ -386,44 +387,47 @@ Series shapes (same file): `ChartSeries`, `BandSeries`, `BoxSeries`,
|
|
|
386
387
|
|
|
387
388
|
### Live values, scales & key types
|
|
388
389
|
|
|
389
|
-
| Export | Purpose
|
|
390
|
-
| ------------------------------------------ |
|
|
391
|
-
| `createLiveValue` / `LiveValue` | Imperative push channel for high-frequency indicator updates (isolated repaint)
|
|
392
|
-
| `scaleTradingTime` / `TradingTimeScale` | Discontinuous time scale collapsing closed-market gaps
|
|
393
|
-
| `DiscontinuityProvider` | Gap topology consumed by the trading-time scale
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
403
|
-
| `
|
|
404
|
-
| `
|
|
405
|
-
| `
|
|
406
|
-
| `
|
|
407
|
-
| `
|
|
408
|
-
| `
|
|
409
|
-
| `
|
|
410
|
-
| `
|
|
411
|
-
| `
|
|
412
|
-
| `
|
|
413
|
-
| `
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
422
|
-
| `
|
|
423
|
-
| `
|
|
424
|
-
| `
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
390
|
+
| Export | Purpose | Source |
|
|
391
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- |
|
|
392
|
+
| `createLiveValue` / `LiveValue` | Imperative push channel for high-frequency indicator updates (isolated repaint) | `packages/charts/src/indicators.tsx` |
|
|
393
|
+
| `scaleTradingTime` / `TradingTimeScale` | Discontinuous time scale collapsing closed-market gaps; `scaleTradingTime(provider, { timeZone })` runs its tick ladder and labels in an IANA zone; `.withTimeZone(zone)` / `.timeZone()` re-derive the same mapping in another zone | `packages/charts/src/tradingTimeScale.ts` |
|
|
394
|
+
| `DiscontinuityProvider` | Gap topology consumed by the trading-time scale; optional `withTimeZone(zone)` for providers whose day anchors move with the zone | `packages/charts/src/tradingTimeScale.ts` |
|
|
395
|
+
| `identityProvider` | The gap-free provider a plain continuous time axis runs on; `identityProvider({ timeZone })` puts its day anchors on that zone's midnights | `packages/charts/src/tradingTimeScale.ts` |
|
|
396
|
+
| `TradingCalendarLike` | Structural shape of a trading calendar `ChartContainer calendar` accepts: `discontinuities({ spacing })` + optional `timeZone` (the axis default) | `packages/charts/src/tradingTimeScale.ts` |
|
|
397
|
+
| `ScaleTimeZoneOptions` | `{ timeZone? }` for `scaleTradingTime` / `identityProvider` | `packages/charts/src/tradingTimeScale.ts` |
|
|
398
|
+
| `scaleBand` / `ScaleBand` | Ordinal slot scale for the category axis | `packages/charts/src/bandScale.ts` |
|
|
399
|
+
| `GapMode` | `'none' \| 'empty' \| 'dashed' \| 'step' \| 'fade'` (Line/Area `gaps` prop) | `packages/charts/src/gaps.ts` |
|
|
400
|
+
| `DecimateOption` | `<LineChart decimate>` — M4 viewport decimation (`bool \| { threshold }`) | `packages/charts/src/decimate.ts` |
|
|
401
|
+
| `CursorMode` | `'none' \| 'line' \| 'point' \| 'inline' \| 'flag' \| 'crosshair' \| 'region'` | `packages/charts/src/context.ts` |
|
|
402
|
+
| `TrackerInfo` / `TrackerSample` | Hover readout payload (`onTrackerChanged`) | `packages/charts/src/context.ts` |
|
|
403
|
+
| `AnnotationKind` / `CreateSpec` | Annotation identity + draw-gesture payload (`onCreate`) | `packages/charts/src/context.ts` |
|
|
404
|
+
| `SelectInfo` | Selection/hover payload (`Selector`/`MultiSelector` `onSelect`/`onHover`) | `packages/charts/src/context.ts` |
|
|
405
|
+
| `SelectModifiers` | Keyboard modifiers on a click, 2nd arg to `onSelect` | `packages/charts/src/context.ts` |
|
|
406
|
+
| `SelectorProps` | `<Selector>`'s props — `enabled?` / `selected?` / `hovered?` / `onSelect?` / `onHover?` / `children?` | `packages/charts/src/selectors.tsx` |
|
|
407
|
+
| `MultiSelectorProps` | `<MultiSelector>`'s props — the above plus `sequence?`, with plural callbacks | `packages/charts/src/selectors.tsx` |
|
|
408
|
+
| `RangeSpan` | `<RangeCursor onDragRelease>` payload — `{ x: [lo, hi], y? }` in axis units | `packages/charts/src/context.ts` |
|
|
409
|
+
| `SpanSelection` | Range entry for `selected` — one layer's marks over `x`/`y`/`rows` (RFC A5.2) | `packages/charts/src/context.ts` |
|
|
410
|
+
| `SelectionEntry` | One `selected` array entry: `SelectInfo \| SpanSelection` | `packages/charts/src/context.ts` |
|
|
411
|
+
| `selectionContains` | Is a hit in a mixed selection? The same membership predicate the layers run | `packages/charts/src/span.ts` |
|
|
412
|
+
| `sameMark` | Are two hits the same mark? Full identity (`id`, `mark`-or-`key`, `label`) | `packages/charts/src/span.ts` |
|
|
413
|
+
| `isSpanSelection` | Entry discriminant — narrows a `SelectionEntry` to `SpanSelection` | `packages/charts/src/span.ts` |
|
|
414
|
+
| `DrawStatsFrame` / `LayerDrawInfo` | Per-repaint draw-cost + decimation stats (`ChartContainer` `onDrawStats`) | `packages/charts/src/context.ts` |
|
|
415
|
+
| `TimeGrain` | Coarse time unit for grain-aware formatting | `packages/charts/src/tickLadder.ts` |
|
|
416
|
+
| `SwatchSpec` / `LegendItemInput` | Legend swatch vocabulary + explicit-rows input (`<Legend items>`) | `packages/charts/src/swatch.ts` |
|
|
417
|
+
| `useChartLegend` | Headless legend hook: rows (items grouped by chart row) + `hover`/`select` verbs | `packages/charts/src/useChartLegend.ts` |
|
|
418
|
+
| `ChartLegend` / `LegendRow` / `LegendItem` | The hook's return shape (`rows` group `items`; items carry `selected`/`hovered`) | `packages/charts/src/useChartLegend.ts` |
|
|
419
|
+
| `useChartFrame` | Resolved plot geometry: plot rect, gutters, x scale, a row's y scales, band slot edges | `packages/charts/src/useChartFrame.ts` |
|
|
420
|
+
| `ChartFrame` / `ChartFrameRow` | The hook's return shape — container x half, plus a row y half that is `null` outside a `<ChartRow>` | `packages/charts/src/useChartFrame.ts` |
|
|
421
|
+
| `ChartBands` / `ChartBand` | Ordinal slot geometry on a category axis (`count`/`pitch`/`labels`/`at(i)`); `null` on time/value | `packages/charts/src/useChartFrame.ts` |
|
|
422
|
+
| `ChartXScale` | The union the container's shared x scale resolves to (time / linear / trading / band / elapsed) | `packages/charts/src/context.ts` |
|
|
423
|
+
| `LegendPlacement` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `packages/charts/src/Legend.tsx` |
|
|
424
|
+
| `Curve` | Path interpolation: `'linear' \| 'monotone' \| 'natural' \| 'basis' \| 'step'` | `packages/charts/src/curve.ts` |
|
|
425
|
+
| `RadiusEncoding` / `ColorEncoding` | Data-driven scatter size/colour | `packages/charts/src/encoding.ts` |
|
|
426
|
+
| `CandleVariant` / `ColorBy` | OHLC mark shape / colouring strategy | `packages/charts/src/ohlc.ts` |
|
|
427
|
+
| `AxisFormat` / `CursorFormat` | Tick and cursor-readout formatting (d3 specifier or fn) | `packages/charts/src/format.ts` |
|
|
428
|
+
| `AxisTransform` | Monotonic `to`/`from` pair for derived-unit x-axis relabeling | `packages/charts/src/derivedTicks.ts` |
|
|
429
|
+
| `AxisMouseEvent` / `AxisMouseHandler` | Axis `onMouseEvent` payload — the mouse event, the axis's `id`, and the value/label under the pointer | `packages/charts/src/axis-events.ts` |
|
|
430
|
+
| `Orientation` | Bar growth direction | `packages/charts/src/bars.ts` |
|
|
427
431
|
|
|
428
432
|
---
|
|
429
433
|
|
|
@@ -618,16 +622,16 @@ erased types. A separate subpath: importing it pulls in every study.
|
|
|
618
622
|
|
|
619
623
|
### Trading calendars & sessions
|
|
620
624
|
|
|
621
|
-
| Export | Purpose
|
|
622
|
-
| ------------------------------------------------------------------------ |
|
|
623
|
-
| `TradingCalendar` |
|
|
624
|
-
| `generateSessions` | `Session[]` from `SessionRules` over a date range (DST-correct)
|
|
625
|
-
| `normalizeSessions` | Validate + sort an explicit session list
|
|
626
|
-
| `identityDiscontinuity` / `segmentDiscontinuity` / `weekendSkip` | `DiscontinuityProvider`s for the trading-time axis
|
|
627
|
-
| Types | `Session`, `SessionBreak`, `SessionRules`, `DateRange`, `InstantRange`, `TaggedSchema`, `LiveSegment`, `DiscontinuityProvider`
|
|
628
|
-
| `SessionSource` / `SessionAnchorOptions` | The session-anchored studies' input: `TradingCalendar \| Session[]` (`sessions`), or a session-id column name (`session`), plus `stamped`
|
|
629
|
-
| `PIVOT_METHODS` / `PivotMethod` | The four pivot formula sets (`standard`, `fibonacci`, `woodie`, `camarilla`)
|
|
630
|
-
| `PivotPointsSchema` / `CamarillaPivotPointsSchema` / `PivotPointsResult` | `pivotPoints`' method-dependent appended-schema types (7 columns, or 9 for Camarilla)
|
|
625
|
+
| Export | Purpose | Source |
|
|
626
|
+
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
627
|
+
| `TradingCalendar` | `timeZone` (the exchange zone: `fromRules`' `rules.timeZone`, or `fromSessions(list, { timeZone })`) + query API: `.sessions()`, `.sessionOn()`, `.isTradingDay()`, `.isOpen()`, `.sessionsInRange()`, `.sessionSequence()`, `.barSequence(period)`, `.tagSessions()`, `.discontinuities()` | `packages/financial/src/calendar/` |
|
|
628
|
+
| `generateSessions` | `Session[]` from `SessionRules` over a date range (DST-correct) | `packages/financial/src/calendar/` |
|
|
629
|
+
| `normalizeSessions` | Validate + sort an explicit session list | `packages/financial/src/calendar/` |
|
|
630
|
+
| `identityDiscontinuity` / `segmentDiscontinuity` / `weekendSkip` | `DiscontinuityProvider`s for the trading-time axis | `packages/financial/src/calendar/` |
|
|
631
|
+
| Types | `Session`, `SessionBreak`, `SessionRules`, `DateRange`, `InstantRange`, `TaggedSchema`, `LiveSegment`, `DiscontinuityProvider` | `packages/financial/src/calendar/` |
|
|
632
|
+
| `SessionSource` / `SessionAnchorOptions` | The session-anchored studies' input: `TradingCalendar \| Session[]` (`sessions`), or a session-id column name (`session`), plus `stamped` | `packages/financial/src/contract/session-anchor.ts` |
|
|
633
|
+
| `PIVOT_METHODS` / `PivotMethod` | The four pivot formula sets (`standard`, `fibonacci`, `woodie`, `camarilla`) | `packages/financial/src/kernels/pivot.ts` |
|
|
634
|
+
| `PivotPointsSchema` / `CamarillaPivotPointsSchema` / `PivotPointsResult` | `pivotPoints`' method-dependent appended-schema types (7 columns, or 9 for Camarilla) | `packages/financial/src/studies/pivot-points.ts` |
|
|
631
635
|
|
|
632
636
|
### Contract & constants
|
|
633
637
|
|
|
@@ -712,31 +716,31 @@ Typed dataflow graphs for pipelines whose **shape is data** (runtime-assembled,
|
|
|
712
716
|
user-edited, one computation fanned out to several consumers). Chaining stays
|
|
713
717
|
the default for pipelines known at authoring time — see the package README.
|
|
714
718
|
|
|
715
|
-
| Group | Exports
|
|
716
|
-
| ----------------------- |
|
|
717
|
-
| Worker pool (Node) | `HostPool` (`start`, `run`, `close`, `size`, `inFlight`); types `HostPoolOptions`, `PoolSetup`, `PoolSetupConfig`; `toWire` / `fromWire`, types `WireResult`, `WireColumn` — subpath `@pond-ts/process/pool`
|
|
718
|
-
| Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`)
|
|
719
|
-
| Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline)
|
|
720
|
-
| Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues`
|
|
721
|
-
| Sources | `source<T>()` → `SourceNode` (`set()`), `fromLive(liveSource)` → `LiveSourceNode` (`dispose()`); `GraphSource` (bind contract — looser than core's `LiveSource`, accepts `LiveAggregation`), `SnapshotSource`, `NoInputs`
|
|
722
|
-
| Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson`
|
|
723
|
-
| Range output buffers | `prepareRange(length, keep, prior)` → `RangeOutput` (`values`, `bits`, `set`, `clear`) carrying `[0, keep)` forward as blocks — values **and** validity; `sealRange(out, length)` → `Float64Column`; `validityByteCount`. Reached from an op as `ctx.out[n]`
|
|
724
|
-
| Ranged recompute | `graph.setSourceFrom(series, changedFrom)` — declares which row first changed; `graph.recomputes` → `{ ranged, full }`. An op opts in with `OpDef.runRange(ctx)`, receiving `{ from, to, previous, previousView, out }` (type `RangeContext`) — write into `out` and return nothing for the block path alongside the usual context. Requires `lookback`. Falls back to a full `run` whenever anything is missing
|
|
725
|
-
| Node budget | `bind(series, { registry, budgetBytes })` — engine-wide cap on retained node values, LRU, enforced after each `run`; `graph.retainedBytes` / `graph.evictions` / `graph.enforceBudget()`. Unbounded when omitted. Skips a node whose consumer still holds its outlet
|
|
726
|
-
| Plan history | `requiredHistory(registry, plan)` → `{ known, rows?, undeclared, byOp }` — the minimum safe tail in rows, folded from per-op `OpDef.lookback`. Sums along nesting, maxes across siblings. `known: false` names ops with no declared lookback rather than defaulting to zero (type `HistoryResult`)
|
|
727
|
-
| Column values | `packColumn` (values → packed `Float64Column`, NaN = missing), `columnBytes` (retained size, for a byte budget), `appendColumn` (column → series; boxing-free when gapless), `columnBuffers` / `columnFromBuffers` (the buffer pair a column is, for an isolate boundary; type `ColumnBuffers`), `columnView` (zero-copy borrowed read view for in-process folds; type `ColumnView`)
|
|
728
|
-
| Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams` (`{ validate: false }` applies defaults and skips every check), `byFamily`, `describe`, `toJsonSchema`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError`
|
|
729
|
-
| Plan — identity | `specId(registry, spec, { validate })` (content-addressed, param-order invariant, defaults materialized; `validate: false` is **total** — names a spec that would not compile in a separate `p1?:` namespace that cannot collide with a valid id; a valid spec's id is identical either way), `SpecIdOptions`, `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey`
|
|
730
|
-
| Plan — types | `Spec`, `Plan`, `Input` (column name \| `Spec` \| `PickedOutput`), `SpecRef`, `Def` (`OpDef` \| `FoldDef`), `OpContext`, `OpResult`, `FoldContext`, `FactBody`, `isFold`, `ParamDef`, `Params`, `Units`, `InputDef`, `OutputDef`
|
|
731
|
-
| Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError`, `UnknownColumnError` (a raw string input naming no column of the bound series, checked over the whole closure and re-checked on the warm path)
|
|
732
|
-
| Plan — request/response | `RunRequest` (`PlanRequest` \| `SlotRequest`), `RunOptions`, `RunResult`, `Select` (`{ on, output?, name? }` — points at a node; what comes back is what that node produces), `ErrorPolicy`, `Fact` (carries `op`), `OutputInfo`, `Skipped` (`spec
|
|
733
|
-
| Plan — host | `createHost({ registry, units, sources })` → `Host` (`add`, `has`, `datasets`, `graphFor`, `run`, `runAsync`), `toWire`, `UnknownDatasetError`; local-string `Envelope` (`PlanEnvelope` \| `SlotEnvelope`), remote-capable `AsyncEnvelope` (`AsyncPlanEnvelope` \| `AsyncSlotEnvelope` \| `Envelope`), `DatasetInfo`, `WireResult`
|
|
734
|
-
| Plan — slots | `expandSlots(slots, columns)` → `Map<slot, Spec>` (expands to the nested form, so ids match by construction; `slot#Output` picks one output), `SlotError`; types `SlotDef` (`{ op, params, in }`), `Slots`
|
|
735
|
-
| Plan — builder | `plan(from)` → low-level `PlanBuilder`; `process(registry, from)` → typed fluent `ProcessBuilder` (`column`, op methods, `outputs`), `BuilderError`; types `NodeHandle`, `OutputHandle`, `FluentColumnRef`, `SingleColumnNode`, `MultiColumnNode`, `ColumnSelection`, `FactRef`, `BuiltRequest`
|
|
736
|
-
| Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef`
|
|
737
|
-
| Plan — folds | `STANDARD_FOLDS` and the four it holds — `last`, `extremes`, `percentileRank`, `shape` — pre-registered by `createRegistry()`; each a plain `FoldDef`, so a consumer can `define` over one
|
|
738
|
-
| Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError`
|
|
739
|
-
| Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput`
|
|
719
|
+
| Group | Exports | Source |
|
|
720
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
721
|
+
| Worker pool (Node) | `HostPool` (`start`, `run`, `close`, `size`, `inFlight`); types `HostPoolOptions`, `PoolSetup`, `PoolSetupConfig`; `toWire` / `fromWire`, types `WireResult`, `WireColumn` — subpath `@pond-ts/process/pool` | `packages/process/src/pool/index.ts` |
|
|
722
|
+
| Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
|
|
723
|
+
| Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
|
|
724
|
+
| Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
|
|
725
|
+
| Sources | `source<T>()` → `SourceNode` (`set()`), `fromLive(liveSource)` → `LiveSourceNode` (`dispose()`); `GraphSource` (bind contract — looser than core's `LiveSource`, accepts `LiveAggregation`), `SnapshotSource`, `NoInputs` | `packages/process/src/source.ts` |
|
|
726
|
+
| Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
|
|
727
|
+
| Range output buffers | `prepareRange(length, keep, prior)` → `RangeOutput` (`values`, `bits`, `set`, `clear`) carrying `[0, keep)` forward as blocks — values **and** validity; `sealRange(out, length)` → `Float64Column`; `validityByteCount`. Reached from an op as `ctx.out[n]` | `packages/process/src/column.ts` |
|
|
728
|
+
| Ranged recompute | `graph.setSourceFrom(series, changedFrom)` — declares which row first changed; `graph.recomputes` → `{ ranged, full }`. An op opts in with `OpDef.runRange(ctx)`, receiving `{ from, to, previous, previousView, out }` (type `RangeContext`) — write into `out` and return nothing for the block path alongside the usual context. Requires `lookback`. Falls back to a full `run` whenever anything is missing | `packages/process/src/plan/graph.ts` |
|
|
729
|
+
| Node budget | `bind(series, { registry, budgetBytes })` — engine-wide cap on retained node values, LRU, enforced after each `run`; `graph.retainedBytes` / `graph.evictions` / `graph.enforceBudget()`. Unbounded when omitted. Skips a node whose consumer still holds its outlet | `packages/process/src/plan/graph.ts` |
|
|
730
|
+
| Plan history | `requiredHistory(registry, plan)` → `{ known, rows?, undeclared, byOp }` — the minimum safe tail in rows, folded from per-op `OpDef.lookback`. Sums along nesting, maxes across siblings. `known: false` names ops with no declared lookback rather than defaulting to zero (type `HistoryResult`) | `packages/process/src/plan/history.ts` |
|
|
731
|
+
| Column values | `packColumn` (values → packed `Float64Column`, NaN = missing), `columnBytes` (retained size, for a byte budget), `appendColumn` (column → series; boxing-free when gapless), `columnBuffers` / `columnFromBuffers` (the buffer pair a column is, for an isolate boundary; type `ColumnBuffers`), `columnView` (zero-copy borrowed read view for in-process folds; type `ColumnView`) | `packages/process/src/column.ts` |
|
|
732
|
+
| Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams` (`{ validate: false }` applies defaults and skips every check), `byFamily`, `describe`, `toJsonSchema`, `checkArity`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError`, `ArityError` | `packages/process/src/plan/registry.ts`, `params.ts` |
|
|
733
|
+
| Plan — identity | `specId(registry, spec, { validate })` (content-addressed, param-order invariant, defaults materialized; `validate: false` is **total over arbitrary JSON** — names a spec that would not compile, including malformed shapes, in a separate `p1?:` namespace that cannot collide with a valid id; a valid spec's id is identical either way. Judges op existence, params **and arity**), `SpecIdOptions`, `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
|
|
734
|
+
| Plan — types | `Spec`, `Plan`, `Input` (column name \| `Spec` \| `PickedOutput`), `SpecRef`, `Def` (`OpDef` \| `FoldDef`), `OpContext`, `OpResult`, `FoldContext`, `FactBody`, `isFold`, `ParamDef`, `Params`, `Units`, `InputDef`, `OutputDef` | `packages/process/src/plan/types.ts` |
|
|
735
|
+
| Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError`, `UnknownColumnError` (a raw string input naming no column of the bound series, checked over the whole closure and re-checked on the warm path) | `packages/process/src/plan/graph.ts`, `run.ts` |
|
|
736
|
+
| Plan — request/response | `RunRequest` (`PlanRequest` \| `SlotRequest`), `RunOptions`, `RunResult`, `Select` (`{ on, output?, name? }` — points at a node; what comes back is what that node produces), `ErrorPolicy`, `Fact` (carries `op`), `OutputInfo`, `Skipped` (`spec` — echoed verbatim, `params`/`inputs` typed `unknown`; `select`, `reason`, `code` — the failure's kind, matching the error class a throw would have carried), `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`) | `packages/process/src/plan/run.ts` |
|
|
737
|
+
| Plan — host | `createHost({ registry, units, sources })` → `Host` (`add`, `has`, `datasets`, `graphFor`, `run`, `runAsync`), `toWire`, `UnknownDatasetError`; local-string `Envelope` (`PlanEnvelope` \| `SlotEnvelope`), remote-capable `AsyncEnvelope` (`AsyncPlanEnvelope` \| `AsyncSlotEnvelope` \| `Envelope`), `DatasetInfo`, `WireResult` | `packages/process/src/plan/host.ts` |
|
|
738
|
+
| Plan — slots | `expandSlots(slots, columns)` → `Map<slot, Spec>` (expands to the nested form, so ids match by construction; `slot#Output` picks one output), `SlotError`; types `SlotDef` (`{ op, params, in }`), `Slots` | `packages/process/src/plan/slots.ts` |
|
|
739
|
+
| Plan — builder | `plan(from)` → low-level `PlanBuilder`; `process(registry, from)` → typed fluent `ProcessBuilder` (`column`, op methods, `outputs`), `BuilderError`; types `NodeHandle`, `OutputHandle`, `FluentColumnRef`, `SingleColumnNode`, `MultiColumnNode`, `ColumnSelection`, `FactRef`, `BuiltRequest` | `packages/process/src/plan/builder.ts`, `fluent.ts` |
|
|
740
|
+
| Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
|
|
741
|
+
| Plan — folds | `STANDARD_FOLDS` and the four it holds — `last`, `extremes`, `percentileRank`, `shape` — pre-registered by `createRegistry()`; each a plain `FoldDef`, so a consumer can `define` over one | `packages/process/src/plan/folds.ts` |
|
|
742
|
+
| Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
|
|
743
|
+
| Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
|
|
740
744
|
|
|
741
745
|
Note: this package's `npm test` includes a `test:dts` step that typechecks the
|
|
742
746
|
**emitted** `dist/*.d.ts` from a consumer's perspective (`test-dts/`,
|
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
|
8
8
|
under a single `v*` tag, so this file covers them all. Pre-1.0: minor bumps may
|
|
9
9
|
include new features and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.70.0...HEAD
|
|
12
|
+
[0.70.0]: https://github.com/pond-ts/pond/compare/v0.69.0...v0.70.0
|
|
13
|
+
[0.69.0]: https://github.com/pond-ts/pond/compare/v0.68.0...v0.69.0
|
|
12
14
|
[0.68.0]: https://github.com/pond-ts/pond/compare/v0.67.0...v0.68.0
|
|
13
15
|
[0.67.0]: https://github.com/pond-ts/pond/compare/v0.66.0...v0.67.0
|
|
14
16
|
[0.66.0]: https://github.com/pond-ts/pond/compare/v0.65.0...v0.66.0
|
|
@@ -71,6 +73,150 @@ include new features and type-level changes; patch bumps are strictly additive.
|
|
|
71
73
|
|
|
72
74
|
## [Unreleased]
|
|
73
75
|
|
|
76
|
+
## [0.70.0] — 2026-09-18
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- `@pond-ts/charts`: **`<BandChart sessionBreaks>`** — the same trading-axis
|
|
81
|
+
session break `<LineChart>` has had since v0.45.0. On a discontinuous
|
|
82
|
+
(`discontinuities` / `calendar`) axis the fill previously ran a near-vertical
|
|
83
|
+
sliver from one session's last sample to the next session's first, because
|
|
84
|
+
the collapsed overnight gap put them a pixel apart; `sessionBreaks` ends the
|
|
85
|
+
envelope at the close and re-starts it at the open, so a band and its centre
|
|
86
|
+
line break in step. A **scale** break, orthogonal to the NaN **data** gaps a
|
|
87
|
+
band always breaks at; default `false` (unchanged output). Decimation
|
|
88
|
+
composes: `decimateBand` folds each break instant into the pixel-column
|
|
89
|
+
edges and bakes a `NaN` sample at it, so no column merges two sessions'
|
|
90
|
+
envelopes. Stories `Axes/TradingTimeAxis / SessionBreaksBand` and
|
|
91
|
+
`Performance/Decimation / TradingSessionBreaksBand`.
|
|
92
|
+
- `@pond-ts/process`: **`ArityError`**, and **arity is now part of what `specId`
|
|
93
|
+
can judge**. A spec whose `inputs` count does not match the op's — including
|
|
94
|
+
one carrying no `inputs` at all — was named `p1:sma(;period=20)`, a _valid_
|
|
95
|
+
id for something that cannot compile, and then died at `compile` as a bare
|
|
96
|
+
`TypeError` reading `.length` of `undefined`, reaching the consumer as a
|
|
97
|
+
`Skipped` with **no `code`** (which under that contract means "op code
|
|
98
|
+
threw"). Arity is decidable from the registry alone, so strict `specId` now
|
|
99
|
+
raises `ArityError` and lenient mode marks the id `p1?:`.
|
|
100
|
+
`Registry.checkArity(op, inputs)` is the shared check `compile` uses too.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- `@pond-ts/financial`: **`StudyOutput.id` documents the `@pond-ts/process`
|
|
105
|
+
bridge**, and a new `test/catalog-process.test.ts` pins it. The field is a
|
|
106
|
+
*financial column suffix*; process's `OutputDef.id` is a *process outlet
|
|
107
|
+
id*; they share a name and are different namespaces. Process names its own
|
|
108
|
+
columns (`specId + OutputDef.id`) and matches an op's return to its outputs
|
|
109
|
+
positionally, so a study's own column names never reach it — which means a
|
|
110
|
+
registry bridging the two chooses its own suffixes, and for the **twelve**
|
|
111
|
+
multi-output studies that claim the bare prefix (`trix`, `superTrend`,
|
|
112
|
+
`klinger`, …) it must: process rejects `''` on a multi-output op, because
|
|
113
|
+
there the column would be named exactly the spec id, itself a legal column
|
|
114
|
+
reference. The map is `outputs.length > 1 && id === ''` → `'value'` — not
|
|
115
|
+
`id === ''`, which would rename all seventy-odd single-output columns for
|
|
116
|
+
nothing. Reported by a consumer that hit the throw at module load and
|
|
117
|
+
worked around it by hand. No API change: the descriptors, the studies and
|
|
118
|
+
the guard are all unchanged, and the round-trip test is what stops the two
|
|
119
|
+
packages drifting — `catalog.test.ts` validates a descriptor against its
|
|
120
|
+
*study*, so it is structurally blind to a cross-package disagreement.
|
|
121
|
+
- `@pond-ts/process`: **`Skipped.spec` echoes the request verbatim**, and its
|
|
122
|
+
`params` / `inputs` are typed `unknown` accordingly. The plan pass normalized
|
|
123
|
+
`params: null` to `{}`, so recomputing an id from the echo produced the
|
|
124
|
+
_defaulted spec's valid id_ — keying a broken persisted entry's report onto a
|
|
125
|
+
legitimate node. The selector pass echoed the original all along, so the two
|
|
126
|
+
passes disagreed. **Migration:** a consumer reading `entry.spec.params` now
|
|
127
|
+
narrows it (`entry.spec.params as Record<string, unknown>`, or a guard) —
|
|
128
|
+
which is the point, since the value may be exactly the malformed thing that
|
|
129
|
+
failed.
|
|
130
|
+
- `@pond-ts/process`: `specId(…, { validate: false })` is **total over
|
|
131
|
+
arbitrary JSON**, not just over well-typed specs. `params: null`, a
|
|
132
|
+
non-array `inputs`, and an input entry that is neither a column name nor a
|
|
133
|
+
spec each used to raise a `TypeError`; they are now named in the `p1?:`
|
|
134
|
+
namespace, distinctly enough that two differently-broken specs stay two ids.
|
|
135
|
+
Strict mode reports them as `ParamError` / `ArityError` / `ProcessError`
|
|
136
|
+
rather than crashing.
|
|
137
|
+
|
|
138
|
+
(All three reported by Tidal against 0.62.0, after adopting it.)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## [0.69.0] — 2026-09-13
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
|
|
145
|
+
- **`<ChartContainer timeZone>` — the time axis in any IANA zone
|
|
146
|
+
([PND-TZAXIS]).** Day / week / month ticks land on that zone's midnights,
|
|
147
|
+
Mondays and month starts; labels, the stacked date bands, the hierarchical
|
|
148
|
+
grid, session dividers and every cursor / marker / annotation readout read
|
|
149
|
+
in it. **Omitted ⇒ the viewer's zone**, exactly as before. The d3 specifier
|
|
150
|
+
strings on `timeFormat` / `cursorFormat` are unchanged; `%Z` / `%z` now read
|
|
151
|
+
the zone's abbreviation / offset. Sub-day ticks align to the zone's wall
|
|
152
|
+
clock, so a 6 h grain reads 00 / 06 / 12 / 18 across a DST jump instead of
|
|
153
|
+
drifting by an hour until the next midnight. The resolved zone is on the
|
|
154
|
+
chart context as `timeZone`. Built on core's `TimeZone` ([PND-TZCAL]), so a
|
|
155
|
+
`Sequence.calendar('day', { timeZone })` bucket edge and the tick that
|
|
156
|
+
labels it are one instant — pinned by a cross-package test.
|
|
157
|
+
- **`<XAxis timeZone>` — a second strip in another zone.** Two time axes
|
|
158
|
+
over one shared mapping, each ticking and labelling (and pilling) in its
|
|
159
|
+
own zone: `<XAxis side="top" timeZone="America/New_York" />` above a
|
|
160
|
+
UTC container's own strip below. Backed by
|
|
161
|
+
`TradingTimeScale.withTimeZone(zone)` / `.timeZone()` and an optional
|
|
162
|
+
`DiscontinuityProvider.withTimeZone` (the identity provider re-derives its
|
|
163
|
+
day anchors; a trading calendar's session opens are zone-independent).
|
|
164
|
+
- **`TradingCalendarLike.timeZone?`** — a calendar that carries its exchange
|
|
165
|
+
zone supplies the axis default (`calendar={cal}` renders in exchange time
|
|
166
|
+
wherever it is viewed); an explicit `timeZone` prop wins.
|
|
167
|
+
- `scaleTradingTime(provider, { timeZone })` and
|
|
168
|
+
`identityProvider({ timeZone })` take the zone directly for consumers
|
|
169
|
+
building the scale themselves; `identityProvider`, `TradingCalendarLike`
|
|
170
|
+
and `ScaleTimeZoneOptions` are now exported. Internally the tick ladder
|
|
171
|
+
runs on a `TickCalendar` seam whose local implementation is the previous
|
|
172
|
+
`Date` arithmetic verbatim — the default path is unchanged.
|
|
173
|
+
- **`TradingCalendar.timeZone` ([PND-TZFIN]).** `@pond-ts/financial`'s
|
|
174
|
+
calendar keeps the zone its sessions were resolved in — `fromRules` carries
|
|
175
|
+
`rules.timeZone`, `fromSessions(list, { timeZone })` takes it — so
|
|
176
|
+
`<ChartContainer calendar={cal}>` renders the axis in exchange time with no
|
|
177
|
+
further wiring.
|
|
178
|
+
- `@pond-ts/charts` now depends on `d3-time-format` directly (it was already
|
|
179
|
+
a transitive dependency via `d3-scale`).
|
|
180
|
+
- **`TimeZone` — the zone-calendar primitive ([PND-TZCAL]).** `pond-ts`
|
|
181
|
+
exports `TimeZone.of(id)` (interned; also `TimeZone.UTC`,
|
|
182
|
+
`TimeZone.local()`) with `startOf(unit, t)`, `next(unit, t)`, `parts(t)`,
|
|
183
|
+
`instant(parts, { disambiguation })`, `offsetAt(t)` and
|
|
184
|
+
`abbreviation(t, { locale })`. Temporal underneath, but each zone caches its
|
|
185
|
+
offset transitions as it discovers them, so steady-state calls are integer
|
|
186
|
+
arithmetic: `startOf('day')` went from ~24 µs to ~23 ns per call, and a
|
|
187
|
+
three-year hourly series aggregated to `America/New_York` days from 38 ms
|
|
188
|
+
to 0.5 ms. `Sequence.calendar`, `TimeRange.fromCalendar` and
|
|
189
|
+
`Interval.fromCalendar` now bucket through it (no behaviour change; pinned
|
|
190
|
+
against Temporal on eight zones including southern-hemisphere DST, a
|
|
191
|
+
30-minute DST shift, a +05:30 zone, a day with no midnight and Samoa's
|
|
192
|
+
skipped day). This is the primitive the charts' time axis will place and
|
|
193
|
+
label ticks with, so a bucket edge and the tick that labels it are one
|
|
194
|
+
instant. First task of the time-zone plan
|
|
195
|
+
(`docs/plans/PND_TIMEZONE_PLAN.md`).
|
|
196
|
+
- **`CalendarUnit` gains `'quarter'` and `'year'`** for
|
|
197
|
+
`Sequence.calendar`, `TimeRange.fromCalendar` and `Interval.fromCalendar`.
|
|
198
|
+
|
|
199
|
+
### Changed
|
|
200
|
+
|
|
201
|
+
- **`Sequence.calendar` validates its inputs at construction.** An unknown
|
|
202
|
+
unit (`'hour'`) or zone (`'Nowhere'`) now throws `RangeError` immediately;
|
|
203
|
+
before, an unknown unit silently produced wrong buckets (the two unit
|
|
204
|
+
dispatchers fell through to different defaults — the 2026-06 audit's §6
|
|
205
|
+
finding) and an unknown zone failed only on first `bounded()`.
|
|
206
|
+
|
|
207
|
+
### Fixed
|
|
208
|
+
|
|
209
|
+
- **`pond-ts`: two type-level corners of [PND-PARTCOL] (0.68.0) found by the Codex pass on #724.** (1) On a broad `TimeSeries<SeriesSchema>` with a _literal_ partition column, the injected `'first'` could not look up a kind and typed the column as `undefined`; `WithPartitionColumns` now takes the schema and leaves the mapping alone when the schema is broad, so the result type is exactly 0.67's. (2) `By` had no variance pin, so `PartitionedTimeSeries<S, K, 'host'>` accepted a view partitioned by `region` (and an untyped view could be narrowed to any column); a phantom contravariant member now rejects both while a specialised view still assigns to the legacy `PartitionedTimeSeries<S>` shape. Type tests cover both plus the `K`-survives-`smooth`/`baseline` claim. No runtime change.
|
|
210
|
+
- **Docs said a wall-clock string without `parse.timeZone` throws. It never
|
|
211
|
+
did** ([PND-TZDOCS]) — it is read as UTC, silently. `creating.mdx`, the
|
|
212
|
+
agent guide (`AGENTS.md`) and the decision table now say so and describe
|
|
213
|
+
how the shift shows up. The agent guide also gains the one time-zone rule:
|
|
214
|
+
pass the same `timeZone` to `Sequence.calendar` and `<ChartContainer>`.
|
|
215
|
+
The aggregation page cross-links `Sequence.calendar` for weekly / monthly
|
|
216
|
+
bars (issue #358 item 1, supersedes #359). The finance gallery's off-chart
|
|
217
|
+
readout takes the calendar's zone instead of hard-coding New York; the
|
|
218
|
+
Niño 3.4 heat map's year grain uses `Sequence.calendar('year')`.
|
|
219
|
+
|
|
74
220
|
## [0.68.0] — 2026-09-13
|
|
75
221
|
|
|
76
222
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @pond-ts/process
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@pond-ts/process)
|
|
4
|
+
[](https://github.com/pond-ts/pond/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pond-ts.org/docs/process/)
|
|
6
|
+
|
|
3
7
|
> **Experimental.** Pre-1.0, and the API is expected to move as friction
|
|
4
8
|
> reports land — pin an exact version. The design iterates in the open
|
|
5
9
|
> against
|
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export { Graph } from './graph.js';
|
|
|
36
36
|
export type { GraphEdge, GraphJson, GraphNodeJson, GraphEdgeJson, } from './graph.js';
|
|
37
37
|
export { ProcessError, CycleError, UnconnectedInputError, MissingOutputError, } from './errors.js';
|
|
38
38
|
export { createRegistry, Registry, int, num, choice, flag, } from './plan/registry.js';
|
|
39
|
-
export { UnknownOpError, ParamError } from './plan/registry.js';
|
|
39
|
+
export { UnknownOpError, ParamError, ArityError } from './plan/registry.js';
|
|
40
40
|
export type { DefMap } from './plan/registry.js';
|
|
41
41
|
export { isFold } from './plan/types.js';
|
|
42
42
|
export { STANDARD_FOLDS, last, extremes, percentileRank, shape, } from './plan/folds.js';
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ export { Graph } from './graph.js';
|
|
|
31
31
|
export { ProcessError, CycleError, UnconnectedInputError, MissingOutputError, } from './errors.js';
|
|
32
32
|
// ─── Plan layer ([PND-DEMOM0]) ──────────────────────────────────
|
|
33
33
|
export { createRegistry, Registry, int, num, choice, flag, } from './plan/registry.js';
|
|
34
|
-
export { UnknownOpError, ParamError } from './plan/registry.js';
|
|
34
|
+
export { UnknownOpError, ParamError, ArityError } from './plan/registry.js';
|
|
35
35
|
export { isFold } from './plan/types.js';
|
|
36
36
|
export { STANDARD_FOLDS, last, extremes, percentileRank, shape, } from './plan/folds.js';
|
|
37
37
|
export { specId, refToId, explain, unitOf, columnsOf, dependsOn, outputKey, } from './plan/identity.js';
|
package/dist/plan/graph.d.ts
CHANGED
|
@@ -142,8 +142,8 @@ export declare class BoundGraph {
|
|
|
142
142
|
* one, nothing is ever evicted.
|
|
143
143
|
*
|
|
144
144
|
* Validation happens here rather than at pull time so a bad plan is
|
|
145
|
-
* rejected before any work:
|
|
146
|
-
* input check.
|
|
145
|
+
* rejected before any work: the op must exist, then arity, then params
|
|
146
|
+
* (all three via strict `specId`), then the typed input check.
|
|
147
147
|
*/
|
|
148
148
|
compile(spec: Spec): Compiled;
|
|
149
149
|
/** Reads one output column of a compiled spec, by output suffix. */
|
package/dist/plan/graph.js
CHANGED
|
@@ -381,8 +381,8 @@ export class BoundGraph {
|
|
|
381
381
|
* one, nothing is ever evicted.
|
|
382
382
|
*
|
|
383
383
|
* Validation happens here rather than at pull time so a bad plan is
|
|
384
|
-
* rejected before any work:
|
|
385
|
-
* input check.
|
|
384
|
+
* rejected before any work: the op must exist, then arity, then params
|
|
385
|
+
* (all three via strict `specId`), then the typed input check.
|
|
386
386
|
*/
|
|
387
387
|
compile(spec) {
|
|
388
388
|
const id = specId(this.registry, spec);
|
|
@@ -413,9 +413,7 @@ export class BoundGraph {
|
|
|
413
413
|
}
|
|
414
414
|
const op = this.registry.get(spec.op);
|
|
415
415
|
const params = this.registry.resolveParams(op, spec.params);
|
|
416
|
-
|
|
417
|
-
throw new ProcessError(`${spec.op} takes ${op.inputs.length} input(s), got ${spec.inputs.length}`);
|
|
418
|
-
}
|
|
416
|
+
this.registry.checkArity(op, spec.inputs);
|
|
419
417
|
// After arity — an input index past the declared list is an arity
|
|
420
418
|
// problem, not a column one — and before the typed-unit pass, whose
|
|
421
419
|
// answer for an absent column is a misleading 'unitless'.
|
package/dist/plan/identity.d.ts
CHANGED
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Both are pinned by tests.
|
|
36
36
|
*/
|
|
37
|
-
import type
|
|
37
|
+
import { type Registry } from './registry.js';
|
|
38
38
|
import type { Params, Spec, SpecRef, Units } from './types.js';
|
|
39
39
|
/** Options for {@link specId}. */
|
|
40
40
|
export interface SpecIdOptions {
|
|
41
41
|
/**
|
|
42
|
-
* Whether the op must exist
|
|
43
|
-
* `true`.
|
|
42
|
+
* Whether the op must exist, its params must be legal, and its
|
|
43
|
+
* `inputs` count must match the op's arity — default `true`.
|
|
44
44
|
*
|
|
45
45
|
* Pass `false` to name a spec that would not compile. See
|
|
46
46
|
* {@link specId} for why identity is separable from validity.
|
|
@@ -70,8 +70,10 @@ export interface SpecIdOptions {
|
|
|
70
70
|
*
|
|
71
71
|
* So `specId(registry, spec, { validate: false })` is **total**: an
|
|
72
72
|
* unknown op keeps its given params verbatim, a known one still gets
|
|
73
|
-
* its defaults applied and its keys sorted, and nothing throws
|
|
74
|
-
*
|
|
73
|
+
* its defaults applied and its keys sorted, and nothing throws — a
|
|
74
|
+
* malformed shape is **named** (marked `p1?:`), not rejected. Validity
|
|
75
|
+
* stays `compile`'s job, with one exception decidable from the registry
|
|
76
|
+
* alone: arity, which strict mode judges here too (`ArityError`).
|
|
75
77
|
*
|
|
76
78
|
* **A valid spec has one id under either mode.** Canonicalization is
|
|
77
79
|
* the same code path and `checkParam` never coerces, so the lenient id
|
package/dist/plan/identity.js
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Both are pinned by tests.
|
|
36
36
|
*/
|
|
37
|
+
import { ProcessError } from '../errors.js';
|
|
38
|
+
import { ParamError } from './registry.js';
|
|
37
39
|
import { isFold, isPicked, specOf } from './types.js';
|
|
38
40
|
/** Id format version. Bumping it invalidates persisted ids deliberately. */
|
|
39
41
|
const VERSION = 'p1';
|
|
@@ -51,6 +53,14 @@ function esc(v) {
|
|
|
51
53
|
* after the version and can never equal one, whatever its params say.
|
|
52
54
|
*/
|
|
53
55
|
const UNVALIDATED = '?';
|
|
56
|
+
/**
|
|
57
|
+
* Key under which a malformed `params` or `inputs` value is recorded in
|
|
58
|
+
* an unvalidated id, so the shape it actually had survives.
|
|
59
|
+
*
|
|
60
|
+
* Only ever emitted inside a `p1?:` id, so it cannot be confused with a
|
|
61
|
+
* declared param — and it is escaped like any other key there.
|
|
62
|
+
*/
|
|
63
|
+
const MALFORMED = '!malformed';
|
|
54
64
|
/**
|
|
55
65
|
* Type-preserving encoding, used **only** inside an unvalidated id.
|
|
56
66
|
*
|
|
@@ -93,8 +103,10 @@ function typedEsc(v) {
|
|
|
93
103
|
*
|
|
94
104
|
* So `specId(registry, spec, { validate: false })` is **total**: an
|
|
95
105
|
* unknown op keeps its given params verbatim, a known one still gets
|
|
96
|
-
* its defaults applied and its keys sorted, and nothing throws
|
|
97
|
-
*
|
|
106
|
+
* its defaults applied and its keys sorted, and nothing throws — a
|
|
107
|
+
* malformed shape is **named** (marked `p1?:`), not rejected. Validity
|
|
108
|
+
* stays `compile`'s job, with one exception decidable from the registry
|
|
109
|
+
* alone: arity, which strict mode judges here too (`ArityError`).
|
|
98
110
|
*
|
|
99
111
|
* **A valid spec has one id under either mode.** Canonicalization is
|
|
100
112
|
* the same code path and `checkParam` never coerces, so the lenient id
|
|
@@ -116,53 +128,129 @@ function typedEsc(v) {
|
|
|
116
128
|
export function specId(registry, spec, options = {}) {
|
|
117
129
|
return build(registry, spec, options.validate === false).id;
|
|
118
130
|
}
|
|
119
|
-
/**
|
|
131
|
+
/** True for an object literal a spec's `params` could legally be. */
|
|
132
|
+
function isParamBag(v) {
|
|
133
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
134
|
+
}
|
|
135
|
+
/** True for an input that is a nested spec or a picked output. */
|
|
136
|
+
function isSpecLike(v) {
|
|
137
|
+
return (typeof v === 'object' &&
|
|
138
|
+
v !== null &&
|
|
139
|
+
('op' in v || 'from' in v) &&
|
|
140
|
+
!Array.isArray(v));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* An id, and whether anything in its closure failed validation.
|
|
144
|
+
*
|
|
145
|
+
* **Totality is over arbitrary JSON, not over well-typed `Spec`s.** The
|
|
146
|
+
* whole reason a consumer reaches for leniency is a persisted object
|
|
147
|
+
* that no longer fits — a dropped `inputs` key, a `null` where a param
|
|
148
|
+
* bag belongs, an input that came back as a bare `null`. Answering those
|
|
149
|
+
* with a `TypeError` is the same failure as throwing `ParamError`, one
|
|
150
|
+
* layer down, so each malformed shape is **named** here rather than
|
|
151
|
+
* crashed on: marked unvalidated, and encoded distinctly enough that two
|
|
152
|
+
* differently-broken specs stay two ids (Tidal, on 0.62.0).
|
|
153
|
+
*/
|
|
120
154
|
function build(registry, spec, lenient) {
|
|
121
155
|
let unvalidated = false;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
const bad = () => {
|
|
157
|
+
unvalidated = true;
|
|
158
|
+
};
|
|
159
|
+
// The op first, because arity needs it. An unknown op under leniency
|
|
160
|
+
// leaves `op` undefined and nothing further is decidable about params
|
|
161
|
+
// or arity — both are declared BY the definition.
|
|
162
|
+
const op = lenient && !registry.has(spec.op) ? undefined : registry.get(spec.op);
|
|
163
|
+
if (op === undefined)
|
|
164
|
+
bad();
|
|
165
|
+
// Arity is part of validity, and decidable from the registry alone —
|
|
166
|
+
// so a spec that fails it must not be named in the valid namespace.
|
|
167
|
+
// It used to be checked only at `compile`, which meant `p1:sma(;…)`
|
|
168
|
+
// named a spec that could not exist, and then `compile` read `.length`
|
|
169
|
+
// off `undefined` and raised a codeless `TypeError` (Tidal, 0.62.0).
|
|
170
|
+
if (op !== undefined) {
|
|
171
|
+
try {
|
|
172
|
+
registry.checkArity(op, spec.inputs);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
if (!lenient)
|
|
176
|
+
throw e;
|
|
177
|
+
bad();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Inputs next: a nested spec that did not validate marks this one, and
|
|
181
|
+
// the mark has to be known before the params are encoded.
|
|
182
|
+
const rawInputs = Array.isArray(spec.inputs)
|
|
183
|
+
? spec.inputs
|
|
184
|
+
: spec.inputs === undefined
|
|
185
|
+
? // The key was dropped. Reads as an empty input list, which is
|
|
186
|
+
// what it is — the arity check above has already marked it.
|
|
187
|
+
(bad(), [])
|
|
188
|
+
: // Present but not a list. Encoded as one token so the shape it
|
|
189
|
+
// actually had survives rather than being flattened to "empty".
|
|
190
|
+
(bad(), [{ [MALFORMED]: spec.inputs }]);
|
|
191
|
+
const inputs = rawInputs
|
|
130
192
|
.map((i) => {
|
|
131
193
|
if (typeof i === 'string')
|
|
132
194
|
return esc(i);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
unvalidated
|
|
139
|
-
|
|
195
|
+
if (isSpecLike(i)) {
|
|
196
|
+
// `#Lower` rather than a separate field: an input picking a
|
|
197
|
+
// different output is a different computation, and the id is
|
|
198
|
+
// what says so.
|
|
199
|
+
const base = build(registry, specOf(i), lenient);
|
|
200
|
+
if (base.unvalidated)
|
|
201
|
+
unvalidated = true;
|
|
202
|
+
return isPicked(i)
|
|
203
|
+
? `${base.id}#${esc(i.output)}`
|
|
204
|
+
: base.id;
|
|
205
|
+
}
|
|
206
|
+
// Neither a column name nor a spec — `null`, a number, an array.
|
|
207
|
+
if (!lenient) {
|
|
208
|
+
throw new ProcessError(`input of '${spec.op}' must be a column name or a spec, got ${JSON.stringify(i) ?? typeof i}`);
|
|
209
|
+
}
|
|
210
|
+
bad();
|
|
211
|
+
// A non-array `inputs` was wrapped above so its shape survives; keep
|
|
212
|
+
// the marker in the token, or `{ inputs: null }` and `{ inputs: [null] }`
|
|
213
|
+
// would mint the same id — two differently-broken specs, one chip.
|
|
214
|
+
return isParamBag(i) && MALFORMED in i
|
|
215
|
+
? esc(`${MALFORMED}:${typeof i[MALFORMED]}:${String(i[MALFORMED])}`)
|
|
216
|
+
: typedEsc(i);
|
|
140
217
|
})
|
|
141
218
|
.join('+');
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
219
|
+
// Params last, so the mark is settled before they are encoded.
|
|
220
|
+
let entries;
|
|
221
|
+
if (op === undefined) {
|
|
222
|
+
entries = Object.entries(isParamBag(spec.params) ? spec.params : {});
|
|
223
|
+
if (spec.params !== undefined && !isParamBag(spec.params)) {
|
|
224
|
+
bad();
|
|
225
|
+
entries = [[MALFORMED, spec.params]];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (spec.params !== undefined && !isParamBag(spec.params)) {
|
|
229
|
+
// `resolveParams` would read keys off it and throw a `TypeError`.
|
|
230
|
+
if (!lenient) {
|
|
231
|
+
throw new ParamError(`${spec.op} params must be an object, got ${JSON.stringify(spec.params) ?? typeof spec.params}`);
|
|
232
|
+
}
|
|
233
|
+
bad();
|
|
234
|
+
entries = [[MALFORMED, spec.params]];
|
|
146
235
|
}
|
|
147
236
|
else {
|
|
148
|
-
const op = registry.get(spec.op);
|
|
149
237
|
try {
|
|
150
238
|
// The strict resolve first even under leniency: when it succeeds
|
|
151
239
|
// the id is byte-identical to the validating one, which is the
|
|
152
240
|
// whole contract. Only its failure moves this spec into the
|
|
153
241
|
// unvalidated namespace.
|
|
154
|
-
|
|
242
|
+
entries = Object.entries(registry.resolveParams(op, spec.params));
|
|
155
243
|
}
|
|
156
244
|
catch (e) {
|
|
157
245
|
if (!lenient)
|
|
158
246
|
throw e;
|
|
159
|
-
|
|
160
|
-
|
|
247
|
+
bad();
|
|
248
|
+
entries = Object.entries(registry.resolveParams(op, spec.params, { validate: false }));
|
|
161
249
|
}
|
|
162
250
|
}
|
|
163
251
|
const encodeKey = unvalidated ? esc : (k) => k;
|
|
164
252
|
const encodeValue = unvalidated ? typedEsc : esc;
|
|
165
|
-
const p =
|
|
253
|
+
const p = entries
|
|
166
254
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
167
255
|
.map(([k, v]) => `${encodeKey(k)}=${encodeValue(v)}`)
|
|
168
256
|
.join(',');
|
package/dist/plan/registry.d.ts
CHANGED
|
@@ -32,6 +32,22 @@ export declare class UnknownOpError extends ProcessError {
|
|
|
32
32
|
export declare class ParamError extends ProcessError {
|
|
33
33
|
static readonly code: string;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when a spec's input list does not match the op's declared
|
|
37
|
+
* arity — including a spec carrying no `inputs` at all.
|
|
38
|
+
*
|
|
39
|
+
* Its own class because arity is decidable from the **registry alone**,
|
|
40
|
+
* with no data bound: it is part of what `specId` can judge, and a spec
|
|
41
|
+
* that fails it must not be named in the valid id namespace. Before
|
|
42
|
+
* this, a spec with no `inputs` was named `p1:sma(;period=20)` — a valid
|
|
43
|
+
* id for something that cannot compile — and then died at `compile` as a
|
|
44
|
+
* bare `TypeError` reading `.length` of undefined, reaching the consumer
|
|
45
|
+
* as a `Skipped` with no `code` at all, which under that contract means
|
|
46
|
+
* "op code threw" (Tidal, on 0.62.0).
|
|
47
|
+
*/
|
|
48
|
+
export declare class ArityError extends ProcessError {
|
|
49
|
+
static readonly code: string;
|
|
50
|
+
}
|
|
35
51
|
/** Op metadata as a picker or a tool catalog wants it. */
|
|
36
52
|
export interface OpDescriptor {
|
|
37
53
|
readonly name: string;
|
|
@@ -103,6 +119,14 @@ export declare class Registry<Defs extends DefMap = {}> {
|
|
|
103
119
|
resolveParams(op: Def, given?: Readonly<Record<string, ParamValue>>, options?: {
|
|
104
120
|
validate?: boolean;
|
|
105
121
|
}): Params;
|
|
122
|
+
/**
|
|
123
|
+
* Checks a spec's input list against the op's declared arity.
|
|
124
|
+
*
|
|
125
|
+
* Lives here beside `get` and `resolveParams` — the three things
|
|
126
|
+
* decidable about a spec from the definition alone, before any data is
|
|
127
|
+
* bound — so `specId` and `compile` ask the same question once.
|
|
128
|
+
*/
|
|
129
|
+
checkArity(op: Def, inputs: unknown): void;
|
|
106
130
|
/** Grouped for a picker. */
|
|
107
131
|
byFamily(): Map<string, OpDescriptor[]>;
|
|
108
132
|
describe(): OpDescriptor[];
|
package/dist/plan/registry.js
CHANGED
|
@@ -24,6 +24,22 @@ export class UnknownOpError extends ProcessError {
|
|
|
24
24
|
export class ParamError extends ProcessError {
|
|
25
25
|
static code = 'ParamError';
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Thrown when a spec's input list does not match the op's declared
|
|
29
|
+
* arity — including a spec carrying no `inputs` at all.
|
|
30
|
+
*
|
|
31
|
+
* Its own class because arity is decidable from the **registry alone**,
|
|
32
|
+
* with no data bound: it is part of what `specId` can judge, and a spec
|
|
33
|
+
* that fails it must not be named in the valid id namespace. Before
|
|
34
|
+
* this, a spec with no `inputs` was named `p1:sma(;period=20)` — a valid
|
|
35
|
+
* id for something that cannot compile — and then died at `compile` as a
|
|
36
|
+
* bare `TypeError` reading `.length` of undefined, reaching the consumer
|
|
37
|
+
* as a `Skipped` with no `code` at all, which under that contract means
|
|
38
|
+
* "op code threw" (Tidal, on 0.62.0).
|
|
39
|
+
*/
|
|
40
|
+
export class ArityError extends ProcessError {
|
|
41
|
+
static code = 'ArityError';
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* Validates one param and returns it.
|
|
29
45
|
*
|
|
@@ -206,6 +222,22 @@ export class Registry {
|
|
|
206
222
|
}
|
|
207
223
|
return out;
|
|
208
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Checks a spec's input list against the op's declared arity.
|
|
227
|
+
*
|
|
228
|
+
* Lives here beside `get` and `resolveParams` — the three things
|
|
229
|
+
* decidable about a spec from the definition alone, before any data is
|
|
230
|
+
* bound — so `specId` and `compile` ask the same question once.
|
|
231
|
+
*/
|
|
232
|
+
checkArity(op, inputs) {
|
|
233
|
+
const want = op.inputs.length;
|
|
234
|
+
if (!Array.isArray(inputs)) {
|
|
235
|
+
throw new ArityError(`${op.name} takes ${want} input(s), got none — 'inputs' is ${inputs === undefined ? 'missing' : (JSON.stringify(inputs) ?? 'unset')}`);
|
|
236
|
+
}
|
|
237
|
+
if (inputs.length !== want) {
|
|
238
|
+
throw new ArityError(`${op.name} takes ${want} input(s), got ${inputs.length}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
209
241
|
/** Grouped for a picker. */
|
|
210
242
|
byFamily() {
|
|
211
243
|
const out = new Map();
|
package/dist/plan/run.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import type { Column, SeriesSchema, TimeSeries } from 'pond-ts';
|
|
21
21
|
import type { BoundGraph } from './graph.js';
|
|
22
22
|
import { type Slots } from './slots.js';
|
|
23
|
-
import type {
|
|
23
|
+
import type { Plan, SpecRef } from './types.js';
|
|
24
24
|
/** What to do when a spec or a selector fails. Covers both, not just resolution. */
|
|
25
25
|
export type ErrorPolicy = 'throw' | 'skip' | 'collect';
|
|
26
26
|
/**
|
|
@@ -160,14 +160,22 @@ export interface NodeTiming {
|
|
|
160
160
|
}
|
|
161
161
|
export interface Skipped {
|
|
162
162
|
/**
|
|
163
|
-
* The spec that failed, echoed back — including `inputs`,
|
|
164
|
-
* plan may hold two specs of the same op and a caller
|
|
165
|
-
* to know which one it was.
|
|
163
|
+
* The spec that failed, echoed back **verbatim** — including `inputs`,
|
|
164
|
+
* because a plan may hold two specs of the same op and a caller
|
|
165
|
+
* retrying needs to know which one it was.
|
|
166
|
+
*
|
|
167
|
+
* `params` and `inputs` are typed `unknown` because this is an echo of
|
|
168
|
+
* whatever arrived, and what arrives is exactly what may be malformed.
|
|
169
|
+
* The plan pass used to normalize — `params: null` came back as `{}` —
|
|
170
|
+
* which was not merely lossy: recomputing an id from the echo then
|
|
171
|
+
* produced the **defaulted spec's valid id**, so a broken persisted
|
|
172
|
+
* entry's report keyed onto a legitimate node (Tidal, on 0.62.0). The
|
|
173
|
+
* selector pass echoed the original all along, so the two disagreed.
|
|
166
174
|
*/
|
|
167
175
|
readonly spec?: {
|
|
168
176
|
op: string;
|
|
169
|
-
params
|
|
170
|
-
inputs
|
|
177
|
+
params?: unknown;
|
|
178
|
+
inputs?: unknown;
|
|
171
179
|
};
|
|
172
180
|
readonly select?: Select;
|
|
173
181
|
readonly reason: string;
|
package/dist/plan/run.js
CHANGED
|
@@ -127,11 +127,7 @@ export function run(graph, request) {
|
|
|
127
127
|
}
|
|
128
128
|
catch (e) {
|
|
129
129
|
fail(e, {
|
|
130
|
-
spec: {
|
|
131
|
-
op: spec.op,
|
|
132
|
-
params: { ...(spec.params ?? {}) },
|
|
133
|
-
inputs: spec.inputs,
|
|
134
|
-
},
|
|
130
|
+
spec: { op: spec.op, params: spec.params, inputs: spec.inputs },
|
|
135
131
|
});
|
|
136
132
|
}
|
|
137
133
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/process",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Computations as data over pond-ts: processing graphs authored fluently or composed as JSON, resolved against a declared op vocabulary with content-addressed caching, provenance, and per-node timings. Experimental, pre-1.0.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"time-series",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"verify": "npm run format:check && npm run build && npm test"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"pond-ts": "^0.
|
|
65
|
+
"pond-ts": "^0.70.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"typescript": "^5.6.3",
|