@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,130 @@
|
|
|
1
|
+
// 22-multihop.test.mjs — REASONING THROUGH a partial result (the second half of
|
|
2
|
+
// "leverage the partial results of an experience").
|
|
3
|
+
//
|
|
4
|
+
// Test 21 pinned the first half: a query that is only a PORTION of a stored
|
|
5
|
+
// experience resolves to that experience (the interior of a fact/answer is a
|
|
6
|
+
// first-class resonance anchor, reachable by the structural climb). This file
|
|
7
|
+
// pins the harder half the task also asks for: the "think" rewrite must be able
|
|
8
|
+
// to USE a partial result as a BUILDING BLOCK — ground one fact, then take the
|
|
9
|
+
// piece of its answer the query still asks about and hop to the next fact.
|
|
10
|
+
//
|
|
11
|
+
// The shape is a two-fact chain that shares a pivot term:
|
|
12
|
+
//
|
|
13
|
+
// A: "What is the capital of France" → "The capital of France is Paris"
|
|
14
|
+
// B: "Paris" → "Paris is famous for the Eiffel Tower"
|
|
15
|
+
//
|
|
16
|
+
// Neither fact alone answers "What is the capital of France famous for". Fact A
|
|
17
|
+
// yields the partial result "Paris"; that partial result is the PIVOT into fact
|
|
18
|
+
// B. A true reasoner over the DAG composes the two: A's answer is not the end, it
|
|
19
|
+
// is the bridge. This is pure structure — the pivot "Paris" ties the two
|
|
20
|
+
// experiences together in the graph the store already built — so the hop falls
|
|
21
|
+
// out of `Mind.reason`: ground fact A, find the longest UNCONSUMED learnt context
|
|
22
|
+
// whose bytes the answer literally CONTAINS ("Paris"), and complete it forward.
|
|
23
|
+
//
|
|
24
|
+
// Byte containment is the gate, and it is hard graph evidence (the same "the form
|
|
25
|
+
// actually runs these bytes" test `bridge` uses): a genuine bridge exists only
|
|
26
|
+
// when the produced answer literally holds another learnt context. A query
|
|
27
|
+
// satisfied by ONE fact — or one whose neighbours merely share a frame (test 17) —
|
|
28
|
+
// produces an answer that contains no other context, so nothing pivots and the
|
|
29
|
+
// answer is returned untouched. That is why multi-hop composes transparently with
|
|
30
|
+
// ordinary recall and never fabricates a bridge.
|
|
31
|
+
//
|
|
32
|
+
// Without the multi-hop step every probe here stops at the first fact's answer;
|
|
33
|
+
// with it, the chain completes to the second fact. The aggregate bar clears with
|
|
34
|
+
// the reasoner and collapses (to ~0) without it.
|
|
35
|
+
|
|
36
|
+
import { test } from "node:test";
|
|
37
|
+
import assert from "node:assert/strict";
|
|
38
|
+
import { Mind } from "../dist/src/index.js";
|
|
39
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
40
|
+
|
|
41
|
+
// Each chain: factA (question → answer whose interior holds the PIVOT), factB
|
|
42
|
+
// (pivot → a further answer), a query that must cross the hop, and a distinctive
|
|
43
|
+
// marker of the SECOND fact's answer.
|
|
44
|
+
const CHAINS = [
|
|
45
|
+
{
|
|
46
|
+
a: ["What is the capital of France", "The capital of France is Paris"],
|
|
47
|
+
b: ["Paris", "Paris is famous for the Eiffel Tower"],
|
|
48
|
+
q: "What is the capital of France famous for",
|
|
49
|
+
want: "Eiffel Tower",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
a: ["Who wrote Hamlet", "Hamlet was written by William Shakespeare"],
|
|
53
|
+
b: ["William Shakespeare", "William Shakespeare was born in Stratford"],
|
|
54
|
+
q: "Where was the writer of Hamlet born",
|
|
55
|
+
want: "Stratford",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
a: ["What is the lightest metal", "The lightest metal is lithium"],
|
|
59
|
+
b: ["lithium", "lithium is used in rechargeable batteries"],
|
|
60
|
+
q: "What is the lightest metal used in",
|
|
61
|
+
want: "batteries",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
a: ["Which planet is the largest", "The largest planet is Jupiter"],
|
|
65
|
+
b: ["Jupiter", "Jupiter has a giant storm called the Great Red Spot"],
|
|
66
|
+
q: "What does the largest planet have",
|
|
67
|
+
want: "Great Red Spot",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
72
|
+
|
|
73
|
+
async function chainMind(chain, seed) {
|
|
74
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
75
|
+
const mind = new Mind({ seed, store });
|
|
76
|
+
await mind.ingest([chain.a, chain.b]);
|
|
77
|
+
return { store, mind };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("a query crosses a two-fact chain THROUGH the partial result", async () => {
|
|
81
|
+
const seed = 7;
|
|
82
|
+
const got = [];
|
|
83
|
+
for (const c of CHAINS) {
|
|
84
|
+
const { store, mind } = await chainMind(c, seed);
|
|
85
|
+
const ans = norm(await mind.respondText(c.q));
|
|
86
|
+
await store.close();
|
|
87
|
+
got.push({ q: c.q, ok: ans.includes(c.want), ans });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const total = got.filter((g) => g.ok).length;
|
|
91
|
+
// A reasoner that USES the partial result clears most of these; one that stops
|
|
92
|
+
// at the first fact's answer (no hop) scores ~0. The bar is generous to absorb
|
|
93
|
+
// ANN/codec jitter while still failing hard on a non-reasoning build.
|
|
94
|
+
assert.ok(
|
|
95
|
+
total >= 3,
|
|
96
|
+
`only ${total}/${CHAINS.length} chains crossed the hop — expected ≥ 3 ` +
|
|
97
|
+
`(the think rewrite must use a partial result as a building block)\n` +
|
|
98
|
+
got.filter((g) => !g.ok).map((g) =>
|
|
99
|
+
` ✗ "${g.q}" → ${g.ans.slice(0, 52)}`
|
|
100
|
+
).join("\n"),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// The gate: a query satisfied by the FIRST fact alone must NOT over-hop into the
|
|
105
|
+
// second. Asking exactly fact A's question returns fact A's answer, unchanged —
|
|
106
|
+
// the chain only fires when the query still asks for what the pivot leads to.
|
|
107
|
+
test("a single-fact query does not over-hop", async () => {
|
|
108
|
+
const c = CHAINS[0];
|
|
109
|
+
const { store, mind } = await chainMind(c, 7);
|
|
110
|
+
const ans = norm(await mind.respondText(c.a[0])); // "What is the capital of France"
|
|
111
|
+
await store.close();
|
|
112
|
+
assert.ok(
|
|
113
|
+
ans.includes("Paris") && !ans.includes("Eiffel"),
|
|
114
|
+
`a single-fact query over-hopped: "${ans.slice(0, 60)}" ` +
|
|
115
|
+
`(expected fact A's answer, not the chained fact B)`,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Determinism: the same chain query resolves identically across runs (the hop is
|
|
120
|
+
// a structural walk, not sampled).
|
|
121
|
+
test("the multi-hop answer is deterministic", async () => {
|
|
122
|
+
const c = CHAINS[0];
|
|
123
|
+
const runs = [];
|
|
124
|
+
for (let i = 0; i < 2; i++) {
|
|
125
|
+
const { store, mind } = await chainMind(c, 7);
|
|
126
|
+
runs.push(norm(await mind.respondText(c.q)));
|
|
127
|
+
await store.close();
|
|
128
|
+
}
|
|
129
|
+
assert.equal(runs[0], runs[1], "multi-hop reasoning must be deterministic");
|
|
130
|
+
});
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// 23-rationale.test.mjs — INFERENCE TRANSPARENCY (invariant I10).
|
|
2
|
+
//
|
|
3
|
+
// One of sema's defining advantages over a weight matrix is that every answer
|
|
4
|
+
// is a DERIVATION over explicit facts — and `inspectRationale` reads that
|
|
5
|
+
// derivation out, as it happens, for debugging the engine and for a human to
|
|
6
|
+
// follow the reasoning. This file pins the STRUCTURAL CONTRACT of that trace so
|
|
7
|
+
// a refactor of the inference path cannot silently break it:
|
|
8
|
+
//
|
|
9
|
+
// • a step's ordering is incremental and unique (the `index`);
|
|
10
|
+
// • the dependency graph is sound — every `parent` and every `dependsOn`
|
|
11
|
+
// references an EARLIER, real step (a DAG, no dangling/forward edges);
|
|
12
|
+
// • the nesting is rooted at `respond`, which spans the whole inference and
|
|
13
|
+
// ends with the produced answer;
|
|
14
|
+
// • inputs and outputs are VECTORS, and the fan-out / fan-in is real
|
|
15
|
+
// (recognise decomposes 1 query → N forms; the cover combines N → 1);
|
|
16
|
+
// • the cover's adapted A*LD proof tree surfaces at the finest grain (its reasoning
|
|
17
|
+
// MOVES are emitted), across representative queries — recall, multi-hop,
|
|
18
|
+
// ALU, concept-revoicing;
|
|
19
|
+
// • it is OFF BY DEFAULT — no callback ⇒ not even built, zero observable cost.
|
|
20
|
+
//
|
|
21
|
+
// The contract is asserted GENERICALLY (shape invariants that must hold for any
|
|
22
|
+
// query), so it keeps guarding as the engine evolves — not a brittle snapshot of
|
|
23
|
+
// one query's exact steps.
|
|
24
|
+
|
|
25
|
+
import { test } from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { Mind } from "../dist/src/index.js";
|
|
28
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
29
|
+
|
|
30
|
+
// A small test image, mirroring 05-concepts — used to bind a cross-name concept
|
|
31
|
+
// halo so the answer gets revoiced in the asker's word (exercises `voice`).
|
|
32
|
+
const img = (seed) => {
|
|
33
|
+
const d = new Uint8Array(16);
|
|
34
|
+
for (let i = 0; i < 16; i++) d[i] = (i * 13 + seed * 41) & 0xff;
|
|
35
|
+
return { width: 4, height: 4, channels: 1, data: d };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Collect the full step stream for one query.
|
|
39
|
+
async function trace(mind, q) {
|
|
40
|
+
const steps = [];
|
|
41
|
+
const ans = await mind.respondText(q, (s) => steps.push(s));
|
|
42
|
+
return { steps, ans };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The structural invariants every rationale stream must satisfy, asserted on the
|
|
46
|
+
// steps in EMISSION order (the order the callback saw them).
|
|
47
|
+
function assertWellFormed(steps, label) {
|
|
48
|
+
assert.ok(steps.length > 0, `${label}: expected at least one step`);
|
|
49
|
+
|
|
50
|
+
// Steps are EMITTED in completion order (a sub-mechanism finishes, and is
|
|
51
|
+
// reported, before the mechanism that called it), while `index` is assigned in
|
|
52
|
+
// ENTRY order (a parent reserves its index before its children run). So a
|
|
53
|
+
// parent always has a LOWER index than its children, but is emitted LATER.
|
|
54
|
+
// First pass: gather every index and its emission position.
|
|
55
|
+
const byIndex = new Map();
|
|
56
|
+
const emitPos = new Map(); // index → position in the emitted stream
|
|
57
|
+
steps.forEach((s, pos) => {
|
|
58
|
+
assert.ok(Number.isInteger(s.index) && s.index >= 0, `${label}: bad index`);
|
|
59
|
+
assert.ok(!byIndex.has(s.index), `${label}: duplicate index ${s.index}`);
|
|
60
|
+
byIndex.set(s.index, s);
|
|
61
|
+
emitPos.set(s.index, pos);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
for (const s of steps) {
|
|
65
|
+
// — shape —
|
|
66
|
+
assert.ok(
|
|
67
|
+
Array.isArray(s.mechanism) && s.mechanism.length >= 1,
|
|
68
|
+
`${label}: step ${s.index} has no mechanism path`,
|
|
69
|
+
);
|
|
70
|
+
assert.ok(Array.isArray(s.inputs), `${label}: step ${s.index} inputs`);
|
|
71
|
+
assert.ok(Array.isArray(s.outputs), `${label}: step ${s.index} outputs`);
|
|
72
|
+
for (const it of [...s.inputs, ...s.outputs]) {
|
|
73
|
+
assert.equal(
|
|
74
|
+
typeof it.text,
|
|
75
|
+
"string",
|
|
76
|
+
`${label}: step ${s.index} item has no text rendering`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// — nesting edge: a parent is entered BEFORE its child, so its index is
|
|
81
|
+
// strictly lower, and (by entry order) it is a real step. It is emitted
|
|
82
|
+
// AFTER the child (post-order), so we check the index relation, not order. —
|
|
83
|
+
if (s.parent !== -1) {
|
|
84
|
+
assert.ok(
|
|
85
|
+
byIndex.has(s.parent),
|
|
86
|
+
`${label}: step ${s.index} parent ${s.parent} is not a real step`,
|
|
87
|
+
);
|
|
88
|
+
assert.ok(
|
|
89
|
+
s.parent < s.index,
|
|
90
|
+
`${label}: step ${s.index} parent ${s.parent} is not earlier (by index)`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// — data-flow edges: every dependency was ENTERED before this step (lower
|
|
95
|
+
// index — no forward or self edges, so the graph is a DAG). A dependency
|
|
96
|
+
// that is NOT the enclosing parent is a genuine PRODUCER — its output
|
|
97
|
+
// became this step's input — so it also COMPLETED earlier (emitted before).
|
|
98
|
+
// The parent-as-default-dep of a first sub-step is the one backward-by-index
|
|
99
|
+
// but emitted-later edge (post-order), and is exempt from the emit check. —
|
|
100
|
+
assert.ok(
|
|
101
|
+
Array.isArray(s.dependsOn),
|
|
102
|
+
`${label}: step ${s.index} dependsOn`,
|
|
103
|
+
);
|
|
104
|
+
for (const d of s.dependsOn) {
|
|
105
|
+
assert.notEqual(
|
|
106
|
+
d,
|
|
107
|
+
s.index,
|
|
108
|
+
`${label}: step ${s.index} depends on itself`,
|
|
109
|
+
);
|
|
110
|
+
assert.ok(
|
|
111
|
+
byIndex.has(d),
|
|
112
|
+
`${label}: step ${s.index} dependsOn ${d} is not a real step`,
|
|
113
|
+
);
|
|
114
|
+
assert.ok(
|
|
115
|
+
d < s.index,
|
|
116
|
+
`${label}: step ${s.index} dependsOn ${d} is not earlier (by index)`,
|
|
117
|
+
);
|
|
118
|
+
if (d !== s.parent) {
|
|
119
|
+
assert.ok(
|
|
120
|
+
emitPos.get(d) < emitPos.get(s.index),
|
|
121
|
+
`${label}: step ${s.index} producer ${d} was not emitted earlier`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// — the root: index 0, parentless, mechanism exactly ["respond"], and it ends
|
|
128
|
+
// with the produced answer (or empty for the degenerate no-answer case). —
|
|
129
|
+
const root = byIndex.get(0);
|
|
130
|
+
assert.ok(root, `${label}: no step 0`);
|
|
131
|
+
assert.deepEqual(
|
|
132
|
+
root.mechanism,
|
|
133
|
+
["respond"],
|
|
134
|
+
`${label}: root is not respond`,
|
|
135
|
+
);
|
|
136
|
+
assert.equal(root.parent, -1, `${label}: root has a parent`);
|
|
137
|
+
|
|
138
|
+
// — every non-root step nests under respond: walking `parent` reaches 0. —
|
|
139
|
+
for (const s of steps) {
|
|
140
|
+
let cur = s;
|
|
141
|
+
let guard = 0;
|
|
142
|
+
while (cur.parent !== -1 && guard++ < 1000) cur = byIndex.get(cur.parent);
|
|
143
|
+
assert.equal(
|
|
144
|
+
cur.index,
|
|
145
|
+
0,
|
|
146
|
+
`${label}: step ${s.index} not rooted at respond`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return byIndex;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The set of innermost mechanism names that appeared.
|
|
154
|
+
const leafMechanisms = (steps) =>
|
|
155
|
+
new Set(steps.map((s) => s.mechanism[s.mechanism.length - 1]));
|
|
156
|
+
|
|
157
|
+
test("the rationale stream is a sound, rooted dependency DAG", async () => {
|
|
158
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
159
|
+
const mind = new Mind({ seed: 7, store });
|
|
160
|
+
await mind.ingest([["ice", "ice is frozen water"]]);
|
|
161
|
+
|
|
162
|
+
const { steps, ans } = await trace(mind, "ice");
|
|
163
|
+
await store.close();
|
|
164
|
+
|
|
165
|
+
assert.equal(ans, "ice is frozen water");
|
|
166
|
+
assertWellFormed(steps, "recall");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("recall traces the cover and its derivation moves", async () => {
|
|
170
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
171
|
+
const mind = new Mind({ seed: 7, store });
|
|
172
|
+
await mind.ingest([["ice", "ice is frozen water"]]);
|
|
173
|
+
|
|
174
|
+
const { steps } = await trace(mind, "ice");
|
|
175
|
+
await store.close();
|
|
176
|
+
|
|
177
|
+
const mechs = leafMechanisms(steps);
|
|
178
|
+
// The orchestration mechanisms…
|
|
179
|
+
for (const m of ["think", "recognise", "cover", "liftAnswer", "articulate"]) {
|
|
180
|
+
assert.ok(mechs.has(m), `recall: missing mechanism "${m}"`);
|
|
181
|
+
}
|
|
182
|
+
// …and the cover's finest grain: the proof tree's reasoning moves.
|
|
183
|
+
assert.ok(mechs.has("follow-edge"), `recall: no follow-edge derivation move`);
|
|
184
|
+
assert.ok(mechs.has("ground"), `recall: no ground derivation move`);
|
|
185
|
+
|
|
186
|
+
// recognise DECOMPOSES: one query in, ≥1 form out.
|
|
187
|
+
const rec = steps.find((s) => s.mechanism.at(-1) === "recognise");
|
|
188
|
+
assert.equal(rec.inputs.length, 1, "recognise should take one query");
|
|
189
|
+
assert.ok(rec.outputs.length >= 1, "recognise should emit forms");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("multi-hop reasoning surfaces the pivot and reason chain", async () => {
|
|
193
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
194
|
+
const mind = new Mind({ seed: 7, store });
|
|
195
|
+
await mind.ingest([
|
|
196
|
+
["What is the capital of France", "The capital of France is Paris"],
|
|
197
|
+
["Paris", "Paris is famous for the Eiffel Tower"],
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
const { steps, ans } = await trace(
|
|
201
|
+
mind,
|
|
202
|
+
"What is the capital of France famous for",
|
|
203
|
+
);
|
|
204
|
+
await store.close();
|
|
205
|
+
|
|
206
|
+
assert.ok(ans.includes("Eiffel"), `multi-hop did not chain: "${ans}"`);
|
|
207
|
+
const byIndex = assertWellFormed(steps, "multihop");
|
|
208
|
+
|
|
209
|
+
const mechs = leafMechanisms(steps);
|
|
210
|
+
// The fallback grounding pipeline + the reasoner that crosses the hop.
|
|
211
|
+
assert.ok(mechs.has("recallByResonance"), "multihop: no recallByResonance");
|
|
212
|
+
assert.ok(mechs.has("reason"), "multihop: no reason scope");
|
|
213
|
+
assert.ok(mechs.has("pivotStep"), "multihop: no pivotStep");
|
|
214
|
+
|
|
215
|
+
// The reason scope depends (transitively) on the grounding step that fed it —
|
|
216
|
+
// i.e. its dependency edge is a REAL earlier step, not the previous sibling by
|
|
217
|
+
// accident. We assert the generic soundness here; assertWellFormed already
|
|
218
|
+
// proved every edge is backward and real.
|
|
219
|
+
const reason = steps.find((s) => s.mechanism.at(-1) === "reason");
|
|
220
|
+
for (const d of reason.dependsOn) {
|
|
221
|
+
assert.ok(byIndex.has(d), "multihop: reason depends on a real step");
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("ALU computation is traced as recognise + evaluate + cover", async () => {
|
|
226
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
227
|
+
const mind = new Mind({ seed: 7, store });
|
|
228
|
+
await mind.ingest([["ice", "ice is frozen water"]]); // a non-empty store
|
|
229
|
+
|
|
230
|
+
const { steps, ans } = await trace(mind, "2+3");
|
|
231
|
+
await store.close();
|
|
232
|
+
|
|
233
|
+
assert.equal(ans, "5");
|
|
234
|
+
assertWellFormed(steps, "alu");
|
|
235
|
+
|
|
236
|
+
const mechs = leafMechanisms(steps);
|
|
237
|
+
assert.ok(mechs.has("computeExtensions"), "alu: no computeExtensions");
|
|
238
|
+
assert.ok(mechs.has("evalComputation"), "alu: no evalComputation");
|
|
239
|
+
|
|
240
|
+
// The cover consumes BOTH recognise and computeExtensions — an EXPLICIT producer edge,
|
|
241
|
+
// not the previous-sibling default. Find the think-level cover and check its
|
|
242
|
+
// deps include the computeExtensions step.
|
|
243
|
+
const compute = steps.find((s) => s.mechanism.at(-1) === "computeExtensions");
|
|
244
|
+
const cover = steps.find((s) =>
|
|
245
|
+
s.mechanism.at(-1) === "cover" && s.mechanism.includes("think")
|
|
246
|
+
);
|
|
247
|
+
assert.ok(cover, "alu: no cover under think");
|
|
248
|
+
assert.ok(
|
|
249
|
+
cover.dependsOn.includes(compute.index),
|
|
250
|
+
`alu: cover ${cover.dependsOn} should depend on computeExtensions ${compute.index}`,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("concept revoicing emits the voice move (the asker's own word)", async () => {
|
|
255
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
256
|
+
const mind = new Mind({ seed: 7, store });
|
|
257
|
+
// Two names for one concept (bound by a shared image halo), one fact.
|
|
258
|
+
await mind.ingest([
|
|
259
|
+
["ice", "ice is frozen water"],
|
|
260
|
+
[img(1), "ice"],
|
|
261
|
+
[img(2), "ice"],
|
|
262
|
+
[img(1), "hielo"],
|
|
263
|
+
[img(2), "hielo"],
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const { steps, ans } = await trace(mind, "hielo");
|
|
267
|
+
await store.close();
|
|
268
|
+
|
|
269
|
+
assert.equal(ans, "hielo is frozen water");
|
|
270
|
+
assertWellFormed(steps, "voice");
|
|
271
|
+
|
|
272
|
+
const mechs = leafMechanisms(steps);
|
|
273
|
+
assert.ok(mechs.has("substitute"), "voice: no substitute step");
|
|
274
|
+
assert.ok(mechs.has("voice"), "voice: no voice derivation move");
|
|
275
|
+
|
|
276
|
+
// The substitute step COMBINES: an answer form in, the asker's voice out.
|
|
277
|
+
const sub = steps.find((s) => s.mechanism.at(-1) === "substitute");
|
|
278
|
+
assert.ok(
|
|
279
|
+
sub.inputs.length >= 1 && sub.outputs.length >= 1,
|
|
280
|
+
"voice: substitute should map answer-forms to asker-voices",
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("off by default: no callback ⇒ no observable tracing, same answer", async () => {
|
|
285
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
286
|
+
const mind = new Mind({ seed: 7, store });
|
|
287
|
+
await mind.ingest([["ice", "ice is frozen water"]]);
|
|
288
|
+
|
|
289
|
+
// No callback at all — the tracer is never built; the answer is identical.
|
|
290
|
+
const plain = await mind.respondText("ice");
|
|
291
|
+
// With a callback that just counts — proves the callback path is what emits.
|
|
292
|
+
let n = 0;
|
|
293
|
+
const traced = await mind.respondText("ice", () => n++);
|
|
294
|
+
await store.close();
|
|
295
|
+
|
|
296
|
+
assert.equal(plain, "ice is frozen water");
|
|
297
|
+
assert.equal(traced, plain, "tracing must not change the answer");
|
|
298
|
+
assert.ok(n > 0, "a callback should receive steps");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("a degenerate empty query yields a single rooted respond step", async () => {
|
|
302
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
303
|
+
const mind = new Mind({ seed: 7, store });
|
|
304
|
+
await mind.ingest([["ice", "ice is frozen water"]]);
|
|
305
|
+
|
|
306
|
+
const steps = [];
|
|
307
|
+
const r = await mind.respond("", (s) => steps.push(s));
|
|
308
|
+
await store.close();
|
|
309
|
+
|
|
310
|
+
assert.equal(r.v, null, "empty query has no answer");
|
|
311
|
+
// think returns before opening its scope on an empty query, so respond is the
|
|
312
|
+
// sole step — still a well-formed (trivial) graph.
|
|
313
|
+
assert.equal(steps.length, 1, "empty query should emit only respond");
|
|
314
|
+
assert.deepEqual(steps[0].mechanism, ["respond"]);
|
|
315
|
+
assert.equal(steps[0].outputs.length, 0, "no answer ⇒ no output item");
|
|
316
|
+
});
|