@hviana/sema 0.9.1 → 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.
@@ -9,13 +9,20 @@
9
9
  import { cosine, Vec } from "../vec.js";
10
10
  import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
11
11
  import { gistOf, read } from "./primitives.js";
12
- import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
12
+ import {
13
+ canonicalWindows,
14
+ chainReach,
15
+ leafIdPrefix,
16
+ leafIdRun,
17
+ } from "./canonical.js";
13
18
  // Imported at the TOP, where every other import is. They used to sit 800 lines
14
19
  // down under a note claiming the position mattered ("before trace module is
15
20
  // loaded") — it does not: an ES module's static imports are HOISTED, so the
16
21
  // file's line order never decides load order. The note described an intention
17
22
  // the runtime does not honour; the imports move and the claim goes.
18
23
  import { decodeText } from "./rationale.js";
24
+ import { latin1 } from "../bytes.js";
25
+ import { type WindowIndex, windowIndex, witness } from "./evidence.js";
19
26
  import type { RationaleItem } from "./rationale.js";
20
27
 
21
28
  // ── Session structural memo ─────────────────────────────────────────────
@@ -696,8 +703,10 @@ export function guidedNext(
696
703
  return pick;
697
704
  }
698
705
 
699
- /** Disambiguate among a node's learnt continuations by distributional
700
- * support. NOTE the `guide` contract: its VALUE is deliberately unused —
706
+ /** Disambiguate among a node's learnt continuations: first by the question's
707
+ * own witness of an establishing context (the exact tier, see
708
+ * {@link askedContinuations}), then by distributional support. NOTE the
709
+ * `guide` contract: its VALUE is deliberately unused —
701
710
  * only its PRESENCE gates disambiguation (a null guide means no query is in
702
711
  * flight, so structural walkers keep plain first-edge behaviour). The
703
712
  * gist-cosine of short answer candidates against a query guide is dominated
@@ -721,13 +730,30 @@ export function chooseNext(
721
730
  if (nx.length === 0) return undefined;
722
731
  if (nx.length === 1 || !guide) return nx[0];
723
732
 
733
+ // THE EXACT TIER — the continuation the QUESTION names. Every other
734
+ // disambiguation below reads popularity, and is right to refuse the gist (see
735
+ // the doc above); but the corpus also wrote down, for each continuation,
736
+ // WHICH QUESTIONS IT ANSWERS — its establishing contexts — and a question is
737
+ // not a gist. When one of them is witnessed by the asker's bytes plus the
738
+ // node's own, that continuation is the one being asked for. Measured on the
739
+ // 31.7M-node store: `Who is the father of Frederick II?` answered the
740
+ // citizenship fact (the most-poured of eight) while `Frederick II father` —
741
+ // one of the father fact's own establishing contexts — lay wholly inside the
742
+ // question. Exact, so it ranks first (exact-vs-approximate.md); when nothing
743
+ // is witnessed the ladder below decides exactly as before.
744
+ const asked = ctx._edgeAsked;
745
+ const named = asked === null ? null : askedContinuations(ctx, id, nx, asked);
746
+ if (named !== null && named.length === 1) return named[0];
747
+
724
748
  // Cap candidates at √N — the same bound the original chooseAmong used.
725
749
  // A hub context can accumulate thousands of continuations; the best-fit
726
750
  // one is among the first √N by insertion order (edges are never deleted,
727
751
  // so the oldest are the most established). A strongly-supported edge
728
752
  // inserted beyond the cap is invisible here — the deliberate trade
729
- // against paying O(fan-out) count reads on every disambiguation.
730
- const capped = nx; // already the hub-capped prefix, by the read above
753
+ // against paying O(fan-out) count reads on every disambiguation. Several
754
+ // continuations named EQUALLY by the question are told apart by the same
755
+ // ladder, over them alone.
756
+ const capped = named ?? nx; // already the hub-capped prefix, by the read above
731
757
 
732
758
  // Distributional-evidence disambiguation, consulting BOTH read-outs of the
733
759
  // evidence the training poured:
@@ -802,6 +828,213 @@ export function chooseNext(
802
828
  return best;
803
829
  }
804
830
 
831
+ /** The response canonicalizer's reading of `bytes` when it keeps every offset
832
+ * — so a window found in the canonical bytes sits at the same place in the
833
+ * asker's — else the bytes themselves. Text canon is offset-preserving on
834
+ * ASCII without interior whitespace runs; where it is not, the raw bytes are
835
+ * read and a case-variant window simply does not match. */
836
+ export function offsetCanon(ctx: MindContext, bytes: Uint8Array): Uint8Array {
837
+ if (ctx.canon === null) return bytes;
838
+ const c = ctx.canon(bytes);
839
+ return c.length === bytes.length ? c : bytes;
840
+ }
841
+
842
+ /** The continuations of `id` that `asked` NAMES (see {@link
843
+ * askedContinuations}) — for a caller holding material other than the whole
844
+ * question: the multi-hop walk asks with what of the question no product has
845
+ * restated yet. null when none is named. */
846
+ export function namedContinuations(
847
+ ctx: MindContext,
848
+ id: number,
849
+ asked: { bytes: Uint8Array; index: WindowIndex },
850
+ ): number[] | null {
851
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
852
+ return nx.length === 0 ? null : askedContinuations(ctx, id, nx, asked);
853
+ }
854
+
855
+ /** The continuations of `id` (among `nx`) that the question NAMES: one of
856
+ * their establishing contexts — a predecessor other than `id` itself — is
857
+ * wholly witnessed by the question plus `id`'s own bytes (evidence.ts), with
858
+ * the question supplying at least one window the node does not. Ranked by
859
+ * how much of the question witnesses it; null when none is named.
860
+ *
861
+ * THE NODE'S OWN BYTES ARE MATERIAL because a derivation stands on them. On
862
+ * the second hop of `Where was the place of death of the director of film
863
+ * Beat Girl?` the node is `Edmond T. Gréville` — reached, never written — and
864
+ * its fact's establishing context `Edmond T. Gréville place of death` is held
865
+ * by neither the question nor the node, only by both. Measured over 5,236
866
+ * held-out 2Wiki questions: such a context is wholly witnessed by the
867
+ * question alone 69 times, by the question and the first hop 2,153 times.
868
+ *
869
+ * BOUNDED: predecessor reads share one √N budget across the candidates (see
870
+ * the loop below), asked cheapest first; past it the tier abstains for the
871
+ * rest, metered, and the ladder decides as before.
872
+ * Each form is read at most to the material's length — a form longer than
873
+ * everything at hand cannot be wholly witnessed without repeating it. */
874
+ function askedContinuations(
875
+ ctx: MindContext,
876
+ id: number,
877
+ nx: readonly number[],
878
+ asked: { bytes: Uint8Array; index: WindowIndex },
879
+ ): number[] | null {
880
+ return askedEntry(ctx, id, nx, asked).named;
881
+ }
882
+
883
+ /** The question spans that NAMED `pick` among `id`'s continuations — the
884
+ * evidence a projection through that pick stands on, so a mechanism can
885
+ * account for what the question said about it (mechanism-market.md:
886
+ * evidence travels). Empty when the question names no continuation of `id`
887
+ * or names others. */
888
+ export function askedEvidence(
889
+ ctx: MindContext,
890
+ id: number,
891
+ pick: number,
892
+ ): Array<[number, number]> {
893
+ const asked = ctx._edgeAsked;
894
+ if (asked === null) return [];
895
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
896
+ const entry = askedEntry(ctx, id, nx, asked);
897
+ return entry.named?.includes(pick) ? entry.spans.get(pick) ?? [] : [];
898
+ }
899
+
900
+ interface AskedEntry {
901
+ named: number[] | null;
902
+ /** Per named continuation, the question spans that witnessed it. */
903
+ spans: Map<number, Array<[number, number]>>;
904
+ }
905
+
906
+ function askedEntry(
907
+ ctx: MindContext,
908
+ id: number,
909
+ nx: readonly number[],
910
+ asked: { bytes: Uint8Array; index: WindowIndex },
911
+ ): AskedEntry {
912
+ let memo = askedMemo.get(asked);
913
+ if (memo === undefined) askedMemo.set(asked, memo = new Map());
914
+ const hit = memo.get(id);
915
+ if (hit !== undefined && !ctx.trace) return hit;
916
+ const entry = askedContinuationsImpl(ctx, id, nx, asked);
917
+ memo.set(id, entry);
918
+ return entry;
919
+ }
920
+
921
+ /** One pick per node per question — every mechanism of a response asks the
922
+ * same node about the same question (the guided-pick memo's own reason). */
923
+ const askedMemo = new WeakMap<object, Map<number, AskedEntry>>();
924
+
925
+ function askedContinuationsImpl(
926
+ ctx: MindContext,
927
+ id: number,
928
+ nx: readonly number[],
929
+ asked: { bytes: Uint8Array; index: WindowIndex },
930
+ ): AskedEntry {
931
+ const none: AskedEntry = { named: null, spans: new Map() };
932
+ const W = ctx.space.maxGroup;
933
+ // A SATURATED READ IS NOT A CANDIDATE SET. When the continuations came back
934
+ // at the √N cap the read may have cut the named one off, so "none of these is
935
+ // named" and "this is the named one" are both unfounded — and this is exactly
936
+ // where witnessing would read most. The tier abstains, metered, and the
937
+ // distributional ladder decides as it always has.
938
+ if (nx.length >= hubBound(ctx)) {
939
+ if (ctx.meter) ctx.meter.askedReadsSaturated++;
940
+ return none;
941
+ }
942
+ const cache = getStructCache(ctx);
943
+ const ownCap = asked.bytes.length * W;
944
+ const own = offsetCanon(ctx, read(ctx, id, ownCap));
945
+ const ownIndex = windowIndex(own, W);
946
+ // Naming needs the question to say at least one window the node does not:
947
+ // when the node already holds every window of the question (the question IS
948
+ // this context, or a piece of it), nothing can be named, and nothing is read.
949
+ let beyond = false;
950
+ for (const key of asked.index.keys()) {
951
+ if (!ownIndex.has(key)) {
952
+ beyond = true;
953
+ break;
954
+ }
955
+ }
956
+ if (!beyond) return none;
957
+ const indexes = [asked.index, ownIndex];
958
+ const formCap = asked.bytes.length + own.length;
959
+ // BOUNDED READS (bounded-reads.md): the decision reads at most √N
960
+ // establishing contexts — floored at the write side's own arity `chainReach(W)`
961
+ // so a store too small for √N to cover one fact's questions still decides.
962
+ // Candidates are asked CHEAPEST FIRST (fewest establishing contexts): a common
963
+ // reply established by hundreds of contexts would otherwise spend the whole
964
+ // allowance alone. The order changes what is READ, never what wins: scores
965
+ // are compared afterwards in the continuations' own order.
966
+ let budget = Math.max(hubBound(ctx), chainReach(W));
967
+ const order = nx
968
+ .map((n, at) => ({ n, at, support: cachedPrevCount(ctx, n, cache) }))
969
+ .filter((c) => c.support >= 2) // only `id` establishes the rest
970
+ .sort((a, b) => a.support - b.support || a.at - b.at);
971
+ const scored: Array<
972
+ {
973
+ n: number;
974
+ at: number;
975
+ score: number;
976
+ by: number;
977
+ spans: Array<[number, number]>;
978
+ }
979
+ > = [];
980
+ for (const { n, at, support } of order) {
981
+ if (support > budget) {
982
+ if (ctx.meter) ctx.meter.askedReadsSaturated++;
983
+ break;
984
+ }
985
+ budget -= support;
986
+ if (ctx.meter) ctx.meter.askedPredecessorReads += support;
987
+ let score = 0;
988
+ let by = -1;
989
+ let spans: Array<[number, number]> = [];
990
+ for (const c of ctx.store.prevFirst(n, support)) {
991
+ if (c === id) continue;
992
+ // The form's FIRST window decides most refusals: one short prefix read
993
+ // before the whole form is reconstructed (a conversation-length
994
+ // predecessor would otherwise be read in full to fail on its opening).
995
+ const head = offsetCanon(ctx, read(ctx, c, W));
996
+ if (head.length < W) continue;
997
+ if (!indexes.some((ix) => ix.has(latin1(head)))) continue;
998
+ const form = read(ctx, c, formCap + 1);
999
+ if (form.length < W || form.length > formCap) continue;
1000
+ const w = witness(offsetCanon(ctx, form), indexes, W);
1001
+ if (!w.complete || w.bytes < W) continue;
1002
+ if (w.bytes > score) {
1003
+ score = w.bytes;
1004
+ by = c;
1005
+ spans = w.spans;
1006
+ }
1007
+ }
1008
+ if (score > 0) scored.push({ n, at, score, by, spans });
1009
+ }
1010
+ scored.sort((a, b) => a.at - b.at);
1011
+ let best: number[] = [];
1012
+ let bestBytes = 0;
1013
+ let witnessed: number | null = null;
1014
+ for (const { n, score, by } of scored) {
1015
+ if (score > bestBytes) {
1016
+ best = [n];
1017
+ bestBytes = score;
1018
+ witnessed = by;
1019
+ } else if (score === bestBytes) best.push(n);
1020
+ }
1021
+ if (best.length === 0) return none;
1022
+ if (ctx.meter) ctx.meter.askedContinuations++;
1023
+ if (ctx.trace && witnessed !== null) {
1024
+ ctx.trace.step(
1025
+ "askedContinuation",
1026
+ [rItemShort(ctx, id, "node"), rItemShort(ctx, witnessed, "asked")],
1027
+ best.map((n) => rItemShort(ctx, n, "named")),
1028
+ `${nx.length} continuations — the question witnesses ` +
1029
+ `${best.length === 1 ? "one's" : `${best.length}'`} own establishing ` +
1030
+ `context (${bestBytes} question byte(s) beyond the node)`,
1031
+ );
1032
+ }
1033
+ const evidence = new Map<number, Array<[number, number]>>();
1034
+ for (const c of scored) if (best.includes(c.n)) evidence.set(c.n, c.spans);
1035
+ return { named: best, spans: evidence };
1036
+ }
1037
+
805
1038
  /** The perceived gist of a candidate node, through the session gist cache.
806
1039
  * Re-gisting a candidate is a full river fold of its bytes — the measured
807
1040
  * recall bottleneck (a hub context offers up to √N continuations, EACH
@@ -903,6 +1136,86 @@ export function allWindowsAreScaffolding(
903
1136
  return sawOne;
904
1137
  }
905
1138
 
1139
+ /** Per offset of `bytes`: 1 when the W-window there is a stored form contained
1140
+ * in more than √N places (corpus-global scaffolding; see the floor below),
1141
+ * else 0. Memoised per
1142
+ * byte array for the life of the store's read-only response. */
1143
+ export function hubWindows(ctx: MindContext, bytes: Uint8Array): Uint8Array {
1144
+ const hit = hubWindowMemo.get(bytes);
1145
+ if (hit !== undefined) return hit;
1146
+ const W = ctx.space.maxGroup;
1147
+ // Floored at the write side's arity: inside ONE deposit's fold a window is
1148
+ // already contained by up to `chainReach(W)` chunks and branches, so on a
1149
+ // store of a few facts the √N reading would call every window frame — that
1150
+ // is fold structure, not corpus commonality.
1151
+ const bound = Math.max(hubBound(ctx), chainReach(W));
1152
+ const hub = new Uint8Array(Math.max(0, bytes.length - W + 1));
1153
+ for (let o = 0; o < hub.length; o++) {
1154
+ const ids = leafIdRun(ctx, bytes, o, o + W);
1155
+ const id = ids === null ? null : ctx.store.findBranch(ids);
1156
+ if (id !== null && ctx.store.containersSlice(id, bound, 1).length > 0) {
1157
+ hub[o] = 1;
1158
+ }
1159
+ }
1160
+ hubWindowMemo.set(bytes, hub);
1161
+ return hub;
1162
+ }
1163
+ const hubWindowMemo = new WeakMap<Uint8Array, Uint8Array>();
1164
+
1165
+ /** The query's SCAFFOLDING CORE, as spans: the bytes every W-window over which
1166
+ * is a hub (see {@link hubWindows}) — what is nothing but frame, where
1167
+ * {@link scaffoldExtents} is what a frame window reaches. */
1168
+ export function scaffoldSpans(
1169
+ ctx: MindContext,
1170
+ query: Uint8Array,
1171
+ ): Array<[number, number]> {
1172
+ const W = ctx.space.maxGroup;
1173
+ const hub = hubWindows(ctx, query);
1174
+ const n = hub.length;
1175
+ if (n <= 0) return [];
1176
+ const spans: Array<[number, number]> = [];
1177
+ let start = -1;
1178
+ for (let i = 0; i < query.length; i++) {
1179
+ let all = true;
1180
+ for (let o = Math.max(0, i - W + 1); o <= Math.min(i, n - 1); o++) {
1181
+ if (!hub[o]) {
1182
+ all = false;
1183
+ break;
1184
+ }
1185
+ }
1186
+ if (all && start < 0) start = i;
1187
+ if (!all && start >= 0) {
1188
+ spans.push([start, i]);
1189
+ start = -1;
1190
+ }
1191
+ }
1192
+ if (start >= 0) spans.push([start, query.length]);
1193
+ return spans;
1194
+ }
1195
+
1196
+ /** The EXTENTS of the query's SCAFFOLDING windows, merged: every byte some
1197
+ * W-window reaches that is a stored form contained in more than √N places —
1198
+ * corpus-global commonality (commonality.md), the same "hub" reading as
1199
+ * {@link allWindowsAreScaffolding} and the bridge's `explainedSpan`. A window
1200
+ * the store never saw is NOT scaffolding. The extent, not the core, is what
1201
+ * a coverage test needs: a span that still holds one hub window can be
1202
+ * "carried" by any fact that holds that window. */
1203
+ export function scaffoldExtents(
1204
+ ctx: MindContext,
1205
+ query: Uint8Array,
1206
+ ): Array<[number, number]> {
1207
+ const W = ctx.space.maxGroup;
1208
+ const hub = hubWindows(ctx, query);
1209
+ const spans: Array<[number, number]> = [];
1210
+ for (let o = 0; o < hub.length; o++) {
1211
+ if (!hub[o]) continue;
1212
+ const last = spans[spans.length - 1];
1213
+ if (last !== undefined && o <= last[1]) last[1] = o + W;
1214
+ else spans.push([o, o + W]);
1215
+ }
1216
+ return spans;
1217
+ }
1218
+
906
1219
  // ── THE PREFIX SUPPLY ───────────────────────────────────────────────────────
907
1220
  //
908
1221
  // A RETRIEVAL capability, not a grounding one: "which trained forms does this
package/src/mind/types.ts CHANGED
@@ -12,6 +12,7 @@ import type { MindConfig } from "../config.js";
12
12
  import type { Meter } from "../meter.js";
13
13
  import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
14
14
  import type { Rationale } from "./rationale.js";
15
+ import type { WindowIndex } from "./evidence.js";
15
16
  import type { ContentFold, Grid } from "../geometry.js";
16
17
 
17
18
  /** One {@link MindContext._depositTrees} entry — see that field's doc.
@@ -411,6 +412,11 @@ export interface MindContext extends GraphSearchHost {
411
412
  * ordinary respond() and for the first turn of a conversation. */
412
413
  currentTurnStart: number;
413
414
  _edgeGuide: Vec | null;
415
+ /** The question currently being answered, as the material `chooseNext`'s
416
+ * exact tier witnesses a continuation's establishing contexts against —
417
+ * its bytes (canonical when the response's canon preserves offsets) and
418
+ * their window index. Set and cleared with `_edgeGuide`. */
419
+ _edgeAsked: { bytes: Uint8Array; index: WindowIndex } | null;
414
420
  _edgeChoice: Map<number, number>;
415
421
  _prevSeen: Set<number> | null;
416
422
  /** Session cache of node-id → perceived gist, for candidate scoring
@@ -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
+ });