@hviana/sema 0.1.5 → 0.1.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 (170) hide show
  1. package/AGENTS.md +6 -5
  2. package/CITATION.cff +49 -0
  3. package/HOW_IT_WORKS.md +11 -12
  4. package/README.md +7 -5
  5. package/dist/example/demo.js +25 -25
  6. package/dist/example/train_base.d.ts +19 -21
  7. package/dist/example/train_base.js +1700 -1952
  8. package/dist/src/alphabet.d.ts +3 -3
  9. package/dist/src/alphabet.js +24 -27
  10. package/dist/src/alu/src/alu.d.ts +169 -194
  11. package/dist/src/alu/src/alu.js +374 -414
  12. package/dist/src/alu/src/expr.d.ts +36 -43
  13. package/dist/src/alu/src/expr.js +252 -278
  14. package/dist/src/alu/src/index.d.ts +9 -98
  15. package/dist/src/alu/src/index.js +10 -64
  16. package/dist/src/alu/src/kernel-arith.d.ts +1 -5
  17. package/dist/src/alu/src/kernel-arith.js +195 -285
  18. package/dist/src/alu/src/kernel-bits.d.ts +1 -5
  19. package/dist/src/alu/src/kernel-bits.js +81 -115
  20. package/dist/src/alu/src/kernel-logic.js +33 -63
  21. package/dist/src/alu/src/kernel-nd.js +155 -195
  22. package/dist/src/alu/src/kernel-numeric.d.ts +16 -80
  23. package/dist/src/alu/src/kernel-numeric.js +259 -331
  24. package/dist/src/alu/src/operation.d.ts +112 -132
  25. package/dist/src/alu/src/operation.js +149 -163
  26. package/dist/src/alu/src/parser.d.ts +168 -175
  27. package/dist/src/alu/src/parser.js +382 -421
  28. package/dist/src/alu/src/resonance.d.ts +16 -25
  29. package/dist/src/alu/src/resonance.js +49 -56
  30. package/dist/src/alu/src/text.d.ts +3 -7
  31. package/dist/src/alu/src/text.js +31 -37
  32. package/dist/src/alu/src/value.d.ts +14 -14
  33. package/dist/src/alu/src/value.js +150 -160
  34. package/dist/src/alu/test/alu.test.js +543 -656
  35. package/dist/src/bytes.d.ts +1 -5
  36. package/dist/src/bytes.js +33 -40
  37. package/dist/src/config.d.ts +99 -102
  38. package/dist/src/config.js +78 -83
  39. package/dist/src/derive/src/deduction.d.ts +60 -62
  40. package/dist/src/derive/src/deduction.js +103 -109
  41. package/dist/src/derive/src/index.d.ts +1 -7
  42. package/dist/src/derive/src/priority-queue.d.ts +8 -8
  43. package/dist/src/derive/src/priority-queue.js +57 -61
  44. package/dist/src/derive/src/rewrite.d.ts +15 -18
  45. package/dist/src/derive/src/rewrite.js +59 -67
  46. package/dist/src/derive/src/trie.d.ts +56 -56
  47. package/dist/src/derive/src/trie.js +175 -179
  48. package/dist/src/derive/test/derive.test.js +96 -100
  49. package/dist/src/extension.d.ts +13 -16
  50. package/dist/src/geometry.d.ts +29 -43
  51. package/dist/src/geometry.js +324 -268
  52. package/dist/src/index.d.ts +2 -17
  53. package/dist/src/index.js +2 -12
  54. package/dist/src/ingest-cache.d.ts +25 -30
  55. package/dist/src/ingest-cache.js +108 -127
  56. package/dist/src/mind/articulation.d.ts +1 -5
  57. package/dist/src/mind/articulation.js +77 -112
  58. package/dist/src/mind/attention.d.ts +40 -108
  59. package/dist/src/mind/attention.js +754 -872
  60. package/dist/src/mind/canonical.d.ts +4 -19
  61. package/dist/src/mind/canonical.js +27 -31
  62. package/dist/src/mind/graph-search.d.ts +201 -225
  63. package/dist/src/mind/graph-search.js +742 -821
  64. package/dist/src/mind/index.d.ts +2 -10
  65. package/dist/src/mind/junction.d.ts +31 -58
  66. package/dist/src/mind/junction.js +172 -237
  67. package/dist/src/mind/learning.d.ts +16 -52
  68. package/dist/src/mind/learning.js +143 -165
  69. package/dist/src/mind/match.d.ts +20 -69
  70. package/dist/src/mind/match.js +259 -318
  71. package/dist/src/mind/mechanisms/alu.js +16 -16
  72. package/dist/src/mind/mechanisms/cast.d.ts +9 -13
  73. package/dist/src/mind/mechanisms/cast.js +363 -456
  74. package/dist/src/mind/mechanisms/confluence.d.ts +8 -12
  75. package/dist/src/mind/mechanisms/confluence.js +152 -183
  76. package/dist/src/mind/mechanisms/cover.d.ts +2 -8
  77. package/dist/src/mind/mechanisms/cover.js +148 -210
  78. package/dist/src/mind/mechanisms/extraction.d.ts +8 -34
  79. package/dist/src/mind/mechanisms/extraction.js +234 -288
  80. package/dist/src/mind/mechanisms/recall.d.ts +6 -10
  81. package/dist/src/mind/mechanisms/recall.js +141 -185
  82. package/dist/src/mind/mind.d.ts +194 -155
  83. package/dist/src/mind/mind.js +392 -289
  84. package/dist/src/mind/pipeline-mechanism.d.ts +112 -124
  85. package/dist/src/mind/pipeline-mechanism.js +161 -172
  86. package/dist/src/mind/pipeline.d.ts +4 -14
  87. package/dist/src/mind/pipeline.js +125 -189
  88. package/dist/src/mind/primitives.d.ts +20 -33
  89. package/dist/src/mind/primitives.js +124 -120
  90. package/dist/src/mind/rationale.d.ts +86 -98
  91. package/dist/src/mind/rationale.js +113 -121
  92. package/dist/src/mind/reasoning.d.ts +2 -13
  93. package/dist/src/mind/reasoning.js +129 -166
  94. package/dist/src/mind/recognition.d.ts +1 -4
  95. package/dist/src/mind/recognition.js +198 -210
  96. package/dist/src/mind/resonance.d.ts +5 -22
  97. package/dist/src/mind/resonance.js +0 -0
  98. package/dist/src/mind/trace.d.ts +6 -25
  99. package/dist/src/mind/trace.js +50 -58
  100. package/dist/src/mind/traverse.d.ts +29 -58
  101. package/dist/src/mind/traverse.js +344 -356
  102. package/dist/src/mind/types.d.ts +122 -127
  103. package/dist/src/mind/types.js +60 -69
  104. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  105. package/dist/src/rabitq-ivf/src/database.js +201 -0
  106. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  107. package/dist/src/{rabitq-hnsw → rabitq-ivf}/src/index.js +1 -3
  108. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  109. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  110. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  111. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  112. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  113. package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
  114. package/dist/src/sema.d.ts +9 -13
  115. package/dist/src/sema.js +26 -40
  116. package/dist/src/store-sqlite.d.ts +171 -166
  117. package/dist/src/store-sqlite.js +768 -662
  118. package/dist/src/store.d.ts +597 -630
  119. package/dist/src/store.js +1434 -1581
  120. package/dist/src/vec.d.ts +5 -9
  121. package/dist/src/vec.js +61 -73
  122. package/example/train_base.ts +33 -15
  123. package/index.html +65 -0
  124. package/package.json +1 -1
  125. package/src/alu/README.md +1 -1
  126. package/src/alu/src/index.ts +1 -1
  127. package/src/config.ts +19 -27
  128. package/src/geometry.ts +93 -1
  129. package/src/index.ts +6 -11
  130. package/src/mind/attention.ts +11 -6
  131. package/src/mind/mechanisms/recall.ts +29 -13
  132. package/src/mind/mind.ts +250 -19
  133. package/src/mind/primitives.ts +43 -6
  134. package/src/mind/recognition.ts +26 -8
  135. package/src/mind/traverse.ts +47 -0
  136. package/src/mind/types.ts +20 -21
  137. package/src/rabitq-ivf/README.md +56 -0
  138. package/src/rabitq-ivf/src/database.ts +276 -0
  139. package/src/{rabitq-hnsw → rabitq-ivf}/src/index.ts +2 -5
  140. package/src/rabitq-ivf/src/ivf.ts +1330 -0
  141. package/src/{rabitq-hnsw → rabitq-ivf}/src/prng.ts +1 -1
  142. package/src/store-sqlite.ts +204 -9
  143. package/src/store.ts +27 -34
  144. package/test/08-storage.test.mjs +3 -3
  145. package/test/13-conversation.test.mjs +190 -20
  146. package/test/14-scaling.test.mjs +2 -2
  147. package/test/35-ivf.test.mjs +263 -0
  148. package/test/36-bloom.test.mjs +123 -0
  149. package/dist/src/rabitq-hnsw/src/database.d.ts +0 -202
  150. package/dist/src/rabitq-hnsw/src/database.js +0 -405
  151. package/dist/src/rabitq-hnsw/src/heap.d.ts +0 -22
  152. package/dist/src/rabitq-hnsw/src/heap.js +0 -94
  153. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +0 -125
  154. package/dist/src/rabitq-hnsw/src/hnsw.js +0 -500
  155. package/dist/src/rabitq-hnsw/src/index.d.ts +0 -15
  156. package/dist/src/rabitq-hnsw/src/prng.d.ts +0 -19
  157. package/dist/src/rabitq-hnsw/src/prng.js +0 -38
  158. package/dist/src/rabitq-hnsw/src/rabitq.d.ts +0 -95
  159. package/dist/src/rabitq-hnsw/src/rabitq.js +0 -299
  160. package/dist/src/rabitq-hnsw/src/store.d.ts +0 -162
  161. package/dist/src/rabitq-hnsw/src/store.js +0 -916
  162. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +0 -1
  163. package/dist/src/rabitq-hnsw/test/hnsw.test.js +0 -1197
  164. package/src/rabitq-hnsw/README.md +0 -303
  165. package/src/rabitq-hnsw/src/database.ts +0 -492
  166. package/src/rabitq-hnsw/src/heap.ts +0 -90
  167. package/src/rabitq-hnsw/src/hnsw.ts +0 -514
  168. package/src/rabitq-hnsw/src/store.ts +0 -994
  169. package/src/rabitq-hnsw/test/hnsw.test.ts +0 -1213
  170. /package/src/{rabitq-hnsw → rabitq-ivf}/src/rabitq.ts +0 -0
@@ -4,22 +4,22 @@ import { type StoreConfig } from "./config.js";
4
4
  export type NodeId = number;
5
5
  /** A node in the graph. Exactly one of `leaf` / `kids` is set. */
6
6
  export interface NodeRec {
7
- id: NodeId;
8
- leaf: Uint8Array | null;
9
- kids: NodeId[] | null;
7
+ id: NodeId;
8
+ leaf: Uint8Array | null;
9
+ kids: NodeId[] | null;
10
10
  }
11
11
  /** A soft-resonance hit. */
12
12
  export interface Hit {
13
- id: NodeId;
14
- /** Estimated cosine (1 − RaBitQ estimated distance) — an APPROXIMATION,
15
- * never an exact rerank. Identity decisions must use content-addressed
16
- * resolve(), never `score >= 1`. */
17
- score: number;
18
- /** This node's halo, when a producer chooses to attach it. NOT populated
19
- * by {@link Store.resonate}/{@link Store.resonateHalo}: no consumer read
20
- * it there, and eagerly fetching k halo rows per query was pure waste.
21
- * Call {@link Store.halo} on the hits that actually need it. */
22
- halo?: Vec;
13
+ id: NodeId;
14
+ /** Estimated cosine (1 − RaBitQ estimated distance) — an APPROXIMATION,
15
+ * never an exact rerank. Identity decisions must use content-addressed
16
+ * resolve(), never `score >= 1`. */
17
+ score: number;
18
+ /** This node's halo, when a producer chooses to attach it. NOT populated
19
+ * by {@link Store.resonate}/{@link Store.resonateHalo}: no consumer read
20
+ * it there, and eagerly fetching k halo rows per query was pure waste.
21
+ * Call {@link Store.halo} on the hits that actually need it. */
22
+ halo?: Vec;
23
23
  }
24
24
  /** Eviction strategy for a {@link BoundedMap}.
25
25
  *
@@ -37,224 +37,221 @@ export type Evict = "lru" | "smallest";
37
37
  * for uniform caches). A miss only costs a little extra work later, never
38
38
  * correctness. */
39
39
  export declare class BoundedMap<K, V> {
40
- readonly maxBytes: number;
41
- private readonly sizeOf;
42
- private readonly evict;
43
- private m;
44
- private _bytes;
45
- private _cursor;
46
- private _candidates;
47
- constructor(maxBytes: number, sizeOf?: (v: V) => number, evict?: Evict);
48
- /** Next key in insertion (≈ LRU) order, resuming where the last call left
49
- * off; wraps to the front when exhausted. Undefined only when empty. */
50
- private nextOldest;
51
- get(k: K): V | undefined;
52
- /** Membership without touching LRU order — a pure peek, for callers that only
53
- * need "is this key present?" and must not promote it to most-recent. */
54
- has(k: K): boolean;
55
- set(k: K, v: V): void;
56
- get size(): number;
57
- get bytes(): number;
58
- /** Remove one entry (point invalidation), with byte accounting. */
59
- delete(k: K): void;
60
- /** Drop every entry (bulk invalidation) — O(1) amortised via fresh maps. */
61
- clear(): void;
40
+ readonly maxBytes: number;
41
+ private readonly sizeOf;
42
+ private readonly evict;
43
+ private m;
44
+ private _bytes;
45
+ private _cursor;
46
+ private _candidates;
47
+ constructor(maxBytes: number, sizeOf?: (v: V) => number, evict?: Evict);
48
+ /** Next key in insertion (≈ LRU) order, resuming where the last call left
49
+ * off; wraps to the front when exhausted. Undefined only when empty. */
50
+ private nextOldest;
51
+ get(k: K): V | undefined;
52
+ /** Membership without touching LRU order — a pure peek, for callers that only
53
+ * need "is this key present?" and must not promote it to most-recent. */
54
+ has(k: K): boolean;
55
+ set(k: K, v: V): void;
56
+ get size(): number;
57
+ get bytes(): number;
58
+ /** Remove one entry (point invalidation), with byte accounting. */
59
+ delete(k: K): void;
60
+ /** Drop every entry (bulk invalidation) — O(1) amortised via fresh maps. */
61
+ clear(): void;
62
62
  }
63
63
  export interface Store {
64
- readonly D: number;
65
- /** Insert a leaf, returning its content id. Idempotent. */
66
- putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
67
- /** Insert a branch over child ids, returning its content id. Idempotent. */
68
- putBranch(kids: NodeId[], gist: Vec): Promise<NodeId>;
69
- /** Whether a node with this id already exists. O(1). */
70
- has(id: NodeId): boolean;
71
- /** The node record, or null. */
72
- get(id: NodeId): NodeRec | null;
73
- /** The bytes a node spans, reconstructed by traversal (cached). */
74
- bytes(id: NodeId): Uint8Array;
75
- /** First `maxLen` bytes of a node, stopping early — far cheaper than
76
- * `bytes()` for large branches when only a prefix is needed. */
77
- bytesPrefix(id: NodeId, maxLen: number): Uint8Array;
78
- /** The CONTENT LENGTH of a node in bytes — under the linear river fold
79
- * this IS the node's gist magnitude, squared: seat permutations
80
- * decorrelate siblings, so ‖gist‖ = √(content bytes) up to decorrelation
81
- * noise. The magnitude is therefore never persisted beside the vector
82
- * (that would duplicate what the content already determines; the ANN
83
- * index keeps unit directions), it is READ here in O(1) amortized — the
84
- * store-side half of angle+magnitude semantics. Mechanisms use it to
85
- * convert a scale-free cosine into the absolute quantities the linear
86
- * geometry carries: shared bytes ≈ cos·√(lenA·lenB), fraction of A
87
- * explained ≈ cos·√(lenB/lenA). `cap`, when given, SATURATES the walk:
88
- * the return value is exact below the cap and merely ≥ cap otherwise —
89
- * for decisions that stop caring past a bound (a fraction that caps at 1,
90
- * a weight that is sub-noise beyond a ratio), so one huge conversation
91
- * root never costs a full subtree walk. Only complete walks are
92
- * memoized. */
93
- contentLen(id: NodeId, cap?: number): number;
94
- findLeaf(bytes: Uint8Array): NodeId | null;
95
- findBranch(kids: NodeId[]): NodeId | null;
96
- /** The branch nodes that list `id` among their children — the reverse of
97
- * `get(id).kids`. Lets the structural DAG be climbed upward, from a
98
- * recognised fragment to the larger learned forms that contain it. */
99
- parents(id: NodeId): NodeId[];
100
- /** The first `limit` structural parents of `id` — the CAPPED read for the
101
- * climb. A heavily shared subtree's parent set grows with the corpus;
102
- * reading `bound + 1` decides "hub or not" exactly (a result of length
103
- * bound + 1 means MORE than bound) while the read stays bounded by the
104
- * bound, never by the fan-in. */
105
- parentsFirst(id: NodeId, limit: number): NodeId[];
106
- /** Whether `id` has ANY structural parent — one LIMITed point probe. */
107
- hasParents(id: NodeId): boolean;
108
- /** The TRANSPARENT CHAIN from `id` upward: `run[0] === id`, and while the
109
- * current node is transparent — no continuation edge in or out and exactly
110
- * ONE structural parent — the run steps to that parent. The last element
111
- * is the first NON-transparent ancestor (or a parentless top). Transparent
112
- * nodes are invisible to the edge climb (they contribute no roots, no
113
- * contexts and no lateral branching), so a climber may hop a whole run in
114
- * one read where a node-at-a-time ascent pays three probes per node — the
115
- * dominant cost of climbing deep single-structure scaffolding. Results
116
- * are cached for the store's LIFETIME (reads are pure between writes) and
117
- * the cache is dropped whenever a write could break transparency: a node
118
- * gaining a structural parent (fresh mint) or a continuation edge (link).
119
- * The run may be truncated at an internal safety depth; a truncated run's
120
- * last element is then still transparent, and a climber that treats the
121
- * terminal generically simply continues from it — semantics never depend
122
- * on completeness. */
123
- chainRun(id: NodeId): readonly NodeId[];
124
- /** Record a CONTAINMENT edge: `parent`'s byte content contains `child`'s,
125
- * even though `child` is not among `parent`'s kids (a sub-span flat branch
126
- * inside a leaf-parent chunk). Kept apart from {@link parents} — the
127
- * structural climb is untouched; {@link containers} serves the climbing
128
- * surface of orphan flat branches durably, where a session-local side
129
- * table would be lost on restart or an ingest-cache replay. Idempotent. */
130
- addContainer(child: NodeId, parent: NodeId): void;
131
- /** The chunks recorded by {@link addContainer} as containing `child`. */
132
- containers(child: NodeId): NodeId[];
133
- /** Whether `child` has ANY containment parent — an EXISTS probe that never
134
- * unpacks the (occurrence-proportional) packed parents blob. */
135
- hasContainers(child: NodeId): boolean;
136
- /** A PAGE of containment parents — `limit` entries starting at `offset`
137
- * (the buffered adds follow the stored ones), so a consumer can STREAM a
138
- * common window's corpus-sized parent list and stop the moment its
139
- * question is decided, with per-page work bounded by `limit`. */
140
- containersSlice(child: NodeId, offset: number, limit: number): NodeId[];
141
- nodeCount(): number;
142
- /** The k nodes whose gist resonates most with v. */
143
- resonate(v: Vec, k: number): Promise<Hit[]>;
144
- /** Mark a node as a RESONANCE TARGET — promote its gist into the content
145
- * index so {@link resonate} can find it. A node's gist is captured at intern
146
- * but indexed LAZILY (only targets are indexed; the ~99.5% intermediate DAG
147
- * is not — that is the store's compression). A node becomes a target
148
- * implicitly via {@link link}/{@link pourHalo}; this is the EXPLICIT hook for
149
- * the one target those do not cover — a DEPOSIT ROOT, the node `express`,
150
- * `bridge`, and whole-query recall resonate the input to. Idempotent; a
151
- * no-op once a node is indexed or if its captured gist has been evicted. */
152
- indexTarget(id: NodeId): void;
153
- /** Remove content-index entries for nodes that are structurally isolated
154
- * (fewer than `minParents` structural parents) and are not roots of any
155
- * experience (no edges, no halo). These nodes are unique to one tree and
156
- * bridge nothing between experiences — their index slots are wasted.
157
- *
158
- * This is a POST-HOC batch operation, not on the training hot path. Run it
159
- * at checkpoints or after training to reclaim index space. The underlying
160
- * vector DB is compacted after deletion to physically free the space.
161
- *
162
- * INCREMENTAL: keep decisions are monotone (parents, edges and halos only
163
- * ever grow), so entries examined by a previous pass are settled; each
164
- * pass scans only entries indexed since the last one (a durable watermark
165
- * over the index's monotone internal ids), making the checkpoint-cadence
166
- * call cheap on a large trained store.
167
- *
168
- * @param minParents keep only nodes with ≥ this many structural parents
169
- * (default 2 — keep nodes that bridge ≥2 experiences)
170
- * @returns number of entries removed */
171
- compactContentIndex(minParents?: number): Promise<number>;
172
- /** Re-index structurally-important nodes whose gists were evicted from the
173
- * pending cache before they could be indexed — the inverse of {@link
174
- * compactContentIndex}. Walks the edge/halo-bearing id set (a repairable
175
- * node must carry edges or a halo, so that set IS the candidate set —
176
- * corpus-of-experiences-sized, not node-count-sized); for each candidate
177
- * that (a) has ≥ `minParents` structural parents and (b) is NOT already
178
- * in the content index, regenerates its gist via `regenerateGist` and adds
179
- * it. Intended as a post-training batch operation, not on the hot path.
180
- *
181
- * `regenerateGist` receives a node id and must return its perceived gist
182
- * vector (or `null` to skip). The store has no access to the
183
- * {@link Space}/{@link Alphabet} needed for perception — the caller (the
184
- * {@link Mind}) wires those in.
185
- *
186
- * @param regenerateGist async callback that regenerates a node's gist
187
- * @param minParents only repair nodes with ≥ this many parents
188
- * (default 2 — structural bridges)
189
- * @returns number of nodes added to the index */
190
- repairContentIndex(
191
- regenerateGist: (id: NodeId) => Promise<Vec | null>,
192
- minParents?: number,
193
- ): Promise<number>;
194
- /** Learn that `to` follows `from`. Idempotent. */
195
- link(from: NodeId, to: NodeId): Promise<void>;
196
- /** What follows `id` (in insertion order). */
197
- next(id: NodeId): NodeId[];
198
- /** Whether ANY edge leaves `id` the EXISTENCE probe for the forward
199
- * relation. Decision points that only ask "does this lead anywhere?"
200
- * must use this instead of materialising {@link next} (a context's edge
201
- * list is a range read; a hub's is large) — the same
202
- * count-instead-of-materialise principle {@link prevCount} serves for the
203
- * reverse relation. One indexed point probe. */
204
- hasNext(id: NodeId): boolean;
205
- /** What `id` follows (for reverse recall). */
206
- prev(id: NodeId): NodeId[];
207
- /** The first `limit` continuations of `id`, in the SAME order {@link next}
208
- * returns (insertion order) the CAPPED read for consumers bounded by the
209
- * hub convention. A LIMITed statement, never a full materialisation: no
210
- * read through this may grow with the corpus. */
211
- nextFirst(id: NodeId, limit: number): NodeId[];
212
- /** The first `limit` predecessors of `id`, in the SAME order {@link prev}
213
- * returns (NEWEST-first prev is seq-descending; see the adapter) — the
214
- * capped read for reverse fan-ins, which are corpus-sized on a common
215
- * continuation. */
216
- prevFirst(id: NodeId, limit: number): NodeId[];
217
- /** How many nodes `id` follows the reverse-edge SUPPORT count. Decision
218
- * points that only weigh evidence (chooseNext) must use this instead of
219
- * materialising {@link prev}, whose list is corpus-sized for a common
220
- * continuation ("Yes."). One indexed COUNT, never a row materialisation. */
221
- prevCount(id: NodeId): number;
222
- /** How many DISTINCT nodes bear a continuation edge — the number of learnt
223
- * contexts (the document count for inverse-document-frequency weighting in
224
- * the consensus climb). O(1) from an index; a coarse count is fine. */
225
- edgeSourceCount(): number;
226
- /** Pour a partner's signature into a node's halo. */
227
- pourHalo(id: NodeId, add: Vec): Promise<void>;
228
- /** Nodes whose halo resonates with `v` (the concept's siblings). */
229
- resonateHalo(v: Vec, k: number): Promise<Hit[]>;
230
- /** A node's halo, or null if it has none / too little mass. */
231
- halo(id: NodeId): Vec | null;
232
- /** Whether {@link halo} would return non-null the EXISTENCE probe.
233
- * {@link halo} decodes and normalizes the full D-element quantized row on
234
- * every call; existence-only consumers (recognition's admission predicate,
235
- * the search's fuse guard) must ask this instead: one row read, a mass
236
- * compare, no decode. */
237
- hasHalo(id: NodeId): boolean;
238
- /** How many episode signatures were poured into `id`'s halo — the DIRECT
239
- * measure of distributional evidence (each training pair pours once, so
240
- * repetition counts, unlike {@link prevCount}, which counts DISTINCT
241
- * contexts). Consulted at disambiguation decision points as the tie-break
242
- * behind distinct-context support: diversity of evidence outranks sheer
243
- * repetition, but repetition outranks insertion-order accident. 0 when
244
- * the node has no halo row. */
245
- haloMass(id: NodeId): number;
246
- size(): Promise<number>;
247
- saveSnapshot(bytes: Uint8Array): Promise<void>;
248
- loadSnapshot(): Promise<Uint8Array | null>;
249
- /** Free-form provenance metadata (e.g. training dataset name). */
250
- setMeta(key: string, val: string): Promise<void>;
251
- getMeta(key: string): Promise<string | null>;
252
- deleteMeta(key: string): Promise<void>;
253
- /** Commit any deferred writes — makes metadata, edges, and node writes
254
- * durable immediately. Safe to call frequently; a no-op if nothing is
255
- * pending. */
256
- commit(): void;
257
- close(): Promise<void>;
64
+ readonly D: number;
65
+ /** Insert a leaf, returning its content id. Idempotent. */
66
+ putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
67
+ /** Insert a branch over child ids, returning its content id. Idempotent. */
68
+ putBranch(kids: NodeId[], gist: Vec): Promise<NodeId>;
69
+ /** Whether a node with this id already exists. O(1). */
70
+ has(id: NodeId): boolean;
71
+ /** The node record, or null. */
72
+ get(id: NodeId): NodeRec | null;
73
+ /** The bytes a node spans, reconstructed by traversal (cached). */
74
+ bytes(id: NodeId): Uint8Array;
75
+ /** First `maxLen` bytes of a node, stopping early — far cheaper than
76
+ * `bytes()` for large branches when only a prefix is needed. */
77
+ bytesPrefix(id: NodeId, maxLen: number): Uint8Array;
78
+ /** The CONTENT LENGTH of a node in bytes — under the linear river fold
79
+ * this IS the node's gist magnitude, squared: seat permutations
80
+ * decorrelate siblings, so ‖gist‖ = √(content bytes) up to decorrelation
81
+ * noise. The magnitude is therefore never persisted beside the vector
82
+ * (that would duplicate what the content already determines; the ANN
83
+ * index keeps unit directions), it is READ here in O(1) amortized — the
84
+ * store-side half of angle+magnitude semantics. Mechanisms use it to
85
+ * convert a scale-free cosine into the absolute quantities the linear
86
+ * geometry carries: shared bytes ≈ cos·√(lenA·lenB), fraction of A
87
+ * explained ≈ cos·√(lenB/lenA). `cap`, when given, SATURATES the walk:
88
+ * the return value is exact below the cap and merely ≥ cap otherwise —
89
+ * for decisions that stop caring past a bound (a fraction that caps at 1,
90
+ * a weight that is sub-noise beyond a ratio), so one huge conversation
91
+ * root never costs a full subtree walk. Only complete walks are
92
+ * memoized. */
93
+ contentLen(id: NodeId, cap?: number): number;
94
+ findLeaf(bytes: Uint8Array): NodeId | null;
95
+ findBranch(kids: NodeId[]): NodeId | null;
96
+ /** The branch nodes that list `id` among their children — the reverse of
97
+ * `get(id).kids`. Lets the structural DAG be climbed upward, from a
98
+ * recognised fragment to the larger learned forms that contain it. */
99
+ parents(id: NodeId): NodeId[];
100
+ /** The first `limit` structural parents of `id` — the CAPPED read for the
101
+ * climb. A heavily shared subtree's parent set grows with the corpus;
102
+ * reading `bound + 1` decides "hub or not" exactly (a result of length
103
+ * bound + 1 means MORE than bound) while the read stays bounded by the
104
+ * bound, never by the fan-in. */
105
+ parentsFirst(id: NodeId, limit: number): NodeId[];
106
+ /** Whether `id` has ANY structural parent — one LIMITed point probe. */
107
+ hasParents(id: NodeId): boolean;
108
+ /** The TRANSPARENT CHAIN from `id` upward: `run[0] === id`, and while the
109
+ * current node is transparent — no continuation edge in or out and exactly
110
+ * ONE structural parent — the run steps to that parent. The last element
111
+ * is the first NON-transparent ancestor (or a parentless top). Transparent
112
+ * nodes are invisible to the edge climb (they contribute no roots, no
113
+ * contexts and no lateral branching), so a climber may hop a whole run in
114
+ * one read where a node-at-a-time ascent pays three probes per node — the
115
+ * dominant cost of climbing deep single-structure scaffolding. Results
116
+ * are cached for the store's LIFETIME (reads are pure between writes) and
117
+ * the cache is dropped whenever a write could break transparency: a node
118
+ * gaining a structural parent (fresh mint) or a continuation edge (link).
119
+ * The run may be truncated at an internal safety depth; a truncated run's
120
+ * last element is then still transparent, and a climber that treats the
121
+ * terminal generically simply continues from it — semantics never depend
122
+ * on completeness. */
123
+ chainRun(id: NodeId): readonly NodeId[];
124
+ /** Record a CONTAINMENT edge: `parent`'s byte content contains `child`'s,
125
+ * even though `child` is not among `parent`'s kids (a sub-span flat branch
126
+ * inside a leaf-parent chunk). Kept apart from {@link parents} — the
127
+ * structural climb is untouched; {@link containers} serves the climbing
128
+ * surface of orphan flat branches durably, where a session-local side
129
+ * table would be lost on restart or an ingest-cache replay. Idempotent. */
130
+ addContainer(child: NodeId, parent: NodeId): void;
131
+ /** The chunks recorded by {@link addContainer} as containing `child`. */
132
+ containers(child: NodeId): NodeId[];
133
+ /** Whether `child` has ANY containment parent — an EXISTS probe that never
134
+ * unpacks the (occurrence-proportional) packed parents blob. */
135
+ hasContainers(child: NodeId): boolean;
136
+ /** A PAGE of containment parents — `limit` entries starting at `offset`
137
+ * (the buffered adds follow the stored ones), so a consumer can STREAM a
138
+ * common window's corpus-sized parent list and stop the moment its
139
+ * question is decided, with per-page work bounded by `limit`. */
140
+ containersSlice(child: NodeId, offset: number, limit: number): NodeId[];
141
+ nodeCount(): number;
142
+ /** The k nodes whose gist resonates most with v. */
143
+ resonate(v: Vec, k: number): Promise<Hit[]>;
144
+ /** Mark a node as a RESONANCE TARGET — promote its gist into the content
145
+ * index so {@link resonate} can find it. A node's gist is captured at intern
146
+ * but indexed LAZILY (only targets are indexed; the ~99.5% intermediate DAG
147
+ * is not — that is the store's compression). A node becomes a target
148
+ * implicitly via {@link link}/{@link pourHalo}; this is the EXPLICIT hook for
149
+ * the one target those do not cover — a DEPOSIT ROOT, the node `express`,
150
+ * `bridge`, and whole-query recall resonate the input to. Idempotent; a
151
+ * no-op once a node is indexed or if its captured gist has been evicted. */
152
+ indexTarget(id: NodeId): void;
153
+ /** Remove content-index entries for nodes that are structurally isolated
154
+ * (fewer than `minParents` structural parents) and are not roots of any
155
+ * experience (no edges, no halo). These nodes are unique to one tree and
156
+ * bridge nothing between experiences — their index slots are wasted.
157
+ *
158
+ * This is a POST-HOC batch operation, not on the training hot path. Run it
159
+ * at checkpoints or after training to reclaim index space. The underlying
160
+ * vector DB is compacted after deletion to physically free the space.
161
+ *
162
+ * INCREMENTAL: keep decisions are monotone (parents, edges and halos only
163
+ * ever grow), so entries examined by a previous pass are settled; each
164
+ * pass scans only entries indexed since the last one (a durable watermark
165
+ * over the index's monotone internal ids), making the checkpoint-cadence
166
+ * call cheap on a large trained store.
167
+ *
168
+ * @param minParents keep only nodes with ≥ this many structural parents
169
+ * (default 2 — keep nodes that bridge ≥2 experiences)
170
+ * @returns number of entries removed */
171
+ compactContentIndex(minParents?: number): Promise<number>;
172
+ /** Re-index structurally-important nodes whose gists were evicted from the
173
+ * pending cache before they could be indexed — the inverse of {@link
174
+ * compactContentIndex}. Walks the edge/halo-bearing id set (a repairable
175
+ * node must carry edges or a halo, so that set IS the candidate set —
176
+ * corpus-of-experiences-sized, not node-count-sized); for each candidate
177
+ * that (a) has ≥ `minParents` structural parents and (b) is NOT already
178
+ * in the content index, regenerates its gist via `regenerateGist` and adds
179
+ * it. Intended as a post-training batch operation, not on the hot path.
180
+ *
181
+ * `regenerateGist` receives a node id and must return its perceived gist
182
+ * vector (or `null` to skip). The store has no access to the
183
+ * {@link Space}/{@link Alphabet} needed for perception — the caller (the
184
+ * {@link Mind}) wires those in.
185
+ *
186
+ * @param regenerateGist async callback that regenerates a node's gist
187
+ * @param minParents only repair nodes with ≥ this many parents
188
+ * (default 2 — structural bridges)
189
+ * @returns number of nodes added to the index */
190
+ repairContentIndex(regenerateGist: (id: NodeId) => Promise<Vec | null>, minParents?: number): Promise<number>;
191
+ /** Learn that `to` follows `from`. Idempotent. */
192
+ link(from: NodeId, to: NodeId): Promise<void>;
193
+ /** What follows `id` (in insertion order). */
194
+ next(id: NodeId): NodeId[];
195
+ /** Whether ANY edge leaves `id` — the EXISTENCE probe for the forward
196
+ * relation. Decision points that only ask "does this lead anywhere?"
197
+ * must use this instead of materialising {@link next} (a context's edge
198
+ * list is a range read; a hub's is large) the same
199
+ * count-instead-of-materialise principle {@link prevCount} serves for the
200
+ * reverse relation. One indexed point probe. */
201
+ hasNext(id: NodeId): boolean;
202
+ /** What `id` follows (for reverse recall). */
203
+ prev(id: NodeId): NodeId[];
204
+ /** The first `limit` continuations of `id`, in the SAME order {@link next}
205
+ * returns (insertion order) the CAPPED read for consumers bounded by the
206
+ * hub convention. A LIMITed statement, never a full materialisation: no
207
+ * read through this may grow with the corpus. */
208
+ nextFirst(id: NodeId, limit: number): NodeId[];
209
+ /** The first `limit` predecessors of `id`, in the SAME order {@link prev}
210
+ * returns (NEWEST-first prev is seq-descending; see the adapter) — the
211
+ * capped read for reverse fan-ins, which are corpus-sized on a common
212
+ * continuation. */
213
+ prevFirst(id: NodeId, limit: number): NodeId[];
214
+ /** How many nodes `id` follows the reverse-edge SUPPORT count. Decision
215
+ * points that only weigh evidence (chooseNext) must use this instead of
216
+ * materialising {@link prev}, whose list is corpus-sized for a common
217
+ * continuation ("Yes."). One indexed COUNT, never a row materialisation. */
218
+ prevCount(id: NodeId): number;
219
+ /** How many DISTINCT nodes bear a continuation edge — the number of learnt
220
+ * contexts (the document count for inverse-document-frequency weighting in
221
+ * the consensus climb). O(1) from an index; a coarse count is fine. */
222
+ edgeSourceCount(): number;
223
+ /** Pour a partner's signature into a node's halo. */
224
+ pourHalo(id: NodeId, add: Vec): Promise<void>;
225
+ /** Nodes whose halo resonates with `v` (the concept's siblings). */
226
+ resonateHalo(v: Vec, k: number): Promise<Hit[]>;
227
+ /** A node's halo, or null if it has none / too little mass. */
228
+ halo(id: NodeId): Vec | null;
229
+ /** Whether {@link halo} would return non-null — the EXISTENCE probe.
230
+ * {@link halo} decodes and normalizes the full D-element quantized row on
231
+ * every call; existence-only consumers (recognition's admission predicate,
232
+ * the search's fuse guard) must ask this instead: one row read, a mass
233
+ * compare, no decode. */
234
+ hasHalo(id: NodeId): boolean;
235
+ /** How many episode signatures were poured into `id`'s halo the DIRECT
236
+ * measure of distributional evidence (each training pair pours once, so
237
+ * repetition counts, unlike {@link prevCount}, which counts DISTINCT
238
+ * contexts). Consulted at disambiguation decision points as the tie-break
239
+ * behind distinct-context support: diversity of evidence outranks sheer
240
+ * repetition, but repetition outranks insertion-order accident. 0 when
241
+ * the node has no halo row. */
242
+ haloMass(id: NodeId): number;
243
+ size(): Promise<number>;
244
+ saveSnapshot(bytes: Uint8Array): Promise<void>;
245
+ loadSnapshot(): Promise<Uint8Array | null>;
246
+ /** Free-form provenance metadata (e.g. training dataset name). */
247
+ setMeta(key: string, val: string): Promise<void>;
248
+ getMeta(key: string): Promise<string | null>;
249
+ deleteMeta(key: string): Promise<void>;
250
+ /** Commit any deferred writes — makes metadata, edges, and node writes
251
+ * durable immediately. Safe to call frequently; a no-op if nothing is
252
+ * pending. */
253
+ commit(): void;
254
+ close(): Promise<void>;
258
255
  }
259
256
  /** The implicit kid list of a flat branch — the inverse of
260
257
  * {@link flatKidsBytes}. */
@@ -271,405 +268,375 @@ export declare function unpackKids(blob: Uint8Array): NodeId[];
271
268
  * protected abstract methods that talk to the actual storage backend.
272
269
  */
273
270
  export declare abstract class AbstractStore implements Store {
274
- /** Open the storage backend. Must set `this._nextId`, `this._D`, and
275
- * `this._maxGroup` from the stored state (or defaults for a fresh store). */
276
- protected abstract _dbOpen(): Promise<void>;
277
- /** Close the storage backend and release all resources. */
278
- protected abstract _dbClose(): void;
279
- protected abstract _dbBeginTx(): void;
280
- protected abstract _dbCommitTx(): void;
281
- protected abstract _dbInsertNode(
282
- id: NodeId,
283
- leaf: Uint8Array | null,
284
- kids: Uint8Array | null,
285
- h: number,
286
- ): void;
287
- protected abstract _dbGetNode(id: NodeId): NodeRec | null;
288
- protected abstract _dbFindLeaf(h: number, bytes: Uint8Array): NodeId | null;
289
- protected abstract _dbFindBranchByLeaf(
290
- h: number,
291
- bytes: Uint8Array,
292
- ): NodeId | null;
293
- protected abstract _dbFindBranchByKids(
294
- h: number,
295
- packed: Uint8Array,
296
- ): NodeId | null;
297
- protected abstract _dbInsertKid(child: NodeId, parent: NodeId): void;
298
- protected abstract _dbGetParents(id: NodeId): NodeId[];
299
- /** LIMITed variant of the parent read same statement, `LIMIT ?`. Must
300
- * NOT be implemented by materialising and slicing. */
301
- protected abstract _dbGetParentsFirst(id: NodeId, limit: number): NodeId[];
302
- protected abstract _dbGetContainParents(child: NodeId): NodeId[];
303
- /** Whether a containment row exists for `child` no blob unpack. */
304
- protected abstract _dbContainExists(child: NodeId): boolean;
305
- /** A page of stored containment parents — partial blob unpack. */
306
- protected abstract _dbGetContainParentsSlice(
307
- child: NodeId,
308
- offset: number,
309
- limit: number,
310
- ): NodeId[];
311
- /** The stored containment-parent COUNT — blob length / 4, no unpack. */
312
- protected abstract _dbGetContainCount(child: NodeId): number;
313
- /** Append containment parents for `child`. MUST NOT re-write the child's
314
- * whole stored list per call — a hot child's fan-in is corpus-sized, and a
315
- * full read-modify-write per flush is quadratic over training. Durable
316
- * dedup may be DEFERRED (e.g. to a geometric merge schedule); readers
317
- * dedup, so transient duplicates only cost bytes, never correctness. */
318
- protected abstract _dbAppendContain(child: NodeId, parents: NodeId[]): void;
319
- protected abstract _dbInsertEdge(src: NodeId, dst: NodeId): void;
320
- protected abstract _dbGetNextEdges(id: NodeId): NodeId[];
321
- protected abstract _dbGetPrevEdges(id: NodeId): NodeId[];
322
- /** LIMITed variants of the two edge reads — same ORDER BY, `LIMIT ?`.
323
- * These exist so a capped consumer's cost is bounded by the cap, not by
324
- * the fan-out; an adapter must NOT implement them by materialising and
325
- * slicing. */
326
- protected abstract _dbGetNextEdgesFirst(id: NodeId, limit: number): NodeId[];
327
- protected abstract _dbGetPrevEdgesFirst(id: NodeId, limit: number): NodeId[];
328
- /** Whether ANY edge already leaves `src` — one indexed point probe, used to
329
- * maintain the distinct-source count incrementally. */
330
- protected abstract _dbEdgeSrcExists(src: NodeId): boolean;
331
- protected abstract _dbEdgeDistinctSrcCount(): number;
332
- protected abstract _dbGetHalo(id: NodeId): {
333
- vec: Uint8Array;
334
- mass: number;
335
- } | null;
336
- protected abstract _dbUpsertHalo(
337
- id: NodeId,
338
- encodedVec: Uint8Array,
339
- mass: number,
340
- ): void;
341
- protected abstract _dbGetMeta(key: string): string | null;
342
- protected abstract _dbSetMeta(key: string, val: string): void;
343
- protected abstract _dbDeleteMeta(key: string): void;
344
- protected abstract _dbSaveSnapshot(bytes: Uint8Array): void;
345
- protected abstract _dbLoadSnapshot(): Uint8Array | null;
346
- protected abstract _vecContentUpsert(
347
- entries: Array<{
348
- id: NodeId;
349
- vector: Float32Array;
350
- ef?: number;
351
- }>,
352
- ): void;
353
- protected abstract _vecContentQuery(
354
- v: Float32Array,
355
- k: number,
356
- ef: number,
357
- ): Array<{
358
- id: number;
359
- distance: number;
360
- }>;
361
- /** Whether `id` is already a live entry of the content index — one point
362
- * query, used to recognise durably-indexed nodes across sessions (the
363
- * in-memory `_indexedIds` cache starts empty on every open). */
364
- protected abstract _vecContentHas(id: NodeId): boolean;
365
- protected abstract _vecContentSize(): number;
366
- protected abstract _vecContentLastReads(): number;
367
- protected abstract _vecContentPhysicalSize(): number;
368
- protected abstract _vecContentCompact(): void;
369
- /** Live content-index entries whose INTERNAL id is > `after`, as
370
- * {ext, internal} pairs in internal-id order. Internal ids are monotone
371
- * at insert and preserved by the index's tombstone-splice compaction, so
372
- * the largest internal id a scan has seen is a durable watermark for
373
- * incremental maintenance ({@link compactContentIndex}). */
374
- protected abstract _vecContentEntriesSince(after: number): IterableIterator<{
375
- ext: NodeId;
376
- internal: number;
377
- }>;
378
- /** Every node id that carries a continuation edge (as source OR target) or
379
- * a halo row the RESONANCE-TARGET id set, sorted ascending and deduped.
380
- * This is the exact keep/repair criterion of index maintenance; driving
381
- * the maintenance loops from this (corpus-of-experiences-sized) set
382
- * instead of probing per node turned repair from an every-node walk into
383
- * a candidates-only walk. */
384
- protected abstract _dbEdgeOrHaloIds(): NodeId[];
385
- /** Remove a batch of entries from the content index by external id, under
386
- * ONE storage transaction (a tombstone per implicit transaction is one WAL
387
- * commit per id — the dominant cost of a bulk prune). Idempotent per id —
388
- * already-deleted or non-existent ids are skipped. */
389
- protected abstract _vecContentDeleteMany(ids: NodeId[]): void;
390
- protected abstract _vecHaloUpsert(
391
- entries: Array<{
392
- id: NodeId;
393
- vector: Float32Array;
394
- }>,
395
- ): void;
396
- protected abstract _vecHaloQuery(
397
- v: Float32Array,
398
- k: number,
399
- ef: number,
400
- ): Array<{
401
- id: number;
402
- distance: number;
403
- }>;
404
- /** Live (non-tombstoned) entry count of the halo index — the denominator
405
- * the tombstone-ratio compaction trigger compares physical size against. */
406
- protected abstract _vecHaloSize(): number;
407
- protected abstract _vecHaloPhysicalSize(): number;
408
- protected abstract _vecHaloCompact(): void;
409
- protected _D: number;
410
- protected _maxGroup: number;
411
- protected readonly minHaloMass: number;
412
- protected readonly efSearch: number;
413
- protected readonly m: number;
414
- protected readonly efConstruction: number;
415
- protected readonly efConstructionInterior: number;
416
- protected readonly overfetch: number;
417
- protected readonly batchSize: number;
418
- protected readonly compactEveryNWrites: number;
419
- /** Branch node ids are a dense, monotonically-increasing integer sequence
420
- * (0,1,2,…). Single-byte leaves occupy the implicit negative range −256…−1.
421
- * They are NEVER deleted, so the count of minted branch ids IS the next id —
422
- * which doubles as the branch-node count and lets has() be an O(1) check.
423
- * Set by `_dbOpen()` from the stored node count; incremented by `mintId()`. */
424
- protected _nextId: number;
425
- protected _writtenSinceCompact: number;
426
- protected closed: boolean;
427
- /** Lifecycle guard resolved once `_dbOpen()` completes. */
428
- protected _ready: Promise<void> | null;
429
- /** Exact-content dedup: content-key node id. Intrinsic compression. */
430
- protected readonly _leafKey: BoundedMap<string, NodeId>;
431
- protected readonly _branchKey: BoundedMap<string, NodeId>;
432
- /** Reconstructed-bytes read cache (regenerable), keyed by node id. */
433
- protected readonly _bytesCache: BoundedMap<NodeId, Uint8Array>;
434
- /** contentLen memo content is immutable, so entries never invalidate. */
435
- protected readonly _lenCache: BoundedMap<NodeId, number>;
436
- /** Node-record cache avoids repeated persistence queries for shared DAG
437
- * nodes. Each record is small (a few ints + short leaf buffer). */
438
- protected readonly _recCache: BoundedMap<NodeId, NodeRec>;
439
- /** Captured-but-not-yet-indexed gists. Sized in bytes (each is D·4); a deposit
440
- * links/pours a node right after interning it, so the working set is one
441
- * deposit's nodes a small budget captures ~all of it, and an eviction only
442
- * means that node is reached by the DAG climb instead of by direct
443
- * resonance. */
444
- protected _pendingGist: BoundedMap<NodeId, Vec>;
445
- /** EXACT halo accumulators for the session's live pours: full-precision in
446
- * memory, 2-bit on disk, so within-session accumulate-then-compare never
447
- * round-trips through the quantizer. Regenerable a miss reads the durable
448
- * 2-bit row. */
449
- protected _haloExact: BoundedMap<NodeId, Vec>;
450
- /** NORMALIZED halo read cache — the decoded, normalized vector {@link halo}
451
- * returns, cached by id so repeat reads skip the per-call 2-bit decode and
452
- * normalize of a full D-element row (measured on a trained store: ~15K
453
- * halo() calls per deep query over ~50 distinct ids — all but the first
454
- * per id pure re-decode). Point-invalidated by {@link pourHalo}, the one
455
- * halo mutation site. Callers receive a COPY, so the cached vector is
456
- * never aliased. Regenerable a miss re-decodes the durable row. */
457
- protected _haloNorm: BoundedMap<NodeId, Vec>;
458
- /** Interiors deliberately SKIPPED by indexSubtree (unique nodes with 1 parent
459
- * that bridge nothing). Remembered so subsequent visits prune the subtree
460
- * without re-checking parent count. LRU-bounded: an evicted entry is
461
- * re-checked on next visit — if it gained parents in the meantime, it will
462
- * be promoted to the index. */
463
- protected _coveredIds: BoundedMap<NodeId, true>;
464
- /** Live content-index id set, LRU-bounded so a massive ingest never leaks
465
- * memory; an evicted entry is still indexed (the row is durable), so the
466
- * only cost of an eviction is a duplicate HNSW probe on next visit. */
467
- protected _indexedIds: BoundedMap<NodeId, true>;
468
- /** ANN read cache for {@link resonate} — keyed by vecKey(v) + ":" + k;
469
- * lazily initialised, dropped on any index mutation. */
470
- protected _resonateCache: Map<string, Hit[]> | null;
471
- /** ANN read cache for {@link resonateHalo} same scheme. */
472
- protected _resonateHaloCache: Map<string, Hit[]> | null;
473
- /** Content (gist) index write buffer. `ef` is the per-entry HNSW
474
- * construction budget: reach-only interiors carry the reduced
475
- * `efConstructionInterior`; dedup targets omit it (full budget). */
476
- protected _contentBuffer: Array<{
477
- id: NodeId;
478
- vector: Float32Array;
479
- ef?: number;
480
- }>;
481
- /** Halo index write buffer — keyed by id so repeats within a batch coalesce. */
482
- protected _haloBuffer: Map<number, Float32Array<ArrayBufferLike>>;
483
- /** Containment write buffer: child → new parents, merged on flush cadence. */
484
- protected _containBuf: Map<number, Set<number>>;
485
- /** Dedup-target candidates still in the write buffer (keyed by id). Only
486
- * roots that have gained an edge/halo are targets; a fresh intermediate
487
- * branch is never folded onto. */
488
- protected _nearDedupBuf: Map<number, Float32Array<ArrayBufferLike>>;
489
- /** Ids currently in `_contentBuffer` (not yet flushed) O(1) membership. */
490
- protected _bufferedIds: Set<number>;
491
- /** {@link Store.chainRun} results, valid for the store's lifetime BETWEEN
492
- * writes: a chain is a pure function of the kid and edge tables, so any
493
- * write that could break a node's transparency (a fresh mint inserting kid
494
- * rows, a link inserting an edge) drops the whole cache — see the two
495
- * invalidation sites. Regenerable; a miss re-walks. */
496
- protected _chainMemo: BoundedMap<NodeId, NodeId[]>;
497
- /** Distinct edge-source count — the store's DOCUMENT COUNT (how many
498
- * learnt contexts predict a continuation), the N of every
499
- * inverse-document-frequency read. −1 until first asked for; from then
500
- * on maintained INCREMENTALLY by {@link link} (edges are never deleted),
501
- * so a read is O(1) — never a table scan on the recall path. */
502
- protected _edgeSrcCount: number;
503
- constructor(config: StoreConfig, D: number, maxGroup: number);
504
- get D(): number;
505
- /** Await the async initialisation performed by the concrete constructor. */
506
- protected _ensureReady(): Promise<void>;
507
- has(id: NodeId): boolean;
508
- protected mintId(): NodeId;
509
- nodeCount(): number;
510
- size(): Promise<number>;
511
- get(id: NodeId): NodeRec | null;
512
- /** Reconstruct the bytes a node spans by traversing the DAG bottom-up.
513
- * Iterative post-order on an explicit stack — the call stack never sees the
514
- * tree depth, so even an adversarial chain of nodes stays safe. */
515
- /** How many reads hit a MISSING node record this session (a dangling edge
516
- * or kid id). Zero in a healthy store; a growing count means references
517
- * outlive their records — the read degrades safely to empty bytes, this
518
- * counter is what keeps that degradation observable. */
519
- danglingReads: number;
520
- bytes(id: NodeId): Uint8Array;
521
- /** First `maxLen` bytes of a node. Walks only the leftmost branch,
522
- * stopping at `maxLen` — so a 1 MB document root costs the same as a
523
- * 4-byte leaf. Recursive, but tree depth is logarithmic.
524
- *
525
- * IMMUTABILITY CONTRACT (applies to {@link bytes} too): returned arrays
526
- * may be shared with the byte cache and with other callers — treat them
527
- * as read-only. Mutating one would corrupt every subsequent read. */
528
- bytesPrefix(id: NodeId, maxLen: number): Uint8Array;
529
- contentLen(id: NodeId, cap?: number): number;
530
- findLeaf(bytes: Uint8Array): NodeId | null;
531
- findBranch(kids: NodeId[]): NodeId | null;
532
- parents(id: NodeId): NodeId[];
533
- parentsFirst(id: NodeId, limit: number): NodeId[];
534
- hasParents(id: NodeId): boolean;
535
- chainRun(id: NodeId): readonly NodeId[];
536
- /** {@link Store.chainRun}'s walk, node at a time through the existing
537
- * probes. Adapters with a set-based query engine should override with a
538
- * single server-side descent (the SQLite adapter uses a recursive CTE). */
539
- protected _chainWalk(id: NodeId, cap: number): NodeId[];
540
- addContainer(child: NodeId, parent: NodeId): void;
541
- hasContainers(child: NodeId): boolean;
542
- containersSlice(child: NodeId, offset: number, limit: number): NodeId[];
543
- containers(child: NodeId): NodeId[];
544
- private flatLeafIds;
545
- /** On a dedup HIT, keep the node's gist available for lazy indexing —
546
- * EXACTLY when it is not already indexed. Replaces the old id-range
547
- * "recency" heuristic (id nextId − cacheWindow), which conflated an LRU
548
- * entry COUNT with an id RANGE and permanently refused to index any node
549
- * that first became a resonance target long after it was minted (an early
550
- * interior later reused as an edge/halo-bearing deposit root was silently
551
- * unreachable by resonance). The durable index itself is the arbiter:
552
- * one point query, cached in `_indexedIds` on a hit so repeats are O(1). */
553
- private captureIfUnindexed;
554
- /** If `id` structurally bridges ≥2 experiences (the post-hoc compaction
555
- * criterion), promote its gist into the content index NOW the exact
556
- * moment it becomes useful for multi-experience recall. The 1→2 parent
557
- * transition fires on {@link _dbInsertKid} during mint, and nodes that
558
- * were already bridges but missed indexing (gist evicted, pre-transition
559
- * store) are recaptured in {@link captureIfUnindexed}.
560
- *
561
- * A no-op when the node is already indexed or its gist is evicted from
562
- * the pending cache — a future re-encounter will retry. */
563
- private promoteBridge;
564
- private intern;
565
- /** Whether the byte content under `kids` and the byte content of `targetId`
566
- * are identical except for ONE local span of at most `W` bytes on each side
567
- * the near dedup's byte-grain definition of a near-duplicate. A
568
- * common-prefix / common-suffix trim: whatever remains after both trims is
569
- * the single differing span (substitution, insertion or deletion), and both
570
- * remainders must fit the budget. Scattered differences leave a wide
571
- * middle and are rejected. */
572
- private differsByOneWindow;
573
- putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
574
- putBranch(kids: NodeId[], gist: Vec): Promise<NodeId>;
575
- /** Promote a node's captured gist into the content (resonance) index, once.
576
- * Called the first time a node becomes a target — i.e. from `link` (it bears
577
- * or receives a continuation edge) or `pourHalo` (it gains distributional
578
- * company). Idempotent: a node already indexed, or whose gist has been evicted
579
- * from the bounded pending map, is a no-op.
580
- *
581
- * `dedupTarget` marks the node a candidate the near dedup may fold a fresh
582
- * near-gist branch ONTO. Only a genuine target an edge/halo-bearing ROOT
583
- * is one; a climb-only interior is reach-indexed but never a dedup sink. */
584
- protected indexGist(id: NodeId, dedupTarget: boolean): void;
585
- /** {@link Store.indexTarget} the public hook for marking a deposit root a
586
- * resonance target, the one target `link`/`pourHalo` do not cover. A deposit
587
- * root is a genuine target (a whole experience), so it is a dedup target
588
- * too. */
589
- indexTarget(id: NodeId): void;
590
- /** Index a node and its interior forms as resonance targets. A node that
591
- * gains an edge is a learnt EXPERIENCE, and the consensus climb
592
- * ({@link Mind.climbAttention}) answers a query naming only a PORTION of it by
593
- * resonating its SUB-REGIONS — branch nodes within the experience — and
594
- * climbing their parents back to it.
595
- *
596
- * EVERY interior branch is indexed unconditionally, and this is
597
- * LOAD-BEARING: indexing only structural bridges (nodes with ≥2 parents,
598
- * the post-hoc compaction criterion) was tried and REJECTED by the test
599
- * suite partial recall of an experience's interior slices, multi-topic
600
- * attention, and counterfactual anchoring all resonate to SINGLE-parent
601
- * interiors (13 tests fail without them). Post-hoc structural compaction
602
- * ({@link compactContentIndex}) may still remove them, but that is a
603
- * storage/recall trade-off for archived stores, not a free optimisation.
604
- * The store's hash-cons bounds the index by the number of DISTINCT byte
605
- * patterns in the corpus — not by the number of deposits.
606
- *
607
- * Only the ROOT is a DEDUP TARGET the whole experience a fresh near-gist
608
- * branch may legitimately fold onto. Interior nodes are REACH-ONLY: they
609
- * let a partial query resonate and climb, but a fresh branch must never
610
- * merge onto an interior node of another experience.
611
- *
612
- * Iterative explicit-queue walk: the call stack never sees tree depth. */
613
- protected indexSubtree(root: NodeId): void;
614
- resonate(v: Vec, k: number): Promise<Hit[]>;
615
- indexedVectorCount(): number;
616
- lastResonateReads(): number;
617
- /** How many physical compaction attempts have failed this session. Zero in
618
- * a healthy store; a growing count means tombstones are accumulating and
619
- * index query cost is drifting up (the first failure also warns once). */
620
- compactFailures: number;
621
- /** Meta key holding the incremental scan watermark of
622
- * {@link compactContentIndex}: "minParents:maxInternalIdScanned". KEEP
623
- * decisions are MONOTONE — parents, edges and halos only ever grow, so an
624
- * entry once kept can never become removable — and removed entries are
625
- * gone, so a pass only ever needs to examine entries indexed AFTER the
626
- * previous pass. Internal ids are monotone and survive the index's
627
- * splice compaction; the watermark is reset whenever a PHYSICAL index
628
- * compaction runs (id reuse after a dropped top row would otherwise hide
629
- * new entries behind it). */
630
- protected static readonly COMPACT_WATERMARK_KEY = "contentCompact.watermark";
631
- /** {@link Store.compactContentIndex} */
632
- compactContentIndex(minParents?: number): Promise<number>;
633
- /** {@link Store.repairContentIndex} */
634
- repairContentIndex(
635
- regenerateGist: (id: NodeId) => Promise<Vec | null>,
636
- minParents?: number,
637
- ): Promise<number>;
638
- link(from: NodeId, to: NodeId): Promise<void>;
639
- next(id: NodeId): NodeId[];
640
- /** {@link Store.hasNext} — one indexed point probe, never a range read. */
641
- hasNext(id: NodeId): boolean;
642
- prev(id: NodeId): NodeId[];
643
- nextFirst(id: NodeId, limit: number): NodeId[];
644
- prevFirst(id: NodeId, limit: number): NodeId[];
645
- /** {@link Store.prevCount}. Subclasses with an indexed reverse-edge count
646
- * should override; this default materialises (correct, not optimal). */
647
- prevCount(id: NodeId): number;
648
- edgeSourceCount(): number;
649
- haloMass(id: NodeId): number;
650
- halo(id: NodeId): Vec | null;
651
- /** {@link Store.hasHalo} — MUST mirror {@link halo}'s null condition
652
- * exactly (row present AND mass ≥ minHaloMass), minus the decode. */
653
- hasHalo(id: NodeId): boolean;
654
- pourHalo(id: NodeId, add: Vec): Promise<void>;
655
- resonateHalo(v: Vec, k: number): Promise<Hit[]>;
656
- private pending;
657
- protected flushContent(): number;
658
- protected flushHalos(): number;
659
- /** Append the buffered containment pairs, inside the deferred transaction.
660
- * Pure appends: durable dedup lives in the adapter (the pair PK), so a
661
- * flush never reads a child's stored list back — the old packed-blob
662
- * read-merge-rewrite was O(fan-in) per touched child per flush, quadratic
663
- * over a long training run on a hot window. */
664
- protected flushContain(): void;
665
- /** Flush all three buffers; compact vector indices on a write-volume cadence. */
666
- protected flush(): void;
667
- protected maybeFlush(): Promise<void>;
668
- setMeta(key: string, val: string): Promise<void>;
669
- getMeta(key: string): Promise<string | null>;
670
- deleteMeta(key: string): Promise<void>;
671
- saveSnapshot(bytes: Uint8Array): Promise<void>;
672
- loadSnapshot(): Promise<Uint8Array | null>;
673
- commit(): void;
674
- close(): Promise<void>;
271
+ /** Open the storage backend. Must set `this._nextId`, `this._D`, and
272
+ * `this._maxGroup` from the stored state (or defaults for a fresh store). */
273
+ protected abstract _dbOpen(): Promise<void>;
274
+ /** Close the storage backend and release all resources. */
275
+ protected abstract _dbClose(): void;
276
+ protected abstract _dbBeginTx(): void;
277
+ protected abstract _dbCommitTx(): void;
278
+ protected abstract _dbInsertNode(id: NodeId, leaf: Uint8Array | null, kids: Uint8Array | null, h: number): void;
279
+ protected abstract _dbGetNode(id: NodeId): NodeRec | null;
280
+ protected abstract _dbFindLeaf(h: number, bytes: Uint8Array): NodeId | null;
281
+ protected abstract _dbFindBranchByLeaf(h: number, bytes: Uint8Array): NodeId | null;
282
+ protected abstract _dbFindBranchByKids(h: number, packed: Uint8Array): NodeId | null;
283
+ protected abstract _dbInsertKid(child: NodeId, parent: NodeId): void;
284
+ protected abstract _dbGetParents(id: NodeId): NodeId[];
285
+ /** LIMITed variant of the parent read same statement, `LIMIT ?`. Must
286
+ * NOT be implemented by materialising and slicing. */
287
+ protected abstract _dbGetParentsFirst(id: NodeId, limit: number): NodeId[];
288
+ protected abstract _dbGetContainParents(child: NodeId): NodeId[];
289
+ /** Whether a containment row exists for `child` — no blob unpack. */
290
+ protected abstract _dbContainExists(child: NodeId): boolean;
291
+ /** A page of stored containment parents — partial blob unpack. */
292
+ protected abstract _dbGetContainParentsSlice(child: NodeId, offset: number, limit: number): NodeId[];
293
+ /** The stored containment-parent COUNT — blob length / 4, no unpack. */
294
+ protected abstract _dbGetContainCount(child: NodeId): number;
295
+ /** Append containment parents for `child`. MUST NOT re-write the child's
296
+ * whole stored list per call a hot child's fan-in is corpus-sized, and a
297
+ * full read-modify-write per flush is quadratic over training. Durable
298
+ * dedup may be DEFERRED (e.g. to a geometric merge schedule); readers
299
+ * dedup, so transient duplicates only cost bytes, never correctness. */
300
+ protected abstract _dbAppendContain(child: NodeId, parents: NodeId[]): void;
301
+ protected abstract _dbInsertEdge(src: NodeId, dst: NodeId): void;
302
+ protected abstract _dbGetNextEdges(id: NodeId): NodeId[];
303
+ protected abstract _dbGetPrevEdges(id: NodeId): NodeId[];
304
+ /** LIMITed variants of the two edge reads — same ORDER BY, `LIMIT ?`.
305
+ * These exist so a capped consumer's cost is bounded by the cap, not by
306
+ * the fan-out; an adapter must NOT implement them by materialising and
307
+ * slicing. */
308
+ protected abstract _dbGetNextEdgesFirst(id: NodeId, limit: number): NodeId[];
309
+ protected abstract _dbGetPrevEdgesFirst(id: NodeId, limit: number): NodeId[];
310
+ /** Whether ANY edge already leaves `src` one indexed point probe, used to
311
+ * maintain the distinct-source count incrementally. */
312
+ protected abstract _dbEdgeSrcExists(src: NodeId): boolean;
313
+ protected abstract _dbEdgeDistinctSrcCount(): number;
314
+ protected abstract _dbGetHalo(id: NodeId): {
315
+ vec: Uint8Array;
316
+ mass: number;
317
+ } | null;
318
+ protected abstract _dbUpsertHalo(id: NodeId, encodedVec: Uint8Array, mass: number): void;
319
+ protected abstract _dbGetMeta(key: string): string | null;
320
+ protected abstract _dbSetMeta(key: string, val: string): void;
321
+ protected abstract _dbDeleteMeta(key: string): void;
322
+ protected abstract _dbSaveSnapshot(bytes: Uint8Array): void;
323
+ protected abstract _dbLoadSnapshot(): Uint8Array | null;
324
+ protected abstract _vecContentUpsert(entries: Array<{
325
+ id: NodeId;
326
+ vector: Float32Array;
327
+ }>): void;
328
+ protected abstract _vecContentQuery(v: Float32Array, k: number, ef: number): Array<{
329
+ id: number;
330
+ distance: number;
331
+ }>;
332
+ /** Whether `id` is already a live entry of the content index — one point
333
+ * query, used to recognise durably-indexed nodes across sessions (the
334
+ * in-memory `_indexedIds` cache starts empty on every open). */
335
+ protected abstract _vecContentHas(id: NodeId): boolean;
336
+ protected abstract _vecContentSize(): number;
337
+ protected abstract _vecContentLastReads(): number;
338
+ protected abstract _vecContentPhysicalSize(): number;
339
+ protected abstract _vecContentClusterCount(): number;
340
+ protected abstract _vecContentCompact(): void;
341
+ /** Live content-index entries whose INTERNAL id is > `after`, as
342
+ * {ext, internal} pairs in internal-id order. Internal ids are monotone
343
+ * at insert and preserved by the index's tombstone-splice compaction, so
344
+ * the largest internal id a scan has seen is a durable watermark for
345
+ * incremental maintenance ({@link compactContentIndex}). */
346
+ protected abstract _vecContentEntriesSince(after: number): IterableIterator<{
347
+ ext: NodeId;
348
+ internal: number;
349
+ }>;
350
+ /** Every node id that carries a continuation edge (as source OR target) or
351
+ * a halo row — the RESONANCE-TARGET id set, sorted ascending and deduped.
352
+ * This is the exact keep/repair criterion of index maintenance; driving
353
+ * the maintenance loops from this (corpus-of-experiences-sized) set
354
+ * instead of probing per node turned repair from an every-node walk into
355
+ * a candidates-only walk. */
356
+ protected abstract _dbEdgeOrHaloIds(): NodeId[];
357
+ /** Remove a batch of entries from the content index by external id, under
358
+ * ONE storage transaction (a tombstone per implicit transaction is one WAL
359
+ * commit per id the dominant cost of a bulk prune). Idempotent per id —
360
+ * already-deleted or non-existent ids are skipped. */
361
+ protected abstract _vecContentDeleteMany(ids: NodeId[]): void;
362
+ protected abstract _vecHaloUpsert(entries: Array<{
363
+ id: NodeId;
364
+ vector: Float32Array;
365
+ }>): void;
366
+ protected abstract _vecHaloQuery(v: Float32Array, k: number, ef: number): Array<{
367
+ id: number;
368
+ distance: number;
369
+ }>;
370
+ /** Live (non-tombstoned) entry count of the halo index — the denominator
371
+ * the tombstone-ratio compaction trigger compares physical size against. */
372
+ protected abstract _vecHaloSize(): number;
373
+ protected abstract _vecHaloPhysicalSize(): number;
374
+ protected abstract _vecHaloClusterCount(): number;
375
+ protected abstract _vecHaloCompact(): void;
376
+ /** Derived query breadth for a partitioned index of C clusters: probe √C
377
+ * of them (the same √-of-the-population convention as the hub bound √N).
378
+ * The IVF maps ef nprobe as ceil(ef/4), so ef = 4·⌈√C⌉ probes exactly
379
+ * ⌈√C⌉ clusters. A FIXED efSearch stops scaling the moment the
380
+ * collection outgrows it: at 4,270 clusters the default 64 probed 16
381
+ * clusters (0.4%), and an exact stored match of a query routinely sat in
382
+ * an unprobed cluster recall silently degraded as the store grew. The
383
+ * configured efSearch remains the floor for small collections. */
384
+ protected efFor(clusterCount: number): number;
385
+ protected _D: number;
386
+ protected _maxGroup: number;
387
+ protected readonly minHaloMass: number;
388
+ protected readonly efSearch: number;
389
+ protected readonly overfetch: number;
390
+ protected readonly batchSize: number;
391
+ protected readonly compactEveryNWrites: number;
392
+ /** Branch node ids are a dense, monotonically-increasing integer sequence
393
+ * (0,1,2,…). Single-byte leaves occupy the implicit negative range −256…−1.
394
+ * They are NEVER deleted, so the count of minted branch ids IS the next id —
395
+ * which doubles as the branch-node count and lets has() be an O(1) check.
396
+ * Set by `_dbOpen()` from the stored node count; incremented by `mintId()`. */
397
+ protected _nextId: number;
398
+ protected _writtenSinceCompact: number;
399
+ protected closed: boolean;
400
+ /** Lifecycle guard — resolved once `_dbOpen()` completes. */
401
+ protected _ready: Promise<void> | null;
402
+ /** Exact-content dedup: content-key node id. Intrinsic compression. */
403
+ protected readonly _leafKey: BoundedMap<string, NodeId>;
404
+ protected readonly _branchKey: BoundedMap<string, NodeId>;
405
+ /** Reconstructed-bytes read cache (regenerable), keyed by node id. */
406
+ protected readonly _bytesCache: BoundedMap<NodeId, Uint8Array>;
407
+ /** contentLen memo — content is immutable, so entries never invalidate. */
408
+ protected readonly _lenCache: BoundedMap<NodeId, number>;
409
+ /** Node-record cache — avoids repeated persistence queries for shared DAG
410
+ * nodes. Each record is small (a few ints + short leaf buffer). */
411
+ protected readonly _recCache: BoundedMap<NodeId, NodeRec>;
412
+ /** Captured-but-not-yet-indexed gists. Sized in bytes (each is D·4); a deposit
413
+ * links/pours a node right after interning it, so the working set is one
414
+ * deposit's nodes a small budget captures ~all of it, and an eviction only
415
+ * means that node is reached by the DAG climb instead of by direct
416
+ * resonance. */
417
+ protected _pendingGist: BoundedMap<NodeId, Vec>;
418
+ /** EXACT halo accumulators for the session's live pours: full-precision in
419
+ * memory, 2-bit on disk, so within-session accumulate-then-compare never
420
+ * round-trips through the quantizer. Regenerable a miss reads the durable
421
+ * 2-bit row. */
422
+ protected _haloExact: BoundedMap<NodeId, Vec>;
423
+ /** NORMALIZED halo read cache — the decoded, normalized vector {@link halo}
424
+ * returns, cached by id so repeat reads skip the per-call 2-bit decode and
425
+ * normalize of a full D-element row (measured on a trained store: ~15K
426
+ * halo() calls per deep query over ~50 distinct ids — all but the first
427
+ * per id pure re-decode). Point-invalidated by {@link pourHalo}, the one
428
+ * halo mutation site. Callers receive a COPY, so the cached vector is
429
+ * never aliased. Regenerable a miss re-decodes the durable row. */
430
+ protected _haloNorm: BoundedMap<NodeId, Vec>;
431
+ /** Interiors deliberately SKIPPED by indexSubtree (unique nodes with 1 parent
432
+ * that bridge nothing). Remembered so subsequent visits prune the subtree
433
+ * without re-checking parent count. LRU-bounded: an evicted entry is
434
+ * re-checked on next visit if it gained parents in the meantime, it will
435
+ * be promoted to the index. */
436
+ protected _coveredIds: BoundedMap<NodeId, true>;
437
+ /** Live content-index id set, LRU-bounded so a massive ingest never leaks
438
+ * memory; an evicted entry is still indexed (the row is durable), so the
439
+ * only cost of an eviction is a duplicate index probe on next visit. */
440
+ protected _indexedIds: BoundedMap<NodeId, true>;
441
+ /** ANN read cache for {@link resonate} — keyed by vecKey(v) + ":" + k;
442
+ * lazily initialised, dropped on any index mutation. */
443
+ protected _resonateCache: Map<string, Hit[]> | null;
444
+ /** ANN read cache for {@link resonateHalo} same scheme. */
445
+ protected _resonateHaloCache: Map<string, Hit[]> | null;
446
+ /** Content (gist) index write buffer. */
447
+ protected _contentBuffer: Array<{
448
+ id: NodeId;
449
+ vector: Float32Array;
450
+ }>;
451
+ /** Halo index write buffer — keyed by id so repeats within a batch coalesce. */
452
+ protected _haloBuffer: Map<number, Float32Array<ArrayBufferLike>>;
453
+ /** Containment write buffer: child new parents, merged on flush cadence. */
454
+ protected _containBuf: Map<number, Set<number>>;
455
+ /** Dedup-target candidates still in the write buffer (keyed by id). Only
456
+ * roots that have gained an edge/halo are targets; a fresh intermediate
457
+ * branch is never folded onto. */
458
+ protected _nearDedupBuf: Map<number, Float32Array<ArrayBufferLike>>;
459
+ /** Ids currently in `_contentBuffer` (not yet flushed) — O(1) membership. */
460
+ protected _bufferedIds: Set<number>;
461
+ /** {@link Store.chainRun} results, valid for the store's lifetime BETWEEN
462
+ * writes: a chain is a pure function of the kid and edge tables, so any
463
+ * write that could break a node's transparency (a fresh mint inserting kid
464
+ * rows, a link inserting an edge) drops the whole cache — see the two
465
+ * invalidation sites. Regenerable; a miss re-walks. */
466
+ protected _chainMemo: BoundedMap<NodeId, NodeId[]>;
467
+ /** Distinct edge-source count the store's DOCUMENT COUNT (how many
468
+ * learnt contexts predict a continuation), the N of every
469
+ * inverse-document-frequency read. −1 until first asked for; from then
470
+ * on maintained INCREMENTALLY by {@link link} (edges are never deleted),
471
+ * so a read is O(1) — never a table scan on the recall path. */
472
+ protected _edgeSrcCount: number;
473
+ constructor(config: StoreConfig, D: number, maxGroup: number);
474
+ get D(): number;
475
+ /** Await the async initialisation performed by the concrete constructor. */
476
+ protected _ensureReady(): Promise<void>;
477
+ has(id: NodeId): boolean;
478
+ protected mintId(): NodeId;
479
+ nodeCount(): number;
480
+ size(): Promise<number>;
481
+ get(id: NodeId): NodeRec | null;
482
+ /** Reconstruct the bytes a node spans by traversing the DAG bottom-up.
483
+ * Iterative post-order on an explicit stack the call stack never sees the
484
+ * tree depth, so even an adversarial chain of nodes stays safe. */
485
+ /** How many reads hit a MISSING node record this session (a dangling edge
486
+ * or kid id). Zero in a healthy store; a growing count means references
487
+ * outlive their records — the read degrades safely to empty bytes, this
488
+ * counter is what keeps that degradation observable. */
489
+ danglingReads: number;
490
+ bytes(id: NodeId): Uint8Array;
491
+ /** First `maxLen` bytes of a node. Walks only the leftmost branch,
492
+ * stopping at `maxLen` — so a 1 MB document root costs the same as a
493
+ * 4-byte leaf. Recursive, but tree depth is logarithmic.
494
+ *
495
+ * IMMUTABILITY CONTRACT (applies to {@link bytes} too): returned arrays
496
+ * may be shared with the byte cache and with other callers — treat them
497
+ * as read-only. Mutating one would corrupt every subsequent read. */
498
+ bytesPrefix(id: NodeId, maxLen: number): Uint8Array;
499
+ contentLen(id: NodeId, cap?: number): number;
500
+ findLeaf(bytes: Uint8Array): NodeId | null;
501
+ findBranch(kids: NodeId[]): NodeId | null;
502
+ parents(id: NodeId): NodeId[];
503
+ parentsFirst(id: NodeId, limit: number): NodeId[];
504
+ hasParents(id: NodeId): boolean;
505
+ chainRun(id: NodeId): readonly NodeId[];
506
+ /** {@link Store.chainRun}'s walk, node at a time through the existing
507
+ * probes. Adapters with a set-based query engine should override with a
508
+ * single server-side descent (the SQLite adapter uses a recursive CTE). */
509
+ protected _chainWalk(id: NodeId, cap: number): NodeId[];
510
+ addContainer(child: NodeId, parent: NodeId): void;
511
+ hasContainers(child: NodeId): boolean;
512
+ containersSlice(child: NodeId, offset: number, limit: number): NodeId[];
513
+ containers(child: NodeId): NodeId[];
514
+ private flatLeafIds;
515
+ /** On a dedup HIT, keep the node's gist available for lazy indexing —
516
+ * EXACTLY when it is not already indexed. Replaces the old id-range
517
+ * "recency" heuristic (id ≥ nextId − cacheWindow), which conflated an LRU
518
+ * entry COUNT with an id RANGE and permanently refused to index any node
519
+ * that first became a resonance target long after it was minted (an early
520
+ * interior later reused as an edge/halo-bearing deposit root was silently
521
+ * unreachable by resonance). The durable index itself is the arbiter:
522
+ * one point query, cached in `_indexedIds` on a hit so repeats are O(1). */
523
+ private captureIfUnindexed;
524
+ /** If `id` structurally bridges ≥2 experiences (the post-hoc compaction
525
+ * criterion), promote its gist into the content index NOW — the exact
526
+ * moment it becomes useful for multi-experience recall. The 1→2 parent
527
+ * transition fires on {@link _dbInsertKid} during mint, and nodes that
528
+ * were already bridges but missed indexing (gist evicted, pre-transition
529
+ * store) are recaptured in {@link captureIfUnindexed}.
530
+ *
531
+ * A no-op when the node is already indexed or its gist is evicted from
532
+ * the pending cache — a future re-encounter will retry. */
533
+ private promoteBridge;
534
+ private intern;
535
+ /** Whether the byte content under `kids` and the byte content of `targetId`
536
+ * are identical except for ONE local span of at most `W` bytes on each side
537
+ * the near dedup's byte-grain definition of a near-duplicate. A
538
+ * common-prefix / common-suffix trim: whatever remains after both trims is
539
+ * the single differing span (substitution, insertion or deletion), and both
540
+ * remainders must fit the budget. Scattered differences leave a wide
541
+ * middle and are rejected. */
542
+ private differsByOneWindow;
543
+ putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
544
+ putBranch(kids: NodeId[], gist: Vec): Promise<NodeId>;
545
+ /** Promote a node's captured gist into the content (resonance) index, once.
546
+ * Called the first time a node becomes a target i.e. from `link` (it bears
547
+ * or receives a continuation edge) or `pourHalo` (it gains distributional
548
+ * company). Idempotent: a node already indexed, or whose gist has been evicted
549
+ * from the bounded pending map, is a no-op.
550
+ *
551
+ * `dedupTarget` marks the node a candidate the near dedup may fold a fresh
552
+ * near-gist branch ONTO. Only a genuine target an edge/halo-bearing ROOT
553
+ * is one; a climb-only interior is reach-indexed but never a dedup sink. */
554
+ protected indexGist(id: NodeId, dedupTarget: boolean): void;
555
+ /** {@link Store.indexTarget} the public hook for marking a deposit root a
556
+ * resonance target, the one target `link`/`pourHalo` do not cover. A deposit
557
+ * root is a genuine target (a whole experience), so it is a dedup target
558
+ * too. */
559
+ indexTarget(id: NodeId): void;
560
+ /** Index a node and its interior forms as resonance targets. A node that
561
+ * gains an edge is a learnt EXPERIENCE, and the consensus climb
562
+ * ({@link Mind.climbAttention}) answers a query naming only a PORTION of it by
563
+ * resonating its SUB-REGIONS branch nodes within the experience and
564
+ * climbing their parents back to it.
565
+ *
566
+ * EVERY interior branch is indexed unconditionally, and this is
567
+ * LOAD-BEARING: indexing only structural bridges (nodes with ≥2 parents,
568
+ * the post-hoc compaction criterion) was tried and REJECTED by the test
569
+ * suite — partial recall of an experience's interior slices, multi-topic
570
+ * attention, and counterfactual anchoring all resonate to SINGLE-parent
571
+ * interiors (13 tests fail without them). Post-hoc structural compaction
572
+ * ({@link compactContentIndex}) may still remove them, but that is a
573
+ * storage/recall trade-off for archived stores, not a free optimisation.
574
+ * The store's hash-cons bounds the index by the number of DISTINCT byte
575
+ * patterns in the corpus not by the number of deposits.
576
+ *
577
+ * Only the ROOT is a DEDUP TARGET — the whole experience a fresh near-gist
578
+ * branch may legitimately fold onto. Interior nodes are REACH-ONLY: they
579
+ * let a partial query resonate and climb, but a fresh branch must never
580
+ * merge onto an interior node of another experience.
581
+ *
582
+ * Iterative explicit-queue walk: the call stack never sees tree depth. */
583
+ protected indexSubtree(root: NodeId): void;
584
+ resonate(v: Vec, k: number): Promise<Hit[]>;
585
+ indexedVectorCount(): number;
586
+ lastResonateReads(): number;
587
+ /** How many physical compaction attempts have failed this session. Zero in
588
+ * a healthy store; a growing count means tombstones are accumulating and
589
+ * index query cost is drifting up (the first failure also warns once). */
590
+ compactFailures: number;
591
+ /** Meta key holding the incremental scan watermark of
592
+ * {@link compactContentIndex}: "minParents:maxInternalIdScanned". KEEP
593
+ * decisions are MONOTONE parents, edges and halos only ever grow, so an
594
+ * entry once kept can never become removable and removed entries are
595
+ * gone, so a pass only ever needs to examine entries indexed AFTER the
596
+ * previous pass. Internal ids are monotone and survive the index's
597
+ * splice compaction; the watermark is reset whenever a PHYSICAL index
598
+ * compaction runs (id reuse after a dropped top row would otherwise hide
599
+ * new entries behind it). */
600
+ protected static readonly COMPACT_WATERMARK_KEY = "contentCompact.watermark";
601
+ /** {@link Store.compactContentIndex} */
602
+ compactContentIndex(minParents?: number): Promise<number>;
603
+ /** {@link Store.repairContentIndex} */
604
+ repairContentIndex(regenerateGist: (id: NodeId) => Promise<Vec | null>, minParents?: number): Promise<number>;
605
+ link(from: NodeId, to: NodeId): Promise<void>;
606
+ next(id: NodeId): NodeId[];
607
+ /** {@link Store.hasNext} one indexed point probe, never a range read. */
608
+ hasNext(id: NodeId): boolean;
609
+ prev(id: NodeId): NodeId[];
610
+ nextFirst(id: NodeId, limit: number): NodeId[];
611
+ prevFirst(id: NodeId, limit: number): NodeId[];
612
+ /** {@link Store.prevCount}. Subclasses with an indexed reverse-edge count
613
+ * should override; this default materialises (correct, not optimal). */
614
+ prevCount(id: NodeId): number;
615
+ edgeSourceCount(): number;
616
+ haloMass(id: NodeId): number;
617
+ halo(id: NodeId): Vec | null;
618
+ /** {@link Store.hasHalo} MUST mirror {@link halo}'s null condition
619
+ * exactly (row present AND mass ≥ minHaloMass), minus the decode. */
620
+ hasHalo(id: NodeId): boolean;
621
+ pourHalo(id: NodeId, add: Vec): Promise<void>;
622
+ resonateHalo(v: Vec, k: number): Promise<Hit[]>;
623
+ private pending;
624
+ protected flushContent(): number;
625
+ protected flushHalos(): number;
626
+ /** Append the buffered containment pairs, inside the deferred transaction.
627
+ * Pure appends: durable dedup lives in the adapter (the pair PK), so a
628
+ * flush never reads a child's stored list back — the old packed-blob
629
+ * read-merge-rewrite was O(fan-in) per touched child per flush, quadratic
630
+ * over a long training run on a hot window. */
631
+ protected flushContain(): void;
632
+ /** Flush all three buffers; compact vector indices on a write-volume cadence. */
633
+ protected flush(): void;
634
+ protected maybeFlush(): Promise<void>;
635
+ setMeta(key: string, val: string): Promise<void>;
636
+ getMeta(key: string): Promise<string | null>;
637
+ deleteMeta(key: string): Promise<void>;
638
+ saveSnapshot(bytes: Uint8Array): Promise<void>;
639
+ loadSnapshot(): Promise<Uint8Array | null>;
640
+ commit(): void;
641
+ close(): Promise<void>;
675
642
  }