@hviana/sema 0.1.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 (110) hide show
  1. package/AGENTS.md +470 -0
  2. package/AUTHORS.md +12 -0
  3. package/COMMERCIAL-LICENSE.md +19 -0
  4. package/CONTRIBUTING.md +15 -0
  5. package/HOW_IT_WORKS.md +4210 -0
  6. package/LICENSE.md +117 -0
  7. package/README.md +290 -0
  8. package/TRADEMARKS.md +19 -0
  9. package/example/demo.ts +46 -0
  10. package/example/train_base.ts +2675 -0
  11. package/index.html +893 -0
  12. package/package.json +25 -0
  13. package/src/alphabet.ts +34 -0
  14. package/src/alu/README.md +332 -0
  15. package/src/alu/src/alu.ts +541 -0
  16. package/src/alu/src/expr.ts +339 -0
  17. package/src/alu/src/index.ts +115 -0
  18. package/src/alu/src/kernel-arith.ts +377 -0
  19. package/src/alu/src/kernel-bits.ts +199 -0
  20. package/src/alu/src/kernel-logic.ts +102 -0
  21. package/src/alu/src/kernel-nd.ts +235 -0
  22. package/src/alu/src/kernel-numeric.ts +466 -0
  23. package/src/alu/src/operation.ts +344 -0
  24. package/src/alu/src/parser.ts +574 -0
  25. package/src/alu/src/resonance.ts +161 -0
  26. package/src/alu/src/text.ts +83 -0
  27. package/src/alu/src/value.ts +327 -0
  28. package/src/alu/test/alu.test.ts +1004 -0
  29. package/src/bytes.ts +62 -0
  30. package/src/config.ts +227 -0
  31. package/src/derive/README.md +295 -0
  32. package/src/derive/src/deduction.ts +263 -0
  33. package/src/derive/src/index.ts +24 -0
  34. package/src/derive/src/priority-queue.ts +70 -0
  35. package/src/derive/src/rewrite.ts +127 -0
  36. package/src/derive/src/trie.ts +242 -0
  37. package/src/derive/test/derive.test.ts +137 -0
  38. package/src/extension.ts +42 -0
  39. package/src/geometry.ts +511 -0
  40. package/src/index.ts +38 -0
  41. package/src/ingest-cache.ts +224 -0
  42. package/src/mind/articulation.ts +137 -0
  43. package/src/mind/attention.ts +1061 -0
  44. package/src/mind/canonical.ts +107 -0
  45. package/src/mind/graph-search.ts +1100 -0
  46. package/src/mind/index.ts +18 -0
  47. package/src/mind/junction.ts +347 -0
  48. package/src/mind/learning.ts +246 -0
  49. package/src/mind/match.ts +507 -0
  50. package/src/mind/mechanisms/alu.ts +33 -0
  51. package/src/mind/mechanisms/cast.ts +568 -0
  52. package/src/mind/mechanisms/confluence.ts +268 -0
  53. package/src/mind/mechanisms/cover.ts +248 -0
  54. package/src/mind/mechanisms/extraction.ts +422 -0
  55. package/src/mind/mechanisms/recall.ts +241 -0
  56. package/src/mind/mind.ts +483 -0
  57. package/src/mind/pipeline-mechanism.ts +326 -0
  58. package/src/mind/pipeline.ts +300 -0
  59. package/src/mind/primitives.ts +201 -0
  60. package/src/mind/rationale.ts +275 -0
  61. package/src/mind/reasoning.ts +198 -0
  62. package/src/mind/recognition.ts +234 -0
  63. package/src/mind/resonance.ts +0 -0
  64. package/src/mind/trace.ts +108 -0
  65. package/src/mind/traverse.ts +556 -0
  66. package/src/mind/types.ts +281 -0
  67. package/src/rabitq-hnsw/README.md +303 -0
  68. package/src/rabitq-hnsw/src/database.ts +492 -0
  69. package/src/rabitq-hnsw/src/heap.ts +90 -0
  70. package/src/rabitq-hnsw/src/hnsw.ts +514 -0
  71. package/src/rabitq-hnsw/src/index.ts +15 -0
  72. package/src/rabitq-hnsw/src/prng.ts +39 -0
  73. package/src/rabitq-hnsw/src/rabitq.ts +308 -0
  74. package/src/rabitq-hnsw/src/store.ts +994 -0
  75. package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
  76. package/src/store-sqlite.ts +928 -0
  77. package/src/store.ts +2148 -0
  78. package/src/vec.ts +124 -0
  79. package/test/00-extract.test.mjs +151 -0
  80. package/test/01-floor.test.mjs +20 -0
  81. package/test/02-roundtrip.test.mjs +83 -0
  82. package/test/03-recall.test.mjs +98 -0
  83. package/test/04-think.test.mjs +627 -0
  84. package/test/05-concepts.test.mjs +84 -0
  85. package/test/08-storage.test.mjs +948 -0
  86. package/test/09-edges.test.mjs +65 -0
  87. package/test/11-universality.test.mjs +180 -0
  88. package/test/13-conversation.test.mjs +228 -0
  89. package/test/14-scaling.test.mjs +503 -0
  90. package/test/15-decomposition-gap.test.mjs +0 -0
  91. package/test/16-bridge.test.mjs +250 -0
  92. package/test/17-intelligence.test.mjs +240 -0
  93. package/test/18-alu.test.mjs +209 -0
  94. package/test/19-nd.test.mjs +254 -0
  95. package/test/20-stability.test.mjs +489 -0
  96. package/test/21-partial.test.mjs +158 -0
  97. package/test/22-multihop.test.mjs +130 -0
  98. package/test/23-rationale.test.mjs +316 -0
  99. package/test/24-generalization.test.mjs +416 -0
  100. package/test/25-embedding.test.mjs +75 -0
  101. package/test/26-cooperative.test.mjs +89 -0
  102. package/test/27-saturation-drop.test.mjs +637 -0
  103. package/test/28-unknowable.test.mjs +88 -0
  104. package/test/29-counterfactual.test.mjs +476 -0
  105. package/test/30-conflict-resolution.test.mjs +166 -0
  106. package/test/31-audit.test.mjs +288 -0
  107. package/test/32-confluence.test.mjs +173 -0
  108. package/test/33-multi-candidate.test.mjs +222 -0
  109. package/test/34-cross-region.test.mjs +252 -0
  110. package/tsconfig.json +16 -0
@@ -0,0 +1,222 @@
1
+ // 33-multi-candidate.test.mjs — the grounding decider's extended surface.
2
+ //
3
+ // Pins five refinements to think()'s grounding decider (pipeline.ts), each
4
+ // verified through the public respond()/inspectRationale API rather than by
5
+ // reaching into internals:
6
+ //
7
+ // 1. CAST's three internal schemas (substitution, redirection, comparison)
8
+ // each contribute their OWN candidate to the decider, instead of the
9
+ // first-fired schema shadowing the rest.
10
+ // 2. Every mechanism's candidate carries a diagnostic `unexplained` label —
11
+ // never priced, only explanatory — surfaced in decideGrounding's trace.
12
+ // 3. A near-tie between the winner and the runner-up is surfaced as a
13
+ // `narrowDecision` trace step.
14
+ // 4. A winning candidate that explains only a sliver of the query is
15
+ // flagged `thinGrounding` — diagnostic, never suppressing the answer.
16
+ // 5. CAST and extraction are skipped outright (`skipMechanism`) when a
17
+ // dynamic floor read from the (memoised) consensus climb shows the
18
+ // mechanism cannot structurally fire.
19
+
20
+ import { test } from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { Mind } from "../dist/src/index.js";
23
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
24
+
25
+ const mk = (seed = 7) =>
26
+ new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
27
+
28
+ async function trace(m, q) {
29
+ const steps = [];
30
+ const r = await m.respond(q, (s) => steps.push(s));
31
+ return { r, steps };
32
+ }
33
+
34
+ const stepsNamed = (steps, name) =>
35
+ steps.filter((s) => s.mechanism.at(-1) === name);
36
+
37
+ test("1 — CAST's schemas each contribute their own candidate to the decider", async () => {
38
+ const m = mk(7);
39
+ await m.ingest([
40
+ ["Ice is cold", "cold"],
41
+ ["Fire is hot", "hot"],
42
+ ["Steel is hard", "hard"],
43
+ ["Water is wet", "wet"],
44
+ ]);
45
+ const { r, steps } = await trace(m, "What if steel were cold?");
46
+ assert.equal(r.provenance, "cast");
47
+
48
+ const schemas = stepsNamed(steps, "castSchema");
49
+ assert.ok(
50
+ schemas.length >= 2,
51
+ `expected at least two CAST schemas to fire, got ${schemas.length}`,
52
+ );
53
+
54
+ const decide = steps.find((s) => s.mechanism.at(-1) === "decideGrounding");
55
+ assert.ok(
56
+ decide,
57
+ "decideGrounding must be traced when multiple candidates compete",
58
+ );
59
+ const castCandidates = decide.inputs.filter((i) => i.role.startsWith("cast"));
60
+ assert.ok(
61
+ castCandidates.length >= 2,
62
+ `decideGrounding must show multiple CAST candidates, got ${castCandidates.length}`,
63
+ );
64
+ await m.store.close();
65
+ });
66
+
67
+ test("1b — different CAST schemas are weighed by their OWN explanatory power, not a shared span", async () => {
68
+ // A 3-point weave: redirection transfers between the DOMINANT ("The Mona
69
+ // Lisa..." exemplar) and the named substitute ("Pablo Picasso"), while
70
+ // comparison pairs the dominant with a DIFFERENT analog reached through
71
+ // the halo gate — two genuinely different point-pairs, so their accounted
72
+ // spans (and hence weights) may legitimately differ by far more than any
73
+ // fixed move-cost offset (at most CONCEPT + STEP ≈ 12). Before the fix,
74
+ // every fired CAST schema shared ONE weave-wide `accounted` array
75
+ // regardless of which points it actually used, so this spread was
76
+ // impossible — schemas could only ever differ by move cost.
77
+ const m = mk(2);
78
+ await m.ingest([
79
+ ["The Mona Lisa was painted by Leonardo da Vinci.", "Leonardo da Vinci"],
80
+ ["The Starry Night was painted by Vincent van Gogh.", "Vincent van Gogh"],
81
+ [
82
+ "The Night Watch was painted by Rembrandt van Rijn.",
83
+ "Rembrandt van Rijn",
84
+ ],
85
+ ["Pablo Picasso", "Pablo Picasso co-founded the Cubist movement"],
86
+ ["Michelangelo", "Il Divino painted the Sistine Chapel ceiling"],
87
+ ["a nickname meaning the divine one", "Michelangelo"],
88
+ ["a nickname meaning the divine one", "Il Divino"],
89
+ ["also called", "Michelangelo"],
90
+ ["also called", "Il Divino"],
91
+ ["known as", "Michelangelo"],
92
+ ["known as", "Il Divino"],
93
+ ["Il Divino", "Il Divino was a nickname coined by his contemporaries"],
94
+ ]);
95
+ const { r, steps } = await trace(
96
+ m,
97
+ "The Weeping Woman was painted by Pablo Picasso.",
98
+ );
99
+ // The correct answer is still reachable — the fix must not cost accuracy.
100
+ const got = new TextDecoder().decode(r.bytes).replace(/\0/g, "");
101
+ assert.ok(got.includes("Cubist"), `expected the Picasso fact, got "${got}"`);
102
+
103
+ const decide = steps.find((s) => s.mechanism.at(-1) === "decideGrounding");
104
+ const castWeights = decide.inputs
105
+ .filter((i) => i.role.startsWith("cast"))
106
+ .map((i) => Number(i.role.match(/weight ([\d.]+)/)[1]));
107
+ assert.ok(
108
+ castWeights.length >= 2,
109
+ `expected at least two CAST candidates, got ${castWeights.length}`,
110
+ );
111
+ const spread = Math.max(...castWeights) - Math.min(...castWeights);
112
+ assert.ok(
113
+ spread > 100,
114
+ `CAST candidates' weights must diverge by more than a fixed move-cost ` +
115
+ `offset (≤ ~12) when their accounted spans genuinely differ — got ` +
116
+ `weights ${castWeights} (spread ${spread})`,
117
+ );
118
+ await m.store.close();
119
+ });
120
+
121
+ test("2 — decideGrounding surfaces each candidate's unexplained label", async () => {
122
+ const m = mk(7);
123
+ await m.ingest([
124
+ ["ice is cold so ice is brittle", "brittle"],
125
+ ["steel is hard so steel is strong", "strong"],
126
+ ["water is frigid so water is freezing", "freezing"],
127
+ ]);
128
+ const { steps } = await trace(m, "steel is frigid so steel is ???");
129
+ const decide = steps.find((s) => s.mechanism.at(-1) === "decideGrounding");
130
+ assert.ok(decide, "no decideGrounding step");
131
+ assert.ok(
132
+ decide.inputs.some((i) => i.role.includes("unexplained:")),
133
+ "at least one candidate must carry a diagnostic unexplained label",
134
+ );
135
+ await m.store.close();
136
+ });
137
+
138
+ test("3 — a near-tie between the winner and runner-up emits narrowDecision", async () => {
139
+ const m = mk(7);
140
+ await m.ingest([
141
+ ["ice is cold so ice is brittle", "brittle"],
142
+ ["steel is hard so steel is strong", "strong"],
143
+ ["water is frigid so water is freezing", "freezing"],
144
+ ]);
145
+ const { steps } = await trace(m, "steel is frigid so steel is ???");
146
+ const narrow = stepsNamed(steps, "narrowDecision");
147
+ assert.equal(narrow.length, 1, "expected exactly one narrowDecision step");
148
+ assert.ok(/margin \d+ grade-unit/.test(narrow[0].note), narrow[0].note);
149
+ assert.equal(narrow[0].inputs.length, 1);
150
+ assert.equal(narrow[0].outputs.length, 1);
151
+ await m.store.close();
152
+ });
153
+
154
+ test("3b — a clean, well-separated decision does NOT emit narrowDecision", async () => {
155
+ const m = mk(7);
156
+ await m.ingest([["ice", "cold"], ["fire", "hot"]]);
157
+ const { steps } = await trace(m, "ice");
158
+ assert.equal(
159
+ stepsNamed(steps, "narrowDecision").length,
160
+ 0,
161
+ "a single, uncontested grounding must not be flagged narrow",
162
+ );
163
+ await m.store.close();
164
+ });
165
+
166
+ test("4 — a thin winning candidate is flagged thinGrounding", async () => {
167
+ const m = mk(42);
168
+ const SYS = "You are a helpful and harmless assistant.\n\n" +
169
+ "You are not allowed to use any tools.\n";
170
+ await m.ingest([
171
+ [
172
+ SYS + "Describe the importance of gender equality in the workplace.",
173
+ "Gender equality means equal chances to be hired and promoted.",
174
+ ],
175
+ [
176
+ SYS + "Provide a summary of the 1992 Dream Team basketball squad.",
177
+ "The Dream Team won gold in Barcelona in 1992.",
178
+ ],
179
+ [
180
+ SYS + "Explain how photosynthesis converts sunlight into energy.",
181
+ "Photosynthesis binds carbon dioxide and water into sugar.",
182
+ ],
183
+ [
184
+ SYS + "Summarize the causes of the fall of the Western Roman Empire.",
185
+ "The Western Roman Empire fell from overreach and migration.",
186
+ ],
187
+ ]);
188
+ const { r, steps } = await trace(
189
+ m,
190
+ SYS +
191
+ "Tell me about gender equality at work, and also the 1992 Dream Team.",
192
+ );
193
+ assert.ok(r.v !== null, "the query must still ground an answer");
194
+ const thin = stepsNamed(steps, "thinGrounding");
195
+ assert.equal(thin.length, 1, "a low-density grounding must be flagged thin");
196
+ assert.ok(/density [\d.]+ is below 1\/W/.test(thin[0].note));
197
+ await m.store.close();
198
+ });
199
+
200
+ test("4b — a fully-covered answer is NOT flagged thinGrounding", async () => {
201
+ const m = mk(7);
202
+ await m.ingest([["what is ice?", "ice is frozen water"]]);
203
+ const { r, steps } = await trace(m, "what is ice?");
204
+ assert.ok(r.v !== null);
205
+ assert.equal(stepsNamed(steps, "thinGrounding").length, 0);
206
+ await m.store.close();
207
+ });
208
+
209
+ test("5 — CAST is skipped by a dynamic floor when the climb can't support a weave", async () => {
210
+ const m = mk(7);
211
+ await m.ingest([["ice", "cold"], ["fire", "hot"]]);
212
+ const { steps } = await trace(m, "ice");
213
+ const skip = stepsNamed(steps, "skipMechanism");
214
+ assert.ok(
215
+ skip.some((s) => /cast skipped/.test(s.note)),
216
+ `expected a cast skipMechanism step, got: ${skip.map((s) => s.note)}`,
217
+ );
218
+ // And no CAST schema should have run at all — the floor pruned it
219
+ // BEFORE counterfactualTransfer's own alignment work.
220
+ assert.equal(stepsNamed(steps, "castSchema").length, 0);
221
+ await m.store.close();
222
+ });
@@ -0,0 +1,252 @@
1
+ // 34-cross-region.test.mjs — DIRECT REGION-TO-REGION INTERACTION (binding).
2
+ //
3
+ // Sema's attention lets each region of the query vote INDEPENDENTLY for the
4
+ // context it climbs to, then POOLS the votes additively (poolVotes). Additive
5
+ // pooling is a soft conjunction: when two regions each cast a vote that
6
+ // includes a shared context, that context accumulates and wins. But pooling
7
+ // can only ever surface a context that at least ONE region already votes for.
8
+ //
9
+ // The gap this exercises — the "binding problem": a context that NO single
10
+ // region votes for, reachable only by letting two regions interact DIRECTLY.
11
+ // With cross-cutting attributes (colour × shape → answer), each attribute
12
+ // alone is ambiguous across two contexts, and — crucially — each region's own
13
+ // resonance climbs to a DIFFERENT context than the joint one. So the joint
14
+ // context receives zero independent votes and additive pooling can never
15
+ // reach it; only composing the two regions can.
16
+ //
17
+ // red circle → alpha red square → beta
18
+ // blue circle → gamma blue square → delta
19
+ //
20
+ // "red" alone attends to `red square`; "circle" alone attends to `circle`.
21
+ // Neither reaches `red circle`. A query naming both, non-adjacently
22
+ // ("red then circle"), MUST attend to `red circle` — and that requires the
23
+ // two regions to interact.
24
+ //
25
+ // ── Why these assertions are on climbAttention, not respondText ────────────
26
+ // The mechanism lives IN attention: its contract is the set of contexts the
27
+ // query attends to. Asserting there tests the mechanism itself, and — unlike
28
+ // an end-to-end respondText assertion — the assertion FLIPS with the
29
+ // mechanism: without region interaction the query provably attends to the
30
+ // wrong context (`red square`), with it to the joint one (`red circle`).
31
+ // (End-to-end voicing of a scattered-attribute query is a separate
32
+ // articulation concern and is deliberately not conflated here.)
33
+
34
+ import { test } from "node:test";
35
+ import assert from "node:assert/strict";
36
+ import { Mind } from "../dist/src/index.js";
37
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
38
+
39
+ const enc = (s) => new TextEncoder().encode(s);
40
+ const dec = (b) =>
41
+ new TextDecoder().decode(b.filter((x) => x !== 0)).replace(/\s+/g, " ")
42
+ .trim();
43
+
44
+ // Attributes are trained as standalone forms too, so each is RECOGNISED as a
45
+ // whole-form region (a site) in the query — otherwise a 6-byte word like
46
+ // "circle" fragments across the 4-byte chunk grid and never forms one clean
47
+ // region to compose with.
48
+ const CORPUS = [
49
+ ["red", "is a color"],
50
+ ["blue", "is a color"],
51
+ ["circle", "is a shape"],
52
+ ["square", "is a shape"],
53
+ ["red circle", "answer alpha"],
54
+ ["red square", "answer beta"],
55
+ ["blue circle", "answer gamma"],
56
+ ["blue square", "answer delta"],
57
+ ];
58
+
59
+ const mk = (seed) =>
60
+ new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
61
+
62
+ /** The contexts the query attends to, most-dominant first, as text. */
63
+ async function attends(m, q) {
64
+ const roots = await m.climbAttention(enc(q), 24);
65
+ return roots.map((r) => dec(m.store.bytesPrefix(r.anchor, 1e9)));
66
+ }
67
+
68
+ // The mechanism is content-addressed and therefore seed-independent; running
69
+ // several seeds guards against a fragile (seed-sensitive) approximation
70
+ // passing by luck.
71
+ const SEEDS = [1, 2, 3, 7, 11];
72
+
73
+ // ═══════════════════════════════════════════════════════════════════════════
74
+ // THE NEED — no single region reaches the joint context.
75
+ //
76
+ // If either attribute alone already attended to `red circle`, additive pooling
77
+ // would suffice and no interaction would be needed. It does not: each single
78
+ // attribute climbs elsewhere. This is what makes the binding query below
79
+ // UNREACHABLE without direct region interaction.
80
+ // ═══════════════════════════════════════════════════════════════════════════
81
+
82
+ test("need: neither attribute alone attends to the joint context", async () => {
83
+ for (const seed of SEEDS) {
84
+ const m = mk(seed);
85
+ await m.ingest(CORPUS);
86
+ const red = await attends(m, "red");
87
+ const circle = await attends(m, "circle");
88
+ assert.ok(
89
+ !red.includes("red circle"),
90
+ `seed ${seed}: "red" alone must not reach the joint context, got [${red}]`,
91
+ );
92
+ assert.ok(
93
+ !circle.includes("red circle"),
94
+ `seed ${seed}: "circle" alone must not reach the joint context, got [${circle}]`,
95
+ );
96
+ await m.store.close();
97
+ }
98
+ });
99
+
100
+ // ═══════════════════════════════════════════════════════════════════════════
101
+ // THE REQUIREMENT — the binding query attends to the joint context.
102
+ //
103
+ // "red" and "circle" fall into non-adjacent regions; each individually climbs
104
+ // to a different context (`red square`, `circle`). Only by composing the two
105
+ // regions does the JOINT context `red circle` become a point of attention.
106
+ // ═══════════════════════════════════════════════════════════════════════════
107
+
108
+ test("binding: two non-adjacent attributes attend to their JOINT context", async () => {
109
+ for (const seed of SEEDS) {
110
+ const m = mk(seed);
111
+ await m.ingest(CORPUS);
112
+ const got = await attends(m, "red then circle");
113
+ assert.equal(
114
+ got[0],
115
+ "red circle",
116
+ `seed ${seed}: "red then circle" must attend to the JOINT context ` +
117
+ `"red circle" (the only fact with BOTH attributes), got [${got}]`,
118
+ );
119
+ await m.store.close();
120
+ }
121
+ });
122
+
123
+ test("binding is symmetric: the other cross-cut also composes", async () => {
124
+ for (const seed of SEEDS) {
125
+ const m = mk(seed);
126
+ await m.ingest(CORPUS);
127
+ const got = await attends(m, "blue then square");
128
+ assert.equal(
129
+ got[0],
130
+ "blue square",
131
+ `seed ${seed}: "blue then square" must attend to "blue square", got [${got}]`,
132
+ );
133
+ await m.store.close();
134
+ }
135
+ });
136
+
137
+ // ═══════════════════════════════════════════════════════════════════════════
138
+ // WRONG-WORLD GUARD — composition must not manufacture a context the two
139
+ // attributes do NOT jointly evidence. "red then circle" shares "circle" with
140
+ // the blue world, but nothing red-and-blue was ever learnt together: the joint
141
+ // evidence is for the RED circle, never a blue one.
142
+ // ═══════════════════════════════════════════════════════════════════════════
143
+
144
+ // ═══════════════════════════════════════════════════════════════════════════
145
+ // ORDER-FREEDOM — a junction evidences that two forms were LEARNT TOGETHER;
146
+ // which one the query happens to mention first is a fact about the query, not
147
+ // about the learnt whole. "circle then red" reverses the stored order of
148
+ // `red circle` and must still compose to it.
149
+ // ═══════════════════════════════════════════════════════════════════════════
150
+
151
+ test("binding is order-free: reversed mention still composes", async () => {
152
+ for (const seed of SEEDS) {
153
+ const m = mk(seed);
154
+ await m.ingest(CORPUS);
155
+ const got = await attends(m, "circle then red");
156
+ assert.equal(
157
+ got[0],
158
+ "red circle",
159
+ `seed ${seed}: "circle then red" must attend to "red circle" even ` +
160
+ `though the query reverses the stored order, got [${got}]`,
161
+ );
162
+ await m.store.close();
163
+ }
164
+ });
165
+
166
+ // ═══════════════════════════════════════════════════════════════════════════
167
+ // N-ARY BINDING — binding is not intrinsically pairwise. With THREE
168
+ // cross-cutting attributes every PAIR is still ambiguous (each pair appears in
169
+ // two contexts); only the triple picks out one. A pairwise-only mechanism
170
+ // tops out at the two-context pair container; n-ary selection must reach the
171
+ // unique triple.
172
+ // ═══════════════════════════════════════════════════════════════════════════
173
+
174
+ const CORPUS3 = [
175
+ ["red", "is a color"],
176
+ ["blue", "is a color"],
177
+ ["big", "is a size"],
178
+ ["small", "is a size"],
179
+ ["circle", "is a shape"],
180
+ ["square", "is a shape"],
181
+ ["red big circle", "answer a"],
182
+ ["red big square", "answer b"],
183
+ ["red small circle", "answer c"],
184
+ ["red small square", "answer d"],
185
+ ["blue big circle", "answer e"],
186
+ ["blue big square", "answer f"],
187
+ ["blue small circle", "answer g"],
188
+ ["blue small square", "answer h"],
189
+ ];
190
+
191
+ test("binding is n-ary: three cross-cutting attributes reach their unique triple", async () => {
192
+ for (const seed of SEEDS) {
193
+ const m = mk(seed);
194
+ await m.ingest(CORPUS3);
195
+ const got = await attends(m, "red then big then circle");
196
+ assert.equal(
197
+ got[0],
198
+ "red big circle",
199
+ `seed ${seed}: "red then big then circle" must attend to the unique ` +
200
+ `TRIPLE context "red big circle" (every pair is ambiguous), got [${got}]`,
201
+ );
202
+ await m.store.close();
203
+ }
204
+ });
205
+
206
+ // ═══════════════════════════════════════════════════════════════════════════
207
+ // CORPUS INDEPENDENCE — binding must not require attributes to have been
208
+ // TRAINED STANDALONE. Without the standalone rows no attribute is a
209
+ // recognised site: "circle" fragments across the 4-byte chunk grid. But the
210
+ // fragments are stored chunks that vote, and their BYTES still compose — the
211
+ // junction ascent matches byte containment, so the fragment pair evidences
212
+ // the same joint container the whole word would. (This also exercises
213
+ // explaining-away: the grid shard " cir" exists verbatim only in `blue
214
+ // circle` — pure alignment accident — and its aliased vote must be
215
+ // superseded by the exact joint evidence, not left to outvote it.)
216
+ // ═══════════════════════════════════════════════════════════════════════════
217
+
218
+ const CORPUS_FACTS_ONLY = [
219
+ ["red circle", "answer alpha"],
220
+ ["red square", "answer beta"],
221
+ ["blue circle", "answer gamma"],
222
+ ["blue square", "answer delta"],
223
+ ];
224
+
225
+ test("corpus independence: binding composes from grid fragments, no standalone training", async () => {
226
+ for (const seed of SEEDS) {
227
+ const m = mk(seed);
228
+ await m.ingest(CORPUS_FACTS_ONLY);
229
+ const got = await attends(m, "red then circle");
230
+ assert.equal(
231
+ got[0],
232
+ "red circle",
233
+ `seed ${seed}: without standalone attribute rows, "red then circle" ` +
234
+ `must still attend to "red circle" via fragment composition, got [${got}]`,
235
+ );
236
+ await m.store.close();
237
+ }
238
+ });
239
+
240
+ test("guard: binding does not leak into a non-jointly-evidenced world", async () => {
241
+ for (const seed of SEEDS) {
242
+ const m = mk(seed);
243
+ await m.ingest(CORPUS);
244
+ const got = await attends(m, "red then circle");
245
+ assert.ok(
246
+ !got.some((c) => c.startsWith("blue")),
247
+ `seed ${seed}: "red then circle" must not attend to any blue-world ` +
248
+ `context, got [${got}]`,
249
+ );
250
+ await m.store.close();
251
+ }
252
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "rootDir": ".",
7
+ "outDir": "dist",
8
+ "declaration": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*.ts", "example/**/*.ts"],
15
+ "exclude": ["dist", "node_modules"]
16
+ }