@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.
- package/dist/src/meter.d.ts +19 -0
- package/dist/src/meter.js +19 -0
- package/dist/src/mind/evidence.d.ts +30 -0
- package/dist/src/mind/evidence.js +102 -0
- package/dist/src/mind/mechanisms/cast.d.ts +1 -1
- package/dist/src/mind/mechanisms/cast.js +73 -12
- package/dist/src/mind/mechanisms/cover.js +25 -3
- package/dist/src/mind/mechanisms/recall.js +93 -9
- package/dist/src/mind/mechanisms/reference.js +25 -0
- package/dist/src/mind/mind.d.ts +7 -0
- package/dist/src/mind/mind.js +3 -0
- package/dist/src/mind/pipeline-mechanism.js +15 -1
- package/dist/src/mind/pipeline.js +51 -8
- package/dist/src/mind/reasoning.js +65 -18
- package/dist/src/mind/resonance.d.ts +3 -2
- package/dist/src/mind/resonance.js +3 -2
- package/dist/src/mind/traverse.d.ts +94 -2
- package/dist/src/mind/traverse.js +611 -7
- package/dist/src/mind/types.d.ts +12 -0
- package/docs/INDEX.md +18 -17
- package/docs/INVARIANTS.md +1 -0
- package/docs/architecture/evidence.md +256 -0
- package/docs/architecture/exact-vs-approximate.md +10 -9
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/meter.ts +19 -0
- package/src/mind/evidence.ts +125 -0
- package/src/mind/mechanisms/cast.ts +77 -3
- package/src/mind/mechanisms/cover.ts +42 -3
- package/src/mind/mechanisms/recall.ts +123 -11
- package/src/mind/mechanisms/reference.ts +24 -1
- package/src/mind/mind.ts +6 -0
- package/src/mind/pipeline-mechanism.ts +15 -1
- package/src/mind/pipeline.ts +64 -8
- package/src/mind/reasoning.ts +82 -17
- package/src/mind/resonance.ts +3 -2
- package/src/mind/traverse.ts +652 -7
- package/src/mind/types.ts +10 -0
- package/test/154-the-question-names-the-step.test.mjs +368 -0
- package/test/155-the-relation-read-off-another-instance.test.mjs +376 -0
- package/test/29-counterfactual.test.mjs +21 -11
- package/test/76-reference-binding.test.mjs +32 -0
package/src/mind/traverse.ts
CHANGED
|
@@ -8,14 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
import { cosine, Vec } from "../vec.js";
|
|
10
10
|
import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
|
|
11
|
-
import { gistOf, read } from "./primitives.js";
|
|
12
|
-
import {
|
|
11
|
+
import { gistOf, read, resolve } from "./primitives.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 { concatBytes, indexOf, 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 ─────────────────────────────────────────────
|
|
@@ -686,7 +693,9 @@ export function guidedNext(
|
|
|
686
693
|
// policy climbMemo and recogniseMemo follow (every mechanism must emit its
|
|
687
694
|
// own steps; a memo hit would swallow the repeat's `disambiguate` step).
|
|
688
695
|
// Consistency does not need the memo: chooseNext is a pure function of the
|
|
689
|
-
// (read-only) store
|
|
696
|
+
// (read-only) store, the guide and the consensus climb's points once they
|
|
697
|
+
// exist (the memo is cleared when they arrive), so recomputation yields the
|
|
698
|
+
// same pick.
|
|
690
699
|
if (!ctx.trace) {
|
|
691
700
|
const memo = ctx._edgeChoice.get(node);
|
|
692
701
|
if (memo !== undefined) return memo === -1 ? undefined : memo;
|
|
@@ -696,8 +705,10 @@ export function guidedNext(
|
|
|
696
705
|
return pick;
|
|
697
706
|
}
|
|
698
707
|
|
|
699
|
-
/** Disambiguate among a node's learnt continuations by
|
|
700
|
-
*
|
|
708
|
+
/** Disambiguate among a node's learnt continuations: first by the question's
|
|
709
|
+
* own witness of an establishing context (the exact tier, see
|
|
710
|
+
* {@link askedContinuations}), then by distributional support. NOTE the
|
|
711
|
+
* `guide` contract: its VALUE is deliberately unused —
|
|
701
712
|
* only its PRESENCE gates disambiguation (a null guide means no query is in
|
|
702
713
|
* flight, so structural walkers keep plain first-edge behaviour). The
|
|
703
714
|
* gist-cosine of short answer candidates against a query guide is dominated
|
|
@@ -721,13 +732,30 @@ export function chooseNext(
|
|
|
721
732
|
if (nx.length === 0) return undefined;
|
|
722
733
|
if (nx.length === 1 || !guide) return nx[0];
|
|
723
734
|
|
|
735
|
+
// THE EXACT TIER — the continuation the QUESTION names. Every other
|
|
736
|
+
// disambiguation below reads popularity, and is right to refuse the gist (see
|
|
737
|
+
// the doc above); but the corpus also wrote down, for each continuation,
|
|
738
|
+
// WHICH QUESTIONS IT ANSWERS — its establishing contexts — and a question is
|
|
739
|
+
// not a gist. When one of them is witnessed by the asker's bytes plus the
|
|
740
|
+
// node's own, that continuation is the one being asked for. Measured on the
|
|
741
|
+
// 31.7M-node store: `Who is the father of Frederick II?` answered the
|
|
742
|
+
// citizenship fact (the most-poured of eight) while `Frederick II father` —
|
|
743
|
+
// one of the father fact's own establishing contexts — lay wholly inside the
|
|
744
|
+
// question. Exact, so it ranks first (exact-vs-approximate.md); when nothing
|
|
745
|
+
// is witnessed the ladder below decides exactly as before.
|
|
746
|
+
const asked = ctx._edgeAsked;
|
|
747
|
+
const named = asked === null ? null : askedContinuations(ctx, id, nx, asked);
|
|
748
|
+
if (named !== null && named.length === 1) return named[0];
|
|
749
|
+
|
|
724
750
|
// Cap candidates at √N — the same bound the original chooseAmong used.
|
|
725
751
|
// A hub context can accumulate thousands of continuations; the best-fit
|
|
726
752
|
// one is among the first √N by insertion order (edges are never deleted,
|
|
727
753
|
// so the oldest are the most established). A strongly-supported edge
|
|
728
754
|
// inserted beyond the cap is invisible here — the deliberate trade
|
|
729
|
-
// against paying O(fan-out) count reads on every disambiguation.
|
|
730
|
-
|
|
755
|
+
// against paying O(fan-out) count reads on every disambiguation. Several
|
|
756
|
+
// continuations named EQUALLY by the question are told apart by the same
|
|
757
|
+
// ladder, over them alone.
|
|
758
|
+
const capped = named ?? nx; // already the hub-capped prefix, by the read above
|
|
731
759
|
|
|
732
760
|
// Distributional-evidence disambiguation, consulting BOTH read-outs of the
|
|
733
761
|
// evidence the training poured:
|
|
@@ -802,6 +830,543 @@ export function chooseNext(
|
|
|
802
830
|
return best;
|
|
803
831
|
}
|
|
804
832
|
|
|
833
|
+
/** The response canonicalizer's reading of `bytes` when it keeps every offset
|
|
834
|
+
* — so a window found in the canonical bytes sits at the same place in the
|
|
835
|
+
* asker's — else the bytes themselves. Text canon is offset-preserving on
|
|
836
|
+
* ASCII without interior whitespace runs; where it is not, the raw bytes are
|
|
837
|
+
* read and a case-variant window simply does not match. */
|
|
838
|
+
export function offsetCanon(ctx: MindContext, bytes: Uint8Array): Uint8Array {
|
|
839
|
+
if (ctx.canon === null) return bytes;
|
|
840
|
+
const c = ctx.canon(bytes);
|
|
841
|
+
return c.length === bytes.length ? c : bytes;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** A FRAGMENT ANSWERS OTHER QUESTIONS. A recognised form that sits inside
|
|
845
|
+
* other forms (it has structural parents or containers) holds continuations
|
|
846
|
+
* because the forms it is a piece of were asked — suffix inheritance gives
|
|
847
|
+
* `f death` the answer of every `… place of death` question, and `director`
|
|
848
|
+
* the answer of every `… director` one. Choosing one of several such
|
|
849
|
+
* continuations by popularity voices some other question's answer. A
|
|
850
|
+
* fragment with several continuations leads somewhere FOR THIS QUESTION only
|
|
851
|
+
* when the question names one (the exact tier below). A form that is
|
|
852
|
+
* (nearly) the whole question is not a piece of it: the question says
|
|
853
|
+
* nothing beyond it, so its continuations answer THIS question. Read by
|
|
854
|
+
* every mechanism that projects through a recognised site (cover's sites,
|
|
855
|
+
* recall's argument binding). */
|
|
856
|
+
export function answersOtherQuestions(
|
|
857
|
+
ctx: MindContext,
|
|
858
|
+
id: number,
|
|
859
|
+
queryLen: number,
|
|
860
|
+
siteLen: number,
|
|
861
|
+
): boolean {
|
|
862
|
+
const asked = ctx._edgeAsked;
|
|
863
|
+
if (asked === null || queryLen - siteLen < ctx.space.maxGroup) return false;
|
|
864
|
+
if (!(ctx.store.hasParents(id) || ctx.store.hasContainers(id))) return false;
|
|
865
|
+
if (ctx.store.nextFirst(id, 2).length < 2) return false;
|
|
866
|
+
return namedContinuations(ctx, id, asked) === null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** The continuations of `id` that `asked` NAMES (see {@link
|
|
870
|
+
* askedContinuations}) — for a caller holding material other than the whole
|
|
871
|
+
* question: the multi-hop walk asks with what of the question no product has
|
|
872
|
+
* restated yet. null when none is named. */
|
|
873
|
+
export function namedContinuations(
|
|
874
|
+
ctx: MindContext,
|
|
875
|
+
id: number,
|
|
876
|
+
asked: { bytes: Uint8Array; index: WindowIndex },
|
|
877
|
+
): number[] | null {
|
|
878
|
+
const nx = ctx.store.nextFirst(id, hubBound(ctx));
|
|
879
|
+
return nx.length === 0 ? null : askedContinuations(ctx, id, nx, asked);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** The continuations of `id` (among `nx`) that the question NAMES: one of
|
|
883
|
+
* their establishing contexts — a predecessor other than `id` itself — is
|
|
884
|
+
* wholly witnessed by the question plus `id`'s own bytes (evidence.ts), with
|
|
885
|
+
* the question supplying at least one window the node does not. Ranked by
|
|
886
|
+
* how much of the question witnesses it; null when none is named.
|
|
887
|
+
*
|
|
888
|
+
* THE NODE'S OWN BYTES ARE MATERIAL because a derivation stands on them. On
|
|
889
|
+
* the second hop of `Where was the place of death of the director of film
|
|
890
|
+
* Beat Girl?` the node is `Edmond T. Gréville` — reached, never written — and
|
|
891
|
+
* its fact's establishing context `Edmond T. Gréville place of death` is held
|
|
892
|
+
* by neither the question nor the node, only by both. Measured over 5,236
|
|
893
|
+
* held-out 2Wiki questions: such a context is wholly witnessed by the
|
|
894
|
+
* question alone 69 times, by the question and the first hop 2,153 times.
|
|
895
|
+
*
|
|
896
|
+
* BOUNDED: predecessor reads share one √N budget across the candidates (see
|
|
897
|
+
* the loop below), asked cheapest first; past it the tier abstains for the
|
|
898
|
+
* rest, metered, and the ladder decides as before.
|
|
899
|
+
* Each form is read at most to the material's length — a form longer than
|
|
900
|
+
* everything at hand cannot be wholly witnessed without repeating it. */
|
|
901
|
+
function askedContinuations(
|
|
902
|
+
ctx: MindContext,
|
|
903
|
+
id: number,
|
|
904
|
+
nx: readonly number[],
|
|
905
|
+
asked: { bytes: Uint8Array; index: WindowIndex },
|
|
906
|
+
): number[] | null {
|
|
907
|
+
return askedEntry(ctx, id, nx, asked).named;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** The question spans that NAMED `pick` among `id`'s continuations — the
|
|
911
|
+
* evidence a projection through that pick stands on, so a mechanism can
|
|
912
|
+
* account for what the question said about it (mechanism-market.md:
|
|
913
|
+
* evidence travels). Empty when the question names no continuation of `id`
|
|
914
|
+
* or names others. */
|
|
915
|
+
export function askedEvidence(
|
|
916
|
+
ctx: MindContext,
|
|
917
|
+
id: number,
|
|
918
|
+
pick: number,
|
|
919
|
+
): Array<[number, number]> {
|
|
920
|
+
const asked = ctx._edgeAsked;
|
|
921
|
+
if (asked === null) return [];
|
|
922
|
+
const nx = ctx.store.nextFirst(id, hubBound(ctx));
|
|
923
|
+
const entry = askedEntry(ctx, id, nx, asked);
|
|
924
|
+
return entry.named?.includes(pick) ? entry.spans.get(pick) ?? [] : [];
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
interface AskedEntry {
|
|
928
|
+
named: number[] | null;
|
|
929
|
+
/** Per named continuation, the question spans that witnessed it. */
|
|
930
|
+
spans: Map<number, Array<[number, number]>>;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function askedEntry(
|
|
934
|
+
ctx: MindContext,
|
|
935
|
+
id: number,
|
|
936
|
+
nx: readonly number[],
|
|
937
|
+
asked: { bytes: Uint8Array; index: WindowIndex },
|
|
938
|
+
): AskedEntry {
|
|
939
|
+
let memo = askedMemo.get(asked);
|
|
940
|
+
if (memo === undefined) askedMemo.set(asked, memo = new Map());
|
|
941
|
+
const hit = memo.get(id);
|
|
942
|
+
if (hit !== undefined && !ctx.trace) return hit;
|
|
943
|
+
const entry = askedContinuationsImpl(ctx, id, nx, asked);
|
|
944
|
+
// Nothing named before the climb has run is provisional: a co-instance
|
|
945
|
+
// read from its points may still name one.
|
|
946
|
+
if (entry.named !== null || ctx._edgeAsked?.points !== undefined) {
|
|
947
|
+
memo.set(id, entry);
|
|
948
|
+
}
|
|
949
|
+
return entry;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** One pick per node per question — every mechanism of a response asks the
|
|
953
|
+
* same node about the same question (the guided-pick memo's own reason). */
|
|
954
|
+
const askedMemo = new WeakMap<object, Map<number, AskedEntry>>();
|
|
955
|
+
|
|
956
|
+
function askedContinuationsImpl(
|
|
957
|
+
ctx: MindContext,
|
|
958
|
+
id: number,
|
|
959
|
+
nx: readonly number[],
|
|
960
|
+
asked: { bytes: Uint8Array; index: WindowIndex },
|
|
961
|
+
): AskedEntry {
|
|
962
|
+
const none: AskedEntry = { named: null, spans: new Map() };
|
|
963
|
+
const W = ctx.space.maxGroup;
|
|
964
|
+
// A SATURATED READ IS NOT A CANDIDATE SET. When the continuations came back
|
|
965
|
+
// at the √N cap the read may have cut the named one off, so "none of these is
|
|
966
|
+
// named" and "this is the named one" are both unfounded — and this is exactly
|
|
967
|
+
// where witnessing would read most. The tier abstains, metered, and the
|
|
968
|
+
// distributional ladder decides as it always has.
|
|
969
|
+
if (nx.length >= hubBound(ctx)) {
|
|
970
|
+
if (ctx.meter) ctx.meter.askedReadsSaturated++;
|
|
971
|
+
return none;
|
|
972
|
+
}
|
|
973
|
+
const cache = getStructCache(ctx);
|
|
974
|
+
const ownCap = asked.bytes.length * W;
|
|
975
|
+
const own = offsetCanon(ctx, read(ctx, id, ownCap));
|
|
976
|
+
const ownIndex = windowIndex(own, W);
|
|
977
|
+
// Naming needs the question to say at least one window the node does not:
|
|
978
|
+
// when the node already holds every window of the question (the question IS
|
|
979
|
+
// this context, or a piece of it), nothing can be named, and nothing is read.
|
|
980
|
+
let beyond = false;
|
|
981
|
+
for (const key of asked.index.keys()) {
|
|
982
|
+
if (!ownIndex.has(key)) {
|
|
983
|
+
beyond = true;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (!beyond) return none;
|
|
988
|
+
const indexes = [asked.index, ownIndex];
|
|
989
|
+
const formCap = asked.bytes.length + own.length;
|
|
990
|
+
// BOUNDED READS (bounded-reads.md): the decision reads at most √N
|
|
991
|
+
// establishing contexts — floored at the write side's own arity `chainReach(W)`
|
|
992
|
+
// so a store too small for √N to cover one fact's questions still decides.
|
|
993
|
+
// Candidates are asked CHEAPEST FIRST (fewest establishing contexts): a common
|
|
994
|
+
// reply established by hundreds of contexts would otherwise spend the whole
|
|
995
|
+
// allowance alone. The order changes what is READ, never what wins: scores
|
|
996
|
+
// are compared afterwards in the continuations' own order.
|
|
997
|
+
const allowance = Math.max(hubBound(ctx), chainReach(W));
|
|
998
|
+
let budget = allowance;
|
|
999
|
+
const order = nx
|
|
1000
|
+
.map((n, at) => ({ n, at, support: cachedPrevCount(ctx, n, cache) }))
|
|
1001
|
+
.filter((c) => c.support >= 2) // only `id` establishes the rest
|
|
1002
|
+
.sort((a, b) => a.support - b.support || a.at - b.at);
|
|
1003
|
+
const scored: Array<
|
|
1004
|
+
{
|
|
1005
|
+
n: number;
|
|
1006
|
+
at: number;
|
|
1007
|
+
score: number;
|
|
1008
|
+
by: number;
|
|
1009
|
+
spans: Array<[number, number]>;
|
|
1010
|
+
}
|
|
1011
|
+
> = [];
|
|
1012
|
+
for (const { n, at, support } of order) {
|
|
1013
|
+
if (support > budget) {
|
|
1014
|
+
if (ctx.meter) ctx.meter.askedReadsSaturated++;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
budget -= support;
|
|
1018
|
+
if (ctx.meter) ctx.meter.askedPredecessorReads += support;
|
|
1019
|
+
let score = 0;
|
|
1020
|
+
let by = -1;
|
|
1021
|
+
let spans: Array<[number, number]> = [];
|
|
1022
|
+
for (const c of ctx.store.prevFirst(n, support)) {
|
|
1023
|
+
if (c === id) continue;
|
|
1024
|
+
// The form's FIRST window decides most refusals: one short prefix read
|
|
1025
|
+
// before the whole form is reconstructed (a conversation-length
|
|
1026
|
+
// predecessor would otherwise be read in full to fail on its opening).
|
|
1027
|
+
const head = offsetCanon(ctx, read(ctx, c, W));
|
|
1028
|
+
if (head.length < W) continue;
|
|
1029
|
+
if (!indexes.some((ix) => ix.has(latin1(head)))) continue;
|
|
1030
|
+
// AN ESTABLISHING CONTEXT IS A DEPOSITED ONE. A span interned inside
|
|
1031
|
+
// bigger forms inherits their edges (the fragment ` born?` leads to the
|
|
1032
|
+
// fact of every `Where was … born?` question), so witnessing it names
|
|
1033
|
+
// every one of them — measured on the 2Wiki fixture with one-hop
|
|
1034
|
+
// questions deposited, ` born?` named nine strangers' birthplaces. The
|
|
1035
|
+
// structural predicate pivotInto already reads: a form with parents or
|
|
1036
|
+
// containers was never a context on its own.
|
|
1037
|
+
if (
|
|
1038
|
+
cachedHasParents(ctx, c, cache) || ctx.store.hasContainers(c)
|
|
1039
|
+
) continue;
|
|
1040
|
+
const form = read(ctx, c, formCap + 1);
|
|
1041
|
+
if (form.length < W || form.length > formCap) continue;
|
|
1042
|
+
const w = witness(offsetCanon(ctx, form), indexes, W);
|
|
1043
|
+
if (!w.complete || w.bytes < W) continue;
|
|
1044
|
+
if (w.bytes > score) {
|
|
1045
|
+
score = w.bytes;
|
|
1046
|
+
by = c;
|
|
1047
|
+
spans = w.spans;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (score > 0) scored.push({ n, at, score, by, spans });
|
|
1051
|
+
}
|
|
1052
|
+
scored.sort((a, b) => a.at - b.at);
|
|
1053
|
+
let best: number[] = [];
|
|
1054
|
+
let bestBytes = 0;
|
|
1055
|
+
let witnessed: number | null = null;
|
|
1056
|
+
for (const { n, score, by } of scored) {
|
|
1057
|
+
if (score > bestBytes) {
|
|
1058
|
+
best = [n];
|
|
1059
|
+
bestBytes = score;
|
|
1060
|
+
witnessed = by;
|
|
1061
|
+
} else if (score === bestBytes) best.push(n);
|
|
1062
|
+
}
|
|
1063
|
+
if (best.length === 0) {
|
|
1064
|
+
return byCoInstance(ctx, id, nx, asked, formCap, allowance);
|
|
1065
|
+
}
|
|
1066
|
+
if (ctx.meter) ctx.meter.askedContinuations++;
|
|
1067
|
+
if (ctx.trace && witnessed !== null) {
|
|
1068
|
+
ctx.trace.step(
|
|
1069
|
+
"askedContinuation",
|
|
1070
|
+
[rItemShort(ctx, id, "node"), rItemShort(ctx, witnessed, "asked")],
|
|
1071
|
+
best.map((n) => rItemShort(ctx, n, "named")),
|
|
1072
|
+
`${nx.length} continuations — the question witnesses ` +
|
|
1073
|
+
`${best.length === 1 ? "one's" : `${best.length}'`} own establishing ` +
|
|
1074
|
+
`context (${bestBytes} question byte(s) beyond the node)`,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const evidence = new Map<number, Array<[number, number]>>();
|
|
1078
|
+
for (const c of scored) if (best.includes(c.n)) evidence.set(c.n, c.spans);
|
|
1079
|
+
return { named: best, spans: evidence };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** The climb's points the question HOLDS under the response's equivalence —
|
|
1083
|
+
* stored forms whose canonical bytes occur in the question's, as sites. The
|
|
1084
|
+
* recognition walk matches bytes and the fold's own boundaries, so a stored
|
|
1085
|
+
* `Man at Bath` inside `… of film Man At Bath work at?` is no site; the
|
|
1086
|
+
* consensus climb reaches it through the windows the two spellings share, and
|
|
1087
|
+
* containment under the canon (offset-preserving, so the span is the
|
|
1088
|
+
* question's own) decides. Approximate proposes, exact decides. Read off
|
|
1089
|
+
* the `chainReach(W)` most corroborated points; empty before the climb. */
|
|
1090
|
+
export function canonHeldPoints(
|
|
1091
|
+
ctx: MindContext,
|
|
1092
|
+
minLen: number,
|
|
1093
|
+
): Array<{ start: number; end: number; payload: number }> {
|
|
1094
|
+
const asked = ctx._edgeAsked;
|
|
1095
|
+
if (asked === null || asked.points === undefined || ctx.canon === null) {
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
const W = ctx.space.maxGroup;
|
|
1099
|
+
const out: Array<{ start: number; end: number; payload: number }> = [];
|
|
1100
|
+
for (const id of asked.points.slice(0, chainReach(W))) {
|
|
1101
|
+
const raw = read(ctx, id, asked.bytes.length);
|
|
1102
|
+
if (raw.length < minLen || raw.length >= asked.bytes.length) continue;
|
|
1103
|
+
const form = ctx.canon(raw);
|
|
1104
|
+
if (form.length !== raw.length) continue;
|
|
1105
|
+
const at = indexOf(asked.bytes, form, 0);
|
|
1106
|
+
if (at >= 0) out.push({ start: at, end: at + raw.length, payload: id });
|
|
1107
|
+
}
|
|
1108
|
+
return out;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** A CO-INSTANCE of the question: a stored form that shares the question's
|
|
1112
|
+
* FRAME — the same opening and the same close, byte for byte under the
|
|
1113
|
+
* response's equivalence — around ONE different filler. `Where was Peter
|
|
1114
|
+
* Jackson born?` against `Where was the director of film Beat Girl born?`
|
|
1115
|
+
* shares `Where was ` and ` born?`, and leaves `Peter Jackson` against `the
|
|
1116
|
+
* director of film Beat Girl`: another instance of the same question, never
|
|
1117
|
+
* this one. It is the ordered limit of witnessing: a frame is not a bag of
|
|
1118
|
+
* windows (an order-free reading lets `Lyon is a city in France` pass for an
|
|
1119
|
+
* instance of `what if the capital of France were Lyon?`, which NAMES Lyon;
|
|
1120
|
+
* and a coincidental window inside a filler splits it).
|
|
1121
|
+
*
|
|
1122
|
+
* The frame must reach one window, every window inside it must be held by
|
|
1123
|
+
* the material (so on the walk, a frame a product already said cannot name a
|
|
1124
|
+
* second step), and the form's filler must be a thing the corpus knows
|
|
1125
|
+
* (below) — a record that differs from the question only inside the frame
|
|
1126
|
+
* by a description (`converts sunlight into chemical energy` against
|
|
1127
|
+
* `works`) is the same question about the frame's own subject. (Requiring
|
|
1128
|
+
* the two fillers to share no window as well was measured redundant with
|
|
1129
|
+
* this and cost two answers on the 2Wiki fixture.) Returns the form's filler span in `raw` and the
|
|
1130
|
+
* question spans of the frame, or null. One reading for every consumer: the
|
|
1131
|
+
* exact tier transfers the relation through it, and the readers that would
|
|
1132
|
+
* VOICE such a form's own continuation refuse it. */
|
|
1133
|
+
export function coInstanceFiller(
|
|
1134
|
+
ctx: MindContext,
|
|
1135
|
+
raw: Uint8Array,
|
|
1136
|
+
indexes: ReadonlyArray<WindowIndex>,
|
|
1137
|
+
question: Uint8Array,
|
|
1138
|
+
): { span: [number, number]; spans: Array<[number, number]> } | null {
|
|
1139
|
+
const W = ctx.space.maxGroup;
|
|
1140
|
+
const form = offsetCanon(ctx, raw);
|
|
1141
|
+
const max = Math.min(form.length, question.length);
|
|
1142
|
+
let a = 0;
|
|
1143
|
+
while (a < max && form[a] === question[a]) a++;
|
|
1144
|
+
let b = 0;
|
|
1145
|
+
while (
|
|
1146
|
+
b < max - a &&
|
|
1147
|
+
form[form.length - 1 - b] === question[question.length - 1 - b]
|
|
1148
|
+
) b++;
|
|
1149
|
+
const fe = form.length - b;
|
|
1150
|
+
if (a + b < W || fe <= a || question.length - b <= a) return null;
|
|
1151
|
+
// Scaffolding is nobody's evidence, so a frame window that is a hub need not
|
|
1152
|
+
// be unsaid (` is ` in `Which country Leo Mittler is from?`, which the first
|
|
1153
|
+
// hop's `… is Arshad Khan.` already said).
|
|
1154
|
+
const hub = hubWindows(ctx, raw);
|
|
1155
|
+
const held = (o: number): boolean => {
|
|
1156
|
+
if (hub[o]) return true;
|
|
1157
|
+
const key = latin1(form.subarray(o, o + W));
|
|
1158
|
+
return indexes.some((ix) => ix.has(key));
|
|
1159
|
+
};
|
|
1160
|
+
for (let o = 0; o + W <= a; o++) if (!held(o)) return null;
|
|
1161
|
+
for (let o = fe; o + W <= form.length; o++) if (!held(o)) return null;
|
|
1162
|
+
// THE FILLER IS A THING THE CORPUS KNOWS: a stored context with
|
|
1163
|
+
// continuations of its own, opening where the slot opens (up to one window
|
|
1164
|
+
// earlier — a filler byte can sit in a window the question holds by chance:
|
|
1165
|
+
// the `T` of `Taika` inside `as t` of `was the`). `Peter Jackson` is one;
|
|
1166
|
+
// `converts sunlight into chemical energy`, the slot of `Explain how
|
|
1167
|
+
// photosynthesis …`, is a description of the frame's own subject, and the
|
|
1168
|
+
// record answers the question. The longest such context is the filler.
|
|
1169
|
+
// One content-addressed probe per byte (the `keyEnds` reading); only a run
|
|
1170
|
+
// some deposit spelled whole is looked up.
|
|
1171
|
+
const ids = leafIdPrefix(ctx, raw);
|
|
1172
|
+
let best: [number, number] | null = null;
|
|
1173
|
+
const cache = getStructCache(ctx);
|
|
1174
|
+
const reach = Math.min(ids.length, fe + W - 1);
|
|
1175
|
+
for (let st = Math.max(0, a - W + 1); st <= a; st++) {
|
|
1176
|
+
const run: number[] = [];
|
|
1177
|
+
for (let en = st + 1; en <= reach; en++) {
|
|
1178
|
+
run.push(ids[en - 1]);
|
|
1179
|
+
if (en - st < W) continue;
|
|
1180
|
+
// The flat branch over the whole run exists only where some deposit
|
|
1181
|
+
// spelled exactly these bytes — the cheap filter; the context it names
|
|
1182
|
+
// is then looked up by content.
|
|
1183
|
+
if (ctx.store.findBranch(run) === null) continue;
|
|
1184
|
+
if (best !== null && en - st <= best[1] - best[0]) continue;
|
|
1185
|
+
const id = resolve(ctx, raw.subarray(st, en));
|
|
1186
|
+
if (id !== null && cachedHasNext(ctx, id, cache)) best = [st, en];
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (best === null) return null;
|
|
1190
|
+
const spans: Array<[number, number]> = [];
|
|
1191
|
+
if (a > 0) spans.push([0, a]);
|
|
1192
|
+
if (b > 0) spans.push([question.length - b, question.length]);
|
|
1193
|
+
return { span: best, spans };
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** THE RELATION, READ OFF ANOTHER INSTANCE — the exact tier's second reading,
|
|
1197
|
+
* asked only when no establishing context of `id`'s continuations is
|
|
1198
|
+
* witnessed outright.
|
|
1199
|
+
*
|
|
1200
|
+
* `When was the director of film Jinpa born?` reaches `Pema Tseden`, whose
|
|
1201
|
+
* date-of-birth fact is established by `Pema Tseden date of birth`: the
|
|
1202
|
+
* question says `born`, the corpus says `date of birth`, and no window joins
|
|
1203
|
+
* them. What can join them is ANOTHER INSTANCE of the same question. A
|
|
1204
|
+
* stored context the question witnesses in every byte but ONE contiguous span
|
|
1205
|
+
* (`When was Peter Jackson born?`, residue `Peter Jackson`) is the question's
|
|
1206
|
+
* own frame around a different filler — a CO-INSTANCE. Its continuation is
|
|
1207
|
+
* established by other contexts too (`Peter Jackson date of birth`), and one
|
|
1208
|
+
* that holds the filler is the relation spelled the way the corpus spells it.
|
|
1209
|
+
* Put this node where the filler was and look the result up by content:
|
|
1210
|
+
* `Pema Tseden date of birth` exists, and its continuation is named.
|
|
1211
|
+
*
|
|
1212
|
+
* Nothing here is stored as a unit — not `born` ≈ `date of birth`, not the
|
|
1213
|
+
* frame — and nothing is approximate: the co-instance is witnessed, the
|
|
1214
|
+
* substitution is bytes, the target is an exact lookup that either exists
|
|
1215
|
+
* with this node's continuation or does not. A frame the question shares only
|
|
1216
|
+
* partly (`Where was … born?` against `When was …`) leaves two residues and
|
|
1217
|
+
* is no co-instance.
|
|
1218
|
+
*
|
|
1219
|
+
* BOUNDED: the frames are a property of the question and are read once per
|
|
1220
|
+
* question (`relationFrames`); co-instances are proposed by the edge-bearing
|
|
1221
|
+
* ancestors of its windows (`edgeAncestors`, the climb's own memoised reach;
|
|
1222
|
+
* a saturated window proposes nothing), rarest first, at most `allowance` of
|
|
1223
|
+
* them, each read once. A node pays one exact lookup per frame. */
|
|
1224
|
+
function byCoInstance(
|
|
1225
|
+
ctx: MindContext,
|
|
1226
|
+
id: number,
|
|
1227
|
+
nx: readonly number[],
|
|
1228
|
+
asked: { bytes: Uint8Array; index: WindowIndex },
|
|
1229
|
+
formCap: number,
|
|
1230
|
+
allowance: number,
|
|
1231
|
+
): AskedEntry {
|
|
1232
|
+
const none: AskedEntry = { named: null, spans: new Map() };
|
|
1233
|
+
const frames = relationFrames(ctx, asked, allowance);
|
|
1234
|
+
if (frames.length === 0) return none;
|
|
1235
|
+
const node = read(ctx, id, formCap);
|
|
1236
|
+
const support = new Map<number, Set<number>>();
|
|
1237
|
+
const spans = new Map<number, Array<[number, number]>>();
|
|
1238
|
+
const via = new Map<number, { q: number; t: number }>();
|
|
1239
|
+
for (const fr of frames) {
|
|
1240
|
+
const t = resolve(ctx, concatBytes([fr.prefix, node, fr.suffix]));
|
|
1241
|
+
if (t === null || t === id) continue;
|
|
1242
|
+
for (const n of ctx.store.nextFirst(t, allowance)) {
|
|
1243
|
+
if (!nx.includes(n)) continue;
|
|
1244
|
+
let by = support.get(n);
|
|
1245
|
+
if (by === undefined) support.set(n, by = new Set());
|
|
1246
|
+
for (const q of fr.by) by.add(q);
|
|
1247
|
+
if (!spans.has(n)) {
|
|
1248
|
+
spans.set(n, fr.spans);
|
|
1249
|
+
via.set(n, { q: fr.by[0], t });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
let best: number[] = [];
|
|
1254
|
+
let most = 0;
|
|
1255
|
+
for (const n of nx) {
|
|
1256
|
+
const k = support.get(n)?.size ?? 0;
|
|
1257
|
+
if (k === 0) continue;
|
|
1258
|
+
if (k > most) {
|
|
1259
|
+
best = [n];
|
|
1260
|
+
most = k;
|
|
1261
|
+
} else if (k === most) best.push(n);
|
|
1262
|
+
}
|
|
1263
|
+
if (best.length === 0) return none;
|
|
1264
|
+
if (ctx.meter) ctx.meter.coInstanceNamings++;
|
|
1265
|
+
const first = via.get(best[0]);
|
|
1266
|
+
if (ctx.trace && first !== undefined) {
|
|
1267
|
+
ctx.trace.step(
|
|
1268
|
+
"askedByCoInstance",
|
|
1269
|
+
[
|
|
1270
|
+
rItemShort(ctx, id, "node"),
|
|
1271
|
+
rItemShort(ctx, first.q, "co-instance"),
|
|
1272
|
+
rItemShort(ctx, first.t, "asked"),
|
|
1273
|
+
],
|
|
1274
|
+
best.map((n) => rItemShort(ctx, n, "named")),
|
|
1275
|
+
`${nx.length} continuations — no establishing context is witnessed; ` +
|
|
1276
|
+
`${most} co-instance(s) of the question carry the relation to ` +
|
|
1277
|
+
`${best.length === 1 ? "one" : best.length} of them`,
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
const evidence = new Map<number, Array<[number, number]>>();
|
|
1281
|
+
for (const n of best) evidence.set(n, spans.get(n) ?? []);
|
|
1282
|
+
return { named: best, spans: evidence };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** One RELATION FRAME read off a co-instance: an establishing context of the
|
|
1286
|
+
* co-instance's continuation with the filler cut out (`Peter Jackson place
|
|
1287
|
+
* of birth` → `` + · + ` place of birth`). Any node put in the gap spells
|
|
1288
|
+
* that node's context for the same relation. */
|
|
1289
|
+
interface RelationFrame {
|
|
1290
|
+
/** The co-instances that spell the relation this way (first one traced). */
|
|
1291
|
+
by: number[];
|
|
1292
|
+
prefix: Uint8Array;
|
|
1293
|
+
suffix: Uint8Array;
|
|
1294
|
+
spans: Array<[number, number]>;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** The relation frames the question's co-instances carry — a property of the
|
|
1298
|
+
* QUESTION, not of the node asked about, so it is read once per question
|
|
1299
|
+
* (and once per walk material) and every node only pays the exact lookups.
|
|
1300
|
+
* A question the corpus holds verbatim is its own instance and reads none. */
|
|
1301
|
+
function relationFrames(
|
|
1302
|
+
ctx: MindContext,
|
|
1303
|
+
asked: { bytes: Uint8Array; index: WindowIndex },
|
|
1304
|
+
allowance: number,
|
|
1305
|
+
): RelationFrame[] {
|
|
1306
|
+
// A co-instance is the question's frame around ONE other filler, so it is
|
|
1307
|
+
// read at most to twice the question's length.
|
|
1308
|
+
const formCap = 2 * asked.bytes.length;
|
|
1309
|
+
const hit = frameMemo.get(asked);
|
|
1310
|
+
if (hit !== undefined && !ctx.trace) return hit;
|
|
1311
|
+
const whole = ctx._edgeAsked;
|
|
1312
|
+
// THE PROPOSALS ARE THE CLIMB'S. The consensus climb already scored the
|
|
1313
|
+
// stored forms the question's regions reach — measured on the 2Wiki
|
|
1314
|
+
// fixture with one-hop questions, the co-instances were among its points —
|
|
1315
|
+
// so the tier reads co-instances from those and climbs nothing of its own.
|
|
1316
|
+
// Before the climb has run there is nothing to read, and nothing is
|
|
1317
|
+
// remembered: a later ask, after the climb, reads its points.
|
|
1318
|
+
const points = whole?.points;
|
|
1319
|
+
if (points === undefined) return [];
|
|
1320
|
+
const frames: RelationFrame[] = [];
|
|
1321
|
+
frameMemo.set(asked, frames);
|
|
1322
|
+
if (whole !== null && resolve(ctx, whole.bytes) !== null) return frames;
|
|
1323
|
+
const W = ctx.space.maxGroup;
|
|
1324
|
+
const cache = getStructCache(ctx);
|
|
1325
|
+
const deposited = (c: number): boolean =>
|
|
1326
|
+
!cachedHasParents(ctx, c, cache) && !ctx.store.hasContainers(c);
|
|
1327
|
+
const spelled = new Map<string, RelationFrame>();
|
|
1328
|
+
// Most corroborated first (the climb's own ranking), at most
|
|
1329
|
+
// `chainReach(W)` read — the exact tier's own floor.
|
|
1330
|
+
const proposals = points.filter((c) => cachedHasNext(ctx, c, cache))
|
|
1331
|
+
.slice(0, Math.min(allowance, chainReach(W)));
|
|
1332
|
+
for (const q of proposals) {
|
|
1333
|
+
if (!deposited(q)) continue;
|
|
1334
|
+
const raw = read(ctx, q, formCap + 1);
|
|
1335
|
+
if (raw.length > formCap) continue;
|
|
1336
|
+
if (ctx.meter) ctx.meter.coInstanceReads++;
|
|
1337
|
+
const co = coInstanceFiller(ctx, raw, [asked.index], asked.bytes);
|
|
1338
|
+
if (co === null) continue;
|
|
1339
|
+
const [fs, fe] = co.span;
|
|
1340
|
+
const filler = raw.subarray(fs, fe);
|
|
1341
|
+
for (const f of ctx.store.nextFirst(q, W)) {
|
|
1342
|
+
for (const c of ctx.store.prevFirst(f, allowance)) {
|
|
1343
|
+
if (c === q || !deposited(c)) continue;
|
|
1344
|
+
const cb = read(ctx, c, formCap + 1);
|
|
1345
|
+
const at = indexOf(cb, filler, 0);
|
|
1346
|
+
if (at < 0) continue;
|
|
1347
|
+
const prefix = cb.subarray(0, at);
|
|
1348
|
+
const suffix = cb.subarray(at + filler.length);
|
|
1349
|
+
if (prefix.length + suffix.length === 0) continue;
|
|
1350
|
+
const key = latin1(prefix) + "\u0000" + latin1(suffix);
|
|
1351
|
+
const known = spelled.get(key);
|
|
1352
|
+
if (known === undefined) {
|
|
1353
|
+
spelled.set(key, { by: [q], prefix, suffix, spans: co.spans });
|
|
1354
|
+
} else if (!known.by.includes(q)) known.by.push(q);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
// ONE INSTANCE AGREES WITH NOTHING. A relation is read off the corpus only
|
|
1359
|
+
// where two co-instances spell it the same way — the agreement `reference`
|
|
1360
|
+
// demands of a frame (MIN_INSTANCES), for the same reason: a single
|
|
1361
|
+
// coincidental alignment is not evidence of what the frame means. Measured
|
|
1362
|
+
// on the trained store, long dialogue turns proposed hundreds of one-off
|
|
1363
|
+
// "frames", each an exact lookup for every node asked about.
|
|
1364
|
+
for (const fr of spelled.values()) if (fr.by.length >= 2) frames.push(fr);
|
|
1365
|
+
frames.sort((a, b) => b.by.length - a.by.length);
|
|
1366
|
+
return frames;
|
|
1367
|
+
}
|
|
1368
|
+
const frameMemo = new WeakMap<object, RelationFrame[]>();
|
|
1369
|
+
|
|
805
1370
|
/** The perceived gist of a candidate node, through the session gist cache.
|
|
806
1371
|
* Re-gisting a candidate is a full river fold of its bytes — the measured
|
|
807
1372
|
* recall bottleneck (a hub context offers up to √N continuations, EACH
|
|
@@ -903,6 +1468,86 @@ export function allWindowsAreScaffolding(
|
|
|
903
1468
|
return sawOne;
|
|
904
1469
|
}
|
|
905
1470
|
|
|
1471
|
+
/** Per offset of `bytes`: 1 when the W-window there is a stored form contained
|
|
1472
|
+
* in more than √N places (corpus-global scaffolding; see the floor below),
|
|
1473
|
+
* else 0. Memoised per
|
|
1474
|
+
* byte array for the life of the store's read-only response. */
|
|
1475
|
+
export function hubWindows(ctx: MindContext, bytes: Uint8Array): Uint8Array {
|
|
1476
|
+
const hit = hubWindowMemo.get(bytes);
|
|
1477
|
+
if (hit !== undefined) return hit;
|
|
1478
|
+
const W = ctx.space.maxGroup;
|
|
1479
|
+
// Floored at the write side's arity: inside ONE deposit's fold a window is
|
|
1480
|
+
// already contained by up to `chainReach(W)` chunks and branches, so on a
|
|
1481
|
+
// store of a few facts the √N reading would call every window frame — that
|
|
1482
|
+
// is fold structure, not corpus commonality.
|
|
1483
|
+
const bound = Math.max(hubBound(ctx), chainReach(W));
|
|
1484
|
+
const hub = new Uint8Array(Math.max(0, bytes.length - W + 1));
|
|
1485
|
+
for (let o = 0; o < hub.length; o++) {
|
|
1486
|
+
const ids = leafIdRun(ctx, bytes, o, o + W);
|
|
1487
|
+
const id = ids === null ? null : ctx.store.findBranch(ids);
|
|
1488
|
+
if (id !== null && ctx.store.containersSlice(id, bound, 1).length > 0) {
|
|
1489
|
+
hub[o] = 1;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
hubWindowMemo.set(bytes, hub);
|
|
1493
|
+
return hub;
|
|
1494
|
+
}
|
|
1495
|
+
const hubWindowMemo = new WeakMap<Uint8Array, Uint8Array>();
|
|
1496
|
+
|
|
1497
|
+
/** The query's SCAFFOLDING CORE, as spans: the bytes every W-window over which
|
|
1498
|
+
* is a hub (see {@link hubWindows}) — what is nothing but frame, where
|
|
1499
|
+
* {@link scaffoldExtents} is what a frame window reaches. */
|
|
1500
|
+
export function scaffoldSpans(
|
|
1501
|
+
ctx: MindContext,
|
|
1502
|
+
query: Uint8Array,
|
|
1503
|
+
): Array<[number, number]> {
|
|
1504
|
+
const W = ctx.space.maxGroup;
|
|
1505
|
+
const hub = hubWindows(ctx, query);
|
|
1506
|
+
const n = hub.length;
|
|
1507
|
+
if (n <= 0) return [];
|
|
1508
|
+
const spans: Array<[number, number]> = [];
|
|
1509
|
+
let start = -1;
|
|
1510
|
+
for (let i = 0; i < query.length; i++) {
|
|
1511
|
+
let all = true;
|
|
1512
|
+
for (let o = Math.max(0, i - W + 1); o <= Math.min(i, n - 1); o++) {
|
|
1513
|
+
if (!hub[o]) {
|
|
1514
|
+
all = false;
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (all && start < 0) start = i;
|
|
1519
|
+
if (!all && start >= 0) {
|
|
1520
|
+
spans.push([start, i]);
|
|
1521
|
+
start = -1;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (start >= 0) spans.push([start, query.length]);
|
|
1525
|
+
return spans;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/** The EXTENTS of the query's SCAFFOLDING windows, merged: every byte some
|
|
1529
|
+
* W-window reaches that is a stored form contained in more than √N places —
|
|
1530
|
+
* corpus-global commonality (commonality.md), the same "hub" reading as
|
|
1531
|
+
* {@link allWindowsAreScaffolding} and the bridge's `explainedSpan`. A window
|
|
1532
|
+
* the store never saw is NOT scaffolding. The extent, not the core, is what
|
|
1533
|
+
* a coverage test needs: a span that still holds one hub window can be
|
|
1534
|
+
* "carried" by any fact that holds that window. */
|
|
1535
|
+
export function scaffoldExtents(
|
|
1536
|
+
ctx: MindContext,
|
|
1537
|
+
query: Uint8Array,
|
|
1538
|
+
): Array<[number, number]> {
|
|
1539
|
+
const W = ctx.space.maxGroup;
|
|
1540
|
+
const hub = hubWindows(ctx, query);
|
|
1541
|
+
const spans: Array<[number, number]> = [];
|
|
1542
|
+
for (let o = 0; o < hub.length; o++) {
|
|
1543
|
+
if (!hub[o]) continue;
|
|
1544
|
+
const last = spans[spans.length - 1];
|
|
1545
|
+
if (last !== undefined && o <= last[1]) last[1] = o + W;
|
|
1546
|
+
else spans.push([o, o + W]);
|
|
1547
|
+
}
|
|
1548
|
+
return spans;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
906
1551
|
// ── THE PREFIX SUPPLY ───────────────────────────────────────────────────────
|
|
907
1552
|
//
|
|
908
1553
|
// A RETRIEVAL capability, not a grounding one: "which trained forms does this
|