@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.
- package/example/train_base.ts +29 -4
- package/package.json +1 -1
- package/src/alu/src/parser.ts +105 -4
- package/src/canon.ts +65 -0
- package/src/geometry.ts +26 -30
- package/src/index.ts +1 -0
- package/src/mind/learning.ts +39 -1
- package/src/mind/match.ts +29 -6
- package/src/mind/mechanisms/cast.ts +33 -13
- package/src/mind/mechanisms/confluence.ts +15 -1
- package/src/mind/mechanisms/recall.ts +116 -41
- package/src/mind/mind.ts +172 -29
- package/src/mind/primitives.ts +66 -1
- package/src/mind/recognition.ts +10 -0
- package/src/mind/types.ts +10 -0
- package/src/store-sqlite.ts +68 -0
- package/src/store.ts +27 -0
- package/test/13-conversation.test.mjs +77 -27
- package/test/35-prefix-edge.test.mjs +86 -0
- package/dist/example/demo.d.ts +0 -1
- package/dist/example/demo.js +0 -39
- package/dist/example/train_base.d.ts +0 -87
- package/dist/example/train_base.js +0 -2232
- package/dist/src/alphabet.d.ts +0 -7
- package/dist/src/alphabet.js +0 -33
- package/dist/src/alu/src/alu.d.ts +0 -185
- package/dist/src/alu/src/alu.js +0 -440
- package/dist/src/alu/src/expr.d.ts +0 -61
- package/dist/src/alu/src/expr.js +0 -318
- package/dist/src/alu/src/index.d.ts +0 -11
- package/dist/src/alu/src/index.js +0 -19
- package/dist/src/alu/src/kernel-arith.d.ts +0 -16
- package/dist/src/alu/src/kernel-arith.js +0 -264
- package/dist/src/alu/src/kernel-bits.d.ts +0 -19
- package/dist/src/alu/src/kernel-bits.js +0 -152
- package/dist/src/alu/src/kernel-logic.d.ts +0 -4
- package/dist/src/alu/src/kernel-logic.js +0 -60
- package/dist/src/alu/src/kernel-nd.d.ts +0 -3
- package/dist/src/alu/src/kernel-nd.js +0 -208
- package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
- package/dist/src/alu/src/kernel-numeric.js +0 -366
- package/dist/src/alu/src/operation.d.ts +0 -168
- package/dist/src/alu/src/operation.js +0 -189
- package/dist/src/alu/src/parser.d.ts +0 -212
- package/dist/src/alu/src/parser.js +0 -469
- package/dist/src/alu/src/resonance.d.ts +0 -55
- package/dist/src/alu/src/resonance.js +0 -126
- package/dist/src/alu/src/text.d.ts +0 -31
- package/dist/src/alu/src/text.js +0 -73
- package/dist/src/alu/src/value.d.ts +0 -109
- package/dist/src/alu/src/value.js +0 -300
- package/dist/src/alu/test/alu.test.d.ts +0 -1
- package/dist/src/alu/test/alu.test.js +0 -764
- package/dist/src/bytes.d.ts +0 -14
- package/dist/src/bytes.js +0 -59
- package/dist/src/config.d.ts +0 -111
- package/dist/src/config.js +0 -91
- package/dist/src/derive/src/deduction.d.ts +0 -125
- package/dist/src/derive/src/deduction.js +0 -155
- package/dist/src/derive/src/index.d.ts +0 -7
- package/dist/src/derive/src/index.js +0 -11
- package/dist/src/derive/src/priority-queue.d.ts +0 -20
- package/dist/src/derive/src/priority-queue.js +0 -73
- package/dist/src/derive/src/rewrite.d.ts +0 -56
- package/dist/src/derive/src/rewrite.js +0 -100
- package/dist/src/derive/src/trie.d.ts +0 -90
- package/dist/src/derive/src/trie.js +0 -217
- package/dist/src/derive/test/derive.test.d.ts +0 -1
- package/dist/src/derive/test/derive.test.js +0 -122
- package/dist/src/extension.d.ts +0 -37
- package/dist/src/extension.js +0 -7
- package/dist/src/geometry.d.ts +0 -148
- package/dist/src/geometry.js +0 -517
- package/dist/src/index.d.ts +0 -15
- package/dist/src/index.js +0 -17
- package/dist/src/ingest-cache.d.ts +0 -41
- package/dist/src/ingest-cache.js +0 -161
- package/dist/src/mind/articulation.d.ts +0 -6
- package/dist/src/mind/articulation.js +0 -99
- package/dist/src/mind/attention.d.ts +0 -72
- package/dist/src/mind/attention.js +0 -898
- package/dist/src/mind/canonical.d.ts +0 -29
- package/dist/src/mind/canonical.js +0 -88
- package/dist/src/mind/graph-search.d.ts +0 -270
- package/dist/src/mind/graph-search.js +0 -847
- package/dist/src/mind/index.d.ts +0 -5
- package/dist/src/mind/index.js +0 -5
- package/dist/src/mind/junction.d.ts +0 -95
- package/dist/src/mind/junction.js +0 -262
- package/dist/src/mind/learning.d.ts +0 -47
- package/dist/src/mind/learning.js +0 -201
- package/dist/src/mind/match.d.ts +0 -111
- package/dist/src/mind/match.js +0 -422
- package/dist/src/mind/mechanisms/alu.d.ts +0 -4
- package/dist/src/mind/mechanisms/alu.js +0 -29
- package/dist/src/mind/mechanisms/cast.d.ts +0 -35
- package/dist/src/mind/mechanisms/cast.js +0 -447
- package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
- package/dist/src/mind/mechanisms/confluence.js +0 -213
- package/dist/src/mind/mechanisms/cover.d.ts +0 -6
- package/dist/src/mind/mechanisms/cover.js +0 -179
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
- package/dist/src/mind/mechanisms/extraction.js +0 -342
- package/dist/src/mind/mechanisms/recall.d.ts +0 -13
- package/dist/src/mind/mechanisms/recall.js +0 -166
- package/dist/src/mind/mind.d.ts +0 -211
- package/dist/src/mind/mind.js +0 -433
- package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
- package/dist/src/mind/pipeline-mechanism.js +0 -213
- package/dist/src/mind/pipeline.d.ts +0 -20
- package/dist/src/mind/pipeline.js +0 -185
- package/dist/src/mind/primitives.d.ts +0 -54
- package/dist/src/mind/primitives.js +0 -184
- package/dist/src/mind/rationale.d.ts +0 -134
- package/dist/src/mind/rationale.js +0 -162
- package/dist/src/mind/reasoning.d.ts +0 -15
- package/dist/src/mind/reasoning.js +0 -162
- package/dist/src/mind/recognition.d.ts +0 -20
- package/dist/src/mind/recognition.js +0 -236
- package/dist/src/mind/resonance.d.ts +0 -23
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +0 -15
- package/dist/src/mind/trace.js +0 -73
- package/dist/src/mind/traverse.d.ts +0 -113
- package/dist/src/mind/traverse.js +0 -488
- package/dist/src/mind/types.d.ts +0 -176
- package/dist/src/mind/types.js +0 -84
- package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
- package/dist/src/rabitq-ivf/src/database.js +0 -201
- package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
- package/dist/src/rabitq-ivf/src/index.js +0 -4
- package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
- package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
- package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
- package/dist/src/rabitq-ivf/src/prng.js +0 -36
- package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
- package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
- package/dist/src/sema.d.ts +0 -31
- package/dist/src/sema.js +0 -63
- package/dist/src/store-sqlite.d.ts +0 -176
- package/dist/src/store-sqlite.js +0 -890
- package/dist/src/store.d.ts +0 -642
- package/dist/src/store.js +0 -1612
- package/dist/src/vec.d.ts +0 -31
- package/dist/src/vec.js +0 -109
package/src/store-sqlite.ts
CHANGED
|
@@ -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 →
|
|
49
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
264
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
assert.ok(
|
|
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
|
-
|
|
340
|
-
|
|
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,
|
|
352
|
-
assert.equal(dogR.state.boundaries.length,
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|
package/dist/example/demo.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/example/demo.js
DELETED
|
@@ -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 {};
|