@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,489 @@
|
|
|
1
|
+
// 20-stability.test.mjs — perception is a PURE, STABLE Merkle DAG.
|
|
2
|
+
//
|
|
3
|
+
// Two properties of perceive() that the rest of sema leans on, guarded here so a
|
|
4
|
+
// future change to the river cannot silently break them:
|
|
5
|
+
//
|
|
6
|
+
// PURITY the same bytes always perceive to the SAME tree — identical
|
|
7
|
+
// structure and identical gist vectors. Recall, recognition,
|
|
8
|
+
// resonance and articulation all re-perceive content and expect
|
|
9
|
+
// to land on the very nodes deposition interned; if perception
|
|
10
|
+
// depended on anything but its input, those lookups would miss.
|
|
11
|
+
//
|
|
12
|
+
// PREFIX-STABLE a stream that CONTINUES another (its bytes begin with the
|
|
13
|
+
// earlier stream's) folds its shared prefix into the SAME
|
|
14
|
+
// subtrees — only the right spine regroups. This is what makes a
|
|
15
|
+
// growing context (an accumulated-conversation turn, an extended
|
|
16
|
+
// buffer, appended frames) cheap to deposit and lets the halo be
|
|
17
|
+
// reinforced on just the NEW content: the prefix's nodes recur by
|
|
18
|
+
// identity instead of being rebuilt as fresh nodes.
|
|
19
|
+
//
|
|
20
|
+
// A node is interned (hash-consed) by its CONTENT: a leaf by its bytes, a branch
|
|
21
|
+
// by its ordered child ids. So a node's content FINGERPRINT (its Merkle hash —
|
|
22
|
+
// leaf bytes, or B(child fingerprints)) is one-to-one with the id it interns to.
|
|
23
|
+
// Sharing a subtree BY FINGERPRINT is therefore exactly sharing it BY ID; the
|
|
24
|
+
// test measures fingerprints, which needs only the pure public perceive(), no
|
|
25
|
+
// store. (See bytesToTree / foldOnce in geometry.ts for why the property holds.)
|
|
26
|
+
|
|
27
|
+
import { test } from "node:test";
|
|
28
|
+
import assert from "node:assert/strict";
|
|
29
|
+
import { Mind } from "../dist/src/index.js";
|
|
30
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
31
|
+
|
|
32
|
+
const newMind = () => new Mind({ seed: 7 });
|
|
33
|
+
|
|
34
|
+
// A node's content fingerprint — its Merkle hash, identical iff the node would
|
|
35
|
+
// hash-cons to the same interned id. A leaf is its raw bytes; a branch is the
|
|
36
|
+
// ordered tuple of its children's fingerprints.
|
|
37
|
+
function fingerprint(node) {
|
|
38
|
+
if (node.kids === null) {
|
|
39
|
+
const b = node.leaf ?? new Uint8Array(0);
|
|
40
|
+
return "L:" + Buffer.from(b).toString("hex");
|
|
41
|
+
}
|
|
42
|
+
return "B(" + node.kids.map(fingerprint).join(",") + ")";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Every subtree fingerprint in a tree (the set of ids it would intern).
|
|
46
|
+
function subtreeIds(node, into = new Set()) {
|
|
47
|
+
into.add(fingerprint(node));
|
|
48
|
+
if (node.kids) { for (const k of node.kids) subtreeIds(k, into); }
|
|
49
|
+
return into;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Deep structural + gist equality of two perceived trees.
|
|
53
|
+
function treesIdentical(a, b) {
|
|
54
|
+
if ((a.kids === null) !== (b.kids === null)) return false;
|
|
55
|
+
if (a.v.length !== b.v.length) return false;
|
|
56
|
+
for (let i = 0; i < a.v.length; i++) if (a.v[i] !== b.v[i]) return false;
|
|
57
|
+
if (a.kids === null) {
|
|
58
|
+
const la = a.leaf ?? new Uint8Array(0);
|
|
59
|
+
const lb = b.leaf ?? new Uint8Array(0);
|
|
60
|
+
if (la.length !== lb.length) return false;
|
|
61
|
+
for (let i = 0; i < la.length; i++) if (la[i] !== lb[i]) return false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (a.kids.length !== b.kids.length) return false;
|
|
65
|
+
for (let i = 0; i < a.kids.length; i++) {
|
|
66
|
+
if (!treesIdentical(a.kids[i], b.kids[i])) return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
72
|
+
|
|
73
|
+
// ── PURITY — same bytes, same tree (structure AND vectors) ────────────────
|
|
74
|
+
test("perceive is pure: identical input yields an identical tree", async () => {
|
|
75
|
+
const mind = newMind();
|
|
76
|
+
const inputs = [
|
|
77
|
+
"the crystal river holds the ancient compass",
|
|
78
|
+
"数据是雨水,记忆是河流。",
|
|
79
|
+
"🌊🧠 memory is the model 🜁",
|
|
80
|
+
"a".repeat(300),
|
|
81
|
+
];
|
|
82
|
+
for (const s of inputs) {
|
|
83
|
+
// Re-perceive the SAME bytes twice — must be byte-for-byte the same tree.
|
|
84
|
+
assert.ok(
|
|
85
|
+
treesIdentical(mind.perceive(s), mind.perceive(s)),
|
|
86
|
+
`perceive("${s.slice(0, 16)}…") was not stable across calls`,
|
|
87
|
+
);
|
|
88
|
+
// And independent of the leaf cache's warmth: a fresh Mind (cold cache,
|
|
89
|
+
// same seed) must perceive the identical tree.
|
|
90
|
+
const cold = newMind();
|
|
91
|
+
assert.equal(
|
|
92
|
+
fingerprint(cold.perceive(s)),
|
|
93
|
+
fingerprint(mind.perceive(s)),
|
|
94
|
+
`perceive depends on cache/history for "${s.slice(0, 16)}…"`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
await mind.store.close();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Order-independence: perceiving B after A must give B the SAME tree it gets
|
|
101
|
+
// when perceived first — perception carries no state from one call to the next.
|
|
102
|
+
test("perceive is order-independent across different inputs", async () => {
|
|
103
|
+
const a = "the silent garden remembers the frozen lantern";
|
|
104
|
+
const b = "an entirely different burning harbor follows the engine";
|
|
105
|
+
const m1 = newMind();
|
|
106
|
+
m1.perceive(a);
|
|
107
|
+
const bAfterA = fingerprint(m1.perceive(b));
|
|
108
|
+
const m2 = newMind();
|
|
109
|
+
const bFirst = fingerprint(m2.perceive(b));
|
|
110
|
+
assert.equal(bAfterA, bFirst, "perceiving A changed how B perceives");
|
|
111
|
+
await m1.store.close();
|
|
112
|
+
await m2.store.close();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── PREFIX-STABILITY — a continuation shares its prefix's subtrees ────────
|
|
116
|
+
test("a continuation re-uses almost all of its prefix's subtrees (by id)", async () => {
|
|
117
|
+
const mind = newMind();
|
|
118
|
+
|
|
119
|
+
// A growing context, exactly the shape multi-turn conversation builds: each
|
|
120
|
+
// step appends a turn to the accumulated prefix (the "\n" join is incidental —
|
|
121
|
+
// the property is about bytes, any continuation qualifies).
|
|
122
|
+
const turns = [
|
|
123
|
+
"the harbor master logged the tide at dawn",
|
|
124
|
+
"the crystal river rose two feet by noon",
|
|
125
|
+
"the ancient compass pointed steadily north",
|
|
126
|
+
"the silent garden flooded near the gilded gate",
|
|
127
|
+
"the wandering engine hauled the last barge home",
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
let prevTree = null;
|
|
131
|
+
let prevLen = 0;
|
|
132
|
+
for (let i = 0; i < turns.length; i++) {
|
|
133
|
+
const ctx = turns.slice(0, i + 1).join("\n");
|
|
134
|
+
const tree = mind.perceive(ctx);
|
|
135
|
+
if (prevTree) {
|
|
136
|
+
const prevIds = subtreeIds(prevTree);
|
|
137
|
+
const nowIds = subtreeIds(tree);
|
|
138
|
+
let kept = 0;
|
|
139
|
+
for (const id of prevIds) if (nowIds.has(id)) kept++;
|
|
140
|
+
const frac = kept / prevIds.size;
|
|
141
|
+
console.log(
|
|
142
|
+
` prefix ${prevLen}B → ${enc(ctx).length}B: ` +
|
|
143
|
+
`${kept}/${prevIds.size} prefix subtrees re-used (${
|
|
144
|
+
(frac * 100).toFixed(1)
|
|
145
|
+
}%)`,
|
|
146
|
+
);
|
|
147
|
+
// The shared prefix re-folds into the SAME subtrees; only the right spine
|
|
148
|
+
// (the junction where the new turn attaches, plus the few nodes above it on
|
|
149
|
+
// the path to the root) regroups. The overwhelming majority of the prefix's
|
|
150
|
+
// interned nodes recur by identity — that is the stable-DAG guarantee, and
|
|
151
|
+
// a regression to a position-defined cut would send this toward ~0.
|
|
152
|
+
assert.ok(
|
|
153
|
+
frac >= 0.85,
|
|
154
|
+
`only ${
|
|
155
|
+
(frac * 100).toFixed(1)
|
|
156
|
+
}% of the prefix's subtrees survived the ` +
|
|
157
|
+
`continuation — perception is not prefix-stable (the river is ` +
|
|
158
|
+
`regrouping shared content, not just the right spine)`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
prevTree = tree;
|
|
162
|
+
prevLen = enc(ctx).length;
|
|
163
|
+
}
|
|
164
|
+
await mind.store.close();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The stable part is the PREFIX: appending bytes must never disturb a subtree
|
|
168
|
+
// that lies wholly within the earlier stream. We check the deepest shared
|
|
169
|
+
// content — the leaves of the prefix region — survive verbatim, so only the
|
|
170
|
+
// junction leaf at the very end can differ.
|
|
171
|
+
test("appending bytes leaves the prefix's leaves untouched", async () => {
|
|
172
|
+
const mind = newMind();
|
|
173
|
+
const prefix = "the gilded archive guards the burning mirror and the ";
|
|
174
|
+
const leavesOf = (s) => {
|
|
175
|
+
const out = [];
|
|
176
|
+
const walk = (n) => {
|
|
177
|
+
if (n.kids === null) out.push(fingerprint(n));
|
|
178
|
+
else for (const k of n.kids) walk(k);
|
|
179
|
+
};
|
|
180
|
+
walk(mind.perceive(s));
|
|
181
|
+
return out;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const base = leavesOf(prefix);
|
|
185
|
+
for (
|
|
186
|
+
const suffix of ["frozen river", "hollow lantern of the deep north sea"]
|
|
187
|
+
) {
|
|
188
|
+
const ext = leavesOf(prefix + suffix);
|
|
189
|
+
// Every prefix leaf except possibly the LAST (the junction leaf, which the
|
|
190
|
+
// continuation re-cuts) must appear, in order, at the front of the extended
|
|
191
|
+
// stream's leaves.
|
|
192
|
+
const stable = base.slice(0, base.length - 1);
|
|
193
|
+
for (let i = 0; i < stable.length; i++) {
|
|
194
|
+
assert.equal(
|
|
195
|
+
ext[i],
|
|
196
|
+
stable[i],
|
|
197
|
+
`leaf ${i} of the prefix changed when "${suffix}" was appended — ` +
|
|
198
|
+
`a content cut moved, so the DAG is not prefix-stable`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await mind.store.close();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
// STORE-LEVEL STRUCTURAL STABILITY — the DAG itself, not just perception.
|
|
207
|
+
//
|
|
208
|
+
// The tests above prove perceive() is a pure, prefix-stable function; the ones
|
|
209
|
+
// below pin the properties the STORE adds on top, which the whole system leans
|
|
210
|
+
// on and which the perception tests cannot see:
|
|
211
|
+
//
|
|
212
|
+
// ROUND-TRIP a deposited input re-perceives and content-addresses back to
|
|
213
|
+
// the very id deposition interned, and that id reconstructs the
|
|
214
|
+
// exact bytes — the address ↔ content bijection.
|
|
215
|
+
// DURABILITY every structural relation (ids, kids, reverse kid rows,
|
|
216
|
+
// continuation edges, containment, the document count) survives
|
|
217
|
+
// a close + reopen unchanged — the graph is the FILE, not the
|
|
218
|
+
// session; caches must be pure accelerators.
|
|
219
|
+
// DETERMINISM two stores fed the same corpus with the same seed are
|
|
220
|
+
// row-for-row identical across every structural table — no
|
|
221
|
+
// hidden clock, randomness, or iteration-order dependence in
|
|
222
|
+
// the whole ingestion pipeline.
|
|
223
|
+
// CLIMB the reverse structural index (kid) exactly mirrors the
|
|
224
|
+
// forward kids lists, and branch ids are dense — edgeAncestors'
|
|
225
|
+
// upward climb can never dead-end on a missing reverse edge.
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
|
|
228
|
+
import { rmSync } from "node:fs";
|
|
229
|
+
import { DatabaseSync } from "node:sqlite";
|
|
230
|
+
|
|
231
|
+
function tmpStem(tag) {
|
|
232
|
+
return `/tmp/sema-stability-${tag}-${process.pid}-${
|
|
233
|
+
Math.floor(performance.now())
|
|
234
|
+
}`;
|
|
235
|
+
}
|
|
236
|
+
function cleanup(stem) {
|
|
237
|
+
for (
|
|
238
|
+
const ext of [
|
|
239
|
+
".sqlite",
|
|
240
|
+
".sqlite-shm",
|
|
241
|
+
".sqlite-wal",
|
|
242
|
+
".content.vec",
|
|
243
|
+
".content.vec-shm",
|
|
244
|
+
".content.vec-wal",
|
|
245
|
+
".halo.vec",
|
|
246
|
+
".halo.vec-shm",
|
|
247
|
+
".halo.vec-wal",
|
|
248
|
+
]
|
|
249
|
+
) {
|
|
250
|
+
try {
|
|
251
|
+
rmSync(stem + ext);
|
|
252
|
+
} catch { /* may not exist */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const CORPUS = [
|
|
257
|
+
[
|
|
258
|
+
"the harbor master logged the tide at dawn",
|
|
259
|
+
"the ledger shows a spring tide",
|
|
260
|
+
],
|
|
261
|
+
["what turns the lighthouse lamp at dusk", "a clockwork of brass gears"],
|
|
262
|
+
["the crystal river rose two feet by noon", "the flood gate held firm"],
|
|
263
|
+
];
|
|
264
|
+
const EXPERIENCE = "the wandering engine hauled the last barge home";
|
|
265
|
+
|
|
266
|
+
async function trainCorpus(mind) {
|
|
267
|
+
for (const [c, a] of CORPUS) await mind.ingest(c, a);
|
|
268
|
+
const exp = await mind.ingest(EXPERIENCE);
|
|
269
|
+
mind.store.commit();
|
|
270
|
+
return exp.id;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── ROUND-TRIP — deposit → resolve → bytes is the identity ────────────────
|
|
274
|
+
test("store round-trip: deposited inputs resolve to their interned ids and back to their bytes", async () => {
|
|
275
|
+
const mind = newMind();
|
|
276
|
+
const expId = await trainCorpus(mind);
|
|
277
|
+
const dec = new TextDecoder();
|
|
278
|
+
|
|
279
|
+
// The experience root: ingest's id, resolve's id, and the bytes all agree.
|
|
280
|
+
assert.equal(
|
|
281
|
+
mind.resolve(enc(EXPERIENCE)),
|
|
282
|
+
expId,
|
|
283
|
+
"re-perceiving a deposited experience did not land on its interned root",
|
|
284
|
+
);
|
|
285
|
+
assert.equal(dec.decode(mind.store.bytes(expId)), EXPERIENCE);
|
|
286
|
+
|
|
287
|
+
// Every trained context and answer is content-addressable, and its id
|
|
288
|
+
// reconstructs its exact bytes.
|
|
289
|
+
for (const [c, a] of CORPUS) {
|
|
290
|
+
for (const s of [c, a]) {
|
|
291
|
+
const id = mind.resolve(enc(s));
|
|
292
|
+
assert.ok(id !== null, `"${s}" did not resolve after deposition`);
|
|
293
|
+
assert.equal(
|
|
294
|
+
dec.decode(mind.store.bytes(id)),
|
|
295
|
+
s,
|
|
296
|
+
`bytes(${id}) did not reconstruct "${s}"`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// The learnt edge is on those very ids: context id → answer id.
|
|
302
|
+
const [c0, a0] = CORPUS[0];
|
|
303
|
+
const nx = mind.store.next(mind.resolve(enc(c0)));
|
|
304
|
+
assert.ok(
|
|
305
|
+
nx.includes(mind.resolve(enc(a0))),
|
|
306
|
+
"the continuation edge does not connect the resolved context/answer ids",
|
|
307
|
+
);
|
|
308
|
+
await mind.store.close();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ── DURABILITY — close + reopen preserves every structural relation ───────
|
|
312
|
+
test("the whole graph survives a store reopen: ids, edges, containment, document count, recall", async () => {
|
|
313
|
+
const stem = tmpStem("reopen");
|
|
314
|
+
try {
|
|
315
|
+
// Session 1: train, snapshot every observable structural fact.
|
|
316
|
+
const before = {};
|
|
317
|
+
{
|
|
318
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
319
|
+
const mind = new Mind({ seed: 7, store });
|
|
320
|
+
await trainCorpus(mind);
|
|
321
|
+
await mind.save();
|
|
322
|
+
before.ids = CORPUS.flat().map((s) => mind.resolve(enc(s)));
|
|
323
|
+
before.n = store.edgeSourceCount();
|
|
324
|
+
before.next = before.ids.map((id) => mind.store.next(id));
|
|
325
|
+
before.parents = before.ids.map((id) => mind.store.parents(id));
|
|
326
|
+
before.answer = await mind.respondText(CORPUS[1][0]);
|
|
327
|
+
before.nodeCount = store.nodeCount();
|
|
328
|
+
await store.close();
|
|
329
|
+
}
|
|
330
|
+
// Session 2: cold caches, same files — everything must read back equal.
|
|
331
|
+
{
|
|
332
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
333
|
+
const mind = new Mind({ seed: 7, store });
|
|
334
|
+
const ids = CORPUS.flat().map((s) => mind.resolve(enc(s)));
|
|
335
|
+
assert.deepEqual(
|
|
336
|
+
ids,
|
|
337
|
+
before.ids,
|
|
338
|
+
"content-addressed ids changed across reopen",
|
|
339
|
+
);
|
|
340
|
+
assert.equal(store.nodeCount(), before.nodeCount, "node count changed");
|
|
341
|
+
assert.equal(
|
|
342
|
+
store.edgeSourceCount(),
|
|
343
|
+
before.n,
|
|
344
|
+
"document count (edgeSourceCount) changed across reopen",
|
|
345
|
+
);
|
|
346
|
+
ids.forEach((id, i) => {
|
|
347
|
+
assert.deepEqual(mind.store.next(id), before.next[i], "edges changed");
|
|
348
|
+
assert.deepEqual(
|
|
349
|
+
mind.store.parents(id),
|
|
350
|
+
before.parents[i],
|
|
351
|
+
"structural parents changed",
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
assert.equal(
|
|
355
|
+
await mind.respondText(CORPUS[1][0]),
|
|
356
|
+
before.answer,
|
|
357
|
+
"recall changed across reopen",
|
|
358
|
+
);
|
|
359
|
+
// Containment was persisted (not a session-local map).
|
|
360
|
+
const db = new DatabaseSync(`${stem}.sqlite`);
|
|
361
|
+
const contain = db.prepare("SELECT COUNT(*) n FROM contain").get().n;
|
|
362
|
+
db.close();
|
|
363
|
+
assert.ok(contain > 0, "containment rows were not durable");
|
|
364
|
+
await store.close();
|
|
365
|
+
}
|
|
366
|
+
} finally {
|
|
367
|
+
cleanup(stem);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ── DETERMINISM — identical corpus + seed ⇒ row-identical stores ──────────
|
|
372
|
+
test("two identically-fed stores are row-for-row identical in every structural table", async () => {
|
|
373
|
+
const stems = [tmpStem("det-a"), tmpStem("det-b")];
|
|
374
|
+
try {
|
|
375
|
+
for (const stem of stems) {
|
|
376
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
377
|
+
const mind = new Mind({ seed: 7, store });
|
|
378
|
+
await trainCorpus(mind);
|
|
379
|
+
await store.close();
|
|
380
|
+
}
|
|
381
|
+
const dump = (stem, sql) => {
|
|
382
|
+
const db = new DatabaseSync(`${stem}.sqlite`);
|
|
383
|
+
const rows = JSON.stringify(
|
|
384
|
+
db.prepare(sql).all(),
|
|
385
|
+
(_, v) => v instanceof Uint8Array ? Buffer.from(v).toString("hex") : v,
|
|
386
|
+
);
|
|
387
|
+
db.close();
|
|
388
|
+
return rows;
|
|
389
|
+
};
|
|
390
|
+
for (
|
|
391
|
+
const [table, sql] of [
|
|
392
|
+
["node", "SELECT id,leaf,kids,h FROM node ORDER BY id"],
|
|
393
|
+
["edge", "SELECT src,dst,seq FROM edge ORDER BY seq"],
|
|
394
|
+
["kid", "SELECT child,parent FROM kid ORDER BY child,parent"],
|
|
395
|
+
["contain", "SELECT id,parents FROM contain ORDER BY id"],
|
|
396
|
+
["halo ids", "SELECT id,mass FROM halo ORDER BY id"],
|
|
397
|
+
]
|
|
398
|
+
) {
|
|
399
|
+
assert.equal(
|
|
400
|
+
dump(stems[0], sql),
|
|
401
|
+
dump(stems[1], sql),
|
|
402
|
+
`${table} rows differ between identically-fed stores — ` +
|
|
403
|
+
`ingestion is not deterministic`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
for (const stem of stems) cleanup(stem);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ── CLIMB INTEGRITY — the reverse index exactly mirrors the kids lists ────
|
|
412
|
+
test("kid rows mirror every branch's kids exactly, and branch ids are dense", async () => {
|
|
413
|
+
const stem = tmpStem("climb");
|
|
414
|
+
try {
|
|
415
|
+
{
|
|
416
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
417
|
+
const mind = new Mind({ seed: 7, store });
|
|
418
|
+
await trainCorpus(mind);
|
|
419
|
+
await store.close();
|
|
420
|
+
}
|
|
421
|
+
const db = new DatabaseSync(`${stem}.sqlite`);
|
|
422
|
+
// Dense ids: rows 0..count-1 with no gaps.
|
|
423
|
+
const { n, mx } = db.prepare(
|
|
424
|
+
"SELECT COUNT(*) n, MAX(id) mx FROM node",
|
|
425
|
+
).get();
|
|
426
|
+
assert.equal(
|
|
427
|
+
mx,
|
|
428
|
+
n - 1,
|
|
429
|
+
"branch ids are not dense (gaps break has()/nextId)",
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Forward → reverse: every real (non-byte-leaf) kid of every MIXED branch
|
|
433
|
+
// has its (child, parent) row. Flat branches (zero-length kids blob) have
|
|
434
|
+
// only implicit byte leaves, which by design get no kid rows.
|
|
435
|
+
const kidSet = new Set(
|
|
436
|
+
db.prepare("SELECT child, parent FROM kid").all()
|
|
437
|
+
.map((r) => `${r.child}:${r.parent}`),
|
|
438
|
+
);
|
|
439
|
+
let checked = 0;
|
|
440
|
+
for (
|
|
441
|
+
const row of db.prepare(
|
|
442
|
+
"SELECT id, kids FROM node WHERE kids IS NOT NULL AND length(kids) > 0",
|
|
443
|
+
).all()
|
|
444
|
+
) {
|
|
445
|
+
const ids = new Int32Array(
|
|
446
|
+
row.kids.buffer.slice(
|
|
447
|
+
row.kids.byteOffset,
|
|
448
|
+
row.kids.byteOffset + row.kids.byteLength,
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
for (const child of ids) {
|
|
452
|
+
if (child < 0) continue; // implicit byte leaf — no reverse edge
|
|
453
|
+
checked++;
|
|
454
|
+
assert.ok(
|
|
455
|
+
kidSet.has(`${child}:${row.id}`),
|
|
456
|
+
`kid row (${child} → ${row.id}) missing — the upward climb dead-ends`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
assert.ok(checked > 0, "no mixed branches checked — corpus too trivial");
|
|
461
|
+
|
|
462
|
+
// Reverse → forward: no kid row points at a parent that does not list it.
|
|
463
|
+
const kidsOf = new Map(
|
|
464
|
+
db.prepare(
|
|
465
|
+
"SELECT id, kids FROM node WHERE kids IS NOT NULL AND length(kids) > 0",
|
|
466
|
+
).all().map((r) => [
|
|
467
|
+
r.id,
|
|
468
|
+
new Set(
|
|
469
|
+
new Int32Array(
|
|
470
|
+
r.kids.buffer.slice(
|
|
471
|
+
r.kids.byteOffset,
|
|
472
|
+
r.kids.byteOffset + r.kids.byteLength,
|
|
473
|
+
),
|
|
474
|
+
),
|
|
475
|
+
),
|
|
476
|
+
]),
|
|
477
|
+
);
|
|
478
|
+
for (const r of db.prepare("SELECT child, parent FROM kid").all()) {
|
|
479
|
+
const set = kidsOf.get(r.parent);
|
|
480
|
+
assert.ok(
|
|
481
|
+
set !== undefined && set.has(r.child),
|
|
482
|
+
`stale kid row (${r.child} → ${r.parent}): parent does not list child`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
db.close();
|
|
486
|
+
} finally {
|
|
487
|
+
cleanup(stem);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// 21-partial.test.mjs — leveraging the PARTIAL results of an experience.
|
|
2
|
+
//
|
|
3
|
+
// Sema learns a fact or experience as a continuation EDGE between two interned
|
|
4
|
+
// trees, and answers by resonating a query to a learnt node and following the
|
|
5
|
+
// graph. The recall machinery has always treated a learnt CONTEXT as reachable
|
|
6
|
+
// down to its interior — `link` reach-indexes the whole `from` subtree, and the
|
|
7
|
+
// consensus climb (test 17) resonates a reworded QUESTION's distinctive
|
|
8
|
+
// sub-regions and climbs the parents-DAG to the context they agree on.
|
|
9
|
+
//
|
|
10
|
+
// The CONTINUATION seat was the missing half. A query that names only a PORTION
|
|
11
|
+
// of a stored answer — a distinctive interior slice of an experience, an
|
|
12
|
+
// intermediate node of the continuation's DAG — had nothing to resonate to: only
|
|
13
|
+
// the answer ROOT was indexed, so the answer's discriminative interior was
|
|
14
|
+
// invisible and a partial-answer query landed on noise. Yet this is exactly the
|
|
15
|
+
// symmetric case: an intermediate node of a fact/experience should be a
|
|
16
|
+
// first-class resonance anchor, reachable and climbable like any other, so the
|
|
17
|
+
// graph can reason from the part to the whole.
|
|
18
|
+
//
|
|
19
|
+
// The fix completes that symmetry with the machinery already there:
|
|
20
|
+
// • `link` reach-indexes BOTH subtrees, so every experience's interior (context
|
|
21
|
+
// AND continuation) is resonance-findable — while keeping each ROOT the sole
|
|
22
|
+
// MERGE target, so enlarging reach never enlarges the merge candidate set
|
|
23
|
+
// (the over-merge pathology the lazy index exists to avoid);
|
|
24
|
+
// • `edgeAncestors` collects a node bearing an edge in EITHER direction, so the
|
|
25
|
+
// climb grounds an answer-interior slice on the continuation that owns it,
|
|
26
|
+
// exactly as it already grounds a question slice on its context.
|
|
27
|
+
//
|
|
28
|
+
// These assertions pin the capability: a query that is only an INTERIOR SLICE of
|
|
29
|
+
// a stored answer resolves to its own experience, not a random neighbour. The
|
|
30
|
+
// aggregate bar clears comfortably with the fix and collapses without it (whole-
|
|
31
|
+
// answer-root indexing answers essentially none of these), so a regression that
|
|
32
|
+
// re-buries the continuation interior fails here.
|
|
33
|
+
|
|
34
|
+
import { test } from "node:test";
|
|
35
|
+
import assert from "node:assert/strict";
|
|
36
|
+
import { Mind } from "../dist/src/index.js";
|
|
37
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
38
|
+
|
|
39
|
+
// Each experience's ANSWER is a rich composite; its distinctive content lives in
|
|
40
|
+
// INTERIOR nodes of the continuation's DAG, never trained as a context of its own.
|
|
41
|
+
const FACTS = [
|
|
42
|
+
[
|
|
43
|
+
"Describe the Apollo 11 mission",
|
|
44
|
+
"Apollo 11 landed Neil Armstrong and Buzz Aldrin on the Moon in 1969.",
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"Describe the Voyager probes",
|
|
48
|
+
"The Voyager probes left the solar system carrying a golden record of Earth.",
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"Describe the Hubble telescope",
|
|
52
|
+
"Hubble orbits Earth and has photographed galaxies billions of light years away.",
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"Describe the theory of relativity",
|
|
56
|
+
"Einstein's relativity showed that space and time bend around mass and energy.",
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"Describe the Great Barrier Reef",
|
|
60
|
+
"The Great Barrier Reef is the largest living structure made by tiny coral polyps.",
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"Describe the printing press",
|
|
64
|
+
"Gutenberg's printing press spread books and ideas across Europe in the fifteenth century.",
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"Describe the human heart",
|
|
68
|
+
"The human heart pumps blood through four chambers to feed every cell with oxygen.",
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"Describe the Amazon rainforest",
|
|
72
|
+
"The Amazon rainforest produces much of the planet's oxygen and shelters countless species.",
|
|
73
|
+
],
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// [interior slice of the answer (NEVER a whole answer, NEVER a context), expected
|
|
77
|
+
// FACT index]. Each is a fragment buried inside one continuation's DAG.
|
|
78
|
+
const PROBES = [
|
|
79
|
+
["Neil Armstrong and Buzz Aldrin", 0],
|
|
80
|
+
["landed on the Moon in 1969", 0],
|
|
81
|
+
["a golden record of Earth", 1],
|
|
82
|
+
["left the solar system carrying", 1],
|
|
83
|
+
["galaxies billions of light years away", 2],
|
|
84
|
+
["space and time bend around mass", 3],
|
|
85
|
+
["the largest living structure", 4],
|
|
86
|
+
["made by tiny coral polyps", 4],
|
|
87
|
+
["spread books and ideas across Europe", 5],
|
|
88
|
+
["pumps blood through four chambers", 6],
|
|
89
|
+
["feed every cell with oxygen", 6],
|
|
90
|
+
["shelters countless species", 7],
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
94
|
+
// A partial-answer query is "resolved" when it lands on its OWN experience —
|
|
95
|
+
// either seat is correct (Sema may voice the question or the answer of the pair,
|
|
96
|
+
// by seat symmetry). Match a distinctive interior slice of either seat.
|
|
97
|
+
const resolvesTo = (ans, [q, a]) => {
|
|
98
|
+
const g = norm(ans);
|
|
99
|
+
return g.includes(norm(a).slice(1, 20)) || g.includes(norm(q).slice(1, 20));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
async function build(seed) {
|
|
103
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
104
|
+
const mind = new Mind({ seed, store });
|
|
105
|
+
await mind.ingest(FACTS);
|
|
106
|
+
return { store, mind };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test("a slice of a stored answer's INTERIOR resolves to its own experience", async () => {
|
|
110
|
+
const { store, mind } = await build(7);
|
|
111
|
+
const got = [];
|
|
112
|
+
for (const [q, idx] of PROBES) {
|
|
113
|
+
const ans = await mind.respondText(q);
|
|
114
|
+
got.push({ q, ok: resolvesTo(ans, FACTS[idx]), ans: norm(ans) });
|
|
115
|
+
}
|
|
116
|
+
await store.close();
|
|
117
|
+
|
|
118
|
+
const total = got.filter((g) => g.ok).length;
|
|
119
|
+
// The fix scores well over half here; whole-answer-root indexing (the old
|
|
120
|
+
// behaviour) answers essentially none of these — every probe is an interior
|
|
121
|
+
// slice, indexed only once its experience's whole subtree is reach-indexed.
|
|
122
|
+
// The bar is set generously below the fix's score to absorb ANN/codec jitter
|
|
123
|
+
// while a reversion (re-burying the continuation interior) drops to ~0 and
|
|
124
|
+
// fails.
|
|
125
|
+
assert.ok(
|
|
126
|
+
total >= 5,
|
|
127
|
+
`only ${total}/${PROBES.length} interior-slice queries resolved — ` +
|
|
128
|
+
`expected ≥ 5 (the continuation interior must be a resonance anchor)\n` +
|
|
129
|
+
got.filter((g) => !g.ok).map((g) =>
|
|
130
|
+
` ✗ "${g.q}" → ${g.ans.slice(0, 44)}`
|
|
131
|
+
).join("\n"),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// The sharpest case: a distinctive interior slice must select ITS experience over
|
|
136
|
+
// the seven frame-sharing neighbours, not merely return SOME stored fact.
|
|
137
|
+
test("a distinctive answer-interior slice selects its own experience, not a neighbour", async () => {
|
|
138
|
+
const { store, mind } = await build(42);
|
|
139
|
+
const ans = norm(await mind.respondText("Neil Armstrong and Buzz Aldrin"));
|
|
140
|
+
await store.close();
|
|
141
|
+
assert.ok(
|
|
142
|
+
resolvesTo(ans, FACTS[0]),
|
|
143
|
+
`the Apollo-interior slice resolved to "${ans.slice(0, 52)}" — ` +
|
|
144
|
+
`expected the Apollo 11 experience`,
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// The capability must not cost the whole-fact recall it generalises: asking a
|
|
149
|
+
// full context still answers with its trained continuation.
|
|
150
|
+
test("whole-fact recall is preserved alongside partial-result recall", async () => {
|
|
151
|
+
const { store, mind } = await build(7);
|
|
152
|
+
const whole = norm(await mind.respondText("Describe the Apollo 11 mission"));
|
|
153
|
+
await store.close();
|
|
154
|
+
assert.ok(
|
|
155
|
+
whole.includes(norm(FACTS[0][1]).slice(1, 20)),
|
|
156
|
+
`whole-fact recall regressed: "${whole.slice(0, 52)}"`,
|
|
157
|
+
);
|
|
158
|
+
});
|