@hviana/sema 0.2.9 → 0.4.0

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 (84) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +5 -4
  12. package/dist/src/mind/attention.js +254 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/index.d.ts +2 -0
  20. package/dist/src/mind/junction.d.ts +10 -0
  21. package/dist/src/mind/junction.js +14 -0
  22. package/dist/src/mind/learning.d.ts +32 -4
  23. package/dist/src/mind/learning.js +26 -4
  24. package/dist/src/mind/match.d.ts +40 -0
  25. package/dist/src/mind/match.js +125 -1
  26. package/dist/src/mind/mechanisms/cast.js +63 -6
  27. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  28. package/dist/src/mind/mechanisms/extraction.js +1 -88
  29. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  30. package/dist/src/mind/mechanisms/recall.js +77 -14
  31. package/dist/src/mind/mind.d.ts +59 -5
  32. package/dist/src/mind/mind.js +115 -93
  33. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  34. package/dist/src/mind/pipeline-mechanism.js +179 -10
  35. package/dist/src/mind/pipeline.d.ts +29 -0
  36. package/dist/src/mind/pipeline.js +79 -21
  37. package/dist/src/mind/primitives.d.ts +11 -15
  38. package/dist/src/mind/primitives.js +47 -28
  39. package/dist/src/mind/reasoning.d.ts +7 -1
  40. package/dist/src/mind/reasoning.js +40 -8
  41. package/dist/src/mind/recognition.js +93 -20
  42. package/dist/src/mind/traverse.d.ts +11 -0
  43. package/dist/src/mind/traverse.js +88 -7
  44. package/dist/src/mind/types.d.ts +39 -5
  45. package/dist/src/store.d.ts +15 -0
  46. package/dist/src/store.js +91 -6
  47. package/package.json +1 -1
  48. package/src/derive/src/deduction.ts +15 -0
  49. package/src/derive/src/index.ts +1 -0
  50. package/src/geometry.ts +350 -122
  51. package/src/index.ts +1 -0
  52. package/src/meter.ts +333 -0
  53. package/src/mind/attention.ts +276 -31
  54. package/src/mind/bridge.ts +187 -10
  55. package/src/mind/canonical.ts +6 -1
  56. package/src/mind/graph-search.ts +60 -21
  57. package/src/mind/index.ts +6 -0
  58. package/src/mind/junction.ts +12 -0
  59. package/src/mind/learning.ts +46 -5
  60. package/src/mind/match.ts +146 -1
  61. package/src/mind/mechanisms/cast.ts +62 -6
  62. package/src/mind/mechanisms/extraction.ts +2 -103
  63. package/src/mind/mechanisms/recall.ts +84 -17
  64. package/src/mind/mind.ts +144 -99
  65. package/src/mind/pipeline-mechanism.ts +203 -13
  66. package/src/mind/pipeline.ts +144 -36
  67. package/src/mind/primitives.ts +49 -33
  68. package/src/mind/reasoning.ts +39 -7
  69. package/src/mind/recognition.ts +89 -19
  70. package/src/mind/traverse.ts +89 -8
  71. package/src/mind/types.ts +39 -5
  72. package/src/store.ts +75 -6
  73. package/test/14-scaling.test.mjs +17 -7
  74. package/test/31-audit.test.mjs +4 -1
  75. package/test/33-multi-candidate.test.mjs +13 -1
  76. package/test/36-already-answered-fusion.test.mjs +10 -3
  77. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  78. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  79. package/test/54-evidence-k-instrumentation.test.mjs +175 -0
  80. package/test/55-cost-meter.test.mjs +284 -0
  81. package/test/56-bridge-identity-admission.test.mjs +209 -0
  82. package/test/57-fusion-order.test.mjs +104 -0
  83. package/test/58-subquantum-sites.test.mjs +112 -0
  84. package/test/59-fold-invariance.test.mjs +226 -0
@@ -0,0 +1,209 @@
1
+ // 56-bridge-identity-admission.test.mjs — the bridge's ZERO-SUBSTITUTION
2
+ // admission: a candidate the alignment explains END TO END on BOTH sides,
3
+ // separated from the query only by material that does not change what the
4
+ // text SAYS, is the SAME learnt form and grounds through its own edge.
5
+ //
6
+ // "Material that does not change what it says" has ONE definition here, and
7
+ // it is read from the corpus, never tuned (AGENTS §2.7, corpus-global
8
+ // population): a span is EXPLAINED when it is sub-quantum (< W — typographic
9
+ // glue) or every W-window in it is COMMON by the store's own climb (the
10
+ // ascent saturates, or it reaches a majority of contexts). A window that
11
+ // reaches NOTHING is novel content and is never explained — the reading that
12
+ // separates a droppable "the process of " from a load-bearing "heavy ".
13
+ //
14
+ // THE GAP THIS CLOSES (measured on the 17.9M-node trained store). The query
15
+ // `Who wrote Romeo and Juliet?` against the trained `Who wrote "Romeo and
16
+ // Juliet"?` — two inserted quote characters — returned honest silence. The
17
+ // gist is a STRUCTURAL signature, so a mid-string insertion shifts every fold
18
+ // boundary after it: the pair scored cos 0.377, BELOW unrelated neighbours
19
+ // like "Who wrote the opera Carmen??" (0.603). Recall's identity tier gates
20
+ // on identityBar (0.969 at that length) and its reach tiers on 0.875, so no
21
+ // gist-based tier could ever see it. Only byte-exact alignment can — and the
22
+ // bridge, which does exactly that, refused it for producing NO substitution.
23
+ //
24
+ // WHAT MUST NOT REGRESS (the documented prefix trap, bridge.ts): when the
25
+ // query is a strict byte-PREFIX of several candidates that continue
26
+ // differently, the bridge must still refuse — nothing corroborates picking
27
+ // one continuation over another, and the surplus IS the invented answer.
28
+ // The two shapes are separated by the candidate-side test: an identity
29
+ // candidate has ≤ W bytes beyond the alignment, a prefix candidate has its
30
+ // whole completion beyond it.
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 mk = () =>
38
+ new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
39
+
40
+ /** Corroboration for the query's own windows: the bridge only aligns against
41
+ * content whose W-windows are corpus-attested, so the fixture trains the
42
+ * phrasing family, not one isolated sentence. */
43
+ const TRAIN = [
44
+ ['Who wrote "Romeo and Juliet"?', " William Shakespeare wrote it."],
45
+ ['Who wrote "Hamlet"?', " William Shakespeare wrote it."],
46
+ ['Who wrote "Macbeth"?', " William Shakespeare wrote it."],
47
+ ["Who wrote the opera Carmen?", " Georges Bizet wrote it."],
48
+ ["Who wrote about Romeo and Juliet in an essay?", " A critic did."],
49
+ ];
50
+
51
+ async function trained() {
52
+ const mind = mk();
53
+ await mind.ingest(TRAIN);
54
+ return mind;
55
+ }
56
+
57
+ test("1. a query differing only by typographic glue reaches the trained form", async () => {
58
+ const mind = await trained();
59
+ // Same form as the trained question, minus the two quote characters.
60
+ const a = await mind.respondText("Who wrote Romeo and Juliet?");
61
+ assert.match(
62
+ a,
63
+ /Shakespeare/,
64
+ `expected the trained continuation, got ${JSON.stringify(a)}`,
65
+ );
66
+ await mind.store.close();
67
+ });
68
+
69
+ test("2. the exact trained form still answers (no path was displaced)", async () => {
70
+ const mind = await trained();
71
+ const a = await mind.respondText('Who wrote "Romeo and Juliet"?');
72
+ assert.match(a, /Shakespeare/);
73
+ await mind.store.close();
74
+ });
75
+
76
+ test("3. a DIFFERENT trained question is not answered by its neighbour", async () => {
77
+ const mind = await trained();
78
+ const a = await mind.respondText("Who wrote the opera Carmen?");
79
+ assert.match(
80
+ a,
81
+ /Bizet/,
82
+ "the identity admission must not let a near neighbour stand in",
83
+ );
84
+ await mind.store.close();
85
+ });
86
+
87
+ test("4. PREFIX TRAP: a strict prefix of several candidates is still refused", async () => {
88
+ const mind = mk();
89
+ // Three facts sharing one prefix, each continuing differently. A query
90
+ // that IS that prefix has no corroboration for any single completion; the
91
+ // candidate-side surplus is exactly the answer that would be invented.
92
+ await mind.ingest([
93
+ ["The capital city of France is Paris.", " It sits on the Seine."],
94
+ ["The capital city of Spain is Madrid.", " It sits on the Manzanares."],
95
+ ["The capital city of Italy is Rome.", " It sits on the Tiber."],
96
+ ]);
97
+ const a = await mind.respondText("The capital city of");
98
+ // Whatever the pipeline does with this, the bridge must not manufacture a
99
+ // country: an answer naming one of the three would be the invented
100
+ // completion the trap describes.
101
+ const named = ["Paris", "Madrid", "Rome"].filter((c) => a.includes(c));
102
+ assert.ok(
103
+ named.length !== 1 || a.includes("capital city of"),
104
+ `bridge invented a single completion (${named[0]}) for a bare prefix: ` +
105
+ JSON.stringify(a),
106
+ );
107
+ await mind.store.close();
108
+ });
109
+
110
+ test("5. honest silence survives — an unrelated query still grounds nothing", async () => {
111
+ const mind = await trained();
112
+ const a = await mind.respondText("What is the zorblatt frequency?");
113
+ assert.equal(
114
+ a.length,
115
+ 0,
116
+ `expected silence, got ${JSON.stringify(a)} — the identity admission ` +
117
+ `must not lower the bar for unrelated content`,
118
+ );
119
+ await mind.store.close();
120
+ });
121
+
122
+ test("6. determinism: the admission is byte-exact, not scored", async () => {
123
+ const a = await trained();
124
+ const b = await trained();
125
+ const x = await a.respondText("Who wrote Romeo and Juliet?");
126
+ const y = await b.respondText("Who wrote Romeo and Juliet?");
127
+ assert.equal(x, y);
128
+ await a.store.close();
129
+ await b.store.close();
130
+ });
131
+
132
+ // ── The scaffolding reading ────────────────────────────────────────────────
133
+
134
+ test("7. an omitted CORPUS-COMMON span is explained; a RARE one is not", async () => {
135
+ const { leafIdRun } = await import("../dist/src/mind/canonical.js");
136
+ const { corpusN, edgeAncestors } = await import(
137
+ "../dist/src/mind/traverse.js"
138
+ );
139
+ const { dominates } = await import("../dist/src/geometry.js");
140
+
141
+ const mind = mk();
142
+ const T = [];
143
+ for (const x of ["A", "B", "C", "D", "E", "F", "G", "H"]) {
144
+ T.push([`What is the process of ${x}ation?`, ` ${x}ation is a process.`]);
145
+ T.push([`Tell me the process of ${x}ing.`, ` ${x}ing is a process.`]);
146
+ }
147
+ T.push(["Is heavy water wet?", " No, heavy water is not wet."]);
148
+ await mind.ingest(T);
149
+
150
+ const N = corpusN(mind);
151
+ const W = mind.space.maxGroup;
152
+ // The predicate under test, mirrored exactly (bridge.ts explainedSpan).
153
+ const explained = (text) => {
154
+ const b = new TextEncoder().encode(text);
155
+ if (b.length < W) return true;
156
+ for (let o = 0; o + W <= b.length; o++) {
157
+ const ids = leafIdRun(mind, b, o, o + W);
158
+ if (ids === null) return false;
159
+ const wid = mind.store.findBranch(ids);
160
+ if (wid === null) return false;
161
+ const r = edgeAncestors(mind, wid, N);
162
+ if (r.saturated) continue;
163
+ if (r.roots.length === 0) return false;
164
+ if (!dominates(r.contextsReached, N)) return false;
165
+ }
166
+ return true;
167
+ };
168
+
169
+ assert.equal(
170
+ explained("the process of "),
171
+ true,
172
+ "corpus-common scaffolding must be droppable",
173
+ );
174
+ assert.equal(
175
+ explained("heavy "),
176
+ false,
177
+ "rare, discriminative content must NEVER be written off as scaffolding — " +
178
+ "a candidate omitting it answers a different, narrower question",
179
+ );
180
+ // The reading, not just the population: a window reaching NOTHING is novel
181
+ // content. Going through reachOf (which maps both saturated and
182
+ // empty-rooted to Infinity) called "heavy " scaffolding and answered
183
+ // "Is water wet?" with "No, heavy water is not wet.".
184
+ assert.equal(
185
+ explained("zqxjwv "),
186
+ false,
187
+ "untrained content is never explained",
188
+ );
189
+ await mind.store.close();
190
+ });
191
+
192
+ test("8. EXACT query coverage — a sub-quantum query-side difference is NOT glue", async () => {
193
+ const mind = mk();
194
+ // A trained arithmetic fact, and a query differing from it only inside a
195
+ // sub-quantum span — but that span is DIGITS, and digits are content.
196
+ await mind.ingest([
197
+ ["what is 2+2?", " 2+2 is 4."],
198
+ ["what is 3+3?", " 3+3 is 6."],
199
+ ["what is 5+5?", " 5+5 is 10."],
200
+ ]);
201
+ const a = await mind.respondText("what is 2^10?");
202
+ assert.ok(
203
+ !a.includes("4."),
204
+ `answered a DIFFERENT arithmetic question: ${JSON.stringify(a)} — the ` +
205
+ `identity admission must require covered === query.length, with no ` +
206
+ `sub-quantum slack on the query side`,
207
+ );
208
+ await mind.store.close();
209
+ });
@@ -0,0 +1,104 @@
1
+ // 57-fusion-order.test.mjs — a fused multi-topic answer must READ in the
2
+ // order the question posed its topics.
3
+ //
4
+ // fuseAttention sorts its pieces by `start`, their position in the query.
5
+ // Every attention ROOT carries its own start. `primary` did not: it was
6
+ // given forest[0].start — the FIRST root's position — which is primary's own
7
+ // source only when primary happens to come from that root. When it does
8
+ // not, primary sorts to a position it never occupied.
9
+ //
10
+ // Observed live on the 17.9M-node trained store:
11
+ // "What is the capital of France? And what is 2 + 2?"
12
+ // -> "4The capital city of France is Paris."
13
+ // Both pieces were correct; only the order was wrong. The ALU result, whose
14
+ // evidence is the "2 + 2" span at the END of the query, had inherited the
15
+ // France root's start of 0.
16
+ //
17
+ // primary's position is now the earliest query byte its own grounding stands
18
+ // on: `accounted` when non-empty, else the computed span (a pure computation
19
+ // is priced out of `accounted` by cover — the same cost-ladder-vs-coverage
20
+ // distinction think() already draws for the fusion remainder).
21
+ //
22
+ // NOT under test: the missing separator between the pieces. joinWithBridge
23
+ // splices only a LEARNT connector; when the corpus holds none the pieces join
24
+ // bare. Inventing a space would synthesize bytes the store never saw, which
25
+ // this system does not do — bare joining is the honest degradation.
26
+
27
+ import { test } from "node:test";
28
+ import assert from "node:assert/strict";
29
+ import { Mind } from "../dist/src/index.js";
30
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
31
+
32
+ const mk = () =>
33
+ new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
34
+
35
+ /** Two independent topics, each with its own trained fact, plus enough
36
+ * same-frame neighbours that the consensus climb commits both as separate
37
+ * points of attention rather than one. */
38
+ const TRAIN = [
39
+ ["What is the capital of France?", " The capital of France is Paris."],
40
+ ["What is the capital of Spain?", " The capital of Spain is Madrid."],
41
+ ["What is the capital of Italy?", " The capital of Italy is Rome."],
42
+ ["What is the largest planet?", " The largest planet is Jupiter."],
43
+ ["What is the largest ocean?", " The largest ocean is the Pacific."],
44
+ ];
45
+
46
+ async function trained() {
47
+ const mind = mk();
48
+ await mind.ingest(TRAIN);
49
+ return mind;
50
+ }
51
+
52
+ /** Index of `needle` in `hay`, or -1. */
53
+ const at = (hay, needle) => hay.indexOf(needle);
54
+
55
+ test("1. two fused topics appear in the query's own order", async () => {
56
+ const mind = await trained();
57
+ const a = await mind.respondText(
58
+ "What is the capital of France? And what is the largest planet?",
59
+ );
60
+ const iParis = at(a, "Paris"), iJup = at(a, "Jupiter");
61
+ if (iParis >= 0 && iJup >= 0) {
62
+ assert.ok(
63
+ iParis < iJup,
64
+ `France was asked FIRST but its answer trails: ${JSON.stringify(a)}`,
65
+ );
66
+ }
67
+ await mind.store.close();
68
+ });
69
+
70
+ test("2. reversing the question reverses the fused answer", async () => {
71
+ const mind = await trained();
72
+ const a = await mind.respondText(
73
+ "What is the largest planet? And what is the capital of France?",
74
+ );
75
+ const iParis = at(a, "Paris"), iJup = at(a, "Jupiter");
76
+ if (iParis >= 0 && iJup >= 0) {
77
+ assert.ok(
78
+ iJup < iParis,
79
+ `the planet was asked FIRST but its answer trails: ${JSON.stringify(a)}`,
80
+ );
81
+ }
82
+ await mind.store.close();
83
+ });
84
+
85
+ test("3. a single-topic answer is unchanged by the ordering rule", async () => {
86
+ const mind = await trained();
87
+ assert.match(
88
+ await mind.respondText("What is the capital of France?"),
89
+ /Paris/,
90
+ );
91
+ assert.match(
92
+ await mind.respondText("What is the largest planet?"),
93
+ /Jupiter/,
94
+ );
95
+ await mind.store.close();
96
+ });
97
+
98
+ test("4. determinism", async () => {
99
+ const a = await trained(), b = await trained();
100
+ const q = "What is the capital of France? And what is the largest planet?";
101
+ assert.equal(await a.respondText(q), await b.respondText(q));
102
+ await a.store.close();
103
+ await b.store.close();
104
+ });
@@ -0,0 +1,112 @@
1
+ // 58-subquantum-sites.test.mjs — at hub scale, a recognised form shorter
2
+ // than one river window (W) is CHANCE, not evidence.
3
+ //
4
+ // recognition.emit is the single choke point every pass emits sites through
5
+ // (structural subtree, chunk sub-runs, canonical leaf-id chains, edge trims).
6
+ // Below W, byte overlap carries no identity information — the SAME quantum
7
+ // floor identityBar prices ("below one river window, byte overlap is chance")
8
+ // and the bridge's attestedQ applies ("spans shorter than W carry no window of
9
+ // their own and can never substitute"). No new constant.
10
+ //
11
+ // Scoped to hub scale by the same `atomsAreHubs` switch that already guards
12
+ // byte atoms, and for the same reason: on a SMALL store a two-byte fact is
13
+ // genuine learnt content and its site is essential; on a large one every short
14
+ // letter-run of every query becomes a "recognised form" that cover can hang an
15
+ // edge off. A span covering the WHOLE query is exempt — then it is not a
16
+ // coincidental fragment of something longer, it is the question.
17
+ //
18
+ // THE LIVE SHAPE (17.9M-node trained store): "In which country is the Eiffel
19
+ // Tower?" recognised the form "hi" — the i=0 sub-run of the fold chunk "hich"
20
+ // in "In w[hi]ch" — alongside "the" and "Eiffel Tower". The canonical pass's
21
+ // own comment already names this coincidence ('"hi" resolving out of
22
+ // "W[hi]ch"'). After this rule only "Eiffel Tower" survives.
23
+ //
24
+ // NOT claimed here: that removing those sites fixes that query. It does not —
25
+ // see bench/README.md. Cover still grounds a greeting there through a 2-byte
26
+ // span that does NOT come from `sites`. This test pins the site rule only.
27
+
28
+ import { test } from "node:test";
29
+ import assert from "node:assert/strict";
30
+ import { Mind } from "../dist/src/index.js";
31
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
32
+
33
+ const mk = () =>
34
+ new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
35
+
36
+ const enc = new TextEncoder();
37
+
38
+ test("1. a sub-quantum coincidental substring is not a recognised site", async () => {
39
+ const mind = mk();
40
+ await mind.ingest([
41
+ ["hi", " Hello there, how can I help?"],
42
+ ["What is the capital of France?", " The capital of France is Paris."],
43
+ ["What is the capital of Spain?", " The capital of Spain is Madrid."],
44
+ ["What is the capital of Italy?", " The capital of Italy is Rome."],
45
+ ]);
46
+ const { recognise } = await import("../dist/src/mind/recognition.js");
47
+ const { atomIsHub, corpusN } = await import("../dist/src/mind/traverse.js");
48
+ const N = corpusN(mind);
49
+ const W = mind.space.maxGroup;
50
+ if (!atomIsHub(mind, N)) {
51
+ // Below hub scale the rule is deliberately inert — nothing to assert.
52
+ await mind.store.close();
53
+ return;
54
+ }
55
+ // "which" contains "hi" at an interior offset.
56
+ const q = enc.encode("In which country is the capital of France?");
57
+ const sites = recognise(mind, q).sites;
58
+ for (const s of sites) {
59
+ assert.ok(
60
+ s.end - s.start >= W || (s.start === 0 && s.end === q.length),
61
+ `sub-quantum site ${
62
+ JSON.stringify(
63
+ new TextDecoder().decode(q.subarray(s.start, s.end)),
64
+ )
65
+ } survived at hub scale`,
66
+ );
67
+ }
68
+ await mind.store.close();
69
+ });
70
+
71
+ test("2. a whole-query short form is EXEMPT — it is the question", async () => {
72
+ const mind = mk();
73
+ await mind.ingest([
74
+ ["hi", " Hello there, how can I help?"],
75
+ ["What is the capital of France?", " The capital of France is Paris."],
76
+ ["What is the capital of Spain?", " The capital of Spain is Madrid."],
77
+ ]);
78
+ const a = await mind.respondText("hi");
79
+ assert.match(
80
+ a,
81
+ /Hello there/,
82
+ "a short form that IS the whole query must still be answerable",
83
+ );
84
+ await mind.store.close();
85
+ });
86
+
87
+ test("3. long forms are untouched", async () => {
88
+ const mind = mk();
89
+ await mind.ingest([
90
+ ["What is the capital of France?", " The capital of France is Paris."],
91
+ ["What is the capital of Spain?", " The capital of Spain is Madrid."],
92
+ ]);
93
+ assert.match(
94
+ await mind.respondText("What is the capital of France?"),
95
+ /Paris/,
96
+ );
97
+ await mind.store.close();
98
+ });
99
+
100
+ test("4. determinism", async () => {
101
+ const run = async () => {
102
+ const m = mk();
103
+ await m.ingest([
104
+ ["hi", " Hello there, how can I help?"],
105
+ ["What is the capital of France?", " The capital of France is Paris."],
106
+ ]);
107
+ const a = await m.respondText("In which country is Paris?");
108
+ await m.store.close();
109
+ return a;
110
+ };
111
+ assert.equal(await run(), await run());
112
+ });
@@ -0,0 +1,226 @@
1
+ // 59-fold-invariance.test.mjs — the fold's segmentation must be decided by
2
+ // CONTENT, not by a byte's absolute position.
3
+ //
4
+ // `riverFold` groups fixed arity from byte 0 and permutes item k by seats[k],
5
+ // k = index mod W. Under that rule a byte's contribution — and therefore the
6
+ // subtree its content sits in — is a function of where it happens to fall, so
7
+ // the same content is a different node at a different offset. Measured
8
+ // consequences before this changed: recognition's site set moved with the
9
+ // query's phase (period W); `What is the capital of France?` answered at pad
10
+ // 0, 1, 4 and went silent at 2, 3, 5, 6, 7; and at pad 3 `In which country is
11
+ // the Eiffel Tower?` answered with unrelated Malagasy text instead of
12
+ // abstaining. Semantic identity must not depend on W at all.
13
+ //
14
+ // `contentBoundaries` decides cuts with a rolling hash, so a change upstream
15
+ // moves only the cut it falls inside. These are the properties that buys, and
16
+ // they are cheap to lose by accident — capping the segment length at W, for
17
+ // instance, makes forced cuts dominate and silently restores the grid while
18
+ // LEAVING EVERY OTHER TEST GREEN (it was measured at 14.3%, exactly the grid,
19
+ // with the rest of the suite passing). That is why invariance is asserted
20
+ // here as a number rather than left implicit.
21
+ //
22
+ // Floors are set well under the measured values so ordinary drift does not
23
+ // fail the suite, and far above the grid's 14.3% so a regression to positional
24
+ // segmentation cannot pass. Measured at the time of writing:
25
+ //
26
+ // deposit-like text 96.7% cuts held / 94.5% segments identical
27
+ // non-Latin scripts 97.4% / 95.8%
28
+ // random binary 98.6% / 97.4%
29
+ // the arithmetic grid 14.3% (only shifts of k ≡ 0 mod W survive)
30
+
31
+ import { test } from "node:test";
32
+ import assert from "node:assert/strict";
33
+ import { bytesToTree, contentBoundaries } from "../dist/src/geometry.js";
34
+ import { Alphabet } from "../dist/src/alphabet.js";
35
+ import { makeKeyring, rng } from "../dist/src/vec.js";
36
+
37
+ const D = 256;
38
+ const W = 4;
39
+ const mkSpace = () => ({
40
+ D,
41
+ seats: makeKeyring(D, Math.max(8, W), rng(1)),
42
+ rand: rng(2),
43
+ maxGroup: W,
44
+ });
45
+ const space = mkSpace();
46
+ const alphabet = new Alphabet(7, D, { roughness: 0.65, seedMask: 0xa1fa17 });
47
+ const enc = (s) => new TextEncoder().encode(s);
48
+
49
+ const SAMPLES = [
50
+ "The Eiffel Tower is a wrought iron lattice tower located in Paris, France.",
51
+ "What is the capital of France? The capital of France is Paris.",
52
+ "Photosynthesis is the process by which plants convert light into energy.",
53
+ "Was ist die Hauptstadt von Deutschland? Die Hauptstadt ist Berlin.",
54
+ "Qual é a capital do Brasil? A capital do Brasil é Brasília.",
55
+ "水は水素と酸素からできています。これは化学の基本です。",
56
+ ];
57
+
58
+ const segmentsOf = (bytes) => {
59
+ const edges = [0, ...contentBoundaries(space, bytes), bytes.length];
60
+ const out = [];
61
+ for (let i = 0; i + 1 < edges.length; i++) {
62
+ out.push(bytes.subarray(edges[i], edges[i + 1]));
63
+ }
64
+ return out;
65
+ };
66
+ const keyOf = (b) => Array.from(b).join(",");
67
+
68
+ /** Shift each sample by 1..2W bytes and measure how much of the segmentation
69
+ * downstream of the disturbance survives. */
70
+ function invariance(samples) {
71
+ let cuts = 0, held = 0, segs = 0, segsHeld = 0;
72
+ for (const bytes of samples) {
73
+ const base = contentBoundaries(space, bytes);
74
+ const baseSegs = new Set(segmentsOf(bytes).map(keyOf));
75
+ for (let k = 1; k <= 2 * W; k++) {
76
+ const padded = new Uint8Array(k + bytes.length);
77
+ padded.set(enc(" ".repeat(k)));
78
+ padded.set(bytes, k);
79
+ const moved = new Set(contentBoundaries(space, padded));
80
+ // Only cuts past the disturbed head are expected to survive.
81
+ const expected = base.filter((c) => c > 2 * W).map((c) => c + k);
82
+ cuts += expected.length;
83
+ held += expected.filter((c) => moved.has(c)).length;
84
+ // Segment identity, skipping the two segments covering the change.
85
+ for (const s of segmentsOf(padded).slice(2)) {
86
+ segs++;
87
+ if (baseSegs.has(keyOf(s))) segsHeld++;
88
+ }
89
+ }
90
+ }
91
+ return { cutRatio: held / cuts, segRatio: segsHeld / segs };
92
+ }
93
+
94
+ test("content cuts survive a byte shift; the arithmetic grid does not", () => {
95
+ const { cutRatio, segRatio } = invariance(SAMPLES.map(enc));
96
+ assert.ok(
97
+ cutRatio >= 0.85,
98
+ `downstream cuts held ${
99
+ (cutRatio * 100).toFixed(1)
100
+ }% — below the 85% floor; ` +
101
+ `at 14.3% the fold has degenerated to the arithmetic grid`,
102
+ );
103
+ assert.ok(
104
+ segRatio >= 0.8,
105
+ `segments byte-identical ${
106
+ (segRatio * 100).toFixed(1)
107
+ }% — below the 80% floor`,
108
+ );
109
+
110
+ // The grid, same corpus, as the control this floor is meaningful against.
111
+ const gridCuts = (b) => {
112
+ const out = [];
113
+ for (let i = W; i < b.length; i += W) out.push(i);
114
+ return out;
115
+ };
116
+ let gCuts = 0, gHeld = 0;
117
+ for (const bytes of SAMPLES.map(enc)) {
118
+ const base = gridCuts(bytes);
119
+ for (let k = 1; k <= 2 * W; k++) {
120
+ const padded = new Uint8Array(k + bytes.length);
121
+ padded.set(bytes, k);
122
+ const moved = new Set(gridCuts(padded));
123
+ const expected = base.filter((c) => c > 2 * W).map((c) => c + k);
124
+ gCuts += expected.length;
125
+ gHeld += expected.filter((c) => moved.has(c)).length;
126
+ }
127
+ }
128
+ const gridRatio = gHeld / gCuts;
129
+ assert.ok(
130
+ gridRatio < 0.3,
131
+ `control: the grid should lose most cuts under a shift, got ${
132
+ (gridRatio * 100).toFixed(1)
133
+ }%`,
134
+ );
135
+ assert.ok(
136
+ cutRatio > gridRatio * 2,
137
+ `content cuts (${(cutRatio * 100).toFixed(1)}%) must beat the grid ` +
138
+ `(${(gridRatio * 100).toFixed(1)}%) by a wide margin`,
139
+ );
140
+ });
141
+
142
+ test("segmentation reads bytes, not text — random binary behaves like prose", () => {
143
+ // Mind is not a text engine; the same fold carries grids and any other
144
+ // modality. A boundary rule that only worked on prose would be importing an
145
+ // assumption the architecture rejects.
146
+ let s = 12345;
147
+ const rnd = () => (s = (Math.imul(s, 1103515245) + 12345) >>> 0) / 2 ** 32;
148
+ const binary = Array.from({ length: 40 }, () => {
149
+ const n = 40 + Math.floor(rnd() * 300);
150
+ return Uint8Array.from({ length: n }, () => Math.floor(rnd() * 256));
151
+ });
152
+ const { cutRatio, segRatio } = invariance(binary);
153
+ assert.ok(
154
+ cutRatio >= 0.85,
155
+ `random binary cuts held ${
156
+ (cutRatio * 100).toFixed(1)
157
+ }% — the rule must not depend on text`,
158
+ );
159
+ assert.ok(
160
+ segRatio >= 0.8,
161
+ `random binary segments ${(segRatio * 100).toFixed(1)}%`,
162
+ );
163
+ });
164
+
165
+ test("segment lengths stay within the write side's declared unit scale", () => {
166
+ // The minimum is W−1 (canonicalWindows' straddle neighbour, the write side's
167
+ // own floor for a unit) and the maximum is the keyring's seat count, because
168
+ // a segment folds as ONE flat node and `fold` has exactly that many seats.
169
+ // A segment outside that range cannot be bound into seats at all.
170
+ const maxLen = space.seats.length;
171
+ for (const bytes of SAMPLES.map(enc)) {
172
+ const segs = segmentsOf(bytes);
173
+ for (const s of segs) {
174
+ assert.ok(
175
+ s.length <= maxLen,
176
+ `segment of ${s.length} bytes exceeds the ${maxLen} available seats`,
177
+ );
178
+ assert.ok(s.length >= 1, "empty segment");
179
+ }
180
+ assert.equal(
181
+ segs.reduce((a, x) => a + x.length, 0),
182
+ bytes.length,
183
+ "segments must tile the stream exactly",
184
+ );
185
+ }
186
+ });
187
+
188
+ test("the fold stays shallow — no left-nested spine", () => {
189
+ // Joining segments left-nested costs a node per segment on ONE spine: at a
190
+ // cut every ~6 bytes a 3 KB stream became a 450-deep chain of 450 fresh
191
+ // D-vectors, ~1.8 MB for a single deposit, and every walker above inherited
192
+ // the depth. Grouping recurses by cut LEVEL instead, so depth stays
193
+ // logarithmic in the stream length.
194
+ const base = "The Eiffel Tower is a wrought iron lattice tower in Paris. ";
195
+ for (const reps of [1, 8, 40]) {
196
+ const bytes = enc(base.repeat(reps));
197
+ const tree = bytesToTree(space, alphabet, bytes);
198
+ let depth = 0, nodes = 0;
199
+ const stack = [[tree, 1]];
200
+ while (stack.length) {
201
+ const [n, d] = stack.pop();
202
+ nodes++;
203
+ if (d > depth) depth = d;
204
+ if (n.kids !== null) { for (const k of n.kids) stack.push([k, d + 1]); }
205
+ }
206
+ // Generous: 4x the ideal log_W depth still catches a linear spine by an
207
+ // enormous margin (a 2.3 KB stream would spine to ~400).
208
+ const ideal = Math.ceil(Math.log(bytes.length) / Math.log(W));
209
+ assert.ok(
210
+ depth <= ideal * 4,
211
+ `depth ${depth} over ${bytes.length} bytes (river ideal ~${ideal}) — a spine, not a tree`,
212
+ );
213
+ assert.ok(
214
+ nodes < bytes.length * 2,
215
+ `${nodes} nodes for ${bytes.length} bytes — the fold should not multiply nodes`,
216
+ );
217
+ }
218
+ });
219
+
220
+ test("the fold is deterministic — identical bytes, identical cuts", () => {
221
+ for (const bytes of SAMPLES.map(enc)) {
222
+ const a = contentBoundaries(space, bytes);
223
+ const b = contentBoundaries(mkSpace(), Uint8Array.from(bytes));
224
+ assert.deepEqual(a, b, "the same bytes must always cut the same way");
225
+ }
226
+ });