@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 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 throws unless
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'` throws; add
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 (daily, hourly, every N) | `packages/core/src/sequence/sequence.ts` |
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 | 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
- | `Event` | Immutable event: temporal key + typed payload | `packages/core/src/core/event.ts` |
108
- | `toTimeRange` | Coerce temporal values to `TimeRange` | `packages/core/src/core/time-range.ts` |
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 | Source |
162
- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
163
- | Schema contract | `SeriesSchema`, `RowForSchema`, `EventForSchema`, `EventDataForSchema`, `EventKeyForSchema`, `TimeSeriesInput`, `TimeSeriesJsonInput` | `packages/core/src/schema/index.ts` |
164
- | Aggregation specs | `AggregateReducer`, `AggregateMap`, `AggregateOutputMap`, `AggregateSchema`, `BinReducerName`, `BinOutput` | `packages/core/src/schema/index.ts`, `packages/core/src/column.ts` |
165
- | Operation schemas | `RollingSchema`, `RollingAlignment`, `AlignSchema`, `DiffSchema`, `SmoothSchema`, `SmoothMethod`, `FillStrategy`, `FillMapping` | `packages/core/src/schema/index.ts` |
166
- | Column/data kinds | `Column`, `KeyColumn`, `ColumnKind`, `ScalarKind`, `ScalarValue`, `ColumnValue`, `ArrayValue`, `ValidityBitmap` | `packages/core/src/columnar/` |
167
- | JSON wire format | `JsonRowFormat`, `JsonRowForSchema`, `JsonObjectRowForSchema`, `JsonValueForKind`, `JsonTimestampInput`, `JsonTimeRangeInput`, `JsonIntervalInput` | `packages/core/src/schema/index.ts` |
168
- | Temporal utility | `TemporalLike`, `DurationInput`, `CalendarUnit`, `TimeZoneOptions`, `KeyLike`, `BatchSampleStrategy`, `SequenceSample`, `SequenceCoverage` | `packages/core/src/core/`, `packages/core/src/sequence/` |
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 | Purpose | Source |
254
- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
255
- | `ChartContainer` | `width`, `range?`, `theme?`, `cursor?`, `panZoom?`, `xScale?`, `bounds?`, `showAxis?`, `calendar?`, `origin?`, `maxBandWidth?`/`bandAlign?`, `onTrackerChanged?`, `onDrawStats?` | Root: shared x-scale, interactions, annotations | `packages/charts/src/ChartContainer.tsx` |
256
- | `ChartRow` | `height`, `cursor?` (deprecated — mount a cursor in the row) | One stacked plot band; owns its y-axes | `packages/charts/src/ChartRow.tsx` |
257
- | `Layers` | children | Mandatory z-stack inside a row (back-to-front) | `packages/charts/src/Layers.tsx` |
258
- | `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` |
259
- | `XAxis` | `side?`, `label?`, `format?`, `ticks?`, `transform?`, `dateStyle?` | Placeable x-axis strip; kind inferred from data | `packages/charts/src/XAxis.tsx` |
260
- | `TimeAxis` / `CategoryAxis` | (XAxis props) | Thin `XAxis` presets | `packages/charts/src/TimeAxis.tsx`, `CategoryAxis.tsx` |
261
- | `Canvas` | `width`, `height`, `draw` | Low-level DPR-aware canvas primitive | `packages/charts/src/Canvas.tsx` |
262
- | `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` |
263
- | `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` |
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 | Source |
390
- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------- |
391
- | `createLiveValue` / `LiveValue` | Imperative push channel for high-frequency indicator updates (isolated repaint) | `packages/charts/src/indicators.tsx` |
392
- | `scaleTradingTime` / `TradingTimeScale` | Discontinuous time scale collapsing closed-market gaps | `packages/charts/src/tradingTimeScale.ts` |
393
- | `DiscontinuityProvider` | Gap topology consumed by the trading-time scale | `packages/charts/src/tradingTimeScale.ts` |
394
- | `scaleBand` / `ScaleBand` | Ordinal slot scale for the category axis | `packages/charts/src/bandScale.ts` |
395
- | `GapMode` | `'none' \| 'empty' \| 'dashed' \| 'step' \| 'fade'` (Line/Area `gaps` prop) | `packages/charts/src/gaps.ts` |
396
- | `DecimateOption` | `<LineChart decimate>` M4 viewport decimation (`bool \| { threshold }`) | `packages/charts/src/decimate.ts` |
397
- | `CursorMode` | `'none' \| 'line' \| 'point' \| 'inline' \| 'flag' \| 'crosshair' \| 'region'` | `packages/charts/src/context.ts` |
398
- | `TrackerInfo` / `TrackerSample` | Hover readout payload (`onTrackerChanged`) | `packages/charts/src/context.ts` |
399
- | `AnnotationKind` / `CreateSpec` | Annotation identity + draw-gesture payload (`onCreate`) | `packages/charts/src/context.ts` |
400
- | `SelectInfo` | Selection/hover payload (`Selector`/`MultiSelector` `onSelect`/`onHover`) | `packages/charts/src/context.ts` |
401
- | `SelectModifiers` | Keyboard modifiers on a click, 2nd arg to `onSelect` | `packages/charts/src/context.ts` |
402
- | `SelectorProps` | `<Selector>`'s props — `enabled?` / `selected?` / `hovered?` / `onSelect?` / `onHover?` / `children?` | `packages/charts/src/selectors.tsx` |
403
- | `MultiSelectorProps` | `<MultiSelector>`'s props the above plus `sequence?`, with plural callbacks | `packages/charts/src/selectors.tsx` |
404
- | `RangeSpan` | `<RangeCursor onDragRelease>` payload `{ x: [lo, hi], y? }` in axis units | `packages/charts/src/context.ts` |
405
- | `SpanSelection` | Range entry for `selected` one layer's marks over `x`/`y`/`rows` (RFC A5.2) | `packages/charts/src/context.ts` |
406
- | `SelectionEntry` | One `selected` array entry: `SelectInfo \| SpanSelection` | `packages/charts/src/context.ts` |
407
- | `selectionContains` | Is a hit in a mixed selection? The same membership predicate the layers run | `packages/charts/src/span.ts` |
408
- | `sameMark` | Are two hits the same mark? Full identity (`id`, `mark`-or-`key`, `label`) | `packages/charts/src/span.ts` |
409
- | `isSpanSelection` | Entry discriminant narrows a `SelectionEntry` to `SpanSelection` | `packages/charts/src/span.ts` |
410
- | `DrawStatsFrame` / `LayerDrawInfo` | Per-repaint draw-cost + decimation stats (`ChartContainer` `onDrawStats`) | `packages/charts/src/context.ts` |
411
- | `TimeGrain` | Coarse time unit for grain-aware formatting | `packages/charts/src/tickLadder.ts` |
412
- | `SwatchSpec` / `LegendItemInput` | Legend swatch vocabulary + explicit-rows input (`<Legend items>`) | `packages/charts/src/swatch.ts` |
413
- | `useChartLegend` | Headless legend hook: rows (items grouped by chart row) + `hover`/`select` verbs | `packages/charts/src/useChartLegend.ts` |
414
- | `ChartLegend` / `LegendRow` / `LegendItem` | The hook's return shape (`rows` group `items`; items carry `selected`/`hovered`) | `packages/charts/src/useChartLegend.ts` |
415
- | `useChartFrame` | Resolved plot geometry: plot rect, gutters, x scale, a row's y scales, band slot edges | `packages/charts/src/useChartFrame.ts` |
416
- | `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` |
417
- | `ChartBands` / `ChartBand` | Ordinal slot geometry on a category axis (`count`/`pitch`/`labels`/`at(i)`); `null` on time/value | `packages/charts/src/useChartFrame.ts` |
418
- | `ChartXScale` | The union the container's shared x scale resolves to (time / linear / trading / band / elapsed) | `packages/charts/src/context.ts` |
419
- | `LegendPlacement` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `packages/charts/src/Legend.tsx` |
420
- | `Curve` | Path interpolation: `'linear' \| 'monotone' \| 'natural' \| 'basis' \| 'step'` | `packages/charts/src/curve.ts` |
421
- | `RadiusEncoding` / `ColorEncoding` | Data-driven scatter size/colour | `packages/charts/src/encoding.ts` |
422
- | `CandleVariant` / `ColorBy` | OHLC mark shape / colouring strategy | `packages/charts/src/ohlc.ts` |
423
- | `AxisFormat` / `CursorFormat` | Tick and cursor-readout formatting (d3 specifier or fn) | `packages/charts/src/format.ts` |
424
- | `AxisTransform` | Monotonic `to`/`from` pair for derived-unit x-axis relabeling | `packages/charts/src/derivedTicks.ts` |
425
- | `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` |
426
- | `Orientation` | Bar growth direction | `packages/charts/src/bars.ts` |
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 | Source |
622
- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
623
- | `TradingCalendar` | Query API: `.sessions()`, `.sessionOn()`, `.isTradingDay()`, `.isOpen()`, `.sessionsInRange()`, `.sessionSequence()`, `.barSequence(period)`, `.tagSessions()`, `.discontinuities()` | `packages/financial/src/calendar/` |
624
- | `generateSessions` | `Session[]` from `SessionRules` over a date range (DST-correct) | `packages/financial/src/calendar/` |
625
- | `normalizeSessions` | Validate + sort an explicit session list | `packages/financial/src/calendar/` |
626
- | `identityDiscontinuity` / `segmentDiscontinuity` / `weekendSkip` | `DiscontinuityProvider`s for the trading-time axis | `packages/financial/src/calendar/` |
627
- | Types | `Session`, `SessionBreak`, `SessionRules`, `DateRange`, `InstantRange`, `TaggedSchema`, `LiveSegment`, `DiscontinuityProvider` | `packages/financial/src/calendar/` |
628
- | `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` |
629
- | `PIVOT_METHODS` / `PivotMethod` | The four pivot formula sets (`standard`, `fibonacci`, `woodie`, `camarilla`) | `packages/financial/src/kernels/pivot.ts` |
630
- | `PivotPointsSchema` / `CamarillaPivotPointsSchema` / `PivotPointsResult` | `pivotPoints`' method-dependent appended-schema types (7 columns, or 9 for Camarilla) | `packages/financial/src/studies/pivot-points.ts` |
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 | Source |
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` | `packages/process/src/pool/index.ts` |
718
- | Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
719
- | Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
720
- | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
721
- | 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` |
722
- | Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
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]` | `packages/process/src/column.ts` |
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 | `packages/process/src/plan/graph.ts` |
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 | `packages/process/src/plan/graph.ts` |
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`) | `packages/process/src/plan/history.ts` |
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`) | `packages/process/src/column.ts` |
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` | `packages/process/src/plan/registry.ts`, `params.ts` |
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` | `packages/process/src/plan/identity.ts` |
730
- | 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` |
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) | `packages/process/src/plan/graph.ts`, `run.ts` |
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`, `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` |
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` | `packages/process/src/plan/host.ts` |
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` | `packages/process/src/plan/slots.ts` |
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` | `packages/process/src/plan/builder.ts`, `fluent.ts` |
736
- | Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
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 | `packages/process/src/plan/folds.ts` |
738
- | Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
739
- | Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
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.68.0...HEAD
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
+ [![npm](https://img.shields.io/npm/v/@pond-ts/process?label=%40pond-ts%2Fprocess)](https://www.npmjs.com/package/@pond-ts/process)
4
+ [![CI](https://github.com/pond-ts/pond/actions/workflows/ci.yml/badge.svg)](https://github.com/pond-ts/pond/actions/workflows/ci.yml)
5
+ [![docs](https://img.shields.io/badge/docs-pond--ts.org-1f6feb)](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';
@@ -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: params first, then arity, then the typed
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. */
@@ -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: params first, then arity, then the typed
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
- if (spec.inputs.length !== op.inputs.length) {
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'.
@@ -34,13 +34,13 @@
34
34
  *
35
35
  * Both are pinned by tests.
36
36
  */
37
- import type { Registry } from './registry.js';
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 and its params must be legal default
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
- * Validity stays `compile`'s job.
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
@@ -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
- * Validity stays `compile`'s job.
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
- /** An id, and whether anything in its closure failed validation. */
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
- // Inputs first: a nested spec that did not validate marks this one,
123
- // and the mark has to be known before the params are encoded.
124
- //
125
- // `?? []` because a spec arriving from persistence may be missing the
126
- // field entirely, and a mode whose promise is totality cannot answer a
127
- // dropped key with a TypeError. Strict mode reaches `compile`'s arity
128
- // check instead, which says what is actually wrong.
129
- const inputs = (spec.inputs ?? [])
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
- // `#Lower` rather than a separate field: an input picking a
134
- // different output is a different computation, and the id is what
135
- // says so.
136
- const base = build(registry, specOf(i), lenient);
137
- if (base.unvalidated)
138
- unvalidated = true;
139
- return isPicked(i) ? `${base.id}#${esc(i.output)}` : base.id;
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
- let params;
143
- if (lenient && !registry.has(spec.op)) {
144
- unvalidated = true;
145
- params = spec.params ?? {};
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
- params = registry.resolveParams(op, spec.params);
242
+ entries = Object.entries(registry.resolveParams(op, spec.params));
155
243
  }
156
244
  catch (e) {
157
245
  if (!lenient)
158
246
  throw e;
159
- unvalidated = true;
160
- params = registry.resolveParams(op, spec.params, { validate: false });
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 = Object.entries(params)
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(',');
@@ -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[];
@@ -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();
@@ -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 { Input, Plan, SpecRef } from './types.js';
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`, because a
164
- * plan may hold two specs of the same op and a caller retrying needs
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: Record<string, unknown>;
170
- inputs: readonly Input[];
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.68.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.68.0"
65
+ "pond-ts": "^0.70.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "typescript": "^5.6.3",