@zakkster/lite-logn 0.4.0 → 0.6.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,136 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-09-20
10
+
11
+ ### Added
12
+
13
+ - **Scapegoat** -- the sixth member and the family's DETERMINISTIC balanced BST, the
14
+ honest PAIR to Treap: a weight-balanced binary search tree that is ALSO an
15
+ order-statistic tree (an AUGMENTED ordered map key -> value). Where a treap randomizes
16
+ its shape to be balanced IN EXPECTATION, a scapegoat keeps a hard WORST-CASE height
17
+ bound -- so `get` is O(log n) WORST-case (never merely expected) -- and pays for it with
18
+ AMORTIZED O(log n) `set` / `delete`, where an occasional subtree rebuild absorbs the
19
+ imbalance. A subtree-size column `_size` (maintained in the same pass as every link
20
+ rewrite and every rebuild) adds O(log n) order statistics. Surface: `get` / `has` /
21
+ `set` (updates the value in place on an existing key) / `delete` (idempotent) /
22
+ `rank(x)` (count of keys STRICTLY less than x) / `select(k)` (the k-th smallest key,
23
+ 0-based) / `successor` (strictly greater) / `predecessor` (strictly less) /
24
+ `rangeIter(lo, hi)` (a VERSION-STAMPED iterator over `[lo, hi]` inclusive, ascending;
25
+ `+-Infinity` bounds allowed, structural OR value mutation mid-iteration throws) /
26
+ `forEach` / `clear`, and `size` / `capacity` / `alpha` getters. Keys and values are
27
+ finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
28
+ fail closed with a `[lite-logn]` throw).
29
+ - **NO priorities, NO RNG (fully deterministic).** Unlike Treap, Scapegoat draws no random
30
+ priority and uses no LCG / `Math.random` anywhere: the tree shape is a deterministic
31
+ function of the insert / delete order. `alpha` (the weight-balance factor) is validated
32
+ to the OPEN interval `(0.55, 0.75)` -- both ends throw -- and frozen at construction
33
+ (default `2/3`); the alpha-derived depth constant `_invAlpha = 1/alpha` is ctor-cached so
34
+ the hot insert path uses no per-op `Math.log` (the depth test is `_invAlpha^d > size`).
35
+ - **Zero-GC rebuild over preallocated scratch (the load-bearing design call).** No fresh
36
+ array per rebuild: ONE `_flat` (`Uint32Array(capacity)`) + ONE `_stack`
37
+ (`Uint32Array(capacity+1)`) are allocated at construction and reused every rebuild. An
38
+ ITERATIVE, Morris-free in-order flatten (via `_stack`) writes sorted slot indices into
39
+ `_flat`; a bounded log-depth balanced rebuild re-links `_left` / `_right` / `_size` on
40
+ the native call stack. Proven 0 B/op even under a rebuild-HEAVY ascending-insert trace by
41
+ the torture gate and a dedicated PerfGate scavenge-clean scenario
42
+ (decisions/0008-scapegoat.md).
43
+ - **Third bind of the shared NodePool.** Nodes are slot INDICES in five flat columns
44
+ (`_key` / `_value` Float64; `_left` / `_right` / `_size` Uint32, `NIL = 0`) over the SAME
45
+ private free-list (`NodePool`) SkipList and Treap ship -- design-parity, not a fork or a
46
+ runtime dep. The conservation invariant `activeSlots + freeListLength === capacity` holds
47
+ after every op, INCLUDING across rebuild storms. `SG_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1:
48
+ slot indices + subtree counts fit a `Uint32`).
49
+ - **The documented asymmetry vs Treap: NO `split` / `merge`.** A scapegoat has no priority
50
+ heap to merge by, and an honest deterministic split/merge would be O(n) rebuilds
51
+ (forfeiting the sub-linear headline), so Scapegoat's surface is the ordered-map +
52
+ order-statistic core and split/merge are deliberately absent -- named on the public
53
+ surface (JSDoc + `llms.txt` + README + the `.d.ts`), not hidden.
54
+ - **Witness: `Scapegoat.get` gated + the amortized-trace assertion.** `get` (a
55
+ deterministic weight-balanced descent) is gated ON the O(log n) line in its own
56
+ calibrated band `SCAPEGOAT_GET_SLOPE_LO/HI = [2.41, 5.63]` (median-of-15 slope 4.02
57
+ ns/level * [0.6, 1.4], MEDIAN-centered per ADR-0004), inheriting the FROZEN shared R^2
58
+ floor 0.958; its O(n) linear-scan foil leaves the line. The rebuild spike lives on the
59
+ AMORTIZED `set` path and is NEVER gated as a per-op line; instead the amortized-trace
60
+ assertion proves the amortization -- the cumulative ascending-insert (rebuild-heavy)
61
+ cost/op tracks a LOG curve (last/first ratio ~1.5x, gated `< 4x`) where a rebuild-less
62
+ BST would blow to ~64x. The five prior members' bands are UNTOUCHED.
63
+
64
+ ### Notes
65
+
66
+ - Append-only: `LogN.js` gains `SG_MAX_CAPACITY` + the `Scapegoat` class after Treap;
67
+ BinaryHeap / Fenwick / SegmentTree / SkipList / Treap are BYTE-IDENTICAL (only the
68
+ `VERSION` const changes). The repo-only benchmark admits Scapegoat as the 6th SUBJECT
69
+ (matrix 5x8=40 -> 6x8=48, a new `OLOGN_AMORTIZED` honesty class for `set` / `delete`).
70
+
71
+ ## [0.5.0] - 2026-09-20
72
+
73
+ ### Added
74
+
75
+ - **Treap** -- the fifth member and the family's balanced BST: a randomized,
76
+ self-balancing binary search tree that is ALSO an order-statistic tree (an AUGMENTED
77
+ ordered map key -> value). A BST order on `_key` x a MAX-HEAP order on a per-node
78
+ random priority `_prio` gives EXPECTED O(log n) height, and a subtree-size column
79
+ `_size` (maintained in the SAME pass as every link rewrite) adds O(log n) order
80
+ statistics + set surgery. Surface: `get` / `has` / `set` (updates the value in place
81
+ on an existing key) / `delete` (idempotent) / `rank(x)` (count of keys STRICTLY less
82
+ than x) / `select(k)` (the k-th smallest key, 0-based) / `successor` (strictly
83
+ greater) / `predecessor` (strictly less) / `rangeIter(lo, hi)` (a VERSION-STAMPED
84
+ iterator over `[lo, hi]` inclusive, ascending; `+-Infinity` bounds allowed,
85
+ structural OR value mutation mid-iteration throws) / `forEach` / `clear` / `split`,
86
+ the static `Treap.merge(a, b)`, and `size` / `capacity` getters. Keys and values are
87
+ finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
88
+ fail closed with a `[lite-logn]` throw).
89
+ - **Pointer-free, zero-GC, second bind of the shared NodePool.** Nodes are slot
90
+ INDICES in six flat columns (`_key` / `_value` Float64; `_left` / `_right` / `_prio`
91
+ / `_size` Uint32, `NIL = 0`) over the SAME private free-list (`NodePool`) SkipList
92
+ ships -- design-parity, not a fork or a runtime dep (decisions/0007-treap.md). The
93
+ conservation invariant `activeSlots + freeListLength === capacity` holds after every
94
+ op. `TR_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1: slot indices + subtree counts fit a
95
+ `Uint32`). Rotations rewrite one child link pair + two `_size` cells; `set` / `delete`
96
+ / `split` / `merge` recurse over slot indices on the native CALL STACK (not the GC
97
+ heap), so every hot op is 0 B/op.
98
+ - **Priority via the repo LCG.** One instance-local Numerical-Recipes LCG draw per
99
+ inserted node; a fixed seed replays an identical structure, ties break by key, so the
100
+ tree shape is a deterministic function of the (key, priority) set.
101
+ - **split / merge are O(log n) EXPECTED (arena-sharing).** `split(key)` returns
102
+ `[left (keys < key), right (keys >= key)]` by rewiring in place, so the two treaps
103
+ SHARE the source's backing arena and the source is CONSUMED (left empty);
104
+ `Treap.merge(a, b)` requires `a` / `b` to share an arena (all keys of a < all of b)
105
+ and consumes both. Fails closed on non-Treap inputs, cross-arena treaps, or an
106
+ overlapping key range.
107
+ - **EXPECTED, not worst-case.** A hot op is EXPECTED O(log n) (the randomized
108
+ priority heap); the MAX single insert (rotation chain) is DISCLOSED by the witness,
109
+ never gated -- the same honesty contract as SkipList.
110
+ - **Witness: one more log line.** `test/witness.mjs` gains `Treap.get` (a BST descent)
111
+ against a linear-scan O(n) foil. Measured on this machine (shared, FROZEN R^2 floor
112
+ 0.958, the four prior members' bands UNTOUCHED): `get` R^2 ~ 0.988, slope ~ 4.03
113
+ ns/level, in its OWN band `[2.55, 5.95]` = median-of-15 fit-runs (median 4.25) x
114
+ `[0.6, 1.4]`, centered on the median (ADR-0004). A treap descent touches one node per
115
+ level, so its per-level slope is lower than SkipList.get's (~8.78) -- expected, which
116
+ is why only the R^2 floor is shared. The linear-scan foil MISSES the floor
117
+ (R^2 ~ 0.79). All EIGHT gated op-rows are ON-LINE; MAX single insert disclosed
118
+ (~54 us on the cold shuffled build trace).
119
+ - **Types + docs.** `LogN.d.ts` gains the `Treap` ambient block; `llms.txt` gains the
120
+ Treap roster entry + full export surface; `decisions/0007-treap.md` records D-06/D-07
121
+ (the balanced-BST pick, the augmentation, the NodePool reuse, the arena-sharing
122
+ split/merge, and the recursion-depth disclosure).
123
+
124
+ ### Verified
125
+
126
+ - Torture: 0 B/op on every Treap hot lane (get / set / delete / rank / select /
127
+ successor / forEach / rangeIter) + the mixed steady-state churn; `gc major = 0`;
128
+ leak `size 0/0`; the private-pool conservation invariant after every soak cycle; a
129
+ 32 B/op control lane proving the instrument has teeth. Prior four members still green.
130
+ - `test/perf/PerfGate.test.mjs`: four Treap scenarios (get / set / delete / rank-select-
131
+ successor mix) at 0 scavenges, backing buffers fixed (the `grows` counter reads 0).
132
+ - `test/Treap.test.mjs`: a >= 1e5 mixed-op differential fuzz vs a Map + sorted-array
133
+ oracle (0 divergences), the three treap invariants checked throughout (BST order,
134
+ heap order, subtree-size correctness), rank/select/split/merge correctness, the
135
+ fail-closed doors ([lite-logn] tag pinned), determinism, and conservation.
136
+ - `LogN.js`: the prior four classes are BYTE-IDENTICAL; only the `VERSION` const and
137
+ the appended `Treap` section changed.
138
+
9
139
  ## [0.4.0] - 2026-09-17
10
140
 
11
141
  ### Added
package/LogN.d.ts CHANGED
@@ -171,3 +171,112 @@ export class SkipList {
171
171
  /** Empty the list, keeping capacity (resets the PRNG to its initial seed). */
172
172
  clear(): this;
173
173
  }
174
+
175
+ /**
176
+ * A treap: a randomized, self-balancing BST that is also an order-statistic tree (an
177
+ * AUGMENTED ordered map key -> value). BST order on keys x max-heap order on a per-node
178
+ * random priority gives EXPECTED O(log n) height; a subtree-size column adds O(log n)
179
+ * rank / select / split / merge. Nodes are slot INDICES in flat typed-array columns
180
+ * over a private free-list (no heap object per op). Keys and values are finite numbers
181
+ * (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed). set
182
+ * on an existing key updates the value in place. EXPECTED, not worst-case (an unlucky
183
+ * priority draw can spike one op; the MAX single insert is disclosed, not gated). Fixed
184
+ * capacity: a full pool throws. Every hot op allocates zero bytes.
185
+ */
186
+ export class Treap {
187
+ /** @param capacity exact max live entries; integer in [1, 2^31-1].
188
+ * @param seed PRNG seed; unsigned 32-bit integer (default fixed). */
189
+ constructor(capacity: number, seed?: number);
190
+
191
+ /** Live entry count. */
192
+ readonly size: number;
193
+ /** The fixed capacity this treap was sized for. */
194
+ readonly capacity: number;
195
+
196
+ /** The value under key, or undefined if absent (no throw). Non-finite key throws. */
197
+ get(key: number): number | undefined;
198
+ /** True iff key is currently stored. Non-finite key throws. */
199
+ has(key: number): boolean;
200
+ /** Insert key -> value, or update the value in place if key exists. Non-finite
201
+ * key/value throws; a full pool throws. */
202
+ set(key: number, value: number): this;
203
+ /** Remove key; true if it was present, false if absent (idempotent). Non-finite key throws. */
204
+ delete(key: number): boolean;
205
+ /** Count of stored keys strictly less than x (its rank), in [0, size]. Non-finite x throws. */
206
+ rank(x: number): number;
207
+ /** The k-th smallest key (0-based), or undefined if k is out of [0, size). Non-integer k throws. */
208
+ select(k: number): number | undefined;
209
+ /** The smallest key strictly greater than key, or undefined. Non-finite key throws. */
210
+ successor(key: number): number | undefined;
211
+ /** The largest key strictly less than key, or undefined. Non-finite key throws. */
212
+ predecessor(key: number): number | undefined;
213
+ /** A version-stamped iterator over keys in [lo, hi] inclusive, ascending. Bounds
214
+ * may be +-Infinity (unbounded ends); NaN or lo > hi throws; mutation during
215
+ * iteration throws. */
216
+ rangeIter(lo: number, hi: number): IterableIterator<number>;
217
+ /** Visit every (key, value) pair in ascending key order. */
218
+ forEach(fn: (key: number, value: number, treap: Treap) => void): void;
219
+ /** Empty the treap, keeping capacity (resets the PRNG to its initial seed). */
220
+ clear(): this;
221
+
222
+ /** Split at key into [left (keys < key), right (keys >= key)]; CONSUMES this and
223
+ * returns two treaps SHARING this treap's backing arena. Non-finite key throws. */
224
+ split(key: number): [Treap, Treap];
225
+ /** Merge two arena-sharing treaps where every key of a < every key of b, CONSUMING
226
+ * both. Non-Treap inputs, different arenas, or an overlapping range throw. */
227
+ static merge(a: Treap, b: Treap): Treap;
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
+ }