@pond-ts/react 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/API.md +25 -25
  2. package/CHANGELOG.md +165 -1
  3. package/package.json +2 -2
package/API.md CHANGED
@@ -521,31 +521,31 @@ Typed dataflow graphs for pipelines whose **shape is data** (runtime-assembled,
521
521
  user-edited, one computation fanned out to several consumers). Chaining stays
522
522
  the default for pipelines known at authoring time — see the package README.
523
523
 
524
- | Group | Exports | Source |
525
- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
526
- | 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` |
527
- | Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
528
- | Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
529
- | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
530
- | 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` |
531
- | Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
532
- | 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` |
533
- | 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` |
534
- | 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` |
535
- | 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` |
536
- | 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` |
537
- | Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams`, `byFamily`, `describe`, `toJsonSchema`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError` | `packages/process/src/plan/registry.ts`, `params.ts` |
538
- | Plan — identity | `specId` (content-addressed, param-order invariant, defaults materialized), `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
539
- | 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` |
540
- | Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError` | `packages/process/src/plan/graph.ts`, `run.ts` |
541
- | 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`, `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`) | `packages/process/src/plan/run.ts` |
542
- | 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` |
543
- | 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` |
544
- | 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` |
545
- | Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
546
- | 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` |
547
- | Errors | `ProcessError` (base), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
548
- | Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
524
+ | Group | Exports | Source |
525
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
526
+ | 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` |
527
+ | Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
528
+ | Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
529
+ | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
530
+ | 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` |
531
+ | Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
532
+ | 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` |
533
+ | 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` |
534
+ | 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` |
535
+ | 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` |
536
+ | 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` |
537
+ | 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` |
538
+ | 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` |
539
+ | 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` |
540
+ | 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` |
541
+ | 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` |
542
+ | 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` |
543
+ | 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` |
544
+ | 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` |
545
+ | Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
546
+ | 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` |
547
+ | Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
548
+ | Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
549
549
 
550
550
  Note: this package's `npm test` includes a `test:dts` step that typechecks the
551
551
  **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.61.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.63.0...HEAD
12
+ [0.63.0]: https://github.com/pond-ts/pond/compare/v0.62.0...v0.63.0
13
+ [0.62.0]: https://github.com/pond-ts/pond/compare/v0.61.0...v0.62.0
12
14
  [0.61.0]: https://github.com/pond-ts/pond/compare/v0.60.0...v0.61.0
13
15
  [0.60.0]: https://github.com/pond-ts/pond/compare/v0.59.0...v0.60.0
14
16
  [0.59.0]: https://github.com/pond-ts/pond/compare/v0.58.0...v0.59.0
@@ -64,6 +66,168 @@ include new features and type-level changes; patch bumps are strictly additive.
64
66
 
65
67
  ## [Unreleased]
66
68
 
69
+ ## [0.63.0] — 2026-08-18
70
+
71
+ ### Added
72
+
73
+ - `@pond-ts/charts`: **axis pan/zoom**, behind a new
74
+ **`<ChartContainer axisPanZoom>`** (`'none'` — the default — / `'x'` / `'y'` /
75
+ `'xy'`). Opted in, the `<XAxis>` strip and each `<YAxis>` gutter become
76
+ grabbable, and **double-click** puts one back (the declared `range` on x, its
77
+ fit on y). The cursor stays an ordinary arrow at rest and becomes directional
78
+ (`↕` / `↔`) only while a gesture is running.
79
+
80
+ **The opt-in is deliberately separate from `panZoom`, in both directions.** An
81
+ already-interactive chart does not grow axis gestures on upgrade — nothing
82
+ changes for any existing chart — and a chart can scale its y axes with
83
+ `panZoom` off entirely, which is what you want when the plot's drag belongs to
84
+ a selection sweep.
85
+
86
+ **The x strip is the canvas gesture, moved to the axis** — drag pans, wheel
87
+ zooms about the pointer — reusing the plot's own domain-space maths, so
88
+ `bounds` and `minDuration` still fence the view and a trading-time axis still
89
+ pans and floors in _trading_ time. A category axis has no continuous domain and
90
+ stays inert.
91
+
92
+ **A y gutter zooms, and only the axis you grabbed** — its sibling and every
93
+ other row hold still. That needed a new per-axis pixel transform
94
+ (`RowFrame.axisTransforms`, layered under the container's uniform
95
+ `yTransform`), since the uniform one exists precisely so a _plot_ gesture never
96
+ has to pick an axis; unlike it, this is not floored at `k ≥ 1`, because
97
+ squashing an axis you grabbed is the point.
98
+
99
+ - `@pond-ts/charts`: **`<YAxis onBoundsChange>`** — the auto-vs-manual hand-off
100
+ for a scaled y axis. Fires with the `[min, max]` a gutter gesture reached, and
101
+ with `null` when the axis is released back to auto-fit, so a UI can show the
102
+ bounds, badge the scale "manual", and offer a toggle back. Providing it makes
103
+ the axis **controlled** (the gesture only reports; `min`/`max` fed back are
104
+ what it draws), exactly as `onTimeRangeChange` does for the x view; omit it and
105
+ the axis holds the zoom itself. `ContainerFrame` also gains `seedRange` — the
106
+ declared view, as against the gestured `timeRange` — which is the x reset's
107
+ target.
108
+
109
+ - **charts: `<ChartContainer height>` + `<ChartRow flex>` — container-owned
110
+ vertical layout** ([PND-HEIGHT]). `height` takes a pixel number or `'auto'`
111
+ (measured by the same `ResizeObserver` as `width="auto"`); omitted stays the
112
+ classic mode where rows declare pixels. A managed container renders as a
113
+ **flex column** — the rows block flexes, the x-axis strip keeps its natural
114
+ height — and flex rows (a bare `<ChartRow>` is `flex={1}`) divide what the
115
+ browser says is left, then read that height back for their y-scales.
116
+
117
+ The design constraint was that **CSS does the subtraction**: the axis
118
+ strip's height depends on its `label`, the theme font size, the tick
119
+ ladder's calendar band row at the current grain, and stacked marker pills —
120
+ it is not a constant a caller can subtract, and every consumer who tried
121
+ carried a wrong one (`20` and `24` in one codebase; this site's own
122
+ resizable-panels recipe said `22`). It also means non-row children between
123
+ rows — the recipe's draggable splitter — keep taking their natural space,
124
+ so that recipe reduces to its drag handler: one flex row absorbing slack
125
+ over one fixed row the drag resizes, no `AXIS_H`, no measuring hook.
126
+
127
+ A flex row paints nothing until its first measurement, latches its last
128
+ non-zero height while hidden (a `display: none` tab switch keeps its
129
+ scales), and warns in dev when mounted in a container that never sizes it.
130
+ The container **warns in dev when a measured dimension stays 0** — the
131
+ unconstrained-parent deadlock, which for height is the _default_ (a
132
+ flex-column child's height is its content) rather than an edge case.
133
+
134
+ Fixed-`height` rows keep their pixels everywhere, managed or not; a
135
+ container with no `height` behaves exactly as before.
136
+
137
+ ### Fixed
138
+
139
+ - **charts: the crosshair's value pill lands on the axis that measured the
140
+ value, in that axis's colour.** With two y-axes on one side, the pill was
141
+ placed by side alone — always against the plot edge — so a reading taken off
142
+ the **outer** axis appeared over the **inner** axis's ticks, in the cursor's
143
+ grey: a number pinned to a ruler that never measured it. The reticle picks one
144
+ series, so the resolved sample now carries its axis's gutter **offset** (the
145
+ reserved widths of the columns between it and the plot) and its
146
+ `<YAxis color>`, and the pill uses both — position _and_ ink say which of two
147
+ stacked scales the number is on. A single axis per side is unchanged (offset
148
+ `0`), as is an axis that sets no colour (the theme's cursor ink).
149
+
150
+ A pill placed further out is now **bridged back to the plot edge** by a 1px
151
+ connector in its own colour — the y-side twin of the crosshair's x-axis time
152
+ connector, without which a pill a column out reads as a value floating in a
153
+ gutter. It draws at half opacity because, unlike the time connector's empty
154
+ strip, it crosses another axis's tick labels; nothing is drawn on the innermost
155
+ axis, where the pill already meets the line.
156
+
157
+ `<YAxis color>` is now part of the axis's registered spec rather than
158
+ presentation-only, because the pill is drawn by the row's cursor overlay, not
159
+ by `<YAxis>`. `<Baseline indicator>` — the other on-axis value pill — got the
160
+ same placement fix and the same connector. New `Cursors/Crosshair` stories fan
161
+ the states out (`AxisColor`, `StackedAxes`, `StackedAxesColored`,
162
+ `StackedAxesLeft`, `StackedAxesBothSides`).
163
+
164
+ Not covered: `<YAxisIndicator>` still takes an explicit `side` alongside its
165
+ `axis`, so it can be pointed at a gutter its axis isn't in; reconciling those
166
+ two props is a public-API question, left for its own change.
167
+
168
+ ## [0.62.0] — 2026-08-16
169
+
170
+ ### Added
171
+
172
+ - `@pond-ts/process`: **`specId` is total under `{ validate: false }`** — a
173
+ third options argument that names a spec which would not compile
174
+ (`specId(registry, spec, { validate: false })`). Identity used to be coupled
175
+ to validity, so the moments a consumer most needs an id — labelling the chip
176
+ it is skipping, keying "this persisted entry is broken", logging what was
177
+ rejected — were exactly the moments it threw, leaving the consumer to
178
+ re-implement canonicalization or carry a second key. Lenient mode still
179
+ applies defaults and sorts keys, carries an undeclared param through rather
180
+ than dropping it, and recurses into nested inputs; **a valid spec has the
181
+ same id under either mode**, so nothing needs a second cache line. Validity
182
+ stays `compile`'s job. `Registry.resolveParams` takes the same
183
+ `{ validate: false }`.
184
+
185
+ An id that did **not** validate is minted in a separate namespace — marked
186
+ `p1?:` instead of `p1:`, with type-preserving param encoding — so it can
187
+ never collide with a legal id. Both measures are confined to that branch:
188
+ a valid id is byte-identical to what shipped in 0.61.0. The mark rides up a
189
+ chain, so a spec over an unvalidated input is unvalidated too.
190
+
191
+ - `@pond-ts/process`: **`Skipped.code`** — every entry in `RunResult.skipped`
192
+ now carries the failure's kind (`'UnknownColumnError'`, `'ParamError'`,
193
+ `'UnitError'`, `'SlotError'`, …) beside its human `reason`. Under
194
+ `onError: 'skip' | 'collect'` nothing is thrown, so `instanceof` — the right
195
+ discriminator when a consumer catches — never reached a consumer reading
196
+ `skipped`, leaving it to match on prose whose wording is not a contract. The
197
+ value is `ProcessError.code`, a **literal declared per class** rather than
198
+ `constructor.name`, so a consumer's minifier cannot silently rename it. It is
199
+ absent when the throw did not come from this package, which is itself the
200
+ signal: op code failed, not the plan layer.
201
+
202
+ ### Fixed
203
+
204
+ - `@pond-ts/process`: **`run` under `onError: 'throw'` — the default — now
205
+ raises the original error rather than a base `ProcessError` rebuilt from its
206
+ message.** A caught `UnknownColumnError`, `ParamError`, `UnitError` or
207
+ `SlotError` reached the caller as a bare `ProcessError`, so `instanceof`
208
+ could not discriminate on the throw path at all.
209
+
210
+ - `@pond-ts/process`: **a raw string input naming a column the bound series
211
+ does not carry is now rejected**, at `compile`, with a new
212
+ `UnknownColumnError`. Nothing checked it at compile or at pull: the op ran
213
+ against an un-widened series, and one that doesn't defend its own inputs
214
+ appended a plausible-looking column of garbage under the spec's id — with
215
+ `skipped` empty and `onError` never engaged. A persisted plan citing a column
216
+ the feed has since dropped now skips (or throws) instead of returning a
217
+ value. The check is the one `expandSlots` already made against the same
218
+ column list, so the two request forms no longer disagree; it runs before the
219
+ unit check, whose "is 'unitless'" answer for an absent column named the wrong
220
+ problem. The key/time column is not a value column and is rejected too.
221
+
222
+ The check covers the **whole spec closure** (a missing column under a typed
223
+ parent otherwise surfaced as `UnitError`) and runs on the **warm** path as
224
+ well as the cold one — `setSource` replaces the data under compiled nodes by
225
+ design, so a memoized node could outlive the column it reads and go on
226
+ emitting the garbage column this fix exists to prevent. A node that fails the
227
+ re-check is dropped from the graph.
228
+ (Both items reported by Tidal —
229
+ `docs/notes/tidal-process-adoption-friction-2026-08.md`.)
230
+
67
231
  ## [0.61.0] — 2026-08-16
68
232
 
69
233
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.61.0",
3
+ "version": "0.63.0",
4
4
  "description": "React hooks for pond-ts live time series",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "test:runtime": "vitest run"
35
35
  },
36
36
  "peerDependencies": {
37
- "pond-ts": "^0.61.0",
37
+ "pond-ts": "^0.63.0",
38
38
  "react": "^18.0.0 || ^19.0.0"
39
39
  },
40
40
  "devDependencies": {