@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,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
|
+
});
|
|
@@ -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
|
+
});
|