@hviana/sema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/AGENTS.md +470 -0
  2. package/AUTHORS.md +12 -0
  3. package/COMMERCIAL-LICENSE.md +19 -0
  4. package/CONTRIBUTING.md +15 -0
  5. package/HOW_IT_WORKS.md +4210 -0
  6. package/LICENSE.md +117 -0
  7. package/README.md +290 -0
  8. package/TRADEMARKS.md +19 -0
  9. package/example/demo.ts +46 -0
  10. package/example/train_base.ts +2675 -0
  11. package/index.html +893 -0
  12. package/package.json +25 -0
  13. package/src/alphabet.ts +34 -0
  14. package/src/alu/README.md +332 -0
  15. package/src/alu/src/alu.ts +541 -0
  16. package/src/alu/src/expr.ts +339 -0
  17. package/src/alu/src/index.ts +115 -0
  18. package/src/alu/src/kernel-arith.ts +377 -0
  19. package/src/alu/src/kernel-bits.ts +199 -0
  20. package/src/alu/src/kernel-logic.ts +102 -0
  21. package/src/alu/src/kernel-nd.ts +235 -0
  22. package/src/alu/src/kernel-numeric.ts +466 -0
  23. package/src/alu/src/operation.ts +344 -0
  24. package/src/alu/src/parser.ts +574 -0
  25. package/src/alu/src/resonance.ts +161 -0
  26. package/src/alu/src/text.ts +83 -0
  27. package/src/alu/src/value.ts +327 -0
  28. package/src/alu/test/alu.test.ts +1004 -0
  29. package/src/bytes.ts +62 -0
  30. package/src/config.ts +227 -0
  31. package/src/derive/README.md +295 -0
  32. package/src/derive/src/deduction.ts +263 -0
  33. package/src/derive/src/index.ts +24 -0
  34. package/src/derive/src/priority-queue.ts +70 -0
  35. package/src/derive/src/rewrite.ts +127 -0
  36. package/src/derive/src/trie.ts +242 -0
  37. package/src/derive/test/derive.test.ts +137 -0
  38. package/src/extension.ts +42 -0
  39. package/src/geometry.ts +511 -0
  40. package/src/index.ts +38 -0
  41. package/src/ingest-cache.ts +224 -0
  42. package/src/mind/articulation.ts +137 -0
  43. package/src/mind/attention.ts +1061 -0
  44. package/src/mind/canonical.ts +107 -0
  45. package/src/mind/graph-search.ts +1100 -0
  46. package/src/mind/index.ts +18 -0
  47. package/src/mind/junction.ts +347 -0
  48. package/src/mind/learning.ts +246 -0
  49. package/src/mind/match.ts +507 -0
  50. package/src/mind/mechanisms/alu.ts +33 -0
  51. package/src/mind/mechanisms/cast.ts +568 -0
  52. package/src/mind/mechanisms/confluence.ts +268 -0
  53. package/src/mind/mechanisms/cover.ts +248 -0
  54. package/src/mind/mechanisms/extraction.ts +422 -0
  55. package/src/mind/mechanisms/recall.ts +241 -0
  56. package/src/mind/mind.ts +483 -0
  57. package/src/mind/pipeline-mechanism.ts +326 -0
  58. package/src/mind/pipeline.ts +300 -0
  59. package/src/mind/primitives.ts +201 -0
  60. package/src/mind/rationale.ts +275 -0
  61. package/src/mind/reasoning.ts +198 -0
  62. package/src/mind/recognition.ts +234 -0
  63. package/src/mind/resonance.ts +0 -0
  64. package/src/mind/trace.ts +108 -0
  65. package/src/mind/traverse.ts +556 -0
  66. package/src/mind/types.ts +281 -0
  67. package/src/rabitq-hnsw/README.md +303 -0
  68. package/src/rabitq-hnsw/src/database.ts +492 -0
  69. package/src/rabitq-hnsw/src/heap.ts +90 -0
  70. package/src/rabitq-hnsw/src/hnsw.ts +514 -0
  71. package/src/rabitq-hnsw/src/index.ts +15 -0
  72. package/src/rabitq-hnsw/src/prng.ts +39 -0
  73. package/src/rabitq-hnsw/src/rabitq.ts +308 -0
  74. package/src/rabitq-hnsw/src/store.ts +994 -0
  75. package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
  76. package/src/store-sqlite.ts +928 -0
  77. package/src/store.ts +2148 -0
  78. package/src/vec.ts +124 -0
  79. package/test/00-extract.test.mjs +151 -0
  80. package/test/01-floor.test.mjs +20 -0
  81. package/test/02-roundtrip.test.mjs +83 -0
  82. package/test/03-recall.test.mjs +98 -0
  83. package/test/04-think.test.mjs +627 -0
  84. package/test/05-concepts.test.mjs +84 -0
  85. package/test/08-storage.test.mjs +948 -0
  86. package/test/09-edges.test.mjs +65 -0
  87. package/test/11-universality.test.mjs +180 -0
  88. package/test/13-conversation.test.mjs +228 -0
  89. package/test/14-scaling.test.mjs +503 -0
  90. package/test/15-decomposition-gap.test.mjs +0 -0
  91. package/test/16-bridge.test.mjs +250 -0
  92. package/test/17-intelligence.test.mjs +240 -0
  93. package/test/18-alu.test.mjs +209 -0
  94. package/test/19-nd.test.mjs +254 -0
  95. package/test/20-stability.test.mjs +489 -0
  96. package/test/21-partial.test.mjs +158 -0
  97. package/test/22-multihop.test.mjs +130 -0
  98. package/test/23-rationale.test.mjs +316 -0
  99. package/test/24-generalization.test.mjs +416 -0
  100. package/test/25-embedding.test.mjs +75 -0
  101. package/test/26-cooperative.test.mjs +89 -0
  102. package/test/27-saturation-drop.test.mjs +637 -0
  103. package/test/28-unknowable.test.mjs +88 -0
  104. package/test/29-counterfactual.test.mjs +476 -0
  105. package/test/30-conflict-resolution.test.mjs +166 -0
  106. package/test/31-audit.test.mjs +288 -0
  107. package/test/32-confluence.test.mjs +173 -0
  108. package/test/33-multi-candidate.test.mjs +222 -0
  109. package/test/34-cross-region.test.mjs +252 -0
  110. package/tsconfig.json +16 -0
@@ -0,0 +1,928 @@
1
+ // store-sqlite.ts — SQLite + rabitq-hnsw persistence adapter.
2
+ //
3
+ // SQliteStore extends AbstractStore and implements only the bare essentials
4
+ // required for database communication: SQL schema, prepared statements, and
5
+ // VectorDatabase management. All domain logic — caching, dedup/merge
6
+ // decisions, interior-node indexing, geometric halo scheduling, buffer
7
+ // management — lives in the AbstractStore base class (store.ts).
8
+ //
9
+ // Two clearly separated concerns, each with its OWN vector index:
10
+ //
11
+ // • STORAGE — the content-addressed DAG (nodes, edges, metadata) in SQLite,
12
+ // plus a VectorDatabase for node gists (resonant lookup), in its OWN
13
+ // SQLite file (`<path>.content.vec`).
14
+ // • CACHE — the distributional "company" of each node (its halo), a
15
+ // derived/regenerable concept layer, in a SEPARATE VectorDatabase,
16
+ // in its own SQLite file (`<path>.halo.vec`).
17
+ //
18
+ // Write discipline:
19
+ //
20
+ // • Integer node ids: a dense 0,1,2,… counter. SQLite stores them inline in
21
+ // the rowid B-tree (no secondary index), child lists pack to 4 bytes each,
22
+ // and the vector index keys on them as a 4-byte int32.
23
+ // • One deferred WRITE TRANSACTION: node rows, edges and halo updates are
24
+ // committed in batches on the flush cadence (and on close/snapshot) rather
25
+ // than one implicit transaction per node.
26
+ // • Each of the three SQLite databases (main DAG, content vectors, halo
27
+ // vectors) is an independent file. The vector databases persist to their
28
+ // own files on every write — no separate serialisation step is needed.
29
+
30
+ import { DatabaseSync } from "node:sqlite";
31
+
32
+ import {
33
+ AbstractStore,
34
+ flatBytesKids,
35
+ type NodeId,
36
+ type NodeRec,
37
+ packKids,
38
+ type Store,
39
+ unpackKids,
40
+ } from "./store.js";
41
+ import { DEFAULT_CONFIG, type StoreConfig } from "./config.js";
42
+ import { VectorDatabase } from "./rabitq-hnsw/src/index.js";
43
+
44
+ export interface SQliteStoreOptions extends Partial<StoreConfig> {
45
+ path?: string;
46
+ D?: number;
47
+ /** Fold group size used during training — tells indexSubtree how tight the
48
+ * reach-net epsilon should be (1 − 1/(2·maxGroup)). Defaults to the
49
+ * Mind's geometry.maxGroup. */
50
+ maxGroup?: number;
51
+ /** Query-side RaBitQ estimator precision for both vector indices. */
52
+ queryBits?: number;
53
+ }
54
+
55
+ const SCHEMA = `
56
+ -- Content-addressed lookup goes through ONE small integer index: h is the
57
+ -- FNV-1a hash of the row's content key (leaf bytes, or the packed kid ids of a
58
+ -- mixed branch). Indexing the hash instead of the blob itself means the lookup
59
+ -- index stores ~9 bytes per node rather than a full copy of every leaf and kid
60
+ -- blob (which doubled the table); the point query fetches the h-candidates and
61
+ -- verifies the actual blob, so collisions cost a fetch, never a wrong id.
62
+ CREATE TABLE IF NOT EXISTS node (
63
+ id INTEGER PRIMARY KEY,
64
+ leaf BLOB,
65
+ kids BLOB,
66
+ h INTEGER NOT NULL
67
+ );
68
+ CREATE INDEX IF NOT EXISTS idx_node_h ON node(h);
69
+ -- The reverse structural edge child→parent. A WITHOUT ROWID table clustered on
70
+ -- (child, parent) IS the lookup index: parents(child) is one B-tree descent to a
71
+ -- contiguous run, with no separate rowid heap and no secondary index to maintain
72
+ -- (~2x smaller than a heap table + idx_kid_child). The composite primary key also
73
+ -- gives free (child,parent) dedup, so a child repeated within one branch's kids
74
+ -- never writes a duplicate row.
75
+ CREATE TABLE IF NOT EXISTS kid (
76
+ child INTEGER NOT NULL,
77
+ parent INTEGER NOT NULL,
78
+ PRIMARY KEY (child, parent)
79
+ ) WITHOUT ROWID;
80
+ -- seq is a plain INTEGER PRIMARY KEY: rowids are assigned max+1 and edges are
81
+ -- never deleted, so insertion order is preserved without AUTOINCREMENT's
82
+ -- sqlite_sequence bookkeeping table.
83
+ CREATE TABLE IF NOT EXISTS edge (
84
+ src INTEGER NOT NULL,
85
+ dst INTEGER NOT NULL,
86
+ seq INTEGER PRIMARY KEY
87
+ );
88
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_edge ON edge(src, dst);
89
+ CREATE INDEX IF NOT EXISTS idx_edge_dst ON edge(dst);
90
+ CREATE TABLE IF NOT EXISTS halo (
91
+ id INTEGER PRIMARY KEY,
92
+ vec BLOB NOT NULL,
93
+ mass REAL NOT NULL DEFAULT 0
94
+ );
95
+ -- CONTAINMENT: parent's byte content contains child's, for children that are
96
+ -- not among any parent's kids list (sub-span flat branches inside leaf-parent
97
+ -- chunks). Kept SEPARATE from kid so parents() — the structural climb — is
98
+ -- untouched; containers() serves the climbing surface of orphan flat branches
99
+ -- durably (a session-local map would be lost on restart or ingest-cache replay).
100
+ -- LOG-STRUCTURED PACKED PAGES: a child's parents are int32-packed blobs split
101
+ -- across (child, seq) rows. A flush APPENDS one page holding only the NEW
102
+ -- parents (never reading the list back), then geometrically merges adjacent
103
+ -- pages (byte concat; full dedup when a merge reaches the seq-0 base page), so
104
+ -- page count stays O(log fan-in) and total merge I/O is amortized linear.
105
+ -- This keeps the packed format's ~4 B/parent density (contain is ~11% of a
106
+ -- trained store; one B-tree cell per pair measured 2.3× larger) while fixing
107
+ -- the single-blob row's flaw: merging new parents into ONE row re-wrote the
108
+ -- child's WHOLE list per flush — quadratic in a hot window's corpus-sized
109
+ -- fan-in. Transient duplicates may exist across pages until a base merge;
110
+ -- readers dedup (containers()) or tolerate repeats by contract (slices).
111
+ -- The (child, seq) key is packed into the INTEGER PRIMARY KEY (rowid) as
112
+ -- child·2^8 + seq — exact in a JS double for every valid id, and a child's
113
+ -- pages are one contiguous rowid range. The geometric merge bounds live page
114
+ -- counts (and thus seq) to O(log fan-in) ≪ 2^8, and a narrow band keeps the
115
+ -- rowid varint within ~1 byte of the bare child id. A rowid table, NOT a WITHOUT ROWID one: index B-tree cells cap at
116
+ -- ~¼ page, so at 1 KiB pages any child beyond ~55 parents would spill each
117
+ -- page row onto a mostly-empty overflow page (measured +26% table size).
118
+ CREATE TABLE IF NOT EXISTS contain (
119
+ id INTEGER PRIMARY KEY,
120
+ parents BLOB NOT NULL
121
+ );
122
+ CREATE TABLE IF NOT EXISTS snapshot (
123
+ id INTEGER PRIMARY KEY CHECK (id = 1),
124
+ data BLOB NOT NULL
125
+ );
126
+ CREATE TABLE IF NOT EXISTS meta (
127
+ key TEXT PRIMARY KEY,
128
+ val TEXT NOT NULL
129
+ );
130
+ `;
131
+
132
+ export class SQliteStore extends AbstractStore implements Store {
133
+ private readonly opts: SQliteStoreOptions;
134
+ private readonly vectorCacheMb: number;
135
+ private readonly vectorSeed: number;
136
+
137
+ // ── the two SEPARATE vector indices ───────────────────────────────────
138
+ private content: VectorDatabase | null = null; // STORAGE: node gists
139
+ private halos: VectorDatabase | null = null; // CACHE: halo gists
140
+
141
+ private sqlite: DatabaseSync | null = null;
142
+
143
+ // Deferred write transaction guard.
144
+ private _inTx = false;
145
+
146
+ private _insertNode: any = null;
147
+ private _insertKid: any = null;
148
+ private _selContain: any = null;
149
+ private _selParents: any = null;
150
+ private _selHalo: any = null;
151
+ private _upHalo: any = null;
152
+ private _selLeaf: any = null;
153
+ private _selFlat: any = null;
154
+ private _selKids: any = null;
155
+ private _selNode: any = null;
156
+ private _insertEdge: any = null;
157
+ private _selNext: any = null;
158
+ private _selPrev: any = null;
159
+ private _setMeta: any = null;
160
+ private _getMeta: any = null;
161
+ private _delMeta: any = null;
162
+ private _insSnapshot: any = null;
163
+ private _selSnapshot: any = null;
164
+
165
+ constructor(opts: SQliteStoreOptions = {}) {
166
+ const d = DEFAULT_CONFIG.store;
167
+ // Resolve config from opts, falling back to defaults.
168
+ const config: StoreConfig = {
169
+ minHaloMass: opts.minHaloMass ?? d.minHaloMass,
170
+ m: opts.m ?? d.m,
171
+ efConstruction: opts.efConstruction ?? d.efConstruction,
172
+ efConstructionInterior: opts.efConstructionInterior ??
173
+ d.efConstructionInterior,
174
+ efSearch: opts.efSearch ?? d.efSearch,
175
+ compactEveryNWrites: opts.compactEveryNWrites ?? d.compactEveryNWrites,
176
+ overfetch: opts.overfetch ?? d.overfetch,
177
+ batchSize: opts.batchSize ?? d.batchSize,
178
+ dedupCacheMax: opts.dedupCacheMax ?? d.dedupCacheMax,
179
+ bytesCacheMax: opts.bytesCacheMax ?? d.bytesCacheMax,
180
+ recCacheBytes: opts.recCacheBytes ?? d.recCacheBytes,
181
+ ingestCacheBytes: opts.ingestCacheBytes ?? d.ingestCacheBytes,
182
+ pendingGistBytes: opts.pendingGistBytes ?? d.pendingGistBytes,
183
+ haloCacheBytes: opts.haloCacheBytes ?? d.haloCacheBytes,
184
+ vectorCacheMb: opts.vectorCacheMb ?? d.vectorCacheMb,
185
+ coveredIdsMax: opts.coveredIdsMax ?? d.coveredIdsMax,
186
+ chainCacheBytes: opts.chainCacheBytes ?? d.chainCacheBytes,
187
+ };
188
+ const D = opts.D ?? 1024;
189
+ const maxGroup = opts.maxGroup ?? DEFAULT_CONFIG.geometry.maxGroup;
190
+ super(config, D, maxGroup);
191
+
192
+ this.opts = opts;
193
+ this.vectorCacheMb = opts.vectorCacheMb ?? d.vectorCacheMb;
194
+ this.vectorSeed = (0x51f15e ^ 0x9e3779b9) >>> 0;
195
+ this._ready = this._dbOpen();
196
+ }
197
+
198
+ // ── Vector-DB paths ───────────────────────────────────────────────────
199
+
200
+ private vectorDbPath(name: "content" | "halo"): string {
201
+ const stem = this.opts.path ?? ":memory:";
202
+ if (stem === ":memory:") return ":memory:";
203
+ return `${stem}.${name}.vec`;
204
+ }
205
+
206
+ private openVectorDB(name: "content" | "halo"): VectorDatabase {
207
+ return new VectorDatabase({
208
+ dbPath: this.vectorDbPath(name),
209
+ dim: this.D,
210
+ M: this.m,
211
+ efConstruction: this.efConstruction,
212
+ efSearch: this.efSearch,
213
+ // Query-side estimator precision. 8 bits: 4 bits measurably misranks
214
+ // tight gist clusters (mixture recall@10 37.5% vs 39.0%, self-recall
215
+ // 69% vs 76%; rabitq test 2a), and the codes on disk are independent of
216
+ // it, so existing stores adopt the sharper setting on reopen. The one
217
+ // decision the sharper ranking exposed — a saturated hub winning a
218
+ // sub-noise rank tie and silencing its region — is handled by the
219
+ // tie-band saturation fallback in attention.ts, which is derived from
220
+ // the estimator's own margin noise rather than tuned to a bit width.
221
+ queryBits: this.opts.queryBits ?? 8,
222
+ seed: this.vectorSeed,
223
+ cacheSizeMb: this.vectorCacheMb,
224
+ });
225
+ }
226
+
227
+ // ── Abstract method implementations ───────────────────────────────────
228
+
229
+ // -- Lifecycle --
230
+
231
+ protected async _dbOpen(): Promise<void> {
232
+ const stem = this.opts.path ?? ":memory:";
233
+ const sp = stem === ":memory:" ? ":memory:" : `${stem}.sqlite`;
234
+
235
+ this.sqlite = new DatabaseSync(sp);
236
+ // Page size matched to the store's row grain BEFORE any table exists (a
237
+ // no-op on a non-empty database). Every table here holds small rows —
238
+ // packed kid/contain pairs, byte-packed flat branches, quantized halos —
239
+ // so SQLite's default 4 KiB pages carry mostly slack; 1 KiB pages keep
240
+ // leaf-page fill high at a negligible extra tree depth.
241
+ this.sqlite.exec("PRAGMA page_size = 1024;");
242
+ // WAL + NORMAL sync: crash-safe (WAL commits are atomic) without a full
243
+ // fsync per commit — the same discipline the vector databases use.
244
+ this.sqlite.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;");
245
+ this.sqlite.exec(SCHEMA);
246
+
247
+ // Recover D from a previous run so loadFromStore() can bootstrap with any
248
+ // default — the real value is read from meta before vector indices load.
249
+ // The meta table always exists here (SCHEMA above creates it), so these
250
+ // reads run bare: a throw is a genuine storage fault, and swallowing it
251
+ // would let the store proceed with a default D/maxGroup that silently
252
+ // disagrees with what training persisted.
253
+ {
254
+ const row = this.sqlite.prepare(
255
+ "SELECT val FROM meta WHERE key = 'train.D'",
256
+ ).get() as { val: string } | undefined;
257
+ if (row) {
258
+ const d = Number(row.val);
259
+ if (Number.isInteger(d) && d > 0) this._D = d;
260
+ }
261
+ }
262
+
263
+ // Recover maxGroup from meta so indexSubtree uses the training-time value.
264
+ {
265
+ const row = this.sqlite.prepare(
266
+ "SELECT val FROM meta WHERE key = 'geometry.maxGroup'",
267
+ ).get() as { val: string } | undefined;
268
+ if (row) {
269
+ const g = Number(row.val);
270
+ if (Number.isInteger(g) && g > 0) this._maxGroup = g;
271
+ }
272
+ }
273
+
274
+ // Persist maxGroup to meta when opening a FRESH store (no rows yet) so
275
+ // indexSubtree always sees the training-time value even when the store is
276
+ // accessed without a Mind / full snapshot.
277
+ if (this._nextId === 0) {
278
+ this.sqlite.prepare(
279
+ "INSERT OR IGNORE INTO meta (key, val) VALUES ('geometry.maxGroup', ?)",
280
+ ).run(String(this._maxGroup));
281
+ }
282
+
283
+ this.content = this.openVectorDB("content");
284
+ this.halos = this.openVectorDB("halo");
285
+
286
+ // Ids are dense (0,1,2,…) and never deleted, so MAX(id)+1 IS the next id
287
+ // — one rightmost B-tree descent on the rowid, O(log n). (COUNT(*) gives
288
+ // the same value but scans every leaf page, so open time grew linearly
289
+ // with the store; a per-row scan into a Set was worse still.)
290
+ this._nextId = ((this.sqlite.prepare(
291
+ "SELECT MAX(id) AS m FROM node",
292
+ ).get() as { m: number | null }).m ?? -1) + 1;
293
+ }
294
+
295
+ protected _dbClose(): void {
296
+ if (this.content) {
297
+ this.content.close();
298
+ this.content = null;
299
+ }
300
+ if (this.halos) {
301
+ this.halos.close();
302
+ this.halos = null;
303
+ }
304
+ if (this.sqlite) {
305
+ this.sqlite.close();
306
+ this.sqlite = null;
307
+ }
308
+ }
309
+
310
+ // -- Transaction --
311
+
312
+ protected _dbBeginTx(): void {
313
+ if (this._inTx || !this.sqlite) return;
314
+ this.sqlite.exec("BEGIN");
315
+ this._inTx = true;
316
+ }
317
+
318
+ protected _dbCommitTx(): void {
319
+ if (!this._inTx || !this.sqlite) return;
320
+ this._inTx = false; // clear first so a throw can't wedge us mid-commit
321
+ this.sqlite.exec("COMMIT");
322
+ }
323
+
324
+ // -- Node CRUD --
325
+
326
+ protected _dbInsertNode(
327
+ id: NodeId,
328
+ leaf: Uint8Array | null,
329
+ kids: Uint8Array | null,
330
+ h: number,
331
+ ): void {
332
+ if (!this._insertNode) {
333
+ this._insertNode = this.sqlite!.prepare(
334
+ "INSERT INTO node (id, leaf, kids, h) VALUES (?, ?, ?, ?)",
335
+ );
336
+ }
337
+ this._insertNode.run(id, leaf, kids, h);
338
+ }
339
+
340
+ protected _dbGetNode(id: NodeId): NodeRec | null {
341
+ if (!this._selNode) {
342
+ this._selNode = this.sqlite!.prepare(
343
+ "SELECT id, leaf, kids FROM node WHERE id = ?",
344
+ );
345
+ }
346
+ const r = this._selNode.get(id) as
347
+ | { id: number; leaf: Uint8Array | null; kids: Uint8Array | null }
348
+ | undefined;
349
+ if (!r) return null;
350
+ // A zero-length kids blob marks a FLAT branch: the leaf column holds its
351
+ // bytes and the kid list is derived, one implicit leaf per byte.
352
+ const flat = r.kids !== null && r.kids.byteLength === 0;
353
+ return {
354
+ id: r.id,
355
+ leaf: r.leaf && !flat ? new Uint8Array(r.leaf) : null,
356
+ kids: flat
357
+ ? flatBytesKids(r.leaf!)
358
+ : (r.kids ? unpackKids(r.kids) : null),
359
+ };
360
+ }
361
+
362
+ protected _dbFindLeaf(
363
+ h: number,
364
+ bytes: Uint8Array,
365
+ ): NodeId | null {
366
+ if (!this._selLeaf) {
367
+ this._selLeaf = this.sqlite!.prepare(
368
+ "SELECT id FROM node WHERE h = ? AND leaf = ? AND kids IS NULL LIMIT 1",
369
+ );
370
+ }
371
+ const row = this._selLeaf.get(h, bytes) as { id: number } | undefined;
372
+ return row ? row.id : null;
373
+ }
374
+
375
+ protected _dbFindBranchByLeaf(
376
+ h: number,
377
+ bytes: Uint8Array,
378
+ ): NodeId | null {
379
+ if (!this._selFlat) {
380
+ this._selFlat = this.sqlite!.prepare(
381
+ "SELECT id FROM node WHERE h = ? AND leaf = ? AND kids IS NOT NULL LIMIT 1",
382
+ );
383
+ }
384
+ const row = this._selFlat.get(h, bytes) as { id: number } | undefined;
385
+ return row ? row.id : null;
386
+ }
387
+
388
+ protected _dbFindBranchByKids(
389
+ h: number,
390
+ packed: Uint8Array,
391
+ ): NodeId | null {
392
+ if (!this._selKids) {
393
+ this._selKids = this.sqlite!.prepare(
394
+ "SELECT id FROM node WHERE h = ? AND kids = ? LIMIT 1",
395
+ );
396
+ }
397
+ const row = this._selKids.get(h, packed) as { id: number } | undefined;
398
+ return row ? row.id : null;
399
+ }
400
+
401
+ // -- Kid (structural parent) edges --
402
+
403
+ protected _dbInsertKid(child: NodeId, parent: NodeId): void {
404
+ if (!this._insertKid) {
405
+ this._insertKid = this.sqlite!.prepare(
406
+ "INSERT OR IGNORE INTO kid (child, parent) VALUES (?, ?)",
407
+ );
408
+ }
409
+ this._insertKid.run(child, parent);
410
+ }
411
+
412
+ protected _dbGetParents(id: NodeId): NodeId[] {
413
+ if (!this._selParents) {
414
+ this._selParents = this.sqlite!.prepare(
415
+ "SELECT parent FROM kid WHERE child = ?",
416
+ );
417
+ }
418
+ return (this._selParents.all(id) as Array<{ parent: number }>).map((r) =>
419
+ r.parent
420
+ );
421
+ }
422
+
423
+ // -- Containment --
424
+
425
+ private _selParentsFirst: any = null;
426
+ protected _dbGetParentsFirst(id: NodeId, limit: number): NodeId[] {
427
+ if (!this._selParentsFirst) {
428
+ this._selParentsFirst = this.sqlite!.prepare(
429
+ "SELECT parent FROM kid WHERE child = ? LIMIT ?",
430
+ );
431
+ }
432
+ return (this._selParentsFirst.all(id, limit) as Array<{ parent: number }>)
433
+ .map((r) => r.parent);
434
+ }
435
+
436
+ private _selChain: any = null;
437
+
438
+ /** {@link Store.chainRun}'s walk as ONE recursive CTE: the whole
439
+ * transparent chain (no edge in or out, exactly one parent) is descended
440
+ * inside SQLite — per-node work is the same three indexed probes as the
441
+ * base class's loop, but without a JS↔SQLite round trip per node, which
442
+ * dominates on the deep single-structure scaffolding this read exists
443
+ * for. */
444
+ protected override _chainWalk(id: NodeId, cap: number): NodeId[] {
445
+ if (!this._selChain) {
446
+ this._selChain = this.sqlite!.prepare(
447
+ `WITH RECURSIVE chain(n, d) AS (
448
+ SELECT ?, 0
449
+ UNION ALL
450
+ SELECT (SELECT parent FROM kid WHERE child = chain.n LIMIT 1),
451
+ chain.d + 1
452
+ FROM chain
453
+ WHERE chain.d < ?
454
+ AND NOT EXISTS (SELECT 1 FROM edge WHERE src = chain.n)
455
+ AND NOT EXISTS (SELECT 1 FROM edge WHERE dst = chain.n)
456
+ AND (SELECT count(*)
457
+ FROM (SELECT 1 FROM kid WHERE child = chain.n LIMIT 2)
458
+ ) = 1
459
+ )
460
+ SELECT n FROM chain ORDER BY d`,
461
+ );
462
+ }
463
+ return (this._selChain.all(id, cap - 1) as Array<{ n: number }>)
464
+ .map((r) => r.n);
465
+ }
466
+
467
+ /** Width of the seq band inside the packed contain rowid: rowid =
468
+ * child·SEQ_SPAN + seq. Page sizes are geometric, so a chain of k live
469
+ * pages needs a base of ≥ 2^k · 4 bytes — seq stays ≤ ~50 for any
470
+ * physically possible list; the append path guards the band anyway. */
471
+ private static readonly SEQ_SPAN = 1 << 8;
472
+
473
+ private _selContainExists: any = null;
474
+ protected _dbContainExists(child: NodeId): boolean {
475
+ if (!this._selContainExists) {
476
+ // A probe of the child's rowid range — one descent.
477
+ this._selContainExists = this.sqlite!.prepare(
478
+ "SELECT 1 AS one FROM contain WHERE id >= ? AND id < ? LIMIT 1",
479
+ );
480
+ }
481
+ const base = child * SQliteStore.SEQ_SPAN;
482
+ return this._selContainExists.get(base, base + SQliteStore.SEQ_SPAN) !==
483
+ undefined;
484
+ }
485
+
486
+ private _selContainPages: any = null;
487
+ /** The child's page directory — (seq, byte length) per page, O(log fan-in)
488
+ * rows, each read from the cell header without loading the blob. */
489
+ private containPages(child: NodeId): Array<{ seq: number; len: number }> {
490
+ if (!this._selContainPages) {
491
+ this._selContainPages = this.sqlite!.prepare(
492
+ "SELECT id, length(parents) AS len FROM contain " +
493
+ "WHERE id >= ? AND id < ? ORDER BY id",
494
+ );
495
+ }
496
+ const base = child * SQliteStore.SEQ_SPAN;
497
+ return (this._selContainPages.all(
498
+ base,
499
+ base + SQliteStore.SEQ_SPAN,
500
+ ) as Array<{ id: number; len: number }>)
501
+ .map((r) => ({ seq: r.id - base, len: r.len }));
502
+ }
503
+
504
+ private _selContainPageSub: any = null;
505
+ private _selContainPage: any = null;
506
+ private containPage(child: NodeId, seq: number): Uint8Array {
507
+ if (!this._selContainPage) {
508
+ this._selContainPage = this.sqlite!.prepare(
509
+ "SELECT parents FROM contain WHERE id = ?",
510
+ );
511
+ }
512
+ return (this._selContainPage.get(child * SQliteStore.SEQ_SPAN + seq) as {
513
+ parents: Uint8Array;
514
+ }).parents;
515
+ }
516
+
517
+ protected _dbGetContainParentsSlice(
518
+ child: NodeId,
519
+ offset: number,
520
+ limit: number,
521
+ ): NodeId[] {
522
+ if (!this._selContainPageSub) {
523
+ // substr on a BLOB returns bytes: unpack only the requested span.
524
+ this._selContainPageSub = this.sqlite!.prepare(
525
+ "SELECT substr(parents, 1 + ? * 4, ? * 4) AS page " +
526
+ "FROM contain WHERE id = ?",
527
+ );
528
+ }
529
+ const out: NodeId[] = [];
530
+ let skip = offset;
531
+ for (const seg of this.containPages(child)) {
532
+ const n = seg.len >>> 2;
533
+ if (skip >= n) {
534
+ skip -= n;
535
+ continue;
536
+ }
537
+ const take = Math.min(limit - out.length, n - skip);
538
+ const row = this._selContainPageSub.get(
539
+ skip,
540
+ take,
541
+ child * SQliteStore.SEQ_SPAN + seg.seq,
542
+ ) as
543
+ | { page: Uint8Array }
544
+ | undefined;
545
+ if (row && row.page.length > 0) out.push(...unpackKids(row.page));
546
+ skip = 0;
547
+ if (out.length >= limit) break;
548
+ }
549
+ return out;
550
+ }
551
+
552
+ private _selContainCount: any = null;
553
+ protected _dbGetContainCount(child: NodeId): number {
554
+ if (!this._selContainCount) {
555
+ // Stored ENTRY count (transient duplicates included) — consistent with
556
+ // what the slice streams, which is all the seam math needs.
557
+ this._selContainCount = this.sqlite!.prepare(
558
+ "SELECT COALESCE(SUM(length(parents)), 0) / 4 AS n " +
559
+ "FROM contain WHERE id >= ? AND id < ?",
560
+ );
561
+ }
562
+ const base = child * SQliteStore.SEQ_SPAN;
563
+ return (this._selContainCount.get(base, base + SQliteStore.SEQ_SPAN) as {
564
+ n: number;
565
+ }).n;
566
+ }
567
+
568
+ protected _dbGetContainParents(child: NodeId): NodeId[] {
569
+ if (!this._selContain) {
570
+ this._selContain = this.sqlite!.prepare(
571
+ "SELECT parents FROM contain WHERE id >= ? AND id < ? ORDER BY id",
572
+ );
573
+ }
574
+ const base = child * SQliteStore.SEQ_SPAN;
575
+ const rows = this._selContain.all(
576
+ base,
577
+ base + SQliteStore.SEQ_SPAN,
578
+ ) as Array<{ parents: Uint8Array }>;
579
+ if (rows.length === 0) return [];
580
+ // Dedup across pages (a pair re-added after its page merged into the base
581
+ // may repeat in the tail until the next base merge), preserving order.
582
+ const seen = new Set<NodeId>();
583
+ const out: NodeId[] = [];
584
+ for (const r of rows) {
585
+ for (const p of unpackKids(r.parents)) {
586
+ if (!seen.has(p)) {
587
+ seen.add(p);
588
+ out.push(p);
589
+ }
590
+ }
591
+ }
592
+ return out;
593
+ }
594
+
595
+ private _upsContainPage: any = null;
596
+ private _delContainPage: any = null;
597
+ protected _dbAppendContain(child: NodeId, parents: NodeId[]): void {
598
+ if (parents.length === 0) return;
599
+ if (!this._upsContainPage) {
600
+ this._upsContainPage = this.sqlite!.prepare(
601
+ "INSERT INTO contain (id, parents) VALUES (?, ?) " +
602
+ "ON CONFLICT(id) DO UPDATE SET parents = excluded.parents",
603
+ );
604
+ this._delContainPage = this.sqlite!.prepare(
605
+ "DELETE FROM contain WHERE id = ?",
606
+ );
607
+ }
608
+ // Fold stored tail pages into the new page IN MEMORY, then write ONCE.
609
+ // Merge rule per fold: below a 256-byte floor always merge (most children
610
+ // have a handful of parents — two tiny rows would double their per-row
611
+ // overhead for no amortization benefit); above it, merge only while the
612
+ // stored page is ≤ 2× the accumulated one, which bounds total re-writing
613
+ // to O(fan-in · log fan-in) bytes and keeps the page directory
614
+ // logarithmic. A fold that reaches the seq-0 base page dedups, squeezing
615
+ // out duplicates on the same geometric schedule. Writing the fold's
616
+ // result as one upsert (an in-place row replace in the common
617
+ // small-child case) rather than delete+insert churn keeps B-tree leaf
618
+ // fill at the packed single-row format's level — measured: churn alone
619
+ // cost ~26% extra pages on the same payload.
620
+ const segs = this.containPages(child);
621
+ let cur = packKids(parents);
622
+ let seq = segs.length > 0 ? segs[segs.length - 1].seq + 1 : 0;
623
+ if (seq >= SQliteStore.SEQ_SPAN) {
624
+ throw new Error(`contain page seq overflow for child ${child}`);
625
+ }
626
+ const folded: number[] = [];
627
+ while (segs.length > 0) {
628
+ const a = segs[segs.length - 1];
629
+ if (a.len + cur.byteLength > 256 && a.len > 2 * cur.byteLength) break;
630
+ const pa = this.containPage(child, a.seq);
631
+ if (a.seq === 0) {
632
+ const seen = new Set<NodeId>();
633
+ const ids: NodeId[] = [];
634
+ for (const p of unpackKids(pa)) {
635
+ if (!seen.has(p)) {
636
+ seen.add(p);
637
+ ids.push(p);
638
+ }
639
+ }
640
+ for (const p of unpackKids(cur)) {
641
+ if (!seen.has(p)) {
642
+ seen.add(p);
643
+ ids.push(p);
644
+ }
645
+ }
646
+ cur = packKids(ids);
647
+ } else {
648
+ const m = new Uint8Array(pa.byteLength + cur.byteLength);
649
+ m.set(pa, 0);
650
+ m.set(cur, pa.byteLength);
651
+ cur = m;
652
+ }
653
+ folded.push(a.seq);
654
+ seq = a.seq;
655
+ segs.pop();
656
+ }
657
+ const base = child * SQliteStore.SEQ_SPAN;
658
+ for (const s of folded) {
659
+ if (s !== seq) this._delContainPage.run(base + s);
660
+ }
661
+ this._upsContainPage.run(base + seq, cur);
662
+ }
663
+
664
+ // -- Continuation edges --
665
+
666
+ protected _dbInsertEdge(src: NodeId, dst: NodeId): void {
667
+ if (!this._insertEdge) {
668
+ this._insertEdge = this.sqlite!.prepare(
669
+ "INSERT OR IGNORE INTO edge (src, dst) VALUES (?, ?)",
670
+ );
671
+ }
672
+ this._insertEdge.run(src, dst);
673
+ }
674
+
675
+ protected _dbGetNextEdges(id: NodeId): NodeId[] {
676
+ if (!this._selNext) {
677
+ this._selNext = this.sqlite!.prepare(
678
+ "SELECT dst FROM edge WHERE src = ? ORDER BY seq ASC",
679
+ );
680
+ }
681
+ return (this._selNext.all(id) as Array<{ dst: number }>).map((r) => r.dst);
682
+ }
683
+
684
+ protected _dbGetPrevEdges(id: NodeId): NodeId[] {
685
+ if (!this._selPrev) {
686
+ this._selPrev = this.sqlite!.prepare(
687
+ "SELECT src FROM edge WHERE dst = ? ORDER BY seq DESC",
688
+ );
689
+ }
690
+ return (this._selPrev.all(id) as Array<{ src: number }>).map((r) => r.src);
691
+ }
692
+
693
+ private _selNextFirst: any = null;
694
+ protected _dbGetNextEdgesFirst(id: NodeId, limit: number): NodeId[] {
695
+ if (!this._selNextFirst) {
696
+ this._selNextFirst = this.sqlite!.prepare(
697
+ "SELECT dst FROM edge WHERE src = ? ORDER BY seq ASC LIMIT ?",
698
+ );
699
+ }
700
+ return (this._selNextFirst.all(id, limit) as Array<{ dst: number }>)
701
+ .map((r) => r.dst);
702
+ }
703
+
704
+ private _selPrevFirst: any = null;
705
+ protected _dbGetPrevEdgesFirst(id: NodeId, limit: number): NodeId[] {
706
+ if (!this._selPrevFirst) {
707
+ this._selPrevFirst = this.sqlite!.prepare(
708
+ "SELECT src FROM edge WHERE dst = ? ORDER BY seq DESC LIMIT ?",
709
+ );
710
+ }
711
+ return (this._selPrevFirst.all(id, limit) as Array<{ src: number }>)
712
+ .map((r) => r.src);
713
+ }
714
+
715
+ private _selPrevCount: any = null;
716
+
717
+ /** {@link Store.prevCount} — one indexed COUNT over idx_edge_dst, never a
718
+ * row materialisation (a common continuation's reverse fan-in is
719
+ * corpus-sized). */
720
+ override prevCount(id: NodeId): number {
721
+ if (!this._selPrevCount) {
722
+ this._selPrevCount = this.sqlite!.prepare(
723
+ "SELECT COUNT(*) AS n FROM edge WHERE dst = ?",
724
+ );
725
+ }
726
+ return (this._selPrevCount.get(id) as { n: number }).n;
727
+ }
728
+
729
+ private _selSrcExists: any = null;
730
+
731
+ protected _dbEdgeSrcExists(src: NodeId): boolean {
732
+ if (!this._selSrcExists) {
733
+ // A prefix probe of idx_edge(src, dst) — one B-tree descent.
734
+ this._selSrcExists = this.sqlite!.prepare(
735
+ "SELECT 1 AS one FROM edge WHERE src = ? LIMIT 1",
736
+ );
737
+ }
738
+ return this._selSrcExists.get(src) !== undefined;
739
+ }
740
+
741
+ protected _dbEdgeDistinctSrcCount(): number {
742
+ return (
743
+ this.sqlite!.prepare(
744
+ "SELECT COUNT(*) AS n FROM (SELECT DISTINCT src FROM edge)",
745
+ ).get() as { n: number }
746
+ ).n;
747
+ }
748
+
749
+ // -- Halos (durable 2-bit quantized rows) --
750
+
751
+ protected _dbGetHalo(
752
+ id: NodeId,
753
+ ): { vec: Uint8Array; mass: number } | null {
754
+ if (!this._selHalo) {
755
+ this._selHalo = this.sqlite!.prepare(
756
+ "SELECT vec, mass FROM halo WHERE id = ?",
757
+ );
758
+ }
759
+ const r = this._selHalo.get(id) as
760
+ | { vec: Uint8Array; mass: number }
761
+ | undefined;
762
+ return r ?? null;
763
+ }
764
+
765
+ protected _dbUpsertHalo(
766
+ id: NodeId,
767
+ encodedVec: Uint8Array,
768
+ mass: number,
769
+ ): void {
770
+ if (!this._upHalo) {
771
+ this._upHalo = this.sqlite!.prepare(
772
+ "INSERT INTO halo (id, vec, mass) VALUES (?, ?, ?) " +
773
+ "ON CONFLICT(id) DO UPDATE SET vec = excluded.vec, mass = excluded.mass",
774
+ );
775
+ }
776
+ this._upHalo.run(id, encodedVec, mass);
777
+ }
778
+
779
+ // -- Meta --
780
+
781
+ protected _dbGetMeta(key: string): string | null {
782
+ if (!this._getMeta) {
783
+ this._getMeta = this.sqlite!.prepare(
784
+ "SELECT val FROM meta WHERE key = ?",
785
+ );
786
+ }
787
+ const row = this._getMeta.get(key) as { val: string } | undefined;
788
+ return row ? row.val : null;
789
+ }
790
+
791
+ protected _dbSetMeta(key: string, val: string): void {
792
+ if (!this._setMeta) {
793
+ this._setMeta = this.sqlite!.prepare(
794
+ "INSERT INTO meta (key, val) VALUES (?, ?) " +
795
+ "ON CONFLICT(key) DO UPDATE SET val = excluded.val",
796
+ );
797
+ }
798
+ this._setMeta.run(key, val);
799
+ }
800
+
801
+ protected _dbDeleteMeta(key: string): void {
802
+ if (!this._delMeta) {
803
+ this._delMeta = this.sqlite!.prepare(
804
+ "DELETE FROM meta WHERE key = ?",
805
+ );
806
+ }
807
+ this._delMeta.run(key);
808
+ }
809
+
810
+ // -- Snapshot --
811
+
812
+ protected _dbSaveSnapshot(bytes: Uint8Array): void {
813
+ if (!this._insSnapshot) {
814
+ this._insSnapshot = this.sqlite!.prepare(
815
+ "INSERT INTO snapshot (id, data) VALUES (1, ?) " +
816
+ "ON CONFLICT(id) DO UPDATE SET data = excluded.data",
817
+ );
818
+ }
819
+ this._insSnapshot.run(bytes);
820
+ }
821
+
822
+ protected _dbLoadSnapshot(): Uint8Array | null {
823
+ if (!this._selSnapshot) {
824
+ this._selSnapshot = this.sqlite!.prepare(
825
+ "SELECT data FROM snapshot WHERE id = 1",
826
+ );
827
+ }
828
+ const r = this._selSnapshot.get() as { data: Uint8Array } | undefined;
829
+ return r ? new Uint8Array(r.data) : null;
830
+ }
831
+
832
+ // -- Vector DB: content (gist) index --
833
+
834
+ protected _vecContentUpsert(
835
+ entries: Array<{ id: NodeId; vector: Float32Array; ef?: number }>,
836
+ ): void {
837
+ this.content!.upsertMany(entries);
838
+ }
839
+
840
+ protected _vecContentQuery(
841
+ v: Float32Array,
842
+ k: number,
843
+ ef: number,
844
+ ): Array<{ id: number; distance: number }> {
845
+ return this.content!.query(v, k, { ef });
846
+ }
847
+
848
+ protected _vecContentHas(id: NodeId): boolean {
849
+ return this.content ? this.content.has(id) : false;
850
+ }
851
+
852
+ protected _vecContentSize(): number {
853
+ return this.content ? this.content.size : 0;
854
+ }
855
+
856
+ protected _vecContentLastReads(): number {
857
+ return this.content ? this.content.lastQueryStorageReads : 0;
858
+ }
859
+
860
+ protected _vecContentPhysicalSize(): number {
861
+ return this.content ? this.content.physicalSize : 0;
862
+ }
863
+
864
+ protected _vecContentCompact(): void {
865
+ this.content!.compact();
866
+ }
867
+
868
+ protected _vecContentDeleteMany(ids: NodeId[]): void {
869
+ this.content!.deleteMany(ids);
870
+ }
871
+
872
+ protected *_vecContentEntriesSince(
873
+ after: number,
874
+ ): IterableIterator<{ ext: NodeId; internal: number }> {
875
+ if (this.content) yield* this.content.keysSince(after);
876
+ }
877
+
878
+ /** {@link AbstractStore._dbEdgeOrHaloIds} — one C-side scan; UNION dedups
879
+ * and (with the ORDER BY) sorts, so the result is ready for the binary-
880
+ * search membership probes and the ascending-id maintenance walks. */
881
+ protected _dbEdgeOrHaloIds(): NodeId[] {
882
+ const rows = this.sqlite!.prepare(
883
+ "SELECT src AS id FROM edge UNION SELECT dst FROM edge " +
884
+ "UNION SELECT id FROM halo ORDER BY 1",
885
+ ).all() as Array<{ id: number }>;
886
+ const out = new Array<NodeId>(rows.length);
887
+ for (let i = 0; i < rows.length; i++) out[i] = rows[i].id;
888
+ return out;
889
+ }
890
+
891
+ // -- Vector DB: halo index --
892
+
893
+ protected _vecHaloUpsert(
894
+ entries: Array<{ id: NodeId; vector: Float32Array }>,
895
+ ): void {
896
+ this.halos!.upsertMany(entries);
897
+ }
898
+
899
+ protected _vecHaloQuery(
900
+ v: Float32Array,
901
+ k: number,
902
+ ef: number,
903
+ ): Array<{ id: number; distance: number }> {
904
+ return this.halos!.query(v, k, { ef });
905
+ }
906
+
907
+ protected _vecHaloSize(): number {
908
+ return this.halos ? this.halos.size : 0;
909
+ }
910
+
911
+ protected _vecHaloPhysicalSize(): number {
912
+ return this.halos ? this.halos.physicalSize : 0;
913
+ }
914
+
915
+ protected _vecHaloCompact(): void {
916
+ this.halos!.compact();
917
+ }
918
+
919
+ /** Pre-fill both vector indices' RAM caches with sequential scans (up to
920
+ * their budget caps) — seconds of streaming instead of the minutes of
921
+ * random point reads a cold training/query session otherwise pays while
922
+ * warming. Optional; call once after open before sustained work.
923
+ * Returns rows warmed. */
924
+ async warmVectorCaches(): Promise<number> {
925
+ await this._ensureReady();
926
+ return (this.content?.warmCache() ?? 0) + (this.halos?.warmCache() ?? 0);
927
+ }
928
+ }