@hviana/sema 0.9.0 → 0.9.2

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 (118) hide show
  1. package/AGENTS.md +7 -7
  2. package/dist/src/alu/src/index.d.ts +1 -1
  3. package/dist/src/alu/src/index.js +1 -1
  4. package/dist/src/alu/src/parser.js +2 -6
  5. package/dist/src/alu/src/resonance.d.ts +13 -0
  6. package/dist/src/alu/src/resonance.js +41 -0
  7. package/dist/src/alu/test/alu.test.js +39 -0
  8. package/dist/src/bytes.d.ts +6 -2
  9. package/dist/src/bytes.js +10 -4
  10. package/dist/src/canon.js +44 -0
  11. package/dist/src/geometry.d.ts +19 -1
  12. package/dist/src/geometry.js +125 -141
  13. package/dist/src/meter.d.ts +33 -0
  14. package/dist/src/meter.js +34 -1
  15. package/dist/src/mind/articulation.js +14 -1
  16. package/dist/src/mind/attention.d.ts +12 -0
  17. package/dist/src/mind/attention.js +44 -16
  18. package/dist/src/mind/bridge.js +3 -3
  19. package/dist/src/mind/derivation.d.ts +40 -0
  20. package/dist/src/mind/derivation.js +34 -0
  21. package/dist/src/mind/evidence.d.ts +24 -0
  22. package/dist/src/mind/evidence.js +90 -0
  23. package/dist/src/mind/graph-search.d.ts +89 -15
  24. package/dist/src/mind/graph-search.js +345 -174
  25. package/dist/src/mind/learning.js +1 -1
  26. package/dist/src/mind/mechanisms/cover.d.ts +19 -3
  27. package/dist/src/mind/mechanisms/cover.js +142 -61
  28. package/dist/src/mind/mechanisms/recall.js +10 -3
  29. package/dist/src/mind/mind.d.ts +6 -0
  30. package/dist/src/mind/mind.js +5 -2
  31. package/dist/src/mind/pipeline.d.ts +5 -1
  32. package/dist/src/mind/pipeline.js +220 -90
  33. package/dist/src/mind/primitives.d.ts +25 -5
  34. package/dist/src/mind/primitives.js +107 -44
  35. package/dist/src/mind/reasoning.d.ts +18 -4
  36. package/dist/src/mind/reasoning.js +487 -328
  37. package/dist/src/mind/recognition.js +29 -13
  38. package/dist/src/mind/resonance.js +1 -11
  39. package/dist/src/mind/traverse.d.ts +45 -5
  40. package/dist/src/mind/traverse.js +285 -8
  41. package/dist/src/mind/types.d.ts +16 -1
  42. package/dist/src/store-sqlite.d.ts +25 -0
  43. package/dist/src/store-sqlite.js +89 -1
  44. package/dist/src/store.d.ts +48 -4
  45. package/dist/src/store.js +86 -6
  46. package/docs/INDEX.md +20 -19
  47. package/docs/INVARIANTS.md +17 -16
  48. package/docs/architecture/bounded-reads.md +1 -1
  49. package/docs/architecture/caches.md +5 -4
  50. package/docs/architecture/closure.md +45 -5
  51. package/docs/architecture/cost-model.md +16 -0
  52. package/docs/architecture/evidence.md +113 -0
  53. package/docs/architecture/exact-vs-approximate.md +10 -9
  54. package/docs/architecture/factored-machinery.md +14 -13
  55. package/docs/architecture/fold-contract.md +51 -1
  56. package/docs/architecture/mechanism-market.md +21 -0
  57. package/docs/architecture/memoization.md +3 -3
  58. package/docs/architecture/meter.md +2 -1
  59. package/docs/architecture/saturation.md +12 -0
  60. package/docs/architecture/store.md +25 -2
  61. package/docs/failures/tempting-but-wrong.md +13 -2
  62. package/docs/harness/gates.md +12 -10
  63. package/docs/mechanisms/cover.md +23 -6
  64. package/jsr.json +1 -1
  65. package/package.json +1 -1
  66. package/src/alu/README.md +10 -2
  67. package/src/alu/src/index.ts +1 -0
  68. package/src/alu/src/parser.ts +6 -6
  69. package/src/alu/src/resonance.ts +42 -0
  70. package/src/alu/test/alu.test.ts +40 -0
  71. package/src/bytes.ts +13 -3
  72. package/src/canon.ts +40 -0
  73. package/src/geometry.ts +183 -154
  74. package/src/meter.ts +34 -1
  75. package/src/mind/articulation.ts +14 -2
  76. package/src/mind/attention.ts +47 -25
  77. package/src/mind/bridge.ts +3 -3
  78. package/src/mind/derivation.ts +77 -0
  79. package/src/mind/evidence.ts +107 -0
  80. package/src/mind/graph-search.ts +449 -221
  81. package/src/mind/learning.ts +1 -7
  82. package/src/mind/match.ts +1 -2
  83. package/src/mind/mechanisms/cast.ts +1 -2
  84. package/src/mind/mechanisms/cover.ts +207 -87
  85. package/src/mind/mechanisms/extraction.ts +1 -2
  86. package/src/mind/mechanisms/prefix-completion.ts +1 -1
  87. package/src/mind/mechanisms/recall.ts +17 -5
  88. package/src/mind/mechanisms/reference.ts +1 -1
  89. package/src/mind/mind.ts +9 -30
  90. package/src/mind/pipeline.ts +263 -104
  91. package/src/mind/primitives.ts +119 -43
  92. package/src/mind/reasoning.ts +611 -419
  93. package/src/mind/recognition.ts +24 -9
  94. package/src/mind/resonance.ts +2 -16
  95. package/src/mind/trace.ts +1 -1
  96. package/src/mind/traverse.ts +321 -8
  97. package/src/mind/types.ts +15 -11
  98. package/src/store-sqlite.ts +92 -1
  99. package/src/store.ts +113 -7
  100. package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
  101. package/test/106-the-join-fires.test.mjs +21 -0
  102. package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
  103. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
  104. package/test/136-the-two-named-limits.test.mjs +3 -2
  105. package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
  106. package/test/148-exact-shortcuts-agree.test.mjs +188 -0
  107. package/test/149-the-closure-engine.test.mjs +138 -0
  108. package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
  109. package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
  110. package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
  111. package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
  112. package/test/154-the-question-names-the-step.test.mjs +281 -0
  113. package/test/24-generalization.test.mjs +32 -0
  114. package/test/36-bloom.test.mjs +53 -0
  115. package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
  116. package/test/48-recognise-turn-connective.test.mjs +3 -2
  117. package/test/55-cost-meter.test.mjs +4 -4
  118. package/test/90-connector-read-cap.test.mjs +7 -7
@@ -0,0 +1,155 @@
1
+ // 153-a-cheaper-bound-is-looked-at-first.test.mjs — the market and the span
2
+ // probe do the same work for less.
3
+ //
4
+ // THE MARKET. Mechanisms run in declared order and are pruned by the
5
+ // incumbent — but a mechanism floored LOWER than one about to invest, declared
6
+ // after it, could not prune it. Measured on the 31.7M-node store: a query that
7
+ // opens a trained form (#114 of the battery) was won by prefix-completion at
8
+ // grade 1 after CAST (floor grade 2) had paid the consensus climb and the weave,
9
+ // and confluence, extraction and reference their reads. The grounding loop now
10
+ // runs such a mechanism AHEAD (pipeline.ts, "a cheaper bound is looked at before
11
+ // a dearer one is paid for") and skips what cannot beat the grade it reaches.
12
+ //
13
+ // THE SPAN PROBE. Recognition's interior pass probes every span of up to
14
+ // `reach` bytes past each endpoint, and each probe hashed its span from the
15
+ // start — O(n·reach²) bytes (251,660,406 for one response on that store).
16
+ // `Store.flatSpans` extends the hash a start was last probed at instead.
17
+ //
18
+ // Pinned:
19
+ // 153.1 a mechanism floored below CAST's, run ahead, skips CAST and the rest
20
+ // of the composition market — and the weave is never paid for;
21
+ // 153.2 the decision is the declared order's: the same answer, from the same
22
+ // mechanism, as a market whose bounds are hidden from the run-ahead;
23
+ // 153.3 a bound that does not undercut the dearer floor runs nothing ahead:
24
+ // a query the cover grounds whole runs the cover alone;
25
+ // 153.4 the span prober answers exactly `findFlatBranch` over the same span,
26
+ // in every order a scanner can ask in.
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 FORM =
34
+ "What time does the movie start? We would need to start between 7-7:30pm";
35
+ const QUERY = "What time does the movie start?";
36
+
37
+ async function fixture() {
38
+ const mind = new Mind({
39
+ seed: 7,
40
+ profile: true,
41
+ store: new SQliteStore({ path: ":memory:" }),
42
+ });
43
+ await mind.ingest([
44
+ [FORM, "Sure, let me check the showtimes."],
45
+ ["Do you like thrillers?", "Yes, I love them."],
46
+ ["The movie was great.", "I agree."],
47
+ ]);
48
+ return mind;
49
+ }
50
+
51
+ /** The decision a market reaches when no mechanism can be run ahead: every
52
+ * floor answers the run-ahead's refusing probe with 0, which no later floor
53
+ * undercuts — so the loop is the plain declared-order loop. (A real
54
+ * `worthRunning` accepts a floor of −∞; only the refusing probe declines it.) */
55
+ function declaredOrder(mind) {
56
+ for (let i = 0; i < mind.mechanisms.length; i++) {
57
+ const m = mind.mechanisms[i];
58
+ mind.mechanisms[i] = {
59
+ ...m,
60
+ floor: (ctx, q, pre, worth) =>
61
+ worth(-Infinity) ? m.floor(ctx, q, pre, worth) : Promise.resolve(0),
62
+ run: m.run.bind(m),
63
+ };
64
+ }
65
+ }
66
+
67
+ /** The winning candidate's provenance, read off the rationale. */
68
+ async function decide(mind, q) {
69
+ let winner = null;
70
+ const answer = await mind.respondText(q, (s) => {
71
+ if (s.mechanism.at(-1) === "decideGrounding") {
72
+ winner = s.data.candidates.find((c) => c.decided);
73
+ }
74
+ });
75
+ return { answer, winner };
76
+ }
77
+
78
+ test("153.1 a cheaper bound run ahead skips the composition market", async () => {
79
+ const mind = await fixture();
80
+ const answer = await mind.respondText(QUERY);
81
+ const c = mind.lastCost.counters;
82
+ const phases = mind.lastCost.phases;
83
+ assert.equal(answer, "Sure, let me check the showtimes.");
84
+ assert.ok(
85
+ c.mechanismsBounded >= 4,
86
+ `cast, confluence, extraction and reference are bounded out: ${c.mechanismsBounded}`,
87
+ );
88
+ assert.equal(phases["cast.run"], undefined, "CAST never runs");
89
+ assert.equal(phases["weave"], undefined, "the weave is never paid for");
90
+ await mind.store.close();
91
+ });
92
+
93
+ test("153.2 the decision is the declared order's", async () => {
94
+ const ahead = await fixture();
95
+ const plain = await fixture();
96
+ declaredOrder(plain);
97
+ const a = await decide(ahead, QUERY);
98
+ const p = await decide(plain, QUERY);
99
+ assert.equal(a.answer, p.answer);
100
+ assert.equal(a.winner.provenance, p.winner.provenance);
101
+ assert.equal(a.winner.weight, p.winner.weight);
102
+ // …and the oracle really is the dearer market: there, CAST paid for the
103
+ // weave, and confluence ran.
104
+ assert.ok(plain.lastCost.phases["weave"] !== undefined);
105
+ assert.ok(plain.lastCost.phases["confluence.run"] !== undefined);
106
+ assert.equal(ahead.lastCost.phases["confluence.run"], undefined);
107
+ await ahead.store.close();
108
+ await plain.store.close();
109
+ });
110
+
111
+ test("153.3 a cover that grounds whole runs nothing ahead", async () => {
112
+ const mind = await fixture();
113
+ assert.equal(
114
+ await mind.respondText("Do you like thrillers?"),
115
+ "Yes, I love them.",
116
+ );
117
+ const c = mind.lastCost.counters;
118
+ assert.equal(c.mechanismRuns, 1, "the cover alone");
119
+ assert.equal(c.mechanismsBounded ?? 0, 0);
120
+ await mind.store.close();
121
+ });
122
+
123
+ test("153.4 the span prober answers exactly findFlatBranch", async () => {
124
+ const mind = await fixture();
125
+ const store = mind.store;
126
+ const bytes = new TextEncoder().encode(
127
+ `${FORM} Do you like thrillers? The movie was great. ${QUERY}`,
128
+ );
129
+ const n = bytes.length;
130
+ const want = (s, e) => store.findFlatBranch(bytes.subarray(s, e));
131
+ const probe = store.flatSpans(bytes);
132
+ let hits = 0, asked = 0;
133
+ const check = (s, e) => {
134
+ const got = probe(s, e);
135
+ assert.equal(got, want(s, e), `span [${s},${e})`);
136
+ asked++;
137
+ if (got !== null) hits++;
138
+ };
139
+ // Ends ascending per start (the extension path)…
140
+ for (let e = 1; e <= n; e++) {
141
+ for (let s = Math.max(0, e - 40); s < e; s++) {
142
+ check(s, e);
143
+ }
144
+ }
145
+ // …a suffix then its one-byte trim (the short path), and revisits that end
146
+ // BEFORE the last probe of their start (the rehash path).
147
+ for (let s = n - 1; s >= 0; s--) {
148
+ check(s, n);
149
+ check(s, n - 1 > s ? n - 1 : n);
150
+ check(s, Math.min(n, s + 3));
151
+ }
152
+ assert.ok(asked > 5_000, `probes asked: ${asked}`);
153
+ assert.ok(hits > 50, `the buffer must hold stored flat spans: ${hits}`);
154
+ await store.close();
155
+ });
@@ -0,0 +1,281 @@
1
+ // 154-the-question-names-the-step.test.mjs — a stored structure is identified
2
+ // by the material at hand, wherever the pieces of that material came from.
3
+ //
4
+ // THE OPERATION (src/mind/evidence.ts). A stored form is WITNESSED by some
5
+ // material when every one of its bytes lies in a W-window the material holds —
6
+ // in any order, at any place. The corpus writes down, for every continuation,
7
+ // the questions that establish it (its predecessors). A continuation is NAMED
8
+ // when one of those questions is witnessed by the asker's bytes plus the node
9
+ // being followed.
10
+ //
11
+ // THE DEFECTS IT CLOSES, measured on the 31.7M-node store and on 5,236 held-out
12
+ // 2WikiMultihopQA compositional questions deposited exactly as the trainer
13
+ // deposits their evidence triples (example/train_base/corpora/wiki2.ts):
14
+ // • the forward choice among a node's continuations read popularity only, so
15
+ // `Who is the father of Frederick II?` answered the most-poured of his
16
+ // eight facts (his citizenship) — the father fact's own establishing
17
+ // question `Frederick II father` lay wholly inside the question;
18
+ // • the walk consumed EVERY form recognised inside the first hop's answer, the
19
+ // entity the hop introduced included, so the chain could not step past it
20
+ // (3 pivot steps over 133 compositional questions);
21
+ // • the second hop's establishing question is held by neither the question
22
+ // (1% of the 5,236) nor the first hop's fact, only by both (41%).
23
+ //
24
+ // Pinned:
25
+ // 154.1 witnessing is order-free, complete or nothing, credits a window to the
26
+ // last source that holds it, and refuses a form below one window;
27
+ // 154.2 the continuation the question names wins over the most-poured one;
28
+ // 154.3 the second hop is named by question material plus the entity the
29
+ // first hop introduced — neither alone holds it;
30
+ // 154.4 a question that names no further step is not extended: the material
31
+ // that named the first hop cannot name a second;
32
+ // 154.5 a fragment (`director`, inside every `… director` question) answers
33
+ // other questions: the cover voices none of its continuations unless the
34
+ // question names one.
35
+
36
+ import { test } from "node:test";
37
+ import assert from "node:assert/strict";
38
+ import { Mind } from "../dist/src/index.js";
39
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
40
+ import { windowIndex, witness } from "../dist/src/mind/evidence.js";
41
+
42
+ const enc = (s) => new TextEncoder().encode(s);
43
+
44
+ /** Deposit triples as the trainer does: the relation fact under both the
45
+ * subject-relation key and the bare subject (wiki2.ts). */
46
+ function deposits(triples) {
47
+ const items = [];
48
+ for (const [s, r, o] of triples) {
49
+ const fact = `The ${r} of ${s} is ${o}.`;
50
+ items.push([`${s} ${r}`, fact], [s, fact]);
51
+ }
52
+ return items;
53
+ }
54
+
55
+ const TRIPLES = [
56
+ // Distractors deposited FIRST: every relation the questions name has older,
57
+ // unrelated instances, so neither first-insertion nor popularity can land on
58
+ // the asked fact by accident.
59
+ ["Marius", "place of death", "Marseille"],
60
+ ["Jinpa", "director", "Pema Tseden"],
61
+ ["Kalippava", "place of death", "Kochi"],
62
+ ["Le Guignolo", "director", "Georges Lautner"],
63
+ // Frederick II: the citizenship fact first and repeated, so popularity and
64
+ // first-insertion both pick it.
65
+ ["Frederick II", "country of citizenship", "Holy Roman Empire"],
66
+ ["Frederick II", "place of death", "Wartburg"],
67
+ ["Frederick II", "father", "Peter III of Aragon"],
68
+ ["Peter III of Aragon", "father", "James I of Aragon"],
69
+ ["Peter III of Aragon", "place of birth", "Valencia"],
70
+ // A two-hop chain.
71
+ ["Beat Girl", "director", "Edmond T. Gréville"],
72
+ ["Edmond T. Gréville", "place of death", "Nice"],
73
+ ["Edmond T. Gréville", "place of birth", "Nice"],
74
+ ["Polish-Russian War", "director", "Xawery Żuławski"],
75
+ ["Xawery Żuławski", "mother", "Małgorzata Braunek"],
76
+ ["Juan Carlos Gumucio", "spouse", "Marie Colvin"],
77
+ ["Marie Colvin", "place of death", "Homs"],
78
+ ["Ronnie Rocket", "director", "David Lynch"],
79
+ ["Who...", "performer", "Ayumi Hamasaki"],
80
+ ["Ayumi Hamasaki", "place of birth", "Fukuoka"],
81
+ ["David Lynch", "place of birth", "Missoula, Montana"],
82
+ ];
83
+
84
+ /** Filler facts in the same template, lexically varied (the test/99 recipe):
85
+ * without them the store is too small for any window to be SCAFFOLDING, and
86
+ * ` is `, `The `, ` of ` would read as owed question material — a regime the
87
+ * trained store never is in. */
88
+ const WORDS =
89
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet " +
90
+ "kilo lima mike november oscar papa quebec romeo sierra tango uniform " +
91
+ "victor whiskey xray yankee zulu amber bronze copper dahlia ember fjord " +
92
+ "gossamer harbour indigo jasmine kestrel lantern marigold nectar opal")
93
+ .split(" ");
94
+ const RELATIONS = ["award received", "employer", "director", "performer"];
95
+ function fillers(n) {
96
+ const out = [];
97
+ for (let i = 0; i < n; i++) {
98
+ const w = (k) => WORDS[(i * 7 + k * 13) % WORDS.length];
99
+ out.push([
100
+ `${w(0)} ${w(1)}`,
101
+ RELATIONS[i % RELATIONS.length],
102
+ `${w(2)} ${w(3)}`,
103
+ ]);
104
+ }
105
+ return out;
106
+ }
107
+
108
+ async function fixture() {
109
+ const mind = new Mind({
110
+ seed: 7,
111
+ profile: true,
112
+ store: new SQliteStore({ path: ":memory:" }),
113
+ });
114
+ await mind.ingest(deposits(fillers(120)));
115
+ await mind.ingest(deposits(TRIPLES));
116
+ // Pour the citizenship fact again, so the distributional ladder prefers it.
117
+ await mind.ingest([[
118
+ "Frederick II",
119
+ "The country of citizenship of Frederick II is Holy Roman Empire.",
120
+ ]]);
121
+ return mind;
122
+ }
123
+
124
+ test("154.1 witnessing is order-free, complete or nothing, and credits the last holder", () => {
125
+ const W = 4;
126
+ const question = enc("What is the country of citizenship of Frederick II?");
127
+ const node = enc("Frederick II");
128
+ const qi = windowIndex(question, W);
129
+ const ni = windowIndex(node, W);
130
+ // Reordered pieces are witnessed; the question is credited only with what
131
+ // the node does not hold.
132
+ const w = witness(enc("Frederick II country of citizenship"), [qi, ni], W);
133
+ assert.equal(w.complete, true);
134
+ const said = w.spans.map(([s, e]) =>
135
+ new TextDecoder().decode(question.subarray(s, e))
136
+ );
137
+ assert.deepEqual(said, [" country of citizenship"]);
138
+ // One byte no source holds refuses the whole form.
139
+ assert.equal(
140
+ witness(enc("Frederick II country of citizenshop"), [qi, ni], W).complete,
141
+ false,
142
+ );
143
+ // Below one window nothing is witnessed.
144
+ assert.equal(witness(enc("Fre"), [qi, ni], W).complete, false);
145
+ });
146
+
147
+ /** The answer plus the rationale steps the assertions read. */
148
+ async function ask(mind, question) {
149
+ const steps = [];
150
+ const answer = await mind.respondText(question, (st) => {
151
+ const name = st.mechanism.at(-1);
152
+ if (
153
+ name === "recallByResonance" || name === "askedContinuation" ||
154
+ name === "pivotStep" || name === "liftAnswer"
155
+ ) {
156
+ steps.push({
157
+ name,
158
+ note: st.note ?? "",
159
+ inputs: st.inputs.map((o) => o.text ?? ""),
160
+ outputs: st.outputs.map((o) => o.text ?? ""),
161
+ });
162
+ }
163
+ });
164
+ return { answer, steps };
165
+ }
166
+
167
+ const FATHER = "The father of Frederick II is Peter III of Aragon.";
168
+ const DEATH = "The place of death of Edmond T. Gréville is Nice.";
169
+
170
+ test("154.2 the continuation the question names wins over the most-poured one", async () => {
171
+ const mind = await fixture();
172
+ const { answer, steps } = await ask(
173
+ mind,
174
+ "Who is the father of Frederick II?",
175
+ );
176
+ // The projection that grounds on the argument `Frederick II` crosses the
177
+ // father fact — the one whose own question `Frederick II father` the asker's
178
+ // bytes witness — not the citizenship fact popularity prefers.
179
+ const bound = steps.find((s) =>
180
+ s.name === "recallByResonance" && /argument binding/.test(s.note)
181
+ );
182
+ assert.ok(bound, "the argument binding must ground");
183
+ assert.equal(bound.outputs[0], FATHER);
184
+ const named = steps.find((s) =>
185
+ s.name === "askedContinuation" && s.inputs[1] === "Frederick II father"
186
+ );
187
+ assert.ok(named, "the rationale names the witnessed question");
188
+ assert.deepEqual(named.outputs, [FATHER]);
189
+ assert.match(answer, /The father of Frederick II is Peter III of Aragon\./);
190
+ await mind.store.close();
191
+ });
192
+
193
+ test("154.3 the second hop is named by the question plus the entity the first hop introduced", async () => {
194
+ const mind = await fixture();
195
+ const { answer, steps } = await ask(
196
+ mind,
197
+ "Where was the place of death of the director of film Beat Girl?",
198
+ );
199
+ const pivot = steps.find((s) => s.name === "pivotStep");
200
+ assert.ok(pivot, "the chain must step past the first hop");
201
+ assert.equal(pivot.inputs[1], "Edmond T. Gréville");
202
+ assert.equal(pivot.outputs[0], DEATH);
203
+ // …through the question neither source holds alone.
204
+ assert.ok(
205
+ steps.some((s) =>
206
+ s.name === "askedContinuation" &&
207
+ s.inputs[1] === "Edmond T. Gréville place of death" &&
208
+ s.outputs.includes(DEATH)
209
+ ),
210
+ "the hop is named by `Edmond T. Gréville place of death`",
211
+ );
212
+ assert.match(answer, /The place of death of Edmond T\. Gréville is Nice\./);
213
+ // The same chain whatever grounded the first hop: a CAST substitution, which
214
+ // declares the anchor it voices — whose continuation IS the answer, voiced and
215
+ // not withheld — and a cover, which declares nothing, so what it spoke for is
216
+ // what of the answer the question already holds, never the entity it added.
217
+ for (
218
+ const [question, fact] of [
219
+ [
220
+ "What is the place of birth of the performer of song Who...?",
221
+ "The place of birth of Ayumi Hamasaki is Fukuoka.",
222
+ ],
223
+ [
224
+ "Where was the place of death of Juan Carlos Gumucio's wife?",
225
+ "The place of death of Marie Colvin is Homs.",
226
+ ],
227
+ ]
228
+ ) {
229
+ const chained = await ask(mind, question);
230
+ assert.equal(chained.answer, fact, question);
231
+ }
232
+ await mind.store.close();
233
+ });
234
+
235
+ test("154.4 a question that names no further step is not extended", async () => {
236
+ const mind = await fixture();
237
+ const { answer, steps } = await ask(
238
+ mind,
239
+ "Who is the father of Frederick II?",
240
+ );
241
+ // `Peter III of Aragon father` is a stored question too — but `father` was
242
+ // said by the hop that reached Peter III, so nothing names the grandfather.
243
+ assert.equal(
244
+ steps.filter((s) => s.name === "pivotStep").length,
245
+ 0,
246
+ "no further step is taken",
247
+ );
248
+ assert.doesNotMatch(answer, /James I/);
249
+ assert.doesNotMatch(answer, /Valencia/);
250
+ // …and a one-hop question whose answer introduces an entity with facts of
251
+ // its own stops at that answer: nothing the asker still owes is discriminative
252
+ // (`Who is the` is frame), so no step can claim to pay it by restating ` is `.
253
+ // The material that named a hop names no second one, however the question
254
+ // is phrased: `Who was Frederick II's father?` says `father` once.
255
+ const again = await ask(mind, "Who was Frederick II's father?");
256
+ assert.equal(again.answer, FATHER);
257
+ const one = await ask(mind, "Who is the director of Beat Girl?");
258
+ assert.equal(one.answer, "The director of Beat Girl is Edmond T. Gréville.");
259
+ assert.equal(one.steps.filter((s) => s.name === "pivotStep").length, 0);
260
+ await mind.store.close();
261
+ });
262
+
263
+ test("154.5 a fragment answers other questions unless the question names one", async () => {
264
+ const mind = await fixture();
265
+ const { steps } = await ask(
266
+ mind,
267
+ "Where did the director of film Beat Girl die?",
268
+ );
269
+ // The cover's own composition: `director` sits inside every `… director`
270
+ // question, so its continuations are the directors of OTHER films — voicing
271
+ // one of them by popularity glued a stranger's fact onto the answer.
272
+ const composed = steps.filter((s) => s.name === "liftAnswer").map((s) =>
273
+ s.outputs[0]
274
+ );
275
+ assert.ok(composed.length > 0, "the cover composed");
276
+ for (const c of composed) {
277
+ assert.equal(c, "The director of Beat Girl is Edmond T. Gréville.");
278
+ }
279
+ assert.ok(mind.lastCost.counters.unaskedFragments > 0);
280
+ await mind.store.close();
281
+ });
@@ -264,6 +264,38 @@ test("3.1 — a two-topic query fuses BOTH points of attention", async () => {
264
264
  );
265
265
  });
266
266
 
267
+ test("3.1 — the second topic is fused on its OWN wording, never on a sub-window anchor", async () => {
268
+ // Observed before the fix (attention.ts canonicalChunkId): the region
269
+ // `ace and ` — inside the OTHER topic's words, "workplace and about" — had a
270
+ // saturated window replaced by its unsaturated 2-byte prefix `e `, that
271
+ // sub-window anchor voted for the Dream Team context, and its position was
272
+ // the "second cluster" the dispersion gate let the topic through on. The
273
+ // fusion passed on scaffolding. Pinned here: the Dream Team root is a
274
+ // committed point of attention whose every contributing region lies in the
275
+ // topic's own words, AND it is fused into the answer.
276
+ const m = await twoTopicMind();
277
+ let anchors = null;
278
+ const got = await m.respondText(TWO_TOPIC_QUERY, (s) => {
279
+ if (s.mechanism.at(-1) === "climbConsensus" && s.data?.anchors) {
280
+ anchors = s.data.anchors;
281
+ }
282
+ });
283
+ const topic = (a) =>
284
+ new TextDecoder().decode(m.store.bytes(a.anchor)).includes("Dream Team");
285
+ const root = anchors?.find((a) => a.commit.status === "root" && topic(a));
286
+ await m.store.close();
287
+ assert.ok(root, "the Dream Team context must be a committed root");
288
+ const own = TWO_TOPIC_QUERY.indexOf(" the 1992 Dream Team");
289
+ const foreign = root.contributingSpans.filter(([s]) => s < own);
290
+ assert.deepEqual(
291
+ foreign,
292
+ [],
293
+ `the Dream Team root is supported from outside its own words: ` +
294
+ JSON.stringify(foreign.map(([s, e]) => TWO_TOPIC_QUERY.slice(s, e))),
295
+ );
296
+ assert.match(got, /Barcelona|gold|Dream Team/i);
297
+ });
298
+
267
299
  test("3.1 — the trace surfaces MORE THAN ONE ordered anchor for a two-topic query", async () => {
268
300
  const m = await twoTopicMind();
269
301
  const steps = [];
@@ -14,11 +14,18 @@
14
14
  // persisted filter (or a stale one); reopen must
15
15
  // rebuild/top-up from the table and STILL dedup all
16
16
  // previously-minted content, with zero duplicates.
17
+ //
18
+ // The CANON index's filter (canon_bloom) has the same hazard — a false
19
+ // negative there is a canonical form `canonFind` silently fails to propose —
20
+ // and is pinned the same way: across reopen, across rows added after a
21
+ // persisted filter exists, and against a writer that adds rows without
22
+ // touching the filter (its stamp disagrees with the meta, so it is rebuilt).
17
23
 
18
24
  import { test } from "node:test";
19
25
  import assert from "node:assert/strict";
20
26
  import { execFileSync } from "node:child_process";
21
27
  import { rmSync } from "node:fs";
28
+ import { DatabaseSync } from "node:sqlite";
22
29
  import { SQliteStore } from "../dist/src/store-sqlite.js";
23
30
 
24
31
  const cleanup = (path) => {
@@ -121,3 +128,49 @@ test("crash: reopen after an unclean exit still dedups everything", (t) => {
121
128
  execFileSync(process.execPath, ["-e", phase2], { stdio: "pipe" });
122
129
  assert.ok(true);
123
130
  });
131
+
132
+ test("canon filter: persisted with its rows, rebuilt when its stamp is stale", async (t) => {
133
+ const path = `/tmp/bloom-canon-${process.pid}`;
134
+ t.after(() => cleanup(path));
135
+ const H = (i) => (i * 2654435761) >>> 0;
136
+ const findsAll = (s, from, to) => {
137
+ for (let i = from; i < to; i++) {
138
+ assert.deepEqual(s.canonFind(H(i)), [i], `canon row ${i} not proposed`);
139
+ }
140
+ };
141
+
142
+ // Session 1: rows written by the store itself, then committed and closed.
143
+ let s = new SQliteStore({ path, D: 64 });
144
+ await s.size();
145
+ for (let i = 0; i < 200; i++) s.canonAdd(H(i), i);
146
+ await s.setMeta("canon.upto", "200");
147
+ s.commit();
148
+ findsAll(s, 0, 200);
149
+ await s.close();
150
+
151
+ // Session 2: the persisted filter is loaded, and rows added on top of it
152
+ // are seen in this session and after the next reopen.
153
+ s = new SQliteStore({ path, D: 64 });
154
+ await s.size();
155
+ findsAll(s, 0, 200);
156
+ for (let i = 200; i < 300; i++) s.canonAdd(H(i), i);
157
+ await s.setMeta("canon.upto", "300");
158
+ s.commit();
159
+ findsAll(s, 0, 300);
160
+ await s.close();
161
+
162
+ // A writer that knows nothing of the filter (an older build) adds rows and
163
+ // advances the cursor, as every canon build does.
164
+ const db = new DatabaseSync(path + ".sqlite");
165
+ for (let i = 300; i < 350; i++) {
166
+ db.prepare("INSERT INTO canon (h, id) VALUES (?, ?)").run(H(i), i);
167
+ }
168
+ db.prepare("UPDATE meta SET val = '350' WHERE key = 'canon.upto'").run();
169
+ db.close();
170
+
171
+ s = new SQliteStore({ path, D: 64 });
172
+ await s.size();
173
+ findsAll(s, 0, 350);
174
+ assert.deepEqual(s.canonFind(H(9999)), [], "an absent key is not proposed");
175
+ await s.close();
176
+ });
@@ -215,3 +215,78 @@ test("fuseAttention: 1 cluster, low breadth — neither measure saves it (the li
215
215
  );
216
216
  await m.store.close();
217
217
  });
218
+
219
+ // ═══════════════════════════════════════════════════════════════════════════
220
+ // The dispersion question asked of the CONTEXT (reasoning.ts sharedPlaces).
221
+ // The votes that land on a root are a lossy witness of where the query and the
222
+ // root's context agree: a region votes once, for its top anchor, so a frame
223
+ // every context shares corroborates whichever anchor a tie hands it. The
224
+ // context's own bytes answer the question exactly — at window scale, in how
225
+ // many separate places of the query does it share content? These two cases
226
+ // pin both directions with REAL anchors (a stored context and its learnt
227
+ // continuation), each fabricated as a one-cluster, low-breadth root so that
228
+ // only the new reading can decide.
229
+ // ═══════════════════════════════════════════════════════════════════════════
230
+
231
+ async function sharedPlacesCase(contextText, query) {
232
+ const m = mk(1);
233
+ await m.ingest([
234
+ ...CORPUS,
235
+ [contextText, "the omega continuation"],
236
+ ]);
237
+ const q = enc(query);
238
+ m.beginResponse();
239
+ const anchor = m.recogniseSpan(enc(contextText)).sites
240
+ .find((s) => s.start === 0 && s.end === contextText.length)?.payload;
241
+ m.endResponse();
242
+ assert.ok(anchor !== undefined, "the context must be a stored form");
243
+ const root = {
244
+ anchor,
245
+ vote: 1,
246
+ idfVote: 1,
247
+ peak: 1,
248
+ start: 0,
249
+ end: q.length,
250
+ clusters: 1,
251
+ breadth: 0.1,
252
+ };
253
+ const placeholder = { ...root, start: q.length, end: q.length };
254
+ const primary = enc("PRIMARYANCHORTEXT");
255
+ const out = dec(
256
+ (await fuseAttention(m, q, {
257
+ product: primary,
258
+ accounted: [],
259
+ remainder: [],
260
+ cost: 0,
261
+ }, {
262
+ attention: async () => ({
263
+ roots: [placeholder, root],
264
+ ranked: [placeholder, root],
265
+ }),
266
+ guide: gistOf(m, q),
267
+ })).product,
268
+ );
269
+ await m.store.close();
270
+ return { out, primary: dec(primary) };
271
+ }
272
+
273
+ test("fuseAttention: a context sharing ONE phrase with the query does not fuse, however long the phrase (the live echo shape)", async () => {
274
+ // The live defect's shape: the query's closing phrase is byte-identical to
275
+ // how an unrelated conversation ended. Many windows, ONE place.
276
+ const { out, primary } = await sharedPlacesCase(
277
+ "Thank you very much!",
278
+ "red circle then 2+2 equals what. Thank you very much!",
279
+ );
280
+ assert.equal(out, primary, `a one-place echo fused in: "${out}"`);
281
+ });
282
+
283
+ test("fuseAttention: a context sharing TWO separate places with the query fuses, even when its votes formed one cluster", async () => {
284
+ const { out, primary } = await sharedPlacesCase(
285
+ "red circle at noon, and then: Thank you very much!",
286
+ "red circle then 2+2 equals what. Thank you very much!",
287
+ );
288
+ assert.ok(
289
+ out.includes("omega") && out.includes(primary),
290
+ `a two-place context must fuse in, got "${out}"`,
291
+ );
292
+ });
@@ -32,7 +32,8 @@ import assert from "node:assert/strict";
32
32
  import { Mind, SQliteStore } from "../dist/src/index.js";
33
33
  import { textCanon } from "../dist/src/canon.js";
34
34
  import { recognise } from "../dist/src/mind/recognition.js";
35
- import { latin1Key, perceive, resolve } from "../dist/src/mind/primitives.js";
35
+ import { perceive, resolve } from "../dist/src/mind/primitives.js";
36
+ import { latin1 } from "../dist/src/bytes.js";
36
37
 
37
38
  const enc = (s) => new TextEncoder().encode(s);
38
39
  const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
@@ -71,7 +72,7 @@ test("recognise(): a real leading connective word ('And ') still finds the train
71
72
  const boundaries = [30, 61];
72
73
  m.perceiveMemo = new Map();
73
74
  m.perceiveMemo.set(
74
- latin1Key(query),
75
+ latin1(query),
75
76
  perceive(m, query, undefined, undefined, boundaries),
76
77
  );
77
78
 
@@ -156,10 +156,10 @@ test("7. a bigger query costs more than a smaller one", async () => {
156
156
  "the capital of France is and the capital of Japan is and Paris is in",
157
157
  );
158
158
  const big = mind.lastCost.counters;
159
- assert.ok(
160
- big.perceivedBytes > small.perceivedBytes,
161
- "perception cost tracks query length",
162
- );
159
+ // Fold work in both of its forms: the vector fold (perception) and the
160
+ // identity fold `exactNode` reads a node id through without vectors.
161
+ const folded = (c) => (c.perceivedBytes ?? 0) + (c.identityBytes ?? 0);
162
+ assert.ok(folded(big) > folded(small), "fold cost tracks query length");
163
163
  await mind.store.close();
164
164
  });
165
165