pond-ts 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +133 -1
  2. package/dist/batch/operators/map.d.ts +14 -2
  3. package/dist/batch/operators/map.js +35 -3
  4. package/dist/batch/partitioned-time-series.d.ts +4 -18
  5. package/dist/batch/time-series.d.ts +13 -22
  6. package/dist/batch/time-series.js +104 -27
  7. package/dist/batch/validate.js +2 -2
  8. package/dist/cjs-fallback.cjs +15 -0
  9. package/dist/columnar/index.d.ts +1 -1
  10. package/dist/columnar/index.js +1 -1
  11. package/dist/columnar/view.d.ts +25 -1
  12. package/dist/columnar/view.js +26 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/live/live-aggregation.d.ts +2 -2
  15. package/dist/live/live-partitioned-series.d.ts +8 -31
  16. package/dist/live/live-rolling-aggregation.d.ts +2 -2
  17. package/dist/live/live-series.d.ts +5 -10
  18. package/dist/live/live-series.js +2 -1
  19. package/dist/live/live-view.d.ts +5 -10
  20. package/dist/live/live-view.js +1 -0
  21. package/dist/reducers/stdev.js +81 -59
  22. package/dist/schema/aggregate.d.ts +162 -64
  23. package/dist/schema/events.d.ts +10 -0
  24. package/dist/schema/index.d.ts +1 -2
  25. package/dist/schema/index.js +8 -0
  26. package/dist/schema/reduce.d.ts +34 -4
  27. package/dist/schema/rolling.d.ts +1 -1
  28. package/package.json +4 -3
  29. package/dist/batch/aggregate-columns.d.ts.map +0 -1
  30. package/dist/batch/aggregate-columns.js.map +0 -1
  31. package/dist/batch/index.d.ts.map +0 -1
  32. package/dist/batch/index.js.map +0 -1
  33. package/dist/batch/json.d.ts.map +0 -1
  34. package/dist/batch/json.js.map +0 -1
  35. package/dist/batch/operators/collapse.d.ts.map +0 -1
  36. package/dist/batch/operators/collapse.js.map +0 -1
  37. package/dist/batch/operators/cumulative.d.ts.map +0 -1
  38. package/dist/batch/operators/cumulative.js.map +0 -1
  39. package/dist/batch/operators/diff-rate.d.ts.map +0 -1
  40. package/dist/batch/operators/diff-rate.js.map +0 -1
  41. package/dist/batch/operators/fill.d.ts.map +0 -1
  42. package/dist/batch/operators/fill.js.map +0 -1
  43. package/dist/batch/operators/map.d.ts.map +0 -1
  44. package/dist/batch/operators/map.js.map +0 -1
  45. package/dist/batch/operators/shift.d.ts.map +0 -1
  46. package/dist/batch/operators/shift.js.map +0 -1
  47. package/dist/batch/partitioned-time-series.d.ts.map +0 -1
  48. package/dist/batch/partitioned-time-series.js.map +0 -1
  49. package/dist/batch/time-series.d.ts.map +0 -1
  50. package/dist/batch/time-series.js.map +0 -1
  51. package/dist/batch/validate.d.ts.map +0 -1
  52. package/dist/batch/validate.js.map +0 -1
  53. package/dist/column.d.ts.map +0 -1
  54. package/dist/column.js.map +0 -1
  55. package/dist/columnar/array-column.d.ts.map +0 -1
  56. package/dist/columnar/array-column.js.map +0 -1
  57. package/dist/columnar/builder.d.ts.map +0 -1
  58. package/dist/columnar/builder.js.map +0 -1
  59. package/dist/columnar/chunked-column.d.ts.map +0 -1
  60. package/dist/columnar/chunked-column.js.map +0 -1
  61. package/dist/columnar/column.d.ts.map +0 -1
  62. package/dist/columnar/column.js.map +0 -1
  63. package/dist/columnar/concat.d.ts.map +0 -1
  64. package/dist/columnar/concat.js.map +0 -1
  65. package/dist/columnar/index.d.ts.map +0 -1
  66. package/dist/columnar/index.js.map +0 -1
  67. package/dist/columnar/key-column.d.ts.map +0 -1
  68. package/dist/columnar/key-column.js.map +0 -1
  69. package/dist/columnar/ring-buffer.d.ts.map +0 -1
  70. package/dist/columnar/ring-buffer.js.map +0 -1
  71. package/dist/columnar/scatter.d.ts.map +0 -1
  72. package/dist/columnar/scatter.js.map +0 -1
  73. package/dist/columnar/store.d.ts.map +0 -1
  74. package/dist/columnar/store.js.map +0 -1
  75. package/dist/columnar/string-column.d.ts.map +0 -1
  76. package/dist/columnar/string-column.js.map +0 -1
  77. package/dist/columnar/types.d.ts.map +0 -1
  78. package/dist/columnar/types.js.map +0 -1
  79. package/dist/columnar/validity.d.ts.map +0 -1
  80. package/dist/columnar/validity.js.map +0 -1
  81. package/dist/columnar/view.d.ts.map +0 -1
  82. package/dist/columnar/view.js.map +0 -1
  83. package/dist/core/calendar.d.ts.map +0 -1
  84. package/dist/core/calendar.js.map +0 -1
  85. package/dist/core/duration.d.ts.map +0 -1
  86. package/dist/core/duration.js.map +0 -1
  87. package/dist/core/errors.d.ts.map +0 -1
  88. package/dist/core/errors.js.map +0 -1
  89. package/dist/core/event.d.ts.map +0 -1
  90. package/dist/core/event.js.map +0 -1
  91. package/dist/core/index.d.ts.map +0 -1
  92. package/dist/core/index.js.map +0 -1
  93. package/dist/core/interval.d.ts.map +0 -1
  94. package/dist/core/interval.js.map +0 -1
  95. package/dist/core/temporal.d.ts.map +0 -1
  96. package/dist/core/temporal.js.map +0 -1
  97. package/dist/core/time-range.d.ts.map +0 -1
  98. package/dist/core/time-range.js.map +0 -1
  99. package/dist/core/time.d.ts.map +0 -1
  100. package/dist/core/time.js.map +0 -1
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/index.js.map +0 -1
  103. package/dist/live/index.d.ts.map +0 -1
  104. package/dist/live/index.js.map +0 -1
  105. package/dist/live/live-aggregation.d.ts.map +0 -1
  106. package/dist/live/live-aggregation.js.map +0 -1
  107. package/dist/live/live-chunked-storage.d.ts.map +0 -1
  108. package/dist/live/live-chunked-storage.js.map +0 -1
  109. package/dist/live/live-fused-rolling.d.ts.map +0 -1
  110. package/dist/live/live-fused-rolling.js.map +0 -1
  111. package/dist/live/live-history.d.ts.map +0 -1
  112. package/dist/live/live-history.js.map +0 -1
  113. package/dist/live/live-partitioned-fused-rolling.d.ts.map +0 -1
  114. package/dist/live/live-partitioned-fused-rolling.js.map +0 -1
  115. package/dist/live/live-partitioned-series.d.ts.map +0 -1
  116. package/dist/live/live-partitioned-series.js.map +0 -1
  117. package/dist/live/live-partitioned-sync-rolling.d.ts.map +0 -1
  118. package/dist/live/live-partitioned-sync-rolling.js.map +0 -1
  119. package/dist/live/live-reduce.d.ts.map +0 -1
  120. package/dist/live/live-reduce.js.map +0 -1
  121. package/dist/live/live-rolling-aggregation.d.ts.map +0 -1
  122. package/dist/live/live-rolling-aggregation.js.map +0 -1
  123. package/dist/live/live-series.d.ts.map +0 -1
  124. package/dist/live/live-series.js.map +0 -1
  125. package/dist/live/live-storage.d.ts.map +0 -1
  126. package/dist/live/live-storage.js.map +0 -1
  127. package/dist/live/live-view.d.ts.map +0 -1
  128. package/dist/live/live-view.js.map +0 -1
  129. package/dist/live/series-store.d.ts.map +0 -1
  130. package/dist/live/series-store.js.map +0 -1
  131. package/dist/live/triggers.d.ts.map +0 -1
  132. package/dist/live/triggers.js.map +0 -1
  133. package/dist/reducers/avg.d.ts.map +0 -1
  134. package/dist/reducers/avg.js.map +0 -1
  135. package/dist/reducers/count.d.ts.map +0 -1
  136. package/dist/reducers/count.js.map +0 -1
  137. package/dist/reducers/difference.d.ts.map +0 -1
  138. package/dist/reducers/difference.js.map +0 -1
  139. package/dist/reducers/first.d.ts.map +0 -1
  140. package/dist/reducers/first.js.map +0 -1
  141. package/dist/reducers/index.d.ts.map +0 -1
  142. package/dist/reducers/index.js.map +0 -1
  143. package/dist/reducers/keep.d.ts.map +0 -1
  144. package/dist/reducers/keep.js.map +0 -1
  145. package/dist/reducers/last.d.ts.map +0 -1
  146. package/dist/reducers/last.js.map +0 -1
  147. package/dist/reducers/max.d.ts.map +0 -1
  148. package/dist/reducers/max.js.map +0 -1
  149. package/dist/reducers/median.d.ts.map +0 -1
  150. package/dist/reducers/median.js.map +0 -1
  151. package/dist/reducers/min.d.ts.map +0 -1
  152. package/dist/reducers/min.js.map +0 -1
  153. package/dist/reducers/percentile.d.ts.map +0 -1
  154. package/dist/reducers/percentile.js.map +0 -1
  155. package/dist/reducers/rolling.d.ts.map +0 -1
  156. package/dist/reducers/rolling.js.map +0 -1
  157. package/dist/reducers/samples.d.ts.map +0 -1
  158. package/dist/reducers/samples.js.map +0 -1
  159. package/dist/reducers/stdev.d.ts.map +0 -1
  160. package/dist/reducers/stdev.js.map +0 -1
  161. package/dist/reducers/sum.d.ts.map +0 -1
  162. package/dist/reducers/sum.js.map +0 -1
  163. package/dist/reducers/top.d.ts.map +0 -1
  164. package/dist/reducers/top.js.map +0 -1
  165. package/dist/reducers/types.d.ts.map +0 -1
  166. package/dist/reducers/types.js.map +0 -1
  167. package/dist/reducers/unique.d.ts.map +0 -1
  168. package/dist/reducers/unique.js.map +0 -1
  169. package/dist/schema/aggregate.d.ts.map +0 -1
  170. package/dist/schema/aggregate.js.map +0 -1
  171. package/dist/schema/diff.d.ts.map +0 -1
  172. package/dist/schema/diff.js.map +0 -1
  173. package/dist/schema/events.d.ts.map +0 -1
  174. package/dist/schema/events.js.map +0 -1
  175. package/dist/schema/index.d.ts.map +0 -1
  176. package/dist/schema/index.js.map +0 -1
  177. package/dist/schema/join.d.ts.map +0 -1
  178. package/dist/schema/join.js.map +0 -1
  179. package/dist/schema/json.d.ts.map +0 -1
  180. package/dist/schema/json.js.map +0 -1
  181. package/dist/schema/public.d.ts.map +0 -1
  182. package/dist/schema/public.js.map +0 -1
  183. package/dist/schema/reduce.d.ts.map +0 -1
  184. package/dist/schema/reduce.js.map +0 -1
  185. package/dist/schema/reshape.d.ts.map +0 -1
  186. package/dist/schema/reshape.js.map +0 -1
  187. package/dist/schema/rolling.d.ts.map +0 -1
  188. package/dist/schema/rolling.js.map +0 -1
  189. package/dist/schema/series.d.ts.map +0 -1
  190. package/dist/schema/series.js.map +0 -1
  191. package/dist/sequence/bounded-sequence.d.ts.map +0 -1
  192. package/dist/sequence/bounded-sequence.js.map +0 -1
  193. package/dist/sequence/index.d.ts.map +0 -1
  194. package/dist/sequence/index.js.map +0 -1
  195. package/dist/sequence/sample.d.ts.map +0 -1
  196. package/dist/sequence/sample.js.map +0 -1
  197. package/dist/sequence/sequence.d.ts.map +0 -1
  198. package/dist/sequence/sequence.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -7,7 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7
7
  file covers both packages. Pre-1.0: minor bumps may include new features and
8
8
  type-level changes; patch bumps are strictly additive.
9
9
 
10
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.21.0...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.23.0...HEAD
11
+ [0.23.0]: https://github.com/pjm17971/pond-ts/compare/v0.22.0...v0.23.0
12
+ [0.22.0]: https://github.com/pjm17971/pond-ts/compare/v0.21.0...v0.22.0
11
13
  [0.21.0]: https://github.com/pjm17971/pond-ts/compare/v0.20.0...v0.21.0
12
14
  [0.20.0]: https://github.com/pjm17971/pond-ts/compare/v0.19.0...v0.20.0
13
15
  [0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
@@ -15,6 +17,136 @@ type-level changes; patch bumps are strictly additive.
15
17
 
16
18
  ## [Unreleased]
17
19
 
20
+ ## [0.23.0] — 2026-06-13
21
+
22
+ ### Added
23
+
24
+ - **`new TimeSeries({ …, sort: true })` (and `TimeSeries.fromJSON`) sort rows by
25
+ key on construction.** Pond requires rows in non-decreasing key order and
26
+ throws otherwise; `sort: true` accepts unsorted input (messy CSVs, merged
27
+ sources) and sorts it for you instead of forcing a manual pre-sort. The sort
28
+ is **stable** — rows with equal keys keep their input order — matching what
29
+ `TimeSeries.fromEvents` already does. The out-of-order error now names the
30
+ option. (Audit v2 §5 F3.)
31
+
32
+ ### Changed
33
+
34
+ - **CommonJS consumers now get a clear error instead of
35
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`.** Both `pond-ts` and `@pond-ts/react`
36
+ add a `require` condition to their `exports["."]` map pointing at a tiny
37
+ shipped CJS stub that throws an ESM-only message naming `import` as the
38
+ fix. The packages remain ES-module-only; this only improves the error a
39
+ `require('pond-ts')` caller sees. (Audit v2 §5 F6/F7/F9/F10/F11)
40
+ - **Published tarballs no longer ship `*.js.map` / `*.d.ts.map` source
41
+ maps.** The maps referenced a `../src` tree that was never included in
42
+ the tarball (`files: ["dist", …]`), so they were dead weight (~⅓ of the
43
+ unpacked size). A `prepack` step now strips them from the published
44
+ artifact for both packages; local `npm run build` still emits them, so
45
+ in-repo debugging is unaffected. (Audit v2 §5 F6/F7/F9/F10/F11)
46
+
47
+ ### Fixed
48
+
49
+ - **Shipped `.d.ts` now type-check under `skipLibCheck: false`.** The internal
50
+ `EMITS_EVICT` marker symbol was `@internal` (stripped from the emitted
51
+ `series.d.ts`) but still referenced by un-stripped public declarations — a
52
+ by-name re-export in `schema/index.d.ts` and the `[EMITS_EVICT]` brand members
53
+ on `LiveSeries` / `LiveView` — leaving dangling references that broke strict
54
+ consumer builds with **TS2305**. Those references are now `@internal` too, so
55
+ the symbol is fully stripped from the published types; runtime behavior is
56
+ unchanged. (Audit v2 §5 F2.)
57
+ - **`TimeSeries.at(-1)` counts from the end**, matching `LiveSeries.at` and
58
+ `Array.prototype.at` (it previously returned `undefined` for any negative
59
+ index). Deep underflow (e.g. `at(-100)` on a 3-event series) still returns
60
+ `undefined`, and the non-integer / `NaN` guard is unchanged. (Audit v2 §5 F8.)
61
+ - **Docs: corrected `Time.asString()` (does not exist), the missing
62
+ `aggregate`/`materialize` → `pivotByGroup` rekey pointer, and an
63
+ inaccurate `rolling().value()` return-type example.** The getting-started
64
+ example now calls `event.key().toDate().toISOString()`; the aggregation
65
+ and reshape pages note that interval-keyed output must be rekeyed with
66
+ `.asTime({ at: 'begin' })` before a time-keyed transform like
67
+ `pivotByGroup` (whose runtime error now says so too); the rolling page
68
+ documents `value()` as `Record<string, ColumnValue | undefined>`.
69
+ (Audit v2 §5 F6/F7/F9/F10/F11)
70
+ - **Mixed shorthand + `{ from, using }` mappings now keep every output
71
+ column in the result type (Audit v2 §5 F1).** Calling
72
+ `aggregate` / `rolling` / `reduce` with a mapping that mixes the
73
+ shorthand form (`cpu: 'avg'`) and the spec form
74
+ (`cpu_p95: { from: 'cpu', using: 'p95' }`) in one call — the
75
+ docs-blessed pattern — previously resolved to the shorthand overload
76
+ and **silently dropped every spec-keyed output column from the result
77
+ type** (`event.get('cpu_p95')` failed to compile with `TS2345`), even
78
+ though the runtime emitted the column. The two overloads
79
+ (`AggregateMap` shorthand + `AggregateOutputMap` spec) are now
80
+ collapsed into one unified mapping shape whose result schema dispatches
81
+ per output key, so all columns survive and each narrows to its
82
+ reducer's output kind. Runtime behavior is unchanged — this is a
83
+ types-only fix plus the tests that should have caught it.
84
+ - **The unified mapping keeps the shorthand compile-time guards.** A
85
+ shorthand reducer is still kind-checked against its source column
86
+ (`host: 'avg'` on a `string` column stays a compile error), and a bare
87
+ reducer on a key that is not a source column (`ghost: 'avg'` — a typo
88
+ the runtime rejects with "unknown source column") is now a compile
89
+ error too. Spec keys (`{ from, using }`) remain free output names.
90
+ Inline mapping literals get full validation; values pre-widened to
91
+ `AggregateMap<S>` and broad-schema (`TimeSeries<SeriesSchema>`)
92
+ callers keep the permissive shape. `AggregateOutputMap` is retained
93
+ as a back-compat alias of `AggregateMap`.
94
+
95
+ ## [0.22.0] — 2026-06-12
96
+
97
+ ### Changed
98
+
99
+ - **`asTime` / `asTimeRange` / `asInterval` are now column-native.** They
100
+ reinterpret the key's kind (a "rekey") straight off the existing key's
101
+ `begin` / `end` buffers instead of materializing events — value columns pass
102
+ through by reference. `asTimeRange` and `asTime` with `begin` / `end` reuse
103
+ the key buffer zero-copy (≈ **9×** faster on a build → rekey → read pipeline);
104
+ `asTime({ at: 'center' })` adds one midpoint pass; `asInterval` builds the
105
+ label column (string → `StringColumn`, number → `Float64Column`, inferred
106
+ from the first label and required consistent across rows). `asTime` with
107
+ `center` / `end` throws if anchoring a source with overlapping extents would
108
+ produce a non-monotonic time axis (preserving the prior validation — `begin`
109
+ is always sorted and is exempt).
110
+ - **Breaking: `asInterval`'s label function now receives the interval's
111
+ `TimeRange` (its `[begin, end]` extent) and index — not the whole `Event`.**
112
+ The canonical form is unchanged: `series.asInterval(range => range.begin())`
113
+ works exactly as before (both `Event` and `TimeRange` expose `begin()` /
114
+ `end()`). Only a label fn that read a _value column_ off the event (e.g.
115
+ `event => event.get('label')`) needs rewriting — compute the label before
116
+ `asInterval`, or derive it from the extent. The constant form
117
+ (`asInterval('bucket')` / `asInterval(42)`) is unaffected. (Pre-1.0 minor;
118
+ this is the change that lets the function form stay on the columnar path.)
119
+
120
+ ### Fixed
121
+
122
+ - **`mapColumns` rejects a non-finite numeric result at write.** A mapper on a
123
+ `number` column that returns `NaN` or `±Infinity` now throws a `RangeError`,
124
+ consistent with construction intake (which already rejects non-finite
125
+ numbers). Previously the value was packed into the column, where the reduce
126
+ fast path and the row path could disagree on the same bucket (e.g.
127
+ `aggregate('min')` returning a different result depending on which path ran).
128
+ A stored `NaN` is still a defined value the mapper sees — map it to a finite
129
+ number, or to `undefined` (missing), to clean it. (Closes a hole introduced
130
+ alongside `mapColumns` in 0.21.0.)
131
+ - **`aggregate('stdev')` is now numerically stable and path-independent.** The
132
+ bucketed row path (`bucketState`) used a one-pass `sq/n − mean²` accumulator
133
+ that cancels catastrophically on near-equal large-magnitude values —
134
+ returning `0` (e.g. `[1e10, 1e10+1, 1e10+2, 1e10+3]` → `0` instead of
135
+ `≈1.118`), or a negative variance whose `sqrt` is `NaN` that the validating
136
+ constructor then rejected with a throw. Because the columnar fast path is
137
+ all-or-nothing, an unrelated mapping (e.g. a `count` over a string column)
138
+ could silently flip the _same_ series' stdev. All three batch paths (`reduce`,
139
+ `reduceColumn`, `bucketState`) now share **one Welford recurrence** — O(1) per
140
+ element, no buffer (so the live aggregation path that shares `bucketState`
141
+ stays O(1)), `m2 ≥ 0` by construction — so they agree regardless of magnitude.
142
+ (Even the prior two-pass `Σv/n`-then-deviations drifted ~8.7% from the true
143
+ value at `2^52`, where the summed mean rounds — so unifying on Welford, not
144
+ two-pass, was necessary.) **Correction:** 0.21.0's columnar `aggregate()` fast
145
+ path (#186) was described as "signature + semantics unchanged", but it did
146
+ change released `stdev` output for fast-path-qualifying aggregates; this fix
147
+ makes every path agree. (`rolling`/`smooth` stdev keep the one-pass form for
148
+ now — a separate, deferred item.)
149
+
18
150
  ## [0.21.0] — 2026-06-11
19
151
 
20
152
  ### Added
@@ -19,8 +19,17 @@ export type ColumnMapper = (value: unknown) => unknown;
19
19
  * Semantics:
20
20
  * - **Missing cells carry.** The mapper is called only on defined
21
21
  * values; a missing (`undefined`) cell stays missing (the mapper is
22
- * not invoked). A stored `NaN` is a defined number, so the mapper
23
- * *is* called on it (matching `typeof raw === 'number'`).
22
+ * not invoked).
23
+ * - **Numeric results must be finite.** For a `number` column, a mapper
24
+ * result of `NaN` or `±Infinity` throws a `RangeError` at write —
25
+ * matching construction intake (`assertCellKind`), which rejects
26
+ * non-finite numbers. This keeps packed numeric columns NaN-free, so
27
+ * the columnar fast-path and the row-path reducers can never diverge
28
+ * on the same bucket (the bug audit v2 §1.3 reproduced). A stored
29
+ * `NaN` *is* a defined value, so the mapper is still invoked on it —
30
+ * use that to clean it (map `NaN` to a finite number, or to
31
+ * `undefined` for a missing cell). (Array columns are not
32
+ * element-checked here; they don't feed the numeric reducers.)
24
33
  * - **Same kind, schema-stable.** The mapper returns the column's own
25
34
  * kind (the method's type enforces `(value: T) => T`), so the output
26
35
  * column keeps its kind and the schema is unchanged. The result is
@@ -36,6 +45,9 @@ export type ColumnMapper = (value: unknown) => unknown;
36
45
  * same-kind builder can't store: `columnFromValuesByKind` coerces it
37
46
  * to missing, so the cell reads back as a gap the declared schema may
38
47
  * not advertise. Both are type-illegal inputs, not handled specially.
48
+ * (A non-finite *number* from a numeric mapper is the one same-kind
49
+ * case that throws rather than coerces — it would corrupt the packed
50
+ * column, not read back as a gap.)
39
51
  */
40
52
  export declare function mapOp<S extends SeriesSchema>(store: ColumnarStore<S>, schema: S, spec: ReadonlyMap<string, ColumnMapper>): {
41
53
  store: ColumnarStore<S>;
@@ -32,8 +32,17 @@ function columnFromValuesByKind(kind, values) {
32
32
  * Semantics:
33
33
  * - **Missing cells carry.** The mapper is called only on defined
34
34
  * values; a missing (`undefined`) cell stays missing (the mapper is
35
- * not invoked). A stored `NaN` is a defined number, so the mapper
36
- * *is* called on it (matching `typeof raw === 'number'`).
35
+ * not invoked).
36
+ * - **Numeric results must be finite.** For a `number` column, a mapper
37
+ * result of `NaN` or `±Infinity` throws a `RangeError` at write —
38
+ * matching construction intake (`assertCellKind`), which rejects
39
+ * non-finite numbers. This keeps packed numeric columns NaN-free, so
40
+ * the columnar fast-path and the row-path reducers can never diverge
41
+ * on the same bucket (the bug audit v2 §1.3 reproduced). A stored
42
+ * `NaN` *is* a defined value, so the mapper is still invoked on it —
43
+ * use that to clean it (map `NaN` to a finite number, or to
44
+ * `undefined` for a missing cell). (Array columns are not
45
+ * element-checked here; they don't feed the numeric reducers.)
37
46
  * - **Same kind, schema-stable.** The mapper returns the column's own
38
47
  * kind (the method's type enforces `(value: T) => T`), so the output
39
48
  * column keeps its kind and the schema is unchanged. The result is
@@ -49,6 +58,9 @@ function columnFromValuesByKind(kind, values) {
49
58
  * same-kind builder can't store: `columnFromValuesByKind` coerces it
50
59
  * to missing, so the cell reads back as a gap the declared schema may
51
60
  * not advertise. Both are type-illegal inputs, not handled specially.
61
+ * (A non-finite *number* from a numeric mapper is the one same-kind
62
+ * case that throws rather than coerces — it would corrupt the packed
63
+ * column, not read back as a gap.)
52
64
  */
53
65
  export function mapOp(store, schema, spec) {
54
66
  const n = store.length;
@@ -65,10 +77,30 @@ export function mapOp(store, schema, spec) {
65
77
  if (col === undefined)
66
78
  continue;
67
79
  const kind = colKind.get(name);
80
+ // Numeric columns reject a non-finite (NaN / ±Infinity) mapper result at
81
+ // write, matching construction intake (assertCellKind). A packed NaN would
82
+ // otherwise be unreachable-by-assumption for the reduce kernels and diverge
83
+ // the fast path from the row path (audit v2 §1.3). Hoisted out of the loop
84
+ // so non-numeric columns pay nothing.
85
+ const rejectNonFinite = kind === 'number';
68
86
  const out = new Array(n);
69
87
  for (let i = 0; i < n; i += 1) {
70
88
  const v = col.read(i);
71
- out[i] = v === undefined ? undefined : fn(v);
89
+ if (v === undefined) {
90
+ out[i] = undefined;
91
+ continue;
92
+ }
93
+ const mapped = fn(v);
94
+ if (rejectNonFinite &&
95
+ typeof mapped === 'number' &&
96
+ !Number.isFinite(mapped)) {
97
+ throw new RangeError(`mapColumns: the mapper for column '${name}' returned a non-finite ` +
98
+ `number (${mapped}) at row ${i}. Numeric columns reject NaN and ` +
99
+ `±Infinity at write, consistent with intake — a packed NaN would ` +
100
+ `diverge the fast-path and row-path reducers on the same data. ` +
101
+ `Map to a finite number, or to undefined for a missing cell.`);
102
+ }
103
+ out[i] = mapped;
72
104
  }
73
105
  result = withColumnReplaced(result, name, columnFromValuesByKind(kind, out));
74
106
  }
@@ -4,8 +4,7 @@ import { Sequence } from '../sequence/sequence.js';
4
4
  import type { DurationInput } from '../core/duration.js';
5
5
  import type { TemporalLike } from '../core/temporal.js';
6
6
  import type { BatchSampleStrategy } from '../sequence/sample.js';
7
- import type { AggregateMap, AggregateOutputMap, AggregateSchema, AlignSchema, BaselineSchema, DedupeKeep, DiffSchema, EventDataForSchema, FillMapping, FillStrategy, MaterializeSchema, NumericColumnNameForSchema, RollingAlignment, RollingSchema, SeriesSchema, SmoothAppendSchema, SmoothMethod, SmoothSchema } from '../schema/index.js';
8
- import type { AggregateOutputMapResultSchema, RollingOutputMapSchema } from '../schema/index.js';
7
+ import type { AggregateSchema, AlignSchema, BaselineSchema, DedupeKeep, DiffSchema, EventDataForSchema, FillMapping, FillStrategy, MaterializeSchema, NumericColumnNameForSchema, RollingAlignment, RollingSchema, SeriesSchema, SmoothAppendSchema, SmoothMethod, SmoothSchema, ValidatedAggregateMap } from '../schema/index.js';
9
8
  type SequenceLike = Sequence | BoundedSequence;
10
9
  type AlignMethod = 'hold' | 'linear';
11
10
  type AlignSample = 'begin' | 'center' | 'end';
@@ -247,26 +246,16 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
247
246
  range?: TemporalLike;
248
247
  }): PartitionedTimeSeries<MaterializeSchema<S>, K>;
249
248
  /** Per-partition `rolling`. See {@link TimeSeries.rolling}. */
250
- rolling<const Mapping extends AggregateMap<S>>(window: DurationInput, mapping: Mapping, options?: {
249
+ rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput, mapping: Mapping, options?: {
251
250
  alignment?: RollingAlignment;
252
251
  minSamples?: number;
253
252
  }): PartitionedTimeSeries<RollingSchema<S, Mapping>, K>;
254
- rolling<const Mapping extends AggregateOutputMap<S>>(window: DurationInput, mapping: Mapping, options?: {
255
- alignment?: RollingAlignment;
256
- minSamples?: number;
257
- }): PartitionedTimeSeries<RollingOutputMapSchema<S, Mapping>, K>;
258
- rolling<const Mapping extends AggregateMap<S>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
253
+ rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
259
254
  alignment?: RollingAlignment;
260
255
  sample?: AlignSample;
261
256
  range?: TemporalLike;
262
257
  minSamples?: number;
263
258
  }): PartitionedTimeSeries<AggregateSchema<S, Mapping>, K>;
264
- rolling<const Mapping extends AggregateOutputMap<S>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
265
- alignment?: RollingAlignment;
266
- sample?: AlignSample;
267
- range?: TemporalLike;
268
- minSamples?: number;
269
- }): PartitionedTimeSeries<AggregateOutputMapResultSchema<S, Mapping>, K>;
270
259
  /** Per-partition `smooth`. See {@link TimeSeries.smooth}. */
271
260
  smooth<const Target extends NumericColumnNameForSchema<S>, const Output extends string | undefined = undefined>(column: Target, method: SmoothMethod, options: {
272
261
  alpha: number;
@@ -319,12 +308,9 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
319
308
  /** Per-partition `shift`. See {@link TimeSeries.shift}. */
320
309
  shift<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], n: number): PartitionedTimeSeries<DiffSchema<S, Target>, K>;
321
310
  /** Per-partition `aggregate`. See {@link TimeSeries.aggregate}. */
322
- aggregate<const Mapping extends AggregateMap<S>>(sequence: SequenceLike, mapping: Mapping, options?: {
311
+ aggregate<const Mapping extends ValidatedAggregateMap<S, Mapping>>(sequence: SequenceLike, mapping: Mapping, options?: {
323
312
  range?: TemporalLike;
324
313
  }): PartitionedTimeSeries<AggregateSchema<S, Mapping>, K>;
325
- aggregate<const Mapping extends AggregateOutputMap<S>>(sequence: SequenceLike, mapping: Mapping, options?: {
326
- range?: TemporalLike;
327
- }): PartitionedTimeSeries<AggregateOutputMapResultSchema<S, Mapping>, K>;
328
314
  }
329
315
  export {};
330
316
  //# sourceMappingURL=partitioned-time-series.d.ts.map
@@ -1,5 +1,4 @@
1
- import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateOutputMap, AggregateMap, AggregateSchema, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap } from '../schema/index.js';
2
- import type { AggregateOutputMapResultSchema, RollingOutputMapSchema } from '../schema/index.js';
1
+ import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
3
2
  import type { RenameSchema, RollingAlignment, RollingSchema, ColumnValue, DedupeKeep, DiffSchema, FillMapping, FillStrategy, ScalarKind, ScalarValue, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesJsonInput, TimeSeriesInput, TimeRangeKeyedSchema, ValueColumnKindForName, ValueColumnNameForSchema, ValueColumnsForSchema } from '../schema/index.js';
4
3
  import { BoundedSequence } from '../sequence/bounded-sequence.js';
5
4
  import { type TimeZoneOptions } from '../core/calendar.js';
@@ -77,6 +76,12 @@ export declare class TimeSeries<S extends SeriesSchema> {
77
76
  */
78
77
  static fromJSON<S extends SeriesSchema>(input: TimeSeriesJsonInput<S> & {
79
78
  parse?: TimeZoneOptions;
79
+ /**
80
+ * Sort rows by key before construction (off by default; see
81
+ * `TimeSeriesInput.sort`). Useful when reviving a wire payload whose
82
+ * rows aren't guaranteed sorted — avoids a manual pre-sort.
83
+ */
84
+ sort?: boolean;
80
85
  }): TimeSeries<S>;
81
86
  /**
82
87
  * Example: `TimeSeries.fromEvents(events, { schema, name })`.
@@ -337,9 +342,9 @@ export declare class TimeSeries<S extends SeriesSchema> {
337
342
  }): TimeSeries<TimeKeyedSchema<S>>;
338
343
  /** Example: `series.asTimeRange()`. Converts the series key type to `"timeRange"` while preserving each event extent. */
339
344
  asTimeRange(): TimeSeries<TimeRangeKeyedSchema<S>>;
340
- /** Example: `series.asInterval(event => event.begin())`. Converts the series key type to `"interval"` while preserving each event extent and supplying interval labels. */
345
+ /** Example: `series.asInterval(range => range.begin())`. Converts the series key type to `"interval"` while preserving each event extent and supplying interval labels. */
341
346
  asInterval(value: IntervalValue): TimeSeries<IntervalKeyedSchema<S>>;
342
- asInterval(value: (event: EventForSchema<S>, index: number) => IntervalValue): TimeSeries<IntervalKeyedSchema<S>>;
347
+ asInterval(value: (range: TimeRange, index: number) => IntervalValue): TimeSeries<IntervalKeyedSchema<S>>;
343
348
  /**
344
349
  * Example: `left.join(right, { type: "left" })`.
345
350
  * Performs an exact-key join of two series with the same key kind.
@@ -497,12 +502,9 @@ export declare class TimeSeries<S extends SeriesSchema> {
497
502
  * `series.partitionBy(col).aggregate(seq, mapping).collect()` to
498
503
  * aggregate per entity. See {@link TimeSeries.partitionBy}.
499
504
  */
500
- aggregate<const Mapping extends AggregateMap<S>>(sequence: SequenceLike, mapping: Mapping, options?: {
505
+ aggregate<const Mapping extends ValidatedAggregateMap<S, Mapping>>(sequence: SequenceLike, mapping: Mapping, options?: {
501
506
  range?: TemporalLike;
502
507
  }): TimeSeries<AggregateSchema<S, Mapping>>;
503
- aggregate<const Mapping extends AggregateOutputMap<S>>(sequence: SequenceLike, mapping: Mapping, options?: {
504
- range?: TemporalLike;
505
- }): TimeSeries<AggregateOutputMapResultSchema<S, Mapping>>;
506
508
  /**
507
509
  * Example: `series.reduce("value", "avg")`.
508
510
  * Collapses the entire series to a single scalar value using the specified reducer.
@@ -516,8 +518,7 @@ export declare class TimeSeries<S extends SeriesSchema> {
516
518
  * a plain value or record.
517
519
  */
518
520
  reduce(column: ValueColumnsForSchema<S>[number]['name'], reducer: AggregateReducer): ColumnValue | undefined;
519
- reduce<const Mapping extends AggregateMap<S>>(mapping: Mapping): ReduceResult<S, Mapping>;
520
- reduce<const Mapping extends AggregateOutputMap<S>>(mapping: Mapping): ReduceResult<S, Mapping>;
521
+ reduce<const Mapping extends ValidatedAggregateMap<S, Mapping>>(mapping: Mapping): ReduceResult<S, Mapping>;
521
522
  /**
522
523
  * Example: `series.groupBy("host")`.
523
524
  * Partitions the series into groups keyed by the distinct values of a payload column.
@@ -900,26 +901,16 @@ export declare class TimeSeries<S extends SeriesSchema> {
900
901
  * `series.partitionBy(col).rolling(...).collect()` to scope per
901
902
  * entity. See {@link TimeSeries.partitionBy}.
902
903
  */
903
- rolling<const Mapping extends AggregateMap<S>>(window: DurationInput, mapping: Mapping, options?: {
904
+ rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput, mapping: Mapping, options?: {
904
905
  alignment?: RollingAlignment;
905
906
  minSamples?: number;
906
907
  }): TimeSeries<RollingSchema<S, Mapping>>;
907
- rolling<const Mapping extends AggregateOutputMap<S>>(window: DurationInput, mapping: Mapping, options?: {
908
- alignment?: RollingAlignment;
909
- minSamples?: number;
910
- }): TimeSeries<RollingOutputMapSchema<S, Mapping>>;
911
- rolling<const Mapping extends AggregateMap<S>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
908
+ rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
912
909
  alignment?: RollingAlignment;
913
910
  sample?: AlignSample;
914
911
  range?: TemporalLike;
915
912
  minSamples?: number;
916
913
  }): TimeSeries<AggregateSchema<S, Mapping>>;
917
- rolling<const Mapping extends AggregateOutputMap<S>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
918
- alignment?: RollingAlignment;
919
- sample?: AlignSample;
920
- range?: TemporalLike;
921
- minSamples?: number;
922
- }): TimeSeries<AggregateOutputMapResultSchema<S, Mapping>>;
923
914
  /**
924
915
  * Example: `series.smooth("value", "ema", { alpha: 0.2 })`.
925
916
  * Applies a smoothing transform to one numeric payload column while preserving the original key
@@ -12,7 +12,7 @@ import { TimeRange } from '../core/time-range.js';
12
12
  import { compareEventKeys } from '../core/temporal.js';
13
13
  import { PartitionedTimeSeries } from './partitioned-time-series.js';
14
14
  import { Sequence } from '../sequence/sequence.js';
15
- import { withColumnsRenamed, withColumnsSelected, withRowRange, } from '../columnar/index.js';
15
+ import { IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, } from '../columnar/index.js';
16
16
  import { SeriesStore } from '../live/series-store.js';
17
17
  import { parseDuration } from '../core/duration.js';
18
18
  import { resolveReducer, } from '../reducers/index.js';
@@ -468,6 +468,7 @@ export class TimeSeries {
468
468
  name: input.name,
469
469
  schema: input.schema,
470
470
  rows: parseJsonRows(input.schema, input.rows, input.parse),
471
+ sort: input.sort ?? false,
471
472
  });
472
473
  }
473
474
  /**
@@ -587,13 +588,21 @@ export class TimeSeries {
587
588
  }
588
589
  else {
589
590
  this.schema = Object.freeze(input.schema.slice());
591
+ // `{ sort: true }` sorts unsorted input by key before intake (otherwise
592
+ // the intake's non-decreasing-order check throws). Stable, so rows with
593
+ // equal keys keep their input order; same key comparator as
594
+ // `fromEvents` / `concat`. Copies the array — the caller's `rows` is not
595
+ // mutated.
596
+ const rows = input.sort
597
+ ? [...input.rows].sort((a, b) => compareEventKeys(toKey(a[0]), toKey(b[0])))
598
+ : input.rows;
590
599
  // `SeriesStore.fromValidatedRows` runs the column-native
591
600
  // intake (`validateAndNormalizeColumnar`) — same validation
592
601
  // rules as the pre-2a row-shape `validateAndNormalize` but
593
602
  // writes directly into columnar buffers without allocating
594
603
  // Event objects + frozen data dicts. Events lazy-materialize
595
604
  // on first `eventAt(i)` access via the store's per-row cache.
596
- this.#store = SeriesStore.fromValidatedRows(this.schema, input.rows);
605
+ this.#store = SeriesStore.fromValidatedRows(this.schema, rows);
597
606
  }
598
607
  Object.freeze(this);
599
608
  }
@@ -782,14 +791,18 @@ export class TimeSeries {
782
791
  * series.events[i]` holds whenever both are accessed.
783
792
  */
784
793
  at(index) {
785
- // Match pre-2a array-indexing semantics: non-integer or NaN
786
- // inputs return undefined rather than throwing downstream from
787
- // `#store.eventAt`. `this.events[NaN]` returned undefined per
788
- // JS array semantics (key coercion + miss); the direct route
789
- // to `#store.eventAt(NaN)` would proceed past the bounds check
790
- // and attempt key materialization at the invalid row. Closed
791
- // Codex round 4's medium finding on PR #150.
792
- if (!Number.isInteger(index) || index < 0 || index >= this.#store.length) {
794
+ // Non-integer / NaN inputs return undefined rather than throwing
795
+ // downstream from `#store.eventAt` (`this.events[NaN]` returned undefined
796
+ // per JS array semantics; the direct route to `#store.eventAt(NaN)` would
797
+ // proceed past the bounds check and materialize a key at an invalid row).
798
+ // Closed Codex round 4's medium finding on PR #150.
799
+ if (!Number.isInteger(index))
800
+ return undefined;
801
+ // Negative indices count from the end parity with `LiveSeries.at` and
802
+ // `Array.prototype.at` (`at(-1)` is the last event).
803
+ if (index < 0)
804
+ index += this.#store.length;
805
+ if (index < 0 || index >= this.#store.length) {
793
806
  return undefined;
794
807
  }
795
808
  return this.#store.eventAt(index);
@@ -847,40 +860,95 @@ export class TimeSeries {
847
860
  }
848
861
  /** Example: `series.asTime({ at: "center" })`. Converts the series key type to `"time"` using the supplied anchor within each event extent. */
849
862
  asTime(options = {}) {
863
+ // Column-native rekey: reinterpret the key as `time` straight off the
864
+ // existing key's begin/end buffers — no `this.events`. `begin`/`end`
865
+ // reuse the source buffer zero-copy; `center` computes midpoints.
850
866
  const schema = Object.freeze([
851
867
  { name: 'time', kind: 'time' },
852
868
  ...this.schema.slice(1),
853
869
  ]);
854
- const resultEvents = this.events.map((event) => event.asTime(options));
855
- if ((options.at ?? 'begin') === 'begin') {
856
- return TimeSeries.#fromTrustedEvents(this.name, schema, resultEvents);
870
+ const keys = this.#store.store.keys;
871
+ const at = options.at ?? 'begin';
872
+ const n = keys.length;
873
+ let beginBuf;
874
+ if (at === 'center') {
875
+ beginBuf = new Float64Array(n);
876
+ for (let i = 0; i < n; i += 1) {
877
+ beginBuf[i] = (keys.begin[i] + keys.end[i]) / 2;
878
+ }
857
879
  }
858
- return new TimeSeries({
859
- name: this.name,
860
- schema,
861
- rows: toRows(schema, resultEvents),
862
- });
880
+ else {
881
+ beginBuf = at === 'end' ? keys.end : keys.begin;
882
+ }
883
+ // The source is ordered by `begin`, so `begin` stays monotonic — but for a
884
+ // ranged source with overlapping extents, anchoring at `end` / `center`
885
+ // can REORDER rows, producing a non-monotonic time axis. The pre-#200 path
886
+ // routed these anchors through the validating constructor, which threw on
887
+ // an unsorted result; `withKeyColumn` → `#fromTrustedStore` trusts the key
888
+ // and would silently accept it (breaking `bisect` / `timeRange` / key-range
889
+ // ops). Restore the throw with an O(n) scan. (`begin` can't reorder, so it
890
+ // is exempt — and `withKeyColumn` documents the monotonic-key precondition.)
891
+ if (at !== 'begin') {
892
+ for (let i = 1; i < n; i += 1) {
893
+ if (beginBuf[i] < beginBuf[i - 1]) {
894
+ throw new Error(`asTime({ at: '${at}' }) produced a non-monotonic time axis at ` +
895
+ `row ${i} (${beginBuf[i]} < ${beginBuf[i - 1]}): the source ` +
896
+ `extents overlap, so anchoring at '${at}' reorders rows. Anchor ` +
897
+ `at 'begin', or re-sort after converting.`);
898
+ }
899
+ }
900
+ }
901
+ const store = withKeyColumn(this.#store.store, schema[0], new TimeKeyColumn(beginBuf, keys.length));
902
+ return TimeSeries.#fromTrustedStore(this.name, schema, store);
863
903
  }
864
904
  /** Example: `series.asTimeRange()`. Converts the series key type to `"timeRange"` while preserving each event extent. */
865
905
  asTimeRange() {
906
+ // Column-native rekey: the timeRange covers each row's existing extent —
907
+ // reuse the key's begin/end buffers zero-copy, no events.
866
908
  const schema = Object.freeze([
867
909
  { name: 'timeRange', kind: 'timeRange' },
868
910
  ...this.schema.slice(1),
869
911
  ]);
870
- const resultEvents = this.events.map((event) => event.asTimeRange());
871
- return TimeSeries.#fromTrustedEvents(this.name, schema, resultEvents);
912
+ const keys = this.#store.store.keys;
913
+ const store = withKeyColumn(this.#store.store, schema[0], new TimeRangeKeyColumn(keys.begin, keys.end, keys.length));
914
+ return TimeSeries.#fromTrustedStore(this.name, schema, store);
872
915
  }
873
916
  asInterval(value) {
917
+ // Column-native rekey: build the interval labels straight off the key's
918
+ // begin/end buffers — no events. The label fn receives the interval's
919
+ // TimeRange (its [begin, end] extent) + index, NOT the whole event
920
+ // (breaking — see CHANGELOG [Unreleased]). Label kind is inferred from the
921
+ // first label — the two `IntervalValue` kinds, string → StringColumn /
922
+ // number → Float64Column — and must be consistent across rows (a mix
923
+ // throws, as at event intake). A type-defeated non-string/number label
924
+ // (e.g. a boolean via `as any`) is rejected here rather than coerced the
925
+ // way intake would (`true → 1`); throwing on the nonsense input is safer.
874
926
  const schema = Object.freeze([
875
927
  { name: 'interval', kind: 'interval' },
876
928
  ...this.schema.slice(1),
877
929
  ]);
878
- const nextEvents = this.events.map((event, index) => {
879
- return typeof value === 'function'
880
- ? event.asInterval(() => value(event, index))
881
- : event.asInterval(value);
882
- });
883
- return TimeSeries.#fromTrustedEvents(this.name, schema, nextEvents);
930
+ const keys = this.#store.store.keys;
931
+ const n = keys.length;
932
+ const labels = new Array(n);
933
+ if (typeof value === 'function') {
934
+ for (let i = 0; i < n; i += 1) {
935
+ labels[i] = value(new TimeRange([keys.begin[i], keys.end[i]]), i);
936
+ }
937
+ }
938
+ else {
939
+ labels.fill(value);
940
+ }
941
+ const labelKind = n > 0 ? typeof labels[0] : 'string';
942
+ for (let i = 1; i < n; i += 1) {
943
+ if (typeof labels[i] !== labelKind) {
944
+ throw new Error(`asInterval: interval label at row ${i} is ${typeof labels[i]} but earlier rows were ${labelKind} — interval labels must be one type throughout`);
945
+ }
946
+ }
947
+ const labelCol = labelKind === 'number'
948
+ ? float64ColumnFromArray(labels)
949
+ : stringColumnFromArray(labels);
950
+ const store = withKeyColumn(this.#store.store, schema[0], new IntervalKeyColumn(keys.begin, keys.end, labelCol, n));
951
+ return TimeSeries.#fromTrustedStore(this.name, schema, store);
884
952
  }
885
953
  join(other, options = {}) {
886
954
  const [left, right] = prepareSeriesForJoin([
@@ -1237,7 +1305,8 @@ export class TimeSeries {
1237
1305
  }
1238
1306
  pivotByGroup(groupCol, valueCol, options = {}) {
1239
1307
  if (this.schema[0].kind !== 'time') {
1240
- throw new TypeError(`pivotByGroup requires a time-keyed series; got ${this.schema[0].kind}`);
1308
+ throw new TypeError(`pivotByGroup requires a time-keyed series; got ${this.schema[0].kind}` +
1309
+ ` — piping from aggregate/materialize? call .asTime({ at: 'begin' }) first`);
1241
1310
  }
1242
1311
  const valueColumnDef = this.schema.find((c) => c.name === valueCol);
1243
1312
  if (!valueColumnDef) {
@@ -2771,6 +2840,12 @@ export class TimeSeries {
2771
2840
  rollingOptions.alignment = alignment;
2772
2841
  if (minSamples !== undefined)
2773
2842
  rollingOptions.minSamples = minSamples;
2843
+ // Trust boundary: a generic mapping can't prove it satisfies the
2844
+ // public overload's ValidatedAggregateMap constraint (S is
2845
+ // unresolved here), so route through the broad-schema escape hatch
2846
+ // — for concrete SeriesSchema the constraint degrades to the
2847
+ // permissive AggregateMap by design. The computed spec keys are
2848
+ // collision-checked above.
2774
2849
  const rolling = this.rolling(window, {
2775
2850
  [avgName]: { from: col, using: 'avg' },
2776
2851
  [sdName]: { from: col, using: 'stdev' },
@@ -2865,6 +2940,8 @@ export class TimeSeries {
2865
2940
  rollingOptions.alignment = alignment;
2866
2941
  if (minSamples !== undefined)
2867
2942
  rollingOptions.minSamples = minSamples;
2943
+ // Trust boundary: see baseline() above — generic mapping routed
2944
+ // through the broad-schema escape hatch of ValidatedAggregateMap.
2868
2945
  const rolling = this.rolling(window, rollingMapping, rollingOptions);
2869
2946
  const kept = [];
2870
2947
  for (let i = 0; i < this.events.length; i += 1) {
@@ -376,11 +376,11 @@ export function validateAndNormalizeColumnar(input) {
376
376
  const prevBegin = beginBuf[i - 1];
377
377
  const curBegin = beginBuf[i];
378
378
  if (prevBegin > curBegin) {
379
- throw new ValidationError(`row ${i} is out of order`);
379
+ throw new ValidationError(`row ${i} is out of order — keys must be non-decreasing; pass { sort: true } to sort rows on construction, or pre-sort them`);
380
380
  }
381
381
  if (prevBegin === curBegin && endBuf !== beginBuf) {
382
382
  if (endBuf[i - 1] > endBuf[i]) {
383
- throw new ValidationError(`row ${i} is out of order`);
383
+ throw new ValidationError(`row ${i} is out of order — keys must be non-decreasing; pass { sort: true } to sort rows on construction, or pre-sort them`);
384
384
  }
385
385
  }
386
386
  }
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ // pond-ts ships as ES modules only. This stub is the `require` target in the
4
+ // package's `exports` map so that CommonJS consumers get a clear, actionable
5
+ // error instead of Node's cryptic `ERR_PACKAGE_PATH_NOT_EXPORTED`.
6
+ //
7
+ // It is copied verbatim into `dist/` during `prepack` (see package.json) so it
8
+ // rides along in the published tarball; the source of truth lives at the
9
+ // package root and is never touched by `tsc`.
10
+
11
+ throw new Error(
12
+ 'pond-ts is an ES module package and cannot be loaded with require(). ' +
13
+ "Use `import { TimeSeries } from 'pond-ts'` instead, or a dynamic " +
14
+ "`await import('pond-ts')` from CommonJS. See https://nodejs.org/api/esm.html.",
15
+ );
@@ -37,5 +37,5 @@ export { type IntervalLabelKind, type KeyColumn, IntervalKeyColumn, TimeKeyColum
37
37
  export { type FromTrustedStoreOptions, ColumnarStore } from './store.js';
38
38
  export { type ColumnBuilder, ArrayColumnBuilder, BooleanColumnBuilder, Float64ColumnBuilder, StringColumnBuilder, columnBuilderForKind, } from './builder.js';
39
39
  export { type AnyColumnKind, type ArrayValue, type ColumnDef, type ColumnSchema, type KeyKind, type ScalarValue, } from './types.js';
40
- export { materialize, withColumnAppended, withColumnReplaced, withColumnsRenamed, withColumnsSelected, withRowRange, withRowSelection, } from './view.js';
40
+ export { materialize, withColumnAppended, withColumnReplaced, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from './view.js';
41
41
  //# sourceMappingURL=index.d.ts.map