@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 +130 -0
- package/LogN.d.ts +109 -0
- package/LogN.js +1087 -1
- package/README.md +174 -8
- package/llms.txt +99 -2
- package/package.json +10 -3
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
|
+
}
|