@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.
- package/AGENTS.md +470 -0
- package/AUTHORS.md +12 -0
- package/COMMERCIAL-LICENSE.md +19 -0
- package/CONTRIBUTING.md +15 -0
- package/HOW_IT_WORKS.md +4210 -0
- package/LICENSE.md +117 -0
- package/README.md +290 -0
- package/TRADEMARKS.md +19 -0
- package/example/demo.ts +46 -0
- package/example/train_base.ts +2675 -0
- package/index.html +893 -0
- package/package.json +25 -0
- package/src/alphabet.ts +34 -0
- package/src/alu/README.md +332 -0
- package/src/alu/src/alu.ts +541 -0
- package/src/alu/src/expr.ts +339 -0
- package/src/alu/src/index.ts +115 -0
- package/src/alu/src/kernel-arith.ts +377 -0
- package/src/alu/src/kernel-bits.ts +199 -0
- package/src/alu/src/kernel-logic.ts +102 -0
- package/src/alu/src/kernel-nd.ts +235 -0
- package/src/alu/src/kernel-numeric.ts +466 -0
- package/src/alu/src/operation.ts +344 -0
- package/src/alu/src/parser.ts +574 -0
- package/src/alu/src/resonance.ts +161 -0
- package/src/alu/src/text.ts +83 -0
- package/src/alu/src/value.ts +327 -0
- package/src/alu/test/alu.test.ts +1004 -0
- package/src/bytes.ts +62 -0
- package/src/config.ts +227 -0
- package/src/derive/README.md +295 -0
- package/src/derive/src/deduction.ts +263 -0
- package/src/derive/src/index.ts +24 -0
- package/src/derive/src/priority-queue.ts +70 -0
- package/src/derive/src/rewrite.ts +127 -0
- package/src/derive/src/trie.ts +242 -0
- package/src/derive/test/derive.test.ts +137 -0
- package/src/extension.ts +42 -0
- package/src/geometry.ts +511 -0
- package/src/index.ts +38 -0
- package/src/ingest-cache.ts +224 -0
- package/src/mind/articulation.ts +137 -0
- package/src/mind/attention.ts +1061 -0
- package/src/mind/canonical.ts +107 -0
- package/src/mind/graph-search.ts +1100 -0
- package/src/mind/index.ts +18 -0
- package/src/mind/junction.ts +347 -0
- package/src/mind/learning.ts +246 -0
- package/src/mind/match.ts +507 -0
- package/src/mind/mechanisms/alu.ts +33 -0
- package/src/mind/mechanisms/cast.ts +568 -0
- package/src/mind/mechanisms/confluence.ts +268 -0
- package/src/mind/mechanisms/cover.ts +248 -0
- package/src/mind/mechanisms/extraction.ts +422 -0
- package/src/mind/mechanisms/recall.ts +241 -0
- package/src/mind/mind.ts +483 -0
- package/src/mind/pipeline-mechanism.ts +326 -0
- package/src/mind/pipeline.ts +300 -0
- package/src/mind/primitives.ts +201 -0
- package/src/mind/rationale.ts +275 -0
- package/src/mind/reasoning.ts +198 -0
- package/src/mind/recognition.ts +234 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/trace.ts +108 -0
- package/src/mind/traverse.ts +556 -0
- package/src/mind/types.ts +281 -0
- package/src/rabitq-hnsw/README.md +303 -0
- package/src/rabitq-hnsw/src/database.ts +492 -0
- package/src/rabitq-hnsw/src/heap.ts +90 -0
- package/src/rabitq-hnsw/src/hnsw.ts +514 -0
- package/src/rabitq-hnsw/src/index.ts +15 -0
- package/src/rabitq-hnsw/src/prng.ts +39 -0
- package/src/rabitq-hnsw/src/rabitq.ts +308 -0
- package/src/rabitq-hnsw/src/store.ts +994 -0
- package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
- package/src/store-sqlite.ts +928 -0
- package/src/store.ts +2148 -0
- package/src/vec.ts +124 -0
- package/test/00-extract.test.mjs +151 -0
- package/test/01-floor.test.mjs +20 -0
- package/test/02-roundtrip.test.mjs +83 -0
- package/test/03-recall.test.mjs +98 -0
- package/test/04-think.test.mjs +627 -0
- package/test/05-concepts.test.mjs +84 -0
- package/test/08-storage.test.mjs +948 -0
- package/test/09-edges.test.mjs +65 -0
- package/test/11-universality.test.mjs +180 -0
- package/test/13-conversation.test.mjs +228 -0
- package/test/14-scaling.test.mjs +503 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/16-bridge.test.mjs +250 -0
- package/test/17-intelligence.test.mjs +240 -0
- package/test/18-alu.test.mjs +209 -0
- package/test/19-nd.test.mjs +254 -0
- package/test/20-stability.test.mjs +489 -0
- package/test/21-partial.test.mjs +158 -0
- package/test/22-multihop.test.mjs +130 -0
- package/test/23-rationale.test.mjs +316 -0
- package/test/24-generalization.test.mjs +416 -0
- package/test/25-embedding.test.mjs +75 -0
- package/test/26-cooperative.test.mjs +89 -0
- package/test/27-saturation-drop.test.mjs +637 -0
- package/test/28-unknowable.test.mjs +88 -0
- package/test/29-counterfactual.test.mjs +476 -0
- package/test/30-conflict-resolution.test.mjs +166 -0
- package/test/31-audit.test.mjs +288 -0
- package/test/32-confluence.test.mjs +173 -0
- package/test/33-multi-candidate.test.mjs +222 -0
- package/test/34-cross-region.test.mjs +252 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
// 08-storage.test.mjs — the storage layer (SQliteStore), end to end.
|
|
2
|
+
//
|
|
3
|
+
// SQliteStore is the whole persistence system. It is THREE independent SQLite
|
|
4
|
+
// databases working as one store, plus the write discipline that keeps them
|
|
5
|
+
// consistent:
|
|
6
|
+
//
|
|
7
|
+
// • <path>.sqlite — the content-addressed DAG: node rows (leaf|kids),
|
|
8
|
+
// the reverse child→parent index, continuation edges,
|
|
9
|
+
// halo accumulators, metadata, the snapshot blob.
|
|
10
|
+
// • <path>.content.vec — the STORAGE vector index over node gists (the
|
|
11
|
+
// resonant lookup that finds a node by meaning).
|
|
12
|
+
// • <path>.halo.vec — the CACHE vector index over halos (concept siblings).
|
|
13
|
+
//
|
|
14
|
+
// This file verifies EACH responsibility the store owns, and that the three
|
|
15
|
+
// pieces are wired together consistently:
|
|
16
|
+
//
|
|
17
|
+
// 1. Construction — the three databases are created and set up.
|
|
18
|
+
// 2. The DAG — hash-consed leaves/branches, dense ids, reverse
|
|
19
|
+
// parents, byte reconstruction.
|
|
20
|
+
// 3. Identity rules — exact dedup (everywhere), near dedup (branches
|
|
21
|
+
// ONLY), leaves never near-merge.
|
|
22
|
+
// 4. Gist index — a node is findable by its own meaning (resonate).
|
|
23
|
+
// 5. Continuation edges— link / next / prev / edgeSourceCount, order & dedup.
|
|
24
|
+
// 6. Halos — pour accumulates, mass gates visibility, the halo
|
|
25
|
+
// index resonates concept siblings, geometric re-index.
|
|
26
|
+
// 7. Metadata & snapshot — key/value provenance and the config blob.
|
|
27
|
+
// 8. Write discipline — deferred transaction, commit(), reads see uncommitted.
|
|
28
|
+
// 9. Durability — close, reopen the SAME path, and EVERYTHING (DAG,
|
|
29
|
+
// edges, halos, gist index, meta) survives a cold cache.
|
|
30
|
+
// 10. Mind round-trip — train → save → reopen → loadFromStore answers exactly.
|
|
31
|
+
// 11. Compression — dedup keeps the store O(distinct chunks): identical
|
|
32
|
+
// content costs nothing, repeats add zero entries, and
|
|
33
|
+
// unique content scales with chunks, not deposits.
|
|
34
|
+
// 12. Concept formation — incremental halo-driven cross-name transfer, training
|
|
35
|
+
// idempotency, deterministic conflict resolution.
|
|
36
|
+
//
|
|
37
|
+
// (This file is the single home for storage correctness; it absorbs the old
|
|
38
|
+
// 06-sleep and 12-compression suites, whose concerns are the store's.)
|
|
39
|
+
//
|
|
40
|
+
// Tests that touch disk use a unique temp stem and clean up all three files.
|
|
41
|
+
|
|
42
|
+
import { test } from "node:test";
|
|
43
|
+
import assert from "node:assert/strict";
|
|
44
|
+
import { Mind } from "../dist/src/index.js";
|
|
45
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
46
|
+
import { existsSync, rmSync, statSync } from "node:fs";
|
|
47
|
+
import { normalize, randomUnit, rng } from "../dist/src/vec.js";
|
|
48
|
+
|
|
49
|
+
const D = 1024;
|
|
50
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
51
|
+
const dec = (b) => new TextDecoder().decode(b);
|
|
52
|
+
|
|
53
|
+
// A fresh in-memory store — D flows through the constructor (single source of truth).
|
|
54
|
+
function memStore() {
|
|
55
|
+
return new SQliteStore({ path: ":memory:", D });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Deterministic unit-vector source, so gists are reproducible per test.
|
|
59
|
+
function vecs(seed) {
|
|
60
|
+
const r = rng(seed);
|
|
61
|
+
return () => randomUnit(D, r);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The interned node id of a string, recovered the way recognition does: perceive
|
|
65
|
+
// the bytes and fold the tree bottom-up through the store's content-addressed
|
|
66
|
+
// maps (findLeaf at leaves, findBranch above). Returns null if any part is
|
|
67
|
+
// unknown (the content was never interned).
|
|
68
|
+
function foldToId(mind, store, text) {
|
|
69
|
+
const fold = (n) => {
|
|
70
|
+
if (n.kids === null) return store.findLeaf(n.leaf ?? new Uint8Array(0));
|
|
71
|
+
const kids = [];
|
|
72
|
+
for (const k of n.kids) {
|
|
73
|
+
const id = fold(k);
|
|
74
|
+
if (id === null) return null;
|
|
75
|
+
kids.push(id);
|
|
76
|
+
}
|
|
77
|
+
return store.findBranch(kids);
|
|
78
|
+
};
|
|
79
|
+
return fold(mind.perceive(text));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A unique on-disk stem + a cleanup that removes all three files.
|
|
83
|
+
function tmpStem(tag) {
|
|
84
|
+
return `/tmp/sema-storage-${tag}-${process.pid}-${
|
|
85
|
+
Math.floor(performance.now())
|
|
86
|
+
}`;
|
|
87
|
+
}
|
|
88
|
+
function cleanup(stem) {
|
|
89
|
+
for (const ext of [".sqlite", ".content.vec", ".halo.vec"]) {
|
|
90
|
+
try {
|
|
91
|
+
rmSync(stem + ext);
|
|
92
|
+
} catch { /* may not exist */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =========================================================================
|
|
97
|
+
// 1 — CONSTRUCTION: the three databases are created and set up
|
|
98
|
+
// =========================================================================
|
|
99
|
+
|
|
100
|
+
test("store opens with dimension from constructor", async () => {
|
|
101
|
+
const s = new SQliteStore({ path: ":memory:", D });
|
|
102
|
+
const id = await s.putLeaf(enc("x"), normalize(randomUnit(D, rng(0))));
|
|
103
|
+
assert.ok(typeof id === "number");
|
|
104
|
+
assert.ok(s.get(id) !== null);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("an on-disk store creates all three SQLite files", async () => {
|
|
108
|
+
const stem = tmpStem("files");
|
|
109
|
+
try {
|
|
110
|
+
const s = new SQliteStore({ path: stem, D });
|
|
111
|
+
const next = vecs(1);
|
|
112
|
+
await s.putLeaf(enc("ab"), next());
|
|
113
|
+
s.commit(); // make node + vector writes durable
|
|
114
|
+
assert.ok(existsSync(`${stem}.sqlite`), "main DAG file exists");
|
|
115
|
+
assert.ok(
|
|
116
|
+
existsSync(`${stem}.content.vec`),
|
|
117
|
+
"content (gist) vector file exists",
|
|
118
|
+
);
|
|
119
|
+
assert.ok(existsSync(`${stem}.halo.vec`), "halo vector file exists");
|
|
120
|
+
await s.close();
|
|
121
|
+
} finally {
|
|
122
|
+
cleanup(stem);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// =========================================================================
|
|
127
|
+
// 2 — THE DAG: hash-consing, dense ids, reverse parents, byte reconstruction
|
|
128
|
+
// =========================================================================
|
|
129
|
+
|
|
130
|
+
test("leaves and branches intern into a dense, reconstructable DAG", async () => {
|
|
131
|
+
const s = memStore();
|
|
132
|
+
const next = vecs(2);
|
|
133
|
+
const a = await s.putLeaf(enc("aa"), next());
|
|
134
|
+
const b = await s.putLeaf(enc("bb"), next());
|
|
135
|
+
const br = await s.putBranch([a, b], next());
|
|
136
|
+
|
|
137
|
+
// Dense, creation-order ids.
|
|
138
|
+
assert.deepEqual([a, b, br], [0, 1, 2]);
|
|
139
|
+
assert.equal(s.nodeCount(), 3);
|
|
140
|
+
assert.equal(await s.size(), 3);
|
|
141
|
+
|
|
142
|
+
// The branch record carries its ordered children; leaves carry bytes.
|
|
143
|
+
assert.deepEqual(s.get(br).kids, [a, b]);
|
|
144
|
+
assert.equal(s.get(br).leaf, null);
|
|
145
|
+
assert.equal(dec(s.get(a).leaf), "aa");
|
|
146
|
+
|
|
147
|
+
// Content-addressed lookups recover a node from its content alone — the
|
|
148
|
+
// hash-cons keys the store is built on.
|
|
149
|
+
assert.equal(
|
|
150
|
+
s.findLeaf(enc("aa")),
|
|
151
|
+
a,
|
|
152
|
+
"findLeaf recovers a leaf by its bytes",
|
|
153
|
+
);
|
|
154
|
+
assert.equal(
|
|
155
|
+
s.findBranch([a, b]),
|
|
156
|
+
br,
|
|
157
|
+
"findBranch recovers a branch by its child ids",
|
|
158
|
+
);
|
|
159
|
+
assert.equal(s.findLeaf(enc("zz")), null, "an unknown leaf is not found");
|
|
160
|
+
assert.equal(
|
|
161
|
+
s.findBranch([b, a]),
|
|
162
|
+
null,
|
|
163
|
+
"child ORDER is part of the branch key",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// has() is an O(1) range check over the dense id space.
|
|
167
|
+
assert.ok(s.has(a) && s.has(br));
|
|
168
|
+
// Single-byte leaves are implicit (negative IDs -1..-256), so has(-1)
|
|
169
|
+
// is true for byte 0x00. Out-of-range negative IDs still return false.
|
|
170
|
+
assert.ok(!s.has(99) && s.has(-1) && !s.has(-257));
|
|
171
|
+
|
|
172
|
+
// Bytes reconstruct bottom-up; the prefix walk stops early.
|
|
173
|
+
assert.equal(dec(s.bytes(br)), "aabb");
|
|
174
|
+
assert.equal(dec(s.bytesPrefix(br, 2)), "aa");
|
|
175
|
+
assert.equal(dec(s.bytesPrefix(br, 99)), "aabb");
|
|
176
|
+
await s.close();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("reverse parents index points each child at the branch that contains it", async () => {
|
|
180
|
+
const s = memStore();
|
|
181
|
+
const next = vecs(3);
|
|
182
|
+
const a = await s.putLeaf(enc("aa"), next());
|
|
183
|
+
const b = await s.putLeaf(enc("bb"), next());
|
|
184
|
+
const br = await s.putBranch([a, b], next());
|
|
185
|
+
|
|
186
|
+
assert.deepEqual(s.parents(a).sort(), [br]);
|
|
187
|
+
assert.deepEqual(s.parents(b).sort(), [br]);
|
|
188
|
+
assert.deepEqual(s.parents(br), [], "the root has no parent");
|
|
189
|
+
|
|
190
|
+
// A child repeated within one branch links to that parent exactly once.
|
|
191
|
+
const rep = await s.putBranch([a, a], next());
|
|
192
|
+
assert.equal(
|
|
193
|
+
s.parents(a).filter((p) => p === rep).length,
|
|
194
|
+
1,
|
|
195
|
+
"no duplicate parent edge",
|
|
196
|
+
);
|
|
197
|
+
await s.close();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// =========================================================================
|
|
201
|
+
// 3 — IDENTITY RULES: exact dedup everywhere; near dedup for BRANCHES only
|
|
202
|
+
// =========================================================================
|
|
203
|
+
|
|
204
|
+
test("exact content is interned once (hash-consing)", async () => {
|
|
205
|
+
const s = memStore();
|
|
206
|
+
const next = vecs(4);
|
|
207
|
+
const a = await s.putLeaf(enc("hi"), next());
|
|
208
|
+
const again = await s.putLeaf(enc("hi"), next()); // same bytes, new gist
|
|
209
|
+
assert.equal(again, a, "identical leaf bytes → the same id");
|
|
210
|
+
|
|
211
|
+
const x = await s.putLeaf(enc("x"), next());
|
|
212
|
+
const y = await s.putLeaf(enc("y"), next());
|
|
213
|
+
const br = await s.putBranch([x, y], next());
|
|
214
|
+
const br2 = await s.putBranch([x, y], next()); // same kids, new gist
|
|
215
|
+
assert.equal(br2, br, "identical child signature → the same branch id");
|
|
216
|
+
// Single-byte leaves ("x", "y") are implicit (negative ids, no DB row).
|
|
217
|
+
// Only the multi-byte leaf ("hi") and the branch occupy DB rows.
|
|
218
|
+
assert.equal(s.nodeCount(), 2, "no duplicate nodes minted");
|
|
219
|
+
await s.close();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("branches merge via near dedup on near gist — against indexed TARGETS; leaves NEVER do", async () => {
|
|
223
|
+
const s = memStore();
|
|
224
|
+
const next = vecs(5);
|
|
225
|
+
const a = await s.putLeaf(enc("p"), next());
|
|
226
|
+
const b = await s.putLeaf(enc("q"), next());
|
|
227
|
+
const c = await s.putLeaf(enc("r"), next());
|
|
228
|
+
|
|
229
|
+
// Near dedup collapses a fresh branch onto a near-gist one — but only onto
|
|
230
|
+
// a node the content index actually holds, i.e. one that has become a
|
|
231
|
+
// resonance TARGET (edge/halo-bearing). This keeps the merge candidate set the
|
|
232
|
+
// small meaningful set (cheap, and it never folds a fresh branch onto a random
|
|
233
|
+
// intermediate one). So: index the first branch by giving it an edge, THEN a
|
|
234
|
+
// near-gist branch merges onto it.
|
|
235
|
+
const g = normalize(next());
|
|
236
|
+
const gNear = normalize(g.map((x, i) => x + (i === 0 ? 1e-4 : 0)));
|
|
237
|
+
const br1 = await s.putBranch([a, b], g);
|
|
238
|
+
await s.link(br1, a); // br1 is now a resonance target → indexed
|
|
239
|
+
const br2 = await s.putBranch([a, c], gNear);
|
|
240
|
+
assert.equal(
|
|
241
|
+
br2,
|
|
242
|
+
br1,
|
|
243
|
+
"near-gist branch merges onto the indexed target (1 - 1/√D)",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// A fresh branch with NO indexed near-neighbour does not merge onto an
|
|
247
|
+
// unindexed intermediate one — it is kept distinct (correct: the structural
|
|
248
|
+
// climb needs the distinct node), costing only a SQLite row, no index slot.
|
|
249
|
+
const d = await s.putLeaf(enc("s"), next());
|
|
250
|
+
const e = await s.putLeaf(enc("t"), next());
|
|
251
|
+
const brX = await s.putBranch([d, e], normalize(next())); // unindexed
|
|
252
|
+
const brY = await s.putBranch([d, a], normalize(next().map((x, i) => x))); // distinct kids
|
|
253
|
+
assert.notEqual(brY, brX, "no merge onto an unindexed intermediate branch");
|
|
254
|
+
|
|
255
|
+
// Leaves never near-merge: distinct bytes stay distinct even with an
|
|
256
|
+
// identical gist — near-merging a leaf would corrupt its bytes.
|
|
257
|
+
const L1 = await s.putLeaf(enc("mm"), g);
|
|
258
|
+
const L2 = await s.putLeaf(enc("nn"), g); // different bytes, SAME gist
|
|
259
|
+
assert.notEqual(L2, L1, "distinct leaf bytes never merge, whatever the gist");
|
|
260
|
+
await s.close();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// =========================================================================
|
|
264
|
+
// 4 — GIST INDEX: a node is findable by its own meaning
|
|
265
|
+
// =========================================================================
|
|
266
|
+
|
|
267
|
+
test("a node resonates to itself once it is a resonance TARGET", async () => {
|
|
268
|
+
const s = memStore();
|
|
269
|
+
const next = vecs(6);
|
|
270
|
+
const v = normalize(next());
|
|
271
|
+
const id = await s.putLeaf(enc("alpha"), v);
|
|
272
|
+
const distractor = await s.putLeaf(enc("beta"), next());
|
|
273
|
+
|
|
274
|
+
// The content (gist) index is a RESONANCE-TARGET index, not a node mirror: a
|
|
275
|
+
// node's gist enters it LAZILY, the first time the node becomes something
|
|
276
|
+
// recall can ground on — it gains a continuation edge or a halo. A bare,
|
|
277
|
+
// never-linked node is intentionally NOT indexed (it is reached only by the
|
|
278
|
+
// structural DAG climb, never by direct resonance), so it costs no index slot.
|
|
279
|
+
// This is the store's core compression: the exploded intermediate DAG (~99.5%
|
|
280
|
+
// of nodes) is never poured into the HNSW. So before any edge/halo, resonate
|
|
281
|
+
// finds nothing.
|
|
282
|
+
assert.equal(
|
|
283
|
+
(await s.resonate(v, 1)).length,
|
|
284
|
+
0,
|
|
285
|
+
"a bare, never-grounded node is not yet a resonance target",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Give the node a continuation edge — now it is a target, and resonates to
|
|
289
|
+
// itself. (link indexes BOTH endpoints, so the distractor is indexed too.)
|
|
290
|
+
await s.link(id, distractor);
|
|
291
|
+
const hits = await s.resonate(v, 1);
|
|
292
|
+
assert.equal(
|
|
293
|
+
hits[0].id,
|
|
294
|
+
id,
|
|
295
|
+
"once edge-bearing, the nearest gist to v is the node stored with v",
|
|
296
|
+
);
|
|
297
|
+
// The 1-bit RaBitQ codec estimates cosine from sign-bit Hamming distance, so
|
|
298
|
+
// even self-identity tops out around ~0.98, not exactly 1 — but it dominates
|
|
299
|
+
// any unrelated node (whose score is ~0). The contract is "found as nearest,
|
|
300
|
+
// with a decisively high score", not bit-exact reconstruction.
|
|
301
|
+
assert.ok(
|
|
302
|
+
hits[0].score >= 0.9,
|
|
303
|
+
`identity resonance is high (got ${hits[0].score.toFixed(3)})`,
|
|
304
|
+
);
|
|
305
|
+
assert.equal((await s.resonate(v, 0)).length, 0, "k=0 returns nothing");
|
|
306
|
+
await s.close();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// =========================================================================
|
|
310
|
+
// 5 — CONTINUATION EDGES: link / next / prev / edgeSourceCount
|
|
311
|
+
// =========================================================================
|
|
312
|
+
|
|
313
|
+
test("edges record what follows what, ordered and de-duplicated", async () => {
|
|
314
|
+
const s = memStore();
|
|
315
|
+
const next = vecs(7);
|
|
316
|
+
const a = await s.putLeaf(enc("a"), next());
|
|
317
|
+
const b = await s.putLeaf(enc("b"), next());
|
|
318
|
+
const c = await s.putLeaf(enc("c"), next());
|
|
319
|
+
|
|
320
|
+
await s.link(a, b);
|
|
321
|
+
await s.link(a, c);
|
|
322
|
+
// next() is in insertion (seq) order; prev() is the reverse relation.
|
|
323
|
+
assert.deepEqual(s.next(a), [b, c]);
|
|
324
|
+
assert.deepEqual(s.prev(b), [a]);
|
|
325
|
+
assert.deepEqual(s.prev(c), [a]);
|
|
326
|
+
assert.deepEqual(s.next(b), [], "a node with no continuation");
|
|
327
|
+
|
|
328
|
+
// link is idempotent — relearning a fact adds no edge.
|
|
329
|
+
await s.link(a, b);
|
|
330
|
+
assert.deepEqual(s.next(a), [b, c], "duplicate link ignored");
|
|
331
|
+
|
|
332
|
+
// edgeSourceCount = distinct edge SOURCES (the learnt-context count).
|
|
333
|
+
assert.equal(s.edgeSourceCount(), 1, "only `a` bears an outgoing edge");
|
|
334
|
+
await s.link(b, c);
|
|
335
|
+
assert.equal(s.edgeSourceCount(), 2, "now a and b both bear one");
|
|
336
|
+
await s.close();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// =========================================================================
|
|
340
|
+
// 6 — HALOS: accumulation, mass gating, concept-sibling resonance
|
|
341
|
+
// =========================================================================
|
|
342
|
+
|
|
343
|
+
test("a halo accumulates poured signatures and gates on mass", async () => {
|
|
344
|
+
const s = memStore();
|
|
345
|
+
const next = vecs(8);
|
|
346
|
+
const id = await s.putLeaf(enc("ice"), next());
|
|
347
|
+
assert.equal(s.halo(id), null, "a fresh node has no halo");
|
|
348
|
+
|
|
349
|
+
// Default minHaloMass is 1, so one pour already makes the halo visible.
|
|
350
|
+
await s.pourHalo(id, normalize(next()));
|
|
351
|
+
const h1 = s.halo(id);
|
|
352
|
+
assert.ok(h1 !== null, "halo present after a pour");
|
|
353
|
+
assert.equal(h1.length, D, "halo is a D-vector");
|
|
354
|
+
|
|
355
|
+
// Accumulation: pouring a partner moves the halo's direction toward the
|
|
356
|
+
// running superposition (it is not simply overwritten).
|
|
357
|
+
await s.pourHalo(id, normalize(next()));
|
|
358
|
+
const h2 = s.halo(id);
|
|
359
|
+
assert.ok(h2 !== null);
|
|
360
|
+
await s.close();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// A multi-turn conversation is deposited as ACCUMULATED-CONTEXT episodes — the
|
|
364
|
+
// pattern HOW_IT_WORKS §19a prescribes and example/train.ts uses:
|
|
365
|
+
// (t0) → t1
|
|
366
|
+
// (t0 + t1) → t2
|
|
367
|
+
// (t0 + t1 + t2) → t3
|
|
368
|
+
// The EDGE must run from the full accumulated context, because that is what
|
|
369
|
+
// disambiguates two conversations sharing a pivot turn (the 13-conversation
|
|
370
|
+
// suite). But the HALO is a different relation: it records distributional
|
|
371
|
+
// COMPANY for synonymy. Reinforcing it on every call against the WHOLE
|
|
372
|
+
// accumulated context pours a SHARED PREFIX's gist (a system preamble, an early
|
|
373
|
+
// turn) into a halo again and again — once per later turn of every conversation
|
|
374
|
+
// that quotes it. That shared prefix is a recurring node, so its halo mass piles
|
|
375
|
+
// up without bound as the corpus grows, swamping the genuine company it kept and
|
|
376
|
+
// polluting concept/synonym recall. That is the "edges and halos are reinforced
|
|
377
|
+
// on every call" behaviour applied where it must NOT be.
|
|
378
|
+
//
|
|
379
|
+
// The fix reinforces the halo on the CHANGED part — what THIS deposit added over
|
|
380
|
+
// the previous one (found by diffing the perceived trees through interned ids, so
|
|
381
|
+
// it is modality-agnostic and separator-free) — not the whole accumulated blob.
|
|
382
|
+
//
|
|
383
|
+
// Contract (behavioural, store-level): a recurring shared prefix must NOT
|
|
384
|
+
// accumulate halo mass that grows with the number of turns/conversations quoting
|
|
385
|
+
// it; its mass stays a small constant. The genuinely-new turns still get a halo,
|
|
386
|
+
// so synonymy/recall is preserved.
|
|
387
|
+
test("multi-turn accumulated context does not pile halo mass on a recurring shared prefix", async () => {
|
|
388
|
+
const m = new Mind({
|
|
389
|
+
D,
|
|
390
|
+
seed: 7,
|
|
391
|
+
});
|
|
392
|
+
const s = m.store;
|
|
393
|
+
|
|
394
|
+
// A shared system preamble in front of many GENUINELY-DISTINCT conversations —
|
|
395
|
+
// the exact shape of real instruction-tuning data. Each conversation is
|
|
396
|
+
// deposited as accumulated-context episodes, so the preamble is quoted by every
|
|
397
|
+
// later turn. The preamble is always the FIRST line and never a standalone
|
|
398
|
+
// context — the first episode's context is already preamble + first user turn,
|
|
399
|
+
// so the preamble only ever appears as a PREFIX of a longer accumulated context
|
|
400
|
+
// (its gist is what gets re-poured on every call in the buggy behaviour).
|
|
401
|
+
//
|
|
402
|
+
// The conversations are about DIFFERENT subjects, each with its own distinctive
|
|
403
|
+
// vocabulary, on purpose: this isolates the property under test — that a
|
|
404
|
+
// recurring shared PREFIX does not accumulate halo mass — from the orthogonal
|
|
405
|
+
// near-dedup behaviour. If the conversations were near-identical (e.g. only
|
|
406
|
+
// an index swapped: "conversation 1", "conversation 2", …) their gists would
|
|
407
|
+
// resonate above the merge threshold and fold into ONE shared node, which would
|
|
408
|
+
// then legitimately keep company with every conversation it now represents —
|
|
409
|
+
// mass growing with the merged count is correct compression, NOT prefix
|
|
410
|
+
// re-pouring, but it would confound this test. Distinct subjects keep the
|
|
411
|
+
// conversations un-merged, so any mass beyond a small constant could ONLY come
|
|
412
|
+
// from re-pouring the shared prefix — exactly the regression guarded here.
|
|
413
|
+
const SYS =
|
|
414
|
+
"you are a careful assistant and you never use any external tools";
|
|
415
|
+
const TOPICS = [
|
|
416
|
+
["gardening", "compost", "tomatoes", "mulch"],
|
|
417
|
+
["astronomy", "nebula", "telescope", "redshift"],
|
|
418
|
+
["cooking", "risotto", "saffron", "simmer"],
|
|
419
|
+
["finance", "dividend", "portfolio", "hedge"],
|
|
420
|
+
["music", "counterpoint", "fugue", "cadence"],
|
|
421
|
+
["geology", "basalt", "tectonic", "sediment"],
|
|
422
|
+
["medicine", "antibody", "plasma", "vaccine"],
|
|
423
|
+
["law", "tort", "statute", "liability"],
|
|
424
|
+
];
|
|
425
|
+
const CONVOS = TOPICS.length;
|
|
426
|
+
for (let c = 0; c < CONVOS; c++) {
|
|
427
|
+
const [a, b, d, e] = TOPICS[c];
|
|
428
|
+
const turns = [
|
|
429
|
+
`${SYS}\nthe user asks about ${a} and especially ${b}`,
|
|
430
|
+
`the assistant explains ${a}: ${b} relates to ${d} in practice`,
|
|
431
|
+
`the user follows up wondering how ${d} affects ${e}`,
|
|
432
|
+
`the assistant concludes that ${e} completes the ${a} picture`,
|
|
433
|
+
];
|
|
434
|
+
for (let i = 0; i + 1 < turns.length; i++) {
|
|
435
|
+
await m.ingest(turns.slice(0, i + 1).join("\n"), turns[i + 1]);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// The shared preamble recurs as the prefix of CONVOS×3 accumulated contexts
|
|
440
|
+
// (4 turns ⇒ 3 ctx→cont episodes per conversation). The harm is a SHARED node
|
|
441
|
+
// accumulating halo mass proportional to how many turns/conversations quote it.
|
|
442
|
+
// So scan every node and take the maximum halo mass: reinforcing on the whole
|
|
443
|
+
// accumulated context would pour the preamble's shared sub-nodes once per
|
|
444
|
+
// quoting turn, driving some shared node's mass toward CONVOS×3. Reinforcing on
|
|
445
|
+
// the changed part keeps every node's mass a small constant — bounded by how
|
|
446
|
+
// many DISTINCT episodes genuinely introduced it, not by the corpus size.
|
|
447
|
+
let maxMass = 0, maxId = -1;
|
|
448
|
+
for (let id = 0; id < s.nodeCount(); id++) {
|
|
449
|
+
const mass = s.haloMass(id);
|
|
450
|
+
if (mass > maxMass) {
|
|
451
|
+
maxMass = mass;
|
|
452
|
+
maxId = id;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const maxBytes = maxId >= 0 ? dec(s.bytesPrefix(maxId, 50)) : "";
|
|
456
|
+
console.log(
|
|
457
|
+
` max halo mass after ${CONVOS} conversations (${
|
|
458
|
+
CONVOS * 3
|
|
459
|
+
} episodes): ` +
|
|
460
|
+
`${maxMass} on "${maxBytes.replace(/\n/g, "|")}"`,
|
|
461
|
+
);
|
|
462
|
+
// The conversations are distinct (no near dedup concentrates company), so
|
|
463
|
+
// the changed-part pour gives every node mass ≈ 1 — each distinct context is a
|
|
464
|
+
// resonance target poured once. A tight bound of 2 absorbs at most one
|
|
465
|
+
// incidental near-merge while still being far below the O(CONVOS×3) a re-poured
|
|
466
|
+
// shared prefix would reach: it catches the "reinforced on every call"
|
|
467
|
+
// regression squarely.
|
|
468
|
+
assert.ok(
|
|
469
|
+
maxMass <= 2,
|
|
470
|
+
`some node accumulated halo mass ${maxMass} across ${CONVOS} distinct ` +
|
|
471
|
+
`conversations (${
|
|
472
|
+
CONVOS * 3
|
|
473
|
+
} episodes) — a multi-turn deposit must reinforce the changed ` +
|
|
474
|
+
`part, not re-pour the whole accumulated context, so no shared node's mass ` +
|
|
475
|
+
`grows with the corpus (got ${maxMass} on "${
|
|
476
|
+
maxBytes.replace(/\n/g, "|")
|
|
477
|
+
}")`,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// Synonymy/recall preserved: a genuinely-new continuation turn carries a halo.
|
|
481
|
+
const turn1 = foldToId(
|
|
482
|
+
m,
|
|
483
|
+
s,
|
|
484
|
+
`the assistant concludes that mulch completes the gardening picture`,
|
|
485
|
+
);
|
|
486
|
+
assert.ok(
|
|
487
|
+
turn1 !== null && s.haloMass(turn1) >= 1,
|
|
488
|
+
"a genuinely-new turn keeps its halo (the relevant part IS reinforced)",
|
|
489
|
+
);
|
|
490
|
+
await s.close();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("halos resonate concept siblings through the separate halo index", async () => {
|
|
494
|
+
const s = memStore();
|
|
495
|
+
const next = vecs(9);
|
|
496
|
+
const ice = await s.putLeaf(enc("ice"), next());
|
|
497
|
+
const hielo = await s.putLeaf(enc("hielo"), next());
|
|
498
|
+
const lava = await s.putLeaf(enc("lava"), next());
|
|
499
|
+
|
|
500
|
+
// ice and hielo keep the SAME company (poured the same signature); lava keeps
|
|
501
|
+
// different company. The halo index must return ice and hielo as siblings of
|
|
502
|
+
// that shared signature, not lava.
|
|
503
|
+
const shared = normalize(next());
|
|
504
|
+
const other = normalize(next());
|
|
505
|
+
for (let i = 0; i < 4; i++) {
|
|
506
|
+
await s.pourHalo(ice, shared);
|
|
507
|
+
await s.pourHalo(hielo, shared);
|
|
508
|
+
await s.pourHalo(lava, other);
|
|
509
|
+
}
|
|
510
|
+
const sibs = await s.resonateHalo(shared, 3);
|
|
511
|
+
const ids = sibs.filter((h) => h.score >= 0.5).map((h) => h.id);
|
|
512
|
+
assert.ok(
|
|
513
|
+
ids.includes(ice) && ids.includes(hielo),
|
|
514
|
+
"ice & hielo are siblings",
|
|
515
|
+
);
|
|
516
|
+
assert.ok(!ids.includes(lava), "lava, with different company, is not");
|
|
517
|
+
await s.close();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// =========================================================================
|
|
521
|
+
// 7 — METADATA & SNAPSHOT
|
|
522
|
+
// =========================================================================
|
|
523
|
+
|
|
524
|
+
test("metadata is set, read, overwritten, and deleted", async () => {
|
|
525
|
+
const s = memStore();
|
|
526
|
+
assert.equal(await s.getMeta("train.dataset"), null, "absent key → null");
|
|
527
|
+
await s.setMeta("train.dataset", "MixtureVitae-v2");
|
|
528
|
+
assert.equal(await s.getMeta("train.dataset"), "MixtureVitae-v2");
|
|
529
|
+
await s.setMeta("train.dataset", "MixtureVitae-v3"); // overwrite
|
|
530
|
+
assert.equal(await s.getMeta("train.dataset"), "MixtureVitae-v3");
|
|
531
|
+
await s.deleteMeta("train.dataset");
|
|
532
|
+
assert.equal(await s.getMeta("train.dataset"), null, "deleted key → null");
|
|
533
|
+
await s.close();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("the snapshot blob round-trips byte-for-byte", async () => {
|
|
537
|
+
const s = memStore();
|
|
538
|
+
assert.equal(await s.loadSnapshot(), null, "no snapshot yet");
|
|
539
|
+
const blob = new Uint8Array([0, 1, 2, 250, 255, 128]);
|
|
540
|
+
await s.saveSnapshot(blob);
|
|
541
|
+
assert.deepEqual([...(await s.loadSnapshot())], [...blob]);
|
|
542
|
+
// A second save overwrites (single-row snapshot).
|
|
543
|
+
await s.saveSnapshot(new Uint8Array([9]));
|
|
544
|
+
assert.deepEqual([...(await s.loadSnapshot())], [9]);
|
|
545
|
+
await s.close();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// =========================================================================
|
|
549
|
+
// 8 — WRITE DISCIPLINE: deferred transaction, commit(), reads see uncommitted
|
|
550
|
+
// =========================================================================
|
|
551
|
+
|
|
552
|
+
test("reads observe not-yet-committed writes; commit() makes them durable", async () => {
|
|
553
|
+
const s = memStore();
|
|
554
|
+
const next = vecs(10);
|
|
555
|
+
const a = await s.putLeaf(enc("uv"), next());
|
|
556
|
+
// Within the same connection, the uncommitted node and edge are visible.
|
|
557
|
+
await s.link(a, a);
|
|
558
|
+
assert.equal(s.findLeaf(enc("uv")), a, "uncommitted leaf is found");
|
|
559
|
+
assert.deepEqual(s.next(a), [a], "uncommitted edge is visible");
|
|
560
|
+
// commit() is safe to call and idempotent (a no-op when nothing pending).
|
|
561
|
+
s.commit();
|
|
562
|
+
s.commit();
|
|
563
|
+
assert.equal(s.findLeaf(enc("uv")), a, "still consistent after commit");
|
|
564
|
+
await s.close();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// =========================================================================
|
|
568
|
+
// 9 — DURABILITY: reopen the SAME path, cold caches, everything survives
|
|
569
|
+
// =========================================================================
|
|
570
|
+
|
|
571
|
+
test("a closed store reopens with DAG, edges, halos, gists and meta intact", async () => {
|
|
572
|
+
const stem = tmpStem("durable");
|
|
573
|
+
try {
|
|
574
|
+
const next = vecs(11);
|
|
575
|
+
const gist = normalize(next());
|
|
576
|
+
let aId;
|
|
577
|
+
|
|
578
|
+
// Session 1 — write across every concern, then close (flushes + commits).
|
|
579
|
+
{
|
|
580
|
+
const s = new SQliteStore({ path: stem, D });
|
|
581
|
+
aId = await s.putLeaf(enc("hi"), gist);
|
|
582
|
+
const b = await s.putLeaf(enc("yo"), next());
|
|
583
|
+
await s.putBranch([aId, b], next());
|
|
584
|
+
await s.link(aId, b);
|
|
585
|
+
for (let i = 0; i < 4; i++) await s.pourHalo(aId, normalize(next()));
|
|
586
|
+
await s.setMeta("k", "v");
|
|
587
|
+
await s.close();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Session 2 — reopen the SAME files with cold in-memory caches. Every store
|
|
591
|
+
// responsibility must reload from disk, not from a warm cache.
|
|
592
|
+
{
|
|
593
|
+
const s = new SQliteStore({ path: stem, D });
|
|
594
|
+
assert.equal(s.nodeCount(), 3, "node count restored from disk");
|
|
595
|
+
assert.equal(s.findLeaf(enc("hi")), aId, "cold content-addressed lookup");
|
|
596
|
+
assert.deepEqual(s.next(aId), [1], "edges restored");
|
|
597
|
+
assert.deepEqual(s.parents(0).sort(), [2], "parent index restored");
|
|
598
|
+
assert.ok(s.halo(aId) !== null, "halo accumulator restored");
|
|
599
|
+
assert.equal(await s.getMeta("k"), "v", "metadata restored");
|
|
600
|
+
const hits = await s.resonate(gist, 1);
|
|
601
|
+
assert.equal(hits[0].id, aId, "the gist vector index survived to disk");
|
|
602
|
+
await s.close();
|
|
603
|
+
}
|
|
604
|
+
} finally {
|
|
605
|
+
cleanup(stem);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// =========================================================================
|
|
610
|
+
// 10 — MIND ROUND-TRIP: train → save → reopen → loadFromStore answers exactly
|
|
611
|
+
// =========================================================================
|
|
612
|
+
|
|
613
|
+
test("a trained Mind persists and serves exactly after reopen", async () => {
|
|
614
|
+
const stem = tmpStem("mind");
|
|
615
|
+
try {
|
|
616
|
+
// Train and snapshot the config.
|
|
617
|
+
{
|
|
618
|
+
const store = new SQliteStore({ path: stem, D });
|
|
619
|
+
const m = new Mind({ D, seed: 7, store });
|
|
620
|
+
await m.ingest([
|
|
621
|
+
["what is the capital of france", "paris"],
|
|
622
|
+
["what is the capital of japan", "tokyo"],
|
|
623
|
+
]);
|
|
624
|
+
await m.save(); // writes the config snapshot
|
|
625
|
+
await store.close();
|
|
626
|
+
}
|
|
627
|
+
// Reopen in a fresh store and restore the Mind from the snapshot alone.
|
|
628
|
+
{
|
|
629
|
+
const store = new SQliteStore({ path: stem, D });
|
|
630
|
+
const m = await Mind.loadFromStore(store);
|
|
631
|
+
assert.equal(
|
|
632
|
+
await m.respondText("what is the capital of france"),
|
|
633
|
+
"paris",
|
|
634
|
+
);
|
|
635
|
+
assert.equal(
|
|
636
|
+
await m.respondText("what is the capital of japan"),
|
|
637
|
+
"tokyo",
|
|
638
|
+
);
|
|
639
|
+
await store.close();
|
|
640
|
+
}
|
|
641
|
+
} finally {
|
|
642
|
+
cleanup(stem);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// In-memory recall still works end to end (the contract the old surreal test
|
|
647
|
+
// guarded), now alongside the full storage coverage above.
|
|
648
|
+
test("in-memory store: exact recall through the gist index", async () => {
|
|
649
|
+
const store = memStore();
|
|
650
|
+
const m = new Mind({ D, seed: 7, store });
|
|
651
|
+
await m.ingest([
|
|
652
|
+
["what is the capital of france", "paris"],
|
|
653
|
+
["what is the capital of japan", "tokyo"],
|
|
654
|
+
["what is the capital of italy", "rome"],
|
|
655
|
+
]);
|
|
656
|
+
assert.equal(await m.respondText("what is the capital of france"), "paris");
|
|
657
|
+
assert.equal(await m.respondText("what is the capital of italy"), "rome");
|
|
658
|
+
await store.close();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// =========================================================================
|
|
662
|
+
// 11 — COMPRESSION: the store stays O(distinct chunks), not O(deposits)
|
|
663
|
+
// =========================================================================
|
|
664
|
+
//
|
|
665
|
+
// Sema's only compression is INTERN-TIME hash-consing (plus branch near
|
|
666
|
+
// dedup, exercised in §3) — identical content reuses an id, so the node count
|
|
667
|
+
// is the number of DISTINCT subtrees ever seen, independent of how many times
|
|
668
|
+
// they are deposited. There is no post-hoc "merge that deletes loser rows": ids
|
|
669
|
+
// are dense and never deleted; relearning is simply idempotent. These tests pin
|
|
670
|
+
// that model down (the old 12-compression assumed deletion and a long-dead
|
|
671
|
+
// `.rvf` file — both gone now).
|
|
672
|
+
|
|
673
|
+
// Repeating the SAME deposit must add ZERO entries — dedup all the way down.
|
|
674
|
+
test("compression: repeated identical deposits add no entries", async () => {
|
|
675
|
+
const store = memStore();
|
|
676
|
+
const m = new Mind({ D, seed: 7, store });
|
|
677
|
+
const ctx = "the quick brown fox jumps over the lazy dog";
|
|
678
|
+
const cont = "a pangram holds every letter of the alphabet once";
|
|
679
|
+
|
|
680
|
+
await m.ingest([[ctx, cont]]);
|
|
681
|
+
const afterFirst = await store.size();
|
|
682
|
+
assert.ok(afterFirst > 1, "the pair decomposes into many distinct nodes");
|
|
683
|
+
|
|
684
|
+
for (let i = 0; i < 50; i++) await m.ingest([[ctx, cont]]);
|
|
685
|
+
assert.equal(
|
|
686
|
+
await store.size(),
|
|
687
|
+
afterFirst,
|
|
688
|
+
"50 re-deposits of identical content must add nothing (full dedup)",
|
|
689
|
+
);
|
|
690
|
+
await store.close();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Shared sub-content across DIFFERENT deposits is interned once: two deposits
|
|
694
|
+
// that share a prefix cost less than the sum of their standalone footprints.
|
|
695
|
+
test("compression: shared sub-content is interned once across deposits", async () => {
|
|
696
|
+
const SHORT = "the quick brown fox";
|
|
697
|
+
const LONG = "the quick brown fox jumps over the lazy dog and runs away";
|
|
698
|
+
|
|
699
|
+
const a = memStore();
|
|
700
|
+
const ma = new Mind({ D, seed: 7, store: a });
|
|
701
|
+
await ma.ingest(SHORT);
|
|
702
|
+
const cShort = await a.size();
|
|
703
|
+
await a.close();
|
|
704
|
+
|
|
705
|
+
const b = memStore();
|
|
706
|
+
const mb = new Mind({ D, seed: 7, store: b });
|
|
707
|
+
await mb.ingest(LONG);
|
|
708
|
+
const cLong = await b.size();
|
|
709
|
+
await b.close();
|
|
710
|
+
|
|
711
|
+
const c = memStore();
|
|
712
|
+
const mc = new Mind({ D, seed: 7, store: c });
|
|
713
|
+
await mc.ingest(SHORT);
|
|
714
|
+
await mc.ingest(LONG);
|
|
715
|
+
const cBoth = await c.size();
|
|
716
|
+
await c.close();
|
|
717
|
+
|
|
718
|
+
// If parts were opaque, both would cost their full sum. Sharing means LONG
|
|
719
|
+
// reuses SHORT's interned subtree, so the combined store beats the sum.
|
|
720
|
+
assert.ok(
|
|
721
|
+
cBoth < cShort + cLong,
|
|
722
|
+
`sharing: combined ${cBoth} must beat the no-share sum ${cShort}+${cLong}`,
|
|
723
|
+
);
|
|
724
|
+
await c.close().catch(() => {});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Unique content scales LINEARLY with deposits (each adds its own chunks); the
|
|
728
|
+
// per-deposit node cost stays bounded — no O(N²) leak as the corpus grows.
|
|
729
|
+
test("compression: unique content scales linearly, with a bounded per-deposit cost", async () => {
|
|
730
|
+
const text = (id, bytes) => {
|
|
731
|
+
let s = "", n = 0;
|
|
732
|
+
while (n < bytes) {
|
|
733
|
+
const w = `w${(id * 7 + n * 3) % 200}`;
|
|
734
|
+
s += w + " ";
|
|
735
|
+
n += w.length + 1;
|
|
736
|
+
}
|
|
737
|
+
return s;
|
|
738
|
+
};
|
|
739
|
+
const measure = async (N) => {
|
|
740
|
+
const store = memStore();
|
|
741
|
+
const m = new Mind({ D, seed: 7, store });
|
|
742
|
+
for (let i = 0; i < N; i++) {
|
|
743
|
+
await m.ingest([[text(i, 80), text(i + 9000, 80)]]);
|
|
744
|
+
}
|
|
745
|
+
const e = await store.size();
|
|
746
|
+
await store.close();
|
|
747
|
+
return e;
|
|
748
|
+
};
|
|
749
|
+
const e50 = await measure(50);
|
|
750
|
+
const e200 = await measure(200);
|
|
751
|
+
const per50 = e50 / 50, per200 = e200 / 200;
|
|
752
|
+
console.log(
|
|
753
|
+
` unique scaling: 50 pairs → ${e50} (${per50.toFixed(1)}/pair), ` +
|
|
754
|
+
`200 pairs → ${e200} (${per200.toFixed(1)}/pair)`,
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
// Per-deposit node cost must not grow with N — 4× the deposits costs ~4× the
|
|
758
|
+
// nodes, not ~16×. A per-pair cost that climbed would betray an O(N²) leak.
|
|
759
|
+
assert.ok(
|
|
760
|
+
per200 < per50 * 1.5 + 1,
|
|
761
|
+
`per-deposit cost grew (${per50.toFixed(1)} → ${
|
|
762
|
+
per200.toFixed(1)
|
|
763
|
+
}) — possible O(N²) leak`,
|
|
764
|
+
);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// On disk the store materialises into exactly its three files; their combined
|
|
768
|
+
// size is a bounded multiple of the logical input (it is a vector index, so it
|
|
769
|
+
// is larger than the text by design — but bounded, not runaway).
|
|
770
|
+
test("compression: on-disk footprint is the three files, bounded in input", async () => {
|
|
771
|
+
const stem = tmpStem("disk");
|
|
772
|
+
try {
|
|
773
|
+
const store = new SQliteStore({ path: stem, D });
|
|
774
|
+
const m = new Mind({ D, seed: 7, store });
|
|
775
|
+
let inputBytes = 0;
|
|
776
|
+
for (let i = 0; i < 60; i++) {
|
|
777
|
+
const q = `record number ${i} about subject ${i % 13}`;
|
|
778
|
+
const aa = `the answer to record ${i} is detail ${i * 3}`;
|
|
779
|
+
inputBytes += enc(q).length + enc(aa).length;
|
|
780
|
+
await m.ingest([[q, aa]]);
|
|
781
|
+
}
|
|
782
|
+
await m.save();
|
|
783
|
+
await store.close();
|
|
784
|
+
|
|
785
|
+
const main = statSync(`${stem}.sqlite`).size;
|
|
786
|
+
const content = statSync(`${stem}.content.vec`).size;
|
|
787
|
+
const halo = statSync(`${stem}.halo.vec`).size;
|
|
788
|
+
const total = main + content + halo;
|
|
789
|
+
console.log(
|
|
790
|
+
` on-disk: input ${inputBytes}B → sqlite ${main}B + content.vec ${content}B + halo.vec ${halo}B = ${total}B (${
|
|
791
|
+
(total / inputBytes).toFixed(1)
|
|
792
|
+
}×)`,
|
|
793
|
+
);
|
|
794
|
+
// Every file is real and non-empty, and the whole store is a sane multiple
|
|
795
|
+
// of the input — a vector index expands the text but does not run away.
|
|
796
|
+
assert.ok(
|
|
797
|
+
main > 0 && content > 0 && halo > 0,
|
|
798
|
+
"all three files materialised",
|
|
799
|
+
);
|
|
800
|
+
assert.ok(
|
|
801
|
+
total < inputBytes * 200,
|
|
802
|
+
"footprint is a bounded multiple of input",
|
|
803
|
+
);
|
|
804
|
+
} finally {
|
|
805
|
+
cleanup(stem);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// STORAGE-AMPLIFICATION GUARD (the regression this whole change fixes). Folding
|
|
810
|
+
// text into a Merkle DAG explodes it into ~0.2 nodes/byte, most of which are
|
|
811
|
+
// intermediate branches that recall reaches by CLIMBING the structural graph,
|
|
812
|
+
// never by direct resonance. Indexing every one into the content HNSW made the
|
|
813
|
+
// on-disk store ~50× the learned content and training crawl at a few KB/s. The
|
|
814
|
+
// fix indexes a form's gist only when it becomes a resonance target AND it is
|
|
815
|
+
// not already covered by the content index — the index adds a
|
|
816
|
+
// form only if it cannot already resonate to a stored gist within ε = 1 − 1/√D,
|
|
817
|
+
// so near-duplicate and frame forms collapse to one anchor and the kept set
|
|
818
|
+
// tracks the diversity of MEANING, not the size of the DAG. This guard pins both
|
|
819
|
+
// halves on disk so the regression cannot return silently: total footprint per
|
|
820
|
+
// learned byte, AND content-index selectivity (indexed vectors ≪ total nodes).
|
|
821
|
+
test("compression: post-hoc compaction bounds content-index footprint", async () => {
|
|
822
|
+
const stem = tmpStem("ampl");
|
|
823
|
+
try {
|
|
824
|
+
const store = new SQliteStore({ path: stem, D });
|
|
825
|
+
const m = new Mind({ D, seed: 7, store });
|
|
826
|
+
let contentBytes = 0;
|
|
827
|
+
// Distinctive, low-dedup episodes — the costly path (genuinely new nodes).
|
|
828
|
+
for (let i = 0; i < 150; i++) {
|
|
829
|
+
const q = `unique question ${i} concerning topic ${i}x${(i * 31) % 97}`;
|
|
830
|
+
const a = `the distinct answer for ${i} is finding ${(i * 7) % 89} alpha`;
|
|
831
|
+
contentBytes += enc(q).length + enc(a).length;
|
|
832
|
+
await m.ingest([[q, a]]);
|
|
833
|
+
}
|
|
834
|
+
await m.save();
|
|
835
|
+
|
|
836
|
+
// Post-hoc compaction: remove structurally-isolated interior nodes.
|
|
837
|
+
// Vector-similarity gating was removed from the training hot path — it was rigid,
|
|
838
|
+
// dimensionally fragile, and a no-op at typical D. Structural
|
|
839
|
+
// compaction (parent-count-based) replaces it as a batch step.
|
|
840
|
+
const nodes = await store.size();
|
|
841
|
+
const rawIndexed = store.indexedVectorCount();
|
|
842
|
+
const rawRatio = (statSync(`${stem}.sqlite`).size +
|
|
843
|
+
statSync(`${stem}.content.vec`).size +
|
|
844
|
+
statSync(`${stem}.halo.vec`).size) / contentBytes;
|
|
845
|
+
console.log(
|
|
846
|
+
` pre-compact: ${contentBytes}B → ${nodes} nodes, ${rawIndexed} indexed ` +
|
|
847
|
+
`(${(rawIndexed / nodes * 100).toFixed(1)}%), ${rawRatio.toFixed(1)}×`,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
const removed = await store.compactContentIndex(2);
|
|
851
|
+
const indexed = store.indexedVectorCount();
|
|
852
|
+
await store.close();
|
|
853
|
+
|
|
854
|
+
const main = statSync(`${stem}.sqlite`).size;
|
|
855
|
+
const vecContent = statSync(`${stem}.content.vec`).size;
|
|
856
|
+
const halo = statSync(`${stem}.halo.vec`).size;
|
|
857
|
+
const total = main + vecContent + halo;
|
|
858
|
+
const ratio = total / contentBytes;
|
|
859
|
+
const selectivity = indexed / nodes;
|
|
860
|
+
console.log(
|
|
861
|
+
` post-compact: removed ${removed}, ${indexed} indexed ` +
|
|
862
|
+
`(${(selectivity * 100).toFixed(1)}% selectivity), ${total}B on disk (${
|
|
863
|
+
ratio.toFixed(1)
|
|
864
|
+
}×)`,
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
// After structural compaction (minParents=2), the content index holds
|
|
868
|
+
// only nodes that bridge multiple experiences (2+ parents) or are
|
|
869
|
+
// experience roots (1 parent + edges). This is a small minority of
|
|
870
|
+
// the exploded DAG. Vector-similarity gating previously tried to achieve this during
|
|
871
|
+
// training via expensive HNSW probes, but was rigid, dimensionally
|
|
872
|
+
// fragile, and a no-op at typical D. Post-hoc structural compaction
|
|
873
|
+
// is adaptive (the data decides what's shared) and doesn't slow training.
|
|
874
|
+
assert.ok(
|
|
875
|
+
selectivity < 0.55,
|
|
876
|
+
`content index holds ${(selectivity * 100).toFixed(1)}% of nodes — ` +
|
|
877
|
+
`expected a minority after structural compaction`,
|
|
878
|
+
);
|
|
879
|
+
// Footprint: a bounded multiple of the learned content after compaction.
|
|
880
|
+
// Before the fix this was ~50×; after structural compaction it drops
|
|
881
|
+
// well below 45×. On larger corpora the ratio drops further as shared
|
|
882
|
+
// scaffoldings dominate the index.
|
|
883
|
+
assert.ok(
|
|
884
|
+
ratio < 45,
|
|
885
|
+
`on-disk footprint is ${ratio.toFixed(1)}× the learned content — ` +
|
|
886
|
+
`expected < 45× after compaction`,
|
|
887
|
+
);
|
|
888
|
+
} finally {
|
|
889
|
+
cleanup(stem);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// =========================================================================
|
|
894
|
+
// 12 — CONCEPT FORMATION: incremental halos, idempotency, conflict
|
|
895
|
+
// =========================================================================
|
|
896
|
+
//
|
|
897
|
+
// Concepts form continuously during deposition — there is no separate "sleep"
|
|
898
|
+
// pass. These are the store-level guarantees the old 06-sleep suite covered,
|
|
899
|
+
// now where they belong: the halo machinery is the store's.
|
|
900
|
+
|
|
901
|
+
// Cross-name transfer works the moment the company is poured — no sleep call.
|
|
902
|
+
test("concepts: cross-name transfer forms incrementally during training", async () => {
|
|
903
|
+
const img = (n) => {
|
|
904
|
+
const g = { width: 4, height: 4, channels: 1, data: new Uint8Array(16) };
|
|
905
|
+
for (let i = 0; i < 16; i++) g.data[i] = (i * 13 + n * 41) & 0xff;
|
|
906
|
+
return g;
|
|
907
|
+
};
|
|
908
|
+
const m = new Mind({ D, seed: 7, store: memStore() });
|
|
909
|
+
await m.ingest([
|
|
910
|
+
["ice", "ice is frozen water"],
|
|
911
|
+
[img(1), "ice"],
|
|
912
|
+
[img(2), "ice"],
|
|
913
|
+
[img(1), "hielo"],
|
|
914
|
+
[img(2), "hielo"],
|
|
915
|
+
]);
|
|
916
|
+
// "hielo" and "ice" keep the same company → the answer crosses the name.
|
|
917
|
+
assert.equal(await m.respondText("hielo"), "hielo is frozen water");
|
|
918
|
+
assert.equal(await m.respondText("ice"), "ice is frozen water");
|
|
919
|
+
await m.store.close();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Relearning a fact changes nothing — deposition is idempotent at every level.
|
|
923
|
+
test("concepts: duplicate training is idempotent", async () => {
|
|
924
|
+
const m = new Mind({ D, seed: 7, store: memStore() });
|
|
925
|
+
await m.ingest([["what is ice?", "ice is frozen water"]]);
|
|
926
|
+
const n1 = await m.store.size();
|
|
927
|
+
await m.ingest([["what is ice?", "ice is frozen water"]]);
|
|
928
|
+
assert.equal(await m.store.size(), n1, "a duplicate deposit adds nothing");
|
|
929
|
+
await m.store.close();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// A context with conflicting continuations resolves to ONE side, the SAME way
|
|
933
|
+
// every run (same seed) — deterministic, never a blend.
|
|
934
|
+
test("concepts: conflicting episodes resolve to one side, deterministically", async () => {
|
|
935
|
+
const train = async () => {
|
|
936
|
+
const m = new Mind({ D, seed: 7, store: memStore() });
|
|
937
|
+
await m.ingest([["the sky", "blue"], ["the sky", "grey"]]);
|
|
938
|
+
const a = await m.respondText("the sky");
|
|
939
|
+
await m.store.close();
|
|
940
|
+
return a;
|
|
941
|
+
};
|
|
942
|
+
const first = await train();
|
|
943
|
+
assert.ok(
|
|
944
|
+
first === "blue" || first === "grey",
|
|
945
|
+
`one learnt side, got "${first}"`,
|
|
946
|
+
);
|
|
947
|
+
assert.equal(await train(), first, "same seed → same deterministic choice");
|
|
948
|
+
});
|