@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.
Files changed (54) hide show
  1. package/AUTHORS.md +0 -1
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -2
  4. package/dist/src/geometry.d.ts +6 -0
  5. package/dist/src/geometry.js +224 -44
  6. package/dist/src/mind/attention.d.ts +11 -0
  7. package/dist/src/mind/attention.js +344 -13
  8. package/dist/src/mind/junction.js +18 -2
  9. package/dist/src/mind/match.d.ts +11 -0
  10. package/dist/src/mind/match.js +13 -2
  11. package/dist/src/mind/mechanisms/cast.js +366 -34
  12. package/dist/src/mind/mechanisms/confluence.js +17 -1
  13. package/dist/src/mind/mechanisms/recall.js +17 -3
  14. package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
  15. package/dist/src/mind/pipeline-mechanism.js +96 -40
  16. package/dist/src/mind/pipeline.js +31 -3
  17. package/dist/src/mind/reasoning.d.ts +4 -2
  18. package/dist/src/mind/reasoning.js +29 -4
  19. package/dist/src/mind/recognition.js +67 -2
  20. package/dist/src/mind/resonance.d.ts +14 -2
  21. package/dist/src/mind/resonance.js +0 -0
  22. package/dist/src/mind/types.d.ts +43 -1
  23. package/dist/src/rabitq-ivf/src/rabitq.js +31 -1
  24. package/dist/src/sema.d.ts +11 -1
  25. package/dist/src/sema.js +16 -2
  26. package/dist/src/store.d.ts +64 -1
  27. package/dist/src/store.js +107 -8
  28. package/index.html +2 -3
  29. package/package.json +1 -1
  30. package/src/geometry.ts +231 -43
  31. package/src/mind/attention.ts +366 -15
  32. package/src/mind/junction.ts +18 -2
  33. package/src/mind/match.ts +18 -2
  34. package/src/mind/mechanisms/cast.ts +376 -43
  35. package/src/mind/mechanisms/confluence.ts +16 -1
  36. package/src/mind/mechanisms/recall.ts +17 -2
  37. package/src/mind/pipeline-mechanism.ts +96 -36
  38. package/src/mind/pipeline.ts +33 -3
  39. package/src/mind/reasoning.ts +31 -4
  40. package/src/mind/recognition.ts +65 -2
  41. package/src/mind/resonance.ts +0 -0
  42. package/src/mind/types.ts +43 -1
  43. package/src/rabitq-ivf/src/rabitq.ts +31 -1
  44. package/src/sema.ts +21 -2
  45. package/src/store.ts +106 -5
  46. package/test/00-extract.test.mjs +28 -0
  47. package/test/15-decomposition-gap.test.mjs +0 -0
  48. package/test/24-generalization.test.mjs +67 -19
  49. package/test/29-counterfactual.test.mjs +106 -42
  50. package/test/33-multi-candidate.test.mjs +56 -12
  51. package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
  52. package/test/63-fold-invariants.test.mjs +489 -0
  53. package/test/64-two-ended-thresholds.test.mjs +76 -0
  54. package/test/65-ann-recall.test.mjs +331 -0
@@ -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
- constructor(maxBytes: number, sizeOf?: (v: V) => number, evict?: Evict);
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
- constructor(maxBytes, sizeOf = () => 1, evict = "lru") {
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 !== undefined) {
112
- this.m.delete(k);
113
- this.m.set(k, v);
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
- this.captureIfUnindexed(flatHit, gist);
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:rogernact@gmail.com">Contact</a>
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.4.4",
3
+ "version": "0.4.7",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",