@zakkster/lite-logn 0.5.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,120 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] - 2026-09-20
10
+
11
+ ### Added
12
+
13
+ - **MinMaxHeap** -- the seventh member and the family's DOUBLE-ENDED priority queue
14
+ (DEPQ): a single array-embedded binary heap whose levels ALTERNATE min / max (Atkinson,
15
+ Sack, Santoro & Strothotte 1986). Even depth (the root is depth 0) is a MIN level, odd
16
+ depth a MAX level, so the global minimum sits at the root and the global maximum is the
17
+ LARGER of the root's up-to-two children. `peekMin` / `peekMax` / `peekMinKey` /
18
+ `peekMaxKey` are O(1); `push` / `popMin` / `popMax` are all WORST-case O(log n) from that
19
+ ONE heap (no second heap, no paired-heap correspondence). Surface: `push(id, key)` /
20
+ `popMin` / `popMax` / `peekMin` / `peekMax` / `peekMinKey` / `peekMaxKey` / `clear` /
21
+ `forEach` / `[Symbol.iterator]`, and `size` / `capacity` getters, plus a Floyd O(n)
22
+ `MinMaxHeap.build(ids, keys, capacity)` (deepest-first, level-aware sift-down). Keys are
23
+ finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity fail
24
+ closed with a `[lite-logn]` throw, key checked FIRST, then the id, then a full heap).
25
+ - **id + key payload, NON-addressable (the asymmetry vs BinaryHeap).** Two parallel
26
+ pointer-free typed-array columns (`_id` Uint32Array + `_key` Float64Array), the BinaryHeap
27
+ id+key idiom -- but with NO `_pos` reverse map, and therefore deliberately NO `changeKey` /
28
+ `remove`. The id is an OPAQUE Uint32 payload (NOT unique; the full [0, 2^32) domain, wider
29
+ than BinaryHeap's [0, capacity)). A DEPQ's job is the two extremes; addressability is the
30
+ separable concern BinaryHeap already carries. There is also NO `kind` argument / getter (a
31
+ DEPQ has both ends; a kind getter would be a lie).
32
+ - **Classic one-element-per-node min-max heap only.** The interval-heap DEPQ (two elements
33
+ per node) is a deliberately deferred alternative -- named, never silently omitted (see
34
+ `decisions/0009-minmaxheap.md` and "What this is not").
35
+ - **Level parity, computed zero-alloc.** Slot i is a MIN level iff
36
+ `((31 - Math.clz32(i + 1)) & 1) === 0`. The sifts are hole-punching (one write per level,
37
+ only local scalar temporaries): `push` compares to the parent to pick the own-level vs
38
+ other-level chain then bubbles by grandparents; `popMin` / `popMax` trickle down over the
39
+ up-to-six descendants (children + grandchildren), bound-checking every grandchild index
40
+ against the live size (the classic min-max off-by-one, verified at n = 1, 2, 3, 4).
41
+
42
+ ### Verified
43
+
44
+ - **Witness (D-M5).** `MinMaxHeap.popMin` is the gated O(log n) witness op (the full-height
45
+ level-aware trickle-down). It inherits the FROZEN family R^2 floor 0.958 and calibrates its
46
+ OWN slope band by the shared ADR-0004 method: median-of-15 popMin fit-runs = 10.30 ns/level
47
+ (runs spanned 9.38..10.59, R^2 0.9897..0.9991), band = median x `[0.6, 1.4]` = `[6.18, 14.42]`.
48
+ A DEPQ whose push / popMin / popMax are ALL worst-case, so there is NO max-single-op line.
49
+ The O(n) foil is a linear min-scan-and-splice extract-min (R^2 ~ 0.77, OFF the line).
50
+ - **Zero-GC.** `node --expose-gc test/torture.mjs` -- 0 B/op on the popMin / popMax / mixed
51
+ push+popMin+popMax churn and the peek read lanes, gc major = 0, the deliberately-allocating
52
+ control lane still non-zero (teeth), arrayBuffers do not grow across fill/clear soak cycles.
53
+ `npm run test:perf` -- 0 scavenges + `grows === 0` on every MinMaxHeap scenario.
54
+
55
+ ### Notes
56
+
57
+ - `LogN.js` gains `MMH_MAX_CAPACITY` (`0x7FFFFFFF`) + the `MinMaxHeap` class, appended after
58
+ Scapegoat; the prior six classes stay BYTE-IDENTICAL (only the `VERSION` const changes above
59
+ the append point). Version bumped to 0.7.0 across `package.json`, `LogN.js`, and `llms.txt`.
60
+
61
+ ## [0.6.0] - 2026-09-20
62
+
63
+ ### Added
64
+
65
+ - **Scapegoat** -- the sixth member and the family's DETERMINISTIC balanced BST, the
66
+ honest PAIR to Treap: a weight-balanced binary search tree that is ALSO an
67
+ order-statistic tree (an AUGMENTED ordered map key -> value). Where a treap randomizes
68
+ its shape to be balanced IN EXPECTATION, a scapegoat keeps a hard WORST-CASE height
69
+ bound -- so `get` is O(log n) WORST-case (never merely expected) -- and pays for it with
70
+ AMORTIZED O(log n) `set` / `delete`, where an occasional subtree rebuild absorbs the
71
+ imbalance. A subtree-size column `_size` (maintained in the same pass as every link
72
+ rewrite and every rebuild) adds O(log n) order statistics. Surface: `get` / `has` /
73
+ `set` (updates the value in place on an existing key) / `delete` (idempotent) /
74
+ `rank(x)` (count of keys STRICTLY less than x) / `select(k)` (the k-th smallest key,
75
+ 0-based) / `successor` (strictly greater) / `predecessor` (strictly less) /
76
+ `rangeIter(lo, hi)` (a VERSION-STAMPED iterator over `[lo, hi]` inclusive, ascending;
77
+ `+-Infinity` bounds allowed, structural OR value mutation mid-iteration throws) /
78
+ `forEach` / `clear`, and `size` / `capacity` / `alpha` getters. Keys and values are
79
+ finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
80
+ fail closed with a `[lite-logn]` throw).
81
+ - **NO priorities, NO RNG (fully deterministic).** Unlike Treap, Scapegoat draws no random
82
+ priority and uses no LCG / `Math.random` anywhere: the tree shape is a deterministic
83
+ function of the insert / delete order. `alpha` (the weight-balance factor) is validated
84
+ to the OPEN interval `(0.55, 0.75)` -- both ends throw -- and frozen at construction
85
+ (default `2/3`); the alpha-derived depth constant `_invAlpha = 1/alpha` is ctor-cached so
86
+ the hot insert path uses no per-op `Math.log` (the depth test is `_invAlpha^d > size`).
87
+ - **Zero-GC rebuild over preallocated scratch (the load-bearing design call).** No fresh
88
+ array per rebuild: ONE `_flat` (`Uint32Array(capacity)`) + ONE `_stack`
89
+ (`Uint32Array(capacity+1)`) are allocated at construction and reused every rebuild. An
90
+ ITERATIVE, Morris-free in-order flatten (via `_stack`) writes sorted slot indices into
91
+ `_flat`; a bounded log-depth balanced rebuild re-links `_left` / `_right` / `_size` on
92
+ the native call stack. Proven 0 B/op even under a rebuild-HEAVY ascending-insert trace by
93
+ the torture gate and a dedicated PerfGate scavenge-clean scenario
94
+ (decisions/0008-scapegoat.md).
95
+ - **Third bind of the shared NodePool.** Nodes are slot INDICES in five flat columns
96
+ (`_key` / `_value` Float64; `_left` / `_right` / `_size` Uint32, `NIL = 0`) over the SAME
97
+ private free-list (`NodePool`) SkipList and Treap ship -- design-parity, not a fork or a
98
+ runtime dep. The conservation invariant `activeSlots + freeListLength === capacity` holds
99
+ after every op, INCLUDING across rebuild storms. `SG_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1:
100
+ slot indices + subtree counts fit a `Uint32`).
101
+ - **The documented asymmetry vs Treap: NO `split` / `merge`.** A scapegoat has no priority
102
+ heap to merge by, and an honest deterministic split/merge would be O(n) rebuilds
103
+ (forfeiting the sub-linear headline), so Scapegoat's surface is the ordered-map +
104
+ order-statistic core and split/merge are deliberately absent -- named on the public
105
+ surface (JSDoc + `llms.txt` + README + the `.d.ts`), not hidden.
106
+ - **Witness: `Scapegoat.get` gated + the amortized-trace assertion.** `get` (a
107
+ deterministic weight-balanced descent) is gated ON the O(log n) line in its own
108
+ calibrated band `SCAPEGOAT_GET_SLOPE_LO/HI = [2.41, 5.63]` (median-of-15 slope 4.02
109
+ ns/level * [0.6, 1.4], MEDIAN-centered per ADR-0004), inheriting the FROZEN shared R^2
110
+ floor 0.958; its O(n) linear-scan foil leaves the line. The rebuild spike lives on the
111
+ AMORTIZED `set` path and is NEVER gated as a per-op line; instead the amortized-trace
112
+ assertion proves the amortization -- the cumulative ascending-insert (rebuild-heavy)
113
+ cost/op tracks a LOG curve (last/first ratio ~1.5x, gated `< 4x`) where a rebuild-less
114
+ BST would blow to ~64x. The five prior members' bands are UNTOUCHED.
115
+
116
+ ### Notes
117
+
118
+ - Append-only: `LogN.js` gains `SG_MAX_CAPACITY` + the `Scapegoat` class after Treap;
119
+ BinaryHeap / Fenwick / SegmentTree / SkipList / Treap are BYTE-IDENTICAL (only the
120
+ `VERSION` const changes). The repo-only benchmark admits Scapegoat as the 6th SUBJECT
121
+ (matrix 5x8=40 -> 6x8=48, a new `OLOGN_AMORTIZED` honesty class for `set` / `delete`).
122
+
9
123
  ## [0.5.0] - 2026-09-20
10
124
 
11
125
  ### Added
@@ -250,7 +364,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
250
364
  [`decisions/0003-pack.md`](./decisions/0003-pack.md) (D-07: `files[]` ships the
251
365
  six files only; `test/`, `benchmark/`, `decisions/`, `demo/` are repo-only).
252
366
 
253
- [Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.4.0...HEAD
367
+ [Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.7.0...HEAD
368
+ [0.7.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.6.0...v0.7.0
254
369
  [0.4.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.3.0...v0.4.0
255
370
  [0.3.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.2.0...v0.3.0
256
371
  [0.2.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...v0.2.0
package/LogN.d.ts CHANGED
@@ -226,3 +226,106 @@ export class Treap {
226
226
  * both. Non-Treap inputs, different arenas, or an overlapping range throw. */
227
227
  static merge(a: Treap, b: Treap): Treap;
228
228
  }
229
+
230
+ /**
231
+ * A scapegoat tree: a DETERMINISTIC, weight-balanced BST that is also an order-statistic
232
+ * tree (an AUGMENTED ordered map key -> value) -- the honest pair to Treap. `get` is
233
+ * WORST-case O(log n) (a hard height bound, never merely expected); `set` / `delete` are
234
+ * AMORTIZED O(log n) (an occasional subtree rebuild absorbs the imbalance). A subtree-size
235
+ * column adds O(log n) rank / select. No priorities, no RNG: the shape is a deterministic
236
+ * function of the insert / delete order. Nodes are slot INDICES in flat typed-array columns
237
+ * over a private free-list, and every rebuild reuses ONE preallocated scratch buffer + index
238
+ * stack (no heap object, no fresh array per op). Keys and values are finite numbers (typeof-
239
+ * guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed). set on an existing
240
+ * key updates the value in place. Fixed capacity: a full pool throws. Every hot op allocates
241
+ * zero bytes. There is deliberately NO split / merge (the treap's arena-sharing surgery has no
242
+ * honest deterministic O(log n) analogue here) -- the documented asymmetry vs Treap.
243
+ */
244
+ export class Scapegoat {
245
+ /** @param capacity exact max live entries; integer in [1, 2^31-1].
246
+ * @param alpha weight-balance factor in the OPEN interval (0.55, 0.75); default 2/3.
247
+ * Both ends throw. Frozen after construction. */
248
+ constructor(capacity: number, alpha?: number);
249
+
250
+ /** Live entry count. */
251
+ readonly size: number;
252
+ /** The fixed capacity this tree was sized for. */
253
+ readonly capacity: number;
254
+ /** The frozen weight-balance factor. */
255
+ readonly alpha: number;
256
+
257
+ /** The value under key, or undefined if absent (no throw). Non-finite key throws. */
258
+ get(key: number): number | undefined;
259
+ /** True iff key is currently stored. Non-finite key throws. */
260
+ has(key: number): boolean;
261
+ /** Insert key -> value, or update the value in place if key exists. Non-finite
262
+ * key/value throws; a full pool throws. */
263
+ set(key: number, value: number): this;
264
+ /** Remove key; true if it was present, false if absent (idempotent). Non-finite key throws. */
265
+ delete(key: number): boolean;
266
+ /** Count of stored keys strictly less than x (its rank), in [0, size]. Non-finite x throws. */
267
+ rank(x: number): number;
268
+ /** The k-th smallest key (0-based), or undefined if k is out of [0, size). Non-integer k throws. */
269
+ select(k: number): number | undefined;
270
+ /** The smallest key strictly greater than key, or undefined. Non-finite key throws. */
271
+ successor(key: number): number | undefined;
272
+ /** The largest key strictly less than key, or undefined. Non-finite key throws. */
273
+ predecessor(key: number): number | undefined;
274
+ /** A version-stamped iterator over keys in [lo, hi] inclusive, ascending. Bounds
275
+ * may be +-Infinity (unbounded ends); NaN or lo > hi throws; mutation during
276
+ * iteration throws. */
277
+ rangeIter(lo: number, hi: number): IterableIterator<number>;
278
+ /** Visit every (key, value) pair in ascending key order. */
279
+ forEach(fn: (key: number, value: number, tree: Scapegoat) => void): void;
280
+ /** Empty the tree, keeping capacity. */
281
+ clear(): this;
282
+ }
283
+
284
+ /**
285
+ * A min-max heap: a DOUBLE-ENDED priority queue (DEPQ) in ONE array-embedded binary heap
286
+ * whose levels ALTERNATE min / max (Atkinson et al. 1986). The minimum is the root; the
287
+ * maximum is the larger of the root's up-to-two children. peekMin / peekMax are O(1);
288
+ * push / popMin / popMax are O(log n) WORST-case. Two parallel typed-array columns (_key
289
+ * Float64, _id Uint32); the id is an OPAQUE Uint32 payload in [0, 2^32) (not unique, no
290
+ * reverse map), so there is deliberately NO changeKey / remove (the asymmetry vs
291
+ * BinaryHeap). Keys are finite numbers (typeof-guarded before coercion; Symbol / BigInt /
292
+ * NaN / +-Infinity fail closed). Fixed capacity: a full heap throws. Every hot op
293
+ * allocates zero bytes.
294
+ */
295
+ export class MinMaxHeap {
296
+ /** @param capacity exact max live entries; integer in [1, 2^31-1]. */
297
+ constructor(capacity: number);
298
+
299
+ /** Live entry count. */
300
+ readonly size: number;
301
+ /** The fixed capacity this heap was sized for. */
302
+ readonly capacity: number;
303
+
304
+ /** Insert id with priority key. Throws on non-finite key, out-of-range id, or a full heap. */
305
+ push(id: number, key: number): void;
306
+ /** Remove and return the id at the minimum key, or undefined if empty. */
307
+ popMin(): number | undefined;
308
+ /** Remove and return the id at the maximum key, or undefined if empty. */
309
+ popMax(): number | undefined;
310
+ /** The id at the minimum key, or undefined if empty. */
311
+ peekMin(): number | undefined;
312
+ /** The id at the maximum key, or undefined if empty. */
313
+ peekMax(): number | undefined;
314
+ /** The minimum key, or undefined if empty. */
315
+ peekMinKey(): number | undefined;
316
+ /** The maximum key, or undefined if empty. */
317
+ peekMaxKey(): number | undefined;
318
+ /** Empty the heap, keeping capacity. */
319
+ clear(): void;
320
+ /** Visit every live (id, key) pair in unspecified (heap-array) order. */
321
+ forEach(fn: (id: number, key: number, heap: MinMaxHeap) => void): void;
322
+ /** Iterate live entity ids in unspecified (heap-array) order (NOT sorted). */
323
+ [Symbol.iterator](): IterableIterator<number>;
324
+
325
+ /** Floyd O(n) bulk build from parallel ids/keys arrays (level-aware sift-down). */
326
+ static build(
327
+ ids: ArrayLike<number>,
328
+ keys: ArrayLike<number>,
329
+ capacity: number,
330
+ ): MinMaxHeap;
331
+ }