@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.
- package/dist/src/meter.d.ts +13 -0
- package/dist/src/meter.js +13 -0
- package/dist/src/mind/evidence.d.ts +24 -0
- package/dist/src/mind/evidence.js +90 -0
- package/dist/src/mind/mechanisms/cover.js +41 -3
- package/dist/src/mind/mechanisms/recall.js +10 -2
- package/dist/src/mind/mind.d.ts +6 -0
- package/dist/src/mind/mind.js +3 -0
- package/dist/src/mind/pipeline.js +46 -4
- package/dist/src/mind/reasoning.js +43 -8
- package/dist/src/mind/traverse.d.ts +42 -2
- package/dist/src/mind/traverse.js +282 -5
- package/dist/src/mind/types.d.ts +9 -0
- package/docs/INDEX.md +18 -17
- package/docs/INVARIANTS.md +1 -0
- package/docs/architecture/evidence.md +113 -0
- package/docs/architecture/exact-vs-approximate.md +10 -9
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/meter.ts +13 -0
- package/src/mind/evidence.ts +107 -0
- package/src/mind/mechanisms/cover.ts +58 -3
- package/src/mind/mechanisms/recall.ts +16 -2
- package/src/mind/mind.ts +4 -0
- package/src/mind/pipeline.ts +59 -4
- package/src/mind/reasoning.ts +54 -7
- package/src/mind/traverse.ts +318 -5
- package/src/mind/types.ts +6 -0
- package/test/154-the-question-names-the-step.test.mjs +281 -0
package/src/mind/traverse.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
700
|
-
*
|
|
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
|
-
|
|
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
|
+
});
|