@hviana/sema 0.4.1 → 0.4.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/package.json +1 -1
- package/src/ingest-cache.ts +4 -0
- package/src/meter.ts +5 -0
- package/src/mind/attention.ts +18 -1
- package/src/mind/bridge.ts +292 -54
- package/src/mind/junction.ts +21 -7
- package/src/mind/match.ts +92 -1
- package/src/mind/mechanisms/cast.ts +12 -0
- package/src/mind/mechanisms/confluence.ts +30 -1
- package/src/mind/mechanisms/cover.ts +36 -4
- package/src/mind/mechanisms/recall.ts +21 -44
- package/src/mind/mind.ts +39 -2
- package/src/mind/pipeline-mechanism.ts +86 -4
- package/src/mind/pipeline.ts +1 -1
- package/src/mind/reasoning.ts +15 -8
- package/src/mind/recognition.ts +40 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/traverse.ts +17 -15
- package/src/mind/types.ts +10 -0
- package/test/49-natural-units-synonym-bridge.test.mjs +56 -15
- 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 -136
- package/dist/src/derive/src/deduction.js +0 -159
- package/dist/src/derive/src/index.d.ts +0 -8
- 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 -175
- package/dist/src/geometry.js +0 -823
- package/dist/src/index.d.ts +0 -17
- package/dist/src/index.js +0 -19
- package/dist/src/ingest-cache.d.ts +0 -41
- package/dist/src/ingest-cache.js +0 -161
- package/dist/src/meter.d.ts +0 -171
- package/dist/src/meter.js +0 -269
- 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 -414
- package/dist/src/mind/attention.js +0 -2065
- package/dist/src/mind/bridge.d.ts +0 -39
- package/dist/src/mind/bridge.js +0 -738
- package/dist/src/mind/canonical.d.ts +0 -34
- package/dist/src/mind/canonical.js +0 -93
- package/dist/src/mind/graph-search.d.ts +0 -294
- package/dist/src/mind/graph-search.js +0 -996
- package/dist/src/mind/index.d.ts +0 -9
- package/dist/src/mind/index.js +0 -5
- package/dist/src/mind/junction.d.ts +0 -134
- package/dist/src/mind/junction.js +0 -331
- package/dist/src/mind/learning.d.ts +0 -75
- package/dist/src/mind/learning.js +0 -270
- package/dist/src/mind/match.d.ts +0 -166
- package/dist/src/mind/match.js +0 -565
- 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 -773
- 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 -33
- package/dist/src/mind/mechanisms/extraction.js +0 -300
- package/dist/src/mind/mechanisms/recall.d.ts +0 -16
- package/dist/src/mind/mechanisms/recall.js +0 -388
- package/dist/src/mind/mind.d.ts +0 -330
- package/dist/src/mind/mind.js +0 -594
- package/dist/src/mind/pipeline-mechanism.d.ts +0 -172
- package/dist/src/mind/pipeline-mechanism.js +0 -382
- package/dist/src/mind/pipeline.d.ts +0 -49
- package/dist/src/mind/pipeline.js +0 -275
- package/dist/src/mind/primitives.d.ts +0 -66
- package/dist/src/mind/primitives.js +0 -306
- package/dist/src/mind/rationale.d.ts +0 -139
- package/dist/src/mind/rationale.js +0 -163
- package/dist/src/mind/reasoning.d.ts +0 -40
- package/dist/src/mind/reasoning.js +0 -272
- package/dist/src/mind/recognition.d.ts +0 -20
- package/dist/src/mind/recognition.js +0 -463
- 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 -124
- package/dist/src/mind/traverse.js +0 -649
- package/dist/src/mind/types.d.ts +0 -323
- 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 -678
- package/dist/src/store.js +0 -1703
- package/dist/src/vec.d.ts +0 -31
- package/dist/src/vec.js +0 -109
package/package.json
CHANGED
package/src/ingest-cache.ts
CHANGED
|
@@ -42,6 +42,8 @@ import { bindSeat, companySignature, type Sema } from "./sema.js";
|
|
|
42
42
|
import type { Input } from "./mind/index.js";
|
|
43
43
|
import { BoundedMap } from "./store.js";
|
|
44
44
|
import type { Vec } from "./vec.js";
|
|
45
|
+
import { invalidateStructuralCaches } from "./mind/traverse.js";
|
|
46
|
+
import { invalidateJunctionCache } from "./mind/junction.js";
|
|
45
47
|
|
|
46
48
|
/** The interned result of perceiving + interning ONE input.
|
|
47
49
|
*
|
|
@@ -122,6 +124,8 @@ export class CachedIngest {
|
|
|
122
124
|
input: Input | (Input | [Input, Input])[],
|
|
123
125
|
second?: Input,
|
|
124
126
|
): Promise<(Sema & { id: number }) | undefined> {
|
|
127
|
+
invalidateStructuralCaches(this.mind);
|
|
128
|
+
invalidateJunctionCache(this.mind);
|
|
125
129
|
// One shape-reading for both ingest paths — see {@link dispatchIngest}.
|
|
126
130
|
return dispatchIngest(
|
|
127
131
|
input,
|
package/src/meter.ts
CHANGED
|
@@ -176,6 +176,11 @@ export class Meter {
|
|
|
176
176
|
/** Nodes popped by those ascents, against their √N·W budget — the counter
|
|
177
177
|
* that shows whether the walks are deciding early or burning the budget. */
|
|
178
178
|
junctionPops = 0;
|
|
179
|
+
/** Arbitrary byte spans whose distributional company was VSA-bundled from
|
|
180
|
+
* existing episode halos. */
|
|
181
|
+
spanHalos = 0;
|
|
182
|
+
/** Canonical W-windows examined while composing those span halos. */
|
|
183
|
+
spanHaloWindows = 0;
|
|
179
184
|
|
|
180
185
|
/** `lightestDerivation` searches started. */
|
|
181
186
|
searches = 0;
|
package/src/mind/attention.ts
CHANGED
|
@@ -2275,6 +2275,16 @@ async function crossRegionVotes(
|
|
|
2275
2275
|
// the same container (or a sub-container of it) twice.
|
|
2276
2276
|
const consumed = new Set<number>();
|
|
2277
2277
|
let probes = 0;
|
|
2278
|
+
// Once atoms themselves are hubs (N > W²), the cross-region analysis gets
|
|
2279
|
+
// one k·W walk allowance per evidence tier. Without a shared allowance,
|
|
2280
|
+
// each of k candidate pairs spends the full corpus-derived budget and a
|
|
2281
|
+
// cumulative dialogue multiplies bounded work into tens of seconds. Small
|
|
2282
|
+
// corpora retain exhaustive exact traversal: below this same scale the
|
|
2283
|
+
// budget would be smaller than the structures the tests deliberately build.
|
|
2284
|
+
const marketScale = k * ctx.space.maxGroup;
|
|
2285
|
+
const corpusScale = N > marketScale ** 3;
|
|
2286
|
+
const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
2287
|
+
const synonymBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
2278
2288
|
|
|
2279
2289
|
for (let a = 0; a < cand.length && probes < k; a++) {
|
|
2280
2290
|
if (consumed.has(cand[a])) continue;
|
|
@@ -2284,6 +2294,12 @@ async function crossRegionVotes(
|
|
|
2284
2294
|
const rb = regions[cand[b]];
|
|
2285
2295
|
if (!strong.has(cand[a]) && !strong.has(cand[b])) continue;
|
|
2286
2296
|
if (ra.end >= rb.start) continue; // overlap or adjacent — nothing between
|
|
2297
|
+
// In a cumulative conversation, an old↔old interaction cannot explain
|
|
2298
|
+
// the user turn currently being answered; it was already available
|
|
2299
|
+
// before that turn existed. Keep old↔current pairs (the current turn may
|
|
2300
|
+
// refer to a prior answer), but do not repeatedly spend the junction
|
|
2301
|
+
// budget recomposing two regions wholly before the current boundary.
|
|
2302
|
+
if (ctx.currentTurnStart > 0 && rb.end <= ctx.currentTurnStart) continue;
|
|
2287
2303
|
// Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
|
|
2288
2304
|
// that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
|
|
2289
2305
|
// genuine, individually-corroborated evidence about what fills the gap
|
|
@@ -2364,7 +2380,7 @@ async function crossRegionVotes(
|
|
|
2364
2380
|
cap,
|
|
2365
2381
|
seedsOf(cand[a]),
|
|
2366
2382
|
seedsOf(cand[b]),
|
|
2367
|
-
|
|
2383
|
+
exactBudget,
|
|
2368
2384
|
true,
|
|
2369
2385
|
);
|
|
2370
2386
|
if (probe) {
|
|
@@ -2384,6 +2400,7 @@ async function crossRegionVotes(
|
|
|
2384
2400
|
maxInterior,
|
|
2385
2401
|
true,
|
|
2386
2402
|
sides,
|
|
2403
|
+
synonymBudget,
|
|
2387
2404
|
);
|
|
2388
2405
|
if (probe) {
|
|
2389
2406
|
const singleAttempted = sides.leftSiblings.length > 0 ||
|
package/src/mind/bridge.ts
CHANGED
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
// them reused across ≥ 2 containers (the same "≥ 2 structural parents"
|
|
27
27
|
// bar propagateSuffixes gates suffix inheritance with). An untrained
|
|
28
28
|
// word ("deadliest") has no stored windows and can never substitute.
|
|
29
|
-
// •
|
|
30
|
-
// conceptThreshold(D)
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
29
|
+
// • GRADED IDENTITY — lexical geometry is tried first at
|
|
30
|
+
// conceptThreshold(D). Differently-spelled forms fall through to VSA
|
|
31
|
+
// company: their stored W-window occurrences ascend to learned episodes,
|
|
32
|
+
// whose bundled halos must clear significanceBar(D), the same
|
|
33
|
+
// distributional-evidence bar used by analogyStrength.
|
|
34
34
|
//
|
|
35
35
|
// A candidate context is accepted when its aligned-plus-substituted spans
|
|
36
36
|
// DOMINATE the query (the same half-dominance predicate used throughout)
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
//
|
|
42
42
|
// COST: nothing on any answering path — the bridge runs only where the
|
|
43
43
|
// alternative was silence. There it pays O(|query|) content-hash probes
|
|
44
|
-
// (the propagateSuffixes trick), at most W anchor climbs and
|
|
45
|
-
// candidate reads, and one
|
|
46
|
-
//
|
|
44
|
+
// (the propagateSuffixes trick), at most W anchor climbs and
|
|
45
|
+
// 2·recallQueryK candidate reads, and one
|
|
46
|
+
// O(|query|·|candidate|)-bounded alignment each.
|
|
47
47
|
//
|
|
48
48
|
// FIXED WRONG-ANSWER GAP (found and closed 2026-07-20): a proper-noun swap
|
|
49
49
|
// could pass both derived gates above and voice a WRONG fact. Live case:
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
// boiling-point and lowercase-France bridge wins are unaffected; full
|
|
91
91
|
// suite green (358/358).
|
|
92
92
|
|
|
93
|
-
import { cosine } from "../vec.js";
|
|
94
|
-
import { conceptThreshold, dominates } from "../geometry.js";
|
|
93
|
+
import { cosine, type Vec } from "../vec.js";
|
|
94
|
+
import { conceptThreshold, dominates, significanceBar } from "../geometry.js";
|
|
95
95
|
import { bytesEqual, indexOf } from "../bytes.js";
|
|
96
96
|
import type { MindContext } from "./types.js";
|
|
97
97
|
import { foldTree, perceive, read } from "./primitives.js";
|
|
@@ -103,6 +103,8 @@ import {
|
|
|
103
103
|
sharedReachMemo,
|
|
104
104
|
} from "./traverse.js";
|
|
105
105
|
import { rItem, rNode } from "./trace.js";
|
|
106
|
+
import { junctionContainersFrom } from "./junction.js";
|
|
107
|
+
import { spanHalo } from "./match.js";
|
|
106
108
|
|
|
107
109
|
/** One accepted substitution: query span [qs,qe) stands in for the
|
|
108
110
|
* candidate context's span — recorded for the rationale trace. */
|
|
@@ -276,8 +278,45 @@ async function bridgeImpl(
|
|
|
276
278
|
if (query.length < 2 * W) return null;
|
|
277
279
|
const bound = hubBound(ctx);
|
|
278
280
|
const N = corpusN(ctx);
|
|
281
|
+
const marketScale = ctx.cfg.recallQueryK * W;
|
|
282
|
+
const candidateCap = N <= marketScale ** 3 ? bound : 2 * ctx.cfg.recallQueryK;
|
|
279
283
|
const bar = conceptThreshold(ctx.store.D);
|
|
284
|
+
const synonymBar = significanceBar(ctx.store.D);
|
|
280
285
|
const reachCap = chainReach(W);
|
|
286
|
+
const diagnostics = ctx.trace
|
|
287
|
+
? {
|
|
288
|
+
anchors: 0,
|
|
289
|
+
picked: 0,
|
|
290
|
+
proposed: 0,
|
|
291
|
+
structuralProposed: 0,
|
|
292
|
+
proposedGrounded: 0,
|
|
293
|
+
synonymChecks: 0,
|
|
294
|
+
bestSynonym: 0,
|
|
295
|
+
climbed: 0,
|
|
296
|
+
phraseScale: 0,
|
|
297
|
+
seeded: 0,
|
|
298
|
+
aligned: 0,
|
|
299
|
+
structurallyValid: 0,
|
|
300
|
+
coverageValid: 0,
|
|
301
|
+
identityValid: 0,
|
|
302
|
+
knownContentValid: 0,
|
|
303
|
+
bestCovered: 0,
|
|
304
|
+
bestRank: -1,
|
|
305
|
+
closest: [] as Array<{
|
|
306
|
+
id: number;
|
|
307
|
+
covered: number;
|
|
308
|
+
leading: number;
|
|
309
|
+
trailing: number;
|
|
310
|
+
gaps: number;
|
|
311
|
+
substitutions: number;
|
|
312
|
+
queryGapBytes: number;
|
|
313
|
+
candidateGapBytes: number;
|
|
314
|
+
gapRanges: Array<[number, number, number, number]>;
|
|
315
|
+
candidateSurplus: number;
|
|
316
|
+
gapsExplained: boolean;
|
|
317
|
+
}>,
|
|
318
|
+
}
|
|
319
|
+
: null;
|
|
281
320
|
|
|
282
321
|
// PHRASE-SCALE CANDIDATE CAP — the same |content|·W bound the weave
|
|
283
322
|
// (pipeline-mechanism.ts), the cross-region junction ladder's
|
|
@@ -332,7 +371,18 @@ async function bridgeImpl(
|
|
|
332
371
|
if (rarity === 0) continue;
|
|
333
372
|
anchors.push({ off: o, id, rarity });
|
|
334
373
|
}
|
|
335
|
-
if (anchors.length
|
|
374
|
+
if (diagnostics) diagnostics.anchors = anchors.length;
|
|
375
|
+
if (anchors.length === 0) {
|
|
376
|
+
ctx.trace?.step(
|
|
377
|
+
"substitutionBridge",
|
|
378
|
+
[rItem(query, "query")],
|
|
379
|
+
[],
|
|
380
|
+
"no stored query window can anchor a corroborated substitution",
|
|
381
|
+
undefined,
|
|
382
|
+
diagnostics!,
|
|
383
|
+
);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
336
386
|
// CORROBORATION (see the module-level doc) over the precomputed window
|
|
337
387
|
// facts: the query span [qs,qe) attests when every full W-window inside
|
|
338
388
|
// it is a stored flat form and at least one is reused across ≥ 2
|
|
@@ -394,17 +444,33 @@ async function bridgeImpl(
|
|
|
394
444
|
to: number,
|
|
395
445
|
): boolean => {
|
|
396
446
|
if (to - from < W) return true;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
447
|
+
const common = (start: number, end: number): boolean => {
|
|
448
|
+
if (end - start < W) return false;
|
|
449
|
+
for (let o = start; o + W <= end; o++) {
|
|
450
|
+
const ids = leafIdRun(ctx, bytes, o, o + W);
|
|
451
|
+
if (ids === null) return false;
|
|
452
|
+
const wid = ctx.store.findBranch(ids);
|
|
453
|
+
if (wid === null) return false;
|
|
454
|
+
const r = edgeAncestors(ctx, wid, N, reachMemo);
|
|
455
|
+
if (r.saturated) continue; // in too many places to discriminate
|
|
456
|
+
if (r.roots.length === 0) return false; // reaches nothing: novel content
|
|
457
|
+
if (!dominates(r.contextsReached, N)) return false;
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
};
|
|
461
|
+
if (common(from, to)) return true;
|
|
462
|
+
// Alignment may attach the shared delimiter to either side of an inserted
|
|
463
|
+
// phrase. Up to W-1 boundary bytes are below the fold's identity scale;
|
|
464
|
+
// classify the phrase by a full-window interior core when one exists.
|
|
465
|
+
// This does not erase a short discriminative insertion: "heavy" still
|
|
466
|
+
// leaves the full `heav`/`eavy` windows for the corpus-global test.
|
|
467
|
+
for (let left = 0; left < W; left++) {
|
|
468
|
+
for (let right = 0; right < W; right++) {
|
|
469
|
+
if (left + right === 0 || left + right >= W) continue;
|
|
470
|
+
if (common(from + left, to - right)) return true;
|
|
471
|
+
}
|
|
406
472
|
}
|
|
407
|
-
return
|
|
473
|
+
return false;
|
|
408
474
|
};
|
|
409
475
|
|
|
410
476
|
anchors.sort((a, b) => a.rarity - b.rarity);
|
|
@@ -415,7 +481,7 @@ async function bridgeImpl(
|
|
|
415
481
|
if (picked.some((p) => Math.abs(p.off - a.off) < W)) continue;
|
|
416
482
|
picked.push(a);
|
|
417
483
|
}
|
|
418
|
-
|
|
484
|
+
if (diagnostics) diagnostics.picked = picked.length;
|
|
419
485
|
// 2. Candidate trained contexts. Two proposal channels, one verifier:
|
|
420
486
|
// (a) the caller's PROPOSED hits — recall's whole-query resonance
|
|
421
487
|
// ranking, the retrieval structure built to surface near-paraphrase
|
|
@@ -425,20 +491,56 @@ async function bridgeImpl(
|
|
|
425
491
|
// candidate passes the same byte-exact alignment and gates below.
|
|
426
492
|
const seen = new Set<number>();
|
|
427
493
|
const candidates: number[] = [];
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
494
|
+
// Exact co-occurrence proposes contexts the whole-form ANN can miss when a
|
|
495
|
+
// short insertion shifts every later fold boundary. The byte alignment below
|
|
496
|
+
// remains the decider. All pairs share one candidateCap·W junction
|
|
497
|
+
// allowance, ordered
|
|
498
|
+
// by their rarest side and then span: a rare content window joined to a
|
|
499
|
+
// distant frame boundary discriminates a whole question better than two
|
|
500
|
+
// neighbouring rare windows inside the same word.
|
|
501
|
+
if (query.length <= 2 * reachCap) {
|
|
502
|
+
const pairs: Array<[typeof picked[number], typeof picked[number]]> = [];
|
|
503
|
+
for (let i = 0; i < picked.length; i++) {
|
|
504
|
+
for (let j = i + 1; j < picked.length; j++) {
|
|
505
|
+
pairs.push([picked[i], picked[j]]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
pairs.sort((a, b) =>
|
|
509
|
+
Math.min(a[0].rarity, a[1].rarity) -
|
|
510
|
+
Math.min(b[0].rarity, b[1].rarity) ||
|
|
511
|
+
Math.abs(b[0].off - b[1].off) - Math.abs(a[0].off - a[1].off) ||
|
|
512
|
+
a[0].rarity + a[1].rarity - b[0].rarity - b[1].rarity
|
|
513
|
+
);
|
|
514
|
+
const structuralBudget = {
|
|
515
|
+
n: chainReach(W) * W * ctx.cfg.recallQueryK,
|
|
516
|
+
};
|
|
517
|
+
for (const [left, right] of pairs.slice(0, W)) {
|
|
518
|
+
const found = junctionContainersFrom(
|
|
519
|
+
ctx,
|
|
520
|
+
query.subarray(left.off, left.off + W),
|
|
521
|
+
query.subarray(right.off, right.off + W),
|
|
522
|
+
capBytes,
|
|
523
|
+
[left.id],
|
|
524
|
+
[right.id],
|
|
525
|
+
structuralBudget,
|
|
526
|
+
true,
|
|
527
|
+
);
|
|
528
|
+
for (const hit of found) {
|
|
529
|
+
if (candidates.length >= candidateCap) break;
|
|
530
|
+
if (seen.has(hit.id) || !ctx.store.hasNext(hit.id)) continue;
|
|
531
|
+
seen.add(hit.id);
|
|
532
|
+
candidates.push(hit.id);
|
|
533
|
+
if (diagnostics) diagnostics.structuralProposed++;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Once exact structural proposals fill the shared cap, no caller proposal
|
|
538
|
+
// can enter the verifier. Do not evaluate the lazy ANN thunk merely to
|
|
539
|
+
// discard every result at the loop's first guard.
|
|
540
|
+
const proposedIds = candidates.length < candidateCap ? await proposed() : [];
|
|
541
|
+
if (diagnostics) diagnostics.proposed = proposedIds.length;
|
|
542
|
+
for (const sid of proposedIds) {
|
|
543
|
+
if (candidates.length >= candidateCap) break;
|
|
442
544
|
if (seen.has(sid)) continue;
|
|
443
545
|
seen.add(sid);
|
|
444
546
|
const tb = candidateBytes(sid);
|
|
@@ -457,32 +559,62 @@ async function bridgeImpl(
|
|
|
457
559
|
seen.add(use);
|
|
458
560
|
}
|
|
459
561
|
candidates.push(use);
|
|
562
|
+
if (diagnostics) diagnostics.proposedGrounded++;
|
|
460
563
|
}
|
|
564
|
+
// Proposal channel — carries its caller's own bound (recall's resonance
|
|
565
|
+
// k), sharing the 2·recallQueryK candidate allowance
|
|
566
|
+
// with the structural and climb channels. A proposal may
|
|
567
|
+
// be a FLAT content twin whose continuation edge lives on the
|
|
568
|
+
// fold-shaped deposit node with the same bytes — the same twin split
|
|
569
|
+
// canonResolve bridges by re-folding (primitives.ts) — but the re-fold
|
|
570
|
+
// (a full perceive of the candidate's bytes) is paid only for proposals
|
|
571
|
+
// that could align at all: alignment can only seed at a picked anchor
|
|
572
|
+
// window occurring literally in the candidate (measured: unconditional
|
|
573
|
+
// re-folds multiplied the refusal-path latency several-fold).
|
|
574
|
+
// FIRST TOUCH of the caller's proposals — past every gate that could have
|
|
575
|
+
// refused without them (see substitutionBridge's doc).
|
|
461
576
|
// Climb channel — edge-bearing ancestors only, decided by the indexed
|
|
462
577
|
// O(1) hasNext; no byte is read here (the climb visits hundreds of
|
|
463
578
|
// roots, and reading each was measured to dominate the refusal path).
|
|
464
|
-
const proposedCount = candidates.length;
|
|
465
579
|
for (const a of picked) {
|
|
466
|
-
const reach = edgeAncestors(ctx, a.id, N);
|
|
580
|
+
const reach = edgeAncestors(ctx, a.id, N, reachMemo);
|
|
467
581
|
for (const sid of reach.roots) {
|
|
468
|
-
if (candidates.length
|
|
582
|
+
if (candidates.length >= candidateCap) break;
|
|
469
583
|
if (seen.has(sid)) continue;
|
|
470
584
|
seen.add(sid);
|
|
471
585
|
if (!ctx.store.hasNext(sid)) continue;
|
|
472
586
|
candidates.push(sid);
|
|
587
|
+
if (diagnostics) diagnostics.climbed++;
|
|
473
588
|
}
|
|
474
|
-
if (candidates.length
|
|
589
|
+
if (candidates.length >= candidateCap) break;
|
|
475
590
|
}
|
|
476
|
-
|
|
477
591
|
// 3. Align each candidate; gate its mismatches; keep the best.
|
|
478
592
|
// Over-cap candidates are dropped here rather than earlier: the climb
|
|
479
593
|
// channel deliberately reads no bytes while collecting (the climb visits
|
|
480
594
|
// hundreds of roots), so this is where its proposals are first sized.
|
|
481
|
-
|
|
482
|
-
|
|
595
|
+
//
|
|
596
|
+
// Candidate bytes are read LAZILY — on first access during the seed
|
|
597
|
+
// check — not eagerly for every collected id. On a 325K-context store
|
|
598
|
+
// the climb channel alone can propose hundreds of edge-bearing ancestors
|
|
599
|
+
// (hubBound = 571), most of which will never contain a picked anchor
|
|
600
|
+
// window and would be discarded at the seed check without their bytes
|
|
601
|
+
// ever being consulted. Eager reads for 500+ candidates each traversing
|
|
602
|
+
// the DAG (profiled at 12K node records and 73KB of bytes read per
|
|
603
|
+
// refusing query) is the dominant remaining bridge cost after the ANN
|
|
604
|
+
// gate. A Map stays available for the frame-unanimity scan below, which
|
|
605
|
+
// only needs bytes of candidates that actually seeded.
|
|
606
|
+
const seededBytes = new Map<number, Uint8Array>();
|
|
607
|
+
/** Read a candidate's bytes once; cache for the seed check AND for the
|
|
608
|
+
* frame-unanimity scan that follows alignment. Returns null when the
|
|
609
|
+
* candidate exceeds the phrase-scale cap or has no content. */
|
|
610
|
+
const bytesOfCandidate = (sid: number): Uint8Array | null => {
|
|
611
|
+
const hit = seededBytes.get(sid);
|
|
612
|
+
if (hit !== undefined) return hit;
|
|
483
613
|
const b = candidateBytes(sid);
|
|
484
|
-
if (b !== null)
|
|
485
|
-
|
|
614
|
+
if (b !== null) seededBytes.set(sid, b);
|
|
615
|
+
return b;
|
|
616
|
+
};
|
|
617
|
+
if (diagnostics) diagnostics.phraseScale = seededBytes.size;
|
|
486
618
|
|
|
487
619
|
// FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
|
|
488
620
|
// groundable only when the collected candidates — the store's own sample
|
|
@@ -507,7 +639,7 @@ async function bridgeImpl(
|
|
|
507
639
|
lf: Uint8Array,
|
|
508
640
|
rf: Uint8Array,
|
|
509
641
|
): boolean => {
|
|
510
|
-
for (const bytes of
|
|
642
|
+
for (const bytes of seededBytes.values()) {
|
|
511
643
|
let from = 0;
|
|
512
644
|
for (;;) {
|
|
513
645
|
const i = indexOf(bytes, lf, from);
|
|
@@ -540,9 +672,19 @@ async function bridgeImpl(
|
|
|
540
672
|
// check — verified live).
|
|
541
673
|
let best: BridgeHit | null = null;
|
|
542
674
|
let bestAccounted = 0;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
675
|
+
const queryHaloMemo = new Map<string, Vec | null>();
|
|
676
|
+
const candidateHaloMemo = new Map<string, Vec | null>();
|
|
677
|
+
for (
|
|
678
|
+
let candidateIndex = 0;
|
|
679
|
+
candidateIndex < candidates.length;
|
|
680
|
+
candidateIndex++
|
|
681
|
+
) {
|
|
682
|
+
const sid = candidates[candidateIndex];
|
|
683
|
+
// Read bytes lazily — most climb-proposed candidates have no picked
|
|
684
|
+
// anchor window and will never pass the seed check below, so their
|
|
685
|
+
// bytes are never read at all.
|
|
686
|
+
const cBytes = bytesOfCandidate(sid);
|
|
687
|
+
if (cBytes === null) continue;
|
|
546
688
|
// Seed at the rarest picked anchor that literally occurs in this
|
|
547
689
|
// candidate.
|
|
548
690
|
let seed: { qo: number; co: number } | null = null;
|
|
@@ -554,7 +696,29 @@ async function bridgeImpl(
|
|
|
554
696
|
}
|
|
555
697
|
}
|
|
556
698
|
if (seed === null) continue;
|
|
699
|
+
if (diagnostics) diagnostics.seeded++;
|
|
557
700
|
const { matched, gaps } = align(ctx, query, cBytes, seed.qo, seed.co);
|
|
701
|
+
if (diagnostics) diagnostics.aligned++;
|
|
702
|
+
|
|
703
|
+
// Investment gate: even treating every two-sided mismatch as a valid
|
|
704
|
+
// synonym, can this alignment satisfy the bridge's final coverage rule?
|
|
705
|
+
// Distributional span composition performs bounded ancestor climbs; never
|
|
706
|
+
// pay for it on a candidate arithmetic already proves cannot win.
|
|
707
|
+
let matchStart = query.length;
|
|
708
|
+
let matchEnd = 0;
|
|
709
|
+
let potential = 0;
|
|
710
|
+
for (const [s, e] of matched) {
|
|
711
|
+
matchStart = Math.min(matchStart, s);
|
|
712
|
+
matchEnd = Math.max(matchEnd, e);
|
|
713
|
+
potential += e - s;
|
|
714
|
+
}
|
|
715
|
+
for (const g of gaps) {
|
|
716
|
+
if (g.qe > g.qs && g.ce > g.cs) potential += g.qe - g.qs;
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
matchStart > W || query.length - matchEnd > W ||
|
|
720
|
+
!dominates(potential, query.length)
|
|
721
|
+
) continue;
|
|
558
722
|
|
|
559
723
|
// Gate each mismatch: a corroborated, geometrically-identified
|
|
560
724
|
// substitution counts as accounted; anything else stays a gap.
|
|
@@ -594,12 +758,14 @@ async function bridgeImpl(
|
|
|
594
758
|
// happen to share a few letters. Uses the SAME dominates() bar
|
|
595
759
|
// (part*2 > whole) applied throughout the codebase, symmetrically:
|
|
596
760
|
// the smaller raw side must be more than half the larger. Applies
|
|
597
|
-
// to the RAW gap, before expansion — expansion
|
|
598
|
-
// sides by IDENTICAL absorbed bytes, so it cannot
|
|
599
|
-
// that was already there.
|
|
761
|
+
// to the RAW gap for GEOMETRIC identity, before expansion — expansion
|
|
762
|
+
// only ever grows both sides by IDENTICAL absorbed bytes, so it cannot
|
|
763
|
+
// fix an imbalance that was already there. Distributional synonym
|
|
764
|
+
// evidence is exempt: two phrases may occupy the same role at very
|
|
765
|
+
// different lengths.
|
|
600
766
|
let accepted = false;
|
|
601
767
|
const balanced = dominates(Math.min(uLen, cLen), Math.max(uLen, cLen));
|
|
602
|
-
const maxExtra =
|
|
768
|
+
const maxExtra = reachCap - Math.max(uLen, cLen);
|
|
603
769
|
outer:
|
|
604
770
|
for (let extra = 0; extra <= maxExtra; extra++) {
|
|
605
771
|
for (let a = 0; a <= extra; a++) {
|
|
@@ -622,7 +788,36 @@ async function bridgeImpl(
|
|
|
622
788
|
if (!attestedQ(qs2, qe2)) continue;
|
|
623
789
|
const u = query.subarray(qs2, qe2);
|
|
624
790
|
const cSpan = cBytes.subarray(cs2, ce2);
|
|
625
|
-
|
|
791
|
+
const geometric = cosine(perceive(ctx, u).v, perceive(ctx, cSpan).v);
|
|
792
|
+
const qKey = `${qs2}:${qe2}`;
|
|
793
|
+
let qHalo = queryHaloMemo.get(qKey);
|
|
794
|
+
if (qHalo === undefined) {
|
|
795
|
+
qHalo = spanHalo(ctx, query, qs2, qe2);
|
|
796
|
+
queryHaloMemo.set(qKey, qHalo);
|
|
797
|
+
}
|
|
798
|
+
const cKey = `${sid}:${cs2}:${ce2}`;
|
|
799
|
+
let cHalo = candidateHaloMemo.get(cKey);
|
|
800
|
+
if (cHalo === undefined) {
|
|
801
|
+
cHalo = spanHalo(ctx, cBytes, cs2, ce2);
|
|
802
|
+
candidateHaloMemo.set(cKey, cHalo);
|
|
803
|
+
}
|
|
804
|
+
const distributional = qHalo !== null && cHalo !== null
|
|
805
|
+
? cosine(qHalo, cHalo)
|
|
806
|
+
: 0;
|
|
807
|
+
if (diagnostics) {
|
|
808
|
+
diagnostics.synonymChecks++;
|
|
809
|
+
diagnostics.bestSynonym = Math.max(
|
|
810
|
+
diagnostics.bestSynonym,
|
|
811
|
+
distributional,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
// Graded identity: byte geometry remains the cheap first tier;
|
|
815
|
+
// VSA company is the synonym tier when differently-spelled forms
|
|
816
|
+
// occupy the same learnt distributional role.
|
|
817
|
+
if (
|
|
818
|
+
(!balanced || geometric < bar) &&
|
|
819
|
+
distributional < synonymBar
|
|
820
|
+
) {
|
|
626
821
|
continue;
|
|
627
822
|
}
|
|
628
823
|
if (
|
|
@@ -655,6 +850,7 @@ async function bridgeImpl(
|
|
|
655
850
|
// mechanism exists to explain SUBSTITUTIONS; a query needing none is
|
|
656
851
|
// recall's job, not the bridge's.
|
|
657
852
|
if (!ok) continue;
|
|
853
|
+
if (diagnostics) diagnostics.structurallyValid++;
|
|
658
854
|
|
|
659
855
|
// Coverage: matched runs plus accepted substitutions must dominate the
|
|
660
856
|
// query, every interior gap already proved ≤ W above, and the EDGES
|
|
@@ -675,8 +871,35 @@ async function bridgeImpl(
|
|
|
675
871
|
covered += e - Math.max(s, reachEnd);
|
|
676
872
|
reachEnd = Math.max(reachEnd, e);
|
|
677
873
|
}
|
|
874
|
+
if (diagnostics) {
|
|
875
|
+
diagnostics.bestCovered = Math.max(diagnostics.bestCovered, covered);
|
|
876
|
+
const candidateGapBytes = gaps.reduce(
|
|
877
|
+
(n, g) => n + g.ce - g.cs,
|
|
878
|
+
0,
|
|
879
|
+
);
|
|
880
|
+
diagnostics.closest.push({
|
|
881
|
+
id: sid,
|
|
882
|
+
covered,
|
|
883
|
+
leading: spans[0][0],
|
|
884
|
+
trailing: query.length - reachEnd,
|
|
885
|
+
gaps: gaps.length,
|
|
886
|
+
substitutions: subs.length,
|
|
887
|
+
queryGapBytes: gaps.reduce((n, g) => n + g.qe - g.qs, 0),
|
|
888
|
+
candidateGapBytes,
|
|
889
|
+
gapRanges: gaps.map((g) => [g.qs, g.qe, g.cs, g.ce]),
|
|
890
|
+
candidateSurplus: cBytes.length - covered - candidateGapBytes,
|
|
891
|
+
gapsExplained: gaps.every((g) => explainedSpan(cBytes, g.cs, g.ce)),
|
|
892
|
+
});
|
|
893
|
+
diagnostics.closest.sort((a, b) =>
|
|
894
|
+
b.covered - a.covered ||
|
|
895
|
+
a.leading + a.trailing - b.leading - b.trailing ||
|
|
896
|
+
a.id - b.id
|
|
897
|
+
);
|
|
898
|
+
if (diagnostics.closest.length > W) diagnostics.closest.length = W;
|
|
899
|
+
}
|
|
678
900
|
if (spans[0][0] > W || query.length - reachEnd > W) continue;
|
|
679
901
|
if (!dominates(covered, query.length)) continue;
|
|
902
|
+
if (diagnostics) diagnostics.coverageValid++;
|
|
680
903
|
|
|
681
904
|
// ZERO-SUBSTITUTION ADMISSION — an IDENTITY claim, not a substitution.
|
|
682
905
|
//
|
|
@@ -741,6 +964,7 @@ async function bridgeImpl(
|
|
|
741
964
|
if (cBytes.length - covered - cGap > W) continue;
|
|
742
965
|
if (!gaps.every((g) => explainedSpan(cBytes, g.cs, g.ce))) continue;
|
|
743
966
|
}
|
|
967
|
+
if (diagnostics) diagnostics.identityValid++;
|
|
744
968
|
|
|
745
969
|
// KNOWN content may never be dismissed — see dismissedKnownContent
|
|
746
970
|
// (the live case: "what is the capital of france" aligning into a
|
|
@@ -748,10 +972,12 @@ async function bridgeImpl(
|
|
|
748
972
|
// trained "France" — as a gap, while genuinely novel spans like
|
|
749
973
|
// test/49's untrained "Name" remain tolerable).
|
|
750
974
|
if (dismissedKnownQ(spans)) continue;
|
|
975
|
+
if (diagnostics) diagnostics.knownContentValid++;
|
|
751
976
|
|
|
752
977
|
if (covered > bestAccounted) {
|
|
753
978
|
bestAccounted = covered;
|
|
754
979
|
best = { id: sid, accounted: spans, subs };
|
|
980
|
+
if (diagnostics) diagnostics.bestRank = candidateIndex;
|
|
755
981
|
}
|
|
756
982
|
}
|
|
757
983
|
|
|
@@ -767,6 +993,18 @@ async function bridgeImpl(
|
|
|
767
993
|
],
|
|
768
994
|
`a trained context accounts for the query up to ${best.subs.length} ` +
|
|
769
995
|
`corroborated substitution(s) — grounding through its learnt edges`,
|
|
996
|
+
undefined,
|
|
997
|
+
diagnostics!,
|
|
998
|
+
);
|
|
999
|
+
} else {
|
|
1000
|
+
ctx.trace?.step(
|
|
1001
|
+
"substitutionBridge",
|
|
1002
|
+
[rItem(query, "query")],
|
|
1003
|
+
[],
|
|
1004
|
+
"candidate contexts were proposed, but none passed the bridge's " +
|
|
1005
|
+
"structural identity and corroboration gates",
|
|
1006
|
+
undefined,
|
|
1007
|
+
diagnostics!,
|
|
770
1008
|
);
|
|
771
1009
|
}
|
|
772
1010
|
return best;
|
package/src/mind/junction.ts
CHANGED
|
@@ -95,9 +95,9 @@ export function junctionSeeds(ctx: MindContext, b: Uint8Array): number[] {
|
|
|
95
95
|
return [wids[0], wids[wids.length - 1]];
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/**
|
|
99
|
-
* parent pages, container pages), keyed by the
|
|
100
|
-
*
|
|
98
|
+
/** Session cache of the identity walks' pure reads (capped bytes,
|
|
99
|
+
* parent pages, container pages), keyed by the write-invalidated structural
|
|
100
|
+
* lifecycle object. One response issues many walks whose ancestries overlap
|
|
101
101
|
* heavily (pair sides repeat across combos, and synonym walks revisit the
|
|
102
102
|
* same neighbourhoods); the store is read-only while a response is in flight,
|
|
103
103
|
* so every one of these reads is a pure function of the id — repeats cost a
|
|
@@ -110,17 +110,28 @@ export interface WalkCache {
|
|
|
110
110
|
containers: Map<number, number[]>;
|
|
111
111
|
}
|
|
112
112
|
const walkCaches = new WeakMap<object, WalkCache>();
|
|
113
|
+
const WALK_CACHE_MAX = 100_000;
|
|
113
114
|
export function walkCache(ctx: MindContext): WalkCache | null {
|
|
114
115
|
if (ctx.climbMemo === null) return null;
|
|
115
|
-
let c = walkCaches.get(ctx.
|
|
116
|
+
let c = walkCaches.get(ctx._structMemoKey);
|
|
116
117
|
if (c === undefined) {
|
|
117
118
|
walkCaches.set(
|
|
118
|
-
ctx.
|
|
119
|
+
ctx._structMemoKey,
|
|
119
120
|
c = { reads: new Map(), parents: new Map(), containers: new Map() },
|
|
120
121
|
);
|
|
122
|
+
} else if (
|
|
123
|
+
c.reads.size + c.parents.size + c.containers.size >= WALK_CACHE_MAX
|
|
124
|
+
) {
|
|
125
|
+
c.reads.clear();
|
|
126
|
+
c.parents.clear();
|
|
127
|
+
c.containers.clear();
|
|
121
128
|
}
|
|
122
129
|
return c;
|
|
123
130
|
}
|
|
131
|
+
|
|
132
|
+
export function invalidateJunctionCache(ctx: MindContext): void {
|
|
133
|
+
walkCaches.delete(ctx._structMemoKey);
|
|
134
|
+
}
|
|
124
135
|
export function cachedRead(
|
|
125
136
|
ctx: MindContext,
|
|
126
137
|
cache: WalkCache | null,
|
|
@@ -352,13 +363,15 @@ export async function junctionSynonyms(
|
|
|
352
363
|
maxInterior: number,
|
|
353
364
|
unordered = false,
|
|
354
365
|
sides?: JunctionSynonymSides,
|
|
366
|
+
sharedBudget?: { n: number },
|
|
355
367
|
): Promise<SynonymJunction[]> {
|
|
356
368
|
const s = sides ?? await loadJunctionSynonymSides(ctx, left, right);
|
|
357
369
|
if (s.leftId === null && s.rightId === null) return [];
|
|
358
370
|
|
|
359
371
|
// ── Tier 2.5a: single-synonym — one side replaced by a halo sibling ──────
|
|
360
372
|
// ONE shared expansion budget across BOTH directions of this tier.
|
|
361
|
-
const singleBudget =
|
|
373
|
+
const singleBudget = sharedBudget ??
|
|
374
|
+
{ n: hubBound(ctx) * ctx.space.maxGroup };
|
|
362
375
|
const singleOut = new Map<number, SynonymJunction>();
|
|
363
376
|
const keepBest = (
|
|
364
377
|
map: Map<number, SynonymJunction>,
|
|
@@ -439,7 +452,8 @@ export async function junctionSynonyms(
|
|
|
439
452
|
);
|
|
440
453
|
|
|
441
454
|
const doubleOut = new Map<number, SynonymJunction>();
|
|
442
|
-
const budget =
|
|
455
|
+
const budget = sharedBudget ??
|
|
456
|
+
{ n: hubBound(ctx) * ctx.space.maxGroup };
|
|
443
457
|
const tries = Math.min(pairs.length, ctx.cfg.haloQueryK);
|
|
444
458
|
for (let i = 0; i < tries; i++) {
|
|
445
459
|
const { l, r, confidence } = pairs[i];
|