@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,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
+ });
@@ -0,0 +1,125 @@
1
+ // 48-recognise-turn-connective.test.mjs — recognise() must find a trained
2
+ // form even when the asker prepends a real discourse connective ("And ")
3
+ // to a follow-up turn — not boundary noise, a genuine extra word.
4
+ //
5
+ // Traced live (analyze_training.ts, dialogue D geography thread):
6
+ // "What is the capital of France?" then "And what is the capital of
7
+ // Spain?" answered with an unrelated CAST analog (Japan/Moon) instead of
8
+ // "Madrid is the capital of Spain." — root-caused directly against the
9
+ // live store: the turn "And what is the capital of Spain?" canon-misses as
10
+ // ONE 33-byte node (turn/segment scale); canonResolve on the WHOLE turn
11
+ // fails because "and " is real content, not an equivalence the injected
12
+ // canonicalizer folds away. The shipped ±1/k-trim fallbacks are bounded to
13
+ // chunk-scale (<= W²) specifically to avoid rediscovering a smaller
14
+ // subtree's own content as a duplicate, overlapping site at root scale
15
+ // (see test/46's regression case) — but that bound also blocked recovering
16
+ // this turn, which is naturally larger than one chunk.
17
+ //
18
+ // Fix: a canon-miss composite ALSO tries canonResolve from every position
19
+ // UP TO W chunk-widths from its own left edge that the query's OWN
20
+ // structural fold already treats as a chunk boundary (`starts` — the same
21
+ // set the canonical pass privileges with full chain reach). This is NOT a
22
+ // blind offset guess (the size-bounded k-trim loop above it already covers
23
+ // that): `starts.has(p)` is fold EVIDENCE the query itself produced —
24
+ // "And " is a genuine W=4-byte leaf-parent chunk, so its own end (byte 65)
25
+ // is already a `starts` boundary before this code ever runs (foldTree
26
+ // visits children before parents). Unbounded in the NODE's size (turn/
27
+ // segment scale is fine), bounded in candidate COUNT (at most W probes,
28
+ // each an O(1) starts.has() before paying for the real canonResolve fold).
29
+
30
+ import { test } from "node:test";
31
+ import assert from "node:assert/strict";
32
+ import { Mind, SQliteStore } from "../dist/src/index.js";
33
+ import { textCanon } from "../dist/src/canon.js";
34
+ import { recognise } from "../dist/src/mind/recognition.js";
35
+ import { latin1Key, perceive, resolve } from "../dist/src/mind/primitives.js";
36
+
37
+ const enc = (s) => new TextEncoder().encode(s);
38
+ const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
39
+
40
+ test("recognise(): a real leading connective word ('And ') still finds the trained follow-up form", async () => {
41
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
42
+ await m.ingest([
43
+ ["What is the capital of France?", "The capital of France is Paris."],
44
+ ["What is the capital of Spain?", "Madrid is the capital of Spain."],
45
+ ]);
46
+ await m.buildCanonIndex(textCanon);
47
+ m.canon = m._canonFor(textCanon);
48
+ m.canonMemo = new Map();
49
+
50
+ const query = enc(
51
+ "What is the capital of France?The capital of France is Paris." +
52
+ "And what is the capital of Spain?",
53
+ );
54
+ const expected = resolve(m, enc("What is the capital of Spain?"));
55
+ assert.ok(
56
+ expected !== null,
57
+ "sanity: the trained fact must resolve standalone",
58
+ );
59
+
60
+ // The live shape is STABLE-PREFIX folded (each turn folds independently
61
+ // from its own local offset 0 — see mind.ts's _growContext) so "And "
62
+ // lands on a genuine chunk boundary; a plain fold of the whole query (no
63
+ // boundaries) radix-aligns from byte 0 instead, which happens to reduce
64
+ // this particular query to the already-shipped ±1 case and would not
65
+ // exercise this fix at all. Priming perceiveMemo with the boundary-aware
66
+ // tree (content-keyed, exactly what _growContext itself does) makes this
67
+ // recognise() call see what the real multi-turn pipeline sees, without
68
+ // going through respondTurn's own memo (a second recognise() call on the
69
+ // same bytes is deliberately non-idempotent — see recognise()'s own
70
+ // doc comment — so this must be the FIRST call on this content).
71
+ const boundaries = [30, 61];
72
+ m.perceiveMemo = new Map();
73
+ m.perceiveMemo.set(
74
+ latin1Key(query),
75
+ perceive(m, query, undefined, undefined, boundaries),
76
+ );
77
+
78
+ const rec = recognise(m, query);
79
+ const hit = rec.sites.find((s) => s.payload === expected);
80
+ assert.ok(
81
+ hit,
82
+ `expected a recognised site for the trained Spain fact despite the ` +
83
+ `leading "And " connective, got sites: ` +
84
+ JSON.stringify(rec.sites.map((s) => [s.start, s.end, s.payload])),
85
+ );
86
+ await m.store.close();
87
+ });
88
+
89
+ test("multi-turn: a follow-up turn prefixed with a real connective word grounds the NEW fact, not a CAST analog", async () => {
90
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
91
+ await m.ingest([
92
+ ["What is the capital of France?", "The capital of France is Paris."],
93
+ ["What is the capital of Spain?", "Madrid is the capital of Spain."],
94
+ [
95
+ "some other prompt",
96
+ "What is the capital of Japan? Tokyo is the capital of Japan.",
97
+ ],
98
+ ]);
99
+ // The live store builds this at training time; respondTurn's own
100
+ // automatic canon wiring (_canonFor) only wraps the canonicalizer
101
+ // function — the INDEX canonResolve searches is a separate, opt-in
102
+ // build step, same as test 44/46.
103
+ await m.buildCanonIndex(textCanon);
104
+
105
+ const conv = m.beginConversation();
106
+ await m.respondTurn(conv, "What is the capital of France?");
107
+ const r2 = await m.respondTurn(conv, "And what is the capital of Spain?");
108
+ const got = dec(r2.response.bytes);
109
+
110
+ assert.ok(
111
+ got.includes("Madrid") && got.includes("Spain"),
112
+ `expected the NEW Spain fact to ground the answer, got ${
113
+ JSON.stringify(got)
114
+ } (provenance: ${r2.response.provenance})`,
115
+ );
116
+ assert.ok(
117
+ !got.includes("Japan") && !got.includes("Tokyo"),
118
+ `must not fall back to an unrelated CAST analog, got ${
119
+ JSON.stringify(got)
120
+ }`,
121
+ );
122
+
123
+ m.endConversation(conv);
124
+ await m.store.close();
125
+ });
@@ -0,0 +1,175 @@
1
+ // 49-natural-units-synonym-bridge.test.mjs — irrefutable demonstration of
2
+ // the architectural gap investigated this session: a query using a
3
+ // near-synonym ("biggest") of a word that was only ever trained in a
4
+ // DIFFERENT phrasing ("largest") finds nothing, even though the fact
5
+ // itself is trained and the synonym relationship is corroborated many
6
+ // times over in the corpus.
7
+ //
8
+ // Root-caused directly (prior turn): "biggest"/"largest" NEVER resolve as
9
+ // independent nodes at all — deposit() only interns whole sentences (as
10
+ // one flat branch) plus tiny W-1/W leaf-id windows (canonical.ts); a
11
+ // 7-byte word floating mid-sentence falls in the gap between those two
12
+ // scales and is never independently addressable, so the EXISTING
13
+ // halo/synonym machinery (conceptHop, already used by CAST) never gets an
14
+ // entry point to it — the halo system is real and works, it is simply
15
+ // never asked about a node that was never minted.
16
+ //
17
+ // Fix (units.ts): a derived, corpus-statistics-only notion of a "natural
18
+ // unit" — a run of adjacent chunks whose pairing recurs at least as often
19
+ // as either chunk alone (the same principle behind BPE/content-defined
20
+ // chunking, computed from this store's own reuse/containment counts, no
21
+ // injected modality-specific segmenter). Interned at deposit time,
22
+ // pairwise halo-poured as each other's company; read at query time via the
23
+ // same derived merge (existingUnits) plus a halo-corroborated substitution
24
+ // fallback in recognise().
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 { resolve } from "../dist/src/mind/primitives.js";
31
+
32
+ const enc = (s) => new TextEncoder().encode(s);
33
+ const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
34
+
35
+ // Parallel "X is the biggest/largest Y" constructions across several
36
+ // different nouns — real cross-sentence corroboration for the
37
+ // biggest~largest collocation pattern, not a single coincidental pairing.
38
+ const TRAIN = [
39
+ [
40
+ "What is the largest planet in our solar system?",
41
+ "The largest planet is Jupiter.",
42
+ ],
43
+ ["What is the biggest ocean on Earth?", "The biggest ocean is the Pacific."],
44
+ ["What is the largest country by area?", "The largest country is Russia."],
45
+ [
46
+ "What is the biggest mammal on Earth?",
47
+ "The biggest mammal is the blue whale.",
48
+ ],
49
+ [
50
+ "What is the largest desert in the world?",
51
+ "The largest desert is the Sahara.",
52
+ ],
53
+ ["What is the biggest city in Japan?", "The biggest city is Tokyo."],
54
+ ["What is the largest lake in Africa?", "The largest lake is Lake Victoria."],
55
+ ["What is the biggest island on Earth?", "The biggest island is Greenland."],
56
+ ];
57
+
58
+ test("baseline: 'biggest'/'largest' never resolve as independent nodes without natural units", async () => {
59
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
60
+ await m.ingest(TRAIN);
61
+ // This assertion documents the ROOT CAUSE (still true even after the
62
+ // fix — units.ts does not change what resolve() itself returns for a
63
+ // bare word; it changes what recognise() can bridge to via halos).
64
+ assert.equal(resolve(m, enc("biggest")), null);
65
+ assert.equal(resolve(m, enc("largest")), null);
66
+ await m.store.close();
67
+ });
68
+
69
+ test("irrefutable failure: a trained fact is unreachable through an untrained near-synonym", async () => {
70
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
71
+ await m.ingest(TRAIN);
72
+
73
+ // Sanity: the TRAINED phrasing works.
74
+ const trained = await m.respond(
75
+ "What is the largest planet in our solar system?",
76
+ );
77
+ assert.ok(
78
+ dec(trained.bytes).includes("Jupiter"),
79
+ `sanity: trained phrasing must answer, got ${
80
+ JSON.stringify(dec(trained.bytes))
81
+ }`,
82
+ );
83
+
84
+ // The untrained near-synonym phrasing. NOTE: a query that keeps the
85
+ // trained sentence's own long, near-identical scaffolding around the one
86
+ // swapped word ("What is the biggest planet in our solar system?")
87
+ // already succeeds WITHOUT this fix — recallByResonance's WHOLE-QUERY
88
+ // gist is similar enough (one word out of a long, highly-repetitive
89
+ // sentence) to clear the reach threshold on its own, an existing
90
+ // capability this test must not accidentally exercise instead. Deliberately
91
+ // SHORT and restructured ("Name the biggest planet.") so the whole-query
92
+ // gist has no such shortcut — confirmed directly (see this session's
93
+ // investigation) to return silence at baseline: recognise() finds 0
94
+ // sites, and the whole-query resonance score is nowhere near the reach
95
+ // bar. Only a WORD-level bridge from "biggest" to its corroborated
96
+ // synonym "largest" can recover the trained fact here.
97
+ const untrained = await m.respond("Name the biggest planet.");
98
+ assert.ok(
99
+ dec(untrained.bytes).includes("Jupiter"),
100
+ `expected the trained fact to be reachable through the corroborated ` +
101
+ `synonym "biggest" ~ "largest", got ${
102
+ JSON.stringify(dec(untrained.bytes))
103
+ }`,
104
+ );
105
+ await m.store.close();
106
+ });
107
+
108
+ test("resonance-proposed bridge: casing/punctuation paraphrase reaches the trained fact", async () => {
109
+ // Root-caused live on the trained store (2026-07-20): the query
110
+ // "what is the capital of france" resonates STRAIGHT to the trained
111
+ // "What is the capital of France?" (nearest whole-query hit) yet stayed
112
+ // silent — the case-changed bytes fall below the reach bar, the canon
113
+ // twin misses on the absent "?", and the bridge's own W-window climb
114
+ // cannot single the right context out of hundreds sharing common
115
+ // windows. Recall now hands its resonance ranking to the bridge as
116
+ // PROPOSED candidates (rank-only, byte-verified there), which recovers
117
+ // the fact through two corroborated one-byte case substitutions.
118
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
119
+ await m.ingest([
120
+ ["What is the capital of France?", "The capital of France is Paris."],
121
+ ["What is the capital of Spain?", "The capital of Spain is Madrid."],
122
+ ["What is the capital of Italy?", "The capital of Italy is Rome."],
123
+ // Lowercase mid-sentence occurrences attest the case-folded windows
124
+ // the substitution's corroboration gate requires — including the
125
+ // frame-bearing " of france" the expansion absorbs into.
126
+ ["He wrote of france and of spain.", "Then he flew home to italy."],
127
+ ["She spoke of france in her diary.", "Her diary told of france."],
128
+ ]);
129
+ const r = await m.respond("what is the capital of france");
130
+ assert.ok(
131
+ dec(r.bytes).includes("Paris"),
132
+ `expected the trained fact through the resonance-proposed bridge, got ${
133
+ JSON.stringify(dec(r.bytes))
134
+ }`,
135
+ );
136
+ await m.store.close();
137
+ });
138
+
139
+ test(
140
+ "FIXED 2026-07-20: a proper-noun substitution must not voice a wrong " +
141
+ "fact when the true answer has no outgoing edge to compete as a " +
142
+ "bridge candidate (RAW BALANCE gate, see bridge.ts)",
143
+ async () => {
144
+ // This miniature corpus does NOT reproduce the live bug (recall's
145
+ // whole-query resonance already answers correctly here — verified: it
146
+ // takes the real 17.9M-node store's specific candidate ranking to
147
+ // surface the failure mode). Kept anyway as a standing regression
148
+ // check on the shape of the bug (terminal fact vs. a same-shaped fact
149
+ // WITH a continuation); the authoritative fix verification was run
150
+ // directly against the trained store: "The capital of France is"
151
+ // used to bridge through a substitution reading "of Fra[nce]" as
152
+ // "of Spain si[nce]" (raw mismatch (3,8) bytes, badly imbalanced);
153
+ // the RAW BALANCE gate (dominates(min(uLen,cLen), max(uLen,cLen)))
154
+ // now refuses it, and recall correctly falls through to an honest
155
+ // echo of the true trained fact instead.
156
+ const m = new Mind({
157
+ seed: 7,
158
+ store: new SQliteStore({ path: ":memory:" }),
159
+ });
160
+ await m.ingest([
161
+ ["What is the capital of France?", "The capital of France is Paris."],
162
+ [
163
+ "Madrid has been the capital of Spain since 1561.",
164
+ "It was established as such by Philip III.",
165
+ ],
166
+ ]);
167
+ const r = await m.respond("The capital of France is");
168
+ assert.ok(
169
+ !dec(r.bytes).includes("Spain"),
170
+ `expected no wrong-entity substitution, got ${
171
+ JSON.stringify(dec(r.bytes))
172
+ }`,
173
+ );
174
+ },
175
+ );
@@ -0,0 +1,179 @@
1
+ // 50-cast-analog-consensus-floor.test.mjs — CAST's analogical comparison
2
+ // must not voice content the query never named through a climb root whose
3
+ // consensus vote is below consensusFloor(N) = ln(N) + 1/2 — the SAME trust
4
+ // bar recallByResonance applies before grounding through a climb root.
5
+ //
6
+ // The live bug (traced on the real trained store, 325k edge sources, floor
7
+ // 13.2): "Tell me the name of the biggest planet orbiting our sun." shares
8
+ // only stopword scraps ("the", "our", generic bigrams) with an unrelated
9
+ // haiku exemplar; those scraps pooled a climb vote of just 1.92, yet the
10
+ // climb still committed the exemplar as FIRST root (the first root is
11
+ // deliberately floor-free in attention.ts — "the dominant one always
12
+ // grounds"). CAST's comparison schema then cited an analog reached through
13
+ // a continuation hop (never named by the query), and voiced the haiku's
14
+ // continuation ("Winds of change blow free…") as the answer — a wrong
15
+ // answer where recall, extraction, and the ALU all honestly refused.
16
+ //
17
+ // The gate (cast.ts): comparison may cite a hop-reached (unnamed) analog
18
+ // only when some committed root's vote clears consensusFloor(N); a
19
+ // DIRECTLY aligned analog (the query's own bytes evidence it, e.g. test/29
20
+ // C1's "Steel is hard") needs no floor. Derived, never tuned — the floor
21
+ // is the one recall.ts already gates the same trust decision with.
22
+ //
23
+ // This miniature reproduces the exact shape: ~200 filler pairs whose LONG
24
+ // contexts (longer than the probe query) can never be direct analogs, so
25
+ // their short continuations arrive only as continuation-hop descendants;
26
+ // the exemplar's distinctive words also occur across fillers, diluting its
27
+ // root vote below the floor (measured: 4.24 and 5.51 against floor 5.81)
28
+ // while it still ranks first and commits floor-free. Confirmed red at
29
+ // baseline: both probes echoed the haiku continuation with provenance
30
+ // "cast"; with the gate both are honest silence.
31
+
32
+ import { test } from "node:test";
33
+ import assert from "node:assert/strict";
34
+ import { Mind } from "../dist/src/index.js";
35
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
36
+
37
+ const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
38
+
39
+ const subjects = [
40
+ "cat",
41
+ "dog",
42
+ "bird",
43
+ "fish",
44
+ "horse",
45
+ "sheep",
46
+ "goat",
47
+ "duck",
48
+ "frog",
49
+ "deer",
50
+ "wolf",
51
+ "bear",
52
+ "lion",
53
+ "tiger",
54
+ "mouse",
55
+ "rabbit",
56
+ "otter",
57
+ "eagle",
58
+ "shark",
59
+ "whale",
60
+ ];
61
+ const verbs = [
62
+ "describes",
63
+ "praises",
64
+ "watches",
65
+ "follows",
66
+ "studies",
67
+ "admires",
68
+ "draws",
69
+ "paints",
70
+ "names",
71
+ "sings",
72
+ ];
73
+ const objects = [
74
+ "freedom",
75
+ "feeling",
76
+ "titles",
77
+ "songs",
78
+ "times",
79
+ "stories",
80
+ "poems",
81
+ "rivers",
82
+ "shadows",
83
+ "seasons",
84
+ ];
85
+
86
+ const TRAIN = [];
87
+ for (const s of subjects) {
88
+ for (const v of verbs) {
89
+ const o =
90
+ objects[(subjects.indexOf(s) + verbs.indexOf(v)) % objects.length];
91
+ TRAIN.push([
92
+ `The ${s} quietly ${v} the many ${o} it meets today in the wide green garden.`,
93
+ `Indeed the ${s} ${v} ${o}.`,
94
+ ]);
95
+ }
96
+ }
97
+ TRAIN.push([
98
+ "Create a haiku that describes the feeling of freedom.",
99
+ "Winds of change blow free\nFeeling of lightness fills my soul",
100
+ ]);
101
+
102
+ // SHORT filler contexts: continuations become directly ALIGNED points, so
103
+ // the junk analog arrives with frame-tier similarity as an aligned point —
104
+ // the configuration the consensusFloor/naming gates alone cannot separate
105
+ // from test/29 C1's legitimate small-corpus comparison. The separator is
106
+ // the IGNORED-KNOWN principle (dismissedKnownContent): under an untrusted
107
+ // root, a frame-tier comparison must account for every STORED window of
108
+ // the query — here "songs"/"times"/"planet"-class trained content is left
109
+ // in gaps, so comparison refuses; C1's gaps ("How ", " like ") are
110
+ // untrained and stay tolerable. Confirmed red at baseline: both probes
111
+ // echoed the haiku + a filler continuation with provenance "cast".
112
+ const SHORT_TRAIN = [];
113
+ for (const s of subjects) {
114
+ for (const v of verbs) {
115
+ const o =
116
+ objects[(subjects.indexOf(s) + verbs.indexOf(v)) % objects.length];
117
+ SHORT_TRAIN.push([
118
+ `The ${s} ${v} the ${o} today.`,
119
+ `Indeed the ${s} ${v} ${o}.`,
120
+ ]);
121
+ }
122
+ }
123
+ SHORT_TRAIN.push([
124
+ "Create a haiku that describes the feeling of freedom.",
125
+ "Winds of change blow free\nFeeling of lightness fills my soul",
126
+ ]);
127
+
128
+ test("comparison refuses an aligned frame-tier analog that dismisses stored query content", async () => {
129
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
130
+ await m.ingest(SHORT_TRAIN);
131
+ for (
132
+ const q of [
133
+ "Give me the title of the finest song describing our times.",
134
+ "Tell me the name of the biggest planet orbiting our sun.",
135
+ ]
136
+ ) {
137
+ const r = await m.respond(q);
138
+ const t = dec(r.bytes);
139
+ assert.equal(
140
+ t,
141
+ "",
142
+ `expected honest silence for ${JSON.stringify(q)}, got ${
143
+ JSON.stringify(t)
144
+ }`,
145
+ );
146
+ }
147
+ await m.store.close();
148
+ });
149
+
150
+ test("comparison refuses a hop-reached analog under a below-floor root", async () => {
151
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
152
+ await m.ingest(TRAIN);
153
+
154
+ // Neither query names any trained fact — the corpus knows nothing about
155
+ // songs, titles, or planets. The only honest answer is silence; at
156
+ // baseline both echoed the haiku exemplar's continuation via CAST.
157
+ for (
158
+ const q of [
159
+ "Give me the title of the finest song describing our times.",
160
+ "Tell me the name of the biggest planet orbiting our sun.",
161
+ ]
162
+ ) {
163
+ const r = await m.respond(q);
164
+ const t = dec(r.bytes);
165
+ assert.ok(
166
+ !t.includes("Winds of change"),
167
+ `CAST voiced an unrelated exemplar's continuation off a below-floor ` +
168
+ `root for ${JSON.stringify(q)}, got ${JSON.stringify(t)}`,
169
+ );
170
+ assert.equal(
171
+ t,
172
+ "",
173
+ `expected honest silence for ${JSON.stringify(q)}, got ${
174
+ JSON.stringify(t)
175
+ }`,
176
+ );
177
+ }
178
+ await m.store.close();
179
+ });