@hviana/sema 0.9.1 → 0.9.3

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 (42) hide show
  1. package/dist/src/meter.d.ts +19 -0
  2. package/dist/src/meter.js +19 -0
  3. package/dist/src/mind/evidence.d.ts +30 -0
  4. package/dist/src/mind/evidence.js +102 -0
  5. package/dist/src/mind/mechanisms/cast.d.ts +1 -1
  6. package/dist/src/mind/mechanisms/cast.js +73 -12
  7. package/dist/src/mind/mechanisms/cover.js +25 -3
  8. package/dist/src/mind/mechanisms/recall.js +93 -9
  9. package/dist/src/mind/mechanisms/reference.js +25 -0
  10. package/dist/src/mind/mind.d.ts +7 -0
  11. package/dist/src/mind/mind.js +3 -0
  12. package/dist/src/mind/pipeline-mechanism.js +15 -1
  13. package/dist/src/mind/pipeline.js +51 -8
  14. package/dist/src/mind/reasoning.js +65 -18
  15. package/dist/src/mind/resonance.d.ts +3 -2
  16. package/dist/src/mind/resonance.js +3 -2
  17. package/dist/src/mind/traverse.d.ts +94 -2
  18. package/dist/src/mind/traverse.js +611 -7
  19. package/dist/src/mind/types.d.ts +12 -0
  20. package/docs/INDEX.md +18 -17
  21. package/docs/INVARIANTS.md +1 -0
  22. package/docs/architecture/evidence.md +256 -0
  23. package/docs/architecture/exact-vs-approximate.md +10 -9
  24. package/jsr.json +1 -1
  25. package/package.json +1 -1
  26. package/src/meter.ts +19 -0
  27. package/src/mind/evidence.ts +125 -0
  28. package/src/mind/mechanisms/cast.ts +77 -3
  29. package/src/mind/mechanisms/cover.ts +42 -3
  30. package/src/mind/mechanisms/recall.ts +123 -11
  31. package/src/mind/mechanisms/reference.ts +24 -1
  32. package/src/mind/mind.ts +6 -0
  33. package/src/mind/pipeline-mechanism.ts +15 -1
  34. package/src/mind/pipeline.ts +64 -8
  35. package/src/mind/reasoning.ts +82 -17
  36. package/src/mind/resonance.ts +3 -2
  37. package/src/mind/traverse.ts +652 -7
  38. package/src/mind/types.ts +10 -0
  39. package/test/154-the-question-names-the-step.test.mjs +368 -0
  40. package/test/155-the-relation-read-off-another-instance.test.mjs +376 -0
  41. package/test/29-counterfactual.test.mjs +21 -11
  42. package/test/76-reference-binding.test.mjs +32 -0
@@ -0,0 +1,376 @@
1
+ // 155-the-relation-read-off-another-instance.test.mjs — a question's relation
2
+ // is identified through ANOTHER INSTANCE of the same question.
3
+ //
4
+ // THE READING (traverse.ts, `byCoInstance`; evidence.md). A stored context the
5
+ // question witnesses in every byte but ONE contiguous span is a CO-INSTANCE:
6
+ // the question's own frame around a different filler (`Where was Peter Jackson
7
+ // born?` against `Where was the director of film Beat Girl born?` leaves
8
+ // `Peter Jackson`). The co-instance's continuation is established by other
9
+ // contexts too (`Peter Jackson place of birth`), and the one holding the filler
10
+ // spells the relation the way the corpus does — read only where two
11
+ // co-instances spell it alike, since one alignment agrees with nothing.
12
+ // Putting the node the
13
+ // derivation stands on where the filler was, and looking the result up by
14
+ // content, names that node's continuation for the same relation — `born`
15
+ // becomes `place of birth` without either equivalence or frame being stored.
16
+ //
17
+ // MEASURED on 2WikiMultihopQA: 58% of compositional questions phrase the
18
+ // second relation differently from its stored label. With the evidence
19
+ // triples of 300 held-out rows plus 261 one-hop questions about OTHER entities
20
+ // (built from other rows by replacing the first hop's phrase with its
21
+ // referent), compositional answers went 35 → 65 of 133; no test entity had a
22
+ // one-hop question of its own.
23
+ //
24
+ // Pinned:
25
+ // 155.1 the second hop's relation is read off another instance of the frame;
26
+ // 155.2 without two instances that spell the relation alike nothing names
27
+ // it, and the chain stops;
28
+ // 155.3 a frame the question shares only partly is no co-instance;
29
+ // 155.4 the same reading names the first hop when the entity is asked
30
+ // directly;
31
+ // 155.5 another instance's own answer is never voiced: an attention point
32
+ // that is a co-instance of the question is neither grounded (recall)
33
+ // nor fused, and the pieces the instances share are no establishing
34
+ // context of their facts;
35
+ // 155.6 a frame a product already said names no further step: `father`,
36
+ // read off another instance for the second hop, does not name a third;
37
+ // 155.7 a record whose slot holds a description, not a thing the corpus
38
+ // knows, is the same question about the frame's own subject;
39
+ // 155.8 fusion does not fuse a co-instance as a further topic;
40
+ // 155.9 a comparison's dominant that is a co-instance is not compared.
41
+
42
+ import { test } from "node:test";
43
+ import assert from "node:assert/strict";
44
+ import { Mind } from "../dist/src/index.js";
45
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
46
+
47
+ /** Deposit triples as the trainer does (wiki2.ts). */
48
+ function deposits(triples) {
49
+ const items = [];
50
+ for (const [s, r, o] of triples) {
51
+ const fact = `The ${r} of ${s} is ${o}.`;
52
+ items.push([`${s} ${r}`, fact], [s, fact]);
53
+ }
54
+ return items;
55
+ }
56
+
57
+ const TRIPLES = [
58
+ ["Marius", "place of death", "Marseille"],
59
+ ["Jinpa", "director", "Pema Tseden"],
60
+ ["Le Guignolo", "director", "Georges Lautner"],
61
+ ["Beat Girl", "director", "Edmond T. Gréville"],
62
+ // The death fact first and poured twice below, so neither first insertion
63
+ // nor popularity lands on the birth fact by accident.
64
+ ["Edmond T. Gréville", "place of death", "Paris"],
65
+ ["Edmond T. Gréville", "place of birth", "Nice"],
66
+ ["Edmond T. Gréville", "date of birth", "20 June 1906"],
67
+ // Other instances of the same relations.
68
+ ["Peter Jackson", "place of death", "Auckland"],
69
+ ["Peter Jackson", "place of birth", "Wellington"],
70
+ ["Taika Waititi", "place of birth", "Raukokore"],
71
+ ["Nicki Minaj", "place of birth", "Port of Spain"],
72
+ ["Stan Rogers", "place of birth", "Hamilton"],
73
+ ];
74
+
75
+ /** One-hop questions about OTHER entities, answered with their facts. */
76
+ const INSTANCES = [
77
+ [
78
+ "Where was Peter Jackson born?",
79
+ "The place of birth of Peter Jackson is Wellington.",
80
+ ],
81
+ [
82
+ "Where was Taika Waititi born?",
83
+ "The place of birth of Taika Waititi is Raukokore.",
84
+ ],
85
+ [
86
+ "Where was Nicki Minaj (Nicki Minaj Song) born?",
87
+ "The place of birth of Nicki Minaj is Port of Spain.",
88
+ ],
89
+ [
90
+ "Where was Stan Rogers (Song) born?",
91
+ "The place of birth of Stan Rogers is Hamilton.",
92
+ ],
93
+ ];
94
+
95
+ const WORDS =
96
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet " +
97
+ "kilo lima mike november oscar papa quebec romeo sierra tango uniform " +
98
+ "victor whiskey xray yankee zulu amber bronze copper dahlia ember fjord " +
99
+ "gossamer harbour indigo jasmine kestrel lantern marigold nectar opal")
100
+ .split(" ");
101
+ const RELATIONS = ["award received", "employer", "director", "performer"];
102
+ /** Lexically varied filler facts in the same template (test/154's recipe). */
103
+ function fillers(n) {
104
+ const out = [];
105
+ for (let i = 0; i < n; i++) {
106
+ const w = (k) => WORDS[(i * 7 + k * 13) % WORDS.length];
107
+ out.push([
108
+ `${w(0)} ${w(1)}`,
109
+ RELATIONS[i % RELATIONS.length],
110
+ `${w(2)} ${w(3)}`,
111
+ ]);
112
+ }
113
+ return out;
114
+ }
115
+
116
+ async function fixture(instances = INSTANCES) {
117
+ const mind = new Mind({
118
+ seed: 7,
119
+ profile: true,
120
+ store: new SQliteStore({ path: ":memory:" }),
121
+ });
122
+ await mind.ingest(deposits(fillers(120)));
123
+ await mind.ingest(deposits(TRIPLES));
124
+ await mind.ingest([[
125
+ "Edmond T. Gréville",
126
+ "The place of death of Edmond T. Gréville is Paris.",
127
+ ]]);
128
+ if (instances.length > 0) await mind.ingest(instances);
129
+ return mind;
130
+ }
131
+
132
+ async function ask(mind, question) {
133
+ const steps = [];
134
+ const answer = await mind.respondText(question, (st) => {
135
+ const name = st.mechanism.at(-1);
136
+ if (name === "askedByCoInstance" || name === "pivotStep") {
137
+ steps.push({
138
+ name,
139
+ inputs: st.inputs.map((o) => o.text ?? ""),
140
+ outputs: st.outputs.map((o) => o.text ?? ""),
141
+ });
142
+ }
143
+ });
144
+ return { answer, steps };
145
+ }
146
+
147
+ const BIRTH = "The place of birth of Edmond T. Gréville is Nice.";
148
+
149
+ test("155.1 the second hop's relation is read off another instance of the frame", async () => {
150
+ const mind = await fixture();
151
+ const { answer, steps } = await ask(
152
+ mind,
153
+ "Where was the director of film Beat Girl born?",
154
+ );
155
+ const named = steps.find((s) =>
156
+ s.name === "askedByCoInstance" && s.inputs[0] === "Edmond T. Gréville"
157
+ );
158
+ assert.ok(named, "a co-instance names the hop");
159
+ assert.match(
160
+ named.inputs[1],
161
+ /^Where was (Peter Jackson|Taika Waititi) born\?$/,
162
+ );
163
+ assert.equal(named.inputs[2], "Edmond T. Gréville place of birth");
164
+ assert.deepEqual(named.outputs, [BIRTH]);
165
+ const pivot = steps.find((s) => s.name === "pivotStep");
166
+ assert.ok(pivot, "the chain steps past the first hop");
167
+ assert.equal(pivot.outputs[0], BIRTH);
168
+ // …and the instances themselves are never the answer: `born?`, the piece
169
+ // of both one-hop questions, is no establishing context of either fact.
170
+ assert.equal(answer, BIRTH);
171
+ assert.doesNotMatch(answer, /Wellington/);
172
+ assert.ok(mind.lastCost.counters.coInstanceNamings > 0);
173
+ await mind.store.close();
174
+ });
175
+
176
+ test("155.2 without two instances that agree nothing names the relation", async () => {
177
+ // None at all, and one alone: a single alignment agrees with nothing, so it
178
+ // is no evidence of what the frame means.
179
+ for (const instances of [[], INSTANCES.slice(0, 1)]) {
180
+ const mind = await fixture(instances);
181
+ const { answer, steps } = await ask(
182
+ mind,
183
+ "Where was the director of film Beat Girl born?",
184
+ );
185
+ assert.equal(
186
+ steps.filter((s) => s.name === "askedByCoInstance").length,
187
+ 0,
188
+ `${instances.length} instance(s)`,
189
+ );
190
+ assert.doesNotMatch(answer, /place of birth/);
191
+ await mind.store.close();
192
+ }
193
+ });
194
+
195
+ test("155.3 a frame the question shares only partly is no co-instance", async () => {
196
+ const mind = await fixture();
197
+ // `When was … born?` asks for a date; `Where was Peter Jackson born?` leaves
198
+ // `Where` and `Peter Jackson` unwitnessed — two residues, not one filler.
199
+ const { answer, steps } = await ask(
200
+ mind,
201
+ "When was the director of film Beat Girl born?",
202
+ );
203
+ assert.equal(steps.filter((s) => s.name === "askedByCoInstance").length, 0);
204
+ assert.doesNotMatch(answer, /Nice/);
205
+ await mind.store.close();
206
+ });
207
+
208
+ test("155.4 the same reading names the first hop when the entity is asked directly", async () => {
209
+ const mind = await fixture();
210
+ const { answer, steps } = await ask(
211
+ mind,
212
+ "Where was Edmond T. Gréville born?",
213
+ );
214
+ assert.ok(
215
+ steps.some((s) =>
216
+ s.name === "askedByCoInstance" && s.outputs.includes(BIRTH)
217
+ ),
218
+ "a co-instance names the fact",
219
+ );
220
+ assert.equal(answer, BIRTH);
221
+ // The co-instances come from the consensus climb, so the pick depends on
222
+ // whether it has run — and an untraced response, which memoises picks, must
223
+ // read exactly what a traced one does.
224
+ const untraced = await fixture();
225
+ assert.equal(
226
+ await untraced.respondText("Where was Edmond T. Gréville born?"),
227
+ BIRTH,
228
+ );
229
+ await untraced.store.close();
230
+ await mind.store.close();
231
+ });
232
+
233
+ test("155.5 another instance's own answer is never voiced", async () => {
234
+ const mind = await fixture();
235
+ // Nothing is stored about the song `God`: the climb's best point is
236
+ // `Where was Nicki Minaj (Nicki Minaj Song) born?` — the question's frame
237
+ // around another filler.
238
+ const notes = [];
239
+ const answer = await mind.respondText(
240
+ "Where was the performer of song God (John Lennon Song) born?",
241
+ (st) => {
242
+ const name = st.mechanism.at(-1);
243
+ if (name === "coInstanceAnchor" || name === "coInstanceRoot") {
244
+ notes.push(name);
245
+ }
246
+ },
247
+ );
248
+ assert.ok(notes.includes("coInstanceAnchor"), "the anchor is refused");
249
+ assert.doesNotMatch(
250
+ answer,
251
+ /Nicki|Port of Spain|Wellington|Raukokore|Hamilton/,
252
+ );
253
+ // …and where the question IS answerable, no instance rides along with it.
254
+ const known = await mind.respondText(
255
+ "Where was the director of film Beat Girl born?",
256
+ );
257
+ assert.equal(known, BIRTH);
258
+ await mind.store.close();
259
+ });
260
+
261
+ test("155.6 a frame a product already said names no further step", async () => {
262
+ const mind = await fixture();
263
+ await mind.ingest(deposits([
264
+ ["Edmond T. Gréville", "father", "Louis Gréville"],
265
+ ["Louis Gréville", "father", "Henri Gréville"],
266
+ ["Peter Jackson", "father", "Bill Jackson"],
267
+ ["Taika Waititi", "father", "Taika Cohen"],
268
+ ]));
269
+ await mind.ingest([
270
+ [
271
+ "Who is the father of Peter Jackson?",
272
+ "The father of Peter Jackson is Bill Jackson.",
273
+ ],
274
+ [
275
+ "Who is the father of Taika Waititi?",
276
+ "The father of Taika Waititi is Taika Cohen.",
277
+ ],
278
+ ]);
279
+ // The second hop's relation is read off `Who is the father of …?`; the fact
280
+ // it reaches says `father`, so the same frame cannot name the grandfather.
281
+ const answer = await mind.respondText(
282
+ "Who is the father of the director of film Beat Girl?",
283
+ );
284
+ assert.equal(answer, "The father of Edmond T. Gréville is Louis Gréville.");
285
+ await mind.store.close();
286
+ });
287
+
288
+ test("155.7 a description in the slot is not another instance", async () => {
289
+ const mind = await fixture();
290
+ const fact =
291
+ "Photosynthesis captures sunlight in chlorophyll to bind carbon dioxide and water into sugar.";
292
+ await mind.ingest([
293
+ [
294
+ "Explain how photosynthesis converts sunlight into chemical energy.",
295
+ fact,
296
+ ],
297
+ [
298
+ "Explain how a rainbow forms after rain.",
299
+ "Sunlight refracts in raindrops and splits into colours.",
300
+ ],
301
+ ]);
302
+ // Same opening and close, different slot — but `converts sunlight into
303
+ // chemical energy` is no stored context: the record is about the frame's
304
+ // own subject, and answers.
305
+ assert.equal(
306
+ await mind.respondText("Explain how photosynthesis works."),
307
+ fact,
308
+ );
309
+ await mind.store.close();
310
+ });
311
+
312
+ test("155.8 fusion does not fuse a co-instance as a further topic", async () => {
313
+ // Two one-hop questions that happen to share an answer value: the climb
314
+ // commits `Where was Peter Jackson born?` as a second point of attention
315
+ // on the frame windows, and fusing it voiced his birthplace beside the
316
+ // answer.
317
+ const mind = await fixture([]);
318
+ await mind.ingest(
319
+ deposits([["Jane Campion", "place of birth", "Wellington"]]),
320
+ );
321
+ await mind.ingest([
322
+ [
323
+ "Where was Peter Jackson born?",
324
+ "The place of birth of Peter Jackson is Wellington.",
325
+ ],
326
+ [
327
+ "Where was Jane Campion born?",
328
+ "The place of birth of Jane Campion is Wellington.",
329
+ ],
330
+ ]);
331
+ const notes = [];
332
+ const answer = await mind.respondText(
333
+ "Where was the director of film Beat Girl born?",
334
+ (st) => {
335
+ if (st.mechanism.at(-1) === "coInstanceRoot") notes.push(st);
336
+ },
337
+ );
338
+ assert.equal(answer, BIRTH);
339
+ assert.ok(notes.length > 0, "the co-instance point is refused");
340
+ await mind.store.close();
341
+ });
342
+
343
+ test("155.9 a comparison's dominant that is a co-instance is not compared", async () => {
344
+ const mind = await fixture();
345
+ const singers = [
346
+ ["Adele", "Tottenham"],
347
+ ["Bjork", "Reykjavik"],
348
+ ["Prince", "Minneapolis"],
349
+ ["Sade", "Ibadan"],
350
+ ["Shakira", "Barranquilla"],
351
+ ["Rihanna", "Saint Michael"],
352
+ ];
353
+ await mind.ingest(
354
+ deposits(singers.map(([s, o]) => [s, "place of birth", o])),
355
+ );
356
+ await mind.ingest(
357
+ singers.map(([s, o]) => [
358
+ `Where was ${s} (${s} Song) born?`,
359
+ `The place of birth of ${s} is ${o}.`,
360
+ ]),
361
+ );
362
+ await mind.ingest(deposits([
363
+ ["Imagine", "performer", "John Lennon"],
364
+ ["John Lennon", "place of birth", "Liverpool"],
365
+ ]));
366
+ // Nothing is stored about the song `God`. The comparison used to set
367
+ // `Where was Shakira (Shakira Song) born?` against `John Lennon` and voice
368
+ // Shakira's birthplace.
369
+ const answer = await mind.respondText(
370
+ "Where was the performer of song God (Lennon Song) born?",
371
+ );
372
+ for (const [s, o] of singers) {
373
+ assert.ok(!answer.includes(o), `${s}'s birthplace voiced: "${answer}"`);
374
+ }
375
+ await mind.store.close();
376
+ });
@@ -382,16 +382,20 @@ test("C2 — cross-domain comparison identifies shared role", async () => {
382
382
  await m.store.close();
383
383
  });
384
384
 
385
- // C3 — CAST's terminal treatment must not be a blanket rule. A comparison's
386
- // seat sentence can itself contain a further pivotable term with its OWN
385
+ // C3 — CAST's terminal treatment is the walk's law, not a rule of its own. A
386
+ // comparison's seat sentence can contain a further pivotable term with its OWN
387
387
  // downstream fact ("Mona Lisa" inside "The Mona Lisa was painted by Leonardo
388
- // da Vinci" leads on to "Mona Lisa hangs in the Louvre") — a genuine further
389
- // hop distinct from Leonardo/Shakespeare's own biographies, which C2 pins
390
- // must NEVER surface. Proven end-to-end through respond/ask, the real
391
- // public surface: the composed comparison must carry BOTH the shared-role
392
- // comparison AND the Louvre fact, and still never leak either analog's own
393
- // biography.
394
- test("C3 — a further hop inside a comparison's seat still fires", async () => {
388
+ // da Vinci" leads on to "Mona Lisa hangs in the Louvre"). This test used to
389
+ // require that hop: a grounding that declared what it voiced was allowed to
390
+ // step past it unasked. On the trained store the same licence turned "Who is
391
+ // the director of The Jerk?" into "The country of citizenship of Carl Reiner
392
+ // is American." — a step nobody asked for, taken because CAST grounded the
393
+ // first hop. A further hop is now taken when the question NAMES it or the
394
+ // step carries what the question still owes, whichever mechanism grounded the
395
+ // first hop (evidence.md; test/154.3 pins a CAST grounding followed by a named
396
+ // second hop). "How is Shakespeare like Leonardo da Vinci?" asks for neither
397
+ // the Louvre nor either analog's biography, so the comparison stands alone.
398
+ test("C3 — a further hop inside a comparison's seat waits to be asked", async () => {
395
399
  const m = mk();
396
400
  await m.ingest([
397
401
  ["The Mona Lisa was painted by Leonardo da Vinci.", "Leonardo da Vinci"],
@@ -413,9 +417,15 @@ test("C3 — a further hop inside a comparison's seat still fires", async () =>
413
417
  ]);
414
418
 
415
419
  const got = await ask(m, "How is Shakespeare like Leonardo da Vinci?");
420
+ assert.match(
421
+ got,
422
+ /Leonardo da Vinci/,
423
+ `the comparison must answer — got "${got}"`,
424
+ );
425
+ assert.match(got, /Shakespeare/, `the comparison must answer — got "${got}"`);
416
426
  assert.ok(
417
- /Louvre/i.test(got),
418
- `a genuine further hop inside the comparison's seat did not fire — got "${got}"`,
427
+ !/Louvre/i.test(got),
428
+ `the question named no further hop — got "${got}"`,
419
429
  );
420
430
  assert.ok(
421
431
  !/39 plays/i.test(got) && !/Renaissance polymath/i.test(got),
@@ -546,3 +546,35 @@ test("the frame inventory and recall share ONE resonance call", async () => {
546
546
  assert.equal(phase.calls, 1, "the shared resonance ran more than once");
547
547
  await m.store.close();
548
548
  });
549
+
550
+ // A FACT FILED UNDER ITS FILLER IS NOT A CARRIAGE. Two one-hop questions whose
551
+ // answers happen to agree (two people born in Wellington) pass the carriage
552
+ // test byte-exactly, and the binding used to construct `The place of birth of
553
+ // Zorblax is Wellington.` — but each answer is a fact the corpus files under
554
+ // its own filler (`Peter Jackson` leads to it), knowledge ABOUT that filler,
555
+ // not a form any occupant takes. `Run gcc hello.c` is filed under the
556
+ // question alone, and still binds.
557
+ test("a fact filed under its filler is not a carriage", async () => {
558
+ const fact = (s, r, o) => `The ${r} of ${s} is ${o}.`;
559
+ const m = await mindWith([
560
+ ["Peter Jackson", fact("Peter Jackson", "place of birth", "Wellington")],
561
+ ["Jane Campion", fact("Jane Campion", "place of birth", "Wellington")],
562
+ [
563
+ "Where was Peter Jackson born?",
564
+ fact("Peter Jackson", "place of birth", "Wellington"),
565
+ ],
566
+ [
567
+ "Where was Jane Campion born?",
568
+ fact("Jane Campion", "place of birth", "Wellington"),
569
+ ],
570
+ ]);
571
+ const got = await m.respondText("Where was Zorblax born?");
572
+ assert.doesNotMatch(got, /Zorblax/, `fabricated "${got}"`);
573
+ await m.store.close();
574
+ const carried = await mindWith(CARRIED);
575
+ assert.match(
576
+ await carried.respondText("How do I compile main.c?"),
577
+ /gcc main\.c/,
578
+ );
579
+ await carried.store.close();
580
+ });