semantic-typescript 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,574 +1,587 @@
1
- # Changelog
2
-
3
- Notable changes to `semantic-typescript`. The format follows
4
- [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the version numbers
5
- follow [Semantic Versioning](https://semver.org/).
6
-
7
- A Simplified Chinese edition is kept in [`CHANGELOG.cn.md`](./CHANGELOG.cn.md).
8
-
9
- ---
10
-
11
- ## [2.0.0] - 2026-11-05
12
-
13
- This release covers the work merged after 1.2.0. It is a large change: the
14
- collection families were split, the phase-ring API was reworked, the statistics,
15
- window, signal and distribution modules became public entry points, and several
16
- asynchronous lifecycle defects were fixed. Read the "Breaking", "Changed" and
17
- "Removed" sections before upgrading.
18
-
19
- ### Breaking
20
-
21
- **Statistics now default to the definitions used by R, NumPy, pandas and Excel**
22
-
23
- These are deliberate, owner-approved breaks: the old values were defined
24
- differently from the mainstream tools, so keeping them would have preserved
25
- compatibility at the cost of correctness. Each old behaviour remains reachable
26
- through an explicit option.
27
-
28
- - **`quantile(probability)` now defaults to Hyndman–Fan type 7**, the
29
- parameterisation `h = (n - 1)p + 1` with linear interpolation, instead of
30
- type 1 (`ceil(np)` nearest rank). Type 7 is the default of R `quantile`,
31
- NumPy `quantile` (`method="linear"`), Excel `PERCENTILE.INC` and pandas
32
- `quantile`. On `[1..8]` at `p = 0.75` it returns `6.25` where it returned
33
- `6`; on `[2,4,4,4,5,5,7,9]` at `p = 0.9` it returns `7.6` where it returned
34
- `9`. All nine types are available as
35
- `quantile(p, { method: "type-1" })` … `{ method: "type-9" }`, and
36
- `"type-1"` restores the old nearest-rank answer exactly.
37
- - **`interquartileRange()` follows the same method** (default type 7), so
38
- Tukey fences now line up with R and NumPy: `[1..8]` gives `3.5`, not `4`,
39
- and `[1,2,3,4]` gives `1.5`, not `2`. Pass `{ method }` to change the type.
40
- - **`kurtosis()` now defaults to the bias-corrected excess estimator `G2`**,
41
- `G2 = ((n - 1) / ((n - 2)(n - 3))) · ((n + 1)g2 + 6)`, matching Excel
42
- `KURT` and pandas `kurt`, instead of the biased `g2 = m4 / m2² - 3`. On
43
- `[1,2,3,4]` it returns `-1.2` where it returned `-1.36`; pass
44
- `{ estimator: "g2" }` for the old value. `{ excess: false }` adds three back
45
- and reports the non-excess fourth standardised moment.
46
- - **`skewness()` now defaults to the bias-corrected `G1`**,
47
- `G1 = √(n(n - 1))/(n - 2) · g1`, matching Excel `SKEW`, pandas `skew` and
48
- SPSS, instead of the biased `g1 = m3 / m2^(3/2)`; pass
49
- `{ estimator: "g1" }` to restore `g1`.
50
- - **`variance()` and `standardDeviation()` now divide by `n - 1`** (Bessel's
51
- correction), matching R `var`/`sd`, pandas and NumPy `var(ddof=1)`, instead
52
- of `n`. On `[1,2,3,4]` the variance is `5/3` where it was `5/4`; pass
53
- `{ ddof: 0 }` for the previous population value. `standardDeviation` is the
54
- square root of that variance, so it remains very slightly biased even under
55
- normality (Jensen's inequality).
56
- - **Small samples now return an empty `Optional` instead of `0` or `NaN`.**
57
- `G2` is undefined below four observations, `G1` below three, and the sample
58
- variance and standard deviation below two; every statistic on an empty view
59
- is undefined too, as are skewness and kurtosis on a constant sample. For
60
- example, with three observations the default `kurtosis()` is empty while
61
- `{ estimator: "g2" }` is defined, and with two observations `skewness()` is
62
- empty while `{ estimator: "g1" }` is `0`. Bigint views return the same
63
- empties; with `{ ddof: 0 }` a single observation still has a defined
64
- variance of zero.
65
- - **`variance()` and `standardDeviation()` now use a two-pass computation**
66
- (mean first, then squared deviations) instead of the collector's
67
- `sum(x²) - sum(x)²/n` form, which suffers catastrophic cancellation when the
68
- standard deviation is small relative to the mean.
69
- - **`skewness` and `kurtosis` gained closed-form standard errors.**
70
- `skewnessStandardError()` and `kurtosisStandardError()` on the numeric
71
- statistics views return `√(6n(n - 1)/((n - 2)(n + 1)(n + 3)))` and
72
- `√(24n(n - 1)²/((n - 3)(n - 2)(n + 3)(n + 5)))`, the SAS/SPSS and
73
- `e1071::skewness`/`e1071::kurtosis` forms. They require three and four
74
- observations respectively. The bigint views omit them because rounding a
75
- standard error to an integer would report zero for most samples.
76
- - **These methods now return `Optional`.** `variance`, `standardDeviation`,
77
- `quantile`, `interquartileRange`, `skewness` and `kurtosis` all wrap their
78
- result, so callers must unwrap with `.get()`, `.orElse(...)` or test
79
- `.isPresent()`. The bigint view still rounds each result to a whole bigint.
80
- - **Reconciliation.** The new defaults reproduce the published results:
81
- `[1,2,3,4]` gives `variance(ddof = 1) = 5/3`, `G2 = -1.2`, `g2 = -1.36`,
82
- `G1 = g1 = 0`; `[1..8]` at `p = 0.75` gives type 7 `6.25` and type 1 `6`;
83
- the Wikipedia kurtosis example (`n = 20`) gives `g1 = 4.129251`,
84
- `G1 = 4.471885`, `g2 = 15.051427`, `G2 = 19.998429`, and standard errors
85
- `0.512103` and `0.992384`. These match R, NumPy, pandas and Excel to the
86
- published precision.
87
-
88
- **Signal analysis now uses the physical power and DFT-even conventions**
89
-
90
- These are also deliberate, owner-approved breaks: the old definitions mixed
91
- amplitude and power or assumed that only complete frames exist, so they returned
92
- systematically biased levels and time-frequency frames.
93
-
94
- - **`decibel` is removed and split into `amplitudeDecibel` and
95
- `powerDecibel`.** `amplitudeDecibel` keeps the old `20·log10` convention for
96
- amplitude-like ratios; `powerDecibel` applies `10·log10` for power, energy,
97
- mean square and power spectral density. Power `0.25` is `−6.02 dB` under
98
- `powerDecibel`, while the old function reported `−12.04 dB`. `Bin.decibel` and
99
- `Spectrum.decibelOf` remain amplitude levels; `Spectrum.powerDecibelOf` is the
100
- matching power level.
101
- - **`Spectrum.bandEnergy(low, high)` now defaults to the one-sided reading.**
102
- A real spectrum's conjugate half is folded into the non-negative axis, with
103
- DC and Nyquist counted once and every interior bin counted twice, so a band
104
- that covers the whole one-sided axis satisfies Parseval (`Σ|X|² = N·Σx²`).
105
- The old raw sum, which counted only the positive half of a band below Nyquist
106
- (−3.01 dB), remains available as `bandEnergy(low, high, "two-sided")`; use it
107
- for complex spectra.
108
- - **`Spectrum.flatness()` now measures the one-sided power spectrum.** The old
109
- method took the geometric/arithmetic mean of magnitudes over both complex
110
- halves. The result is now computed from `power()`, over bins `0` through
111
- `floor(count/2)`, without the display-only amplitude doubling of
112
- `oneSided()`; a zero bin still reports `0`.
113
- - **`Sampler.shortTimeFourierTransform` and `Sampler.spectrogram` now default
114
- to the periodic Hann window, `boundary: "zeros"` and `padded: true`.** The
115
- symmetric Hann window and the old "complete frames only" path (`7` frames for
116
- `64` samples, frame `16`, hop `8`) remain reachable with
117
- `useHannWindow`/`useHannPeriodicWindow` and
118
- `{ boundary: "none", padded: false }`; the default now returns `9` frames,
119
- loses no tail samples, and matches SciPy's `stft` defaults. The new optional
120
- fifth argument takes `{ boundary, padded }`.
121
- - **`Spectrogram` frame times are now frame centres.** `getTime(k)` returns
122
- `(k·hop + frameSize/2)/rate` for an unextended view and
123
- `(k·hop + (frameSize mod 2)/2)/rate` for an extended one, instead of the frame
124
- start. `getDuration()` and `toFrame()` also trim the boundary extension.
125
- The constructor gained an optional fourth argument, the boundary mode, which
126
- defaults to `"none"`.
127
-
128
- ### Added
129
-
130
- **Collection families**
131
-
132
- - **`OrderedSet` / `UnorderedSet`**, each in synchronous and asynchronous form.
133
- `OrderedSet` decides uniqueness and iteration order by a comparator (defaulting
134
- to `useCompare`); `UnorderedSet` buckets by `useHash` and compares with
135
- SameValueZero. Iteration order is part of the contract for the ordered variant
136
- and explicitly unspecified for the unordered one.
137
- - **`OrderedLRUSet` / `UnorderedLRUSet`** and **`OrderedSkipSet` /
138
- `UnorderedSkipSet`**, closing the gap where the LRU and SKIP families offered
139
- ordered and unordered maps but only an undifferentiated set.
140
- - **`OrderedMap` / `UnorderedMap`** promoted to independent root interfaces. The
141
- former common base is gone, so neither variant inherits ordering promises from
142
- the other.
143
-
144
- **Error model**
145
-
146
- - A typed error hierarchy in `structure/error.ts`: fifteen classes covering
147
- validation, indexing, capacity, state, concurrency, collection protocol,
148
- serialisation, numeric, phase, and configuration failures. Each carries a
149
- stable `name`, an optional `cause` for the originating error, and an optional
150
- `context` bag for diagnostics, and each has a brand guard.
151
- - **`SemanticRangeError`**, an alias for the exported `RangeError` class. Both
152
- names denote one class, so `instanceof` checks hold across them. The class
153
- keeps its historical name — no library module imports it implicitly, so a bare
154
- `new RangeError(...)` still reaches the global constructor — but a consumer that
155
- writes `import { RangeError }` shadows that global for the whole module,
156
- turning an incidental `new RangeError(...)` in the same file into a library
157
- error. Importing `SemanticRangeError` instead leaves the global untouched.
158
-
159
- **Module layout**
160
-
161
- - **Collector factories moved to `collectors.ts`.** `collector.ts` now declares
162
- only `SynchronousCollector` and `AsynchronousCollector` (about 340 and 350
163
- lines); the fifty-three `useSynchronousXxx` and fifty-three `useAsynchronousXxx`
164
- factories live in `synchronous/collectors.ts` and `asynchronous/collectors.ts`.
165
- The dependency runs one way — `collectors.ts` imports `collector.ts`, never the
166
- reverse — and `factory.ts` remains the module that builds `Semantic` values, so
167
- creating a `Semantic` and creating a collector are now separate jobs.
168
- - **Forests get their own module.** `rootedForest` and `unionFindForest` left
169
- `structure/tree/` for `structure/forest/`, with `forest/index.ts` and a
170
- `structure/forest.ts` barrel beside the existing `structure/tree.ts`. The
171
- binomial and Fibonacci heaps, the breadth-first and depth-first search trees
172
- and the minimum spanning tree import the rooted forest from the new path, as
173
- does the tree-API boundary type test.
174
-
175
- **Phase rings**
176
-
177
- - `PhaseField` and `useField` in `mathematics/field.ts`: a bounded score in
178
- `[-1, 1]` defined as a truncated cosine series. The constructor enforces
179
- `|offset| + Σ|weight| ≤ 1`, so the bound is a consequence of the construction
180
- rather than a clamp applied afterwards.
181
- - `Cycle` now reports its own state: `purity()`, `sequences()`,
182
- `zeroSequence()`, `isBalanced()`, `resultant()`, `relativeAt()`, `viewAt()`,
183
- `viewsAt()`, `jetAt()`, `windingAt()`, `errorIntegralAt()`, `scoreAt()`,
184
- `scoresAt()`, `analyse()`, and `toSpectrum()`.
185
- - `combineCycles(a, b)` joins two rings on their least common multiple and
186
- reports the pair-compatibility condition, so the joint ring is a pullback
187
- rather than a product.
188
- - `useAngularSpectrum(cycle, instants, plan)` builds the two-dimensional
189
- `(ω, n)` plane, one column per spatial mode.
190
- - `BlobLike`, a structural description of the bytes `useBlob` needs, so the
191
- published declarations no longer depend on the DOM library being present in the
192
- consumer's compilation.
193
-
194
- **Signal analysis**
195
-
196
- - **Periodic, DFT-even windows**: `useHannPeriodicWindow`,
197
- `useHammingPeriodicWindow`, `useBlackmanPeriodicWindow`,
198
- `useNuttallPeriodicWindow`, `useFlatTopPeriodicWindow` and
199
- `useKaiserPeriodicWindow(beta)`. They evaluate the same shapes as the
200
- symmetric windows against `count` rather than `count - 1`, so a periodic Hann
201
- window has a constant overlap-add sum of `1.000000` at a half-frame hop for
202
- every length, while the symmetric form ripples by `0.841 dB` at `N = 16` and
203
- still by `0.013 dB` at `N = 1024`. The existing symmetric exports are
204
- unchanged.
205
- - **`Spectrum.powerDecibelOf(index, normalization?, reference?)`**, the power
206
- counterpart of `decibelOf`, returning `10·log10(power / reference)`.
207
- - **`BandSidedness`** (`"one-sided" | "two-sided"`), the explicit argument type
208
- of the corrected `Spectrum.bandEnergy`.
209
- - **`TimeFrequencyOptions`**, the `{ boundary?, padded? }` options of
210
- `shortTimeFourierTransform` and `spectrogram`, and the `BoundaryExtension`
211
- (`"zeros" | "constant" | "none"`) type they use.
212
- - **`Spectrogram.getBoundary()`** and **`Sampler.spectrogram(..., options)`**,
213
- so a spectrogram retains the extension mode and trims it during `toFrame`.
214
-
215
- **Public modules**
216
-
217
- - **`statistics.ts`, `window.ts`, `signal.ts` and `distribution.ts` are now the
218
- home of their API**, in both `synchronous/` and `asynchronous/`. The classes
219
- and value objects they own are re-exported by `semantic.ts`, so the historical
220
- import path `#semantic/synchronous/semantic` (and its asynchronous twin) keeps
221
- exposing them while the duplicate definitions are gone.
222
- - **New named exports**: `SynchronousStatistics`, `SynchronousBigIntStatistics`,
223
- `SynchronousNumericStatistics`, `SynchronousWindowCollectable`,
224
- `SynchronousWindowArrayCollectable`, `SynchronousSignalCollectable`,
225
- `SynchronousDistributionCollectable` and their `Asynchronous*` counterparts,
226
- plus the distribution value objects (`Histogram`, `HistogramBin`,
227
- `EmpiricalDistribution`, `MomentSummary`, `RobustSummary`, `DensityEstimate`)
228
- and `SpectralDensity`. Every class carries the synchronous/asynchronous prefix
229
- the rest of the library uses, because an unprefixed pair would collide in the
230
- package barrel.
231
- - **Deep imports**: `package.json` now maps `semantic-typescript/signal`,
232
- `semantic-typescript/statistics`, `semantic-typescript/window` and
233
- `semantic-typescript/distribution` to the synchronous module of the same name,
234
- with its declaration file, so a consumer can import one analysis module
235
- without loading the whole barrel.
236
- - **`SynchronousWindowArrayCollectable`** (and its asynchronous twin) materialises
237
- `slide` / `tumble` into arrays. `toWindow()` keeps returning
238
- `SynchronousWindowCollectable`, the lazy pipeline form the README documents, so
239
- a window can still be transformed before it is consumed.
240
-
241
- **Project**
242
-
243
- - README rewritten in nine languages on a shared thirteen-section outline, with a
244
- new section on periodic analysis.
245
- - `.vscode/settings.json` so editor auto-imports prefer the project's path alias
246
- over relative specifiers.
247
-
248
- ### Changed
249
-
250
- - **`Cycle.frequency` is the ambient rotation rate**, not a phase step between
251
- positions. The environment advances every phasor by the same angle and therefore
252
- cannot break the intrinsic pattern. Position phases are now derived from the
253
- exact residue `i mod n`, which makes `phaseAt(i)` and `phaseAt(i + n)`
254
- identical to the bit rather than merely close.
255
- - **`useText` slices by code point** rather than by UTF-16 index, so astral
256
- characters stay whole; inverted bounds are still swapped and negative positions
257
- still clamped to zero, matching the documented slice behaviour.
258
- - **`useRange` accepts descending ranges** when the step is negative, and rejects
259
- a step whose numeric kind differs from the bounds' kind instead of coercing
260
- between `number` and `bigint`.
261
- - The JSDoc for every factory in `factory.ts` and every hook in `hook.ts` was
262
- rewritten to document actual behaviour, including the overloads.
263
- - Every one of the 296 interfaces exported from `structure/tree/` now carries an
264
- interface JSDoc block that states its contract, defaults and cost. Both
265
- `collectors.ts` modules document their thirty-four factory interfaces and every
266
- overload in the same way.
267
-
268
- ### Removed
269
-
270
- - **`SynchronousSet` / `AsynchronousSet` / `SynchronousMap` / `AsynchronousMap`**,
271
- together with their brand symbols and guards. Use the ordered or unordered
272
- variant that matches the semantics you need. There is no compatibility alias.
273
- - The `Collector` base types of the map family, which are replaced by internal,
274
- unbranded contracts used only to compose the LRU and SKIP families.
275
- - **The `structure/tree/rootedForest` and `structure/tree/unionFindForest` entry
276
- points**, together with their re-exports from `tree/index.ts`. Both contracts
277
- now live under `structure/forest/`; no compatibility re-export remains at the
278
- old path.
279
-
280
- ### Fixed
281
-
282
- - **`useSubscription` no longer leaks its subscription.** Teardown is idempotent
283
- and runs on every exit path: the consumer asking to stop, the consumer throwing,
284
- a subscription failing partway through, and an event arriving after the run has
285
- ended. Every recorded `unsubscribe` is attempted even when an earlier one
286
- throws, and `unmount` is attempted even when an unsubscribe threw, so one
287
- failing host method cannot strand the others. Errors raised during teardown are
288
- swallowed rather than masking the error that caused the teardown.
289
- - **`useSubscription` completes at the accepted limit** rather than waiting for
290
- one more event, and an empty event name list now completes immediately instead
291
- of never settling. A host that fires the handler synchronously from `subscribe`
292
- is also handled: the handler is unsubscribed once registration returns.
293
- - **Event names are no longer restricted to strings.** A symbol or a number is
294
- accepted as a single event name; previously it was rejected even though such
295
- keys are ordinary property keys.
296
- - **`useInterval` clears both of its handles.** A consumer that throws used to
297
- leave the timer running forever, and a pending initial delay was not tracked at
298
- all. Both are now cleared as soon as the run stops, and the consumer's error is
299
- propagated unchanged.
300
- - **`usePromise` no longer hangs.** The asynchronous executor that swallowed
301
- rejections is gone: a rejected source and a throwing consumer both reject the
302
- collection with the original reason.
303
- - **`useBlob` cancels the stream on the error path.** It previously released the
304
- reader's lock without cancelling the stream, leaving the underlying source
305
- producing into a discarded buffer.
306
- - **The flat-top window keeps its negative lobe.** `useFlatTopWindow` clamped
307
- every weight at zero with `Math.max(0, ...)`, but a flat-top window is defined
308
- to be partially negative: at `N = 1024` the unclamped symmetric window has
309
- `578` negative samples (minimum `−0.070560`), coherent gain `0.215368` and
310
- noise bandwidth `3.773946` bins, where the clamped version reported `0.233280`
311
- and `3.199558`. Scalloping loss recovers from `−0.3087 dB` to `−0.0097 dB`
312
- (about 32× better), and the periodic form matches the research target
313
- (`CG 0.215579`, `ENBW 3.7702`, `HPBW 3.7247`, `−0.0098 dB`, `577` negative
314
- samples).
315
- - **The short-time transform no longer drops the signal tail.** The default
316
- `boundary: "zeros"` and `padded: true` extend the signal by half a frame at
317
- each end and zero-pad the tail, so `64` samples at frame `16` and hop `8`
318
- produce `9` frames instead of `7` and every source sample is covered; a
319
- `1000`-sample signal at frame `256` and hop `128` produces `9` frames instead
320
- of `6` (232 samples were previously unreachable).
321
- - **The short-time transform uses a periodic window by default.** The symmetric
322
- Hann window does not satisfy constant-overlap-add: at 50 per cent overlap its
323
- overlap-add sum ripples by `0.841 dB` at `N = 16` and `0.013 dB` at
324
- `N = 1024`, whereas the new default `useHannPeriodicWindow` sums to
325
- `1.000000` at every length.
326
- - **`Spectrum.flatness()` uses the power spectrum.** The old method took the
327
- geometric/arithmetic mean of magnitudes over the full two-sided spectrum; it
328
- now uses `power()` over the one-sided half. Two bins with powers `4` and `1`
329
- report `0.8` where the amplitude reading gave `0.9428`, and a unit impulse
330
- reports `1`.
331
- - **`Spectrum.bandEnergy()` no longer halves a band below Nyquist.** The old raw
332
- two-sided sum counted only the positive half of a real spectrum: a unit cosine
333
- at bin `1` of an `8`-point spectrum read `16` over `[0, 2)` although its total
334
- power is `32`. The default one-sided reading folds the conjugate half (DC and
335
- Nyquist once, interior bins twice) and now reads `32`, satisfying Parseval;
336
- `"two-sided"` preserves the raw sum.
337
-
338
- ### Tests
339
-
340
- - 684 assertions, all passing, with `tsc` reporting no diagnostics. New files
341
- cover the ordered and unordered sets, the error hierarchy, forest
342
- construction, and the 2.0.0 statistics definitions: all nine Hyndman–Fan
343
- quantile types, `G1`/`G2` against `g1`/`g2`, `ddof`, the closed-form
344
- standard errors, and the small-sample empty optionals across the
345
- synchronous, asynchronous, numeric and bigint views.
346
-
347
- ---
348
-
349
- ## [1.2.0] - 2026-09-11
350
-
351
- This release redrew the **division of responsibility in the index model**: a
352
- pipeline performs only cheap, lazy index rewriting, and index tidying is left
353
- entirely to the collectable. `translate` returns to a pure shift, `findAt` and
354
- `findNegativeAt` address materialised order, and four asynchronous and boundary
355
- defects are fixed.
356
-
357
- ### Changed
358
-
359
- - **`translate` is a pure lazy index shift again.** Both the numeric and the
360
- functional overload now do only `i + offset` and `i + mapper(element, i)`, and
361
- no longer materialise the stream (in 1.1.0 the numeric overload buffered
362
- everything before taking the remainder). `translate` can therefore be used on an
363
- infinite stream followed by `limit`.
364
- - **The ordered collectable normalises indices.** `toOrdered()` rewrites every
365
- index to `((i mod n) + n) mod n`, where `n` is the element count, and then sorts
366
- by index. The rotation, reversal and shuffling produced by `reverse`,
367
- `translate` and `shuffle` only become visible at this step — `translate(7)` and
368
- `translate(2)` are equivalent on five elements.
369
- - **The unordered collectable does not tidy indices**, as a performance contract.
370
- `toUnordered()` stores the raw index in a `Map` without normalisation or
371
- sorting, keeping O(n) throughput; `translate(2)` collected unordered still
372
- reports indices `2n, 3n, 4n, …`.
373
- - **`findAt(k)` and `findNegativeAt(k)` address materialised order.** `findAt(1n)`
374
- is always the second element of the materialised sequence and
375
- `findNegativeAt(-1n)` always the last, whatever index rewriting happened
376
- upstream. Both stop early, at O(k) and O(n − k) respectively.
377
- - **`reverse()` is `-index - 1n`** rather than `-index`. Under remainder
378
- normalisation `-index` pins the first element in place, because `-0n === 0n`;
379
- only `-index - 1n` reverses.
380
- - **`redirect` is the sole reordering mechanism.** `reverse`, `translate` and
381
- `shuffle` are preset strategies over it, and all of them require `toOrdered()`
382
- to take effect; an unordered collectable promises arrival order only.
383
-
384
- ### Fixed
385
-
386
- - An asynchronous `translate` failed to chain onto the upstream promise, losing
387
- elements or returning nothing (there was no such defect on the synchronous
388
- side).
389
- - The asynchronous ordered collectable omitted the remainder normalisation and
390
- disagreed with the synchronous side; `translate(2).toOrdered()` returned an
391
- unrotated sequence.
392
- - Asynchronous `flatMap` resolved after the first inner iterable and silently
393
- dropped the rest.
394
- - `dropWhile` discarded the first element that failed the predicate, in both
395
- families: `[1,2,3,4,1].dropWhile(n < 3)` returned `[4,1]` instead of `[3,4,1]`.
396
-
397
- ### Documentation
398
-
399
- - README rewritten in full (English as the reference edition, in British
400
- English). Every factory, operator, collectable and terminal now states its time
401
- and space cost, and the design rationale, architecture and data flow have their
402
- own chapters.
403
- - The index-model chapter was rewritten around the split between lazy pipeline
404
- rewriting and collectable tidying.
405
- - All nine language editions were regenerated: the Simplified Chinese and
406
- Japanese tables of contents were left untranslated and the Russian edition was
407
- missing. Every code block is byte-identical to the English edition.
408
-
409
- ---
410
-
411
- ## [1.1.0] - 2026-09-11
412
-
413
- A **comprehensive defect release**: it fixes sixteen blocking defects introduced
414
- in 1.0.0 — among them "the published package cannot be imported from Node", "index
415
- reordering has no effect" and "the asynchronous chain returns nothing or hangs
416
- forever" — and introduces regression tests for the first time. Three factories
417
- changed their return type; read "Changed" before upgrading.
418
-
419
- ### Changed
420
-
421
- - **`useBlob(...)` returns `AsynchronousSemantic`** (was `SynchronousSemantic`).
422
- Reading a blob is inherently asynchronous, so a synchronous collectable saw an
423
- empty array.
424
- - **`useInterval(...)` returns `AsynchronousSemantic`** (was
425
- `SynchronousSemantic`). The data is produced by a timer, and a synchronous
426
- collectable finished before the first tick.
427
- - **`useFrom(AsyncIterable)` returns `AsynchronousSemantic`** (was
428
- `SynchronousSemantic`). The previous implementation silently returned `[]`.
429
- - **`translate(offset)` became a cyclic shift**, matching README example 5
430
- (positive rotates right, negative rotates left). The numeric overload needs the
431
- stream length and therefore materialises it; the functional overload
432
- `translate(mapper)` remains element-wise and lazy.
433
- - **`partition(count)` is defined as bucket selection by element index**, that is
434
- `partitionBy((_element, index) => index % count)`.
435
- - **`shuffle()` is genuinely random.** The previous implementation derived indices
436
- from element hashes, so the same input produced the same order every run.
437
-
438
- ### Fixed
439
-
440
- **Packaging and tooling**
441
-
442
- - The published artefact could not be loaded from Node: `tsconfig` moved to
443
- `module`/`moduleResolution: NodeNext` and sixty relative imports in the source
444
- gained their `.js` extension. Both ESM and CJS consumers used to fail with
445
- `ERR_MODULE_NOT_FOUND`; only bundlers could resolve the package.
446
- - `engines.node` corrected from `>=14.0.0` to `>=18.17.0`, matching the ES2022
447
- target and the use of `Blob` and `WritableStream` in the types.
448
- - `devDependencies.typescript` was missing, so `npm run build` failed on any fresh
449
- clone.
450
-
451
- **Indices and collectables**
452
-
453
- - `toOrdered()` lost the real index of each element, having faked it from the
454
- array position; `redirect`, `reverse`, `translate` and `shuffle` now reorder
455
- correctly.
456
- - Added the `toOrdered(comparator)` overload, which sorts by value and renumbers
457
- the indices in sequence (the statistics collectables depend on that).
458
- - The unordered collectable falls back to a spare key on a duplicate index rather
459
- than silently dropping data; `useText("a,b,c", ",")` used to return only
460
- `["c"]`.
461
-
462
- **Asynchronous chain**
463
-
464
- - The asynchronous ordered collectable defined `buffer` as a read-only property at
465
- construction, so assigning it from the `then` callback threw a `TypeError`; the
466
- symptom was `toOrdered()` and the statistics collectables returning `[]`, or
467
- `.summate()` hanging forever.
468
- - `source()` resolved without waiting for the data to be ready, which is why
469
- `useInterval` was always empty.
470
- - Added `settled()`: `findFirst`, `findLast`, `findAny`, `findAt`, `findMaximum`
471
- and `findMinimum` now wait or reject correctly instead of hanging forever when
472
- the stream is unfinished or empty.
473
- - `findMaximum` and `findMinimum` compare by element value when no comparator is
474
- given, rather than by index.
475
-
476
- **Terminal operations**
477
-
478
- - `findFirst`, `findLast`, `findAny`, `findMaximum` and `findMinimum` always
479
- returned an empty value.
480
- - `reduce(accumulator)`, the overload without an identity, always returned an
481
- empty `Optional`.
482
- - `numericStatistics.range()` consequently always returned `0`, because it depends
483
- on `findFirst` and `findLast`.
484
- - `nonMatch()` returned the exact opposite of the truth: the collectable finisher
485
- negated one time too many.
486
-
487
- **Operators and factories**
488
-
489
- - `partition` threw `RangeError: Division by zero` on the first element.
490
- - `partitionBy` used the element index rather than the classifier, raising a
491
- `TypeError`.
492
- - `sub(start, end)` deadlocked its counter and never produced an element.
493
- - `useGenerate(supplier, interrupt)` shadowed its second parameter with an
494
- identically named local, so the stop condition never applied.
495
- - `useIterate` produced only the first element of an asynchronous iterable (a
496
- stray `break`) and never advanced its index.
497
- - `useText` gained the single-argument overload (splitting by Unicode code point,
498
- so emoji are handled) and no longer loses data in the delimiter branch, where
499
- the index failed to advance.
500
- - `useStringify` re-entered serialisation recursively inside the replacer and threw
501
- a `TypeError` on a cyclic reference.
502
- - `useRandom` looped forever with probability about 15/16 on wide bigint ranges,
503
- because rejection sampling reused one sample; the low three bits of a random
504
- bigint were always zero, so `useRandom(1n, 10n)` returned only `1` or `9`.
505
- - `useBlob` pointed every chunk at the same in-place buffer, so chunks overwrote
506
- one another.
507
- - `useInterval` checked the interrupt after `accept`, leaking the boundary
508
- element.
509
- - `useSubscription` unsubscribed twice when the boundary was reached.
510
- - `toWindow().slide()` and `tumble()` produced zero windows on an asynchronous
511
- stream, reading the buffer before it was ready.
512
-
513
- **Statistics**
514
-
515
- - The numeric kurtosis formula is corrected from `sum(z⁴)/n² - 3` to
516
- `sum(z⁴)/n - 3`: `[1,2,3,4]` changes from `-2.59` to `-1.36`.
517
- - Bigint statistics: `standardDeviation`, `skewness` and `kurtosis` threw because
518
- `BigInt(Math.sqrt(...))` rejects a non-integral square root, and variance and
519
- standard deviation suffered integer-division truncation (`[1,2,3,4]` reported a
520
- variance of `3`). The statistics are now computed in floating point and rounded
521
- back to `bigint`, matching the numeric editions.
522
-
523
- **Other**
524
-
525
- - `useHash`, `register`, `unregister` and `maskOf` were not exported, because
526
- `src/index.ts` omitted `./hash`.
527
- - The synchronous and asynchronous statistics brands shared one `Symbol.for` key,
528
- and two guard functions mis-spelled their field names;
529
- `isSynchronousNumericStatistics` returned `false` for a numeric statistics
530
- object.
531
- - `useTraverse` pushed onto its path without ever popping, so the path accumulated
532
- across sibling branches.
533
- - The primitive hash cache gained a ceiling (cleared beyond a hundred thousand
534
- entries) so a long-running process does not grow without bound.
535
-
536
- ### Added
537
-
538
- - **Regression tests**: `test/semantic.test.mjs`, 55 assertions covering every
539
- fixed defect, the normal paths, the window operators and the asynchronous chain.
540
- They use the built-in `node:test`, adding no runtime dependency.
541
- - **`npm test`**: `npm run build && node --test`, ready for CI.
542
- - **`CHANGELOG.md`**: this file.
543
- - **`FIXES.md`**: a defect-by-defect account with root causes, the list of
544
- breaking changes and follow-up scheduling.
545
-
546
- ### Changed (tooling)
547
-
548
- - `package.json` gained `"sideEffects": false` so bundlers can tree-shake.
549
- - The `dist/` artefacts joined `.gitignore`; they are built by `prepublishOnly`.
550
-
551
- ### Acknowledgements
552
-
553
- Thanks to issues `#1` to `#5`. This release adopts their findings on build
554
- artefacts, naming and extensibility, and keeps the remainder on the schedule
555
- described in section four of `FIXES.md`.
556
-
557
- ---
558
-
559
- ## Earlier versions
560
-
561
- - **[1.0.0] - 2026-04-07**: the latest version previously on npm. It carries the
562
- sixteen blocking defects listed above, so **upgrading straight to 2.0.0 is
563
- recommended**. The repository `package.json` was still at `0.7.1` when it was
564
- published, so the repository and the published artefact were briefly out of
565
- step.
566
- - **0.6.0 and earlier**: every version before 0.6.0 is marked deprecated on npm
567
- (`< 0.5.0` lacks the event-stream API, and `0.5.x` has circular dependencies).
568
- - The repository had no git tags; `.github/workflows/publish.yml` triggers on
569
- `v*` tags, and the `v1.1.0` tag was added with that release.
570
-
571
- [2.0.0]: https://github.com/eloyhere/semantic-typescript/releases/tag/v2.0.0
572
- [1.2.0]: https://github.com/eloyhere/semantic-typescript/releases/tag/v1.2.0
573
- [1.1.0]: https://github.com/eloyhere/semantic-typescript/releases/tag/v1.1.0
574
- [1.0.0]: https://www.npmjs.com/package/semantic-typescript/v/1.0.0
1
+ # Changelog
2
+
3
+ Notable changes to `semantic-typescript`. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the version numbers
5
+ follow [Semantic Versioning](https://semver.org/).
6
+
7
+ A Simplified Chinese edition is kept in [`CHANGELOG.cn.md`](./CHANGELOG.cn.md).
8
+
9
+ ---
10
+
11
+ ## [2.0.1] - 2026-11-07
12
+
13
+ ### Fixed
14
+
15
+ - **`Optional` no longer freezes `Function.prototype`.** Importing the module ran
16
+ `Object.freeze(Object.getPrototypeOf(Optional))`; for a base class that value is
17
+ the shared `Function.prototype`, so the whole process lost the ability to augment
18
+ functions (`Object.defineProperty(Function.prototype, …)` threw a `TypeError`).
19
+ Instance immutability is unchanged: the constructor still freezes every instance
20
+ and defines its fields as non-writable, and the class plus its own prototype stay
21
+ frozen. Only the mutation of the global intrinsic was removed.
22
+ - **Unordered collectable buffers now use a read-only `Map` facade.** `Object.freeze(map)` does not disable `Map.prototype.set`, `delete` or `clear`, so the old buffer freeze did not actually protect buffered data. Attempts to mutate either synchronous or completed asynchronous unordered buffers now throw `TypeError`.
23
+
24
+ ## [2.0.0] - 2026-11-05
25
+
26
+ This release covers the work merged after 1.2.0. It is a large change: the
27
+ collection families were split, the phase-ring API was reworked, the statistics,
28
+ window, signal and distribution modules became public entry points, and several
29
+ asynchronous lifecycle defects were fixed. Read the "Breaking", "Changed" and
30
+ "Removed" sections before upgrading.
31
+
32
+ ### Breaking
33
+
34
+ **Statistics now default to the definitions used by R, NumPy, pandas and Excel**
35
+
36
+ These are deliberate, owner-approved breaks: the old values were defined
37
+ differently from the mainstream tools, so keeping them would have preserved
38
+ compatibility at the cost of correctness. Each old behaviour remains reachable
39
+ through an explicit option.
40
+
41
+ - **`quantile(probability)` now defaults to Hyndman–Fan type 7**, the
42
+ parameterisation `h = (n - 1)p + 1` with linear interpolation, instead of
43
+ type 1 (`ceil(np)` nearest rank). Type 7 is the default of R `quantile`,
44
+ NumPy `quantile` (`method="linear"`), Excel `PERCENTILE.INC` and pandas
45
+ `quantile`. On `[1..8]` at `p = 0.75` it returns `6.25` where it returned
46
+ `6`; on `[2,4,4,4,5,5,7,9]` at `p = 0.9` it returns `7.6` where it returned
47
+ `9`. All nine types are available as
48
+ `quantile(p, { method: "type-1" })` … `{ method: "type-9" }`, and
49
+ `"type-1"` restores the old nearest-rank answer exactly.
50
+ - **`interquartileRange()` follows the same method** (default type 7), so
51
+ Tukey fences now line up with R and NumPy: `[1..8]` gives `3.5`, not `4`,
52
+ and `[1,2,3,4]` gives `1.5`, not `2`. Pass `{ method }` to change the type.
53
+ - **`kurtosis()` now defaults to the bias-corrected excess estimator `G2`**,
54
+ `G2 = ((n - 1) / ((n - 2)(n - 3))) · ((n + 1)g2 + 6)`, matching Excel
55
+ `KURT` and pandas `kurt`, instead of the biased `g2 = m4 / m2² - 3`. On
56
+ `[1,2,3,4]` it returns `-1.2` where it returned `-1.36`; pass
57
+ `{ estimator: "g2" }` for the old value. `{ excess: false }` adds three back
58
+ and reports the non-excess fourth standardised moment.
59
+ - **`skewness()` now defaults to the bias-corrected `G1`**,
60
+ `G1 = √(n(n - 1))/(n - 2) · g1`, matching Excel `SKEW`, pandas `skew` and
61
+ SPSS, instead of the biased `g1 = m3 / m2^(3/2)`; pass
62
+ `{ estimator: "g1" }` to restore `g1`.
63
+ - **`variance()` and `standardDeviation()` now divide by `n - 1`** (Bessel's
64
+ correction), matching R `var`/`sd`, pandas and NumPy `var(ddof=1)`, instead
65
+ of `n`. On `[1,2,3,4]` the variance is `5/3` where it was `5/4`; pass
66
+ `{ ddof: 0 }` for the previous population value. `standardDeviation` is the
67
+ square root of that variance, so it remains very slightly biased even under
68
+ normality (Jensen's inequality).
69
+ - **Small samples now return an empty `Optional` instead of `0` or `NaN`.**
70
+ `G2` is undefined below four observations, `G1` below three, and the sample
71
+ variance and standard deviation below two; every statistic on an empty view
72
+ is undefined too, as are skewness and kurtosis on a constant sample. For
73
+ example, with three observations the default `kurtosis()` is empty while
74
+ `{ estimator: "g2" }` is defined, and with two observations `skewness()` is
75
+ empty while `{ estimator: "g1" }` is `0`. Bigint views return the same
76
+ empties; with `{ ddof: 0 }` a single observation still has a defined
77
+ variance of zero.
78
+ - **`variance()` and `standardDeviation()` now use a two-pass computation**
79
+ (mean first, then squared deviations) instead of the collector's
80
+ `sum(x²) - sum(x)²/n` form, which suffers catastrophic cancellation when the
81
+ standard deviation is small relative to the mean.
82
+ - **`skewness` and `kurtosis` gained closed-form standard errors.**
83
+ `skewnessStandardError()` and `kurtosisStandardError()` on the numeric
84
+ statistics views return `√(6n(n - 1)/((n - 2)(n + 1)(n + 3)))` and
85
+ `√(24n(n - 1)²/((n - 3)(n - 2)(n + 3)(n + 5)))`, the SAS/SPSS and
86
+ `e1071::skewness`/`e1071::kurtosis` forms. They require three and four
87
+ observations respectively. The bigint views omit them because rounding a
88
+ standard error to an integer would report zero for most samples.
89
+ - **These methods now return `Optional`.** `variance`, `standardDeviation`,
90
+ `quantile`, `interquartileRange`, `skewness` and `kurtosis` all wrap their
91
+ result, so callers must unwrap with `.get()`, `.orElse(...)` or test
92
+ `.isPresent()`. The bigint view still rounds each result to a whole bigint.
93
+ - **Reconciliation.** The new defaults reproduce the published results:
94
+ `[1,2,3,4]` gives `variance(ddof = 1) = 5/3`, `G2 = -1.2`, `g2 = -1.36`,
95
+ `G1 = g1 = 0`; `[1..8]` at `p = 0.75` gives type 7 `6.25` and type 1 `6`;
96
+ the Wikipedia kurtosis example (`n = 20`) gives `g1 = 4.129251`,
97
+ `G1 = 4.471885`, `g2 = 15.051427`, `G2 = 19.998429`, and standard errors
98
+ `0.512103` and `0.992384`. These match R, NumPy, pandas and Excel to the
99
+ published precision.
100
+
101
+ **Signal analysis now uses the physical power and DFT-even conventions**
102
+
103
+ These are also deliberate, owner-approved breaks: the old definitions mixed
104
+ amplitude and power or assumed that only complete frames exist, so they returned
105
+ systematically biased levels and time-frequency frames.
106
+
107
+ - **`decibel` is removed and split into `amplitudeDecibel` and
108
+ `powerDecibel`.** `amplitudeDecibel` keeps the old `20·log10` convention for
109
+ amplitude-like ratios; `powerDecibel` applies `10·log10` for power, energy,
110
+ mean square and power spectral density. Power `0.25` is `−6.02 dB` under
111
+ `powerDecibel`, while the old function reported `−12.04 dB`. `Bin.decibel` and
112
+ `Spectrum.decibelOf` remain amplitude levels; `Spectrum.powerDecibelOf` is the
113
+ matching power level.
114
+ - **`Spectrum.bandEnergy(low, high)` now defaults to the one-sided reading.**
115
+ A real spectrum's conjugate half is folded into the non-negative axis, with
116
+ DC and Nyquist counted once and every interior bin counted twice, so a band
117
+ that covers the whole one-sided axis satisfies Parseval (`Σ|X|² = N·Σx²`).
118
+ The old raw sum, which counted only the positive half of a band below Nyquist
119
+ (−3.01 dB), remains available as `bandEnergy(low, high, "two-sided")`; use it
120
+ for complex spectra.
121
+ - **`Spectrum.flatness()` now measures the one-sided power spectrum.** The old
122
+ method took the geometric/arithmetic mean of magnitudes over both complex
123
+ halves. The result is now computed from `power()`, over bins `0` through
124
+ `floor(count/2)`, without the display-only amplitude doubling of
125
+ `oneSided()`; a zero bin still reports `0`.
126
+ - **`Sampler.shortTimeFourierTransform` and `Sampler.spectrogram` now default
127
+ to the periodic Hann window, `boundary: "zeros"` and `padded: true`.** The
128
+ symmetric Hann window and the old "complete frames only" path (`7` frames for
129
+ `64` samples, frame `16`, hop `8`) remain reachable with
130
+ `useHannWindow`/`useHannPeriodicWindow` and
131
+ `{ boundary: "none", padded: false }`; the default now returns `9` frames,
132
+ loses no tail samples, and matches SciPy's `stft` defaults. The new optional
133
+ fifth argument takes `{ boundary, padded }`.
134
+ - **`Spectrogram` frame times are now frame centres.** `getTime(k)` returns
135
+ `(k·hop + frameSize/2)/rate` for an unextended view and
136
+ `(k·hop + (frameSize mod 2)/2)/rate` for an extended one, instead of the frame
137
+ start. `getDuration()` and `toFrame()` also trim the boundary extension.
138
+ The constructor gained an optional fourth argument, the boundary mode, which
139
+ defaults to `"none"`.
140
+
141
+ ### Added
142
+
143
+ **Collection families**
144
+
145
+ - **`OrderedSet` / `UnorderedSet`**, each in synchronous and asynchronous form.
146
+ `OrderedSet` decides uniqueness and iteration order by a comparator (defaulting
147
+ to `useCompare`); `UnorderedSet` buckets by `useHash` and compares with
148
+ SameValueZero. Iteration order is part of the contract for the ordered variant
149
+ and explicitly unspecified for the unordered one.
150
+ - **`OrderedLRUSet` / `UnorderedLRUSet`** and **`OrderedSkipSet` /
151
+ `UnorderedSkipSet`**, closing the gap where the LRU and SKIP families offered
152
+ ordered and unordered maps but only an undifferentiated set.
153
+ - **`OrderedMap` / `UnorderedMap`** promoted to independent root interfaces. The
154
+ former common base is gone, so neither variant inherits ordering promises from
155
+ the other.
156
+
157
+ **Error model**
158
+
159
+ - A typed error hierarchy in `structure/error.ts`: fifteen classes covering
160
+ validation, indexing, capacity, state, concurrency, collection protocol,
161
+ serialisation, numeric, phase, and configuration failures. Each carries a
162
+ stable `name`, an optional `cause` for the originating error, and an optional
163
+ `context` bag for diagnostics, and each has a brand guard.
164
+ - **`SemanticRangeError`**, an alias for the exported `RangeError` class. Both
165
+ names denote one class, so `instanceof` checks hold across them. The class
166
+ keeps its historical name — no library module imports it implicitly, so a bare
167
+ `new RangeError(...)` still reaches the global constructor — but a consumer that
168
+ writes `import { RangeError }` shadows that global for the whole module,
169
+ turning an incidental `new RangeError(...)` in the same file into a library
170
+ error. Importing `SemanticRangeError` instead leaves the global untouched.
171
+
172
+ **Module layout**
173
+
174
+ - **Collector factories moved to `collectors.ts`.** `collector.ts` now declares
175
+ only `SynchronousCollector` and `AsynchronousCollector` (about 340 and 350
176
+ lines); the fifty-three `useSynchronousXxx` and fifty-three `useAsynchronousXxx`
177
+ factories live in `synchronous/collectors.ts` and `asynchronous/collectors.ts`.
178
+ The dependency runs one way — `collectors.ts` imports `collector.ts`, never the
179
+ reverse — and `factory.ts` remains the module that builds `Semantic` values, so
180
+ creating a `Semantic` and creating a collector are now separate jobs.
181
+ - **Forests get their own module.** `rootedForest` and `unionFindForest` left
182
+ `structure/tree/` for `structure/forest/`, with `forest/index.ts` and a
183
+ `structure/forest.ts` barrel beside the existing `structure/tree.ts`. The
184
+ binomial and Fibonacci heaps, the breadth-first and depth-first search trees
185
+ and the minimum spanning tree import the rooted forest from the new path, as
186
+ does the tree-API boundary type test.
187
+
188
+ **Phase rings**
189
+
190
+ - `PhaseField` and `useField` in `mathematics/field.ts`: a bounded score in
191
+ `[-1, 1]` defined as a truncated cosine series. The constructor enforces
192
+ `|offset| + Σ|weight| ≤ 1`, so the bound is a consequence of the construction
193
+ rather than a clamp applied afterwards.
194
+ - `Cycle` now reports its own state: `purity()`, `sequences()`,
195
+ `zeroSequence()`, `isBalanced()`, `resultant()`, `relativeAt()`, `viewAt()`,
196
+ `viewsAt()`, `jetAt()`, `windingAt()`, `errorIntegralAt()`, `scoreAt()`,
197
+ `scoresAt()`, `analyse()`, and `toSpectrum()`.
198
+ - `combineCycles(a, b)` joins two rings on their least common multiple and
199
+ reports the pair-compatibility condition, so the joint ring is a pullback
200
+ rather than a product.
201
+ - `useAngularSpectrum(cycle, instants, plan)` builds the two-dimensional
202
+ `(ω, n)` plane, one column per spatial mode.
203
+ - `BlobLike`, a structural description of the bytes `useBlob` needs, so the
204
+ published declarations no longer depend on the DOM library being present in the
205
+ consumer's compilation.
206
+
207
+ **Signal analysis**
208
+
209
+ - **Periodic, DFT-even windows**: `useHannPeriodicWindow`,
210
+ `useHammingPeriodicWindow`, `useBlackmanPeriodicWindow`,
211
+ `useNuttallPeriodicWindow`, `useFlatTopPeriodicWindow` and
212
+ `useKaiserPeriodicWindow(beta)`. They evaluate the same shapes as the
213
+ symmetric windows against `count` rather than `count - 1`, so a periodic Hann
214
+ window has a constant overlap-add sum of `1.000000` at a half-frame hop for
215
+ every length, while the symmetric form ripples by `0.841 dB` at `N = 16` and
216
+ still by `0.013 dB` at `N = 1024`. The existing symmetric exports are
217
+ unchanged.
218
+ - **`Spectrum.powerDecibelOf(index, normalization?, reference?)`**, the power
219
+ counterpart of `decibelOf`, returning `10·log10(power / reference)`.
220
+ - **`BandSidedness`** (`"one-sided" | "two-sided"`), the explicit argument type
221
+ of the corrected `Spectrum.bandEnergy`.
222
+ - **`TimeFrequencyOptions`**, the `{ boundary?, padded? }` options of
223
+ `shortTimeFourierTransform` and `spectrogram`, and the `BoundaryExtension`
224
+ (`"zeros" | "constant" | "none"`) type they use.
225
+ - **`Spectrogram.getBoundary()`** and **`Sampler.spectrogram(..., options)`**,
226
+ so a spectrogram retains the extension mode and trims it during `toFrame`.
227
+
228
+ **Public modules**
229
+
230
+ - **`statistics.ts`, `window.ts`, `signal.ts` and `distribution.ts` are now the
231
+ home of their API**, in both `synchronous/` and `asynchronous/`. The classes
232
+ and value objects they own are re-exported by `semantic.ts`, so the historical
233
+ import path `#semantic/synchronous/semantic` (and its asynchronous twin) keeps
234
+ exposing them while the duplicate definitions are gone.
235
+ - **New named exports**: `SynchronousStatistics`, `SynchronousBigIntStatistics`,
236
+ `SynchronousNumericStatistics`, `SynchronousWindowCollectable`,
237
+ `SynchronousWindowArrayCollectable`, `SynchronousSignalCollectable`,
238
+ `SynchronousDistributionCollectable` and their `Asynchronous*` counterparts,
239
+ plus the distribution value objects (`Histogram`, `HistogramBin`,
240
+ `EmpiricalDistribution`, `MomentSummary`, `RobustSummary`, `DensityEstimate`)
241
+ and `SpectralDensity`. Every class carries the synchronous/asynchronous prefix
242
+ the rest of the library uses, because an unprefixed pair would collide in the
243
+ package barrel.
244
+ - **Deep imports**: `package.json` now maps `semantic-typescript/signal`,
245
+ `semantic-typescript/statistics`, `semantic-typescript/window` and
246
+ `semantic-typescript/distribution` to the synchronous module of the same name,
247
+ with its declaration file, so a consumer can import one analysis module
248
+ without loading the whole barrel.
249
+ - **`SynchronousWindowArrayCollectable`** (and its asynchronous twin) materialises
250
+ `slide` / `tumble` into arrays. `toWindow()` keeps returning
251
+ `SynchronousWindowCollectable`, the lazy pipeline form the README documents, so
252
+ a window can still be transformed before it is consumed.
253
+
254
+ **Project**
255
+
256
+ - README rewritten in nine languages on a shared thirteen-section outline, with a
257
+ new section on periodic analysis.
258
+ - `.vscode/settings.json` so editor auto-imports prefer the project's path alias
259
+ over relative specifiers.
260
+
261
+ ### Changed
262
+
263
+ - **`Cycle.frequency` is the ambient rotation rate**, not a phase step between
264
+ positions. The environment advances every phasor by the same angle and therefore
265
+ cannot break the intrinsic pattern. Position phases are now derived from the
266
+ exact residue `i mod n`, which makes `phaseAt(i)` and `phaseAt(i + n)`
267
+ identical to the bit rather than merely close.
268
+ - **`useText` slices by code point** rather than by UTF-16 index, so astral
269
+ characters stay whole; inverted bounds are still swapped and negative positions
270
+ still clamped to zero, matching the documented slice behaviour.
271
+ - **`useRange` accepts descending ranges** when the step is negative, and rejects
272
+ a step whose numeric kind differs from the bounds' kind instead of coercing
273
+ between `number` and `bigint`.
274
+ - The JSDoc for every factory in `factory.ts` and every hook in `hook.ts` was
275
+ rewritten to document actual behaviour, including the overloads.
276
+ - Every one of the 296 interfaces exported from `structure/tree/` now carries an
277
+ interface JSDoc block that states its contract, defaults and cost. Both
278
+ `collectors.ts` modules document their thirty-four factory interfaces and every
279
+ overload in the same way.
280
+
281
+ ### Removed
282
+
283
+ - **`SynchronousSet` / `AsynchronousSet` / `SynchronousMap` / `AsynchronousMap`**,
284
+ together with their brand symbols and guards. Use the ordered or unordered
285
+ variant that matches the semantics you need. There is no compatibility alias.
286
+ - The `Collector` base types of the map family, which are replaced by internal,
287
+ unbranded contracts used only to compose the LRU and SKIP families.
288
+ - **The `structure/tree/rootedForest` and `structure/tree/unionFindForest` entry
289
+ points**, together with their re-exports from `tree/index.ts`. Both contracts
290
+ now live under `structure/forest/`; no compatibility re-export remains at the
291
+ old path.
292
+
293
+ ### Fixed
294
+
295
+ - **`useSubscription` no longer leaks its subscription.** Teardown is idempotent
296
+ and runs on every exit path: the consumer asking to stop, the consumer throwing,
297
+ a subscription failing partway through, and an event arriving after the run has
298
+ ended. Every recorded `unsubscribe` is attempted even when an earlier one
299
+ throws, and `unmount` is attempted even when an unsubscribe threw, so one
300
+ failing host method cannot strand the others. Errors raised during teardown are
301
+ swallowed rather than masking the error that caused the teardown.
302
+ - **`useSubscription` completes at the accepted limit** rather than waiting for
303
+ one more event, and an empty event name list now completes immediately instead
304
+ of never settling. A host that fires the handler synchronously from `subscribe`
305
+ is also handled: the handler is unsubscribed once registration returns.
306
+ - **Event names are no longer restricted to strings.** A symbol or a number is
307
+ accepted as a single event name; previously it was rejected even though such
308
+ keys are ordinary property keys.
309
+ - **`useInterval` clears both of its handles.** A consumer that throws used to
310
+ leave the timer running forever, and a pending initial delay was not tracked at
311
+ all. Both are now cleared as soon as the run stops, and the consumer's error is
312
+ propagated unchanged.
313
+ - **`usePromise` no longer hangs.** The asynchronous executor that swallowed
314
+ rejections is gone: a rejected source and a throwing consumer both reject the
315
+ collection with the original reason.
316
+ - **`useBlob` cancels the stream on the error path.** It previously released the
317
+ reader's lock without cancelling the stream, leaving the underlying source
318
+ producing into a discarded buffer.
319
+ - **The flat-top window keeps its negative lobe.** `useFlatTopWindow` clamped
320
+ every weight at zero with `Math.max(0, ...)`, but a flat-top window is defined
321
+ to be partially negative: at `N = 1024` the unclamped symmetric window has
322
+ `578` negative samples (minimum `−0.070560`), coherent gain `0.215368` and
323
+ noise bandwidth `3.773946` bins, where the clamped version reported `0.233280`
324
+ and `3.199558`. Scalloping loss recovers from `−0.3087 dB` to `−0.0097 dB`
325
+ (about 32× better), and the periodic form matches the research target
326
+ (`CG 0.215579`, `ENBW 3.7702`, `HPBW 3.7247`, `−0.0098 dB`, `577` negative
327
+ samples).
328
+ - **The short-time transform no longer drops the signal tail.** The default
329
+ `boundary: "zeros"` and `padded: true` extend the signal by half a frame at
330
+ each end and zero-pad the tail, so `64` samples at frame `16` and hop `8`
331
+ produce `9` frames instead of `7` and every source sample is covered; a
332
+ `1000`-sample signal at frame `256` and hop `128` produces `9` frames instead
333
+ of `6` (232 samples were previously unreachable).
334
+ - **The short-time transform uses a periodic window by default.** The symmetric
335
+ Hann window does not satisfy constant-overlap-add: at 50 per cent overlap its
336
+ overlap-add sum ripples by `0.841 dB` at `N = 16` and `0.013 dB` at
337
+ `N = 1024`, whereas the new default `useHannPeriodicWindow` sums to
338
+ `1.000000` at every length.
339
+ - **`Spectrum.flatness()` uses the power spectrum.** The old method took the
340
+ geometric/arithmetic mean of magnitudes over the full two-sided spectrum; it
341
+ now uses `power()` over the one-sided half. Two bins with powers `4` and `1`
342
+ report `0.8` where the amplitude reading gave `0.9428`, and a unit impulse
343
+ reports `1`.
344
+ - **`Spectrum.bandEnergy()` no longer halves a band below Nyquist.** The old raw
345
+ two-sided sum counted only the positive half of a real spectrum: a unit cosine
346
+ at bin `1` of an `8`-point spectrum read `16` over `[0, 2)` although its total
347
+ power is `32`. The default one-sided reading folds the conjugate half (DC and
348
+ Nyquist once, interior bins twice) and now reads `32`, satisfying Parseval;
349
+ `"two-sided"` preserves the raw sum.
350
+
351
+ ### Tests
352
+
353
+ - 684 assertions, all passing, with `tsc` reporting no diagnostics. New files
354
+ cover the ordered and unordered sets, the error hierarchy, forest
355
+ construction, and the 2.0.0 statistics definitions: all nine Hyndman–Fan
356
+ quantile types, `G1`/`G2` against `g1`/`g2`, `ddof`, the closed-form
357
+ standard errors, and the small-sample empty optionals across the
358
+ synchronous, asynchronous, numeric and bigint views.
359
+
360
+ ---
361
+
362
+ ## [1.2.0] - 2026-09-11
363
+
364
+ This release redrew the **division of responsibility in the index model**: a
365
+ pipeline performs only cheap, lazy index rewriting, and index tidying is left
366
+ entirely to the collectable. `translate` returns to a pure shift, `findAt` and
367
+ `findNegativeAt` address materialised order, and four asynchronous and boundary
368
+ defects are fixed.
369
+
370
+ ### Changed
371
+
372
+ - **`translate` is a pure lazy index shift again.** Both the numeric and the
373
+ functional overload now do only `i + offset` and `i + mapper(element, i)`, and
374
+ no longer materialise the stream (in 1.1.0 the numeric overload buffered
375
+ everything before taking the remainder). `translate` can therefore be used on an
376
+ infinite stream followed by `limit`.
377
+ - **The ordered collectable normalises indices.** `toOrdered()` rewrites every
378
+ index to `((i mod n) + n) mod n`, where `n` is the element count, and then sorts
379
+ by index. The rotation, reversal and shuffling produced by `reverse`,
380
+ `translate` and `shuffle` only become visible at this step — `translate(7)` and
381
+ `translate(2)` are equivalent on five elements.
382
+ - **The unordered collectable does not tidy indices**, as a performance contract.
383
+ `toUnordered()` stores the raw index in a `Map` without normalisation or
384
+ sorting, keeping O(n) throughput; `translate(2)` collected unordered still
385
+ reports indices `2n, 3n, 4n, …`.
386
+ - **`findAt(k)` and `findNegativeAt(k)` address materialised order.** `findAt(1n)`
387
+ is always the second element of the materialised sequence and
388
+ `findNegativeAt(-1n)` always the last, whatever index rewriting happened
389
+ upstream. Both stop early, at O(k) and O(n − k) respectively.
390
+ - **`reverse()` is `-index - 1n`** rather than `-index`. Under remainder
391
+ normalisation `-index` pins the first element in place, because `-0n === 0n`;
392
+ only `-index - 1n` reverses.
393
+ - **`redirect` is the sole reordering mechanism.** `reverse`, `translate` and
394
+ `shuffle` are preset strategies over it, and all of them require `toOrdered()`
395
+ to take effect; an unordered collectable promises arrival order only.
396
+
397
+ ### Fixed
398
+
399
+ - An asynchronous `translate` failed to chain onto the upstream promise, losing
400
+ elements or returning nothing (there was no such defect on the synchronous
401
+ side).
402
+ - The asynchronous ordered collectable omitted the remainder normalisation and
403
+ disagreed with the synchronous side; `translate(2).toOrdered()` returned an
404
+ unrotated sequence.
405
+ - Asynchronous `flatMap` resolved after the first inner iterable and silently
406
+ dropped the rest.
407
+ - `dropWhile` discarded the first element that failed the predicate, in both
408
+ families: `[1,2,3,4,1].dropWhile(n < 3)` returned `[4,1]` instead of `[3,4,1]`.
409
+
410
+ ### Documentation
411
+
412
+ - README rewritten in full (English as the reference edition, in British
413
+ English). Every factory, operator, collectable and terminal now states its time
414
+ and space cost, and the design rationale, architecture and data flow have their
415
+ own chapters.
416
+ - The index-model chapter was rewritten around the split between lazy pipeline
417
+ rewriting and collectable tidying.
418
+ - All nine language editions were regenerated: the Simplified Chinese and
419
+ Japanese tables of contents were left untranslated and the Russian edition was
420
+ missing. Every code block is byte-identical to the English edition.
421
+
422
+ ---
423
+
424
+ ## [1.1.0] - 2026-09-11
425
+
426
+ A **comprehensive defect release**: it fixes sixteen blocking defects introduced
427
+ in 1.0.0 — among them "the published package cannot be imported from Node", "index
428
+ reordering has no effect" and "the asynchronous chain returns nothing or hangs
429
+ forever" — and introduces regression tests for the first time. Three factories
430
+ changed their return type; read "Changed" before upgrading.
431
+
432
+ ### Changed
433
+
434
+ - **`useBlob(...)` returns `AsynchronousSemantic`** (was `SynchronousSemantic`).
435
+ Reading a blob is inherently asynchronous, so a synchronous collectable saw an
436
+ empty array.
437
+ - **`useInterval(...)` returns `AsynchronousSemantic`** (was
438
+ `SynchronousSemantic`). The data is produced by a timer, and a synchronous
439
+ collectable finished before the first tick.
440
+ - **`useFrom(AsyncIterable)` returns `AsynchronousSemantic`** (was
441
+ `SynchronousSemantic`). The previous implementation silently returned `[]`.
442
+ - **`translate(offset)` became a cyclic shift**, matching README example 5
443
+ (positive rotates right, negative rotates left). The numeric overload needs the
444
+ stream length and therefore materialises it; the functional overload
445
+ `translate(mapper)` remains element-wise and lazy.
446
+ - **`partition(count)` is defined as bucket selection by element index**, that is
447
+ `partitionBy((_element, index) => index % count)`.
448
+ - **`shuffle()` is genuinely random.** The previous implementation derived indices
449
+ from element hashes, so the same input produced the same order every run.
450
+
451
+ ### Fixed
452
+
453
+ **Packaging and tooling**
454
+
455
+ - The published artefact could not be loaded from Node: `tsconfig` moved to
456
+ `module`/`moduleResolution: NodeNext` and sixty relative imports in the source
457
+ gained their `.js` extension. Both ESM and CJS consumers used to fail with
458
+ `ERR_MODULE_NOT_FOUND`; only bundlers could resolve the package.
459
+ - `engines.node` corrected from `>=14.0.0` to `>=18.17.0`, matching the ES2022
460
+ target and the use of `Blob` and `WritableStream` in the types.
461
+ - `devDependencies.typescript` was missing, so `npm run build` failed on any fresh
462
+ clone.
463
+
464
+ **Indices and collectables**
465
+
466
+ - `toOrdered()` lost the real index of each element, having faked it from the
467
+ array position; `redirect`, `reverse`, `translate` and `shuffle` now reorder
468
+ correctly.
469
+ - Added the `toOrdered(comparator)` overload, which sorts by value and renumbers
470
+ the indices in sequence (the statistics collectables depend on that).
471
+ - The unordered collectable falls back to a spare key on a duplicate index rather
472
+ than silently dropping data; `useText("a,b,c", ",")` used to return only
473
+ `["c"]`.
474
+
475
+ **Asynchronous chain**
476
+
477
+ - The asynchronous ordered collectable defined `buffer` as a read-only property at
478
+ construction, so assigning it from the `then` callback threw a `TypeError`; the
479
+ symptom was `toOrdered()` and the statistics collectables returning `[]`, or
480
+ `.summate()` hanging forever.
481
+ - `source()` resolved without waiting for the data to be ready, which is why
482
+ `useInterval` was always empty.
483
+ - Added `settled()`: `findFirst`, `findLast`, `findAny`, `findAt`, `findMaximum`
484
+ and `findMinimum` now wait or reject correctly instead of hanging forever when
485
+ the stream is unfinished or empty.
486
+ - `findMaximum` and `findMinimum` compare by element value when no comparator is
487
+ given, rather than by index.
488
+
489
+ **Terminal operations**
490
+
491
+ - `findFirst`, `findLast`, `findAny`, `findMaximum` and `findMinimum` always
492
+ returned an empty value.
493
+ - `reduce(accumulator)`, the overload without an identity, always returned an
494
+ empty `Optional`.
495
+ - `numericStatistics.range()` consequently always returned `0`, because it depends
496
+ on `findFirst` and `findLast`.
497
+ - `nonMatch()` returned the exact opposite of the truth: the collectable finisher
498
+ negated one time too many.
499
+
500
+ **Operators and factories**
501
+
502
+ - `partition` threw `RangeError: Division by zero` on the first element.
503
+ - `partitionBy` used the element index rather than the classifier, raising a
504
+ `TypeError`.
505
+ - `sub(start, end)` deadlocked its counter and never produced an element.
506
+ - `useGenerate(supplier, interrupt)` shadowed its second parameter with an
507
+ identically named local, so the stop condition never applied.
508
+ - `useIterate` produced only the first element of an asynchronous iterable (a
509
+ stray `break`) and never advanced its index.
510
+ - `useText` gained the single-argument overload (splitting by Unicode code point,
511
+ so emoji are handled) and no longer loses data in the delimiter branch, where
512
+ the index failed to advance.
513
+ - `useStringify` re-entered serialisation recursively inside the replacer and threw
514
+ a `TypeError` on a cyclic reference.
515
+ - `useRandom` looped forever with probability about 15/16 on wide bigint ranges,
516
+ because rejection sampling reused one sample; the low three bits of a random
517
+ bigint were always zero, so `useRandom(1n, 10n)` returned only `1` or `9`.
518
+ - `useBlob` pointed every chunk at the same in-place buffer, so chunks overwrote
519
+ one another.
520
+ - `useInterval` checked the interrupt after `accept`, leaking the boundary
521
+ element.
522
+ - `useSubscription` unsubscribed twice when the boundary was reached.
523
+ - `toWindow().slide()` and `tumble()` produced zero windows on an asynchronous
524
+ stream, reading the buffer before it was ready.
525
+
526
+ **Statistics**
527
+
528
+ - The numeric kurtosis formula is corrected from `sum(z⁴)/n² - 3` to
529
+ `sum(z⁴)/n - 3`: `[1,2,3,4]` changes from `-2.59` to `-1.36`.
530
+ - Bigint statistics: `standardDeviation`, `skewness` and `kurtosis` threw because
531
+ `BigInt(Math.sqrt(...))` rejects a non-integral square root, and variance and
532
+ standard deviation suffered integer-division truncation (`[1,2,3,4]` reported a
533
+ variance of `3`). The statistics are now computed in floating point and rounded
534
+ back to `bigint`, matching the numeric editions.
535
+
536
+ **Other**
537
+
538
+ - `useHash`, `register`, `unregister` and `maskOf` were not exported, because
539
+ `src/index.ts` omitted `./hash`.
540
+ - The synchronous and asynchronous statistics brands shared one `Symbol.for` key,
541
+ and two guard functions mis-spelled their field names;
542
+ `isSynchronousNumericStatistics` returned `false` for a numeric statistics
543
+ object.
544
+ - `useTraverse` pushed onto its path without ever popping, so the path accumulated
545
+ across sibling branches.
546
+ - The primitive hash cache gained a ceiling (cleared beyond a hundred thousand
547
+ entries) so a long-running process does not grow without bound.
548
+
549
+ ### Added
550
+
551
+ - **Regression tests**: `test/semantic.test.mjs`, 55 assertions covering every
552
+ fixed defect, the normal paths, the window operators and the asynchronous chain.
553
+ They use the built-in `node:test`, adding no runtime dependency.
554
+ - **`npm test`**: `npm run build && node --test`, ready for CI.
555
+ - **`CHANGELOG.md`**: this file.
556
+ - **`FIXES.md`**: a defect-by-defect account with root causes, the list of
557
+ breaking changes and follow-up scheduling.
558
+
559
+ ### Changed (tooling)
560
+
561
+ - `package.json` gained `"sideEffects": false` so bundlers can tree-shake.
562
+ - The `dist/` artefacts joined `.gitignore`; they are built by `prepublishOnly`.
563
+
564
+ ### Acknowledgements
565
+
566
+ Thanks to issues `#1` to `#5`. This release adopts their findings on build
567
+ artefacts, naming and extensibility, and keeps the remainder on the schedule
568
+ described in section four of `FIXES.md`.
569
+
570
+ ---
571
+
572
+ ## Earlier versions
573
+
574
+ - **[1.0.0] - 2026-04-07**: the latest version previously on npm. It carries the
575
+ sixteen blocking defects listed above, so **upgrading straight to 2.0.0 is
576
+ recommended**. The repository `package.json` was still at `0.7.1` when it was
577
+ published, so the repository and the published artefact were briefly out of
578
+ step.
579
+ - **0.6.0 and earlier**: every version before 0.6.0 is marked deprecated on npm
580
+ (`< 0.5.0` lacks the event-stream API, and `0.5.x` has circular dependencies).
581
+ - The repository had no git tags; `.github/workflows/publish.yml` triggers on
582
+ `v*` tags, and the `v1.1.0` tag was added with that release.
583
+
584
+ [2.0.0]: https://github.com/eloyhere/semantic-typescript/releases/tag/v2.0.0
585
+ [1.2.0]: https://github.com/eloyhere/semantic-typescript/releases/tag/v1.2.0
586
+ [1.1.0]: https://github.com/eloyhere/semantic-typescript/releases/tag/v1.1.0
587
+ [1.0.0]: https://www.npmjs.com/package/semantic-typescript/v/1.0.0