@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.
- package/AGENTS.md +470 -0
- package/AUTHORS.md +12 -0
- package/COMMERCIAL-LICENSE.md +19 -0
- package/CONTRIBUTING.md +15 -0
- package/HOW_IT_WORKS.md +4210 -0
- package/LICENSE.md +117 -0
- package/README.md +290 -0
- package/TRADEMARKS.md +19 -0
- package/example/demo.ts +46 -0
- package/example/train_base.ts +2675 -0
- package/index.html +893 -0
- package/package.json +25 -0
- package/src/alphabet.ts +34 -0
- package/src/alu/README.md +332 -0
- package/src/alu/src/alu.ts +541 -0
- package/src/alu/src/expr.ts +339 -0
- package/src/alu/src/index.ts +115 -0
- package/src/alu/src/kernel-arith.ts +377 -0
- package/src/alu/src/kernel-bits.ts +199 -0
- package/src/alu/src/kernel-logic.ts +102 -0
- package/src/alu/src/kernel-nd.ts +235 -0
- package/src/alu/src/kernel-numeric.ts +466 -0
- package/src/alu/src/operation.ts +344 -0
- package/src/alu/src/parser.ts +574 -0
- package/src/alu/src/resonance.ts +161 -0
- package/src/alu/src/text.ts +83 -0
- package/src/alu/src/value.ts +327 -0
- package/src/alu/test/alu.test.ts +1004 -0
- package/src/bytes.ts +62 -0
- package/src/config.ts +227 -0
- package/src/derive/README.md +295 -0
- package/src/derive/src/deduction.ts +263 -0
- package/src/derive/src/index.ts +24 -0
- package/src/derive/src/priority-queue.ts +70 -0
- package/src/derive/src/rewrite.ts +127 -0
- package/src/derive/src/trie.ts +242 -0
- package/src/derive/test/derive.test.ts +137 -0
- package/src/extension.ts +42 -0
- package/src/geometry.ts +511 -0
- package/src/index.ts +38 -0
- package/src/ingest-cache.ts +224 -0
- package/src/mind/articulation.ts +137 -0
- package/src/mind/attention.ts +1061 -0
- package/src/mind/canonical.ts +107 -0
- package/src/mind/graph-search.ts +1100 -0
- package/src/mind/index.ts +18 -0
- package/src/mind/junction.ts +347 -0
- package/src/mind/learning.ts +246 -0
- package/src/mind/match.ts +507 -0
- package/src/mind/mechanisms/alu.ts +33 -0
- package/src/mind/mechanisms/cast.ts +568 -0
- package/src/mind/mechanisms/confluence.ts +268 -0
- package/src/mind/mechanisms/cover.ts +248 -0
- package/src/mind/mechanisms/extraction.ts +422 -0
- package/src/mind/mechanisms/recall.ts +241 -0
- package/src/mind/mind.ts +483 -0
- package/src/mind/pipeline-mechanism.ts +326 -0
- package/src/mind/pipeline.ts +300 -0
- package/src/mind/primitives.ts +201 -0
- package/src/mind/rationale.ts +275 -0
- package/src/mind/reasoning.ts +198 -0
- package/src/mind/recognition.ts +234 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/trace.ts +108 -0
- package/src/mind/traverse.ts +556 -0
- package/src/mind/types.ts +281 -0
- package/src/rabitq-hnsw/README.md +303 -0
- package/src/rabitq-hnsw/src/database.ts +492 -0
- package/src/rabitq-hnsw/src/heap.ts +90 -0
- package/src/rabitq-hnsw/src/hnsw.ts +514 -0
- package/src/rabitq-hnsw/src/index.ts +15 -0
- package/src/rabitq-hnsw/src/prng.ts +39 -0
- package/src/rabitq-hnsw/src/rabitq.ts +308 -0
- package/src/rabitq-hnsw/src/store.ts +994 -0
- package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
- package/src/store-sqlite.ts +928 -0
- package/src/store.ts +2148 -0
- package/src/vec.ts +124 -0
- package/test/00-extract.test.mjs +151 -0
- package/test/01-floor.test.mjs +20 -0
- package/test/02-roundtrip.test.mjs +83 -0
- package/test/03-recall.test.mjs +98 -0
- package/test/04-think.test.mjs +627 -0
- package/test/05-concepts.test.mjs +84 -0
- package/test/08-storage.test.mjs +948 -0
- package/test/09-edges.test.mjs +65 -0
- package/test/11-universality.test.mjs +180 -0
- package/test/13-conversation.test.mjs +228 -0
- package/test/14-scaling.test.mjs +503 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/16-bridge.test.mjs +250 -0
- package/test/17-intelligence.test.mjs +240 -0
- package/test/18-alu.test.mjs +209 -0
- package/test/19-nd.test.mjs +254 -0
- package/test/20-stability.test.mjs +489 -0
- package/test/21-partial.test.mjs +158 -0
- package/test/22-multihop.test.mjs +130 -0
- package/test/23-rationale.test.mjs +316 -0
- package/test/24-generalization.test.mjs +416 -0
- package/test/25-embedding.test.mjs +75 -0
- package/test/26-cooperative.test.mjs +89 -0
- package/test/27-saturation-drop.test.mjs +637 -0
- package/test/28-unknowable.test.mjs +88 -0
- package/test/29-counterfactual.test.mjs +476 -0
- package/test/30-conflict-resolution.test.mjs +166 -0
- package/test/31-audit.test.mjs +288 -0
- package/test/32-confluence.test.mjs +173 -0
- package/test/33-multi-candidate.test.mjs +222 -0
- package/test/34-cross-region.test.mjs +252 -0
- 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
|
+
}
|