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