@hviana/sema 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/mind/articulation.js +1 -1
- package/dist/src/mind/attention.d.ts +18 -2
- package/dist/src/mind/attention.js +88 -18
- package/dist/src/mind/bridge.d.ts +30 -0
- package/dist/src/mind/bridge.js +569 -0
- package/dist/src/mind/graph-search.d.ts +16 -1
- package/dist/src/mind/graph-search.js +34 -5
- package/dist/src/mind/match.d.ts +15 -2
- package/dist/src/mind/match.js +3 -8
- package/dist/src/mind/mechanisms/alu.js +8 -1
- package/dist/src/mind/mechanisms/cast.d.ts +54 -0
- package/dist/src/mind/mechanisms/cast.js +303 -48
- package/dist/src/mind/mechanisms/cover.js +24 -32
- package/dist/src/mind/mechanisms/extraction.js +75 -30
- package/dist/src/mind/mechanisms/recall.js +66 -0
- package/dist/src/mind/mind.d.ts +1 -0
- package/dist/src/mind/mind.js +6 -1
- package/dist/src/mind/pipeline.js +34 -2
- package/dist/src/mind/reasoning.d.ts +20 -1
- package/dist/src/mind/reasoning.js +84 -6
- package/dist/src/mind/recognition.js +157 -13
- package/dist/src/mind/traverse.js +16 -0
- package/dist/src/mind/types.d.ts +65 -2
- package/dist/src/mind/types.js +53 -7
- package/dist/src/store.d.ts +12 -3
- package/dist/src/store.js +9 -3
- package/package.json +1 -1
- package/src/mind/articulation.ts +1 -0
- package/src/mind/attention.ts +105 -17
- package/src/mind/bridge.ts +596 -0
- package/src/mind/graph-search.ts +59 -2
- package/src/mind/match.ts +19 -5
- package/src/mind/mechanisms/alu.ts +8 -1
- package/src/mind/mechanisms/cast.ts +336 -46
- package/src/mind/mechanisms/cover.ts +31 -36
- package/src/mind/mechanisms/extraction.ts +101 -40
- package/src/mind/mechanisms/recall.ts +79 -0
- package/src/mind/mind.ts +7 -1
- package/src/mind/pipeline.ts +37 -2
- package/src/mind/reasoning.ts +97 -5
- package/src/mind/recognition.ts +160 -12
- package/src/mind/traverse.ts +17 -0
- package/src/mind/types.ts +110 -6
- package/src/store.ts +19 -5
- package/test/35-attention-confidence.test.mjs +139 -0
- package/test/36-already-answered-fusion.test.mjs +128 -0
- package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
- package/test/38-reason-restate-guard.test.mjs +94 -0
- package/test/39-cast-restate-guard.test.mjs +102 -0
- package/test/40-choosenext-scale-guard.test.mjs +75 -0
- package/test/41-seatofnode-direction.test.mjs +85 -0
- package/test/42-recognise-trace-idempotence.test.mjs +106 -0
- package/test/43-cast-analog-seat.test.mjs +244 -0
- package/test/44-recognise-edge-whitespace.test.mjs +63 -0
- package/test/45-liftanswer-restated-trim.test.mjs +60 -0
- package/test/46-recognise-multibyte-edge.test.mjs +85 -0
- package/test/47-cast-comparison-coverage.test.mjs +134 -0
- package/test/48-recognise-turn-connective.test.mjs +125 -0
- package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
- package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 36-already-answered-fusion.test.mjs — a point of attention whose own
|
|
2
|
+
// learnt continuation is ALREADY present later in the query must not be
|
|
3
|
+
// fused in again.
|
|
4
|
+
//
|
|
5
|
+
// This is Category A of a two-part defect found while investigating a
|
|
6
|
+
// multi-turn dialogue's garbled fusion ("Thank you very much!" fused with
|
|
7
|
+
// unrelated points of attention). One of those points (root "Hello",
|
|
8
|
+
// anchor of the greeting) traced back to substantial, genuine regions —
|
|
9
|
+
// not noise — and its learnt continuation ("Hello! How can I assist you
|
|
10
|
+
// today?") was found to be VERBATIM present earlier in the query, because
|
|
11
|
+
// it was Sema's own prior reply, appended by addTurn the same way any turn
|
|
12
|
+
// is appended. Re-surfacing it is redundant: the query has already spoken
|
|
13
|
+
// its own answer.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately turn-agnostic and Mind-bookkeeping-free: Mind's multi-turn
|
|
16
|
+
// API is strictly a computational optimization (incremental fold reuse) —
|
|
17
|
+
// it must never be the thing inference depends on for correctness. This
|
|
18
|
+
// check uses only what already exists for ANY accumulated byte stream,
|
|
19
|
+
// single-shot or multi-turn: `follow()` (the same content-addressed
|
|
20
|
+
// continuation walk `reason()`'s own echo guard already uses,
|
|
21
|
+
// `ctx.store.prevCount(qId) > 0`, just applied per-candidate-root instead
|
|
22
|
+
// of to the whole query) and plain byte containment. A query that embeds
|
|
23
|
+
// its own prior exchange — via real respondTurn(), or a caller manually
|
|
24
|
+
// pasting a transcript into one respond() call — is caught identically.
|
|
25
|
+
//
|
|
26
|
+
// (Category B — coincidental echo of a short, generic CURRENT-turn phrase
|
|
27
|
+
// against unrelated corpus content, unrelated to staleness — is a SEPARATE
|
|
28
|
+
// defect, not addressed here; see the investigation notes for why breadth
|
|
29
|
+
// and regionSupport both fail to discriminate it from genuine fusion.)
|
|
30
|
+
|
|
31
|
+
import { test } from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { Mind } from "../dist/src/index.js";
|
|
34
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
35
|
+
import { fuseAttention } from "../dist/src/mind/reasoning.js";
|
|
36
|
+
import { gistOf } from "../dist/src/mind/primitives.js";
|
|
37
|
+
|
|
38
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
39
|
+
const dec = (b) =>
|
|
40
|
+
new TextDecoder().decode(b.filter((x) => x !== 0)).replace(/\s+/g, " ")
|
|
41
|
+
.trim();
|
|
42
|
+
|
|
43
|
+
// "greet" leads to "reply-greet" — a learnt exchange the query embeds BOTH
|
|
44
|
+
// halves of, exactly the shape addTurn produces (ask, then the system's own
|
|
45
|
+
// reply, concatenated raw). "red circle" is a genuine, unrelated second
|
|
46
|
+
// topic (test/34's own binding corpus) whose own continuation is nowhere
|
|
47
|
+
// in the query — the ordinary multi-topic case, which must still fuse.
|
|
48
|
+
const CORPUS = [
|
|
49
|
+
["greet", "reply-greet"],
|
|
50
|
+
["red", "is a color"],
|
|
51
|
+
["blue", "is a color"],
|
|
52
|
+
["circle", "is a shape"],
|
|
53
|
+
["square", "is a shape"],
|
|
54
|
+
["red circle", "answer alpha"],
|
|
55
|
+
["red square", "answer beta"],
|
|
56
|
+
["blue circle", "answer gamma"],
|
|
57
|
+
["blue square", "answer delta"],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const mk = (seed) =>
|
|
61
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
62
|
+
|
|
63
|
+
test("fuseAttention: a root whose continuation is already answered in the query is excluded", async () => {
|
|
64
|
+
const m = mk(1);
|
|
65
|
+
await m.ingest(CORPUS);
|
|
66
|
+
// "greet"'s own continuation, "reply-greet", is embedded right in the
|
|
67
|
+
// query — the ask and its answer both present, exactly like a
|
|
68
|
+
// conversation's own turn + reply. "red circle" is a genuine further
|
|
69
|
+
// topic with no such embedded answer.
|
|
70
|
+
const q = enc("greet reply-greet then red then circle");
|
|
71
|
+
const roots = await m.climbAttention(q, 24);
|
|
72
|
+
const greet = roots.find((r) => r.start === 0);
|
|
73
|
+
const redCircle = roots.find((r) => r.start !== 0);
|
|
74
|
+
assert.ok(greet, "expected the 'greet' root at query offset 0");
|
|
75
|
+
assert.ok(redCircle, "expected the 'red circle' binding root");
|
|
76
|
+
|
|
77
|
+
const guide = gistOf(m, q);
|
|
78
|
+
const primary = enc("PRIMARYANCHORTEXT");
|
|
79
|
+
// forest[0] is never independently projected (see reasoning.ts: it is
|
|
80
|
+
// treated as already primary's own source) — a placeholder keeps BOTH
|
|
81
|
+
// real roots in `rest`, where this filter actually applies.
|
|
82
|
+
const placeholder = { ...greet, start: q.length, end: q.length };
|
|
83
|
+
const pre = {
|
|
84
|
+
attention: async () => ({
|
|
85
|
+
roots: [placeholder, greet, redCircle],
|
|
86
|
+
ranked: [placeholder, greet, redCircle],
|
|
87
|
+
}),
|
|
88
|
+
guide,
|
|
89
|
+
};
|
|
90
|
+
const out = dec(await fuseAttention(m, q, primary, pre));
|
|
91
|
+
assert.ok(
|
|
92
|
+
!out.includes("reply-greet"),
|
|
93
|
+
`a root whose continuation is already answered in the query must not fuse in, got "${out}"`,
|
|
94
|
+
);
|
|
95
|
+
assert.ok(
|
|
96
|
+
out.includes("answer alpha"),
|
|
97
|
+
`a genuine further topic with no embedded answer must still fuse in, got "${out}"`,
|
|
98
|
+
);
|
|
99
|
+
await m.store.close();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("fuseAttention: ordinary multi-topic fusion (no embedded answers) is completely unaffected", async () => {
|
|
103
|
+
const m = mk(1);
|
|
104
|
+
await m.ingest(CORPUS);
|
|
105
|
+
const q = enc("red then circle");
|
|
106
|
+
const roots = await m.climbAttention(q, 24);
|
|
107
|
+
assert.equal(roots.length, 1, "expected the single joint binding root");
|
|
108
|
+
// Force a second, independent (unclimbed-style) inclusion path by
|
|
109
|
+
// reusing the SAME root twice at different synthetic positions, neither
|
|
110
|
+
// of which has an embedded answer — a pure regression check that the
|
|
111
|
+
// new filter doesn't fire when there is nothing to catch.
|
|
112
|
+
const guide = gistOf(m, q);
|
|
113
|
+
const primary = enc("PRIMARYANCHORTEXT");
|
|
114
|
+
const placeholder = { ...roots[0], start: q.length, end: q.length };
|
|
115
|
+
const pre = {
|
|
116
|
+
attention: async () => ({
|
|
117
|
+
roots: [placeholder, roots[0]],
|
|
118
|
+
ranked: [placeholder, roots[0]],
|
|
119
|
+
}),
|
|
120
|
+
guide,
|
|
121
|
+
};
|
|
122
|
+
const out = dec(await fuseAttention(m, q, primary, pre));
|
|
123
|
+
assert.ok(
|
|
124
|
+
out.includes("answer alpha"),
|
|
125
|
+
`an ordinary further topic must still fuse in, got "${out}"`,
|
|
126
|
+
);
|
|
127
|
+
await m.store.close();
|
|
128
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// 37-cluster-dispersion-fusion.test.mjs — Category B of the multi-turn
|
|
2
|
+
// garbling investigation: a further point of attention corroborated from
|
|
3
|
+
// only ONE local neighbourhood of the query must not be trusted to fuse in,
|
|
4
|
+
// regardless of how strong its vote is.
|
|
5
|
+
//
|
|
6
|
+
// Two measures were tried and falsified before this one:
|
|
7
|
+
// - Attention.breadth (dominance-fraction) starves a genuine, evenly-split
|
|
8
|
+
// multi-topic query — no root in a real N-way split can exceed half the
|
|
9
|
+
// vote (test/24 gap 3.1).
|
|
10
|
+
// - raw regionSupport (corroboration COUNT, summed) doesn't separate a
|
|
11
|
+
// genuine topic from a coincidental echo either — a short, structurally
|
|
12
|
+
// simple echo racks up as many corroborating regions as a real topic
|
|
13
|
+
// does (test/35's arithmetic echo: 4 regions, the same range as gap
|
|
14
|
+
// 3.1's real topics: 5-6).
|
|
15
|
+
//
|
|
16
|
+
// Both are MAGNITUDE measures over the SAME span. The live defect (traced
|
|
17
|
+
// directly against the trained store) was different in kind: an unrelated
|
|
18
|
+
// node.js answer got fused in because the query's own short closing phrase
|
|
19
|
+
// ("Thank you very much!") happened to be byte-identical to how ONE other,
|
|
20
|
+
// wholly unrelated training conversation also ended — a single coincidental
|
|
21
|
+
// match, corroborated from exactly ONE place in the query. Compared
|
|
22
|
+
// against gap 3.1's two GENUINE topics, each of which is corroborated from
|
|
23
|
+
// TWO distinct, widely separated clusters of the query (verified directly:
|
|
24
|
+
// "Dream Team" from spans [80,96] and [132,148]; "gender equality" from
|
|
25
|
+
// [96,112] and [116,120] — the query's own SYS-scaffolding regions plus the
|
|
26
|
+
// topic's own distinctive wording, never just one).
|
|
27
|
+
//
|
|
28
|
+
// The discriminator is therefore not "how much" evidence but "how many
|
|
29
|
+
// separate PLACES in the query" corroborate it: cluster the contributing
|
|
30
|
+
// regions by merging any two whose gap is strictly less than one river-fold
|
|
31
|
+
// quantum W (ctx.space.maxGroup — the same quantum thinBar, identityBar and
|
|
32
|
+
// reachThreshold already derive from; never a new tuned number) and require
|
|
33
|
+
// at least 2 clusters. Verified against the boundary case directly: gap
|
|
34
|
+
// 3.1's "gender equality" clusters are exactly W apart, and `gap < W`
|
|
35
|
+
// (strict) is what keeps them separate — `gap <= W` would collapse them
|
|
36
|
+
// into one and break the pinned gap-3.1 requirement.
|
|
37
|
+
|
|
38
|
+
import { test } from "node:test";
|
|
39
|
+
import assert from "node:assert/strict";
|
|
40
|
+
import { Mind } from "../dist/src/index.js";
|
|
41
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
42
|
+
import { fuseAttention } from "../dist/src/mind/reasoning.js";
|
|
43
|
+
import { gistOf } from "../dist/src/mind/primitives.js";
|
|
44
|
+
|
|
45
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
46
|
+
const dec = (b) =>
|
|
47
|
+
new TextDecoder().decode(b.filter((x) => x !== 0)).replace(/\s+/g, " ")
|
|
48
|
+
.trim();
|
|
49
|
+
|
|
50
|
+
// A genuine cross-region binding (test/34's corpus) mixed with a
|
|
51
|
+
// coincidental arithmetic echo (test/35's corpus) in the SAME climb —
|
|
52
|
+
// reusing the exact same real anchors test/36 verified project to
|
|
53
|
+
// "answer alpha" and "2" respectively. The echo root's `end` is widened
|
|
54
|
+
// past the full "2+2" expression below (a harmless adjustment to a
|
|
55
|
+
// synthetic Attention object we already fabricate) so Fix A's own
|
|
56
|
+
// already-answered scan does not trip over the SECOND "2" digit sitting
|
|
57
|
+
// immediately after the echo's single-byte match — an unrelated collision
|
|
58
|
+
// specific to this corpus, not the mechanism under test here.
|
|
59
|
+
const CORPUS = [
|
|
60
|
+
["red", "is a color"],
|
|
61
|
+
["blue", "is a color"],
|
|
62
|
+
["circle", "is a shape"],
|
|
63
|
+
["square", "is a shape"],
|
|
64
|
+
["red circle", "answer alpha"],
|
|
65
|
+
["red square", "answer beta"],
|
|
66
|
+
["blue circle", "answer gamma"],
|
|
67
|
+
["blue square", "answer delta"],
|
|
68
|
+
["1+2", "3"],
|
|
69
|
+
["2+2", "4"],
|
|
70
|
+
["2+3", "5"],
|
|
71
|
+
["3+3", "6"],
|
|
72
|
+
["3+5", "8"],
|
|
73
|
+
["4+3", "7"],
|
|
74
|
+
["2+5", "7"],
|
|
75
|
+
["1+5", "6"],
|
|
76
|
+
["6+1", "7"],
|
|
77
|
+
["4+1", "5"],
|
|
78
|
+
["3+4", "7"],
|
|
79
|
+
["5+2", "7"],
|
|
80
|
+
["1+1", "2"],
|
|
81
|
+
["5+3", "8"],
|
|
82
|
+
["7+1", "8"],
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const mk = (seed) =>
|
|
86
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
87
|
+
|
|
88
|
+
async function setup() {
|
|
89
|
+
const m = mk(1);
|
|
90
|
+
await m.ingest(CORPUS);
|
|
91
|
+
const q = enc("red circle then 2+2 equals what");
|
|
92
|
+
const roots = await m.climbAttention(q, 24);
|
|
93
|
+
const binding = roots.find((r) => r.start === 0); // "red circle" [0,10)
|
|
94
|
+
const echo = roots.find((r) => r.start === 16); // "1+1" [16,17)
|
|
95
|
+
assert.ok(binding, "expected the 'red circle' binding root");
|
|
96
|
+
assert.ok(echo, "expected the '1+1' echo root");
|
|
97
|
+
const guide = gistOf(m, q);
|
|
98
|
+
return { m, q, binding, echo, guide };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The gate is an OR of two independent measures (verified against the full
|
|
102
|
+
// suite: a plain AND-only cluster requirement broke test/34's own joint
|
|
103
|
+
// binding, whose crossRegionVotes conclusion is pooled from a single
|
|
104
|
+
// synthetic region — one cluster by construction — despite already, by
|
|
105
|
+
// design, accounting for BOTH original query mentions; `breadth` already
|
|
106
|
+
// recognises that case correctly, since a genuine joint binding dominates
|
|
107
|
+
// the query on its own). These three tests cover the three quadrants a
|
|
108
|
+
// root can land in; breadth is set explicitly (these are already fabricated
|
|
109
|
+
// synthetic Attention objects) so each quadrant is tested precisely rather
|
|
110
|
+
// than relying on incidental corpus values.
|
|
111
|
+
|
|
112
|
+
test("fuseAttention: 2 clusters, low breadth — dispersion alone is enough (gap 3.1's shape)", async () => {
|
|
113
|
+
const { m, q, binding, echo, guide } = await setup();
|
|
114
|
+
const primary = enc("PRIMARYANCHORTEXT");
|
|
115
|
+
const dispersedLowBreadth = { ...binding, clusters: 2, breadth: 0.12 };
|
|
116
|
+
const placeholder = {
|
|
117
|
+
...binding,
|
|
118
|
+
start: q.length,
|
|
119
|
+
end: q.length,
|
|
120
|
+
clusters: 1,
|
|
121
|
+
breadth: 0.12,
|
|
122
|
+
};
|
|
123
|
+
const pre = {
|
|
124
|
+
attention: async () => ({
|
|
125
|
+
roots: [placeholder, dispersedLowBreadth],
|
|
126
|
+
ranked: [placeholder, dispersedLowBreadth],
|
|
127
|
+
}),
|
|
128
|
+
guide,
|
|
129
|
+
};
|
|
130
|
+
const out = dec(await fuseAttention(m, q, primary, pre));
|
|
131
|
+
assert.equal(
|
|
132
|
+
out,
|
|
133
|
+
"answer alpha" + dec(primary),
|
|
134
|
+
`2 clusters must fuse in even with low breadth, got "${out}"`,
|
|
135
|
+
);
|
|
136
|
+
await m.store.close();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("fuseAttention: 1 cluster, dominant breadth — dominance alone is enough (test/34's joint-binding shape)", async () => {
|
|
140
|
+
const { m, q, binding, guide } = await setup();
|
|
141
|
+
const primary = enc("PRIMARYANCHORTEXT");
|
|
142
|
+
const tightButDominant = { ...binding, clusters: 1, breadth: 0.857 };
|
|
143
|
+
const placeholder = {
|
|
144
|
+
...binding,
|
|
145
|
+
start: q.length,
|
|
146
|
+
end: q.length,
|
|
147
|
+
clusters: 1,
|
|
148
|
+
breadth: 0.857,
|
|
149
|
+
};
|
|
150
|
+
const pre = {
|
|
151
|
+
attention: async () => ({
|
|
152
|
+
roots: [placeholder, tightButDominant],
|
|
153
|
+
ranked: [placeholder, tightButDominant],
|
|
154
|
+
}),
|
|
155
|
+
guide,
|
|
156
|
+
};
|
|
157
|
+
const out = dec(await fuseAttention(m, q, primary, pre));
|
|
158
|
+
assert.equal(
|
|
159
|
+
out,
|
|
160
|
+
"answer alpha" + dec(primary),
|
|
161
|
+
`dominant breadth must fuse in even with 1 cluster, got "${out}"`,
|
|
162
|
+
);
|
|
163
|
+
await m.store.close();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("fuseAttention: 1 cluster, low breadth — neither measure saves it (the live coincidental-echo shape)", async () => {
|
|
167
|
+
const { m, q, echo, guide } = await setup();
|
|
168
|
+
const primary = enc("PRIMARYANCHORTEXT");
|
|
169
|
+
const tightAndWeak = {
|
|
170
|
+
...echo,
|
|
171
|
+
end: echo.end + 2,
|
|
172
|
+
clusters: 1,
|
|
173
|
+
breadth: 0.211,
|
|
174
|
+
};
|
|
175
|
+
const placeholder = { ...tightAndWeak, start: q.length, end: q.length };
|
|
176
|
+
const pre = {
|
|
177
|
+
attention: async () => ({
|
|
178
|
+
roots: [placeholder, tightAndWeak],
|
|
179
|
+
ranked: [placeholder, tightAndWeak],
|
|
180
|
+
}),
|
|
181
|
+
guide,
|
|
182
|
+
};
|
|
183
|
+
const out = dec(await fuseAttention(m, q, primary, pre));
|
|
184
|
+
assert.equal(
|
|
185
|
+
out,
|
|
186
|
+
dec(primary),
|
|
187
|
+
`1 cluster AND low breadth must not fuse in, got "${out}"`,
|
|
188
|
+
);
|
|
189
|
+
await m.store.close();
|
|
190
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 38-reason-restate-guard.test.mjs — reason()'s multi-hop chain
|
|
2
|
+
// (absorbForward/pivotStep) must not walk onto a fixpoint whose bytes are
|
|
3
|
+
// ALREADY PRESENT in the query.
|
|
4
|
+
//
|
|
5
|
+
// Traced live (analyze_training.ts, dialogue D): a scaffolding-dominated
|
|
6
|
+
// query ("Can you help me with something?", accumulated on top of "Hello,
|
|
7
|
+
// how are you today?Hello! How can I assist you today?") legitimately
|
|
8
|
+
// grounds (recallByResonance tier 2, consensus-floor-gated) on an unrelated
|
|
9
|
+
// but well-corroborated anchor. reason() then hops FORWARD from that
|
|
10
|
+
// grounding across two further, ungated steps and lands on the literal
|
|
11
|
+
// bytes "Hello" — content already sitting in the query's own first turn.
|
|
12
|
+
// The final answer is that bare, contextless echo.
|
|
13
|
+
//
|
|
14
|
+
// recallByResonance already refuses exactly this shape at grounding time —
|
|
15
|
+
// three separate guards (`restates`, tier 2's subspan check, tier 0b's
|
|
16
|
+
// argument-binding subspan check) all reject a candidate whose bytes are a
|
|
17
|
+
// proper byte-subspan of the query, because voicing them back only restates
|
|
18
|
+
// part of the question. reason() is the one place that walks MULTIPLE
|
|
19
|
+
// additional hops past the initial (already-vetted) grounding, and it had
|
|
20
|
+
// no equivalent guard — every hop only checked structural novelty
|
|
21
|
+
// (`consumed`), never whether the result had drifted onto something the
|
|
22
|
+
// query already contains. Fix: apply the same restates/subspan guard,
|
|
23
|
+
// verbatim, to reason()'s own hop candidates — no new tuned constant, just
|
|
24
|
+
// the existing convention applied where it was missing.
|
|
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
|
+
import { reason } from "../dist/src/mind/reasoning.js";
|
|
31
|
+
import { gistOf, resolve } from "../dist/src/mind/primitives.js";
|
|
32
|
+
|
|
33
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
34
|
+
const dec = (b) => new TextDecoder().decode(b);
|
|
35
|
+
|
|
36
|
+
const mk = (seed) =>
|
|
37
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
38
|
+
|
|
39
|
+
test("reason(): a forward hop landing on bytes already present in the query is refused", async () => {
|
|
40
|
+
const m = mk(1);
|
|
41
|
+
// "bridge context" is a learnt fact SOURCE whose continuation is "Hello" —
|
|
42
|
+
// the multi-hop chain's next, and only, forward step.
|
|
43
|
+
await m.ingest([["ask topic", "bridge context"], [
|
|
44
|
+
"bridge context",
|
|
45
|
+
"Hello",
|
|
46
|
+
]]);
|
|
47
|
+
|
|
48
|
+
// The query already contains "Hello" verbatim — e.g. a prior turn's own
|
|
49
|
+
// greeting, exactly the accumulated-conversation shape from the live
|
|
50
|
+
// trace. The grounded starting answer is "bridge context" (as if some
|
|
51
|
+
// upstream mechanism already grounded it) — reason()'s job is only to
|
|
52
|
+
// decide whether to hop FURTHER from it.
|
|
53
|
+
const query = enc("Hello, ask topic what happens next");
|
|
54
|
+
const answer = enc("bridge context");
|
|
55
|
+
const pre = {
|
|
56
|
+
guide: gistOf(m, query),
|
|
57
|
+
queryResolved: resolve(m, query),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const out = await reason(m, query, answer, new Set(), pre);
|
|
61
|
+
assert.equal(
|
|
62
|
+
dec(out),
|
|
63
|
+
"bridge context",
|
|
64
|
+
`must not hop onto "Hello" — it already restates content the query itself contains, got "${
|
|
65
|
+
dec(out)
|
|
66
|
+
}"`,
|
|
67
|
+
);
|
|
68
|
+
await m.store.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("reason(): an ordinary forward hop onto genuinely new content is unaffected", async () => {
|
|
72
|
+
const m = mk(1);
|
|
73
|
+
await m.ingest([["ask topic", "bridge context"], [
|
|
74
|
+
"bridge context",
|
|
75
|
+
"a wholly new fact never mentioned in any query",
|
|
76
|
+
]]);
|
|
77
|
+
|
|
78
|
+
const query = enc("Hello, ask topic what happens next");
|
|
79
|
+
const answer = enc("bridge context");
|
|
80
|
+
const pre = {
|
|
81
|
+
guide: gistOf(m, query),
|
|
82
|
+
queryResolved: resolve(m, query),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const out = await reason(m, query, answer, new Set(), pre);
|
|
86
|
+
assert.equal(
|
|
87
|
+
dec(out),
|
|
88
|
+
"a wholly new fact never mentioned in any query",
|
|
89
|
+
`an ordinary hop onto genuinely new content must still fire, got "${
|
|
90
|
+
dec(out)
|
|
91
|
+
}"`,
|
|
92
|
+
);
|
|
93
|
+
await m.store.close();
|
|
94
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// 39-cast-restate-guard.test.mjs — CAST's substitution schema
|
|
2
|
+
// (projectCounterfactual, cast.ts) must not append a forward-followed edge
|
|
3
|
+
// whose bytes are ALREADY PRESENT in the query — the same defect test/38
|
|
4
|
+
// fixed in reason(), found again in a sibling mechanism.
|
|
5
|
+
//
|
|
6
|
+
// Traced live (analyze_training.ts, dialogue D, turn 4): CAST's substitution
|
|
7
|
+
// schema transfers a displaced structure onto a filler ("Bitte spiele...");
|
|
8
|
+
// after building the substitution, it separately follows the displaced
|
|
9
|
+
// structure's OWN learnt forward-continuation edge and appends whatever
|
|
10
|
+
// that turns up — in the live trace, the literal bytes "Hello", because in
|
|
11
|
+
// the training corpus that structure happens to be followed, in some
|
|
12
|
+
// wholly unrelated conversation, by another dialogue opening with "Hello".
|
|
13
|
+
// The accumulated query's own first turn is "Hello, how are you today?", so
|
|
14
|
+
// the appended "Hello" restates content the query already contains — the
|
|
15
|
+
// SAME defect class as test/38, just reached through cast.ts's own
|
|
16
|
+
// `follow()` call (line ~299) instead of reason()'s hop loop. The old
|
|
17
|
+
// guard there only checked the candidate wasn't already inside the
|
|
18
|
+
// PARTIALLY-BUILT ANSWER (`indexOf(answer, fwd, 0) < 0`) — never that it
|
|
19
|
+
// wasn't already in the QUERY. Fix: reuse `restatesQuery` (exported from
|
|
20
|
+
// reasoning.ts, the exact function test/38 introduced) at the same call
|
|
21
|
+
// site — no new tuned constant, the existing convention extended to a
|
|
22
|
+
// second place it was missing.
|
|
23
|
+
|
|
24
|
+
import { test } from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { Mind } from "../dist/src/index.js";
|
|
27
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
28
|
+
|
|
29
|
+
const mk = (seed) =>
|
|
30
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
31
|
+
|
|
32
|
+
test("CAST substitution: a followed edge landing on bytes already present in the query is refused", async () => {
|
|
33
|
+
const m = mk(7);
|
|
34
|
+
// "Ice is cold" is CAST's displaced structure for "What if steel were
|
|
35
|
+
// cold?" (mirrors test/33's own substitution corpus). A second edge from
|
|
36
|
+
// the SAME source, corroborated by a second, unrelated predecessor, makes
|
|
37
|
+
// "Water is wet" — and, one further hop, "wet" — its resonance-preferred
|
|
38
|
+
// forward continuation, exactly the "some other conversation continues
|
|
39
|
+
// from the same node" shape the live trace showed.
|
|
40
|
+
await m.ingest([
|
|
41
|
+
["Ice is cold", "cold"],
|
|
42
|
+
["Fire is hot", "hot"],
|
|
43
|
+
["Steel is hard", "hard"],
|
|
44
|
+
["Water is wet", "wet"],
|
|
45
|
+
["Ice is cold", "Water is wet"],
|
|
46
|
+
["Something else entirely", "Water is wet"],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const steps = [];
|
|
50
|
+
// The query already contains "wet" verbatim (as if some earlier turn had
|
|
51
|
+
// already mentioned it) — reason() and CAST both must recognise the
|
|
52
|
+
// followed edge restates it, not append it a second time.
|
|
53
|
+
const r = await m.respond(
|
|
54
|
+
"What if steel were cold? Not wet.",
|
|
55
|
+
(s) => steps.push(s),
|
|
56
|
+
);
|
|
57
|
+
const got = new TextDecoder().decode(r.bytes).replace(/\0/g, "");
|
|
58
|
+
assert.ok(
|
|
59
|
+
!got.includes("wet") || got.includes("Not wet"),
|
|
60
|
+
`substitution must not append "wet" a second time — it already restates ` +
|
|
61
|
+
`the query, got "${got}"`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const proj = steps.find((s) =>
|
|
65
|
+
s.mechanism.at(-1) === "projectCounterfactual" &&
|
|
66
|
+
s.inputs.some((i) => i.role === "filler")
|
|
67
|
+
);
|
|
68
|
+
assert.ok(
|
|
69
|
+
proj,
|
|
70
|
+
"expected the substitution schema's projectCounterfactual step",
|
|
71
|
+
);
|
|
72
|
+
const projected = new TextDecoder().decode(
|
|
73
|
+
new TextEncoder().encode(proj.outputs[0].text),
|
|
74
|
+
);
|
|
75
|
+
assert.ok(
|
|
76
|
+
!projected.includes("wet"),
|
|
77
|
+
`the substitution's own projection must stop before the restating hop, got "${projected}"`,
|
|
78
|
+
);
|
|
79
|
+
await m.store.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("CAST substitution: an ordinary followed edge onto genuinely new content is unaffected", async () => {
|
|
83
|
+
const m = mk(7);
|
|
84
|
+
await m.ingest([
|
|
85
|
+
["Ice is cold", "cold"],
|
|
86
|
+
["Fire is hot", "hot"],
|
|
87
|
+
["Steel is hard", "hard"],
|
|
88
|
+
["Water is wet", "wet"],
|
|
89
|
+
["Ice is cold", "Water is wet"],
|
|
90
|
+
["Something else entirely", "Water is wet"],
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const steps = [];
|
|
94
|
+
const r = await m.respond("What if steel were cold?", (s) => steps.push(s));
|
|
95
|
+
const got = new TextDecoder().decode(r.bytes).replace(/\0/g, "");
|
|
96
|
+
assert.ok(
|
|
97
|
+
got.includes("wet"),
|
|
98
|
+
`an ordinary followed edge onto content the query never mentioned must ` +
|
|
99
|
+
`still fire, got "${got}"`,
|
|
100
|
+
);
|
|
101
|
+
await m.store.close();
|
|
102
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// 40-choosenext-scale-guard.test.mjs — chooseNext's disambiguation must not
|
|
2
|
+
// be gated by consensusFloor(N) at corpus scale.
|
|
3
|
+
//
|
|
4
|
+
// Traced live (analyze_training.ts, robustness probe "typo in query"):
|
|
5
|
+
// "What is the capitol of France?" (typo: capitol→capital) correctly
|
|
6
|
+
// resonates onto the learnt node "What is the capital of France?", whose
|
|
7
|
+
// forward edges are UNAMBIGUOUSLY dominated by "The capital of France is
|
|
8
|
+
// Paris." (prevCount 2, vs 1/1/1 for three other edges) — a genuine,
|
|
9
|
+
// structural winner by chooseNext's own loop (traverse.ts:517-529), which
|
|
10
|
+
// already requires strict dominance; a tie leaves first-inserted as the
|
|
11
|
+
// pick, exactly the "no real winner" case a floor would matter for.
|
|
12
|
+
//
|
|
13
|
+
// But chooseNext ALSO gated this pick on `bestSupport < consensusFloor(N)`
|
|
14
|
+
// once the corpus scale crosses atomIsHub's threshold (traverse.ts:541-546)
|
|
15
|
+
// — reusing the SAME ln(N)+0.5 floor recallByResonance and commitVotes use
|
|
16
|
+
// for POOLED, IDF-weighted CLIMB VOTES (each region worth up to ln N, so a
|
|
17
|
+
// sum exceeding ln N + 0.5 is more than any one region could say alone —
|
|
18
|
+
// HOW_IT_WORKS.md §8.6). `prevCount(candidate)` is a different kind of
|
|
19
|
+
// quantity: a raw count of how many training contexts independently
|
|
20
|
+
// predicted ONE destination, bounded by how many times that specific fact
|
|
21
|
+
// was retold — NOT by corpus size N. Gating an N-invariant count against
|
|
22
|
+
// an N-growing threshold guarantees failure once N is large enough
|
|
23
|
+
// (verified live: N≈325K gives a floor of ≈13.19, so a genuinely dominant
|
|
24
|
+
// but only-doubly-attested fact like "capital of France → Paris" was
|
|
25
|
+
// refused, falling back to a noisy concept-hop that produced the wrong
|
|
26
|
+
// answer). HOW_IT_WORKS.md's own canonical chooseNext pseudocode (§25)
|
|
27
|
+
// has NO such floor — it's undocumented implementation drift, not a
|
|
28
|
+
// deliberate design surface. Fix: remove the gate; chooseNext's existing
|
|
29
|
+
// strict-dominance loop already IS the "genuinely competing" test.
|
|
30
|
+
|
|
31
|
+
import { test } from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { Mind } from "../dist/src/index.js";
|
|
34
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
35
|
+
import { chooseNext } from "../dist/src/mind/traverse.js";
|
|
36
|
+
import { gistOf, resolve } from "../dist/src/mind/primitives.js";
|
|
37
|
+
|
|
38
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
39
|
+
|
|
40
|
+
test("chooseNext: a genuinely-dominant edge is trusted at large corpus scale", async () => {
|
|
41
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
42
|
+
|
|
43
|
+
// Push edgeSourceCount() well past atomIsHub's crossover (N > ~4096 at
|
|
44
|
+
// the default maxGroup=4) with cheap, unrelated filler facts — this is
|
|
45
|
+
// the ONLY way to exercise the scale-gated branch at all; every existing
|
|
46
|
+
// chooseNext test (test/30) stays at small N, where the gate was already
|
|
47
|
+
// inert (consensusFloor(N) < 2 for N < 4.5), so none of them cover this.
|
|
48
|
+
const filler = [];
|
|
49
|
+
for (let i = 0; i < 4300; i++) filler.push([`filler-${i}`, `f${i}`]);
|
|
50
|
+
await m.ingest(filler);
|
|
51
|
+
|
|
52
|
+
// "trigger" has three continuations: "winner" is corroborated by TWO
|
|
53
|
+
// distinct contexts ("trigger" itself and "also trigger"); the other two
|
|
54
|
+
// by one each — an unambiguous, strictly-dominant winner.
|
|
55
|
+
await m.ingest([
|
|
56
|
+
["trigger", "winner"],
|
|
57
|
+
["trigger", "loserA"],
|
|
58
|
+
["trigger", "loserB"],
|
|
59
|
+
["also trigger", "winner"],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const triggerId = resolve(m, enc("trigger"));
|
|
63
|
+
const winnerId = resolve(m, enc("winner"));
|
|
64
|
+
assert.ok(triggerId !== null && winnerId !== null, "corpus must resolve");
|
|
65
|
+
|
|
66
|
+
const guide = gistOf(m, enc("trigger"));
|
|
67
|
+
const picked = chooseNext(m, triggerId, guide);
|
|
68
|
+
assert.equal(
|
|
69
|
+
picked,
|
|
70
|
+
winnerId,
|
|
71
|
+
`expected chooseNext to trust the strictly-dominant edge (prevCount 2 ` +
|
|
72
|
+
`vs 1/1/1) even at large corpus scale, got ${picked}`,
|
|
73
|
+
);
|
|
74
|
+
await m.store.close();
|
|
75
|
+
});
|