@hviana/sema 0.1.6 → 0.1.8
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/dist/example/train_base.js +42 -9
- package/dist/src/alu/src/parser.d.ts +9 -0
- package/dist/src/alu/src/parser.js +110 -2
- package/dist/src/canon.d.ts +26 -0
- package/dist/src/canon.js +57 -0
- package/dist/src/geometry.d.ts +13 -2
- package/dist/src/geometry.js +101 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/mind/attention.js +11 -7
- package/dist/src/mind/learning.js +33 -1
- package/dist/src/mind/match.d.ts +4 -2
- package/dist/src/mind/match.js +30 -6
- package/dist/src/mind/mechanisms/cast.js +18 -4
- package/dist/src/mind/mechanisms/confluence.js +13 -1
- package/dist/src/mind/mechanisms/recall.js +115 -31
- package/dist/src/mind/mind.d.ts +138 -12
- package/dist/src/mind/mind.js +284 -22
- package/dist/src/mind/primitives.d.ts +22 -2
- package/dist/src/mind/primitives.js +95 -6
- package/dist/src/mind/recognition.js +31 -8
- package/dist/src/mind/traverse.d.ts +13 -0
- package/dist/src/mind/traverse.js +41 -0
- package/dist/src/mind/types.d.ts +33 -21
- package/dist/src/store-sqlite.d.ts +10 -0
- package/dist/src/store-sqlite.js +58 -0
- package/dist/src/store.d.ts +23 -0
- package/dist/src/store.js +13 -2
- package/example/train_base.ts +49 -9
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/alu/src/parser.ts +105 -4
- package/src/canon.ts +65 -0
- package/src/geometry.ts +105 -1
- package/src/index.ts +1 -0
- package/src/mind/attention.ts +11 -6
- package/src/mind/learning.ts +39 -1
- package/src/mind/match.ts +29 -6
- package/src/mind/mechanisms/cast.ts +21 -6
- package/src/mind/mechanisms/confluence.ts +15 -1
- package/src/mind/mechanisms/recall.ts +132 -41
- package/src/mind/mind.ts +396 -22
- package/src/mind/primitives.ts +109 -7
- package/src/mind/recognition.ts +36 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +30 -21
- package/src/store-sqlite.ts +76 -0
- package/src/store.ts +46 -2
- package/test/13-conversation.test.mjs +240 -20
- package/test/35-prefix-edge.test.mjs +86 -0
|
@@ -14,14 +14,10 @@
|
|
|
14
14
|
// The assertions describe BEHAVIOUR only — they never mention how context is
|
|
15
15
|
// represented — so they stay valid for any implementation.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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."
|
|
17
|
+
// Sema trains on the raw concatenation of prior turns and queries the same
|
|
18
|
+
// accumulated bytes at inference. The Conversation API tracks turn-boundary
|
|
19
|
+
// offsets explicitly so no separator character is needed — the geometry never
|
|
20
|
+
// inspects content to find turn boundaries.
|
|
25
21
|
// ─────────────────────────────────────────────────────────────────────────
|
|
26
22
|
|
|
27
23
|
import { test } from "node:test";
|
|
@@ -34,16 +30,14 @@ const newMind = () => new Mind({ seed: 7 });
|
|
|
34
30
|
// ═══════════════════════════════════════════════════════════════════════
|
|
35
31
|
// teachConversation — store one conversation (an ordered list of turns).
|
|
36
32
|
//
|
|
37
|
-
// Accumulate
|
|
38
|
-
// full conversation history:
|
|
39
|
-
//
|
|
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.
|
|
33
|
+
// Accumulate prior turns by raw concatenation — no separator character.
|
|
34
|
+
// Each episode carries the full conversation history: (t₀ → t₁),
|
|
35
|
+
// (t₀+t₁ → t₂), (t₀+t₁+t₂ → t₃) … where + is byte concatenation.
|
|
43
36
|
// ═══════════════════════════════════════════════════════════════════════
|
|
44
37
|
async function teachConversation(mind, turns) {
|
|
38
|
+
let context = "";
|
|
45
39
|
for (let i = 0; i + 1 < turns.length; i++) {
|
|
46
|
-
|
|
40
|
+
context += turns[i];
|
|
47
41
|
await mind.ingest(context, turns[i + 1]);
|
|
48
42
|
}
|
|
49
43
|
}
|
|
@@ -51,13 +45,24 @@ async function teachConversation(mind, turns) {
|
|
|
51
45
|
// ═══════════════════════════════════════════════════════════════════════
|
|
52
46
|
// predictNext — given the turns spoken so far, predict the next turn.
|
|
53
47
|
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
48
|
+
// Uses the Conversation API: beginConversation → addTurn for each prior
|
|
49
|
+
// turn (BOTH speakers' lines are history being replayed, not questions to
|
|
50
|
+
// answer — respondTurn would answer each one AND append its own reply to
|
|
51
|
+
// the context) → respondTurnText for the final turn → endConversation.
|
|
52
|
+
// Turns are raw strings, concatenated by the Mind — no separator.
|
|
57
53
|
// ═══════════════════════════════════════════════════════════════════════
|
|
58
54
|
async function predictNext(mind, priorTurns) {
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
if (priorTurns.length === 0) return "";
|
|
56
|
+
const conv = mind.beginConversation();
|
|
57
|
+
for (let i = 0; i < priorTurns.length - 1; i++) {
|
|
58
|
+
mind.addTurn(conv, priorTurns[i]);
|
|
59
|
+
}
|
|
60
|
+
const { response } = await mind.respondTurnText(
|
|
61
|
+
conv,
|
|
62
|
+
priorTurns[priorTurns.length - 1],
|
|
63
|
+
);
|
|
64
|
+
mind.endConversation(conv);
|
|
65
|
+
return response;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
// A small convenience: teach a conversation, then ask for the continuation
|
|
@@ -226,3 +231,218 @@ test("D2: prediction is deterministic across runs", async () => {
|
|
|
226
231
|
};
|
|
227
232
|
assert.equal(await run(), await run());
|
|
228
233
|
});
|
|
234
|
+
|
|
235
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
236
|
+
// Section E — Conversation API lifecycle & cache management
|
|
237
|
+
//
|
|
238
|
+
// Tests for the beginConversation / respondTurn / endConversation API
|
|
239
|
+
// itself, independent of the conversation-training pattern above.
|
|
240
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
241
|
+
|
|
242
|
+
test("E1: begin → respondTurn → end completes a single-turn conversation", async () => {
|
|
243
|
+
const mind = newMind();
|
|
244
|
+
await mind.ingest("hello", "world");
|
|
245
|
+
|
|
246
|
+
const conv = mind.beginConversation();
|
|
247
|
+
const { response, state } = await mind.respondTurnText(conv, "hello");
|
|
248
|
+
assert.equal(response, "world");
|
|
249
|
+
// The reply is part of the exchange: one boundary, between the turn and
|
|
250
|
+
// the appended reply.
|
|
251
|
+
assert.equal(state.boundaries.length, 1);
|
|
252
|
+
mind.endConversation(conv);
|
|
253
|
+
await mind.store.close();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("E2: multi-turn conversation accumulates context and boundaries", async () => {
|
|
257
|
+
const mind = newMind();
|
|
258
|
+
await teachConversation(mind, CAT);
|
|
259
|
+
|
|
260
|
+
const conv = mind.beginConversation();
|
|
261
|
+
// First turn establishes the subject — scripted history, so it is FED
|
|
262
|
+
// (addTurn), not asked.
|
|
263
|
+
mind.addTurn(conv, "I adopted a cat");
|
|
264
|
+
|
|
265
|
+
// Second turn is the pivot query.
|
|
266
|
+
const t2 = await mind.respondTurnText(conv, "what is its name?");
|
|
267
|
+
// Two boundaries: after the first turn, and after the pivot (the reply
|
|
268
|
+
// is part of the exchange).
|
|
269
|
+
assert.equal(t2.state.boundaries.length, 2);
|
|
270
|
+
assert.equal(t2.response, "her name is Whiskers");
|
|
271
|
+
|
|
272
|
+
mind.endConversation(conv);
|
|
273
|
+
await mind.store.close();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("E3: save → end → restore → continue works", async () => {
|
|
277
|
+
const mind = newMind();
|
|
278
|
+
await teachConversation(mind, CAT);
|
|
279
|
+
|
|
280
|
+
// First two turns (context fed, pivot asked).
|
|
281
|
+
const conv = mind.beginConversation();
|
|
282
|
+
mind.addTurn(conv, "I adopted a cat");
|
|
283
|
+
const { state: saved } = await mind.respondTurnText(
|
|
284
|
+
conv,
|
|
285
|
+
"what is its name?",
|
|
286
|
+
);
|
|
287
|
+
assert.equal(saved.boundaries.length, 2);
|
|
288
|
+
|
|
289
|
+
// Save and end.
|
|
290
|
+
mind.endConversation(conv);
|
|
291
|
+
|
|
292
|
+
// Restore from the saved state into a new handle.
|
|
293
|
+
const conv2 = mind.beginConversation(saved);
|
|
294
|
+
// Boundaries are intact from the restore, and the conversation continues
|
|
295
|
+
// from where it left off.
|
|
296
|
+
const state2 = mind.addTurn(conv2, "and she likes to nap");
|
|
297
|
+
assert.equal(state2.boundaries.length, saved.boundaries.length + 1);
|
|
298
|
+
const { response } = await mind.respondTurnText(conv2, "what is its name?");
|
|
299
|
+
assert.ok(typeof response === "string");
|
|
300
|
+
|
|
301
|
+
mind.endConversation(conv2);
|
|
302
|
+
await mind.store.close();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("E4: endConversation is idempotent", async () => {
|
|
306
|
+
const mind = newMind();
|
|
307
|
+
const conv = mind.beginConversation();
|
|
308
|
+
mind.endConversation(conv);
|
|
309
|
+
// Second end on the same handle — must not throw.
|
|
310
|
+
mind.endConversation(conv);
|
|
311
|
+
await mind.store.close();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("E5: respondTurn on an ended conversation throws", async () => {
|
|
315
|
+
const mind = newMind();
|
|
316
|
+
const conv = mind.beginConversation();
|
|
317
|
+
mind.endConversation(conv);
|
|
318
|
+
await assert.rejects(
|
|
319
|
+
() => mind.respondTurn(conv, "hello"),
|
|
320
|
+
/not found/,
|
|
321
|
+
);
|
|
322
|
+
await mind.store.close();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("E6: conversationState returns null after endConversation", async () => {
|
|
326
|
+
const mind = newMind();
|
|
327
|
+
const conv = mind.beginConversation();
|
|
328
|
+
mind.endConversation(conv);
|
|
329
|
+
assert.equal(mind.conversationState(conv), null);
|
|
330
|
+
await mind.store.close();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("E7: multiple concurrent conversations are independent", async () => {
|
|
334
|
+
const mind = newMind();
|
|
335
|
+
await teachConversation(mind, CAT);
|
|
336
|
+
await teachConversation(mind, DOG);
|
|
337
|
+
|
|
338
|
+
const catConv = mind.beginConversation();
|
|
339
|
+
const dogConv = mind.beginConversation();
|
|
340
|
+
|
|
341
|
+
// Feed the distinguishing context to each.
|
|
342
|
+
mind.addTurn(catConv, "I adopted a cat");
|
|
343
|
+
mind.addTurn(dogConv, "I adopted a dog");
|
|
344
|
+
|
|
345
|
+
// The same pivot turn — different answers.
|
|
346
|
+
const catR = await mind.respondTurnText(catConv, "what is its name?");
|
|
347
|
+
const dogR = await mind.respondTurnText(dogConv, "what is its name?");
|
|
348
|
+
|
|
349
|
+
assert.equal(catR.response, "her name is Whiskers");
|
|
350
|
+
assert.equal(dogR.response, "his name is Rex");
|
|
351
|
+
assert.notEqual(catR.response, dogR.response);
|
|
352
|
+
|
|
353
|
+
// Each conversation has its own boundaries (turn|pivot, pivot|reply).
|
|
354
|
+
assert.equal(catR.state.boundaries.length, 2);
|
|
355
|
+
assert.equal(dogR.state.boundaries.length, 2);
|
|
356
|
+
|
|
357
|
+
// Ending one does not affect the other.
|
|
358
|
+
mind.endConversation(catConv);
|
|
359
|
+
assert.equal(mind.conversationState(catConv), null);
|
|
360
|
+
assert.notEqual(mind.conversationState(dogConv), null);
|
|
361
|
+
|
|
362
|
+
// End the second — both are now gone.
|
|
363
|
+
mind.endConversation(dogConv);
|
|
364
|
+
assert.equal(mind.conversationState(dogConv), null);
|
|
365
|
+
|
|
366
|
+
// Ending again is idempotent.
|
|
367
|
+
mind.endConversation(dogConv);
|
|
368
|
+
await mind.store.close();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("E8: respondTurn with a forged handle throws", async () => {
|
|
372
|
+
const mind = newMind();
|
|
373
|
+
// { id: 999 } was never returned by beginConversation.
|
|
374
|
+
await assert.rejects(
|
|
375
|
+
() => mind.respondTurn({ id: 999 }, "hello"),
|
|
376
|
+
/not found/,
|
|
377
|
+
);
|
|
378
|
+
await mind.store.close();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("E9: respond and respondTurn give the same answer for the same cumulative bytes", async () => {
|
|
382
|
+
const mind = newMind();
|
|
383
|
+
await teachConversation(mind, CAT);
|
|
384
|
+
|
|
385
|
+
// Via respond() — raw concatenation, same as respondTurn.
|
|
386
|
+
const viaRespond = await mind.respondText(
|
|
387
|
+
"I adopted a catwhat is its name?",
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Via the Conversation API — the first turn is fed (addTurn keeps the
|
|
391
|
+
// cumulative bytes identical to the raw concatenation above; respondTurn
|
|
392
|
+
// would append its own reply between the turns).
|
|
393
|
+
const conv = mind.beginConversation();
|
|
394
|
+
mind.addTurn(conv, "I adopted a cat");
|
|
395
|
+
const { response: viaConv } = await mind.respondTurnText(
|
|
396
|
+
conv,
|
|
397
|
+
"what is its name?",
|
|
398
|
+
);
|
|
399
|
+
mind.endConversation(conv);
|
|
400
|
+
|
|
401
|
+
assert.equal(viaRespond, viaConv);
|
|
402
|
+
await mind.store.close();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("E10: an empty turn neither grows the context nor marks a boundary", async () => {
|
|
406
|
+
const mind = newMind();
|
|
407
|
+
await teachConversation(mind, CAT);
|
|
408
|
+
|
|
409
|
+
const conv = mind.beginConversation();
|
|
410
|
+
mind.addTurn(conv, "I adopted a cat");
|
|
411
|
+
const before = mind.conversationState(conv);
|
|
412
|
+
|
|
413
|
+
// Empty turns are no turns: addTurn and respondTurn alike must leave the
|
|
414
|
+
// context bytes and the (strictly increasing) boundaries untouched.
|
|
415
|
+
const viaAdd = mind.addTurn(conv, "");
|
|
416
|
+
assert.equal(viaAdd.context.length, before.context.length);
|
|
417
|
+
assert.deepEqual(viaAdd.boundaries, before.boundaries);
|
|
418
|
+
|
|
419
|
+
// respondTurn("") answers the ACCUMULATED context (an empty turn adds no
|
|
420
|
+
// bytes and no boundary of its own) — only its reply may grow the state,
|
|
421
|
+
// through the same append+boundary path as any turn.
|
|
422
|
+
const { response: r0, state: viaTurn } = await mind.respondTurnText(
|
|
423
|
+
conv,
|
|
424
|
+
"",
|
|
425
|
+
);
|
|
426
|
+
const replyLen = new TextEncoder().encode(r0).length;
|
|
427
|
+
assert.equal(viaTurn.context.length, before.context.length + replyLen);
|
|
428
|
+
assert.equal(
|
|
429
|
+
viaTurn.boundaries.length,
|
|
430
|
+
before.boundaries.length + (replyLen > 0 ? 1 : 0),
|
|
431
|
+
);
|
|
432
|
+
for (let i = 1; i < viaTurn.boundaries.length; i++) {
|
|
433
|
+
assert.ok(viaTurn.boundaries[i] > viaTurn.boundaries[i - 1]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// The conversation still answers afterwards — the zero-growth refold must
|
|
437
|
+
// not have corrupted the pyramid's shared raw interiors. (The empty
|
|
438
|
+
// turn's own reply may have advanced the exchange, so the answer is
|
|
439
|
+
// asserted by content, not byte-exactly.)
|
|
440
|
+
const { response } = await mind.respondTurnText(conv, "what is its name?");
|
|
441
|
+
assert.ok(
|
|
442
|
+
response.includes("her name is Whiskers"),
|
|
443
|
+
`expected the name to surface, got ${JSON.stringify(response)}`,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
mind.endConversation(conv);
|
|
447
|
+
await mind.store.close();
|
|
448
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// test/35-prefix-edge.test.mjs — RC7: right-edge suffix inheritance
|
|
2
|
+
//
|
|
3
|
+
// When ingestPair learns C → D, every right-aligned byte suffix of C
|
|
4
|
+
// that is already a known form inherits the edge. Gate: ≥ 2 structural
|
|
5
|
+
// parents, or (halo > 0 ∧ already an edge source). Pure answers do
|
|
6
|
+
// not qualify — they are destinations, not sources.
|
|
7
|
+
//
|
|
8
|
+
// All phrases verified via instrumentation first (see MISTAKES.md).
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { Mind } from "../dist/src/index.js";
|
|
13
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
14
|
+
import { resolve } from "../dist/src/mind/primitives.js";
|
|
15
|
+
|
|
16
|
+
const enc = new TextEncoder();
|
|
17
|
+
const dec = new TextDecoder();
|
|
18
|
+
const text = (b) => dec.decode(b.filter((x) => x !== 0));
|
|
19
|
+
const mk = (s = 7) =>
|
|
20
|
+
new Mind({ seed: s, store: new SQliteStore({ path: ":memory:" }) });
|
|
21
|
+
|
|
22
|
+
// Establish via pair (gives halo mass). The second deposit where the
|
|
23
|
+
// form appears as a fold-tree constituent gives ≥ 1 structural parent.
|
|
24
|
+
async function establish(m, phrase) {
|
|
25
|
+
await m.ingest([phrase, "established"]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("A1 — right-aligned known suffix gains edge", async () => {
|
|
29
|
+
const m = mk();
|
|
30
|
+
await establish(m, "planet in our solar system");
|
|
31
|
+
const sid = resolve(m, enc.encode("planet in our solar system"));
|
|
32
|
+
|
|
33
|
+
await m.ingest([
|
|
34
|
+
"What is the largest planet in our solar system",
|
|
35
|
+
"Jupiter.",
|
|
36
|
+
]);
|
|
37
|
+
const next = m.store.nextFirst(sid, 10).map((n) => text(m.store.bytes(n)));
|
|
38
|
+
assert.ok(next.some((s) => s.includes("Jupiter")));
|
|
39
|
+
await m.store.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("A2 — suffix NOT previously known gets NO edge", async () => {
|
|
43
|
+
const m = mk();
|
|
44
|
+
const before = m.store.edgeSourceCount();
|
|
45
|
+
await m.ingest(["What is the largest planet", "Jupiter."]);
|
|
46
|
+
assert.equal(m.store.edgeSourceCount() - before, 1);
|
|
47
|
+
await m.store.close();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("A3 — unestablished ≥ W suffix does NOT inherit", async () => {
|
|
51
|
+
const m = mk();
|
|
52
|
+
const before = m.store.edgeSourceCount();
|
|
53
|
+
// "prefix abcd" has "abcd" as a ≥W suffix, but it was never established.
|
|
54
|
+
await m.ingest(["prefix abcd", "answer"]);
|
|
55
|
+
// Only the full context should be a new edge source — no suffix edge.
|
|
56
|
+
assert.equal(m.store.edgeSourceCount() - before, 1);
|
|
57
|
+
await m.store.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("B1 — inherited edge is present after propagation", async () => {
|
|
61
|
+
const m = mk();
|
|
62
|
+
await establish(m, "largest planet in our solar system");
|
|
63
|
+
await m.ingest([
|
|
64
|
+
"What is the largest planet in our solar system",
|
|
65
|
+
"Jupiter.",
|
|
66
|
+
]);
|
|
67
|
+
const sid = resolve(m, enc.encode("largest planet in our solar system"));
|
|
68
|
+
const next = m.store.nextFirst(sid, 10).map((n) => text(m.store.bytes(n)));
|
|
69
|
+
assert.ok(next.some((s) => s.includes("Jupiter")));
|
|
70
|
+
await m.store.close();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("B2 — bidirectional pair via suffix inheritance", async () => {
|
|
74
|
+
const m = mk();
|
|
75
|
+
await establish(m, "thank you");
|
|
76
|
+
await establish(m, "merci");
|
|
77
|
+
await m.ingest(["translates to merci", "thank you"]);
|
|
78
|
+
await m.ingest(["translates to thank you", "merci"]);
|
|
79
|
+
|
|
80
|
+
const sid = resolve(m, enc.encode("thank you"));
|
|
81
|
+
const next = m.store.nextFirst(sid, 10).map((n) => text(m.store.bytes(n)));
|
|
82
|
+
// The establish edge ("established") is still there, but the inherited
|
|
83
|
+
// edge ("merci") must ALSO be present.
|
|
84
|
+
assert.ok(next.some((s) => s.includes("merci")));
|
|
85
|
+
await m.store.close();
|
|
86
|
+
});
|