@hviana/sema 0.2.4 → 0.2.6
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/package.json +1 -1
- package/src/geometry.ts +52 -3
- package/src/mind/attention.ts +1469 -120
- package/src/mind/index.ts +16 -0
- package/src/mind/junction.ts +125 -16
- package/src/mind/mind.ts +15 -0
- package/src/mind/rationale.ts +12 -4
- package/src/mind/traverse.ts +75 -7
- package/src/mind/types.ts +25 -0
- package/test/51-structural-resonance-ladder.test.mjs +552 -0
- package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
- package/test/53-cross-region-probe-instrumentation.test.mjs +356 -0
- package/dist/example/demo.d.ts +0 -1
- package/dist/example/demo.js +0 -39
- package/dist/example/train_base.d.ts +0 -87
- package/dist/example/train_base.js +0 -2252
- package/dist/src/alphabet.d.ts +0 -7
- package/dist/src/alphabet.js +0 -33
- package/dist/src/alu/src/alu.d.ts +0 -185
- package/dist/src/alu/src/alu.js +0 -440
- package/dist/src/alu/src/expr.d.ts +0 -61
- package/dist/src/alu/src/expr.js +0 -318
- package/dist/src/alu/src/index.d.ts +0 -11
- package/dist/src/alu/src/index.js +0 -19
- package/dist/src/alu/src/kernel-arith.d.ts +0 -16
- package/dist/src/alu/src/kernel-arith.js +0 -264
- package/dist/src/alu/src/kernel-bits.d.ts +0 -19
- package/dist/src/alu/src/kernel-bits.js +0 -152
- package/dist/src/alu/src/kernel-logic.d.ts +0 -4
- package/dist/src/alu/src/kernel-logic.js +0 -60
- package/dist/src/alu/src/kernel-nd.d.ts +0 -3
- package/dist/src/alu/src/kernel-nd.js +0 -208
- package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
- package/dist/src/alu/src/kernel-numeric.js +0 -366
- package/dist/src/alu/src/operation.d.ts +0 -168
- package/dist/src/alu/src/operation.js +0 -189
- package/dist/src/alu/src/parser.d.ts +0 -221
- package/dist/src/alu/src/parser.js +0 -577
- package/dist/src/alu/src/resonance.d.ts +0 -55
- package/dist/src/alu/src/resonance.js +0 -126
- package/dist/src/alu/src/text.d.ts +0 -31
- package/dist/src/alu/src/text.js +0 -73
- package/dist/src/alu/src/value.d.ts +0 -109
- package/dist/src/alu/src/value.js +0 -300
- package/dist/src/alu/test/alu.test.d.ts +0 -1
- package/dist/src/alu/test/alu.test.js +0 -764
- package/dist/src/bytes.d.ts +0 -14
- package/dist/src/bytes.js +0 -59
- package/dist/src/canon.d.ts +0 -26
- package/dist/src/canon.js +0 -57
- package/dist/src/config.d.ts +0 -111
- package/dist/src/config.js +0 -91
- package/dist/src/derive/src/deduction.d.ts +0 -125
- package/dist/src/derive/src/deduction.js +0 -155
- package/dist/src/derive/src/index.d.ts +0 -7
- package/dist/src/derive/src/index.js +0 -11
- package/dist/src/derive/src/priority-queue.d.ts +0 -20
- package/dist/src/derive/src/priority-queue.js +0 -73
- package/dist/src/derive/src/rewrite.d.ts +0 -56
- package/dist/src/derive/src/rewrite.js +0 -100
- package/dist/src/derive/src/trie.d.ts +0 -90
- package/dist/src/derive/src/trie.js +0 -217
- package/dist/src/derive/test/derive.test.d.ts +0 -1
- package/dist/src/derive/test/derive.test.js +0 -122
- package/dist/src/extension.d.ts +0 -37
- package/dist/src/extension.js +0 -7
- package/dist/src/geometry.d.ts +0 -176
- package/dist/src/geometry.js +0 -546
- package/dist/src/index.d.ts +0 -16
- package/dist/src/index.js +0 -18
- package/dist/src/ingest-cache.d.ts +0 -41
- package/dist/src/ingest-cache.js +0 -161
- package/dist/src/mind/articulation.d.ts +0 -6
- package/dist/src/mind/articulation.js +0 -99
- package/dist/src/mind/attention.d.ts +0 -88
- package/dist/src/mind/attention.js +0 -997
- package/dist/src/mind/bridge.d.ts +0 -30
- package/dist/src/mind/bridge.js +0 -569
- package/dist/src/mind/canonical.d.ts +0 -29
- package/dist/src/mind/canonical.js +0 -88
- package/dist/src/mind/graph-search.d.ts +0 -285
- package/dist/src/mind/graph-search.js +0 -956
- package/dist/src/mind/index.d.ts +0 -5
- package/dist/src/mind/index.js +0 -5
- package/dist/src/mind/junction.d.ts +0 -95
- package/dist/src/mind/junction.js +0 -262
- package/dist/src/mind/learning.d.ts +0 -47
- package/dist/src/mind/learning.js +0 -248
- package/dist/src/mind/match.d.ts +0 -126
- package/dist/src/mind/match.js +0 -441
- package/dist/src/mind/mechanisms/alu.d.ts +0 -4
- package/dist/src/mind/mechanisms/alu.js +0 -36
- package/dist/src/mind/mechanisms/cast.d.ts +0 -89
- package/dist/src/mind/mechanisms/cast.js +0 -716
- package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
- package/dist/src/mind/mechanisms/confluence.js +0 -225
- package/dist/src/mind/mechanisms/cover.d.ts +0 -6
- package/dist/src/mind/mechanisms/cover.js +0 -203
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
- package/dist/src/mind/mechanisms/extraction.js +0 -387
- package/dist/src/mind/mechanisms/recall.d.ts +0 -13
- package/dist/src/mind/mechanisms/recall.js +0 -325
- package/dist/src/mind/mind.d.ts +0 -274
- package/dist/src/mind/mind.js +0 -572
- package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
- package/dist/src/mind/pipeline-mechanism.js +0 -213
- package/dist/src/mind/pipeline.d.ts +0 -20
- package/dist/src/mind/pipeline.js +0 -217
- package/dist/src/mind/primitives.d.ts +0 -70
- package/dist/src/mind/primitives.js +0 -287
- package/dist/src/mind/rationale.d.ts +0 -134
- package/dist/src/mind/rationale.js +0 -162
- package/dist/src/mind/reasoning.d.ts +0 -34
- package/dist/src/mind/reasoning.js +0 -240
- package/dist/src/mind/recognition.d.ts +0 -20
- package/dist/src/mind/recognition.js +0 -390
- package/dist/src/mind/resonance.d.ts +0 -23
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/trace.d.ts +0 -15
- package/dist/src/mind/trace.js +0 -73
- package/dist/src/mind/traverse.d.ts +0 -113
- package/dist/src/mind/traverse.js +0 -504
- package/dist/src/mind/types.d.ts +0 -271
- package/dist/src/mind/types.js +0 -130
- package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
- package/dist/src/rabitq-ivf/src/database.js +0 -201
- package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
- package/dist/src/rabitq-ivf/src/index.js +0 -4
- package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
- package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
- package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
- package/dist/src/rabitq-ivf/src/prng.js +0 -36
- package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
- package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
- package/dist/src/sema.d.ts +0 -31
- package/dist/src/sema.js +0 -63
- package/dist/src/store-sqlite.d.ts +0 -184
- package/dist/src/store-sqlite.js +0 -942
- package/dist/src/store.d.ts +0 -663
- package/dist/src/store.js +0 -1618
- package/dist/src/vec.d.ts +0 -31
- package/dist/src/vec.js +0 -109
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
// reasoning.ts — multi-hop reasoning + multi-topic fusion (Section 4 of the mind).
|
|
2
|
-
//
|
|
3
|
-
// reason — extend an answer forward across facts (multi-hop)
|
|
4
|
-
// fuseAttention — fuse independent points of attention (multi-topic)
|
|
5
|
-
import { rItem, rNode } from "./trace.js";
|
|
6
|
-
import { bytesEqual, indexOf } from "../bytes.js";
|
|
7
|
-
import { resolve } from "./primitives.js";
|
|
8
|
-
import { corpusN } from "./traverse.js";
|
|
9
|
-
import { follow, haloSiblings, project } from "./match.js";
|
|
10
|
-
import { joinWithBridge, pivotInto } from "./resonance.js";
|
|
11
|
-
/** Whether `bytes` is a proper byte-subspan of `query` — already present in
|
|
12
|
-
* the question, so voicing it back only restates part of what was asked,
|
|
13
|
-
* never answers it. The exact guard recallByResonance already applies to
|
|
14
|
-
* its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
|
|
15
|
-
* check, tier 0b's argument-binding subspan check) — every mechanism that
|
|
16
|
-
* walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
|
|
17
|
-
* (reason()'s own hops below, and CAST's `projectCounterfactual` seat
|
|
18
|
-
* substitution — see cast.ts) needs the same guard applied to what the
|
|
19
|
-
* walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
|
|
20
|
-
* the query at all — only of what structurally continues what. */
|
|
21
|
-
export function restatesQuery(query, bytes) {
|
|
22
|
-
return bytes.length < query.length && indexOf(query, bytes, 0) >= 0;
|
|
23
|
-
}
|
|
24
|
-
/** Extend a grounded answer forward across facts (multi-hop reasoning).
|
|
25
|
-
* Pivots on the longest unconsumed learnt context each answer contains,
|
|
26
|
-
* then follows the pivot's continuation to the next fact. Repeats up
|
|
27
|
-
* to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
|
|
28
|
-
* spoken for by the grounding stage (cover/extract/CAST). `pre` is the
|
|
29
|
-
* response's shared pre-computation — the post-grounding stages read the
|
|
30
|
-
* same container the mechanisms did. */
|
|
31
|
-
export async function reason(ctx, query, answer, preConsumed, pre) {
|
|
32
|
-
// Echo guard: a query that is ITSELF a learnt continuation (some context's
|
|
33
|
-
// answer) is being asked back at the system — hopping forward from it would
|
|
34
|
-
// chain through the very fact that produced it and echo the conversation
|
|
35
|
-
// back. The grounded answer alone is the honest read-out. Deliberately a
|
|
36
|
-
// broad structural gate; pinned by test/31-audit.
|
|
37
|
-
const qId = pre.queryResolved;
|
|
38
|
-
if (qId !== null && ctx.store.prevCount(qId) > 0)
|
|
39
|
-
return answer;
|
|
40
|
-
const consumed = new Set();
|
|
41
|
-
// Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
|
|
42
|
-
// the hub bound, via the store's LIMITed edge reads: a common continuation's
|
|
43
|
-
// reverse fan-in (and a hub context's forward fan-out) is corpus-sized, and
|
|
44
|
-
// no per-hop operation may grow with the corpus. The cap follows the one
|
|
45
|
-
// convention every fan-out decision uses (first √N in the relation's own
|
|
46
|
-
// read order); a pivot suppressed only by a beyond-cap neighbour may now
|
|
47
|
-
// fire — the same visibility trade chooseNext documents.
|
|
48
|
-
const hubBound = Math.ceil(Math.sqrt(corpusN(ctx)));
|
|
49
|
-
const consumeNode = (id) => {
|
|
50
|
-
if (id === null)
|
|
51
|
-
return;
|
|
52
|
-
consumed.add(id);
|
|
53
|
-
for (const p of ctx.store.prevFirst(id, hubBound))
|
|
54
|
-
consumed.add(p);
|
|
55
|
-
};
|
|
56
|
-
const consumeAll = (id) => {
|
|
57
|
-
if (id === null)
|
|
58
|
-
return;
|
|
59
|
-
consumeNode(id);
|
|
60
|
-
for (const n of ctx.store.nextFirst(id, hubBound))
|
|
61
|
-
consumed.add(n);
|
|
62
|
-
};
|
|
63
|
-
// Pre-consume whatever the grounding stage already spoke for. The halo
|
|
64
|
-
// sweep is one ANN query per node — cap it at haloQueryK sweeps (cover
|
|
65
|
-
// grounding can pre-consume one node per recognised site, O(query length));
|
|
66
|
-
// nodes past the cap are still consumed directly, they just skip the
|
|
67
|
-
// synonym expansion.
|
|
68
|
-
let haloSweeps = 0;
|
|
69
|
-
for (const id of preConsumed) {
|
|
70
|
-
consumeNode(id);
|
|
71
|
-
if (haloSweeps >= ctx.cfg.haloQueryK)
|
|
72
|
-
continue;
|
|
73
|
-
const h = ctx.store.halo(id);
|
|
74
|
-
if (!h)
|
|
75
|
-
continue;
|
|
76
|
-
haloSweeps++;
|
|
77
|
-
for (const sib of await haloSiblings(ctx, id, h))
|
|
78
|
-
consumeNode(sib.id);
|
|
79
|
-
}
|
|
80
|
-
let cur = answer;
|
|
81
|
-
const qv = pre.guide; // the response-wide guide IS the query's gist
|
|
82
|
-
let t;
|
|
83
|
-
const startedFrom = answer;
|
|
84
|
-
for (let hop = 0; hop < ctx.cfg.recallQueryK; hop++) {
|
|
85
|
-
const curId = resolve(ctx, cur);
|
|
86
|
-
consumeNode(curId);
|
|
87
|
-
// Forward-absorb: follow only UNCONSUMED continuations. The gate below
|
|
88
|
-
// checks an unconsumed edge EXISTS, but follow()'s chooseNext knows
|
|
89
|
-
// nothing of `consumed` and may still walk to a consumed fixpoint —
|
|
90
|
-
// absorbing it would repeat content the grounding stage already spoke
|
|
91
|
-
// for, so a consumed fixpoint falls through to the pivot step instead.
|
|
92
|
-
if (curId !== null &&
|
|
93
|
-
ctx.store.nextFirst(curId, hubBound).some((n) => !consumed.has(n))) {
|
|
94
|
-
const fwd = await follow(ctx, curId, qv);
|
|
95
|
-
const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
|
|
96
|
-
if (fwd !== null && !bytesEqual(fwd, cur) &&
|
|
97
|
-
(fwdId === null || !consumed.has(fwdId)) &&
|
|
98
|
-
!restatesQuery(query, fwd)) {
|
|
99
|
-
consumeAll(curId);
|
|
100
|
-
t ??= ctx.trace?.enter("reason", [
|
|
101
|
-
rItem(startedFrom, "grounded"),
|
|
102
|
-
]);
|
|
103
|
-
ctx.trace?.step("absorbForward", [rItem(cur, "answer", curId)], [rItem(fwd, "answer", resolve(ctx, fwd) ?? undefined)], "the answer is itself a learnt fact — follow its continuation to the fixpoint");
|
|
104
|
-
cur = fwd;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Pivot: find the longest unconsumed learnt context the answer contains.
|
|
109
|
-
consumeAll(curId);
|
|
110
|
-
const pivot = await pivotInto(ctx, cur, consumed);
|
|
111
|
-
if (pivot === null)
|
|
112
|
-
break;
|
|
113
|
-
const fc = await follow(ctx, pivot, qv);
|
|
114
|
-
consumeAll(pivot);
|
|
115
|
-
if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc))
|
|
116
|
-
break;
|
|
117
|
-
t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
|
|
118
|
-
ctx.trace?.step("pivotStep", [rItem(cur, "answer"), rNode(ctx, pivot, "pivot")], [rItem(fc, "answer", resolve(ctx, fc) ?? undefined)], "pivot on the shared span this answer contains, then step forward across that fact");
|
|
119
|
-
cur = fc;
|
|
120
|
-
}
|
|
121
|
-
t?.done([rItem(cur, "answer", resolve(ctx, cur) ?? undefined)], "the multi-hop chain's fixpoint");
|
|
122
|
-
return cur;
|
|
123
|
-
}
|
|
124
|
-
/** Fuse independent points of attention into one answer (multi-topic).
|
|
125
|
-
* When the consensus climb finds more than one dominant point, each
|
|
126
|
-
* independent point grounds its own answer; they are bridged together
|
|
127
|
-
* by any learnt connector the graph holds between them. */
|
|
128
|
-
export async function fuseAttention(ctx, query, primary, pre,
|
|
129
|
-
/** True when `primary` never touched the consensus climb at all — e.g. a
|
|
130
|
-
* pure ALU computation, which has no anchor of its own. commitVotes
|
|
131
|
-
* ALWAYS admits the dominant root regardless of its vote (attention.ts:
|
|
132
|
-
* "roots.length === 0 || …") on the assumption a lone root already IS
|
|
133
|
-
* primary's own source; that assumption is exactly backwards when
|
|
134
|
-
* primary is unclimbed. Absent or false preserves the original
|
|
135
|
-
* behaviour exactly. */
|
|
136
|
-
unclimbed = false) {
|
|
137
|
-
// When the answer is structurally drawn from the query itself
|
|
138
|
-
// (extraction), it already spans all the query's pieces — fusion
|
|
139
|
-
// would only add noise from unrelated stored contexts. The gate is
|
|
140
|
-
// STRICT containment (resolved node in the query's tree, or a contiguous
|
|
141
|
-
// byte run): the old sparse-subsequence test was trivially satisfied by
|
|
142
|
-
// short answers over long queries, silently starving multi-topic queries
|
|
143
|
-
// of fusion.
|
|
144
|
-
if (containsSpan(ctx, query, primary))
|
|
145
|
-
return primary;
|
|
146
|
-
// The committed points of attention ARE the shared climb's roots (same
|
|
147
|
-
// query, same k, same DF mode) — read them from Precomputed instead of
|
|
148
|
-
// re-climbing, so even a traced response pays for the climb once.
|
|
149
|
-
const forest = (await pre.attention()).roots;
|
|
150
|
-
// A LONE root is ordinarily primary's own source — nothing to fuse. But
|
|
151
|
-
// when primary is unclimbed, the lone root was never checked against
|
|
152
|
-
// anything: it is admitted by commitVotes unconditionally, so it may be
|
|
153
|
-
// genuine consensus (Attention.breadth dominates — most of the query's
|
|
154
|
-
// OWN regions corroborate it) or a coincidental echo (breadth does not
|
|
155
|
-
// dominate — see test/35-attention-confidence). breadth is the SCALE-
|
|
156
|
-
// INVARIANT read of exactly this question: the raw IDF vote cannot serve
|
|
157
|
-
// here, since it is an absolute ln(N)-scaled quantity (a genuine root on
|
|
158
|
-
// a large store can score BELOW its own floor while a coincidental echo
|
|
159
|
-
// on a small one scores comfortably above its own, smaller, floor).
|
|
160
|
-
const lonePromotes = unclimbed && forest.length === 1 &&
|
|
161
|
-
forest[0].breadth > 0.5;
|
|
162
|
-
if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
|
|
163
|
-
return primary;
|
|
164
|
-
}
|
|
165
|
-
const pieces = [
|
|
166
|
-
{ start: forest[0].start, bytes: primary },
|
|
167
|
-
];
|
|
168
|
-
const qv = pre.guide; // once, not per root
|
|
169
|
-
const rest = lonePromotes ? forest : forest.slice(1);
|
|
170
|
-
const t = ctx.trace?.enter("fuseAttention", [
|
|
171
|
-
rItem(primary, "primary"),
|
|
172
|
-
...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
|
|
173
|
-
]);
|
|
174
|
-
for (const root of rest) {
|
|
175
|
-
// DISPERSION: this root's contributing regions are confined to a
|
|
176
|
-
// single cluster (see Attention.clusters) — one local neighbourhood of
|
|
177
|
-
// the query, not several separate places. Raw region count already
|
|
178
|
-
// failed to discriminate a coincidental match from a genuine further
|
|
179
|
-
// topic (test/24 gap 3.1 vs test/35's echo); dispersion is a different
|
|
180
|
-
// question — not how MUCH evidence, but how many separate PLACES in the
|
|
181
|
-
// query corroborate it — and a coincidental match is structurally
|
|
182
|
-
// confined to one cluster no matter how strongly it resonates.
|
|
183
|
-
//
|
|
184
|
-
// EXCEPTION: crossRegionVotes' own joint conclusions (a query naming
|
|
185
|
-
// two attributes that were only ever learnt TOGETHER — test/34's own
|
|
186
|
-
// binding corpus) are inherently ONE fused context and are pooled from
|
|
187
|
-
// a single synthetic region, so they always read as one cluster even
|
|
188
|
-
// though they already, by construction, account for both original
|
|
189
|
-
// mentions. `breadth` (dominates — the same > half-the-query bar used
|
|
190
|
-
// everywhere else) still correctly recognises these: a genuine joint
|
|
191
|
-
// binding explains the MAJORITY of the query's regions on its own,
|
|
192
|
-
// which a coincidental echo never does (verified: test/35's echo tops
|
|
193
|
-
// out at 0.40). So a root is trusted when EITHER measure alone
|
|
194
|
-
// indicates real signal — excluded only when BOTH are weak. Cheap and
|
|
195
|
-
// synchronous — checked before the async already-answered walk below.
|
|
196
|
-
if (root.clusters < 2 && root.breadth <= 0.5) {
|
|
197
|
-
ctx.trace?.step("singleCluster", [rNode(ctx, root.anchor, "point", root.vote)], [], "this point's evidence is confined to one local neighbourhood of the query — not trusted as an independent topic");
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
// ALREADY ANSWERED: this root's own learnt continuation — the same
|
|
201
|
-
// content-addressed walk reason()'s echo guard already trusts
|
|
202
|
-
// (`ctx.store.prevCount(qId) > 0`), here applied per-candidate instead
|
|
203
|
-
// of to the whole query — is VERBATIM present later in the query. A
|
|
204
|
-
// query that embeds both an exchange's ask and its own already-given
|
|
205
|
-
// reply (a conversation's turn plus its own prior answer, concatenated
|
|
206
|
-
// raw by addTurn — or any caller pasting a transcript into one
|
|
207
|
-
// respond() call; the check is Mind-bookkeeping-free, so it treats both
|
|
208
|
-
// identically) has already spoken this root's answer — fusing it in
|
|
209
|
-
// again would only restate it. Deliberately NOT a magnitude measure:
|
|
210
|
-
// it fires on exact content-addressed recurrence, not on how strongly
|
|
211
|
-
// the root resonates.
|
|
212
|
-
const cont = await follow(ctx, root.anchor, qv);
|
|
213
|
-
if (cont !== null && cont.length > 0 && indexOf(query, cont, root.end) >= 0) {
|
|
214
|
-
ctx.trace?.step("alreadyAnswered", [rNode(ctx, root.anchor, "point", root.vote)], [rItem(cont, "continuation")], "this point's own learnt continuation already appears later in the query — already answered, not fused");
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
const g = await project(ctx, root.anchor, qv);
|
|
218
|
-
if (g === null || g.length === 0)
|
|
219
|
-
continue;
|
|
220
|
-
if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0))
|
|
221
|
-
continue;
|
|
222
|
-
pieces.push({ start: root.start, bytes: g });
|
|
223
|
-
}
|
|
224
|
-
if (pieces.length === 1) {
|
|
225
|
-
t?.done([rItem(primary, "answer")], "no further independent point grounded");
|
|
226
|
-
return primary;
|
|
227
|
-
}
|
|
228
|
-
pieces.sort((a, b) => a.start - b.start);
|
|
229
|
-
let out = pieces[0].bytes;
|
|
230
|
-
for (let i = 1; i < pieces.length; i++) {
|
|
231
|
-
// An approximate-resonance miss (or a genuinely unlearnt junction) joins
|
|
232
|
-
// the pieces bare — joinWithBridge surfaces it as a bridgeMiss step.
|
|
233
|
-
out = await joinWithBridge(ctx, out, pieces[i].bytes);
|
|
234
|
-
}
|
|
235
|
-
t?.done([rItem(out, "answer", resolve(ctx, out) ?? undefined)], `fused ${pieces.length} independent points of attention into one answer`);
|
|
236
|
-
return out;
|
|
237
|
-
}
|
|
238
|
-
// (resonance.js is already a static dependency above — `bridge` — so the old
|
|
239
|
-
// dynamic import of pivotInto guarded against a cycle that does not exist.)
|
|
240
|
-
import { containsSpan } from "./mechanisms/extraction.js";
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { MindContext, Recognition, Segment } from "./types.js";
|
|
2
|
-
/** Decompose a byte stream into every stored form that leads somewhere
|
|
3
|
-
* (has a continuation edge or a halo). Two complementary readings:
|
|
4
|
-
*
|
|
5
|
-
* • structural — walk the query's own perceived tree, naming each subtree
|
|
6
|
-
* by findLeaf at the leaves and findBranch above. Catches every form
|
|
7
|
-
* aligned to the query's segmentation.
|
|
8
|
-
*
|
|
9
|
-
* • canonical — re-derive the store's segmentation directly: at each byte,
|
|
10
|
-
* the longest known leaf, chained into flat branches. Names forms the
|
|
11
|
-
* query's own cut cannot, and records sub-leaf boundaries as `splits`.
|
|
12
|
-
*
|
|
13
|
-
* Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
|
|
14
|
-
export declare function recognise(ctx: MindContext, bytes: Uint8Array): Recognition;
|
|
15
|
-
/** Segment bytes using the geometry's own groupings — leaf-parent
|
|
16
|
-
* nodes from the perceived tree, with consecutive bare leaves merged
|
|
17
|
-
* into one segment. Each segment's gist is perceived from its bytes
|
|
18
|
-
* IN ISOLATION, so the same content has the same gist regardless of
|
|
19
|
-
* where it appears. */
|
|
20
|
-
export declare function segment(ctx: MindContext, bytes: Uint8Array): Segment[];
|
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
// recognition.ts — Section 2 of the mind:
|
|
2
|
-
// Address + Read over byte streams — decompose a query into its known forms.
|
|
3
|
-
//
|
|
4
|
-
// recognise — structural + canonical decomposition into every stored form
|
|
5
|
-
// that leads somewhere (has a continuation edge or a halo).
|
|
6
|
-
// segment — leaf-parent segmentation using the geometry's own groupings.
|
|
7
|
-
import { rItem } from "./trace.js";
|
|
8
|
-
import { canonResolve, foldTree, gistOf, latin1Key, perceive, resolve, } from "./primitives.js";
|
|
9
|
-
import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
|
|
10
|
-
import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
|
|
11
|
-
import { isChunk } from "../sema.js";
|
|
12
|
-
/** Decompose a byte stream into every stored form that leads somewhere
|
|
13
|
-
* (has a continuation edge or a halo). Two complementary readings:
|
|
14
|
-
*
|
|
15
|
-
* • structural — walk the query's own perceived tree, naming each subtree
|
|
16
|
-
* by findLeaf at the leaves and findBranch above. Catches every form
|
|
17
|
-
* aligned to the query's segmentation.
|
|
18
|
-
*
|
|
19
|
-
* • canonical — re-derive the store's segmentation directly: at each byte,
|
|
20
|
-
* the longest known leaf, chained into flat branches. Names forms the
|
|
21
|
-
* query's own cut cannot, and records sub-leaf boundaries as `splits`.
|
|
22
|
-
*
|
|
23
|
-
* Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
|
|
24
|
-
export function recognise(ctx, bytes) {
|
|
25
|
-
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
26
|
-
// respondTurn() (where the map persists across calls). ALWAYS consulted,
|
|
27
|
-
// regardless of tracing — matching perceive()'s own memo, which carries no
|
|
28
|
-
// trace gate at all. This memo is NOT an optional accelerator: recogniseImpl
|
|
29
|
-
// walks the query's perceived tree via foldTree, whose subtree-resolution
|
|
30
|
-
// fast path (see primitives.ts) skips invoking `visit` — and therefore
|
|
31
|
-
// skips EMITTING SITES — for any subtree already cached in
|
|
32
|
-
// ctx._resolvedSubtrees. A multi-turn conversation's stable-prefix fold
|
|
33
|
-
// deliberately shares node OBJECTS across turns, so by the second call on
|
|
34
|
-
// the exact same bytes, large swaths of the tree are already cached and
|
|
35
|
-
// foldTree stops short of recursing into them — a second recogniseImpl
|
|
36
|
-
// call on the SAME bytes is not idempotent; it silently finds FEWER sites
|
|
37
|
-
// than the first (observed live: 31 sites → 5 on an immediate repeat
|
|
38
|
-
// call). Skipping this memo "only while tracing" used to mean every
|
|
39
|
-
// traced turn re-ran recogniseImpl from scratch at every one of the many
|
|
40
|
-
// call sites that recognise the same query (cover, reason, articulate...),
|
|
41
|
-
// each subsequent call silently more incomplete than the last — measurably
|
|
42
|
-
// changing which mechanism grounds the answer, not just costing time. The
|
|
43
|
-
// trace step must still fire on every call regardless (a cache hit is not
|
|
44
|
-
// silent), so it is emitted here directly instead of only inside
|
|
45
|
-
// recogniseImpl.
|
|
46
|
-
if (ctx.recogniseMemo) {
|
|
47
|
-
const key = latin1Key(bytes);
|
|
48
|
-
const hit = ctx.recogniseMemo.get(key);
|
|
49
|
-
if (hit !== undefined) {
|
|
50
|
-
ctx.trace?.step("recognise", [rItem(bytes, "query")], hit.sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
|
|
51
|
-
s.start,
|
|
52
|
-
s.end,
|
|
53
|
-
])), `decompose the query into ${hit.sites.length} learnt form(s) that ` +
|
|
54
|
-
`lead somewhere (over ${hit.leaves.length} perceived leaves) [cached]`);
|
|
55
|
-
return hit;
|
|
56
|
-
}
|
|
57
|
-
const fresh = recogniseImpl(ctx, bytes);
|
|
58
|
-
ctx.recogniseMemo.set(key, fresh);
|
|
59
|
-
return fresh;
|
|
60
|
-
}
|
|
61
|
-
return recogniseImpl(ctx, bytes);
|
|
62
|
-
}
|
|
63
|
-
function recogniseImpl(ctx, bytes) {
|
|
64
|
-
const store = ctx.store;
|
|
65
|
-
const sites = [];
|
|
66
|
-
const leaves = [];
|
|
67
|
-
const splits = new Set();
|
|
68
|
-
const starts = new Set();
|
|
69
|
-
if (bytes.length === 0)
|
|
70
|
-
return { sites, leaves, splits, starts };
|
|
71
|
-
// Span-resolve memo for THIS call: the structural pass (sub-runs inside
|
|
72
|
-
// leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
|
|
73
|
-
// spans, and each resolve() is a full fold of the sub-span (fresh subarray
|
|
74
|
-
// objects — the per-response perceive memo cannot see them). Keyed
|
|
75
|
-
// numerically by (start, end); resolve is pure and the store is read-only
|
|
76
|
-
// here, so a hit is exact.
|
|
77
|
-
const spanIds = new Map();
|
|
78
|
-
const resolveSpan = (start, end) => {
|
|
79
|
-
const key = start * (bytes.length + 1) + end;
|
|
80
|
-
let id = spanIds.get(key);
|
|
81
|
-
if (id === undefined) {
|
|
82
|
-
id = resolve(ctx, bytes.subarray(start, end));
|
|
83
|
-
spanIds.set(key, id);
|
|
84
|
-
}
|
|
85
|
-
return id;
|
|
86
|
-
};
|
|
87
|
-
// Byte atoms (implicit negative-id single-byte leaves) are admitted as
|
|
88
|
-
// recognised sites only while atoms can still DISCRIMINATE at this corpus
|
|
89
|
-
// scale (see {@link atomIsHub}). On a small store a single-letter fact
|
|
90
|
-
// ("a" → "A") is genuine learnt content and its site is essential; on a
|
|
91
|
-
// large one every letter of every query would otherwise become a
|
|
92
|
-
// "recognised form" — the bridge then finds junction connectors between
|
|
93
|
-
// bare letters, cover follows edges hanging off them, and pure noise
|
|
94
|
-
// ("qq8f3kz9…") grounds to an arbitrary learnt sentence instead of
|
|
95
|
-
// silence. Atoms stay available as leaves (PASS-carried literals) and
|
|
96
|
-
// through exact tier-0 resolution regardless.
|
|
97
|
-
const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
|
|
98
|
-
// Distinct probes (structural exact match, canon fallback, edge trims at
|
|
99
|
-
// several offsets) can legitimately re-derive the SAME (start, end, id)
|
|
100
|
-
// site from different tree nodes — a wide edge-trim search is exactly
|
|
101
|
-
// this on purpose (see below). Duplicate site entries are not wrong
|
|
102
|
-
// evidence, but they double the weight cover's derivation search gives
|
|
103
|
-
// that span, distorting its cost model — the same span must count once.
|
|
104
|
-
const seen = new Set();
|
|
105
|
-
const emit = (start, end, id) => {
|
|
106
|
-
if (id < 0 && atomsAreHubs)
|
|
107
|
-
return;
|
|
108
|
-
const key = start + "," + end + "," + id;
|
|
109
|
-
if (seen.has(key))
|
|
110
|
-
return;
|
|
111
|
-
seen.add(key);
|
|
112
|
-
if (leadsSomewhere(ctx, id)) {
|
|
113
|
-
sites.push({ start, end, payload: id });
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
// ── structural: the query's own perceived tree ──────────────────────
|
|
117
|
-
starts.add(0);
|
|
118
|
-
foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
|
|
119
|
-
if (n.kids === null) {
|
|
120
|
-
leaves.push({ start, end, bytes: n.leaf ?? new Uint8Array(0), node });
|
|
121
|
-
}
|
|
122
|
-
if (node !== null)
|
|
123
|
-
emit(start, end, node);
|
|
124
|
-
// Canonical fallback: a subtree whose exact content-addressed lookup
|
|
125
|
-
// missed may still be a stored form under the response's equivalence
|
|
126
|
-
// (case, width, whitespace — whatever the injected canonicalizer says).
|
|
127
|
-
// O(subtree bytes) per miss, memoised per response; a no-op when no
|
|
128
|
-
// canonicalizer was injected or the store has no canon index. A raw
|
|
129
|
-
// leaf (n.kids === null) is single-byte and handled by the byte-atom
|
|
130
|
-
// path above instead — canon equivalence only applies to composites.
|
|
131
|
-
else if (n.kids !== null) {
|
|
132
|
-
const cid = canonResolve(ctx, bytes.subarray(start, end));
|
|
133
|
-
if (cid !== null)
|
|
134
|
-
emit(start, end, cid);
|
|
135
|
-
// The edge-trim fallbacks below remove 1 byte from a side; the
|
|
136
|
-
// remainder must still be a composite (>= 2 bytes, the same floor
|
|
137
|
-
// n.kids !== null enforces above) rather than degenerate into
|
|
138
|
-
// single-byte-atom territory, which atomIsHub already governs
|
|
139
|
-
// separately.
|
|
140
|
-
else if (end - start - 1 >= 2) {
|
|
141
|
-
// The chunk's own boundary is drawn by content geometry, not by
|
|
142
|
-
// any notion of "form" — it can include one edge byte the query's
|
|
143
|
-
// fold happened to attach here that the trained span never had
|
|
144
|
-
// (e.g. a separator from the preceding chunk). The core has no
|
|
145
|
-
// idea what that byte means; it only knows resolve()/canonResolve
|
|
146
|
-
// are self-verifying (hash-then-verify, same discipline as every
|
|
147
|
-
// content lookup here), so a blind one-byte-shorter guess on
|
|
148
|
-
// either edge costs nothing when wrong and is trustworthy when it
|
|
149
|
-
// hits. Two extra probes, only on the already-failed miss path.
|
|
150
|
-
const left = resolve(ctx, bytes.subarray(start + 1, end));
|
|
151
|
-
if (left !== null)
|
|
152
|
-
emit(start + 1, end, left);
|
|
153
|
-
const right = resolve(ctx, bytes.subarray(start, end - 1));
|
|
154
|
-
if (right !== null)
|
|
155
|
-
emit(start, end - 1, right);
|
|
156
|
-
// A misalignment wider than one byte (e.g. more than one edge
|
|
157
|
-
// separator swallowed) is not itself geometry-quantized — the
|
|
158
|
-
// WRITE side's canonical index (canonicalWindows) interns sliding
|
|
159
|
-
// W−1/W-length windows over leaf ids at EVERY offset, not just
|
|
160
|
-
// radix-aligned ones (see canonical.ts) — so the offset that
|
|
161
|
-
// recovers a trained span can be anything, not a multiple of W.
|
|
162
|
-
// What IS bounded is how far it's worth looking: chainReach(W)=W²,
|
|
163
|
-
// the same reach the canonical pass (tryChain) trusts for a chain
|
|
164
|
-
// rebuilt off the query's own fold. Every candidate offset is
|
|
165
|
-
// gated by store.findBranch(leafIds) first — the SAME cheap,
|
|
166
|
-
// fold-free existence check tryChain already uses — so the extra
|
|
167
|
-
// resolve() fold (the real cost) is only paid when a branch could
|
|
168
|
-
// plausibly exist there, not for every offset. The node itself is
|
|
169
|
-
// also bounded to chunk-scale (end - start <= W²): widening this at
|
|
170
|
-
// whole-query/root scale can rediscover a smaller subtree's own
|
|
171
|
-
// content as a second, overlapping site the structural walk's own
|
|
172
|
-
// finer recursion already emits correctly on its own — a duplicate
|
|
173
|
-
// that downstream derivation can stitch into a wrong answer.
|
|
174
|
-
const W = ctx.space.maxGroup;
|
|
175
|
-
for (let k = 1; end - start <= W * W && k <= W * W && start + k < end - 1; k++) {
|
|
176
|
-
const lIds = leafIdRun(ctx, bytes, start + k, end);
|
|
177
|
-
if (lIds !== null && store.findBranch(lIds) !== null) {
|
|
178
|
-
const eLeft = resolve(ctx, bytes.subarray(start + k, end));
|
|
179
|
-
if (eLeft !== null)
|
|
180
|
-
emit(start + k, end, eLeft);
|
|
181
|
-
}
|
|
182
|
-
const rIds = leafIdRun(ctx, bytes, start, end - k);
|
|
183
|
-
if (rIds !== null && store.findBranch(rIds) !== null) {
|
|
184
|
-
const eRight = resolve(ctx, bytes.subarray(start, end - k));
|
|
185
|
-
if (eRight !== null)
|
|
186
|
-
emit(start, end - k, eRight);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// A REAL extra word at the left edge (a discourse connective like
|
|
190
|
-
// "And " prepended to a follow-up turn — not boundary noise, actual
|
|
191
|
-
// content the injected canonicalizer has no equivalence for) shows
|
|
192
|
-
// up as a canon-miss too big for the chunk-scale search above: the
|
|
193
|
-
// turn is its OWN segment (stable-prefix folded independently — see
|
|
194
|
-
// mind.ts's _growContext), so it can be turn/segment-scale, not
|
|
195
|
-
// chunk-scale. Widening the size bound itself reopens the root-
|
|
196
|
-
// scale false-positive this module already fixed once (test/46);
|
|
197
|
-
// widening the SEARCH instead — trying every position up to W
|
|
198
|
-
// chunk-widths from the left edge that the query's OWN fold treats
|
|
199
|
-
// as a chunk boundary (`starts`, the same set the canonical pass
|
|
200
|
-
// privileges with full chain reach) — does not: `starts.has(p)` is
|
|
201
|
-
// fold EVIDENCE the query produced on its own (a leaf-parent chunk
|
|
202
|
-
// like "And " is visited, and its start added to `starts`, before
|
|
203
|
-
// this composite ever runs — foldTree is post-order), never a blind
|
|
204
|
-
// guess. Bounded to W candidates, each an O(1) set lookup before
|
|
205
|
-
// paying for the real canonResolve fold — canonResolve, not
|
|
206
|
-
// resolve()/findBranch, because the gap here is often exactly the
|
|
207
|
-
// kind of equivalence (case, in the live trace) canon exists for,
|
|
208
|
-
// not just an exact-content coincidence.
|
|
209
|
-
for (let k = 1; k <= W; k++) {
|
|
210
|
-
const p = start + k * W;
|
|
211
|
-
if (p >= end - 1 || !starts.has(p))
|
|
212
|
-
continue;
|
|
213
|
-
const cid = canonResolve(ctx, bytes.subarray(p, end));
|
|
214
|
-
if (cid !== null)
|
|
215
|
-
emit(p, end, cid);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (isChunk(n)) {
|
|
220
|
-
starts.add(start);
|
|
221
|
-
// Try every sub-span within this leaf-parent.
|
|
222
|
-
const leafOffsets = [];
|
|
223
|
-
let off = start;
|
|
224
|
-
for (const k of n.kids) {
|
|
225
|
-
leafOffsets.push(off);
|
|
226
|
-
off += k.leaf?.length ?? 0;
|
|
227
|
-
}
|
|
228
|
-
// Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
|
|
229
|
-
// query's own fold did not itself choose as a boundary — the same
|
|
230
|
-
// opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
|
|
231
|
-
// guards below (see its comment). Only the chunk's own left edge
|
|
232
|
-
// (i === 0, already registered in `starts` above) carries the fold's
|
|
233
|
-
// evidence; interior sub-starts are exempt from the guard only while
|
|
234
|
-
// atoms themselves still discriminate at this corpus scale.
|
|
235
|
-
for (let i = 0; i < n.kids.length; i++) {
|
|
236
|
-
if (i > 0 && atomsAreHubs)
|
|
237
|
-
break;
|
|
238
|
-
const subIds = [];
|
|
239
|
-
for (let j = i; j < n.kids.length; j++) {
|
|
240
|
-
const kj = n.kids[j];
|
|
241
|
-
if (kj.kids !== null || !kj.leaf)
|
|
242
|
-
break;
|
|
243
|
-
const lid = store.findLeaf(kj.leaf);
|
|
244
|
-
if (lid === null)
|
|
245
|
-
break;
|
|
246
|
-
subIds.push(lid);
|
|
247
|
-
const branch = store.findBranch(subIds);
|
|
248
|
-
if (branch === null)
|
|
249
|
-
continue;
|
|
250
|
-
const subEnd = leafOffsets[j] + (kj.leaf?.length ?? 0);
|
|
251
|
-
const resolved = resolveSpan(leafOffsets[i], subEnd);
|
|
252
|
-
if (resolved !== null)
|
|
253
|
-
emit(leafOffsets[i], subEnd, resolved);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
// ── canonical: longest-known-leaf re-segmentation ──────────────────
|
|
259
|
-
const W = ctx.space.maxGroup;
|
|
260
|
-
const singleLeaf = new Array(bytes.length).fill(null);
|
|
261
|
-
for (let p = 0; p < bytes.length; p++) {
|
|
262
|
-
const id = leafIdAt(ctx, bytes, p);
|
|
263
|
-
if (id !== null)
|
|
264
|
-
singleLeaf[p] = { id, end: p + 1 };
|
|
265
|
-
}
|
|
266
|
-
const leafFrom = (p) => {
|
|
267
|
-
if (p >= bytes.length)
|
|
268
|
-
return null;
|
|
269
|
-
return singleLeaf[p];
|
|
270
|
-
};
|
|
271
|
-
const chunkEnd = new Uint32Array(bytes.length);
|
|
272
|
-
const sorted = [...starts].sort((a, b) => a - b);
|
|
273
|
-
for (let si = 0; si < sorted.length; si++) {
|
|
274
|
-
const chunkStart = sorted[si];
|
|
275
|
-
const chunkLimit = si + 1 < sorted.length ? sorted[si + 1] : bytes.length;
|
|
276
|
-
for (let p = chunkStart; p < chunkLimit; p++) {
|
|
277
|
-
chunkEnd[p] = chunkLimit;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
// A chain rebuilt from a NON-boundary offset (the query's own perceived
|
|
281
|
-
// cut, `starts`, never chose to segment here) is opportunistic: the same
|
|
282
|
-
// byte-atom coincidence the hub guard above already exists for, just
|
|
283
|
-
// spelled over 2+ leaves instead of 1. At small corpus scale that's fine
|
|
284
|
-
// — coincidence is rare and every chain is real evidence (see `atomIsHub`).
|
|
285
|
-
// Past the scale where atoms themselves stop discriminating, the same
|
|
286
|
-
// uniform-expectation argument bounds a CHAIN'S commonality too: it is at
|
|
287
|
-
// least as rare as its rarest atom, so a store where atoms are hubs makes
|
|
288
|
-
// interior chain reconstructions no more trustworthy than the atoms they
|
|
289
|
-
// are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
|
|
290
|
-
// hub-scale atoms, chained at an offset nothing in the query's own fold
|
|
291
|
-
// selected). Chains that start ON a boundary carry the fold's own
|
|
292
|
-
// evidence instead and are exempt.
|
|
293
|
-
const tryChain = (p, maxIds, boundary) => {
|
|
294
|
-
const first = leafFrom(p);
|
|
295
|
-
if (!first)
|
|
296
|
-
return;
|
|
297
|
-
emit(p, first.end, first.id);
|
|
298
|
-
const ids = [first.id];
|
|
299
|
-
let pos = first.end;
|
|
300
|
-
let prevId = null;
|
|
301
|
-
for (let depth = 1; pos < bytes.length && ids.length <= maxIds; depth++) {
|
|
302
|
-
const nx = leafFrom(pos);
|
|
303
|
-
if (!nx)
|
|
304
|
-
break;
|
|
305
|
-
ids.push(nx.id);
|
|
306
|
-
pos = nx.end;
|
|
307
|
-
if (store.findBranch(ids) === null)
|
|
308
|
-
continue;
|
|
309
|
-
if (!boundary && atomsAreHubs)
|
|
310
|
-
continue;
|
|
311
|
-
const id = resolveSpan(p, pos);
|
|
312
|
-
if (id === null || id === prevId)
|
|
313
|
-
continue;
|
|
314
|
-
prevId = id;
|
|
315
|
-
emit(p, pos, id);
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
for (let p = 0; p < bytes.length; p++) {
|
|
319
|
-
if (starts.has(p)) {
|
|
320
|
-
tryChain(p, chainReach(W), true); // boundary start — full reach
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
const limit = chunkEnd[p] + W;
|
|
324
|
-
tryChain(p, Math.min(limit - p, chainReach(W)), false);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
// ── splits: a form boundary that does not fall on a leaf edge ────────
|
|
328
|
-
const leafEdges = new Set([bytes.length]);
|
|
329
|
-
for (const lf of leaves)
|
|
330
|
-
leafEdges.add(lf.start);
|
|
331
|
-
for (const s of sites) {
|
|
332
|
-
if (!leafEdges.has(s.start))
|
|
333
|
-
splits.add(s.start);
|
|
334
|
-
if (!leafEdges.has(s.end))
|
|
335
|
-
splits.add(s.end);
|
|
336
|
-
}
|
|
337
|
-
ctx.trace?.step("recognise", [rItem(bytes, "query")], sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
|
|
338
|
-
s.start,
|
|
339
|
-
s.end,
|
|
340
|
-
])), `decompose the query into ${sites.length} learnt form(s) that lead somewhere` +
|
|
341
|
-
` (over ${leaves.length} perceived leaves)`);
|
|
342
|
-
return { sites, leaves, splits, starts };
|
|
343
|
-
}
|
|
344
|
-
/** Segment bytes using the geometry's own groupings — leaf-parent
|
|
345
|
-
* nodes from the perceived tree, with consecutive bare leaves merged
|
|
346
|
-
* into one segment. Each segment's gist is perceived from its bytes
|
|
347
|
-
* IN ISOLATION, so the same content has the same gist regardless of
|
|
348
|
-
* where it appears. */
|
|
349
|
-
export function segment(ctx, bytes) {
|
|
350
|
-
const tree = perceive(ctx, bytes);
|
|
351
|
-
const out = [];
|
|
352
|
-
let pendingStart = -1;
|
|
353
|
-
let pendingEnd = -1;
|
|
354
|
-
const flush = () => {
|
|
355
|
-
if (pendingStart >= 0 && pendingEnd > pendingStart) {
|
|
356
|
-
out.push({
|
|
357
|
-
start: pendingStart,
|
|
358
|
-
end: pendingEnd,
|
|
359
|
-
v: gistOf(ctx, bytes.subarray(pendingStart, pendingEnd)),
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
pendingStart = -1;
|
|
363
|
-
pendingEnd = -1;
|
|
364
|
-
};
|
|
365
|
-
const walk = (n, start) => {
|
|
366
|
-
if (n.kids === null) {
|
|
367
|
-
const end = start + (n.leaf?.length ?? 0);
|
|
368
|
-
if (pendingStart < 0)
|
|
369
|
-
pendingStart = start;
|
|
370
|
-
pendingEnd = end;
|
|
371
|
-
return end;
|
|
372
|
-
}
|
|
373
|
-
if (isChunk(n)) {
|
|
374
|
-
flush();
|
|
375
|
-
let end = start;
|
|
376
|
-
for (const c of n.kids)
|
|
377
|
-
end += c.leaf?.length ?? 0;
|
|
378
|
-
out.push({ start, end, v: gistOf(ctx, bytes.subarray(start, end)) });
|
|
379
|
-
return end;
|
|
380
|
-
}
|
|
381
|
-
flush();
|
|
382
|
-
let pos = start;
|
|
383
|
-
for (const c of n.kids)
|
|
384
|
-
pos = walk(c, pos);
|
|
385
|
-
return pos;
|
|
386
|
-
};
|
|
387
|
-
walk(tree, 0);
|
|
388
|
-
flush();
|
|
389
|
-
return out;
|
|
390
|
-
}
|