@hviana/sema 0.2.3 → 0.2.5
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/geometry.d.ts +26 -0
- package/dist/src/geometry.js +32 -3
- package/dist/src/mind/attention.d.ts +246 -7
- package/dist/src/mind/attention.js +771 -173
- package/dist/src/mind/bridge.d.ts +30 -0
- package/dist/src/mind/bridge.js +569 -0
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +30 -1
- package/dist/src/mind/junction.js +73 -18
- package/dist/src/mind/match.d.ts +15 -2
- package/dist/src/mind/match.js +3 -8
- package/dist/src/mind/mechanisms/cast.d.ts +54 -0
- package/dist/src/mind/mechanisms/cast.js +268 -37
- package/dist/src/mind/mechanisms/cover.js +16 -31
- package/dist/src/mind/mechanisms/recall.js +66 -0
- package/dist/src/mind/mind.d.ts +2 -0
- package/dist/src/mind/rationale.d.ts +7 -2
- package/dist/src/mind/rationale.js +6 -5
- package/dist/src/mind/reasoning.d.ts +11 -0
- package/dist/src/mind/reasoning.js +58 -2
- package/dist/src/mind/recognition.js +127 -7
- package/dist/src/mind/traverse.js +90 -25
- package/dist/src/mind/types.d.ts +57 -2
- package/dist/src/mind/types.js +38 -7
- package/dist/src/store.d.ts +12 -3
- package/dist/src/store.js +9 -3
- package/package.json +1 -1
- package/src/geometry.ts +52 -3
- package/src/mind/attention.ts +1199 -128
- package/src/mind/bridge.ts +596 -0
- package/src/mind/index.ts +16 -0
- package/src/mind/junction.ts +125 -16
- package/src/mind/match.ts +19 -5
- package/src/mind/mechanisms/cast.ts +290 -38
- package/src/mind/mechanisms/cover.ts +23 -36
- package/src/mind/mechanisms/recall.ts +79 -0
- package/src/mind/mind.ts +15 -0
- package/src/mind/rationale.ts +12 -4
- package/src/mind/reasoning.ts +71 -2
- package/src/mind/recognition.ts +132 -7
- package/src/mind/traverse.ts +91 -24
- package/src/mind/types.ts +95 -6
- package/src/store.ts +19 -5
- 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
- package/test/51-structural-resonance-ladder.test.mjs +552 -0
- package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// 41-seatofnode-direction.test.mjs — CAST's seatOfNode (comparison schema)
|
|
2
|
+
// must seat a node by its own FORWARD identity when it has one, not by an
|
|
3
|
+
// incidental predecessor.
|
|
4
|
+
//
|
|
5
|
+
// Traced live (analyze_training.ts, dialogue D geography thread): "And what
|
|
6
|
+
// is the capital of Spain?" answered with "Create an example of a types of
|
|
7
|
+
// questions a GPT model can answer.?And what is the capital of the Moon?"
|
|
8
|
+
// — CAST's comparison schema correctly identified "What is the capital of
|
|
9
|
+
// France?" as the dominant structure and a genuine analog ("capital of
|
|
10
|
+
// Japan"), but seated the dominant by REVERSE context instead of forward:
|
|
11
|
+
// seatOfNode's old test (`prevCount(id) === 0`) skipped the forward branch
|
|
12
|
+
// because that exact question happens to have ONE coincidental predecessor
|
|
13
|
+
// elsewhere in the corpus (a generic "generate example questions"
|
|
14
|
+
// meta-prompt) — even though its own forward edge unambiguously resolves
|
|
15
|
+
// to "The capital of France is Paris."
|
|
16
|
+
//
|
|
17
|
+
// A broad sample of this store's own question-shaped nodes showed this
|
|
18
|
+
// isn't rare: ~71% have at least one predecessor, the large majority from
|
|
19
|
+
// a handful of generic, high-fan-out sentences that recur as incidental
|
|
20
|
+
// neighbours to dozens of unrelated destinations — a SmolSent-style
|
|
21
|
+
// adjacency artifact, not a real identity-establishing lead-in. There is
|
|
22
|
+
// no reliable way to tell a meaningful predecessor from an incidental one
|
|
23
|
+
// by count alone (the same category error already found and removed from
|
|
24
|
+
// chooseNext), so the fix restores the ONE priority every other consumer
|
|
25
|
+
// of follow()/reverseContext() already uses (project(), pivotInto, cast's
|
|
26
|
+
// own substitution schema): forward first, reverse only as a fallback when
|
|
27
|
+
// forward doesn't resolve.
|
|
28
|
+
|
|
29
|
+
import { test } from "node:test";
|
|
30
|
+
import assert from "node:assert/strict";
|
|
31
|
+
import { Mind } from "../dist/src/index.js";
|
|
32
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
33
|
+
import { seatOfNode } from "../dist/src/mind/mechanisms/cast.js";
|
|
34
|
+
import { gistOf, resolve } from "../dist/src/mind/primitives.js";
|
|
35
|
+
|
|
36
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
37
|
+
const dec = (b) => new TextDecoder().decode(b);
|
|
38
|
+
|
|
39
|
+
const mk = (seed) =>
|
|
40
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
41
|
+
|
|
42
|
+
test("seatOfNode: a node with a strong forward identity is seated forward despite a coincidental predecessor", async () => {
|
|
43
|
+
const m = mk(7);
|
|
44
|
+
// "the question" has both a genuine forward answer AND one coincidental
|
|
45
|
+
// predecessor — exactly the live shape (prevCount > 0, hasNext true).
|
|
46
|
+
await m.ingest([
|
|
47
|
+
["the question", "the real answer"],
|
|
48
|
+
["a generic template prompt", "the question"],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const id = resolve(m, enc("the question"));
|
|
52
|
+
assert.ok(id !== null, "corpus must resolve");
|
|
53
|
+
assert.ok(m.store.prevCount(id) > 0, "must have a coincidental predecessor");
|
|
54
|
+
assert.ok(m.store.hasNext(id), "must have a forward continuation");
|
|
55
|
+
|
|
56
|
+
const guide = gistOf(m, enc("the question"));
|
|
57
|
+
const seat = await seatOfNode(m, id, guide, enc("fallback"));
|
|
58
|
+
assert.equal(
|
|
59
|
+
dec(seat),
|
|
60
|
+
"the real answer",
|
|
61
|
+
`must seat by forward identity, not the coincidental predecessor, got "${
|
|
62
|
+
dec(seat)
|
|
63
|
+
}"`,
|
|
64
|
+
);
|
|
65
|
+
await m.store.close();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("seatOfNode: a node with NO forward identity still falls back to its reverse context", async () => {
|
|
69
|
+
const m = mk(7);
|
|
70
|
+
await m.ingest([["known as", "the entity"]]);
|
|
71
|
+
|
|
72
|
+
const id = resolve(m, enc("the entity"));
|
|
73
|
+
assert.ok(id !== null, "corpus must resolve");
|
|
74
|
+
assert.equal(m.store.hasNext(id), false, "must have no forward continuation");
|
|
75
|
+
|
|
76
|
+
const guide = gistOf(m, enc("the entity"));
|
|
77
|
+
const seat = await seatOfNode(m, id, guide, enc("fallback"));
|
|
78
|
+
assert.equal(
|
|
79
|
+
dec(seat),
|
|
80
|
+
"known as",
|
|
81
|
+
`an entity with no forward edge must still fall back to its reverse ` +
|
|
82
|
+
`context, got "${dec(seat)}"`,
|
|
83
|
+
);
|
|
84
|
+
await m.store.close();
|
|
85
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// 42-recognise-trace-idempotence.test.mjs — recognise() (and, by the same
|
|
2
|
+
// defect, climbAttentionAll()) must return the SAME result whether or not
|
|
3
|
+
// inspectRationale is attached — tracing must never change what the
|
|
4
|
+
// system decides, only whether it explains itself.
|
|
5
|
+
//
|
|
6
|
+
// Traced live (analyze_training.ts): the same 4-turn dialogue produced a
|
|
7
|
+
// DIFFERENT final answer depending only on whether inspectRationale was
|
|
8
|
+
// attached to the last turn — a deterministic, reproducible divergence,
|
|
9
|
+
// not cross-process randomness (verified: 5 untraced runs, byte-identical;
|
|
10
|
+
// traced vs untraced, consistently different).
|
|
11
|
+
//
|
|
12
|
+
// Root cause, isolated directly: recogniseImpl walks the query's perceived
|
|
13
|
+
// tree via foldTree(ctx, tree, 0, visit) — and foldTree's subtree-resolution
|
|
14
|
+
// fast path (primitives.ts) returns immediately for any subtree already
|
|
15
|
+
// cached in ctx._resolvedSubtrees, WITHOUT recursing into its children, so
|
|
16
|
+
// `visit` never fires for anything below that point. A multi-turn
|
|
17
|
+
// conversation's stable-prefix fold deliberately shares node OBJECTS
|
|
18
|
+
// across turns and within a turn's own walk, so by a SECOND call on the
|
|
19
|
+
// exact same bytes, large parts of the tree are already cached and
|
|
20
|
+
// recogniseImpl silently finds FEWER sites than the first call — it is not
|
|
21
|
+
// safe to call twice on the same input once `_resolvedSubtrees` is warm.
|
|
22
|
+
//
|
|
23
|
+
// Under ordinary (untraced) operation this never surfaces: recogniseMemo
|
|
24
|
+
// (keyed by exact byte content) ensures recogniseImpl only ever runs ONCE
|
|
25
|
+
// per distinct query string per conversation. But the memo was previously
|
|
26
|
+
// SKIPPED whenever `ctx.trace` was truthy ("so every call still emits its
|
|
27
|
+
// rationale step") — meaning a traced turn re-ran recogniseImpl from
|
|
28
|
+
// scratch at every one of the many call sites that recognise the same
|
|
29
|
+
// query within one response (cover, reason, articulate...), each
|
|
30
|
+
// subsequent call more incomplete than the last, silently changing which
|
|
31
|
+
// mechanism grounds the answer. Fix: consult (and populate) the memo
|
|
32
|
+
// unconditionally — matching perceive()'s own memo, which never had a
|
|
33
|
+
// trace gate — and emit the trace step from the cache-hit path directly,
|
|
34
|
+
// so tracing stays fully observable without ever bypassing correctness.
|
|
35
|
+
// climbAttentionAll() had the identical `!ctx.trace` gate over the same
|
|
36
|
+
// class of foldTree-based computation (collectRegions) and got the same
|
|
37
|
+
// fix, on the same reasoning, in the same change.
|
|
38
|
+
|
|
39
|
+
import { test } from "node:test";
|
|
40
|
+
import assert from "node:assert/strict";
|
|
41
|
+
import { Mind } from "../dist/src/index.js";
|
|
42
|
+
import { recognise } from "../dist/src/mind/recognition.js";
|
|
43
|
+
|
|
44
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
45
|
+
|
|
46
|
+
test("recognise(): a traced call returns the identical cached result, not a degraded recompute", async () => {
|
|
47
|
+
const m = new Mind({ seed: 7 });
|
|
48
|
+
// A long, deeply-chunked query (enough leaf-parents for foldTree's
|
|
49
|
+
// subtree cache to matter) that also recurs as a learnt SOURCE, so its
|
|
50
|
+
// own subtree — and many of its word-level sub-leaves — resolve and get
|
|
51
|
+
// cached into ctx._resolvedSubtrees on the first walk.
|
|
52
|
+
const words = [
|
|
53
|
+
"hello",
|
|
54
|
+
"world",
|
|
55
|
+
"foo",
|
|
56
|
+
"bar",
|
|
57
|
+
"baz",
|
|
58
|
+
"qux",
|
|
59
|
+
"quux",
|
|
60
|
+
"corge",
|
|
61
|
+
"grault",
|
|
62
|
+
"garply",
|
|
63
|
+
"waldo",
|
|
64
|
+
"fred",
|
|
65
|
+
"plugh",
|
|
66
|
+
"xyzzy",
|
|
67
|
+
"thud",
|
|
68
|
+
];
|
|
69
|
+
const long = words.join(" ") + " " + words.slice().reverse().join(" ");
|
|
70
|
+
await m.ingest([
|
|
71
|
+
[long, "some reply text here"],
|
|
72
|
+
...words.map((w, i) => [w, `word ${i}`]),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// _resolvedSubtrees is only ever populated during respondTurn's
|
|
76
|
+
// conversation machinery (a plain respond() leaves it null, immune to
|
|
77
|
+
// this defect) — reproduce that shape directly for a controlled,
|
|
78
|
+
// deterministic unit test.
|
|
79
|
+
m.perceiveMemo = new Map();
|
|
80
|
+
m.recogniseMemo = new Map();
|
|
81
|
+
m._resolvedSubtrees = new WeakMap();
|
|
82
|
+
m.trace = null;
|
|
83
|
+
|
|
84
|
+
const bytes = enc(long);
|
|
85
|
+
const untraced = recognise(m, bytes);
|
|
86
|
+
assert.ok(
|
|
87
|
+
untraced.sites.length > 0,
|
|
88
|
+
"sanity: the untraced call must find sites",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
m.trace = { enter: () => undefined, step: () => undefined };
|
|
92
|
+
const traced = recognise(m, bytes);
|
|
93
|
+
|
|
94
|
+
assert.equal(
|
|
95
|
+
traced,
|
|
96
|
+
untraced,
|
|
97
|
+
"a traced call must return the SAME cached Recognition object, not " +
|
|
98
|
+
"recompute (and potentially degrade) it",
|
|
99
|
+
);
|
|
100
|
+
assert.equal(
|
|
101
|
+
traced.sites.length,
|
|
102
|
+
untraced.sites.length,
|
|
103
|
+
`traced and untraced site counts must match, got ${traced.sites.length} ` +
|
|
104
|
+
`vs ${untraced.sites.length}`,
|
|
105
|
+
);
|
|
106
|
+
});
|