@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,244 @@
|
|
|
1
|
+
// 43-cast-analog-seat.test.mjs — CAST's analogical-comparison schema must
|
|
2
|
+
// never chase an analog's own FORWARD continuation, whether the analog is
|
|
3
|
+
// a nextOf descendant or a directly aligned point.
|
|
4
|
+
//
|
|
5
|
+
// Traced live (analyze_training.ts, dialogue D geography thread): "And what
|
|
6
|
+
// is the capital of Spain?" answered "The capital of France is Paris.And
|
|
7
|
+
// what is the capital of the Moon?" — the comparison schema correctly
|
|
8
|
+
// identified "What is the capital of France?" as dominant and correctly
|
|
9
|
+
// found a genuine analog, "What is the capital of Japan?\nTokyo is the
|
|
10
|
+
// capital of Japan." (validated via analogyStrength, 0.7667) — a DIRECTLY
|
|
11
|
+
// aligned point (found in alignStructures' own output), not a nextOf
|
|
12
|
+
// descendant. seatOfNode's forward branch fired on it: the analog node is
|
|
13
|
+
// already a complete, self-answering unit with prevCount 0 (no
|
|
14
|
+
// establishing predecessor either), and its SOLE forward edge is a wholly
|
|
15
|
+
// unrelated quiz question that happens to follow it in one training
|
|
16
|
+
// document ("And what is the capital of the Moon?") — landing one hop past
|
|
17
|
+
// the informative content that made it a genuine analog in the first
|
|
18
|
+
// place.
|
|
19
|
+
//
|
|
20
|
+
// Two related shapes, two fixes:
|
|
21
|
+
// 1. A nextOf DESCENDANT (AnalogCandidate.point === null, reached by
|
|
22
|
+
// following another aligned point's own continuation edge — never
|
|
23
|
+
// matched in the query) — its own bytes ARE its seat directly, no
|
|
24
|
+
// projection at all (the module's own doc comment on the alignment
|
|
25
|
+
// loop: "the hub's own [...] context will be the seat").
|
|
26
|
+
// 2. A DIRECTLY aligned point (point !== null, the live shape above) —
|
|
27
|
+
// still goes through seatOfNode for its reverse-establishing check
|
|
28
|
+
// (a bare entity NAME like "Leonardo da Vinci" needs it — test/29's
|
|
29
|
+
// C2/C3), but with `allowForward: false`: the analog is only being
|
|
30
|
+
// CITED for comparison, the query never asked about it, so chasing
|
|
31
|
+
// its own further continuation is never appropriate — only reverse
|
|
32
|
+
// context or its own bytes.
|
|
33
|
+
|
|
34
|
+
import { test } from "node:test";
|
|
35
|
+
import assert from "node:assert/strict";
|
|
36
|
+
import { Mind } from "../dist/src/index.js";
|
|
37
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
38
|
+
import { counterfactualTransfer } from "../dist/src/mind/mechanisms/cast.js";
|
|
39
|
+
import { gistOf, resolve } from "../dist/src/mind/primitives.js";
|
|
40
|
+
|
|
41
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
42
|
+
const dec = (b) => new TextDecoder().decode(b);
|
|
43
|
+
|
|
44
|
+
test("CAST comparison: a nextOf-descendant analog is seated by its own bytes, not re-projected", async () => {
|
|
45
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
46
|
+
await m.ingest([
|
|
47
|
+
["What is the capital of France?", "The capital of France is Paris."],
|
|
48
|
+
// "some other prompt" stands in for whatever real aligned point's own
|
|
49
|
+
// continuation edge reaches the self-contained analog below — in the
|
|
50
|
+
// live trace this was reached through the query's own weave, not a
|
|
51
|
+
// direct textual match, hence AnalogCandidate.point === null.
|
|
52
|
+
["some other prompt", "Japan capital is Tokyo."],
|
|
53
|
+
// The coincidental further edge: the analog's OWN forward continuation,
|
|
54
|
+
// unrelated to the comparison, mirroring "...capital of Japan?" being
|
|
55
|
+
// followed by "...capital of the Moon?" in one training document.
|
|
56
|
+
["Japan capital is Tokyo.", "Unrelated next quiz question."],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const query = enc(
|
|
60
|
+
"What is the capital of France? What is the capital of Spain?",
|
|
61
|
+
);
|
|
62
|
+
const franceId = resolve(m, enc("What is the capital of France?"));
|
|
63
|
+
const spainAnalogSrcId = resolve(m, enc("some other prompt"));
|
|
64
|
+
|
|
65
|
+
const dominant = {
|
|
66
|
+
anchor: franceId,
|
|
67
|
+
vote: 100,
|
|
68
|
+
ctx: enc("What is the capital of France?"),
|
|
69
|
+
runs: [{ qs: 0, qe: 30, cs: 0, weight: 1 }],
|
|
70
|
+
};
|
|
71
|
+
// The "Spain" point aligned in the query, whose OWN nextOf reaches the
|
|
72
|
+
// self-contained Japan analog (via a real learnt edge) — this is what
|
|
73
|
+
// makes the Japan node a nextOf descendant (point === null) rather than
|
|
74
|
+
// a directly aligned point.
|
|
75
|
+
const spainPoint = {
|
|
76
|
+
anchor: spainAnalogSrcId,
|
|
77
|
+
vote: 50,
|
|
78
|
+
ctx: enc("some other prompt"),
|
|
79
|
+
runs: [{ qs: 32, qe: 62, cs: 0, weight: 1 }],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const pre = {
|
|
83
|
+
attention: async () => ({
|
|
84
|
+
roots: [
|
|
85
|
+
{
|
|
86
|
+
anchor: franceId,
|
|
87
|
+
vote: 100,
|
|
88
|
+
start: 0,
|
|
89
|
+
end: 30,
|
|
90
|
+
breadth: 1,
|
|
91
|
+
clusters: 1,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
ranked: [
|
|
95
|
+
{
|
|
96
|
+
anchor: franceId,
|
|
97
|
+
vote: 100,
|
|
98
|
+
start: 0,
|
|
99
|
+
end: 30,
|
|
100
|
+
breadth: 1,
|
|
101
|
+
clusters: 1,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
anchor: spainAnalogSrcId,
|
|
105
|
+
vote: 50,
|
|
106
|
+
start: 32,
|
|
107
|
+
end: 62,
|
|
108
|
+
breadth: 0.5,
|
|
109
|
+
clusters: 1,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
weave: async () => ({
|
|
114
|
+
points: [dominant, spainPoint],
|
|
115
|
+
depth: new Float64Array(query.length),
|
|
116
|
+
}),
|
|
117
|
+
rec: { sites: [] },
|
|
118
|
+
guide: gistOf(m, query),
|
|
119
|
+
k: 24,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const results = await counterfactualTransfer(m, query, pre);
|
|
123
|
+
const comparison = results.find((r) =>
|
|
124
|
+
dec(r.bytes).includes("Japan capital is Tokyo.")
|
|
125
|
+
);
|
|
126
|
+
assert.ok(
|
|
127
|
+
comparison,
|
|
128
|
+
`expected the comparison schema's own candidate among the results, got ` +
|
|
129
|
+
results.map((r) => dec(r.bytes)),
|
|
130
|
+
);
|
|
131
|
+
assert.ok(
|
|
132
|
+
!dec(comparison.bytes).includes("Unrelated next quiz question"),
|
|
133
|
+
`the nextOf-descendant analog must not be re-projected past its own ` +
|
|
134
|
+
`bytes, got "${dec(comparison.bytes)}"`,
|
|
135
|
+
);
|
|
136
|
+
await m.store.close();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("CAST comparison: a DIRECTLY aligned analog is never re-projected past its own bytes", async () => {
|
|
140
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
141
|
+
await m.ingest([
|
|
142
|
+
["What is the capital of France?", "The capital of France is Paris."],
|
|
143
|
+
// The analog itself: an already-complete Q+A unit, mirroring the live
|
|
144
|
+
// "What is the capital of Japan?\nTokyo is the capital of Japan." node
|
|
145
|
+
// — matched DIRECTLY in the query this time (point !== null), not via
|
|
146
|
+
// nextOf.
|
|
147
|
+
[
|
|
148
|
+
"some other prompt",
|
|
149
|
+
"What is the capital of Japan? Tokyo is the capital of Japan.",
|
|
150
|
+
],
|
|
151
|
+
// The analog's own coincidental further edge — an unrelated quiz
|
|
152
|
+
// question, exactly the "capital of the Moon" shape.
|
|
153
|
+
[
|
|
154
|
+
"What is the capital of Japan? Tokyo is the capital of Japan.",
|
|
155
|
+
"Unrelated next quiz question.",
|
|
156
|
+
],
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// The query itself never mentions Japan at all — exactly the live shape:
|
|
160
|
+
// "And what is the capital of Spain?" never contains Japan's text either;
|
|
161
|
+
// the analog is found by STRUCTURAL (halo) similarity to the dominant,
|
|
162
|
+
// not by a literal substring match — hence bestAnalog.point !== null
|
|
163
|
+
// (a genuinely ALIGNED point, just not aligned by literal quotation).
|
|
164
|
+
const query = enc(
|
|
165
|
+
"What is the capital of France? What is the capital of Spain?",
|
|
166
|
+
);
|
|
167
|
+
const franceId = resolve(m, enc("What is the capital of France?"));
|
|
168
|
+
const japanId = resolve(
|
|
169
|
+
m,
|
|
170
|
+
enc("What is the capital of Japan? Tokyo is the capital of Japan."),
|
|
171
|
+
);
|
|
172
|
+
assert.ok(franceId !== null && japanId !== null, "corpus must resolve");
|
|
173
|
+
|
|
174
|
+
const dominant = {
|
|
175
|
+
anchor: franceId,
|
|
176
|
+
vote: 100,
|
|
177
|
+
ctx: enc("What is the capital of France?"),
|
|
178
|
+
runs: [{ qs: 0, qe: 30, cs: 0, weight: 1 }],
|
|
179
|
+
};
|
|
180
|
+
// Aligned to the "Spain" span of the query by halo similarity (not literal
|
|
181
|
+
// quotation) — a directly aligned point, so bestAnalog.point !== null.
|
|
182
|
+
const japanPoint = {
|
|
183
|
+
anchor: japanId,
|
|
184
|
+
vote: 50,
|
|
185
|
+
ctx: enc("What is the capital of Japan? Tokyo is the capital of Japan."),
|
|
186
|
+
runs: [{ qs: 32, qe: 62, cs: 0, weight: 1 }],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const pre = {
|
|
190
|
+
attention: async () => ({
|
|
191
|
+
roots: [
|
|
192
|
+
{
|
|
193
|
+
anchor: franceId,
|
|
194
|
+
vote: 100,
|
|
195
|
+
start: 0,
|
|
196
|
+
end: 30,
|
|
197
|
+
breadth: 1,
|
|
198
|
+
clusters: 1,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
ranked: [
|
|
202
|
+
{
|
|
203
|
+
anchor: franceId,
|
|
204
|
+
vote: 100,
|
|
205
|
+
start: 0,
|
|
206
|
+
end: 30,
|
|
207
|
+
breadth: 1,
|
|
208
|
+
clusters: 1,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
anchor: japanId,
|
|
212
|
+
vote: 50,
|
|
213
|
+
start: 32,
|
|
214
|
+
end: 62,
|
|
215
|
+
breadth: 0.5,
|
|
216
|
+
clusters: 1,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
}),
|
|
220
|
+
weave: async () => ({
|
|
221
|
+
points: [dominant, japanPoint],
|
|
222
|
+
depth: new Float64Array(query.length),
|
|
223
|
+
}),
|
|
224
|
+
rec: { sites: [] },
|
|
225
|
+
guide: gistOf(m, query),
|
|
226
|
+
k: 24,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const results = await counterfactualTransfer(m, query, pre);
|
|
230
|
+
const comparison = results.find((r) =>
|
|
231
|
+
dec(r.bytes).includes("Tokyo is the capital of Japan")
|
|
232
|
+
);
|
|
233
|
+
assert.ok(
|
|
234
|
+
comparison,
|
|
235
|
+
`expected the comparison schema's own candidate among the results, got ` +
|
|
236
|
+
results.map((r) => dec(r.bytes)),
|
|
237
|
+
);
|
|
238
|
+
assert.ok(
|
|
239
|
+
!dec(comparison.bytes).includes("Unrelated next quiz question"),
|
|
240
|
+
`a directly aligned analog must not be re-projected past its own ` +
|
|
241
|
+
`bytes, got "${dec(comparison.bytes)}"`,
|
|
242
|
+
);
|
|
243
|
+
await m.store.close();
|
|
244
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// 44-recognise-edge-whitespace.test.mjs — recognise() must still find a
|
|
2
|
+
// stored form when the query's own fold chunk swallows a leading/trailing
|
|
3
|
+
// separator byte the trained form never had.
|
|
4
|
+
//
|
|
5
|
+
// Traced live (analyze_training.ts, dialogue D geography thread): the
|
|
6
|
+
// accumulated query "...And what is the capital of Spain?" recognises
|
|
7
|
+
// nothing near "what is the capital of Spain?" even though the exact fact
|
|
8
|
+
// is trained and well-formed ("What is the capital of Spain?", resolves
|
|
9
|
+
// cleanly in isolation). Root-caused directly: the query's OWN fold draws
|
|
10
|
+
// a chunk boundary at " what is the capital of Spain?" (WITH the leading
|
|
11
|
+
// space folded in from the preceding "And "), and canon.ts's own
|
|
12
|
+
// documented contract says edge whitespace belongs between forms, never to
|
|
13
|
+
// one — so canonResolve correctly refuses that padded span. The one-byte
|
|
14
|
+
// trimmed span DOES canon-resolve to the trained fact (verified directly:
|
|
15
|
+
// canonResolve on bytes[65,94) returns the same id as resolving "What is
|
|
16
|
+
// the capital of Spain?" standalone) — but nothing tried the trimmed span.
|
|
17
|
+
// Fix: when a chunk's canon fallback misses, retry the two one-byte-
|
|
18
|
+
// shorter edge variants via resolve() (self-verifying, like every
|
|
19
|
+
// content-addressed lookup here) before giving up — mind/recognition.ts
|
|
20
|
+
// has no notion of "whitespace" at all, it just trusts the store's own
|
|
21
|
+
// hash-then-verify discipline. Two extra probes, only on the
|
|
22
|
+
// already-failed miss path.
|
|
23
|
+
|
|
24
|
+
import { test } from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { Mind, SQliteStore } from "../dist/src/index.js";
|
|
27
|
+
import { textCanon } from "../dist/src/canon.js";
|
|
28
|
+
import { recognise } from "../dist/src/mind/recognition.js";
|
|
29
|
+
import { resolve } from "../dist/src/mind/primitives.js";
|
|
30
|
+
|
|
31
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
32
|
+
|
|
33
|
+
test("recognise(): a chunk that swallows a leading separator still finds the trained form after trimming", async () => {
|
|
34
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
35
|
+
await m.ingest([
|
|
36
|
+
["What is the capital of France?", "The capital of France is Paris."],
|
|
37
|
+
["What is the capital of Spain?", "Madrid is the capital of Spain."],
|
|
38
|
+
]);
|
|
39
|
+
await m.buildCanonIndex(textCanon);
|
|
40
|
+
m.canon = m._canonFor(textCanon);
|
|
41
|
+
m.canonMemo = new Map();
|
|
42
|
+
|
|
43
|
+
const query = enc(
|
|
44
|
+
"What is the capital of France?The capital of France is Paris." +
|
|
45
|
+
"And what is the capital of Spain?",
|
|
46
|
+
);
|
|
47
|
+
const expected = resolve(m, enc("What is the capital of Spain?"));
|
|
48
|
+
assert.ok(
|
|
49
|
+
expected !== null,
|
|
50
|
+
"sanity: the trained fact must resolve standalone",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const rec = recognise(m, query);
|
|
54
|
+
const hit = rec.sites.find((s) => s.payload === expected);
|
|
55
|
+
assert.ok(
|
|
56
|
+
hit,
|
|
57
|
+
`expected a recognised site for the trained Spain fact, got sites: ` +
|
|
58
|
+
JSON.stringify(
|
|
59
|
+
rec.sites.map((s) => [s.start, s.end, s.payload]),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
await m.store.close();
|
|
63
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 45-liftanswer-restated-trim.test.mjs — cover's derivation must not be
|
|
2
|
+
// discarded WHOLESALE just because one of its segments restates content the
|
|
3
|
+
// query already contains; liftAnswer should TRIM the restating segment and
|
|
4
|
+
// keep the rest.
|
|
5
|
+
//
|
|
6
|
+
// Traced live (analyze_training.ts, dialogue D geography thread) and
|
|
7
|
+
// reproduced minimally here: in a multi-turn conversation, turn 1's own
|
|
8
|
+
// reply ("The capital of France is Paris.") becomes part of turn 2's
|
|
9
|
+
// accumulated query verbatim. cover's search correctly answers turn 1's
|
|
10
|
+
// embedded question by FOLLOWING its edge to that same reply text — which
|
|
11
|
+
// now duplicates content already sitting later in the query (the prior
|
|
12
|
+
// turn's own answer, echoed back). The OLD behaviour (cover.ts's
|
|
13
|
+
// `restatedSpan` check) detected this correctly but responded by discarding
|
|
14
|
+
// the ENTIRE candidate — throwing away the genuinely NEW answer (turn 2's
|
|
15
|
+
// own question) along with the stale restated one. Root cause: node
|
|
16
|
+
// "The capital of France is Paris." has no forward edge and no concept
|
|
17
|
+
// sibling (a plain declarative answer, not itself a question), so cover's
|
|
18
|
+
// search also has to fall back to a very expensive byte-by-byte cover for
|
|
19
|
+
// its own re-occurrence — but that is a SEPARATE, already-documented
|
|
20
|
+
// concern; this test's fix is specifically about not discarding good
|
|
21
|
+
// content alongside restated content once a derivation IS found.
|
|
22
|
+
//
|
|
23
|
+
// Fix: liftAnswer (types.ts) now takes the query bytes and the geometry's
|
|
24
|
+
// quantum W, and excludes any recognised segment whose SUBSTITUTED bytes
|
|
25
|
+
// (not the segment's own literal text) already occur elsewhere in the
|
|
26
|
+
// query — from both the framing (lo/hi) decision AND the final
|
|
27
|
+
// concatenation — instead of cover.ts rejecting the whole derivation.
|
|
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
|
+
|
|
34
|
+
const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
|
|
35
|
+
|
|
36
|
+
test("cover: a restated segment is trimmed from the answer, not discarded wholesale", async () => {
|
|
37
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
38
|
+
await m.ingest([
|
|
39
|
+
["What is the capital of France?", "The capital of France is Paris."],
|
|
40
|
+
["What is the capital of Spain?", "Madrid is the capital of Spain."],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const conv = m.beginConversation();
|
|
44
|
+
await m.respondTurn(conv, "What is the capital of France?");
|
|
45
|
+
const r2 = await m.respondTurn(conv, "What is the capital of Spain?");
|
|
46
|
+
const got = dec(r2.response.bytes);
|
|
47
|
+
|
|
48
|
+
assert.ok(
|
|
49
|
+
got.length > 0,
|
|
50
|
+
"expected a non-empty answer — the genuinely new Spain answer must not " +
|
|
51
|
+
"be discarded just because the France answer restates itself",
|
|
52
|
+
);
|
|
53
|
+
assert.ok(
|
|
54
|
+
got.includes("Madrid") || got.includes("Spain"),
|
|
55
|
+
`expected the NEW answer (Spain) to survive, got ${JSON.stringify(got)}`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
m.endConversation(conv);
|
|
59
|
+
await m.store.close();
|
|
60
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// 46-recognise-multibyte-edge.test.mjs — recognise()'s canon-miss fallback
|
|
2
|
+
// must recover a trained form whose edge misalignment is MORE than one byte
|
|
3
|
+
// (the shipped ±1 fix only covers a single stray edge byte).
|
|
4
|
+
//
|
|
5
|
+
// bytesToTree's plain fold is RADIX-ALIGNED: a chunk's own boundary is a
|
|
6
|
+
// multiple of the geometry's group width W from the LOCAL start of whatever
|
|
7
|
+
// it was folded from (see geometry.ts's FoldPyramid comment). A query whose
|
|
8
|
+
// recognised span sits at a different local offset than the trained deposit
|
|
9
|
+
// did — e.g. extra leading whitespace, which canon deliberately preserves
|
|
10
|
+
// verbatim at the edges, only collapsing INTERIOR whitespace — shifts every
|
|
11
|
+
// chunk boundary inside it by that many bytes, which can exceed 1.
|
|
12
|
+
//
|
|
13
|
+
// Fix: widen the miss-fallback with bounded W-quantum edge trims, each
|
|
14
|
+
// gated by the SAME cheap store.findBranch(leafIds) pre-filter the
|
|
15
|
+
// canonical pass (tryChain) already uses — so the (rare) miss path pays for
|
|
16
|
+
// a real resolve() fold only when a branch could plausibly exist there —
|
|
17
|
+
// and capped to nodes no larger than chainReach(W) = W² (a chunk-scale
|
|
18
|
+
// bound, not a whole-query one): widening at ROOT scale can rediscover
|
|
19
|
+
// content the structural walk's own finer recursion already owns as a
|
|
20
|
+
// SEPARATE, duplicate site, and downstream derivation can then stitch a
|
|
21
|
+
// wrong answer out of the two overlapping sites (see the second test here).
|
|
22
|
+
|
|
23
|
+
import { test } from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import { Mind, SQliteStore } from "../dist/src/index.js";
|
|
26
|
+
import { recognise } from "../dist/src/mind/recognition.js";
|
|
27
|
+
import { resolve } from "../dist/src/mind/primitives.js";
|
|
28
|
+
|
|
29
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
30
|
+
const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
|
|
31
|
+
|
|
32
|
+
test("recognise(): a >1-byte edge misalignment still finds the trained form", async () => {
|
|
33
|
+
// A whole-query-scale offset (the entire query is nothing but the trained
|
|
34
|
+
// fact plus a swallowed prefix) is a DIFFERENT, still-open problem — see
|
|
35
|
+
// the "does not corrupt" test below for why widening this fix to root
|
|
36
|
+
// scale is exactly what must NOT happen. What this fix targets is a
|
|
37
|
+
// canon-miss node bounded to chunk scale (<= W², chainReach's own bound)
|
|
38
|
+
// whose trim offset is more than the shipped ±1 fallback reaches.
|
|
39
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
40
|
+
await m.ingest([["cats meow", "yes they do"]]);
|
|
41
|
+
|
|
42
|
+
const expected = resolve(m, enc("cats meow"));
|
|
43
|
+
assert.ok(
|
|
44
|
+
expected !== null,
|
|
45
|
+
"sanity: the trained fact must resolve standalone",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Two EXTRA leading spaces relative to the trained form — canon only
|
|
49
|
+
// collapses interior whitespace, so this is a 2-byte edge offset, wider
|
|
50
|
+
// than the shipped ±1 fallback covers.
|
|
51
|
+
const query = enc(" cats meow");
|
|
52
|
+
const rec = recognise(m, query);
|
|
53
|
+
const hit = rec.sites.find((s) => s.payload === expected);
|
|
54
|
+
assert.ok(
|
|
55
|
+
hit,
|
|
56
|
+
`expected a recognised site for the trained fact despite the 2-byte ` +
|
|
57
|
+
`edge offset, got sites: ` +
|
|
58
|
+
JSON.stringify(rec.sites.map((s) => [s.start, s.end, s.payload])),
|
|
59
|
+
);
|
|
60
|
+
await m.store.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("recognise(): a wide edge trim does not corrupt an unrelated short-form answer", async () => {
|
|
64
|
+
// Regression guard for the root-scale false positive: widening the
|
|
65
|
+
// fallback's reach must not let it re-derive a smaller subtree's own
|
|
66
|
+
// content as a SEPARATE site at the whole-query node, which previously
|
|
67
|
+
// let cover's derivation stitch two overlapping sites into a wrong
|
|
68
|
+
// answer ("4+15" instead of "4").
|
|
69
|
+
const TABLE = [
|
|
70
|
+
["1+2", "3"],
|
|
71
|
+
["2+2", "4"],
|
|
72
|
+
["2+3", "5"],
|
|
73
|
+
["3+3", "6"],
|
|
74
|
+
["3+5", "8"],
|
|
75
|
+
["4+3", "7"],
|
|
76
|
+
["2+5", "7"],
|
|
77
|
+
["1+5", "6"],
|
|
78
|
+
["6+1", "7"],
|
|
79
|
+
["4+1", "5"],
|
|
80
|
+
];
|
|
81
|
+
const m = new Mind({ seed: 42 });
|
|
82
|
+
await m.ingest(TABLE);
|
|
83
|
+
const r = await m.respond("2+2 は何ですか");
|
|
84
|
+
assert.equal(dec(r.bytes), "4");
|
|
85
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// 47-cast-comparison-coverage.test.mjs — CAST's analogical-comparison schema
|
|
2
|
+
// must not fire when its own accounted evidence leaves a substantial,
|
|
3
|
+
// contiguous stretch of the query untouched — the same half-dominance bar
|
|
4
|
+
// CAST's own frame gate and liftAnswer already apply, PLUS a topic-scale
|
|
5
|
+
// bar (see cast.ts's comparison-gate comment for why one alone isn't enough).
|
|
6
|
+
//
|
|
7
|
+
// Traced live (analyze_training.ts, dialogue D geography thread): "And what
|
|
8
|
+
// is the capital of Spain?" answered "The capital of France is Paris.What is
|
|
9
|
+
// the capital of Japan?\nTokyo is the capital of Japan." — a wrong analog.
|
|
10
|
+
// Root-caused directly against the live store: recognise() never finds a
|
|
11
|
+
// site for "capital of Spain" at all (a genuine extra word, "And ", not
|
|
12
|
+
// boundary noise — see the session's own investigation), so the consensus
|
|
13
|
+
// climb commits only ONE root (France) and CAST's comparison schema treats
|
|
14
|
+
// the query as "about one thing."
|
|
15
|
+
//
|
|
16
|
+
// The live numbers are the important part of this test: comparison's own
|
|
17
|
+
// accounted spans (dominant [0,30) + analog [77,88)) leave a 47-byte gap in
|
|
18
|
+
// the 94-byte query — exactly HALF, so the query-relative bar alone
|
|
19
|
+
// (dominates: strictly more than half) does NOT decisively refuse it; only
|
|
20
|
+
// the topic-relative bar (47 ≥ dominant's own 30-byte context) does. This
|
|
21
|
+
// test pins the EXACT borderline shape, not an easier, more comfortably
|
|
22
|
+
// over-half one — a fix that only handled a clear majority-gap would still
|
|
23
|
+
// leave the live bug live.
|
|
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
|
+
import { counterfactualTransfer } from "../dist/src/mind/mechanisms/cast.js";
|
|
30
|
+
import { gistOf, resolve } from "../dist/src/mind/primitives.js";
|
|
31
|
+
|
|
32
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
33
|
+
const dec = (b) => new TextDecoder().decode(b);
|
|
34
|
+
|
|
35
|
+
test("CAST comparison: refuses to fire when its own accounted evidence leaves an exactly-half, topic-scale gap", async () => {
|
|
36
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
37
|
+
await m.ingest([
|
|
38
|
+
["What is the capital of France?", "The capital of France is Paris."],
|
|
39
|
+
[
|
|
40
|
+
"some other prompt",
|
|
41
|
+
"What is the capital of Japan? Tokyo is the capital of Japan.",
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"What is the capital of Japan? Tokyo is the capital of Japan.",
|
|
45
|
+
"Unrelated next quiz question.",
|
|
46
|
+
],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// The real accumulated multi-turn shape: dominant covers only the FIRST
|
|
50
|
+
// 30 bytes (the France sentence); the rest of the query — including the
|
|
51
|
+
// whole "And what is the capital of Spain?" tail — is never aligned to
|
|
52
|
+
// anything beyond a bare 5-byte "Spain" token, mirroring the live case
|
|
53
|
+
// where recognise() found nothing bigger for that span.
|
|
54
|
+
const query = enc(
|
|
55
|
+
"What is the capital of France?The capital of France is Paris." +
|
|
56
|
+
"And what is the capital of Spain?",
|
|
57
|
+
);
|
|
58
|
+
const franceId = resolve(m, enc("What is the capital of France?"));
|
|
59
|
+
const japanId = resolve(
|
|
60
|
+
m,
|
|
61
|
+
enc("What is the capital of Japan? Tokyo is the capital of Japan."),
|
|
62
|
+
);
|
|
63
|
+
assert.ok(franceId !== null && japanId !== null, "corpus must resolve");
|
|
64
|
+
|
|
65
|
+
const dominant = {
|
|
66
|
+
anchor: franceId,
|
|
67
|
+
vote: 100,
|
|
68
|
+
ctx: enc("What is the capital of France?"),
|
|
69
|
+
runs: [{ qs: 0, qe: 30, cs: 0, weight: 1 }],
|
|
70
|
+
};
|
|
71
|
+
// Directly aligned, but its OWN evidence is just "capital of" (bytes
|
|
72
|
+
// 77..88 of the 94-byte query) — the exact live span — not the whole
|
|
73
|
+
// clause and not even the word "Spain" itself.
|
|
74
|
+
const japanPoint = {
|
|
75
|
+
anchor: japanId,
|
|
76
|
+
vote: 50,
|
|
77
|
+
ctx: enc("What is the capital of Japan? Tokyo is the capital of Japan."),
|
|
78
|
+
runs: [{ qs: 77, qe: 88, cs: 0, weight: 1 }],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const pre = {
|
|
82
|
+
attention: async () => ({
|
|
83
|
+
roots: [
|
|
84
|
+
{
|
|
85
|
+
anchor: franceId,
|
|
86
|
+
vote: 100,
|
|
87
|
+
start: 0,
|
|
88
|
+
end: 30,
|
|
89
|
+
breadth: 1,
|
|
90
|
+
clusters: 1,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
ranked: [
|
|
94
|
+
{
|
|
95
|
+
anchor: franceId,
|
|
96
|
+
vote: 100,
|
|
97
|
+
start: 0,
|
|
98
|
+
end: 30,
|
|
99
|
+
breadth: 1,
|
|
100
|
+
clusters: 1,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
anchor: japanId,
|
|
104
|
+
vote: 50,
|
|
105
|
+
start: 77,
|
|
106
|
+
end: 88,
|
|
107
|
+
breadth: 0.5,
|
|
108
|
+
clusters: 1,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
weave: async () => ({
|
|
113
|
+
points: [dominant, japanPoint],
|
|
114
|
+
depth: new Float64Array(query.length),
|
|
115
|
+
}),
|
|
116
|
+
rec: { sites: [] },
|
|
117
|
+
guide: gistOf(m, query),
|
|
118
|
+
k: 24,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const results = await counterfactualTransfer(m, query, pre);
|
|
122
|
+
const comparison = results.find((r) =>
|
|
123
|
+
dec(r.bytes).includes("Tokyo is the capital of Japan")
|
|
124
|
+
);
|
|
125
|
+
assert.ok(
|
|
126
|
+
!comparison,
|
|
127
|
+
`comparison should refuse when the largest gap (47 bytes) is exactly ` +
|
|
128
|
+
`half the 94-byte query and at least as large as the 30-byte ` +
|
|
129
|
+
`dominant, but fired: ${
|
|
130
|
+
comparison ? JSON.stringify(dec(comparison.bytes)) : ""
|
|
131
|
+
}`,
|
|
132
|
+
);
|
|
133
|
+
await m.store.close();
|
|
134
|
+
});
|