@pond-ts/react 0.11.4 → 0.11.5

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 (4) hide show
  1. package/CHANGELOG.md +1092 -0
  2. package/LICENSE +21 -0
  3. package/README.md +460 -0
  4. package/package.json +4 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,1092 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ `pond-ts` and `@pond-ts/react` release together under a single `v*` tag, so this
7
+ file covers both packages. Pre-1.0: minor bumps may include new features and
8
+ type-level changes; patch bumps are strictly additive.
9
+
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.11.5...HEAD
11
+
12
+ ## [Unreleased]
13
+
14
+ ## [0.11.5] — 2026-04-29
15
+
16
+ ### Fixed
17
+
18
+ - Published tarballs for both `pond-ts` and `@pond-ts/react` now
19
+ include `README.md`, `LICENSE`, and `CHANGELOG.md`. Earlier
20
+ releases shipped only `dist/` + `package.json`, which left the
21
+ npm page rendering as "This package does not have a README"
22
+ despite the comprehensive root README. The repo-root files were
23
+ invisible to `npm pack` because npm publishes from the package
24
+ directory and only auto-includes README/LICENSE when those files
25
+ live in the package dir itself. Each package now has a `prepack`
26
+ step that copies them in from the repo root before build.
27
+
28
+ [0.11.5]: https://github.com/pjm17971/pond-ts/compare/v0.11.4...v0.11.5
29
+
30
+ ## [0.11.4] — 2026-04-29
31
+
32
+ ### Added
33
+
34
+ - **`LiveSeries` snapshot/append primitives** — closes the gap
35
+ where networked `LiveSeries` setups (gRPC, WebSocket fanout) had
36
+ to hand-roll the parallel APIs that already existed on
37
+ `TimeSeries`.
38
+ - **Codec-agnostic typed-tuple primitives:** `LiveSeries.toRows()`,
39
+ `LiveSeries.toObjects()`, `LiveSeries.pushMany(rows)`,
40
+ `Event.toRow(schema)`. Operate in `RowForSchema<S>` typed
41
+ tuples — JSON, MessagePack, protobuf, anything else applies at
42
+ the application boundary, not inside the library.
43
+ - **JSON sugar layered on top:** `LiveSeries.toJSON()`,
44
+ `LiveSeries.fromJSON(input, options?)`,
45
+ `LiveSeries.pushJson(rows)`, `Event.toJsonRow(schema)`. Closes
46
+ the wire→push safety hole — `pushJson` validates a
47
+ `JsonRowForSchema<S>` against the schema at compile time, so
48
+ schema evolution breaks the call site instead of swallowing
49
+ via `live.push(row as never)`.
50
+ - **`pushMany(rows)` is non-variadic.** Pair with the existing
51
+ variadic `push(...rows)` (now a one-line wrapper); reach for
52
+ `pushMany` when ingesting a snapshot or any large array —
53
+ variadic spread allocates a stack frame per element and can
54
+ blow on multi-thousand-row snapshots.
55
+
56
+ Surfaced by the gRPC experiment's M1 milestone
57
+ ([pond-grpc-experiment#3](https://github.com/pjm17971/pond-grpc-experiment/pull/3)).
58
+ See PLAN.md Phase 4 for the deferred adaptor-extraction
59
+ framing (codec strategies parked until two real codecs exist
60
+ in working code).
61
+
62
+ ### Changed
63
+
64
+ - `LiveSeries.push(...rows)` is now a wrapper around
65
+ `LiveSeries.pushMany(rows)`. Behavior is identical — same
66
+ validation, listener fires, and retention pass.
67
+
68
+ [0.11.4]: https://github.com/pjm17971/pond-ts/compare/v0.11.3...v0.11.4
69
+
70
+ ## [0.11.3] — 2026-04-28
71
+
72
+ ### Added
73
+
74
+ - **`pond-ts/types` subpath export** — type-only entry point that
75
+ exposes the schema-shape, row-shape, and JSON-shape types
76
+ (`SeriesSchema`, `ColumnDef`, `RowForSchema`,
77
+ `JsonRowForSchema`, etc.) without dragging in the runtime.
78
+ Schema-as-contract consumers — packages whose only job is to
79
+ declare the `as const` schema flowing through producer /
80
+ aggregator / web — can now constrain literals via
81
+ `satisfies SeriesSchema` without adding `pond-ts` as a runtime
82
+ dependency. Surfaced by the gRPC experiment's `packages/shared`,
83
+ where `import { SeriesSchema } from 'pond-ts'` would have
84
+ pulled in the whole library for one type.
85
+
86
+ ```ts
87
+ import type { SeriesSchema } from 'pond-ts/types';
88
+ export const schema = [
89
+ { name: 'time', kind: 'time' },
90
+ { name: 'cpu', kind: 'number' },
91
+ ] as const satisfies SeriesSchema;
92
+ ```
93
+
94
+ Existing `import { SeriesSchema } from 'pond-ts'` calls keep
95
+ working unchanged.
96
+
97
+ [0.11.3]: https://github.com/pjm17971/pond-ts/compare/v0.11.2...v0.11.3
98
+
99
+ ## [0.11.2] — 2026-04-28
100
+
101
+ ### Added
102
+
103
+ - `minSamples` option on `TimeSeries.rolling`,
104
+ `PartitionedTimeSeries.rolling`, `LiveRollingAggregation`, and
105
+ the `LivePartitionedSeries` rolling sugar — suppresses output
106
+ rows whose window contains fewer than the configured number of
107
+ source events. Forwarded to `TimeSeries.baseline` and
108
+ `TimeSeries.outliers` (and their per-partition variants), which
109
+ pass it to their internal rolling pass. Defaults to `0` (no
110
+ gate) so existing call sites are unaffected. Use it on noisy
111
+ rolling stats (e.g. the rolling stdev that feeds
112
+ `baseline()`'s ±σ bands) to hide the warm-up region where a
113
+ tiny-sample stdev would collapse the band tight enough to
114
+ false-flag normal events.
115
+
116
+ [0.11.2]: https://github.com/pjm17971/pond-ts/compare/v0.11.1...v0.11.2
117
+
118
+ ## [0.11.1] — 2026-04-27
119
+
120
+ Closes a packaging footgun the dashboard agent surfaced while
121
+ upgrading from `pond-ts@0.10.1` to `pond-ts@0.11.0`.
122
+
123
+ When users had `@pond-ts/react@0.10.1` (which declared
124
+ `dependencies: { "pond-ts": "^0.10.0" }`) and bumped only
125
+ `pond-ts` to `0.11.0`, npm satisfied the react package's `^0.10.0`
126
+ range by nesting a _second_ copy of `pond-ts@0.10.1` under
127
+ `@pond-ts/react/node_modules/`. Two pond-ts copies meant two
128
+ distinct `Sequence` / `Time` / etc. classes with non-shared JS
129
+ private (`#`) brands. TypeScript surfaced this as
130
+ `Property '#private' refers to a different member`, which is
131
+ opaque without the package context.
132
+
133
+ ### Changed
134
+
135
+ - **`@pond-ts/react`**: moved `pond-ts` from `dependencies` to
136
+ `peerDependencies` (range unchanged: `^0.11.0`). With peer-dep
137
+ semantics, npm refuses to install a duplicate `pond-ts`; instead
138
+ it warns at install time about peer-version mismatch — concrete,
139
+ actionable feedback rather than a runtime brand-check failure.
140
+
141
+ This is the standard pattern for packages that wrap another
142
+ library's classes (`react-dom` peer-deps `react`, etc.):
143
+ `@pond-ts/react`'s hooks return and operate on `pond-ts`
144
+ instances, so they MUST share class identity with the consumer's
145
+ `pond-ts`.
146
+
147
+ **Mild break:** consumers who installed only `@pond-ts/react`
148
+ and relied on the transitive `pond-ts` will now get an npm
149
+ warning and need to add `pond-ts` to their direct dependencies.
150
+ In practice anyone using `@pond-ts/react` is already importing
151
+ `pond-ts` types/classes, so the typical setup already has it
152
+ declared explicitly.
153
+
154
+ ### Notes
155
+
156
+ - **Why caret (`^0.11.0`) and not exact pin?** Pre-1.0 caret
157
+ semver only accepts patches within the same minor (so
158
+ `^0.11.0` matches 0.11.x but not 0.12.0). That already
159
+ enforces minor-level lockstep — exact pinning would force
160
+ consumers to bump both packages for every patch, even when one
161
+ package's bump is a lockstep no-op.
162
+
163
+ [0.11.1]: https://github.com/pjm17971/pond-ts/compare/v0.11.0...v0.11.1
164
+
165
+ ## [0.11.0] — 2026-04-27
166
+
167
+ The "live partitioning" release. Closes the cross-entity
168
+ correctness story end-to-end — the per-partition primitives we
169
+ shipped in v0.9.0 / v0.10.0 for batch now have a live counterpart
170
+ that handles ingestion, retention, grace, and stateful pipelines
171
+ on multi-host streams.
172
+
173
+ Without this, every multi-host live pipeline (rolling avg, fill,
174
+ diff, rate, cumulative, pctChange) silently mixes data across
175
+ entities — the same hazard the partitionBy work resolved for
176
+ batch, but live-side. Dashboard agent's v0.9.0 round-2 feedback
177
+ explicitly named "LivePartitionedSeries would be the obvious next
178
+ step" as the missing piece.
179
+
180
+ ### Added
181
+
182
+ - **`liveSeries.partitionBy(col, options?)`** — returns
183
+ `LivePartitionedSeries<S, K>`, the live counterpart to
184
+ `PartitionedTimeSeries`. Routes events from a source
185
+ `LiveSource<S>` into per-partition `LiveSeries<S>` sub-buffers,
186
+ each with its own retention, grace window, and stateful
187
+ operator pipeline.
188
+
189
+ Per-partition semantics (settled in design):
190
+ - Retention applies per partition (a chatty host can't squeeze
191
+ a quiet one out of the buffer)
192
+ - Grace windows apply per partition (late events touch only
193
+ their own partition)
194
+ - Aggregation timing is per partition (one host's rolling avg
195
+ fires when that host has enough data)
196
+ - Auto-spawn on new partition values; optional `groups` for
197
+ typed declared partitions (mirrors batch typed-groups)
198
+
199
+ Terminals:
200
+ - `.toMap()` → `Map<K, LiveSource<S>>` for direct per-partition
201
+ subscription
202
+ - `.collect()` → unified `LiveSeries<S>` (append-only fan-in)
203
+ - `.apply(factory)` → unified `LiveSeries<R>` with per-
204
+ partition operator chains
205
+ - `.dispose()` cleans up source subscription, all per-partition
206
+ pipeline subscribers, and `toMap`-created factory chains
207
+
208
+ - **Typed chainable sugar** — `partitioned.fill(...).rolling(...).collect()`
209
+ matches the batch chainable view. Sugar coverage on both
210
+ `LivePartitionedSeries` and the chained `LivePartitionedView`:
211
+ `fill`, `diff`, `rate`, `pctChange`, `cumulative`, `rolling`.
212
+
213
+ ```ts
214
+ const cpuSmoothed = live
215
+ .partitionBy('host')
216
+ .fill({ cpu: 'hold' })
217
+ .rolling('1m', { cpu: 'avg', host: 'last' })
218
+ .collect();
219
+ ```
220
+
221
+ `LivePartitionedView<SBase, R, K>` is a lazy chain step holding
222
+ a composed factory; terminals delegate to the root partitioned
223
+ series. Auto-spawn flows through the chain — a new partition
224
+ triggers a fresh factory invocation.
225
+
226
+ - **`LivePartitionedView`** exported from package root.
227
+
228
+ - **`ARCHITECTURE.md`** at repo root — first-pass document for
229
+ contributors (human or AI) reading the codebase cold. Covers
230
+ layered model, stateful primitives, recurring patterns
231
+ (typed-groups, trusted construction via `static #foo`,
232
+ factory-based per-partition state, append-only fan-in vs
233
+ mirrored materialization, per-method JSDoc warnings, perf-
234
+ check discipline), decision log, and conventions.
235
+
236
+ ### Changed
237
+
238
+ - **CLAUDE.md** points to `ARCHITECTURE.md` so future sessions
239
+ discover it alongside `PLAN.md`.
240
+
241
+ ### Notes
242
+
243
+ - **Append-only fan-in semantics** for `collect()` and `apply()`
244
+ on `LivePartitionedSeries` — per-partition retention/grace
245
+ evictions do NOT propagate to the unified buffer. Documented
246
+ via JSDoc; the unified buffer's own retention is independent.
247
+ Use `toMap()` for current per-partition state.
248
+
249
+ - **Post-commit error semantics for partition rejection** — when
250
+ the partition view throws inside the source's event listener
251
+ (rogue value, partition ordering rejection), the source has
252
+ already committed the event. Documented in
253
+ `LiveSeries.partitionBy` JSDoc; recommend upstream input
254
+ validation if source/partition atomicity matters.
255
+
256
+ - **Rolling drops partition column unless explicitly added.**
257
+ `LiveSeries.rolling` (and the partitioned chain via it) only
258
+ retains columns named in `mapping` — include `host: 'last'` (or
259
+ similar) to keep the partition tag visible in the unified
260
+ output. Documented in `rolling`'s JSDoc on both the
261
+ `LivePartitionedSeries` and `LivePartitionedView` surfaces.
262
+
263
+ ### Performance
264
+
265
+ - Routing overhead measured at ~88ms for 100k events × 10 hosts
266
+ (50ms over bare push). Apples-to-apples vs equivalent un-
267
+ partitioned operator chains: ~1.8-2.6× cost. Constant per
268
+ event (~0.8 µs); cardinality scales flat (Map lookup is O(1)).
269
+ See `scripts/perf-live-partitioned.mjs`.
270
+
271
+ - An `_acceptEvent` private-method optimization to bypass row
272
+ re-validation in partition routing was scoped and rejected for
273
+ v0.11 — the benefit (~0.3-0.4 µs/event saved) is marginal for
274
+ typical telemetry workloads (1-10k events/sec) and the cost
275
+ (validation-bypass primitive on the public API surface) wasn't
276
+ justified. May revisit if a high-throughput user surfaces the
277
+ bottleneck with real workload data.
278
+
279
+ [0.11.0]: https://github.com/pjm17971/pond-ts/compare/v0.10.1...v0.11.0
280
+
281
+ ## [0.10.1] — 2026-04-27
282
+
283
+ Strictly additive over v0.10.0. Closes the export gap surfaced by
284
+ the Codex CSV-cleaner v0.10 retest:
285
+
286
+ > `MaterializeSchema` exists in `dist/types.d.ts` but is not
287
+ > exported from the package root, so the script had to spell out
288
+ > the materialized schema locally for strict typing.
289
+
290
+ ### Added
291
+
292
+ - **`MaterializeSchema<S>`** now exported from the package root.
293
+ Users typing `materialize` output (or composing it into wrapper
294
+ utilities) can import the type directly from `pond-ts` instead
295
+ of digging into the dist-types.
296
+ - **`DedupeKeep<S>`** also exported (was the same gap — the type
297
+ for the `dedupe({ keep })` resolver function shape). Closes the
298
+ same friction for callers writing custom dedupe resolvers in
299
+ isolation.
300
+
301
+ [0.10.1]: https://github.com/pjm17971/pond-ts/compare/v0.10.0...v0.10.1
302
+
303
+ ## [0.10.0] — 2026-04-27
304
+
305
+ The "round-2 dashboard agent feedback" release. After v0.9.0
306
+ shipped the cross-entity correctness wave, three independent
307
+ agents (Codex CSV-cleaner, fresh CSV-cleaner eval, dashboard
308
+ agent) flagged refinements. v0.10 delivers all three:
309
+
310
+ - A grid-completion primitive that doesn't pre-pick a fill method
311
+ (Codex's "regularize without filling" friction)
312
+ - A terminal `toMap` that materializes the partition view directly
313
+ to a Map keyed by partition value (dashboard agent's
314
+ `.collect().groupBy(col, fn)` chain pain)
315
+ - Typed partition declaration via `groups` for narrowed Map keys
316
+ and declared-order iteration (dashboard agent's third
317
+ refinement; mirrors `pivotByGroup({ groups })`)
318
+
319
+ Strictly additive over v0.9.x — no behavior changes for existing
320
+ callers.
321
+
322
+ ### Added
323
+
324
+ - **`series.materialize(sequence, options?)`** — emits one
325
+ time-keyed row per sequence bucket, populating value columns
326
+ from a chosen source event in the bucket (or `undefined` for
327
+ empty buckets). Does only the grid step; pairs naturally with
328
+ `fill()` for explicit fill-policy control:
329
+
330
+ ```ts
331
+ series
332
+ .partitionBy('host')
333
+ .materialize(Sequence.every('1m'))
334
+ .fill({ cpu: 'linear' }, { maxGap: '3m' })
335
+ .collect();
336
+ ```
337
+
338
+ Three `select` modes: `'first'` / `'last'` (default) /
339
+ `'nearest'` — all bucket-bounded; empty buckets emit
340
+ `undefined` regardless. Three `sample` anchors:
341
+ `'begin'` (default) / `'center'` / `'end'`. Output schema
342
+ widens value columns to optional (`MaterializeSchema<S>`).
343
+
344
+ The `PartitionedTimeSeries.materialize` sugar auto-populates
345
+ the partition column on every output row, including
346
+ empty-bucket rows — without this, downstream code would need a
347
+ `.fill({ host: 'hold' })` step that fails for partitions where
348
+ every event sits in a long-outage gap.
349
+
350
+ Distinct from `align()` (which mandates a `'hold'` or
351
+ `'linear'` fill method and returns interval-keyed) and
352
+ `aggregate()` (which applies a per-column reducer). See
353
+ `cleaning.mdx` for the full operator-comparison table.
354
+
355
+ - **`PartitionedTimeSeries.toMap(transform?)`** — terminal that
356
+ returns `Map<key, TimeSeries<S>>` (or `Map<key, R>` with a
357
+ transform) directly from the partition view. Replaces the
358
+ `.collect().groupBy(col, fn)` chain dashboard code was using.
359
+
360
+ Three overloads cover the common shapes: bare per-partition
361
+ `TimeSeries`, transform that returns `TimeSeries<R>`, and
362
+ transform that returns arbitrary `R`. Map iteration order
363
+ matches the order each partition was first encountered in the
364
+ source events (or declared order when `groups` is set).
365
+
366
+ Map keys are stringified partition values for single-column
367
+ partitions (preserving the natural string representation:
368
+ `'api-1'`, `'eu'`, etc.), or JSON arrays for composite
369
+ partitions (`'["api-1","eu"]'`). `undefined` partition values
370
+ use the leading-space sentinel `' undefined'` to avoid
371
+ collision with the literal string `'undefined'` — distinct
372
+ from `groupBy`'s bare `'undefined'` key, which silently
373
+ collapses the two cases. Documented as an intentional
374
+ improvement; migrators changing from `.get('undefined')` to
375
+ `.get(' undefined')`.
376
+
377
+ **3.3× faster than the `.collect().groupBy(col, fn)` chain it
378
+ replaces** at 100k events × 10 hosts (33 ms vs 108 ms,
379
+ measured by `scripts/perf-partitioned-toMap.mjs`).
380
+
381
+ - **`series.partitionBy(col, { groups })` typed declaration**
382
+ — pre-declares the expected partition values, narrowing the
383
+ partition view's `K` type from `string` to the literal union.
384
+ Propagates through every sugar method's return type and through
385
+ `toMap`'s `Map` key:
386
+
387
+ ```ts
388
+ const HOSTS = ['api-1', 'api-2', 'api-3'] as const;
389
+ const byHost = series
390
+ .partitionBy('host', { groups: HOSTS })
391
+ .fill({ cpu: 'linear' })
392
+ .toMap();
393
+ // byHost: Map<'api-1' | 'api-2' | 'api-3', TimeSeries<S>>
394
+ ```
395
+
396
+ Mirrors `pivotByGroup({ groups })` — same design vocabulary,
397
+ same discipline: declared-order iteration, empty declared
398
+ groups produce empty entries, partition values not in `groups`
399
+ throw at construction time, empty `groups: []` and duplicate
400
+ values throw upfront, single-column only (composite + groups
401
+ throws). Numeric and boolean partition columns are stringified
402
+ by the encoder, so declared groups must be the stringified
403
+ form (`groups: ['1', '2']` for a numeric column).
404
+
405
+ - **Per-method `**Multi-entity series:**` JSDoc warnings**
406
+ remain on every stateful operator (shipped in v0.9.0); the
407
+ v0.10 operators (`materialize`, `toMap`) inherit the same
408
+ discoverability.
409
+
410
+ ### Changed
411
+
412
+ - **CLAUDE.md adds a perf-check policy.** New operators that walk
413
+ events, allocate per-event, or scale with input dimensions
414
+ must have an analytical complexity statement, a benchmark
415
+ script (`packages/core/scripts/perf-<operator>.mjs`), and
416
+ before/after numbers in the commit message. Surfaces in the
417
+ Layer 1 self-review checklist. Every v0.10 PR followed this:
418
+ `materialize` got `perf-materialize.mjs` (and two optimization
419
+ passes that landed –41% on the partitioned variant);
420
+ `toMap` got `perf-partitioned-toMap.mjs` (3.3× speedup
421
+ measurement); typed `groups` got `perf-partitionby-groups.mjs`
422
+ (zero chain-step regression via the class-private trusted
423
+ factory).
424
+
425
+ [0.10.0]: https://github.com/pjm17971/pond-ts/compare/v0.9.1...v0.10.0
426
+
427
+ ## [0.9.1] — 2026-04-26
428
+
429
+ Strictly additive over v0.9.0. Closes a sugar-method type bug
430
+ identified independently by two agents (a fresh CSV-cleaner eval
431
+ against v0.9.0 and Codex on a v0.9.0 retest), plus folds in two
432
+ fresh-agent doc improvements.
433
+
434
+ ### Fixed
435
+
436
+ - **`PartitionedTimeSeries.fill` now accepts `maxGap`.** PR #78
437
+ added `maxGap` to `TimeSeries.fill` for v0.9.0 but the partitioned
438
+ sugar's option type was not widened, so the headline v0.9.0 chain —
439
+ `partitionBy('host').fill('linear', { maxGap: '5m' })` — failed
440
+ type checking and forced callers into `.apply()`. The underlying
441
+ impl already passed options through, so this is a one-line type
442
+ widening: `{ limit?: number; maxGap?: DurationInput }`.
443
+
444
+ ### Added
445
+
446
+ - **9 new tests** under `TimeSeries.partitionBy.test.ts`:
447
+ - 4 regression tests pinning the partitioned `fill(maxGap)` chain
448
+ works (bare `maxGap`, all-or-nothing per-partition span,
449
+ `limit + maxGap` composition, full `partitionBy + dedupe +
450
+ fill(maxGap)` chain).
451
+ - 5 composite-key round-trip tests addressing a refinement flagged
452
+ by the dashboard agent: `partitionBy(['host', 'region'])`
453
+ preserves both key columns in the schema, on every output event,
454
+ keeps `(host, region)` tuples distinct (no collapse on host
455
+ alone), and round-trips through `apply()` and the full chain.
456
+ - **`cleaning.mdx` "Schema first — `required: false`" section.**
457
+ Leads the page; documents why optional cells need the flag and
458
+ surfaces the `fromJSON`/`null` workaround for the known
459
+ `RowForSchema` variance limitation. Previously this prose only
460
+ lived in the 0.8.2 changelog (fresh-agent feedback).
461
+ - **`cleaning.mdx` "End-to-end multi-entity cleaning pipeline"
462
+ section.** The unified `partitionBy + dedupe + fill(maxGap)`
463
+ chain in one place plus a step-by-step hazard table.
464
+ Previously split across three sections (fresh-agent feedback).
465
+
466
+ [0.9.1]: https://github.com/pjm17971/pond-ts/compare/v0.9.0...v0.9.1
467
+
468
+ ## [0.9.0] — 2026-04-26
469
+
470
+ The "cross-entity correctness + cleaning hygiene" release. Three
471
+ independent CSV-cleaner agent runs (Codex, Claude, Gemini) all hit
472
+ the same shape: stateful transforms (`fill('linear')`, `rolling`,
473
+ `diff`, etc.) silently mix data across entities on multi-host
474
+ series, and `fill('linear', { limit: 3 })` fabricates interpolated
475
+ data across long outages instead of leaving the unknown unknown.
476
+
477
+ v0.9.0 ships three operator-level fixes plus a discoverability pass
478
+ on every affected method's JSDoc.
479
+
480
+ ### Added
481
+
482
+ - **`series.partitionBy(col).<op>(...).collect()`** — chainable
483
+ per-partition view over `TimeSeries`. Sugar methods for every
484
+ stateful operator (`fill`, `align`, `rolling`, `smooth`,
485
+ `baseline`, `outliers`, `diff`, `rate`, `pctChange`, `cumulative`,
486
+ `shift`, `aggregate`, `dedupe`) run the underlying transform per
487
+ partition. `.collect()` materializes back to `TimeSeries<S>`.
488
+ `.apply(g => /* arbitrary chain */)` is the terminal escape hatch.
489
+ One primitive covers the cross-entity hazard for every at-risk
490
+ method, instead of adding a `partitionBy` option to each.
491
+ - **`series.dedupe({ keep })`** — first-class deduplication with
492
+ policies: `'first' | 'last' | 'error' | 'drop' | { min: col } |
493
+ { max: col } | (events) => Event`. Default key is the full event
494
+ key (`begin` for time-keyed, `begin+end` for time-range,
495
+ `begin+end+value` for interval-keyed); default resolution is
496
+ `'last'`. `partitionBy('host').dedupe()` is the multi-entity
497
+ pattern.
498
+ - **`fill(strategy, { maxGap })`** — duration-based gap cap,
499
+ complements the existing count-based `limit`. Both compose; most
500
+ restrictive wins.
501
+
502
+ ### Changed
503
+
504
+ - **`fill` is now all-or-nothing.** A gap either fits both caps and
505
+ is filled entirely, or exceeds either cap and is left fully
506
+ unfilled. Previously `limit: 3` on a 5-cell gap filled 3 cells and
507
+ left 2 unfilled — propagating stale `'hold'` values past their
508
+ useful lifetime and inventing misleading `'linear'` slopes across
509
+ long outages. Existing `limit` callers see strictly more
510
+ conservative behavior; to opt back in to partial fill, set
511
+ `limit`/`maxGap` larger than any gap you want filled.
512
+ - **Every stateful TimeSeries method's JSDoc** now includes a
513
+ `**Multi-entity series:**` warning paragraph naming the operator's
514
+ specific cross-entity hazard and pointing at the
515
+ `partitionBy(col).<method>(...).collect()` pattern. Discoverable
516
+ in LSP hover, IDE quick-help, and any tool that reads type
517
+ definitions.
518
+ - **`PartitionedTimeSeries` view** preserves partition state across
519
+ every sugar call, so multi-step per-partition chains compose
520
+ cleanly without re-partitioning at each step.
521
+
522
+ ### Fixed
523
+
524
+ - Pre-existing brand-check bug on `series.filter(...).diff(...)`
525
+ and similar chains: events constructed via
526
+ `#fromTrustedEvents` (which uses `Object.create` to bypass the
527
+ constructor) hit a JS-private brand check on `#diffOrRate` and
528
+ threw. Refactored to a class-static private (`static
529
+ #diffOrRate`) — runtime-private without the per-instance brand
530
+ failure.
531
+
532
+ [0.9.0]: https://github.com/pjm17971/pond-ts/compare/v0.8.2...v0.9.0
533
+
534
+ ## [0.8.2] — 2026-04-26
535
+
536
+ Strictly additive over v0.8.1. Closes friction surfaced by two
537
+ independent agent runs against a realistic CSV-cleaning task —
538
+ specifically, the missing fan-in primitive that forces callers out
539
+ of the typed contract when reassembling per-host transformed
540
+ subseries.
541
+
542
+ ### Added
543
+
544
+ - **`TimeSeries.concat([s1, s2, ...])`** — concatenates the events of
545
+ N same-schema `TimeSeries` instances, re-sorted by key. The
546
+ row-append / vertical-stack counterpart to `joinMany` (which
547
+ column-merges by key). Matches `Array.prototype.concat` /
548
+ `pandas.concat(axis=0)` / SQL `UNION ALL` semantics. Closes the
549
+ round-trip after `groupBy(col, fn)` + per-group transforms without
550
+ forcing callers to unwrap events back to row tuples.
551
+
552
+ ```ts
553
+ const filledByHost = series.groupBy('host', (g) =>
554
+ g.fill({ cpu: 'linear' }, { limit: 2 }),
555
+ );
556
+ const combined = TimeSeries.concat([...filledByHost.values()]);
557
+ // back to one TimeSeries<S>; events from all hosts re-sorted.
558
+ ```
559
+
560
+ Schemas must match column-by-column on `name` and `kind`; throws
561
+ upfront on mismatch. Same-key events from different inputs are
562
+ both kept (row-append, not key-dedupe).
563
+
564
+ Coming from pondjs: `timeSeriesListMerge`'s concatenation case
565
+ maps to `TimeSeries.concat([...])`; its column-union case maps to
566
+ `TimeSeries.joinMany([...])`.
567
+
568
+ - **`TimeSeries.fromEvents(events, { schema, name })`** — builds a
569
+ typed series from a flat `Event[]` array. Sorts by key. Companion
570
+ to `merge` for the case where you have raw events rather than a
571
+ list of series.
572
+
573
+ - **`TimeRange.toJSON()`** returns `{ start: number, end: number }`,
574
+ the same shape `JsonTimeRangeInput` accepts, so
575
+ `new TimeRange(range.toJSON())` round-trips. Implicitly invoked by
576
+ `JSON.stringify(range)`.
577
+
578
+ - **`TimeRange.toString()`** returns ISO-8601 `start/end` format
579
+ (e.g. `2025-01-15T09:00:00.000Z/2025-01-15T10:00:00.000Z`) for
580
+ debug logs and human-readable display.
581
+
582
+ ### Known limitation
583
+
584
+ Two type-level fixes flagged by the agents are tracked but deferred
585
+ to a future variance refactor:
586
+
587
+ - `toJSON()` returns `TimeSeriesJsonInput<SeriesSchema>` (loose),
588
+ not `TimeSeriesJsonInput<S>`. Cast the result at the call site
589
+ if you need the narrow schema preserved.
590
+ - `RowForSchema` doesn't honor `required: false`. Use `fromJSON`
591
+ with `null` cells instead of the row-array constructor with
592
+ `undefined`.
593
+
594
+ Both are real but blocked by class-wide invariance through method
595
+ overloads. See PLAN.md "Known type-level limitation" for the full
596
+ story.
597
+
598
+ ## [0.8.1] — 2026-04-26
599
+
600
+ Strictly additive over v0.8.0 — typed overload narrows result types when
601
+ opted in via `groups`; untyped form is unchanged. Plus a docs reorg.
602
+
603
+ ### Added
604
+
605
+ - **`pivotByGroup` typed overload** — pass `{ groups: [...] as const }`
606
+ and the output schema becomes literal-typed, so downstream
607
+ `baseline` / `rolling` / `toPoints` calls narrow without `as never`
608
+ casts. Eliminates the dashboard friction reported on v0.8.0.
609
+
610
+ ```ts
611
+ const HOSTS = ['api-1', 'api-2'] as const;
612
+ const wide = long.pivotByGroup('host', 'cpu', { groups: HOSTS });
613
+ // wide.schema is now literal-typed:
614
+ // [time, { name: 'api-1_cpu', kind: 'number', required: false },
615
+ // { name: 'api-2_cpu', kind: 'number', required: false }]
616
+ wide.baseline('api-1_cpu', { window: '1m', sigma: 2 }); // no cast
617
+ ```
618
+
619
+ Behavior in the typed path: declaration order (not alphabetical),
620
+ declared-but-empty groups still emit columns, runtime values not
621
+ in the declared set throw upfront. Untyped form (no `groups`)
622
+ keeps existing alphabetical / dynamic-discovery / loose-output
623
+ behavior.
624
+
625
+ ### Changed
626
+
627
+ - **Docs site reorganized.** `Transforms` → **TimeSeries**;
628
+ `Live` → **LiveSeries**; new **Advanced** section for charting and
629
+ array columns. Concepts moves to `Start here`. New **Reshaping**
630
+ page splits `pivotByGroup` / `groupBy` / `join` / `joinMany` from
631
+ Aggregation, plus a new **Queries** page covering `at` / `first` /
632
+ `timeRange` / `includesKey` / `intersection` / iterators / output
633
+ forms — everything that interrogates a series rather than
634
+ transforming it. JSON ingest renamed to **Ingest** and slotted as
635
+ the first page under TimeSeries.
636
+
637
+ ## [0.8.0] — 2026-04-25
638
+
639
+ ### Added
640
+
641
+ - **`TimeSeries.pivotByGroup(groupCol, valueCol, options?)`** — long-to-wide
642
+ reshape on a categorical column. Each distinct value of `groupCol` becomes
643
+ its own column in the output schema named `${group}_${value}`, holding the
644
+ value column at that timestamp. Rows sharing a timestamp collapse into one
645
+ output row; missing `(timestamp, group)` cells are `undefined`.
646
+
647
+ ```ts
648
+ // Long: { ts, cpu, host } per row
649
+ // Wide: { ts, "api-1_cpu", "api-2_cpu", ... } per row
650
+ long.pivotByGroup('host', 'cpu').toPoints();
651
+ // Drops straight into <Line dataKey="api-1_cpu" /> etc.
652
+ ```
653
+
654
+ Duplicate `(timestamp, group)` pairs throw by default; opt-in
655
+ `{ aggregate: 'avg' | 'sum' | 'first' | 'last' | 'min' | 'max' | 'median' | 'p95' | ... }`
656
+ to combine. The aggregator's output kind must match the value column's
657
+ kind — `count`, `unique`, `topN` and other kind-changing reducers are
658
+ rejected upfront with a clear error. Output schema is dynamic so the
659
+ return type is `TimeSeries<SeriesSchema>` (loosely typed). Time-keyed
660
+ input required.
661
+
662
+ Use `pivotByGroup` for the per-group dashboard case ("one source, many
663
+ producers, one chart line per producer"). Use `groupBy + joinMany` when
664
+ each group spawns multiple derived columns (e.g. per-host baseline →
665
+ cpu/avg/upper/lower per host). At 200k events × 100 groups, runs in
666
+ ~43 ms — at parity with hand-rolled JS that skips `TimeSeries`
667
+ construction entirely.
668
+
669
+ ### Changed
670
+
671
+ - Charting docs lead with `series.join(other, ...).toPoints()` for
672
+ cross-source overlays. The manual `mergeWideRows` recipe is demoted to
673
+ "non-`TimeSeries` inputs". A new "Per-group wide rows" section covers
674
+ `pivotByGroup` end-to-end with Recharts.
675
+
676
+ ### Notes
677
+
678
+ - **Live counterpart deferred.** No `LiveSeries.pivotByGroup` /
679
+ `LiveSeries.merge` / `LiveSeries.join` yet — see PLAN.md "Known scope
680
+ gap: live merge / join". Snapshot-then-batch is the workaround:
681
+ `useSnapshot` per source + `useMemo` running a batch `pivotByGroup` or
682
+ `join`.
683
+
684
+ ## [0.7.0] — 2026-04-25
685
+
686
+ ### Changed (breaking)
687
+
688
+ - **`TimeSeries.toPoints()` returns wide rows** instead of single-column
689
+ `{ ts, value }[]`. Every event becomes one row with `ts` plus every
690
+ value column from the schema as a top-level key:
691
+
692
+ ```ts
693
+ // Before: // After:
694
+ series.toPoints('cpu');
695
+ series.toPoints();
696
+ // [{ ts, value }, ...] // [{ ts, cpu, host, ... }, ...]
697
+ ```
698
+
699
+ This aligns pond-ts's multi-column nature with what every chart
700
+ library actually wants (Recharts, Observable Plot, visx all consume
701
+ wide rows directly). Band charts, multi-series overlays, and
702
+ `<Area>` ranged-`dataKey` patterns become a single `toPoints()`
703
+ call instead of a manual merge.
704
+
705
+ **Migration:** for the common single-column case, compose with
706
+ `select`:
707
+
708
+ ```ts
709
+ const cpuPoints = series.select('cpu').toPoints();
710
+ // [{ ts, cpu }, ...]
711
+ ```
712
+
713
+ Then read the column by name (`row.cpu`) instead of the old
714
+ `.value`. Wide form keeps every event — the old narrow form
715
+ dropped events whose column was `undefined`; the new form preserves
716
+ them so chart libraries can render gaps via `connectNulls={false}`.
717
+
718
+ **Watch out for `value`-named columns.** If your schema has a value
719
+ column literally named `value`, the new wide rows will have a
720
+ `value` key that looks identical to the old narrow shape — but it's
721
+ the column-named-`value`, not the narrow-form `value`. Audit any
722
+ `row.value` reads after upgrading; the safe migration is
723
+ `row.<schema-column-name>`.
724
+
725
+ - **`TimeSeries.fromPoints()` accepts wide-row points** with a schema
726
+ of any number of value columns. Schema's first column must still be
727
+ `kind: 'time'`.
728
+
729
+ ```ts
730
+ TimeSeries.fromPoints(
731
+ [{ ts: 0, cpu: 0.3, host: 'api-1' }, ...],
732
+ {
733
+ schema: [
734
+ { name: 'time', kind: 'time' },
735
+ { name: 'cpu', kind: 'number' },
736
+ { name: 'host', kind: 'string' },
737
+ ] as const,
738
+ },
739
+ );
740
+ ```
741
+
742
+ Previously restricted to exactly two columns with `{ ts, value }`
743
+ rows; that form is gone.
744
+
745
+ ## [0.6.0] — 2026-04-25
746
+
747
+ ### Added
748
+
749
+ - **`'end'` sample option** for `align()` and `Sequence.bounded()`. Joins
750
+ `'begin'` and `'center'` as a third anchor inside each grid step.
751
+ Useful for end-of-period readings (close-of-day, last value before
752
+ bucket close). Inclusion semantics are left-exclusive
753
+ (`sample ∈ (range.begin, range.end]`) so an end-sample at exactly
754
+ `range.begin()` doesn't pull in an interval that sits entirely
755
+ before the range.
756
+
757
+ ### Type-surface change
758
+
759
+ - `AlignSample` and `SequenceSample` literal unions widen from
760
+ `'begin' | 'center'` to `'begin' | 'center' | 'end'`. Pattern-matching
761
+ consumers that exhaustively `switch` on the old two-value union
762
+ silently miss the new arm — minor bump rather than a patch per this
763
+ project's "patch bumps are strictly additive" rule. Update any
764
+ `switch (sample)` blocks to handle `'end'` (or add a `default`).
765
+
766
+ ## [0.5.11] — 2026-04-24
767
+
768
+ ### Fixed
769
+
770
+ - **`LiveSeries` rejects `graceWindow > retention.maxAge` at construction.**
771
+ A late event accepted within grace but older than `maxAge` would be evicted
772
+ immediately by retention — the grace contract would be meaningless. The
773
+ guard only fires when both options are set explicitly; default behavior is
774
+ unchanged. `LiveAggregation` bucket closure (which inherits grace from the
775
+ source) still behaves as before.
776
+
777
+ ### Changed
778
+
779
+ - Docs: clarified `graceWindow`'s scope in the `LiveSeriesOptions`
780
+ docstring. Enforced at ingest and honored by `LiveAggregation` bucket
781
+ closure; `rolling()` / `window()` live views do not re-flow late events
782
+ through historical windows. Matches the actual pipeline behavior; full
783
+ late-event propagation through live transforms is explicitly out of
784
+ scope (see Akidau's Streaming 102 for the larger story).
785
+
786
+ ## [0.5.10] — 2026-04-24
787
+
788
+ ### Fixed
789
+
790
+ - **`baseline()` emits `undefined` for `upper` / `lower` when the rolling
791
+ window is flat (`sd === 0`)** — matching `outliers()`'s behavior. Before,
792
+ a zero-width band would cause a naive `value > upper || value < lower`
793
+ filter to flag every non-equal point as anomalous inside a constant run.
794
+ The `avg` and `sd` columns still report their true values; only the band
795
+ edges collapse to `undefined`.
796
+
797
+ ### Changed
798
+
799
+ - Internal: consolidated a duplicate `OptionalNumberCol` type alias into
800
+ the pre-existing `OptionalNumberColumn`. No surface change.
801
+ - Docs: walked back an over-claim in `outliers()`'s docstring. It was
802
+ documented as "sugar over `baseline().filter()`" but is implemented
803
+ independently. Now says the two are conceptually equivalent.
804
+
805
+ ## [0.5.9] — 2026-04-23
806
+
807
+ ### Added
808
+
809
+ - **`TimeSeries.baseline(col, opts)`** — rolling-stats primitive. Runs one
810
+ rolling pass and appends four optional number columns (`avg`, `sd`,
811
+ `upper = avg + σ·sd`, `lower = avg - σ·sd`) to the source schema. Band
812
+ charts read `toPoints('upper')` / `toPoints('lower')` directly; outlier
813
+ filters compare against `upper` / `lower`. Replaces the band-plus-outliers
814
+ two-pass pattern with one call. Custom column names via `{ names }` if the
815
+ defaults collide.
816
+
817
+ [0.8.2]: https://github.com/pjm17971/pond-ts/compare/v0.8.1...v0.8.2
818
+ [0.8.1]: https://github.com/pjm17971/pond-ts/compare/v0.8.0...v0.8.1
819
+ [0.8.0]: https://github.com/pjm17971/pond-ts/compare/v0.7.0...v0.8.0
820
+ [0.7.0]: https://github.com/pjm17971/pond-ts/compare/v0.6.0...v0.7.0
821
+ [0.6.0]: https://github.com/pjm17971/pond-ts/compare/v0.5.11...v0.6.0
822
+ [0.5.11]: https://github.com/pjm17971/pond-ts/compare/v0.5.10...v0.5.11
823
+ [0.5.10]: https://github.com/pjm17971/pond-ts/compare/v0.5.9...v0.5.10
824
+ [0.5.9]: https://github.com/pjm17971/pond-ts/compare/v0.5.8...v0.5.9
825
+
826
+ ## [0.5.8] — 2026-04-23
827
+
828
+ ### Added
829
+
830
+ - **`TimeSeries.outliers(col, { window, sigma, alignment? })`** —
831
+ rolling-baseline outlier detection. Returns `TimeSeries<S>` filtered to
832
+ events whose value deviates from the trailing rolling average by more than
833
+ `sigma · rolling_stdev`. Composes directly with aggregate, groupBy, etc.
834
+ - **`TimeSeries.prototype.toPoints(col)`** — flat `{ ts, value }[]` export
835
+ matching conventional chart-library shape (Recharts, Observable Plot, d3).
836
+ Filters `undefined` values; returns a frozen array.
837
+ - **`TimeSeries.fromPoints(points, { schema, name? })`** — inverse
838
+ constructor for round-tripping chart-style points back into pond-native
839
+ operations. Schema must have exactly two columns.
840
+
841
+ [0.5.8]: https://github.com/pjm17971/pond-ts/compare/v0.5.7...v0.5.8
842
+
843
+ ## [0.5.7] — 2026-04-23
844
+
845
+ ### Added
846
+
847
+ - **`smooth('ema', { warmup: N })`** — drops the first `N` output rows so
848
+ callers don't have to write `.slice(N)` after every EMA call. The smoother
849
+ still processes those events, so kept rows are computed against a warm EMA.
850
+ `warmup: 0` is a no-op; warmup ≥ series length returns an empty series.
851
+
852
+ [0.5.7]: https://github.com/pjm17971/pond-ts/compare/v0.5.6...v0.5.7
853
+
854
+ ## [0.5.6] — 2026-04-23
855
+
856
+ ### Added
857
+
858
+ - **`useCurrent` reference stability** — the returned record and each of its
859
+ fields are reference-stable across renders when structurally unchanged. A
860
+ no-op push (same aggregate values) hands back the previous references;
861
+ downstream `useMemo([current.host], ...)` only re-runs when that specific
862
+ field changes. Scalar fields compare via `===`; array fields compare length
863
+ then elementwise.
864
+
865
+ [0.5.6]: https://github.com/pjm17971/pond-ts/compare/v0.5.5...v0.5.6
866
+
867
+ ## [0.5.5] — 2026-04-23
868
+
869
+ ### Added
870
+
871
+ - **Narrow return types for `rolling` + `aggregate` output-map overloads.**
872
+ `rolling(w, { avg: { from: 'cpu', using: 'avg' }, ... })` now returns
873
+ `TimeSeries<RollingOutputMapSchema<S, M>>` — `e.get('avg')` narrows to
874
+ `number | undefined` instead of `ColumnValue | undefined`, and `e.key()`
875
+ preserves the source's first-column kind. Same fix on `aggregate`'s
876
+ output-map overload.
877
+
878
+ ### Fixed
879
+
880
+ - `min` / `max` were missing from the numeric-reducer list in `ReduceResult`
881
+ (v0.5.2 regression). Both reducers have `outputKind: 'number'` at runtime;
882
+ the type now agrees. `reduce({ cpu: 'max' })` narrows to `number | undefined`.
883
+
884
+ [0.5.5]: https://github.com/pjm17971/pond-ts/compare/v0.5.4...v0.5.5
885
+
886
+ ## [0.5.4] — 2026-04-23
887
+
888
+ ### Added
889
+
890
+ - **`rolling` accepts `AggregateOutputMap`** — feature parity with
891
+ `aggregate`. Multi-reducer-per-column now works in one pass:
892
+ ```ts
893
+ series.rolling('1m', {
894
+ avg: { from: 'cpu', using: 'avg' },
895
+ sd: { from: 'cpu', using: 'stdev' },
896
+ });
897
+ ```
898
+ Two new overloads on both window-only and sequence-driven forms.
899
+
900
+ ### Changed
901
+
902
+ - `rolling`'s internal column walker now routes through the shared
903
+ `normalizeAggregateColumns` helper. Schema-column order is preserved for
904
+ `AggregateMap` inputs so the runtime layout continues to match
905
+ `RollingSchema<S, M>`.
906
+
907
+ [0.5.4]: https://github.com/pjm17971/pond-ts/compare/v0.5.3...v0.5.4
908
+
909
+ ## [0.5.3] — 2026-04-23
910
+
911
+ ### Added
912
+
913
+ - **Source-kind narrowing on array-output reducers in `ReduceResult`.**
914
+ `unique` and `` `top${number}` `` now narrow their output to
915
+ `ReadonlyArray<T>` where `T` is the source column's element type:
916
+ ```ts
917
+ series.reduce({ host: 'unique' }).host;
918
+ // ^ ReadonlyArray<string> | undefined (was ReadonlyArray<ScalarValue>)
919
+ ```
920
+ Array-kind source columns fall back to the wide `ReadonlyArray<ScalarValue>`
921
+ union since element kind isn't schema-visible.
922
+
923
+ [0.5.3]: https://github.com/pjm17971/pond-ts/compare/v0.5.2...v0.5.3
924
+
925
+ ## [0.5.2] — 2026-04-23
926
+
927
+ ### Added
928
+
929
+ - **`TimeSeries.reduce` per-entry type narrowing.** Numeric reducers
930
+ (`sum`/`avg`/`count`/`median`/`stdev`/`difference`/`pNN`) narrow to
931
+ `number | undefined`; `unique`/`top${N}` narrow to `ReadonlyArray<…> |
932
+ undefined`; `first`/`last`/`keep` preserve the source column kind. Custom
933
+ reducer functions and `AggregateOutputSpec` entries keep the wide
934
+ `ColumnValue | undefined` fallback. Narrowing lives in the new
935
+ `types-reduce.ts` — same file-split pattern used later for the output-map
936
+ narrowing.
937
+
938
+ ### Changed
939
+
940
+ - `useCurrent` now aliases `ReduceResult<S, Mapping>` directly; the hook's
941
+ duplicated narrowing logic is gone.
942
+
943
+ [0.5.2]: https://github.com/pjm17971/pond-ts/compare/v0.5.1...v0.5.2
944
+
945
+ ## [0.5.1] — 2026-04-23
946
+
947
+ ### Added
948
+
949
+ - **`TimeSeries.tail(duration?)`** — trailing temporal slice, the
950
+ counterpart to `Array.slice(-n)`. Called with no argument, returns the
951
+ whole series. Composes with every other `TimeSeries` method.
952
+ - **`useCurrent` hook (`@pond-ts/react`)** — subscribes to a live source and
953
+ returns the current value of a reducer mapping. Signature:
954
+ `useCurrent(source, mapping, { tail?, throttle? })`. Stable-shape record
955
+ even while the source is empty, so destructuring on first render is safe.
956
+
957
+ [0.5.1]: https://github.com/pjm17971/pond-ts/compare/v0.5.0...v0.5.1
958
+
959
+ ## [0.5.0] — 2026-04-23
960
+
961
+ ### Added
962
+
963
+ - **First-class `'array'` column kind.** New `ArrayValue = ReadonlyArray<ScalarValue>`
964
+ and `ColumnValue = ScalarValue | ArrayValue` types. Array columns are inert
965
+ with respect to numerical operators (`diff`, `rate`, `cumulative`,
966
+ `rolling`-over-numbers skip them automatically via `NumericColumnNameForSchema`).
967
+ - **`unique` reducer** — distinct sorted values; works in `reduce`,
968
+ `aggregate`, and `rolling`. Flattens array-kind sources one level (set union
969
+ across arrays in a bucket).
970
+ - **`top(n)` reducer** — top N values by frequency with deterministic
971
+ tie-break. String-pattern dispatch (`'top3'`, `'top10'`) parallel to `pNN`,
972
+ plus a `top(n)` helper that returns the typed string literal. Incremental
973
+ bucket + rolling state via a count map. Also flattens array-kind sources.
974
+ - **Five array-prefixed operators on `TimeSeries`**:
975
+ - `arrayContains(col, value)` — has this one
976
+ - `arrayContainsAll(col, values)` — has every one (AND / subset)
977
+ - `arrayContainsAny(col, values)` — has at least one (OR / intersection)
978
+ - `arrayAggregate(col, reducer, { as?, kind? })` — per-event reduction
979
+ reusing the full reducer registry (count, sum, avg, unique, custom, etc.).
980
+ Replace in place or append via `as`.
981
+ - `arrayExplode(col, { as?, kind? })` — fan each event out into one event
982
+ per array element. Replace the array column or keep it alongside a scalar
983
+ sibling.
984
+ - **LiveSeries accepts `kind: 'array'`** on its schema with array cells
985
+ frozen on push.
986
+ - **JSON round-trip** for array cells works unchanged (toJSON / fromJSON
987
+ pass arrays through naturally).
988
+ - **Docs**: new `guides/arrays.mdx` reference page;
989
+ `examples/error-rate-dashboard.mdx` scenario walkthrough backed 1:1 by an
990
+ E2E test; `reducer-reference.mdx` expanded with concrete input/output
991
+ examples for `unique` and `top(n)`.
992
+
993
+ ### Changed
994
+
995
+ - **`reduce()` / `ReduceResult` / `CustomAggregateReducer` return types** widened
996
+ from `ScalarValue | undefined` to `ColumnValue | undefined`. Narrowed
997
+ annotations (`: number | undefined`) keep working; only callers with
998
+ explicit `: ScalarValue | undefined` annotations need to widen.
999
+ (v0.5.2 narrows these further per-entry.)
1000
+
1001
+ [0.5.0]: https://github.com/pjm17971/pond-ts/compare/v0.4.3...v0.5.0
1002
+
1003
+ ## [0.4.3] — 2026-04-22
1004
+
1005
+ ### Added
1006
+
1007
+ - `useLiveQuery` and `useLatest` hooks in `@pond-ts/react`.
1008
+
1009
+ ### Fixed
1010
+
1011
+ - LiveView eviction mirroring (uses `EMITS_EVICT` symbol to safely detect
1012
+ evict-capable sources; avoids duck-typing that broke on `LiveAggregation`).
1013
+ - Type narrowing through `LiveAggregation` / `LiveRollingAggregation` via
1014
+ `Out` type parameter.
1015
+ - `Time.toDate()` convenience method.
1016
+ - `useWindow` under React StrictMode (view creation moved to `useEffect`).
1017
+ - `TimeSeries[Symbol.iterator]` and `toArray()` for ergonomic iteration.
1018
+ - `useSnapshot` accepts `SnapshotSource<S>` structural type (no casts for
1019
+ `LiveAggregation` input).
1020
+
1021
+ [0.4.3]: https://github.com/pjm17971/pond-ts/compare/v0.4.2...v0.4.3
1022
+
1023
+ ## [0.4.2] — 2026-04-21
1024
+
1025
+ ### Changed
1026
+
1027
+ - First release using npm OIDC Trusted Publisher (no stored tokens).
1028
+
1029
+ [0.4.2]: https://github.com/pjm17971/pond-ts/compare/v0.4.1...v0.4.2
1030
+
1031
+ ## [0.4.1] — 2026-04-21
1032
+
1033
+ Administrative — no behavioral changes.
1034
+
1035
+ [0.4.1]: https://github.com/pjm17971/pond-ts/compare/v0.4.0...v0.4.1
1036
+
1037
+ ## [0.4.0] — 2026-04-21
1038
+
1039
+ ### Added
1040
+
1041
+ - **`@pond-ts/react` package** — React hooks for live series
1042
+ (`useLiveSeries`, `useTimeSeries`, `useSnapshot`, `useWindow`, `useDerived`,
1043
+ `takeSnapshot`). Monorepo restructure completed.
1044
+ - **LiveView + LiveSource composition** — `filter`, `map`, `select`,
1045
+ `window` views that compose with `LiveAggregation` / `LiveRollingAggregation`
1046
+ via a shared `LiveSource<S>` interface.
1047
+ - **Live per-event and carry-forward transforms** — `diff`, `rate`,
1048
+ `pctChange`, `fill`, `cumulative` available as LiveView variants.
1049
+ - **Grace period on `LiveAggregation`** — delays bucket closing so
1050
+ out-of-order events within a window accumulate into their correct bucket.
1051
+ Defaults from source `LiveSeries`'s `graceWindow`.
1052
+ - **Streaming dashboard example** with E2E tests.
1053
+ - **Benchmark suite** comparing `pond-ts` vs `pondjs`.
1054
+
1055
+ [0.4.0]: https://github.com/pjm17971/pond-ts/compare/v0.3.0...v0.4.0
1056
+
1057
+ ## [0.3.0] — 2026-04-21
1058
+
1059
+ ### Added
1060
+
1061
+ - **`LiveSeries`** — mutable, append-optimized streaming buffer sharing the
1062
+ same schema type as `TimeSeries`. Retention policies (`maxEvents`,
1063
+ `maxAge`, `maxBytes`). Synchronous subscriptions (`event`, `batch`,
1064
+ `evict`). Ordering modes (`strict`, `drop`, `reorder`).
1065
+ - **`LiveAggregation`** — incremental bucketed aggregation over a
1066
+ `LiveSource`.
1067
+ - **`LiveRollingAggregation`** — sliding-window reduction over a
1068
+ `LiveSource`.
1069
+
1070
+ [0.3.0]: https://github.com/pjm17971/pond-ts/compare/v0.2.0...v0.3.0
1071
+
1072
+ ## [0.2.0] — 2026-04-16
1073
+
1074
+ ### Added
1075
+
1076
+ - **Phase 2 batch expansion**: `reduce`, `groupBy`, `diff`, `rate`, `fill`.
1077
+ - **Phase 2.5 columnar primitives**: `pctChange`, `cumulative`, `shift`,
1078
+ `bfill` fill strategy.
1079
+ - **Aggregator parity with pondjs**: `median`, `stdev`, `percentile`
1080
+ (`pNN`), `difference`, `keep`.
1081
+
1082
+ [0.2.0]: https://github.com/pjm17971/pond-ts/compare/v0.1.4...v0.2.0
1083
+
1084
+ ## [0.1.x] — 2026-04-16
1085
+
1086
+ Phase 0 (core performance) and Phase 1 (batch hardening) releases. Five
1087
+ critical O(N²) hot paths optimized (172× aggregate, 182× rolling, 15×
1088
+ movingAverage, 7.5× loess, 819× includesKey, 134× alignLinearAt).
1089
+ `toJSON`/`fromJSON` round-trip, custom aggregate reducers, edge-case
1090
+ coverage across every analytical primitive.
1091
+
1092
+ See [tag history](https://github.com/pjm17971/pond-ts/tags) for details.