@hviana/sema 0.4.4 → 0.4.7
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/AUTHORS.md +0 -1
- package/LICENSE.md +1 -1
- package/README.md +2 -2
- package/dist/src/geometry.d.ts +6 -0
- package/dist/src/geometry.js +224 -44
- package/dist/src/mind/attention.d.ts +11 -0
- package/dist/src/mind/attention.js +344 -13
- package/dist/src/mind/junction.js +18 -2
- package/dist/src/mind/match.d.ts +11 -0
- package/dist/src/mind/match.js +13 -2
- package/dist/src/mind/mechanisms/cast.js +366 -34
- package/dist/src/mind/mechanisms/confluence.js +17 -1
- package/dist/src/mind/mechanisms/recall.js +17 -3
- package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
- package/dist/src/mind/pipeline-mechanism.js +96 -40
- package/dist/src/mind/pipeline.js +31 -3
- package/dist/src/mind/reasoning.d.ts +4 -2
- package/dist/src/mind/reasoning.js +29 -4
- package/dist/src/mind/recognition.js +67 -2
- package/dist/src/mind/resonance.d.ts +14 -2
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/types.d.ts +43 -1
- package/dist/src/rabitq-ivf/src/rabitq.js +31 -1
- package/dist/src/sema.d.ts +11 -1
- package/dist/src/sema.js +16 -2
- package/dist/src/store.d.ts +64 -1
- package/dist/src/store.js +107 -8
- package/index.html +2 -3
- package/package.json +1 -1
- package/src/geometry.ts +231 -43
- package/src/mind/attention.ts +366 -15
- package/src/mind/junction.ts +18 -2
- package/src/mind/match.ts +18 -2
- package/src/mind/mechanisms/cast.ts +376 -43
- package/src/mind/mechanisms/confluence.ts +16 -1
- package/src/mind/mechanisms/recall.ts +17 -2
- package/src/mind/pipeline-mechanism.ts +96 -36
- package/src/mind/pipeline.ts +33 -3
- package/src/mind/reasoning.ts +31 -4
- package/src/mind/recognition.ts +65 -2
- package/src/mind/resonance.ts +0 -0
- package/src/mind/types.ts +43 -1
- package/src/rabitq-ivf/src/rabitq.ts +31 -1
- package/src/sema.ts +21 -2
- package/src/store.ts +106 -5
- package/test/00-extract.test.mjs +28 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/24-generalization.test.mjs +67 -19
- package/test/29-counterfactual.test.mjs +106 -42
- package/test/33-multi-candidate.test.mjs +56 -12
- package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
- package/test/63-fold-invariants.test.mjs +489 -0
- package/test/64-two-ended-thresholds.test.mjs +76 -0
- package/test/65-ann-recall.test.mjs +331 -0
package/dist/src/store.d.ts
CHANGED
|
@@ -41,14 +41,65 @@ export declare class BoundedMap<K, V> {
|
|
|
41
41
|
readonly maxBytes: number;
|
|
42
42
|
private readonly sizeOf;
|
|
43
43
|
private readonly evict;
|
|
44
|
+
/** How a HIT records recency.
|
|
45
|
+
*
|
|
46
|
+
* `"reorder"` (default) promotes the entry to most-recent by
|
|
47
|
+
* `m.delete(k); m.set(k, v)` — exact LRU, and the only policy that is
|
|
48
|
+
* safe for a cache whose CONTENTS are load-bearing rather than merely
|
|
49
|
+
* warm. `_depositTrees` (8 entries, feeds stablePrefixFoldIncremental)
|
|
50
|
+
* is exactly that: which of its entries survives changes how the next
|
|
51
|
+
* turn FOLDS, so test/13 D1 flips answer when the victim changes.
|
|
52
|
+
*
|
|
53
|
+
* `"clock"` records recency as a BIT instead of as position, spent by
|
|
54
|
+
* the eviction sweep (see `nextOldest`). Correct only for a TRANSPARENT
|
|
55
|
+
* cache — one where evicting the wrong entry costs a re-read and nothing
|
|
56
|
+
* else. Opt in deliberately, per cache. */
|
|
57
|
+
private readonly recency;
|
|
44
58
|
private m;
|
|
45
59
|
private _bytes;
|
|
46
60
|
private _cursor;
|
|
47
61
|
private _candidates;
|
|
48
|
-
|
|
62
|
+
private _used;
|
|
63
|
+
constructor(maxBytes: number, sizeOf?: (v: V) => number, evict?: Evict,
|
|
64
|
+
/** How a HIT records recency.
|
|
65
|
+
*
|
|
66
|
+
* `"reorder"` (default) promotes the entry to most-recent by
|
|
67
|
+
* `m.delete(k); m.set(k, v)` — exact LRU, and the only policy that is
|
|
68
|
+
* safe for a cache whose CONTENTS are load-bearing rather than merely
|
|
69
|
+
* warm. `_depositTrees` (8 entries, feeds stablePrefixFoldIncremental)
|
|
70
|
+
* is exactly that: which of its entries survives changes how the next
|
|
71
|
+
* turn FOLDS, so test/13 D1 flips answer when the victim changes.
|
|
72
|
+
*
|
|
73
|
+
* `"clock"` records recency as a BIT instead of as position, spent by
|
|
74
|
+
* the eviction sweep (see `nextOldest`). Correct only for a TRANSPARENT
|
|
75
|
+
* cache — one where evicting the wrong entry costs a re-read and nothing
|
|
76
|
+
* else. Opt in deliberately, per cache. */
|
|
77
|
+
recency?: "reorder" | "clock");
|
|
49
78
|
/** Next key in insertion (≈ LRU) order, resuming where the last call left
|
|
50
79
|
* off; wraps to the front when exhausted. Undefined only when empty. */
|
|
51
80
|
private nextOldest;
|
|
81
|
+
/** RECENCY WITHOUT MUTATING THE MAP (clock policy only).
|
|
82
|
+
*
|
|
83
|
+
* The default `"reorder"` policy below is the textbook JS LRU
|
|
84
|
+
* (`m.delete(k); m.set(k, v)`), and on the read path that idiom was
|
|
85
|
+
* measured as the single largest CPU consumer in inference: 55% of
|
|
86
|
+
* profiled self time, 14.6s of a 26.4s battery, over 6.4M gets — 5.3M of
|
|
87
|
+
* them on `_bytesCache` alone at an 83% hit rate. Every hit deletes and
|
|
88
|
+
* reinserts a live key, and each delete leaves a hole in V8's ordered
|
|
89
|
+
* backing store that is compacted only on rehash — the same O(size) cliff
|
|
90
|
+
* the eviction cursor above already documents, paid here on the path taken
|
|
91
|
+
* orders of magnitude more often.
|
|
92
|
+
*
|
|
93
|
+
* Under `"clock"`, a hit sets a BIT that the eviction sweep spends.
|
|
94
|
+
* `Set.add` of a key already present neither inserts nor rehashes, so hot
|
|
95
|
+
* keys — the 83% — cost one hash probe and nothing else. Measured on the
|
|
96
|
+
* two transparent store caches, with every counter byte-identical
|
|
97
|
+
* (nodeRecords 488,468 / byteReads 62,959 in both arms — the SAME entries
|
|
98
|
+
* stayed cached): multi-turn think 11,399ms -> 2,022ms, its crossRegion
|
|
99
|
+
* 8,833ms -> 771ms; single-turn think 12,977ms -> 6,779ms.
|
|
100
|
+
*
|
|
101
|
+
* It is NOT the default, because it is only sound where eviction costs a
|
|
102
|
+
* re-read. See the `recency` parameter. */
|
|
52
103
|
get(k: K): V | undefined;
|
|
53
104
|
/** Membership without touching LRU order — a pure peek, for callers that only
|
|
54
105
|
* need "is this key present?" and must not promote it to most-recent. */
|
|
@@ -567,6 +618,18 @@ export declare abstract class AbstractStore implements Store {
|
|
|
567
618
|
* A no-op when the node is already indexed or its gist is evicted from
|
|
568
619
|
* the pending cache — a future re-encounter will retry. */
|
|
569
620
|
private promoteBridge;
|
|
621
|
+
/** Re-index a node under a DIFFERENT gist for the same content.
|
|
622
|
+
*
|
|
623
|
+
* Normally a node's gist is a pure function of its id, so indexGist skips
|
|
624
|
+
* anything already indexed. Step 1b of {@link intern} breaks that: it
|
|
625
|
+
* reuses an id for the same BYTES folded a different way, and the two
|
|
626
|
+
* foldings have different gists. The index holds one vector per id, so
|
|
627
|
+
* the node must carry the gist a direct query of those bytes will present
|
|
628
|
+
* — otherwise it is unreachable from exactly the query that names it.
|
|
629
|
+
*
|
|
630
|
+
* A no-op when the gists agree, so the ordinary path pays one comparison
|
|
631
|
+
* and nothing else. */
|
|
632
|
+
private recaptureGist;
|
|
570
633
|
private intern;
|
|
571
634
|
/** Whether the byte content under `kids` and the byte content of `targetId`
|
|
572
635
|
* are identical except for ONE local span of at most `W` bytes on each side
|
package/dist/src/store.js
CHANGED
|
@@ -74,6 +74,7 @@ export class BoundedMap {
|
|
|
74
74
|
maxBytes;
|
|
75
75
|
sizeOf;
|
|
76
76
|
evict;
|
|
77
|
+
recency;
|
|
77
78
|
m = new Map();
|
|
78
79
|
_bytes = 0;
|
|
79
80
|
// Persistent eviction cursor over the Map's insertion order. A fresh
|
|
@@ -86,32 +87,87 @@ export class BoundedMap {
|
|
|
86
87
|
// "smallest" mode: oldest-entry candidates carried between evictions, fed
|
|
87
88
|
// from the cursor, so the LRU window never rescans from the front.
|
|
88
89
|
_candidates = [];
|
|
89
|
-
|
|
90
|
+
// SECOND-CHANCE (CLOCK) RECENCY BITS — see `get`. Populated only under
|
|
91
|
+
// `recency: "clock"`; the default policy leaves this empty and unread.
|
|
92
|
+
_used = new Set();
|
|
93
|
+
constructor(maxBytes, sizeOf = () => 1, evict = "lru",
|
|
94
|
+
/** How a HIT records recency.
|
|
95
|
+
*
|
|
96
|
+
* `"reorder"` (default) promotes the entry to most-recent by
|
|
97
|
+
* `m.delete(k); m.set(k, v)` — exact LRU, and the only policy that is
|
|
98
|
+
* safe for a cache whose CONTENTS are load-bearing rather than merely
|
|
99
|
+
* warm. `_depositTrees` (8 entries, feeds stablePrefixFoldIncremental)
|
|
100
|
+
* is exactly that: which of its entries survives changes how the next
|
|
101
|
+
* turn FOLDS, so test/13 D1 flips answer when the victim changes.
|
|
102
|
+
*
|
|
103
|
+
* `"clock"` records recency as a BIT instead of as position, spent by
|
|
104
|
+
* the eviction sweep (see `nextOldest`). Correct only for a TRANSPARENT
|
|
105
|
+
* cache — one where evicting the wrong entry costs a re-read and nothing
|
|
106
|
+
* else. Opt in deliberately, per cache. */
|
|
107
|
+
recency = "reorder") {
|
|
90
108
|
this.maxBytes = maxBytes;
|
|
91
109
|
this.sizeOf = sizeOf;
|
|
92
110
|
this.evict = evict;
|
|
111
|
+
this.recency = recency;
|
|
93
112
|
}
|
|
94
113
|
/** Next key in insertion (≈ LRU) order, resuming where the last call left
|
|
95
114
|
* off; wraps to the front when exhausted. Undefined only when empty. */
|
|
96
115
|
nextOldest() {
|
|
116
|
+
// SECOND CHANCE (clock recency only): a key whose bit is set is not a
|
|
117
|
+
// victim — the bit is CLEARED and the sweep moves on, so it survives this
|
|
118
|
+
// pass but not the next unless `get` touches it again. Each skip spends
|
|
119
|
+
// one bit and bits are only set by hits, so a sweep skips at most `size`
|
|
120
|
+
// times before finding a victim: amortised O(1), as before.
|
|
121
|
+
let skips = this.m.size;
|
|
97
122
|
for (let wrapped = false;;) {
|
|
98
123
|
if (this._cursor === null)
|
|
99
124
|
this._cursor = this.m.keys();
|
|
100
125
|
const n = this._cursor.next();
|
|
101
|
-
if (!n.done)
|
|
126
|
+
if (!n.done) {
|
|
127
|
+
if (this._used.size > 0 && this._used.has(n.value) && skips-- > 0) {
|
|
128
|
+
this._used.delete(n.value);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
102
131
|
return n.value;
|
|
132
|
+
}
|
|
103
133
|
this._cursor = null;
|
|
104
134
|
if (this.m.size === 0 || wrapped)
|
|
105
135
|
return undefined;
|
|
106
136
|
wrapped = true;
|
|
107
137
|
}
|
|
108
138
|
}
|
|
139
|
+
/** RECENCY WITHOUT MUTATING THE MAP (clock policy only).
|
|
140
|
+
*
|
|
141
|
+
* The default `"reorder"` policy below is the textbook JS LRU
|
|
142
|
+
* (`m.delete(k); m.set(k, v)`), and on the read path that idiom was
|
|
143
|
+
* measured as the single largest CPU consumer in inference: 55% of
|
|
144
|
+
* profiled self time, 14.6s of a 26.4s battery, over 6.4M gets — 5.3M of
|
|
145
|
+
* them on `_bytesCache` alone at an 83% hit rate. Every hit deletes and
|
|
146
|
+
* reinserts a live key, and each delete leaves a hole in V8's ordered
|
|
147
|
+
* backing store that is compacted only on rehash — the same O(size) cliff
|
|
148
|
+
* the eviction cursor above already documents, paid here on the path taken
|
|
149
|
+
* orders of magnitude more often.
|
|
150
|
+
*
|
|
151
|
+
* Under `"clock"`, a hit sets a BIT that the eviction sweep spends.
|
|
152
|
+
* `Set.add` of a key already present neither inserts nor rehashes, so hot
|
|
153
|
+
* keys — the 83% — cost one hash probe and nothing else. Measured on the
|
|
154
|
+
* two transparent store caches, with every counter byte-identical
|
|
155
|
+
* (nodeRecords 488,468 / byteReads 62,959 in both arms — the SAME entries
|
|
156
|
+
* stayed cached): multi-turn think 11,399ms -> 2,022ms, its crossRegion
|
|
157
|
+
* 8,833ms -> 771ms; single-turn think 12,977ms -> 6,779ms.
|
|
158
|
+
*
|
|
159
|
+
* It is NOT the default, because it is only sound where eviction costs a
|
|
160
|
+
* re-read. See the `recency` parameter. */
|
|
109
161
|
get(k) {
|
|
110
162
|
const v = this.m.get(k);
|
|
111
|
-
if (v
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
if (v === undefined)
|
|
164
|
+
return v;
|
|
165
|
+
if (this.recency === "clock") {
|
|
166
|
+
this._used.add(k);
|
|
167
|
+
return v;
|
|
114
168
|
}
|
|
169
|
+
this.m.delete(k);
|
|
170
|
+
this.m.set(k, v);
|
|
115
171
|
return v;
|
|
116
172
|
}
|
|
117
173
|
/** Membership without touching LRU order — a pure peek, for callers that only
|
|
@@ -158,6 +214,7 @@ export class BoundedMap {
|
|
|
158
214
|
this._candidates.splice(bestI, 1);
|
|
159
215
|
this._bytes -= bestSz;
|
|
160
216
|
this.m.delete(bestK);
|
|
217
|
+
this._used.delete(bestK);
|
|
161
218
|
}
|
|
162
219
|
else {
|
|
163
220
|
const lru = this.nextOldest();
|
|
@@ -168,6 +225,7 @@ export class BoundedMap {
|
|
|
168
225
|
continue;
|
|
169
226
|
this._bytes -= this.sizeOf(lruv);
|
|
170
227
|
this.m.delete(lru);
|
|
228
|
+
this._used.delete(lru);
|
|
171
229
|
}
|
|
172
230
|
}
|
|
173
231
|
}
|
|
@@ -184,6 +242,7 @@ export class BoundedMap {
|
|
|
184
242
|
return;
|
|
185
243
|
this._bytes -= this.sizeOf(v);
|
|
186
244
|
this.m.delete(k);
|
|
245
|
+
this._used.delete(k);
|
|
187
246
|
}
|
|
188
247
|
/** Drop every entry (bulk invalidation) — O(1) amortised via fresh maps. */
|
|
189
248
|
clear() {
|
|
@@ -193,6 +252,8 @@ export class BoundedMap {
|
|
|
193
252
|
this._bytes = 0;
|
|
194
253
|
this._cursor = null;
|
|
195
254
|
this._candidates = [];
|
|
255
|
+
if (this._used.size > 0)
|
|
256
|
+
this._used = new Set();
|
|
196
257
|
}
|
|
197
258
|
}
|
|
198
259
|
// ── Serialisation utilities (pure functions, no DB dependency) ───────────
|
|
@@ -477,9 +538,9 @@ export class AbstractStore {
|
|
|
477
538
|
this.compactEveryNWrites = config.compactEveryNWrites;
|
|
478
539
|
this._leafKey = new BoundedMap(config.dedupCacheMax);
|
|
479
540
|
this._branchKey = new BoundedMap(config.dedupCacheMax);
|
|
480
|
-
this._bytesCache = new BoundedMap(config.bytesCacheMax, (v) => v.byteLength, "smallest");
|
|
541
|
+
this._bytesCache = new BoundedMap(config.bytesCacheMax, (v) => v.byteLength, "smallest", "clock");
|
|
481
542
|
this._lenCache = new BoundedMap(config.bytesCacheMax, () => 16);
|
|
482
|
-
this._recCache = new BoundedMap(config.recCacheBytes, (r) => (r.leaf?.byteLength ?? 0) + (r.kids?.length ?? 0) * 4 + 12);
|
|
543
|
+
this._recCache = new BoundedMap(config.recCacheBytes, (r) => (r.leaf?.byteLength ?? 0) + (r.kids?.length ?? 0) * 4 + 12, "lru", "clock");
|
|
483
544
|
this._pendingGist = new BoundedMap(config.pendingGistBytes, (v) => v.byteLength);
|
|
484
545
|
this._haloExact = new BoundedMap(config.haloCacheBytes, (v) => v.byteLength);
|
|
485
546
|
this._haloNorm = new BoundedMap(config.haloCacheBytes, (v) => v.byteLength);
|
|
@@ -894,6 +955,33 @@ export class AbstractStore {
|
|
|
894
955
|
this.indexGist(id, false);
|
|
895
956
|
}
|
|
896
957
|
// ── Core interning: dedup → near-dedup → mint ──────────────────────────
|
|
958
|
+
/** Re-index a node under a DIFFERENT gist for the same content.
|
|
959
|
+
*
|
|
960
|
+
* Normally a node's gist is a pure function of its id, so indexGist skips
|
|
961
|
+
* anything already indexed. Step 1b of {@link intern} breaks that: it
|
|
962
|
+
* reuses an id for the same BYTES folded a different way, and the two
|
|
963
|
+
* foldings have different gists. The index holds one vector per id, so
|
|
964
|
+
* the node must carry the gist a direct query of those bytes will present
|
|
965
|
+
* — otherwise it is unreachable from exactly the query that names it.
|
|
966
|
+
*
|
|
967
|
+
* A no-op when the gists agree, so the ordinary path pays one comparison
|
|
968
|
+
* and nothing else. */
|
|
969
|
+
recaptureGist(id, gist) {
|
|
970
|
+
const v = normalize(copy(gist));
|
|
971
|
+
const current = this._pendingGist.get(id);
|
|
972
|
+
// Same direction — the ordinary case, where the bytes folded identically.
|
|
973
|
+
if (current !== undefined && dot(current, v) >= 1 - 1e-6)
|
|
974
|
+
return;
|
|
975
|
+
if (current === undefined && !this._indexedIds.has(id) &&
|
|
976
|
+
!this._vecContentHas(id)) {
|
|
977
|
+
this.captureIfUnindexed(id, gist);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this._pendingGist.set(id, v);
|
|
981
|
+
this._indexedIds.set(id, true);
|
|
982
|
+
this._contentBuffer.push({ id, vector: v });
|
|
983
|
+
this._bufferedIds.add(id);
|
|
984
|
+
}
|
|
897
985
|
async intern(leaf, kids, gist) {
|
|
898
986
|
await this._ensureReady();
|
|
899
987
|
// 1. Exact dedup — equal content → one id, no vector work. Primary
|
|
@@ -919,7 +1007,18 @@ export class AbstractStore {
|
|
|
919
1007
|
if (leafIds !== null) {
|
|
920
1008
|
const flatHit = this.findBranch(leafIds);
|
|
921
1009
|
if (flatHit !== null) {
|
|
922
|
-
|
|
1010
|
+
// The id is reused — same bytes, same node, as documented above.
|
|
1011
|
+
// But the GIST is not a pure function of the id here, which is the
|
|
1012
|
+
// assumption indexGist makes: these bytes fold one way standing
|
|
1013
|
+
// alone and another way embedded, because any bounded-memory cut
|
|
1014
|
+
// rule sees no context before a stream's first bytes. Whichever
|
|
1015
|
+
// folding arrived first owned the index entry, and a query naming
|
|
1016
|
+
// exactly these bytes — which perceives the STANDALONE folding —
|
|
1017
|
+
// could not reach the node at all (test/02: express returned
|
|
1018
|
+
// nothing for a node whose bytes were right there). Re-index with
|
|
1019
|
+
// the incoming gist, which is the standalone perception and so the
|
|
1020
|
+
// one a direct query will present.
|
|
1021
|
+
this.recaptureGist(flatHit, gist);
|
|
923
1022
|
return flatHit;
|
|
924
1023
|
}
|
|
925
1024
|
}
|
package/index.html
CHANGED
|
@@ -913,7 +913,7 @@
|
|
|
913
913
|
<div class="cta">
|
|
914
914
|
<a class="btn primary" href="https://github.com/hviana/sema"
|
|
915
915
|
>View on GitHub</a>
|
|
916
|
-
<a class="btn ghost" href="mailto:
|
|
916
|
+
<a class="btn ghost" href="mailto:reis.marcelo@gmail.com">Contact</a>
|
|
917
917
|
</div>
|
|
918
918
|
</div>
|
|
919
919
|
</main>
|
|
@@ -942,8 +942,7 @@
|
|
|
942
942
|
</div>
|
|
943
943
|
<div>
|
|
944
944
|
commercial licensing:
|
|
945
|
-
<a href="mailto:reis.marcelo@gmail.com">reis.marcelo@gmail.com</a>
|
|
946
|
-
<a href="mailto:rogernact@gmail.com">rogernact@gmail.com</a>
|
|
945
|
+
<a href="mailto:reis.marcelo@gmail.com">reis.marcelo@gmail.com</a>
|
|
947
946
|
</div>
|
|
948
947
|
<div class="lic">
|
|
949
948
|
Released under the
|