@pond-ts/react 0.11.4 → 0.11.6

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