@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.
- package/AGENTS.md +470 -0
- package/AUTHORS.md +12 -0
- package/COMMERCIAL-LICENSE.md +19 -0
- package/CONTRIBUTING.md +15 -0
- package/HOW_IT_WORKS.md +4210 -0
- package/LICENSE.md +117 -0
- package/README.md +290 -0
- package/TRADEMARKS.md +19 -0
- package/example/demo.ts +46 -0
- package/example/train_base.ts +2675 -0
- package/index.html +893 -0
- package/package.json +25 -0
- package/src/alphabet.ts +34 -0
- package/src/alu/README.md +332 -0
- package/src/alu/src/alu.ts +541 -0
- package/src/alu/src/expr.ts +339 -0
- package/src/alu/src/index.ts +115 -0
- package/src/alu/src/kernel-arith.ts +377 -0
- package/src/alu/src/kernel-bits.ts +199 -0
- package/src/alu/src/kernel-logic.ts +102 -0
- package/src/alu/src/kernel-nd.ts +235 -0
- package/src/alu/src/kernel-numeric.ts +466 -0
- package/src/alu/src/operation.ts +344 -0
- package/src/alu/src/parser.ts +574 -0
- package/src/alu/src/resonance.ts +161 -0
- package/src/alu/src/text.ts +83 -0
- package/src/alu/src/value.ts +327 -0
- package/src/alu/test/alu.test.ts +1004 -0
- package/src/bytes.ts +62 -0
- package/src/config.ts +227 -0
- package/src/derive/README.md +295 -0
- package/src/derive/src/deduction.ts +263 -0
- package/src/derive/src/index.ts +24 -0
- package/src/derive/src/priority-queue.ts +70 -0
- package/src/derive/src/rewrite.ts +127 -0
- package/src/derive/src/trie.ts +242 -0
- package/src/derive/test/derive.test.ts +137 -0
- package/src/extension.ts +42 -0
- package/src/geometry.ts +511 -0
- package/src/index.ts +38 -0
- package/src/ingest-cache.ts +224 -0
- package/src/mind/articulation.ts +137 -0
- package/src/mind/attention.ts +1061 -0
- package/src/mind/canonical.ts +107 -0
- package/src/mind/graph-search.ts +1100 -0
- package/src/mind/index.ts +18 -0
- package/src/mind/junction.ts +347 -0
- package/src/mind/learning.ts +246 -0
- package/src/mind/match.ts +507 -0
- package/src/mind/mechanisms/alu.ts +33 -0
- package/src/mind/mechanisms/cast.ts +568 -0
- package/src/mind/mechanisms/confluence.ts +268 -0
- package/src/mind/mechanisms/cover.ts +248 -0
- package/src/mind/mechanisms/extraction.ts +422 -0
- package/src/mind/mechanisms/recall.ts +241 -0
- package/src/mind/mind.ts +483 -0
- package/src/mind/pipeline-mechanism.ts +326 -0
- package/src/mind/pipeline.ts +300 -0
- package/src/mind/primitives.ts +201 -0
- package/src/mind/rationale.ts +275 -0
- package/src/mind/reasoning.ts +198 -0
- package/src/mind/recognition.ts +234 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/trace.ts +108 -0
- package/src/mind/traverse.ts +556 -0
- package/src/mind/types.ts +281 -0
- package/src/rabitq-hnsw/README.md +303 -0
- package/src/rabitq-hnsw/src/database.ts +492 -0
- package/src/rabitq-hnsw/src/heap.ts +90 -0
- package/src/rabitq-hnsw/src/hnsw.ts +514 -0
- package/src/rabitq-hnsw/src/index.ts +15 -0
- package/src/rabitq-hnsw/src/prng.ts +39 -0
- package/src/rabitq-hnsw/src/rabitq.ts +308 -0
- package/src/rabitq-hnsw/src/store.ts +994 -0
- package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
- package/src/store-sqlite.ts +928 -0
- package/src/store.ts +2148 -0
- package/src/vec.ts +124 -0
- package/test/00-extract.test.mjs +151 -0
- package/test/01-floor.test.mjs +20 -0
- package/test/02-roundtrip.test.mjs +83 -0
- package/test/03-recall.test.mjs +98 -0
- package/test/04-think.test.mjs +627 -0
- package/test/05-concepts.test.mjs +84 -0
- package/test/08-storage.test.mjs +948 -0
- package/test/09-edges.test.mjs +65 -0
- package/test/11-universality.test.mjs +180 -0
- package/test/13-conversation.test.mjs +228 -0
- package/test/14-scaling.test.mjs +503 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/16-bridge.test.mjs +250 -0
- package/test/17-intelligence.test.mjs +240 -0
- package/test/18-alu.test.mjs +209 -0
- package/test/19-nd.test.mjs +254 -0
- package/test/20-stability.test.mjs +489 -0
- package/test/21-partial.test.mjs +158 -0
- package/test/22-multihop.test.mjs +130 -0
- package/test/23-rationale.test.mjs +316 -0
- package/test/24-generalization.test.mjs +416 -0
- package/test/25-embedding.test.mjs +75 -0
- package/test/26-cooperative.test.mjs +89 -0
- package/test/27-saturation-drop.test.mjs +637 -0
- package/test/28-unknowable.test.mjs +88 -0
- package/test/29-counterfactual.test.mjs +476 -0
- package/test/30-conflict-resolution.test.mjs +166 -0
- package/test/31-audit.test.mjs +288 -0
- package/test/32-confluence.test.mjs +173 -0
- package/test/33-multi-candidate.test.mjs +222 -0
- package/test/34-cross-region.test.mjs +252 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// 24-generalization.test.mjs — GENERALIZATION VIA THE REASONER.
|
|
2
|
+
//
|
|
3
|
+
// Sema is a RAG+reasoner mind. The RAG half is resonance search over the DAG;
|
|
4
|
+
// the reasoner half explores the DAG's STRUCTURE (the consensus climb, the
|
|
5
|
+
// alignment read, the multi-hop walk). The reasoner is the PRIMARY component,
|
|
6
|
+
// not a fallback: it must produce answers the retrieval half cannot reach at
|
|
7
|
+
// all — a value never stored, a LIST synthesized from an unseen sentence, a
|
|
8
|
+
// SUMMARY generated in a learnt shape, an answer that fuses SEVERAL distinct
|
|
9
|
+
// points of attention. This file pins three gaps the earlier suites left open
|
|
10
|
+
// and proves, irrefutably, that the reasoner closes them.
|
|
11
|
+
//
|
|
12
|
+
// GAP 1 — the reasoner is primary, not a fallback. A query whose answer is a
|
|
13
|
+
// value NEVER stored (a generated list / summary) has no node to
|
|
14
|
+
// retrieve; only structural reasoning can answer it. The produced
|
|
15
|
+
// answer must therefore NOT be any stored node — proof it was derived,
|
|
16
|
+
// not recalled.
|
|
17
|
+
//
|
|
18
|
+
// GAP 3.1 — the consensus climb must not resolve to a SINGLE point of attention. A
|
|
19
|
+
// query naming K unrelated distinctive topics (K ≥ 2) decomposes into K
|
|
20
|
+
// independent roots; the reasoner must sequence them by evidence and
|
|
21
|
+
// combine all into one coherent answer — not silently drop any. (The
|
|
22
|
+
// trace must also surface more than one ordered anchor.) The forest
|
|
23
|
+
// read-out is generic for any K via naturalBreak + span-disjoint
|
|
24
|
+
// overlap + the saturation gate (leadingEnd). Tests here cover
|
|
25
|
+
// K = 2 (the minimum multi-root case).
|
|
26
|
+
//
|
|
27
|
+
// GAP 3.2 — extractByAlignment must extract MULTIPLE pieces, not one span. A
|
|
28
|
+
// skill whose answer is several NON-CONTIGUOUS spans of its context
|
|
29
|
+
// (a list, a summary) must transfer to an unseen sentence: read every
|
|
30
|
+
// analogous piece and synthesize them in the learnt shape — generating
|
|
31
|
+
// something entirely new.
|
|
32
|
+
//
|
|
33
|
+
// The assertions are literal / marker-based and corpus-independent. They FAIL
|
|
34
|
+
// on a build that resolves one point of attention and reads one span; they PASS
|
|
35
|
+
// only when the reasoner generalizes multi-piece and multi-root.
|
|
36
|
+
|
|
37
|
+
import { test } from "node:test";
|
|
38
|
+
import assert from "node:assert/strict";
|
|
39
|
+
import { Mind } from "../dist/src/index.js";
|
|
40
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
41
|
+
|
|
42
|
+
const mk = () =>
|
|
43
|
+
new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
44
|
+
const ask = async (m, q) =>
|
|
45
|
+
(await m.respondText(q)).replace(/\s+/g, " ").trim();
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// GAP 3.2 — MULTI-PIECE EXTRACTION: generate a LIST from an unseen sentence.
|
|
49
|
+
//
|
|
50
|
+
// The skill: "The team includes X, Y, and Z." → "X, Y, Z". The answer is NOT a
|
|
51
|
+
// contiguous span of the context (the context has an "and" the answer drops), so
|
|
52
|
+
// it is THREE spans that must each be read from the query and re-joined in the
|
|
53
|
+
// learnt shape. Asked an unseen sentence, the mind must produce the analogous
|
|
54
|
+
// list — three values it was never told, assembled into a string never stored.
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
56
|
+
test("3.2 — a LIST skill generates an unseen list (multi-piece extraction)", async () => {
|
|
57
|
+
const m = mk();
|
|
58
|
+
const taught = [
|
|
59
|
+
["The team includes Alice, Bob, and Carol.", "Alice, Bob, Carol"],
|
|
60
|
+
["The team includes Dan, Eve, and Frank.", "Dan, Eve, Frank"],
|
|
61
|
+
["The team includes Gina, Hank, and Iris.", "Gina, Hank, Iris"],
|
|
62
|
+
["The team includes Jack, Kate, and Leo.", "Jack, Kate, Leo"],
|
|
63
|
+
];
|
|
64
|
+
await m.ingest(taught);
|
|
65
|
+
|
|
66
|
+
const got = await ask(m, "The team includes Mona, Nick, and Omar.");
|
|
67
|
+
|
|
68
|
+
// Every unseen member must appear — the pieces were read from the QUESTION.
|
|
69
|
+
for (const name of ["Mona", "Nick", "Omar"]) {
|
|
70
|
+
assert.ok(
|
|
71
|
+
got.includes(name),
|
|
72
|
+
`list must contain the unseen member "${name}" — got "${got}"`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
// It must be NEW: not a verbatim copy of any taught answer (proof it was
|
|
76
|
+
// generated, not recalled — GAP 1).
|
|
77
|
+
for (const [, ans] of taught) {
|
|
78
|
+
assert.notEqual(
|
|
79
|
+
got,
|
|
80
|
+
ans,
|
|
81
|
+
`answer "${got}" is a recalled exemplar, not generated`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
// And it must NOT leak a taught member (a single-span read would return one
|
|
85
|
+
// stored list wholesale).
|
|
86
|
+
for (const stale of ["Alice", "Gina", "Jack", "Carol", "Iris", "Leo"]) {
|
|
87
|
+
assert.ok(
|
|
88
|
+
!got.includes(stale),
|
|
89
|
+
`answer leaked a stored member "${stale}": "${got}"`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
await m.store.close();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// GAP 3.2 (b) — SUMMARY generation: the answer is a SUBSET of the context's
|
|
96
|
+
// words (non-contiguous salient spans). The skill transfers to an unseen
|
|
97
|
+
// sentence, generating a summary of words drawn from the QUESTION itself.
|
|
98
|
+
test("3.2 — a SUMMARY skill generates an unseen summary from salient spans", async () => {
|
|
99
|
+
const m = mk();
|
|
100
|
+
await m.ingest([
|
|
101
|
+
[
|
|
102
|
+
"The report notes that revenue rose sharply across Europe.",
|
|
103
|
+
"revenue rose Europe",
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
"The report notes that profit fell steeply across Asia.",
|
|
107
|
+
"profit fell Asia",
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"The report notes that output grew slowly across Africa.",
|
|
111
|
+
"output grew Africa",
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
"The report notes that demand dropped quickly across America.",
|
|
115
|
+
"demand dropped America",
|
|
116
|
+
],
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const got = await ask(
|
|
120
|
+
m,
|
|
121
|
+
"The report notes that traffic surged massively across Oceania.",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// The generated summary must draw its salient words from the unseen sentence.
|
|
125
|
+
const salient = ["traffic", "surged", "Oceania"];
|
|
126
|
+
const present = salient.filter((w) => got.includes(w)).length;
|
|
127
|
+
assert.ok(
|
|
128
|
+
present >= 2,
|
|
129
|
+
`summary must synthesize the unseen sentence's salient words ` +
|
|
130
|
+
`(got ${present}/3 of ${salient.join(",")}) — "${got}"`,
|
|
131
|
+
);
|
|
132
|
+
// Not a recalled exemplar.
|
|
133
|
+
assert.ok(
|
|
134
|
+
!got.includes("revenue") && !got.includes("Europe") &&
|
|
135
|
+
!got.includes("Asia"),
|
|
136
|
+
`summary leaked stored words: "${got}"`,
|
|
137
|
+
);
|
|
138
|
+
await m.store.close();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// GAP 3.2 (c) — multi-piece extraction COMPOSES with a reasoning hop. Generating
|
|
142
|
+
// the list is one stage of a single pipeline, not an isolated branch.
|
|
143
|
+
test("3.2 — multi-piece extraction composes with a downstream hop", async () => {
|
|
144
|
+
const m = mk();
|
|
145
|
+
await m.ingest([
|
|
146
|
+
["The pair includes Alice and Bob.", "Alice and Bob"],
|
|
147
|
+
["The pair includes Dan and Eve.", "Dan and Eve"],
|
|
148
|
+
["The pair includes Gina and Hank.", "Gina and Hank"],
|
|
149
|
+
]);
|
|
150
|
+
// A fact keyed on the exact string the skill will generate.
|
|
151
|
+
await m.ingest(["Mona and Nick", "Mona and Nick lead the project"]);
|
|
152
|
+
|
|
153
|
+
const got = await ask(m, "The pair includes Mona and Nick.");
|
|
154
|
+
assert.ok(
|
|
155
|
+
got.includes("lead the project"),
|
|
156
|
+
`generated pair must hop to its downstream fact — got "${got}"`,
|
|
157
|
+
);
|
|
158
|
+
await m.store.close();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
162
|
+
// GAP 3.1 — MULTIPLE ORDERED POINTS OF ATTENTION.
|
|
163
|
+
//
|
|
164
|
+
// A scaffolding-dominated corpus (17-intelligence's pathology). A query names TWO
|
|
165
|
+
// unrelated distinctive topics under the shared scaffolding. The consensus climb
|
|
166
|
+
// must not pick ONE: it must expose two independent roots, the reasoner grounds
|
|
167
|
+
// each and fuses both. The answer must carry a marker of BOTH records.
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
169
|
+
const SYS =
|
|
170
|
+
"You are a helpful and harmless assistant.\n\nYou are not allowed to use any tools.\n";
|
|
171
|
+
|
|
172
|
+
async function twoTopicMind() {
|
|
173
|
+
const m = mk();
|
|
174
|
+
await m.ingest([
|
|
175
|
+
[
|
|
176
|
+
SYS + "Describe the importance of gender equality in the workplace.",
|
|
177
|
+
"Gender equality means equal chances to be hired and promoted.",
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
SYS + "Provide a summary of the 1992 Dream Team basketball squad.",
|
|
181
|
+
"The Dream Team won gold in Barcelona in 1992.",
|
|
182
|
+
],
|
|
183
|
+
[
|
|
184
|
+
SYS + "Explain how photosynthesis converts sunlight into energy.",
|
|
185
|
+
"Photosynthesis binds carbon dioxide and water into sugar.",
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
SYS + "Summarize the causes of the fall of the Western Roman Empire.",
|
|
189
|
+
"The Western Roman Empire fell from overreach and migration.",
|
|
190
|
+
],
|
|
191
|
+
]);
|
|
192
|
+
return m;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test("3.1 — a two-topic query fuses BOTH points of attention", async () => {
|
|
196
|
+
const m = await twoTopicMind();
|
|
197
|
+
const got = await ask(
|
|
198
|
+
m,
|
|
199
|
+
SYS +
|
|
200
|
+
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
201
|
+
);
|
|
202
|
+
await m.store.close();
|
|
203
|
+
|
|
204
|
+
const hasEquality = /hired|promoted|equal chances/i.test(got);
|
|
205
|
+
const hasDreamTeam = /Barcelona|gold|Dream Team/i.test(got);
|
|
206
|
+
assert.ok(
|
|
207
|
+
hasEquality && hasDreamTeam,
|
|
208
|
+
`a two-topic query must answer BOTH points of attention — ` +
|
|
209
|
+
`equality=${hasEquality} dreamTeam=${hasDreamTeam} — got "${got}"`,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("3.1 — the trace surfaces MORE THAN ONE ordered anchor for a two-topic query", async () => {
|
|
214
|
+
const m = await twoTopicMind();
|
|
215
|
+
const steps = [];
|
|
216
|
+
await m.respondText(
|
|
217
|
+
SYS +
|
|
218
|
+
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
219
|
+
(s) => steps.push(s),
|
|
220
|
+
);
|
|
221
|
+
await m.store.close();
|
|
222
|
+
|
|
223
|
+
// The consensus climb must expose the attention forest — more than one ordered
|
|
224
|
+
// anchor — not a single winner. (Emitted as the outputs of the climb step.)
|
|
225
|
+
const climbs = steps.filter((s) => s.mechanism.at(-1) === "climbConsensus");
|
|
226
|
+
assert.ok(climbs.length > 0, "no climbConsensus step was traced");
|
|
227
|
+
const maxAnchors = Math.max(...climbs.map((s) => s.outputs.length));
|
|
228
|
+
assert.ok(
|
|
229
|
+
maxAnchors >= 2,
|
|
230
|
+
`climbConsensus resolved a single point of attention ` +
|
|
231
|
+
`(max anchors ${maxAnchors}); the reasoner must order MULTIPLE`,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// The fusion of the further point must itself be traced (traceability, I10).
|
|
235
|
+
assert.ok(
|
|
236
|
+
steps.some((s) => s.mechanism.at(-1) === "fuseAttention"),
|
|
237
|
+
"the multi-root fusion was not surfaced in the rationale",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// …and the multi-root path must keep the rationale a SOUND, rooted DAG (no
|
|
241
|
+
// dangling / forward / self edges introduced by the new mechanisms).
|
|
242
|
+
const byIndex = new Map();
|
|
243
|
+
const emitPos = new Map();
|
|
244
|
+
steps.forEach((s, pos) => {
|
|
245
|
+
byIndex.set(s.index, s);
|
|
246
|
+
emitPos.set(s.index, pos);
|
|
247
|
+
});
|
|
248
|
+
for (const s of steps) {
|
|
249
|
+
if (s.parent !== -1) {
|
|
250
|
+
assert.ok(
|
|
251
|
+
byIndex.has(s.parent) && s.parent < s.index,
|
|
252
|
+
`bad parent @${s.index}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const d of s.dependsOn) {
|
|
256
|
+
assert.ok(
|
|
257
|
+
d !== s.index && byIndex.has(d) && d < s.index,
|
|
258
|
+
`bad dep @${s.index}`,
|
|
259
|
+
);
|
|
260
|
+
if (d !== s.parent) {
|
|
261
|
+
assert.ok(
|
|
262
|
+
emitPos.get(d) < emitPos.get(s.index),
|
|
263
|
+
`bad emit order @${s.index}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const root = byIndex.get(0);
|
|
269
|
+
assert.deepEqual(root.mechanism, ["respond"], "root is not respond");
|
|
270
|
+
assert.equal(root.parent, -1, "root has a parent");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// GAP 3.1 — a SINGLE-topic query must still resolve to ONE answer (no spurious
|
|
274
|
+
// second root injected). The multi-root machinery must be dominance-gated so it
|
|
275
|
+
// never degrades the scaffolding-dominated single-topic case that 17-intelligence pins.
|
|
276
|
+
test("3.1 — a single-topic query is not fragmented", async () => {
|
|
277
|
+
const m = await twoTopicMind();
|
|
278
|
+
// A single distinctive topic (resolves cleanly at baseline). The multi-root
|
|
279
|
+
// machinery must NOT drag in an unrelated second record.
|
|
280
|
+
const got = await ask(
|
|
281
|
+
m,
|
|
282
|
+
SYS + "the importance of gender equality in the workplace",
|
|
283
|
+
);
|
|
284
|
+
await m.store.close();
|
|
285
|
+
assert.ok(
|
|
286
|
+
/hired|promoted|equal chances/i.test(got),
|
|
287
|
+
`single topic lost: "${got}"`,
|
|
288
|
+
);
|
|
289
|
+
// Must not have dragged in an unrelated record.
|
|
290
|
+
assert.ok(
|
|
291
|
+
!/Barcelona|photosynthesis|Roman/i.test(got),
|
|
292
|
+
`single topic fragmented: "${got}"`,
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// GAP 3.1 — DOCUMENT FREQUENCY, three ways. The consensus climb weights each
|
|
297
|
+
// part's reach by document frequency; requirement 3.1 says it must leverage the
|
|
298
|
+
// INVERSE form (discrimination — what sets a part apart), the DIRECT form (theme
|
|
299
|
+
// — what a part shares with many contexts), AND a COMBINED read applying both to
|
|
300
|
+
// each part on the same input. All three must produce coherent, distinct
|
|
301
|
+
// orderings over the same query — the capability, not one hard-wired weighting.
|
|
302
|
+
test("3.1 — the consensus climb (climbAttention) leverages inverse, direct, AND combined document frequency", async () => {
|
|
303
|
+
const m = await twoTopicMind();
|
|
304
|
+
const q = new TextEncoder().encode(
|
|
305
|
+
SYS +
|
|
306
|
+
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// climbAttention is the multi-hierarchical core; each mode is a real read.
|
|
310
|
+
const inv = await m.climbAttention(q, 16, "inverse");
|
|
311
|
+
const dir = await m.climbAttention(q, 16, "direct");
|
|
312
|
+
const cmb = await m.climbAttention(q, 16, "combined");
|
|
313
|
+
await m.store.close();
|
|
314
|
+
|
|
315
|
+
// Each mode resolves the same two-topic forest (independent of the weighting).
|
|
316
|
+
for (
|
|
317
|
+
const [name, forest] of [["inverse", inv], ["direct", dir], [
|
|
318
|
+
"combined",
|
|
319
|
+
cmb,
|
|
320
|
+
]]
|
|
321
|
+
) {
|
|
322
|
+
assert.ok(
|
|
323
|
+
forest.length >= 2,
|
|
324
|
+
`${name} did not order multiple points of attention`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
// COMBINED is inverse+direct applied to each part: strictly the larger vote
|
|
328
|
+
// (both operations contribute), the defining property of the combined read.
|
|
329
|
+
assert.ok(
|
|
330
|
+
cmb[0].vote > inv[0].vote && cmb[0].vote > dir[0].vote,
|
|
331
|
+
`combined (${cmb[0].vote.toFixed(2)}) must exceed both inverse ` +
|
|
332
|
+
`(${inv[0].vote.toFixed(2)}) and direct (${dir[0].vote.toFixed(2)})`,
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// GAP 3.1 — the root COUNT is the data's own answer, not a tuned threshold. How
|
|
337
|
+
// many points of attention a query has must come from the NATURAL BREAK in the
|
|
338
|
+
// vote distribution: a single-topic query yields exactly ONE root, a K-topic
|
|
339
|
+
// query exactly K — with no magic constant deciding the cut. naturalBreak is
|
|
340
|
+
// scale-free (a ratio, not an absolute bar), so it holds across corpora without
|
|
341
|
+
// tuning. The mechanism is generic for any K; this test covers K = 1 and K = 2.
|
|
342
|
+
test("3.1 — the number of roots is read from the vote distribution, not a constant", async () => {
|
|
343
|
+
const m = await twoTopicMind();
|
|
344
|
+
const climb = async (q) =>
|
|
345
|
+
(await m.climbAttention(new TextEncoder().encode(SYS + q), 16)).length;
|
|
346
|
+
|
|
347
|
+
// One distinctive topic → ONE root (natural break is right after the top vote).
|
|
348
|
+
assert.equal(
|
|
349
|
+
await climb("the importance of gender equality in the workplace"),
|
|
350
|
+
1,
|
|
351
|
+
"a single-topic query must yield exactly one root (no fragmentation)",
|
|
352
|
+
);
|
|
353
|
+
// Two distinctive topics on disjoint spans → TWO roots.
|
|
354
|
+
assert.equal(
|
|
355
|
+
await climb(
|
|
356
|
+
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
357
|
+
),
|
|
358
|
+
2,
|
|
359
|
+
"a K-topic query must yield exactly K roots (K = 2 here)",
|
|
360
|
+
);
|
|
361
|
+
await m.store.close();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// GAP 3.1 — the natural break is SCALE-FREE: multiplying every vote by a constant
|
|
365
|
+
// (which a different corpus size / resonance regime would do) must not change the
|
|
366
|
+
// root count. This is the property a fixed absolute threshold could never have.
|
|
367
|
+
test("3.1 — the root decision is scale-invariant (relative, not absolute)", async () => {
|
|
368
|
+
// naturalBreak is a pure function of the sorted votes; prove the count it
|
|
369
|
+
// implies is invariant under uniform scaling on representative distributions.
|
|
370
|
+
const m = await twoTopicMind();
|
|
371
|
+
// A single-dominant distribution → 1 above the break; a plateau-of-two → 2.
|
|
372
|
+
const single = [3.66, 1.69, 1.21, 0.59];
|
|
373
|
+
const two = [2.99, 2.59, 0.82, 0.71];
|
|
374
|
+
const countAbove = (votes) => {
|
|
375
|
+
const cut = m.naturalBreak(votes);
|
|
376
|
+
return votes.filter((v) => v >= cut).length;
|
|
377
|
+
};
|
|
378
|
+
await m.store.close();
|
|
379
|
+
for (const scale of [1, 10, 0.01]) {
|
|
380
|
+
assert.equal(
|
|
381
|
+
countAbove(single.map((v) => v * scale)),
|
|
382
|
+
1,
|
|
383
|
+
`single @${scale}`,
|
|
384
|
+
);
|
|
385
|
+
assert.equal(countAbove(two.map((v) => v * scale)), 2, `two @${scale}`);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
390
|
+
// GAP 1 — the reasoner is PRIMARY: the produced answer is DERIVED, not recalled.
|
|
391
|
+
// The generated list is not any stored node — retrieval literally cannot return
|
|
392
|
+
// it, so the answer proves the reasoner is doing the work.
|
|
393
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
394
|
+
test("1 — the generated answer is not a stored node (derived, not retrieved)", async () => {
|
|
395
|
+
const m = mk();
|
|
396
|
+
await m.ingest([
|
|
397
|
+
["The team includes Alice, Bob, and Carol.", "Alice, Bob, Carol"],
|
|
398
|
+
["The team includes Dan, Eve, and Frank.", "Dan, Eve, Frank"],
|
|
399
|
+
["The team includes Gina, Hank, and Iris.", "Gina, Hank, Iris"],
|
|
400
|
+
]);
|
|
401
|
+
const got = await ask(m, "The team includes Mona, Nick, and Omar.");
|
|
402
|
+
|
|
403
|
+
// If this exact string were retrievable, it would resolve to a node; it must
|
|
404
|
+
// not — it was assembled by the reasoner from the query's own bytes.
|
|
405
|
+
const node = m.store.findLeaf?.(new TextEncoder().encode(got)) ?? null;
|
|
406
|
+
assert.equal(
|
|
407
|
+
node,
|
|
408
|
+
null,
|
|
409
|
+
`the generated answer "${got}" is a stored leaf — not derived`,
|
|
410
|
+
);
|
|
411
|
+
assert.ok(
|
|
412
|
+
got.includes("Mona") && got.includes("Omar"),
|
|
413
|
+
`not generated: "${got}"`,
|
|
414
|
+
);
|
|
415
|
+
await m.store.close();
|
|
416
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { cosine, Mind } from "../dist/src/index.js";
|
|
4
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
5
|
+
|
|
6
|
+
const D = 1024;
|
|
7
|
+
const mk = async (facts = []) => {
|
|
8
|
+
const m = new Mind({ seed: 7 });
|
|
9
|
+
if (facts.length) await m.ingest(facts);
|
|
10
|
+
return m;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const FACTS = [
|
|
14
|
+
["what is ice?", "ice is frozen water"],
|
|
15
|
+
["what is fire?", "fire is hot plasma"],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
test("embedding of an empty store / empty input is null", async () => {
|
|
19
|
+
const m = await mk();
|
|
20
|
+
assert.equal(await m.embedding("anything at all"), null);
|
|
21
|
+
const trained = await mk(FACTS);
|
|
22
|
+
assert.equal(await trained.embedding(""), null);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("embedding is a unit-length D-vector", async () => {
|
|
26
|
+
const m = await mk(FACTS);
|
|
27
|
+
const v = await m.embedding(FACTS[0][0]);
|
|
28
|
+
assert.ok(v !== null, "known query must embed");
|
|
29
|
+
assert.equal(v.length, D);
|
|
30
|
+
let sq = 0;
|
|
31
|
+
for (const x of v) sq += x * x;
|
|
32
|
+
assert.ok(
|
|
33
|
+
Math.abs(Math.sqrt(sq) - 1) < 1e-4,
|
|
34
|
+
`expected unit norm, got ${Math.sqrt(sq)}`,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("embedding IS the gist of respond()'s root (same vector)", async () => {
|
|
39
|
+
const m = await mk(FACTS);
|
|
40
|
+
const emb = await m.embedding(FACTS[0][0]);
|
|
41
|
+
const resp = await m.respond(FACTS[0][0]);
|
|
42
|
+
assert.ok(emb !== null && resp.v !== null);
|
|
43
|
+
assert.deepEqual([...emb], [...resp.v], "embedding must equal respond().v");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("embedding is deterministic for the same input", async () => {
|
|
47
|
+
const m = await mk(FACTS);
|
|
48
|
+
const a = await m.embedding(FACTS[1][0]);
|
|
49
|
+
const b = await m.embedding(FACTS[1][0]);
|
|
50
|
+
assert.deepEqual([...a], [...b]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a query embeds to the gist of its recalled answer", async () => {
|
|
54
|
+
// embedding = gist of the answer's root, so a question's embedding coincides
|
|
55
|
+
// with perceiving its learnt answer text directly.
|
|
56
|
+
const m = await mk(FACTS);
|
|
57
|
+
const q = await m.embedding("what is ice?");
|
|
58
|
+
const answerGist = m.perceive("ice is frozen water").v;
|
|
59
|
+
assert.ok(q !== null);
|
|
60
|
+
assert.ok(
|
|
61
|
+
cosine(q, answerGist) > 0.99,
|
|
62
|
+
`expected answer gist, cosine=${cosine(q, answerGist)}`,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("distinct concepts embed apart", async () => {
|
|
67
|
+
const m = await mk(FACTS);
|
|
68
|
+
const ice = await m.embedding("what is ice?");
|
|
69
|
+
const fire = await m.embedding("what is fire?");
|
|
70
|
+
assert.ok(ice !== null && fire !== null);
|
|
71
|
+
assert.ok(
|
|
72
|
+
cosine(ice, fire) < 0.9,
|
|
73
|
+
`expected separation, cosine=${cosine(ice, fire)}`,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// 26-cooperative.test.mjs — INGESTION MUST YIELD TO THE EVENT LOOP.
|
|
2
|
+
//
|
|
3
|
+
// A long ingest is CPU-bound work (perceive + intern + the synchronous vector-
|
|
4
|
+
// index writes at each batch flush). Node is single-threaded and cooperative:
|
|
5
|
+
// the ONLY way a timer callback, a pending I/O completion, or any other queued
|
|
6
|
+
// MACROTASK gets to run is if the running code returns to the event loop between
|
|
7
|
+
// units of work. `await` alone does NOT do this — a promise that is already
|
|
8
|
+
// resolved (or resolves synchronously, as every await inside the store's ingest
|
|
9
|
+
// path does) schedules its continuation on the MICROTASK queue, which the engine
|
|
10
|
+
// drains to EMPTY before it ever services a single macrotask. So a bulk ingest
|
|
11
|
+
// that only ever awaits already-settled promises monopolises the thread for its
|
|
12
|
+
// entire duration: nothing else scheduled can run until it finishes.
|
|
13
|
+
//
|
|
14
|
+
// This is the defect this file guards. It is a property of the CORE ingest path,
|
|
15
|
+
// not of any UI: a training driver that pins a 250ms progress timer, or overlaps
|
|
16
|
+
// downloading the next shard while depositing the current one, is silently
|
|
17
|
+
// starved — the timer does not fire, the socket is not drained — for as long as
|
|
18
|
+
// a deposit burst runs. The symptom a caller SEES is a frozen display and stalls
|
|
19
|
+
// in concurrent work; the CAUSE is here, so the guard is here.
|
|
20
|
+
//
|
|
21
|
+
// The test is deterministic and timing-jitter-free: it does NOT assert on wall
|
|
22
|
+
// clock. It arms a macrotask (setTimeout(…,0)) BEFORE a burst of ingests and
|
|
23
|
+
// checks whether it ran DURING the burst. A cooperative ingest path lets it run;
|
|
24
|
+
// a starving one does not. That yes/no fact is exact.
|
|
25
|
+
|
|
26
|
+
import { test } from "node:test";
|
|
27
|
+
import assert from "node:assert/strict";
|
|
28
|
+
import { Mind } from "../dist/src/index.js";
|
|
29
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
30
|
+
|
|
31
|
+
// Genuinely novel content: distinctive tokens that intern fresh leaves/branches
|
|
32
|
+
// and index new gists every call, so the burst does REAL work (many batch
|
|
33
|
+
// flushes), the same expensive path a real corpus drives — never a dedup no-op.
|
|
34
|
+
function novel(i, salt) {
|
|
35
|
+
const w = [];
|
|
36
|
+
for (let k = 0; k < 16; k++) w.push(`${salt}z${i}q${k}u${(i * 97 + k * 7)}`);
|
|
37
|
+
return w.join(" ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("ingestion yields to the event loop (a macrotask armed before a burst runs DURING it)", async () => {
|
|
41
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
42
|
+
const mind = new Mind({ seed: 7, store });
|
|
43
|
+
|
|
44
|
+
// Warm past the first batch flush so the burst measures steady state.
|
|
45
|
+
for (let i = 0; i < 200; i++) {
|
|
46
|
+
await mind.ingest(novel(i, "warm"), novel(i, "warmb"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A macrotask that records how many ingests had completed each time it ran.
|
|
50
|
+
// Re-arms itself so we sample the WHOLE burst, not just the first gap.
|
|
51
|
+
let ticks = 0;
|
|
52
|
+
let done = 0;
|
|
53
|
+
const firstTickAt = [];
|
|
54
|
+
let stop = false;
|
|
55
|
+
const arm = () => {
|
|
56
|
+
if (stop) return;
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
ticks++;
|
|
59
|
+
firstTickAt.push(done);
|
|
60
|
+
arm();
|
|
61
|
+
}, 0);
|
|
62
|
+
};
|
|
63
|
+
arm();
|
|
64
|
+
|
|
65
|
+
// A burst big enough to span MANY batch flushes (batchSize=1024 vectors; a
|
|
66
|
+
// 16-word pair indexes dozens of vectors, so ~600 pairs is tens of flushes).
|
|
67
|
+
const BURST = 600;
|
|
68
|
+
for (let i = 0; i < BURST; i++) {
|
|
69
|
+
await mind.ingest(novel(i, "burst"), novel(i, "burstb"));
|
|
70
|
+
done++;
|
|
71
|
+
}
|
|
72
|
+
stop = true;
|
|
73
|
+
await store.close();
|
|
74
|
+
|
|
75
|
+
console.log(
|
|
76
|
+
` during a ${BURST}-ingest burst, the 0ms macrotask ran ${ticks} time(s)` +
|
|
77
|
+
(ticks > 0 ? ` (first after ${firstTickAt[0]} ingests)` : " — STARVED"),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// The contract: the event loop is serviced DURING the burst, not only after it.
|
|
81
|
+
// A starving path leaves ticks === 0 (the timer callback is stuck behind the
|
|
82
|
+
// whole burst's microtask chain). A cooperative path lets it fire repeatedly.
|
|
83
|
+
assert.ok(
|
|
84
|
+
ticks > 0,
|
|
85
|
+
`a macrotask armed before a ${BURST}-ingest burst never ran during it — the ` +
|
|
86
|
+
`ingest path monopolises the event loop (microtask-only awaits), starving ` +
|
|
87
|
+
`timers, I/O completions, and any overlapped work for the burst's duration`,
|
|
88
|
+
);
|
|
89
|
+
});
|