@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,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
+ });