@hviana/sema 0.1.7 → 0.1.9

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 (145) hide show
  1. package/example/train_base.ts +29 -4
  2. package/package.json +1 -1
  3. package/src/alu/src/parser.ts +105 -4
  4. package/src/canon.ts +65 -0
  5. package/src/geometry.ts +26 -30
  6. package/src/index.ts +1 -0
  7. package/src/mind/learning.ts +39 -1
  8. package/src/mind/match.ts +29 -6
  9. package/src/mind/mechanisms/cast.ts +33 -13
  10. package/src/mind/mechanisms/confluence.ts +15 -1
  11. package/src/mind/mechanisms/recall.ts +116 -41
  12. package/src/mind/mind.ts +172 -29
  13. package/src/mind/primitives.ts +66 -1
  14. package/src/mind/recognition.ts +10 -0
  15. package/src/mind/types.ts +10 -0
  16. package/src/store-sqlite.ts +68 -0
  17. package/src/store.ts +27 -0
  18. package/test/13-conversation.test.mjs +77 -27
  19. package/test/35-prefix-edge.test.mjs +86 -0
  20. package/dist/example/demo.d.ts +0 -1
  21. package/dist/example/demo.js +0 -39
  22. package/dist/example/train_base.d.ts +0 -87
  23. package/dist/example/train_base.js +0 -2232
  24. package/dist/src/alphabet.d.ts +0 -7
  25. package/dist/src/alphabet.js +0 -33
  26. package/dist/src/alu/src/alu.d.ts +0 -185
  27. package/dist/src/alu/src/alu.js +0 -440
  28. package/dist/src/alu/src/expr.d.ts +0 -61
  29. package/dist/src/alu/src/expr.js +0 -318
  30. package/dist/src/alu/src/index.d.ts +0 -11
  31. package/dist/src/alu/src/index.js +0 -19
  32. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  33. package/dist/src/alu/src/kernel-arith.js +0 -264
  34. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  35. package/dist/src/alu/src/kernel-bits.js +0 -152
  36. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  37. package/dist/src/alu/src/kernel-logic.js +0 -60
  38. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  39. package/dist/src/alu/src/kernel-nd.js +0 -208
  40. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  41. package/dist/src/alu/src/kernel-numeric.js +0 -366
  42. package/dist/src/alu/src/operation.d.ts +0 -168
  43. package/dist/src/alu/src/operation.js +0 -189
  44. package/dist/src/alu/src/parser.d.ts +0 -212
  45. package/dist/src/alu/src/parser.js +0 -469
  46. package/dist/src/alu/src/resonance.d.ts +0 -55
  47. package/dist/src/alu/src/resonance.js +0 -126
  48. package/dist/src/alu/src/text.d.ts +0 -31
  49. package/dist/src/alu/src/text.js +0 -73
  50. package/dist/src/alu/src/value.d.ts +0 -109
  51. package/dist/src/alu/src/value.js +0 -300
  52. package/dist/src/alu/test/alu.test.d.ts +0 -1
  53. package/dist/src/alu/test/alu.test.js +0 -764
  54. package/dist/src/bytes.d.ts +0 -14
  55. package/dist/src/bytes.js +0 -59
  56. package/dist/src/config.d.ts +0 -111
  57. package/dist/src/config.js +0 -91
  58. package/dist/src/derive/src/deduction.d.ts +0 -125
  59. package/dist/src/derive/src/deduction.js +0 -155
  60. package/dist/src/derive/src/index.d.ts +0 -7
  61. package/dist/src/derive/src/index.js +0 -11
  62. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  63. package/dist/src/derive/src/priority-queue.js +0 -73
  64. package/dist/src/derive/src/rewrite.d.ts +0 -56
  65. package/dist/src/derive/src/rewrite.js +0 -100
  66. package/dist/src/derive/src/trie.d.ts +0 -90
  67. package/dist/src/derive/src/trie.js +0 -217
  68. package/dist/src/derive/test/derive.test.d.ts +0 -1
  69. package/dist/src/derive/test/derive.test.js +0 -122
  70. package/dist/src/extension.d.ts +0 -37
  71. package/dist/src/extension.js +0 -7
  72. package/dist/src/geometry.d.ts +0 -148
  73. package/dist/src/geometry.js +0 -517
  74. package/dist/src/index.d.ts +0 -15
  75. package/dist/src/index.js +0 -17
  76. package/dist/src/ingest-cache.d.ts +0 -41
  77. package/dist/src/ingest-cache.js +0 -161
  78. package/dist/src/mind/articulation.d.ts +0 -6
  79. package/dist/src/mind/articulation.js +0 -99
  80. package/dist/src/mind/attention.d.ts +0 -72
  81. package/dist/src/mind/attention.js +0 -898
  82. package/dist/src/mind/canonical.d.ts +0 -29
  83. package/dist/src/mind/canonical.js +0 -88
  84. package/dist/src/mind/graph-search.d.ts +0 -270
  85. package/dist/src/mind/graph-search.js +0 -847
  86. package/dist/src/mind/index.d.ts +0 -5
  87. package/dist/src/mind/index.js +0 -5
  88. package/dist/src/mind/junction.d.ts +0 -95
  89. package/dist/src/mind/junction.js +0 -262
  90. package/dist/src/mind/learning.d.ts +0 -47
  91. package/dist/src/mind/learning.js +0 -201
  92. package/dist/src/mind/match.d.ts +0 -111
  93. package/dist/src/mind/match.js +0 -422
  94. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  95. package/dist/src/mind/mechanisms/alu.js +0 -29
  96. package/dist/src/mind/mechanisms/cast.d.ts +0 -35
  97. package/dist/src/mind/mechanisms/cast.js +0 -447
  98. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  99. package/dist/src/mind/mechanisms/confluence.js +0 -213
  100. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  101. package/dist/src/mind/mechanisms/cover.js +0 -179
  102. package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
  103. package/dist/src/mind/mechanisms/extraction.js +0 -342
  104. package/dist/src/mind/mechanisms/recall.d.ts +0 -13
  105. package/dist/src/mind/mechanisms/recall.js +0 -166
  106. package/dist/src/mind/mind.d.ts +0 -211
  107. package/dist/src/mind/mind.js +0 -433
  108. package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
  109. package/dist/src/mind/pipeline-mechanism.js +0 -213
  110. package/dist/src/mind/pipeline.d.ts +0 -20
  111. package/dist/src/mind/pipeline.js +0 -185
  112. package/dist/src/mind/primitives.d.ts +0 -54
  113. package/dist/src/mind/primitives.js +0 -184
  114. package/dist/src/mind/rationale.d.ts +0 -134
  115. package/dist/src/mind/rationale.js +0 -162
  116. package/dist/src/mind/reasoning.d.ts +0 -15
  117. package/dist/src/mind/reasoning.js +0 -162
  118. package/dist/src/mind/recognition.d.ts +0 -20
  119. package/dist/src/mind/recognition.js +0 -236
  120. package/dist/src/mind/resonance.d.ts +0 -23
  121. package/dist/src/mind/resonance.js +0 -0
  122. package/dist/src/mind/trace.d.ts +0 -15
  123. package/dist/src/mind/trace.js +0 -73
  124. package/dist/src/mind/traverse.d.ts +0 -113
  125. package/dist/src/mind/traverse.js +0 -488
  126. package/dist/src/mind/types.d.ts +0 -176
  127. package/dist/src/mind/types.js +0 -84
  128. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  129. package/dist/src/rabitq-ivf/src/database.js +0 -201
  130. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  131. package/dist/src/rabitq-ivf/src/index.js +0 -4
  132. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  133. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  134. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  135. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  136. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  137. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  138. package/dist/src/sema.d.ts +0 -31
  139. package/dist/src/sema.js +0 -63
  140. package/dist/src/store-sqlite.d.ts +0 -176
  141. package/dist/src/store-sqlite.js +0 -890
  142. package/dist/src/store.d.ts +0 -642
  143. package/dist/src/store.js +0 -1612
  144. package/dist/src/vec.d.ts +0 -31
  145. package/dist/src/vec.js +0 -109
@@ -134,6 +134,18 @@ CREATE TABLE IF NOT EXISTS contain (
134
134
  id INTEGER PRIMARY KEY,
135
135
  parents BLOB NOT NULL
136
136
  );
137
+ -- CANONICAL-FORM index (Store.canonAdd/canonFind): h is the 32-bit hash of a
138
+ -- node's CANONICAL content key (the modality's canonicalizer output — case-
139
+ -- folded, whitespace-collapsed text, etc.; the store never sees the key
140
+ -- itself, only its hash). Same hash-then-verify discipline as idx_node_h:
141
+ -- the caller re-canonicalizes each candidate's bytes before trusting it, so
142
+ -- a collision costs a read, never a wrong id. WITHOUT ROWID on (h, id):
143
+ -- the composite key IS the lookup index and gives free dedup.
144
+ CREATE TABLE IF NOT EXISTS canon (
145
+ h INTEGER NOT NULL,
146
+ id INTEGER NOT NULL,
147
+ PRIMARY KEY (h, id)
148
+ ) WITHOUT ROWID;
137
149
  CREATE TABLE IF NOT EXISTS snapshot (
138
150
  id INTEGER PRIMARY KEY CHECK (id = 1),
139
151
  data BLOB NOT NULL
@@ -241,6 +253,10 @@ export class SQliteStore extends AbstractStore implements Store {
241
253
  private _selPrev: any = null;
242
254
  private _setMeta: any = null;
243
255
  private _getMeta: any = null;
256
+ private _insCanon: any = null;
257
+ private _selCanon: any = null;
258
+ private _cntCanon: any = null;
259
+ private _selContentFrom: any = null;
244
260
  private _delMeta: any = null;
245
261
  private _insSnapshot: any = null;
246
262
  private _selSnapshot: any = null;
@@ -990,6 +1006,58 @@ export class SQliteStore extends AbstractStore implements Store {
990
1006
  this._delMeta.run(key);
991
1007
  }
992
1008
 
1009
+ // -- Canonical-form index (Store optional capability) --
1010
+
1011
+ canonAdd(h: number, id: number): void {
1012
+ if (!this._insCanon) {
1013
+ this._insCanon = this.sqlite!.prepare(
1014
+ "INSERT OR IGNORE INTO canon (h, id) VALUES (?, ?)",
1015
+ );
1016
+ }
1017
+ // Join the deferred write transaction (committed by flush/commit), so a
1018
+ // bulk index build coalesces instead of paying autocommit per row.
1019
+ this._dbBeginTx();
1020
+ this._insCanon.run(h, id);
1021
+ }
1022
+
1023
+ canonFind(h: number): number[] {
1024
+ if (!this._selCanon) {
1025
+ this._selCanon = this.sqlite!.prepare(
1026
+ "SELECT id FROM canon WHERE h = ?",
1027
+ );
1028
+ }
1029
+ return (this._selCanon.all(h) as Array<{ id: number }>).map((r) => r.id);
1030
+ }
1031
+
1032
+ canonCount(): number {
1033
+ if (!this._cntCanon) {
1034
+ this._cntCanon = this.sqlite!.prepare(
1035
+ "SELECT count(*) AS c FROM canon",
1036
+ );
1037
+ }
1038
+ return (this._cntCanon.get() as { c: number }).c;
1039
+ }
1040
+
1041
+ eachContent(
1042
+ cb: (id: number, bytes: Uint8Array) => void,
1043
+ fromId = 0,
1044
+ ): void {
1045
+ if (!this._selContentFrom) {
1046
+ // Content-bearing nodes are FLAT branches: leaf present, kids an
1047
+ // empty (zero-length) blob — the same population PART 2 of the
1048
+ // diagnostics calls "distinct content spans".
1049
+ this._selContentFrom = this.sqlite!.prepare(
1050
+ "SELECT id, leaf FROM node " +
1051
+ "WHERE id >= ? AND leaf IS NOT NULL " +
1052
+ "AND (kids IS NULL OR length(kids) = 0)",
1053
+ );
1054
+ }
1055
+ for (const row of this._selContentFrom.iterate(fromId)) {
1056
+ const r = row as { id: number; leaf: Uint8Array };
1057
+ cb(r.id, new Uint8Array(r.leaf));
1058
+ }
1059
+ }
1060
+
993
1061
  // -- Snapshot --
994
1062
 
995
1063
  protected _dbSaveSnapshot(bytes: Uint8Array): void {
package/src/store.ts CHANGED
@@ -417,6 +417,33 @@ export interface Store {
417
417
  * the node has no halo row. */
418
418
  haloMass(id: NodeId): number;
419
419
 
420
+ // ── canonical-form index (optional capability) ─────────────────────────
421
+ // A small hash index from CANONICAL content keys to node ids, enabling
422
+ // equivalence-class resolution ("WHAT" finds the stored "What") without
423
+ // the store knowing what the equivalence IS: the canonicalizer lives with
424
+ // the modality (see src/canon.ts) and is applied by the CALLER — the store
425
+ // only maps 32-bit key hashes to candidate ids, and the caller verifies
426
+ // canon(stored bytes) === key before trusting any candidate (the same
427
+ // hash-then-verify discipline as the node table's own `h` index).
428
+ // Backends that do not implement the capability leave all three absent;
429
+ // resolution then simply has no canonical fallback.
430
+
431
+ /** Record that node `id`'s canonical key hashes to `h`. Idempotent. */
432
+ canonAdd?(h: number, id: NodeId): void;
433
+ /** All candidate node ids whose canonical key hashes to `h` (collisions
434
+ * included — the caller verifies). */
435
+ canonFind?(h: number): NodeId[];
436
+ /** Number of (h, id) rows in the canon index — 0 means never built. */
437
+ canonCount?(): number;
438
+ /** Visit every content-bearing node (flat branch: `leaf` present, no
439
+ * kids) — the population a canonical index is built over. `fromId`
440
+ * restricts the scan to ids ≥ fromId, so an index refresh after further
441
+ * training only visits the new rows. */
442
+ eachContent?(
443
+ cb: (id: NodeId, bytes: Uint8Array) => void,
444
+ fromId?: NodeId,
445
+ ): void;
446
+
420
447
  // ── lifecycle ──────────────────────────────────────────────────────────
421
448
  size(): Promise<number>;
422
449
  saveSnapshot(bytes: Uint8Array): Promise<void>;
@@ -45,15 +45,17 @@ async function teachConversation(mind, turns) {
45
45
  // ═══════════════════════════════════════════════════════════════════════
46
46
  // predictNext — given the turns spoken so far, predict the next turn.
47
47
  //
48
- // Uses the Conversation API: beginConversation → respondTurn for each
49
- // prior turn respondTurnText for the query endConversation.
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.
50
52
  // Turns are raw strings, concatenated by the Mind — no separator.
51
53
  // ═══════════════════════════════════════════════════════════════════════
52
54
  async function predictNext(mind, priorTurns) {
53
55
  if (priorTurns.length === 0) return "";
54
56
  const conv = mind.beginConversation();
55
57
  for (let i = 0; i < priorTurns.length - 1; i++) {
56
- await mind.respondTurn(conv, priorTurns[i]);
58
+ mind.addTurn(conv, priorTurns[i]);
57
59
  }
58
60
  const { response } = await mind.respondTurnText(
59
61
  conv,
@@ -244,8 +246,9 @@ test("E1: begin → respondTurn → end completes a single-turn conversation", a
244
246
  const conv = mind.beginConversation();
245
247
  const { response, state } = await mind.respondTurnText(conv, "hello");
246
248
  assert.equal(response, "world");
247
- // Single turn no boundaries yet.
248
- assert.equal(state.boundaries.length, 0);
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);
249
252
  mind.endConversation(conv);
250
253
  await mind.store.close();
251
254
  });
@@ -255,13 +258,15 @@ test("E2: multi-turn conversation accumulates context and boundaries", async ()
255
258
  await teachConversation(mind, CAT);
256
259
 
257
260
  const conv = mind.beginConversation();
258
- // First turn establishes the subject.
259
- await mind.respondTurn(conv, "I adopted a cat");
261
+ // First turn establishes the subject — scripted history, so it is FED
262
+ // (addTurn), not asked.
263
+ mind.addTurn(conv, "I adopted a cat");
260
264
 
261
265
  // Second turn is the pivot query.
262
266
  const t2 = await mind.respondTurnText(conv, "what is its name?");
263
- // Now we have one boundary: the byte position after the first turn.
264
- assert.equal(t2.state.boundaries.length, 1);
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);
265
270
  assert.equal(t2.response, "her name is Whiskers");
266
271
 
267
272
  mind.endConversation(conv);
@@ -272,28 +277,26 @@ test("E3: save → end → restore → continue works", async () => {
272
277
  const mind = newMind();
273
278
  await teachConversation(mind, CAT);
274
279
 
275
- // First two turns.
280
+ // First two turns (context fed, pivot asked).
276
281
  const conv = mind.beginConversation();
277
- await mind.respondTurn(conv, "I adopted a cat");
282
+ mind.addTurn(conv, "I adopted a cat");
278
283
  const { state: saved } = await mind.respondTurnText(
279
284
  conv,
280
285
  "what is its name?",
281
286
  );
282
- assert.equal(saved.boundaries.length, 1);
287
+ assert.equal(saved.boundaries.length, 2);
283
288
 
284
289
  // Save and end.
285
290
  mind.endConversation(conv);
286
291
 
287
292
  // Restore from the saved state into a new handle.
288
293
  const conv2 = mind.beginConversation(saved);
289
- const { response, state: state2 } = await mind.respondTurnText(
290
- conv2,
291
- "her name is Whiskers",
292
- );
293
- // The restored conversation continues from where it left off.
294
- assert.ok(response.length > 0 || response === "");
295
- // Boundaries are intact from the restore.
296
- assert.equal(state2.boundaries.length, 2);
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");
297
300
 
298
301
  mind.endConversation(conv2);
299
302
  await mind.store.close();
@@ -336,8 +339,8 @@ test("E7: multiple concurrent conversations are independent", async () => {
336
339
  const dogConv = mind.beginConversation();
337
340
 
338
341
  // Feed the distinguishing context to each.
339
- await mind.respondTurn(catConv, "I adopted a cat");
340
- await mind.respondTurn(dogConv, "I adopted a dog");
342
+ mind.addTurn(catConv, "I adopted a cat");
343
+ mind.addTurn(dogConv, "I adopted a dog");
341
344
 
342
345
  // The same pivot turn — different answers.
343
346
  const catR = await mind.respondTurnText(catConv, "what is its name?");
@@ -347,9 +350,9 @@ test("E7: multiple concurrent conversations are independent", async () => {
347
350
  assert.equal(dogR.response, "his name is Rex");
348
351
  assert.notEqual(catR.response, dogR.response);
349
352
 
350
- // Each conversation has its own boundaries.
351
- assert.equal(catR.state.boundaries.length, 1);
352
- assert.equal(dogR.state.boundaries.length, 1);
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);
353
356
 
354
357
  // Ending one does not affect the other.
355
358
  mind.endConversation(catConv);
@@ -384,9 +387,11 @@ test("E9: respond and respondTurn give the same answer for the same cumulative b
384
387
  "I adopted a catwhat is its name?",
385
388
  );
386
389
 
387
- // Via respondTurn — the new way.
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).
388
393
  const conv = mind.beginConversation();
389
- await mind.respondTurnText(conv, "I adopted a cat");
394
+ mind.addTurn(conv, "I adopted a cat");
390
395
  const { response: viaConv } = await mind.respondTurnText(
391
396
  conv,
392
397
  "what is its name?",
@@ -396,3 +401,48 @@ test("E9: respond and respondTurn give the same answer for the same cumulative b
396
401
  assert.equal(viaRespond, viaConv);
397
402
  await mind.store.close();
398
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
+ });
@@ -1 +0,0 @@
1
- export {};
@@ -1,39 +0,0 @@
1
- // demo.ts — one short session that drives the WHOLE pipeline from one memory.
2
- //
3
- // We give Sema a handful of plain notes, then ask things that no single note
4
- // answers. The headline query is the third one: from three worked examples Sema
5
- // learns the shape of "X was painted by Y", lifts the painter out of a sentence
6
- // it has NEVER seen, and then — in the same pass — reasons forward to a separate
7
- // fact about that painter. The reply contains no word from the question. That is
8
- // retrieval, generalization, and reasoning composing as a single act, with every
9
- // step traceable back to the notes behind it.
10
- import { Mind } from "../src/index.js";
11
- import { SQliteStore } from "../src/store-sqlite.js";
12
- async function main() {
13
- const mind = new Mind({ store: new SQliteStore({ path: ":memory:" }) });
14
- const ask = async (q) => (await mind.respondText(q)).trim();
15
- // ── Jot down what we know. Each line is just (context → what follows). ──
16
- await mind.ingest([
17
- // One relation, shown three times — a pattern taught purely by example:
18
- ["The Mona Lisa was painted by Leonardo da Vinci.", "Leonardo da Vinci"],
19
- ["The Starry Night was painted by Vincent van Gogh.", "Vincent van Gogh"],
20
- [
21
- "The Night Watch was painted by Rembrandt van Rijn.",
22
- "Rembrandt van Rijn",
23
- ],
24
- // One stray fact, keyed on a name none of the examples mention:
25
- ["Pablo Picasso", "Pablo Picasso co-founded the Cubist movement"],
26
- ]);
27
- // 1) GENERALIZE — apply the learned pattern to an unseen sentence and read out
28
- // the painter. "Pablo Picasso" was never given as an answer; Sema locates it
29
- // by analogy to the three examples.
30
- console.log(await ask("The Weeping Woman was painted by Pablo Picasso."));
31
- // → "Pablo Picasso co-founded the Cubist movement"
32
- // …and, having found the painter, it KEEPS GOING: the name bridges into the
33
- // one fact it holds about him. The answer appears in no word of the question.
34
- // 2) COMPUTE — exact arithmetic, grounded right where the notes go silent.
35
- console.log(await ask("a museum charges 12*4 for a family ticket"));
36
- // → "48"
37
- await mind.store.close();
38
- }
39
- main();
@@ -1,87 +0,0 @@
1
- export interface Episode {
2
- context: string;
3
- continuation: string;
4
- }
5
- export type TrainingItem = string | Episode;
6
- /** Dedup + trim a concept's items: drop empty/degenerate pairs and exact
7
- * repeats so a concept never deposits the same form twice. */
8
- export declare function refineItems(items: TrainingItem[]): TrainingItem[];
9
- /** One normalized SmolSent row. */
10
- export interface SmolSentRow {
11
- src: string;
12
- trg: string;
13
- sl: string;
14
- tl: string;
15
- }
16
- /** Normalize a raw datasets-server row into a SmolSentRow, or null when it lacks
17
- * both sides or a side is implausibly large (a dump, not a sentence). */
18
- export declare function toSmolSentRow(row: unknown): SmolSentRow | null;
19
- /** Translate ONE SmolSent pair into SEMA facts: the two sentences are one
20
- * meaning in two languages, so bind them BOTH ways. refineItems drops the
21
- * degenerate case where src === trg. */
22
- export declare function smolSentRowToItems(row: SmolSentRow): TrainingItem[];
23
- /** One normalized Aya row. */
24
- export interface AyaRow {
25
- inputs: string;
26
- targets: string;
27
- language: string;
28
- }
29
- /** Normalize a raw datasets-server row object into an AyaRow, or null when it
30
- * lacks a usable prompt/answer or a field is implausibly large (a dump, not a
31
- * cognitive example). Trims surrounding whitespace; keeps inner text verbatim
32
- * (human prose, possibly multi-paragraph). */
33
- export declare function toAyaRow(row: unknown): AyaRow | null;
34
- /** Translate ONE Aya row into SEMA training items. A row is a single human
35
- * (question → answer) exchange — exactly one FACT, the (inputs → targets) edge.
36
- * No standalone-answer experience and no one-exchange "cumulative" walk: a lone
37
- * Q→A is not multi-turn, and both would only replicate the same edge. */
38
- export declare function ayaRowToItems(row: AyaRow): TrainingItem[];
39
- /** A single oasst2 message node (the fields we use; the tree nests via replies). */
40
- interface OasstNode {
41
- role?: string;
42
- text?: string;
43
- rank?: number | null;
44
- deleted?: boolean;
45
- replies?: OasstNode[];
46
- }
47
- /** One conversational turn extracted from a tree. */
48
- export interface OasstTurn {
49
- role: string;
50
- text: string;
51
- }
52
- /** Collapse a conversation tree to ONE linear path: at each node, descend into
53
- * its best-ranked, non-deleted reply (rank 0 preferred; unranked sorts last).
54
- * Returns the ordered turns (already strictly alternating in this corpus). */
55
- export declare function bestOasstPath(root: OasstNode): OasstTurn[];
56
- /** Translate ONE multi-turn oasst2 conversation into SEMA training items.
57
- *
58
- * This is the ONE stage where cumulative continuous context is truly necessary:
59
- * the data is a real multi-turn dialogue, and what must be learned is how each
60
- * turn follows from the WHOLE conversation so far — not from the previous turn
61
- * alone. The conversation is emitted ONLY as the accumulated walk; standalone
62
- * turn experiences and local adjacent-pair facts are NOT emitted (they are
63
- * subsumed by it and would merely replicate the content).
64
- *
65
- * The walk is byte-for-byte the pattern proven in test/13-conversation.test.mjs
66
- * ("teachConversation"): each turn is the continuation of all prior turns joined
67
- * by "\n", with BARE turn text — NO "User:/Assistant:" labels. Roles already
68
- * alternate by position in an oasst2 best-path (the root is a prompter), so a
69
- * label adds nothing the position does not, while a clean continuation matches
70
- * the test's recall (predictNext queries bare prior turns) and lets a turn share
71
- * its gist with the same text elsewhere (e.g. an Aya question stored bare).
72
- *
73
- * Returns [] for a conversation below the multi-turn threshold, so callers can
74
- * simply skip empties. */
75
- export declare function oasstConversationToItems(turns: OasstTurn[]): TrainingItem[];
76
- /** One normalized General-Knowledge row. */
77
- export interface GenKnowRow {
78
- question: string;
79
- answer: string;
80
- }
81
- /** Normalize a raw datasets-server row into a GenKnowRow, or null when it lacks
82
- * a usable question/answer or a side is implausibly large (corruption). */
83
- export declare function toGenKnowRow(row: unknown): GenKnowRow | null;
84
- /** Translate ONE General-Knowledge row into SEMA items: exactly one
85
- * (question → answer) FACT. refineItems drops a degenerate question === answer. */
86
- export declare function genKnowRowToItems(row: GenKnowRow): TrainingItem[];
87
- export {};