@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,65 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Mind } from "../dist/src/index.js";
4
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
5
+
6
+ const txt = (r) => new TextDecoder().decode(r.bytes).replace(/\u0000+/g, "");
7
+ const img = (n) => {
8
+ const g = { width: 4, height: 4, channels: 1, data: new Uint8Array(16) };
9
+ for (let i = 0; i < 16; i++) g.data[i] = (i * 13 + n * 41) & 0xff;
10
+ return g;
11
+ };
12
+
13
+ test("a 2KB document roundtrips exactly", async () => {
14
+ const m = new Mind({
15
+ seed: 7,
16
+ });
17
+ const doc = ("memory is the model. " + "the river cuts by content. ").repeat(
18
+ 45,
19
+ ).slice(0, 2048);
20
+ const bytes = new TextEncoder().encode(doc);
21
+ const t = await m.ingest(bytes);
22
+ const out = await m.express(t.v);
23
+ assert.deepEqual([...out], [...bytes]);
24
+ });
25
+
26
+ test("respond accepts raw bytes", async () => {
27
+ const m = new Mind({
28
+ seed: 7,
29
+ });
30
+ await m.ingest([["what is ice?", "ice is frozen water"]]);
31
+ const r = await m.respond(new TextEncoder().encode("what is ice?"));
32
+ assert.equal(txt(r), "ice is frozen water");
33
+ });
34
+
35
+ test("an image can be a context: respond(image) → caption", async () => {
36
+ const m = new Mind({
37
+ seed: 7,
38
+ });
39
+ await m.ingest([[img(3), "a small grey cat"]]);
40
+ assert.equal(await m.respondText(img(3)), "a small grey cat");
41
+ // Unknown image: may return null or a closest approximation.
42
+ const r = await m.respond(img(4));
43
+ assert.ok(r.v !== undefined, "respond returns a valid response");
44
+ });
45
+
46
+ test("train([]) is a no-op that does not throw", async () => {
47
+ const m = new Mind({
48
+ seed: 7,
49
+ });
50
+ await m.ingest([]);
51
+ assert.equal((await m.respond("anything")).v, null);
52
+ });
53
+
54
+ test("very long single-token inputs roundtrip correctly", async () => {
55
+ const m = new Mind({
56
+ seed: 7,
57
+ });
58
+ for (const n of [513, 1025]) {
59
+ const bytes = new TextEncoder().encode("z".repeat(n));
60
+ const t = await m.ingest(bytes);
61
+ const out = await m.express(t.v);
62
+ assert.equal(out.length, n);
63
+ assert.ok([...out].every((b) => b === 122));
64
+ }
65
+ });
@@ -0,0 +1,180 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Mind } from "../dist/src/index.js";
4
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
5
+
6
+ const img = (n) => {
7
+ const g = { width: 4, height: 4, channels: 1, data: new Uint8Array(16) };
8
+ for (let i = 0; i < 16; i++) g.data[i] = (i * 13 + n * 41) & 0xff;
9
+ return g;
10
+ };
11
+
12
+ // The mechanism has no concept of language: perception cuts bytes by
13
+ // outline, recall completes episodes, thinking rewrites licensed parts.
14
+ // These tests assert the LAW — same behaviour regardless of script,
15
+ // script-mixing, or where in the stream a known part stands.
16
+
17
+ const TABLE = [
18
+ ["1+2", "3"],
19
+ ["2+2", "4"],
20
+ ["2+3", "5"],
21
+ ["3+3", "6"],
22
+ ["3+5", "8"],
23
+ ["4+3", "7"],
24
+ ["2+5", "7"],
25
+ ["1+5", "6"],
26
+ ["6+1", "7"],
27
+ ["4+1", "5"],
28
+ ];
29
+ const txt = (r) => new TextDecoder().decode(r.bytes).replace(/\u0000+/g, "");
30
+
31
+ test("concepts fuse across scripts: 氷 + hielo + ice", async () => {
32
+ for (const seed of [1, 7, 42]) {
33
+ const m = new Mind({
34
+ seed,
35
+ });
36
+ await m.ingest([
37
+ ["ice", "ice is frozen water"],
38
+ [img(1), "ice"],
39
+ [img(2), "ice"],
40
+ [img(1), "hielo"],
41
+ [img(2), "hielo"],
42
+ [img(1), "氷"],
43
+ [img(2), "氷"],
44
+ ]);
45
+ assert.equal(
46
+ await m.respondText("氷"),
47
+ "氷 is frozen water",
48
+ `seed ${seed}`,
49
+ );
50
+ assert.equal(
51
+ await m.respondText("hielo"),
52
+ "hielo is frozen water",
53
+ `seed ${seed}`,
54
+ );
55
+ }
56
+ });
57
+
58
+ test("a known part answers wherever it stands, whatever surrounds it", async () => {
59
+ // Frame language and fact position are irrelevant to the law. Before
60
+ // the byte-window glance, mid-stream facts answered on ~2/8 seeds and
61
+ // stream-initial on 8/8 — a position accident posing as capability.
62
+ const frames = [
63
+ "2+2 equals what?",
64
+ "¿cuánto es 2+2 dime?",
65
+ "2+2 は何ですか",
66
+ "dime 2+2 は何 please",
67
+ ];
68
+ for (const seed of [1, 7, 42, 99]) {
69
+ const m = new Mind({
70
+ seed,
71
+ });
72
+ await m.ingest(TABLE);
73
+ for (const q of frames) {
74
+ assert.equal(txt(await m.respond(q)), "4", `seed ${seed} "${q}"`);
75
+ }
76
+ }
77
+ });
78
+
79
+ // ARTICULATION — the production-direction half of the concept law.
80
+ // Recognition settles any name onto its gist (comprehension crosses
81
+ // names inward); articulation chooses, among gist-equal forms, the
82
+ // ASKER's form (production crosses names outward). No translation
83
+ // machinery: the conceive merge read outward.
84
+
85
+ const ground = (name, a, b) => [
86
+ [img(a), name],
87
+ [img(b), name],
88
+ ];
89
+
90
+ test("the answer speaks in the asker's name — across scripts", async () => {
91
+ for (const seed of [1, 7, 42]) {
92
+ const m = new Mind({
93
+ seed,
94
+ });
95
+ await m.ingest([
96
+ ["ice", "ice is frozen water"],
97
+ ...ground("ice", 1, 2),
98
+ ...ground("hielo", 1, 2),
99
+ ...ground("氷", 1, 2),
100
+ ]);
101
+ assert.equal(
102
+ await m.respondText("hielo"),
103
+ "hielo is frozen water",
104
+ `seed ${seed}`,
105
+ );
106
+ assert.equal(
107
+ await m.respondText("氷"),
108
+ "氷 is frozen water",
109
+ `seed ${seed}`,
110
+ );
111
+ assert.equal(
112
+ await m.respondText("ice"),
113
+ "ice is frozen water",
114
+ `seed ${seed} own name unchanged`,
115
+ );
116
+ }
117
+ });
118
+
119
+ test("a name inside a mixed-script frame voices the answer", async () => {
120
+ const m = new Mind({
121
+ seed: 7,
122
+ });
123
+ await m.ingest([
124
+ ["ice", "ice is frozen water"],
125
+ ...ground("ice", 1, 2),
126
+ ...ground("氷", 1, 2),
127
+ ]);
128
+ assert.equal(await m.respondText("¿qué es 氷 ahora?"), "氷 is frozen water");
129
+ });
130
+
131
+ test("question-level + word-level concepts compose: cross-language QA, fully voiced", async () => {
132
+ for (const seed of [1, 7, 42]) {
133
+ const m = new Mind({
134
+ seed,
135
+ });
136
+ await m.ingest([
137
+ ["what is ice?", "ice is frozen water"],
138
+ ...ground("what is ice?", 5, 6),
139
+ ...ground("¿qué es hielo?", 5, 6),
140
+ ...ground("ice", 1, 2),
141
+ ...ground("hielo", 1, 2),
142
+ ]);
143
+ assert.equal(
144
+ await m.respondText("¿qué es hielo?"),
145
+ "hielo is frozen water",
146
+ `seed ${seed}`,
147
+ );
148
+ }
149
+ });
150
+
151
+ test("the floor does not name: single-byte forms never voice", async () => {
152
+ // digit answers can over-fuse by halo resemblance ({6,7}); a query's
153
+ // operand must never re-voice the answer
154
+ const TABLE = [
155
+ ["1+2", "3"],
156
+ ["2+2", "4"],
157
+ ["2+3", "5"],
158
+ ["3+3", "6"],
159
+ ["3+5", "8"],
160
+ ["4+3", "7"],
161
+ ["2+5", "7"],
162
+ ["1+5", "6"],
163
+ ["6+1", "7"],
164
+ ["4+1", "5"],
165
+ ];
166
+ for (const seed of [1, 7, 42]) {
167
+ const m = new Mind({
168
+ seed,
169
+ });
170
+ await m.ingest(TABLE);
171
+ for (const [q, want] of [["6+1", "7"], ["2+5", "7"], ["1+5", "6"]]) {
172
+ const r = await m.respond(q);
173
+ const got = new TextDecoder().decode(r.bytes).replace(/\u0000+/g, "");
174
+ assert.ok(
175
+ r.v === null || got === want,
176
+ `seed ${seed} "${q}" -> "${got}"`,
177
+ );
178
+ }
179
+ }
180
+ });
@@ -0,0 +1,228 @@
1
+ // 13-conversation.test.mjs — multi-turn conversation context.
2
+ //
3
+ // The question under test: when a conversation has many turns, does a later
4
+ // turn still have the context established earlier? Plain adjacency (each
5
+ // episode = previous turn → next turn) keeps only ONE turn of context, so it
6
+ // cannot tell apart two conversations that share a turn but differ earlier.
7
+ //
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+ // Two functions drive every test:
10
+ //
11
+ // teachConversation(mind, turns) — accumulate prior turns into the context
12
+ // predictNext(mind, priorTurns) — join prior turns and recall the next
13
+ //
14
+ // The assertions describe BEHAVIOUR only — they never mention how context is
15
+ // represented — so they stay valid for any implementation.
16
+ //
17
+ // Note: Sema trains on the accumulated context string ("turn0\nturn1\nturn2")
18
+ // and queries the same accumulated string at inference. This is not a
19
+ // weakness — LLMs work the same way: a chat model receives the full
20
+ // conversation history as its prompt, and that same history must be
21
+ // provided at inference to produce the next turn. The difference is in
22
+ // how the answer is produced: Sema composes it from learned graph forms;
23
+ // an LLM samples it from a parametric distribution. Neither is "just a
24
+ // lookup."
25
+ // ─────────────────────────────────────────────────────────────────────────
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
+ // ═══════════════════════════════════════════════════════════════════════
35
+ // teachConversation — store one conversation (an ordered list of turns).
36
+ //
37
+ // Accumulate every prior turn into the context so each episode carries the
38
+ // full conversation history: (t₀ → t₁), (t₀+t₁ → t₂), (t₀+t₁+t₂ → t₃) …
39
+ // A pivot turn ("what is its name?") that appears in two conversations now
40
+ // produces different episode vectors because the context side encodes which
41
+ // conversation it belongs to. All SEMA operations remain sublinear — the
42
+ // river cuts any context into O(log N) chunks regardless of length.
43
+ // ═══════════════════════════════════════════════════════════════════════
44
+ async function teachConversation(mind, turns) {
45
+ for (let i = 0; i + 1 < turns.length; i++) {
46
+ const context = turns.slice(0, i + 1).join("\n");
47
+ await mind.ingest(context, turns[i + 1]);
48
+ }
49
+ }
50
+
51
+ // ═══════════════════════════════════════════════════════════════════════
52
+ // predictNext — given the turns spoken so far, predict the next turn.
53
+ //
54
+ // Join every prior turn into one accumulated context, mirroring the storage
55
+ // pattern in teachConversation. The resulting query string is the exact
56
+ // context side of the matching episode, so recall resolves unambiguously.
57
+ // ═══════════════════════════════════════════════════════════════════════
58
+ async function predictNext(mind, priorTurns) {
59
+ const context = priorTurns.join("\n");
60
+ return await mind.respondText(context);
61
+ }
62
+
63
+ // A small convenience: teach a conversation, then ask for the continuation
64
+ // after the first `k` turns.
65
+ async function continueAfter(turns, k) {
66
+ const mind = newMind();
67
+ await teachConversation(mind, turns);
68
+ const out = await predictNext(mind, turns.slice(0, k));
69
+ await mind.store.close();
70
+ return out;
71
+ }
72
+
73
+ // ═══════════════════════════════════════════════════════════════════════
74
+ // Section A — basic completion (passes with one turn of context)
75
+ // ═══════════════════════════════════════════════════════════════════════
76
+
77
+ const CHAT = [
78
+ "good morning",
79
+ "hello there",
80
+ "are you ready to start?",
81
+ "yes, let us begin",
82
+ ];
83
+
84
+ test("A1: first turn completes", async () => {
85
+ assert.equal(await continueAfter(CHAT, 1), "hello there");
86
+ });
87
+
88
+ test("A2: middle turn completes", async () => {
89
+ assert.equal(await continueAfter(CHAT, 2), "are you ready to start?");
90
+ });
91
+
92
+ test("A3: last step completes", async () => {
93
+ assert.equal(await continueAfter(CHAT, 3), "yes, let us begin");
94
+ });
95
+
96
+ // ═══════════════════════════════════════════════════════════════════════
97
+ // Section B — context disambiguates a shared pivot turn (TARGET)
98
+ //
99
+ // Both conversations contain the identical turn "what is its name?". Adjacency
100
+ // binds that turn to two different answers, so without earlier context the
101
+ // continuation is ambiguous. The earlier turn ("a cat" vs "a dog") must decide.
102
+ // ═══════════════════════════════════════════════════════════════════════
103
+
104
+ const CAT = ["I adopted a cat", "what is its name?", "her name is Whiskers"];
105
+ const DOG = ["I adopted a dog", "what is its name?", "his name is Rex"];
106
+
107
+ async function teachBoth() {
108
+ const mind = newMind();
109
+ await teachConversation(mind, CAT);
110
+ await teachConversation(mind, DOG);
111
+ return mind;
112
+ }
113
+
114
+ test("B1: earlier context selects the cat's answer", async () => {
115
+ const mind = await teachBoth();
116
+ assert.equal(
117
+ await predictNext(mind, ["I adopted a cat", "what is its name?"]),
118
+ "her name is Whiskers",
119
+ );
120
+ await mind.store.close();
121
+ });
122
+
123
+ test("B2: earlier context selects the dog's answer", async () => {
124
+ const mind = await teachBoth();
125
+ assert.equal(
126
+ await predictNext(mind, ["I adopted a dog", "what is its name?"]),
127
+ "his name is Rex",
128
+ );
129
+ await mind.store.close();
130
+ });
131
+
132
+ test("B3: the same pivot yields DIFFERENT answers per conversation", async () => {
133
+ const mind = await teachBoth();
134
+ const a = await predictNext(mind, ["I adopted a cat", "what is its name?"]);
135
+ const b = await predictNext(mind, ["I adopted a dog", "what is its name?"]);
136
+ assert.notEqual(a, b);
137
+ await mind.store.close();
138
+ });
139
+
140
+ // ═══════════════════════════════════════════════════════════════════════
141
+ // Section C — context that lives SEVERAL turns back (TARGET)
142
+ //
143
+ // The distinguishing turn ("Japan" vs "Italy") is two turns before the shared
144
+ // pivot, so it is invisible to adjacency AND to any fixed small window. Only an
145
+ // accumulated context carries it forward.
146
+ // ═══════════════════════════════════════════════════════════════════════
147
+
148
+ const TRIP_JP = [
149
+ "we are planning a vacation",
150
+ "I want to visit Japan",
151
+ "what should we see first?",
152
+ "start with Tokyo",
153
+ ];
154
+ const TRIP_IT = [
155
+ "we are planning a vacation",
156
+ "I want to visit Italy",
157
+ "what should we see first?",
158
+ "start with Rome",
159
+ ];
160
+
161
+ async function teachTrips() {
162
+ const mind = newMind();
163
+ await teachConversation(mind, TRIP_JP);
164
+ await teachConversation(mind, TRIP_IT);
165
+ return mind;
166
+ }
167
+
168
+ test("C1: Japan context (2 turns back) selects Tokyo", async () => {
169
+ const mind = await teachTrips();
170
+ assert.equal(
171
+ await predictNext(mind, TRIP_JP.slice(0, 3)),
172
+ "start with Tokyo",
173
+ );
174
+ await mind.store.close();
175
+ });
176
+
177
+ test("C2: Italy context (2 turns back) selects Rome", async () => {
178
+ const mind = await teachTrips();
179
+ assert.equal(
180
+ await predictNext(mind, TRIP_IT.slice(0, 3)),
181
+ "start with Rome",
182
+ );
183
+ await mind.store.close();
184
+ });
185
+
186
+ // ═══════════════════════════════════════════════════════════════════════
187
+ // Section D — length & determinism (TARGET for the long-range case)
188
+ //
189
+ // A longer conversation that still hinges on an early distinguishing turn:
190
+ // there must be no per-conversation turn cap, and repeating the experiment
191
+ // must give the same answer.
192
+ // ═══════════════════════════════════════════════════════════════════════
193
+
194
+ const longTrip = (place, first) => [
195
+ "hello",
196
+ `I am organising a long trip to ${place}`,
197
+ "that sounds exciting",
198
+ "we have two weeks",
199
+ "and a generous budget",
200
+ "we love food and history",
201
+ "what should we see first?",
202
+ `start with ${first}`,
203
+ ];
204
+
205
+ test("D1: an early turn decides the answer many turns later", async () => {
206
+ const mind = newMind();
207
+ await teachConversation(mind, longTrip("Japan", "Tokyo"));
208
+ await teachConversation(mind, longTrip("Italy", "Rome"));
209
+ const jp = await predictNext(mind, longTrip("Japan", "Tokyo").slice(0, 7));
210
+ const it = await predictNext(mind, longTrip("Italy", "Rome").slice(0, 7));
211
+ assert.equal(jp, "start with Tokyo");
212
+ assert.equal(it, "start with Rome");
213
+ await mind.store.close();
214
+ });
215
+
216
+ test("D2: prediction is deterministic across runs", async () => {
217
+ const run = async () => {
218
+ const mind = newMind();
219
+ await teachConversation(mind, CAT);
220
+ const out = await predictNext(mind, [
221
+ "I adopted a cat",
222
+ "what is its name?",
223
+ ]);
224
+ await mind.store.close();
225
+ return out;
226
+ };
227
+ assert.equal(await run(), await run());
228
+ });