@hviana/sema 0.4.3 → 0.4.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/AUTHORS.md +0 -1
- package/LICENSE.md +1 -1
- package/README.md +2 -2
- package/dist/src/geometry.d.ts +6 -0
- package/dist/src/geometry.js +224 -44
- package/dist/src/mind/attention.d.ts +11 -0
- package/dist/src/mind/attention.js +344 -13
- package/dist/src/mind/bridge.js +46 -21
- package/dist/src/mind/junction.js +18 -2
- package/dist/src/mind/match.d.ts +11 -0
- package/dist/src/mind/match.js +13 -2
- package/dist/src/mind/mechanisms/cast.js +366 -34
- package/dist/src/mind/mechanisms/confluence.js +17 -1
- package/dist/src/mind/mechanisms/recall.js +17 -3
- package/dist/src/mind/mind.js +11 -2
- package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
- package/dist/src/mind/pipeline-mechanism.js +96 -40
- package/dist/src/mind/pipeline.js +31 -3
- package/dist/src/mind/reasoning.d.ts +4 -2
- package/dist/src/mind/reasoning.js +29 -4
- package/dist/src/mind/recognition.js +67 -2
- package/dist/src/mind/resonance.d.ts +14 -2
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/types.d.ts +43 -1
- package/dist/src/sema.d.ts +11 -1
- package/dist/src/sema.js +16 -2
- package/dist/src/store.d.ts +64 -1
- package/dist/src/store.js +107 -8
- package/index.html +2 -3
- package/package.json +1 -1
- package/src/geometry.ts +231 -43
- package/src/mind/attention.ts +366 -15
- package/src/mind/bridge.ts +55 -18
- package/src/mind/junction.ts +18 -2
- package/src/mind/match.ts +18 -2
- package/src/mind/mechanisms/cast.ts +376 -43
- package/src/mind/mechanisms/confluence.ts +16 -1
- package/src/mind/mechanisms/recall.ts +17 -2
- package/src/mind/mind.ts +11 -2
- package/src/mind/pipeline-mechanism.ts +96 -36
- package/src/mind/pipeline.ts +33 -3
- package/src/mind/reasoning.ts +31 -4
- package/src/mind/recognition.ts +65 -2
- package/src/mind/resonance.ts +0 -0
- package/src/mind/types.ts +43 -1
- package/src/sema.ts +21 -2
- package/src/store.ts +106 -5
- package/test/00-extract.test.mjs +28 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/24-generalization.test.mjs +67 -19
- package/test/29-counterfactual.test.mjs +106 -42
- package/test/33-multi-candidate.test.mjs +56 -12
- package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
- package/test/63-fold-invariants.test.mjs +489 -0
- package/test/64-two-ended-thresholds.test.mjs +76 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { isChunk } from "../sema.js";
|
|
10
10
|
import { lightestDerivation, } from "../derive/src/index.js";
|
|
11
11
|
import { composeStructuralGist, consensusFloor, dominates, estimatorNoise, } from "../geometry.js";
|
|
12
|
-
import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
|
|
12
|
+
import { foldTree, gistOf, latin1Key, perceive, read, resolve, } from "./primitives.js";
|
|
13
13
|
import { recognise } from "./recognition.js";
|
|
14
14
|
import { leafIdRun } from "./canonical.js";
|
|
15
15
|
import { corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
|
|
@@ -139,6 +139,18 @@ export async function computeAttention(ctx, query, k, mode) {
|
|
|
139
139
|
// positional accident this work exists to remove.
|
|
140
140
|
chunk: false,
|
|
141
141
|
known: true, // a recognised site IS a stored form
|
|
142
|
+
// …and CARRY WHICH ONE. `known: true` claimed exactness while the
|
|
143
|
+
// identity itself was dropped, leaving the climb to re-derive it from
|
|
144
|
+
// the gist through the ANN — so which stored node an exact site voted
|
|
145
|
+
// with turned on approximate rank. Measured on test/34: the site
|
|
146
|
+
// "square" ([10,16), payload 40) resonated to "quare" (119) instead;
|
|
147
|
+
// the exact junction tier then found no container holding both "blue"
|
|
148
|
+
// and 119, fell through to the single-synonym tier, and "blue then
|
|
149
|
+
// square" attended to "red square" — a context NEITHER attribute
|
|
150
|
+
// attends to alone. This is the same exact-first economy chunks
|
|
151
|
+
// already get from canonicalChunkId, and it REMOVES an ANN query
|
|
152
|
+
// rather than adding one.
|
|
153
|
+
id: s.payload,
|
|
142
154
|
});
|
|
143
155
|
}
|
|
144
156
|
// The trace draft (spec §9): allocated ONLY when a trace was requested —
|
|
@@ -273,6 +285,105 @@ export function collectRegions(ctx, query) {
|
|
|
273
285
|
// left the sentences CAST needs with no free run at all (C2, C3). A
|
|
274
286
|
// region must come from the fold, not from a stride over it.
|
|
275
287
|
});
|
|
288
|
+
// ─── FORMS THE QUERY'S OWN CUT SPLIT ────────────────────────────────────
|
|
289
|
+
// The walk above enumerates FOLD NODES ONLY, so a stored form the query's
|
|
290
|
+
// content-defined cut happens to split is not addressable at all — however
|
|
291
|
+
// discriminative it is. Measured: `request_id=1042` against a 200-record
|
|
292
|
+
// log, the query's best match, cut as `...uest_id=|10|42 and r`; "1042"
|
|
293
|
+
// reaches exactly ONE context of 205 (maximal IDF) and cast no vote, while
|
|
294
|
+
// the scaffolding "=10" — which matches every record 1000–1099 — did. The
|
|
295
|
+
// climb was voting on the only evidence it could address, and that was the
|
|
296
|
+
// non-discriminative kind.
|
|
297
|
+
//
|
|
298
|
+
// The WRITE path already made these reachable: canonicalWindows interns a
|
|
299
|
+
// form at both lengths precisely so one straddling a cut resolves from
|
|
300
|
+
// either side. The read path simply never used the guarantee. So this is
|
|
301
|
+
// recovered here by lookup — the fold, its invariants and the write path
|
|
302
|
+
// are untouched.
|
|
303
|
+
//
|
|
304
|
+
// Admitting every resolvable window is REFUTED (it is the ascent-sites
|
|
305
|
+
// failure): on a 5-context corpus a 26-byte query yielded 17 "unique"
|
|
306
|
+
// windows that were all fragments of ONE word (" pai" "pain" "aint" …).
|
|
307
|
+
// No per-window threshold separates that from the log case — the two need
|
|
308
|
+
// the same windows ADMITTED and COLLAPSED at identical per-window IDF. It
|
|
309
|
+
// is a REDUNDANCY problem, so overlapping admitted windows are COALESCED
|
|
310
|
+
// into maximal spans: the log query then yields the two disjoint records it
|
|
311
|
+
// names, and the 17 fragments yield the one span " painted the Mona Lisa".
|
|
312
|
+
const W = ctx.space.maxGroup;
|
|
313
|
+
if (query.length > W) {
|
|
314
|
+
const N = corpusN(ctx);
|
|
315
|
+
const reachMemo = sharedReachMemo(ctx);
|
|
316
|
+
// Coalesce while sweeping left to right: a window overlapping (or just
|
|
317
|
+
// touching) the span under construction extends it. Merging cannot
|
|
318
|
+
// inflate what the climb pays for this evidence — the merged span votes
|
|
319
|
+
// as ITSELF, and a longer span is at least as discriminative as its most
|
|
320
|
+
// discriminative part, i.e. its reach is bounded by the MIN over the
|
|
321
|
+
// windows that built it.
|
|
322
|
+
const spans = [];
|
|
323
|
+
// COVERAGE BY PREFIX MAXIMUM, NOT BY RESCANNING THE REGIONS.
|
|
324
|
+
// The containment test below is the loop's hot path — it rejects 97% of
|
|
325
|
+
// windows — and asking it as `regions.some(...)` re-walked every region
|
|
326
|
+
// at every offset: O(|query| · |regions|). That is quadratic in the
|
|
327
|
+
// input, and the region count grows with it — measured 1,510 regions on
|
|
328
|
+
// an 8,195-byte query, i.e. ~12.4M predicate evaluations in ONE call,
|
|
329
|
+
// against the constant-KB/s law test/14 asserts.
|
|
330
|
+
//
|
|
331
|
+
// A region contains the window [o, o+W) exactly when it starts at or
|
|
332
|
+
// before `o` and ends at or after `o+W`. So the only thing the test
|
|
333
|
+
// needs from the regions is, per offset, the FARTHEST end among those
|
|
334
|
+
// starting at or before it — a prefix maximum, built in one pass and
|
|
335
|
+
// read in O(1). Identical verdict by construction, no behaviour change.
|
|
336
|
+
const maxEndFrom = new Int32Array(query.length + 1);
|
|
337
|
+
for (const r of regions) {
|
|
338
|
+
if (r.start <= query.length && r.end > maxEndFrom[r.start]) {
|
|
339
|
+
maxEndFrom[r.start] = r.end;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (let i = 1; i <= query.length; i++) {
|
|
343
|
+
if (maxEndFrom[i - 1] > maxEndFrom[i])
|
|
344
|
+
maxEndFrom[i] = maxEndFrom[i - 1];
|
|
345
|
+
}
|
|
346
|
+
for (let o = 0; o + W <= query.length; o++) {
|
|
347
|
+
// A window some fold region wholly contains offers no address the walk
|
|
348
|
+
// above did not already offer.
|
|
349
|
+
if (maxEndFrom[o] >= o + W)
|
|
350
|
+
continue;
|
|
351
|
+
const ids = leafIdRun(ctx, query, o, o + W);
|
|
352
|
+
if (ids === null)
|
|
353
|
+
continue;
|
|
354
|
+
const wid = ctx.store.findBranch(ids);
|
|
355
|
+
if (wid === null)
|
|
356
|
+
continue;
|
|
357
|
+
const reach = edgeAncestors(ctx, wid, N, reachMemo);
|
|
358
|
+
// Saturated = the climb ABSTAINED; no roots = it reached nothing that
|
|
359
|
+
// could corroborate anything. Neither is evidence.
|
|
360
|
+
if (reach.saturated || reach.roots.length === 0)
|
|
361
|
+
continue;
|
|
362
|
+
const last = spans[spans.length - 1];
|
|
363
|
+
if (last && o <= last.end)
|
|
364
|
+
last.end = o + W;
|
|
365
|
+
else
|
|
366
|
+
spans.push({ start: o, end: o + W });
|
|
367
|
+
}
|
|
368
|
+
for (const { start, end } of spans) {
|
|
369
|
+
// The same wrapper filter the fold regions pass through.
|
|
370
|
+
if (dominates(end - start, query.length) && regions.length > 0)
|
|
371
|
+
continue;
|
|
372
|
+
regions.push({
|
|
373
|
+
v: gistOf(ctx, query.subarray(start, end)),
|
|
374
|
+
start,
|
|
375
|
+
end,
|
|
376
|
+
// NOT a chunk: `chunk` means "a smallest grouped unit the FOLD
|
|
377
|
+
// produced", and this span was assembled here. Setting it is
|
|
378
|
+
// REFUTED — it cost 5 tests (honest silence, fusion direction, both
|
|
379
|
+
// test/50 probes) where chunk:false costs none.
|
|
380
|
+
chunk: false,
|
|
381
|
+
known: true,
|
|
382
|
+
// EVIDENCE, NOT A POINT OF ATTENTION — see Region.corroborating.
|
|
383
|
+
corroborating: true,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
276
387
|
return regions;
|
|
277
388
|
}
|
|
278
389
|
export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td) {
|
|
@@ -286,7 +397,89 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
|
|
|
286
397
|
// `v`/`start`/`end` are rebindable: a long approximate segment may vote
|
|
287
398
|
// with the sub-span that actually carries its evidence — see below.
|
|
288
399
|
let { v, start, end } = regions[ri];
|
|
289
|
-
const { chunk
|
|
400
|
+
const { chunk } = regions[ri];
|
|
401
|
+
// BELOW ONE RIVER WINDOW, BYTE IDENTITY IS NOT EVIDENCE. The same
|
|
402
|
+
// principle identityBar states and recognition's own `emit` already
|
|
403
|
+
// enforces on sites ("below one river window, byte overlap is chance"),
|
|
404
|
+
// applied to what the climb calls EXACT. It was unnecessary while the
|
|
405
|
+
// fold grouped at fixed arity — every chunk was then exactly W bytes —
|
|
406
|
+
// but content-defined cuts run from W-1 up to the keyring's seat count,
|
|
407
|
+
// so sub-window segments now exist, and a 3-byte string is interned by
|
|
408
|
+
// triviality rather than by evidence.
|
|
409
|
+
//
|
|
410
|
+
// Such a region is NOT dropped: it still votes on its gist, through the
|
|
411
|
+
// contrastive-margin gate every approximate region pays. Dropping them
|
|
412
|
+
// outright was measured and REFUTED — the suite fell 441 -> 406, because
|
|
413
|
+
// short regions do carry real evidence; what they must not carry is the
|
|
414
|
+
// EXACT tier's full mutual weight and its exemption from the margin.
|
|
415
|
+
//
|
|
416
|
+
// Measured on test/50's junk query: the 3-byte chunk "of " voted exact
|
|
417
|
+
// at mutual 1.00 with idf 4.22, and an unrelated haiku exemplar's pooled
|
|
418
|
+
// vote went 1.13 -> 5.94 — past consensusFloor (5.82), making a junk root
|
|
419
|
+
// TRUSTED and licensing CAST to compare content the query never named.
|
|
420
|
+
// consensusFloor did not drift; what fed it stopped being evidence.
|
|
421
|
+
//
|
|
422
|
+
// A region spanning the WHOLE query is exempt, exactly as the site rule
|
|
423
|
+
// exempts a whole-query span: it is then not a fragment of something
|
|
424
|
+
// longer, it is the question ("red" asked on its own — test/34).
|
|
425
|
+
const subWindow = end - start < W &&
|
|
426
|
+
!(start === 0 && end === query.length);
|
|
427
|
+
// EXACTNESS IS A PROPERTY OF THE CONTENT, NOT OF THIS QUERY'S GROUPING.
|
|
428
|
+
// `known` used to mean "these bytes resolve to ONE stored node", which
|
|
429
|
+
// conflates two different things: whether the store has seen the content,
|
|
430
|
+
// and whether this query's cut happened to group it the same way the
|
|
431
|
+
// deposit did. Under fixed-arity folding those coincided; under
|
|
432
|
+
// content-defined cuts they routinely do not.
|
|
433
|
+
//
|
|
434
|
+
// Measured over 42 voting regions (attributes / capitals / artists),
|
|
435
|
+
// against a graded reading — what fraction of the region's river windows
|
|
436
|
+
// are content-addressed:
|
|
437
|
+
//
|
|
438
|
+
// known=true cov=1.0 90% cov=0 0% 0<cov<1 10%
|
|
439
|
+
// known=false cov=1.0 5% cov=0 52% 0<cov<1 43%
|
|
440
|
+
//
|
|
441
|
+
// The 5% are regions where EVERY window resolves — the content is
|
|
442
|
+
// entirely in the store — yet the region was called unknown purely
|
|
443
|
+
// because this cut grouped it differently, so it paid the contrastive
|
|
444
|
+
// margin as though it were an approximate gist. That is grouping churn
|
|
445
|
+
// taxed as uncertainty.
|
|
446
|
+
//
|
|
447
|
+
// Only the fully-addressed case is promoted here: every window resolving
|
|
448
|
+
// is exact evidence about the content by the same content-addressing the
|
|
449
|
+
// whole-region test uses, just read at the window scale identityBar
|
|
450
|
+
// already calls the floor below which overlap is chance. The partial band
|
|
451
|
+
// (43%) is deliberately NOT promoted — it is genuinely mixed evidence and
|
|
452
|
+
// the margin is the right price for it. Paid only when the cheap whole-
|
|
453
|
+
// region test already failed, and bounded by the region's own length.
|
|
454
|
+
// HOW MUCH OF THIS REGION IS CONTENT-ADDRESSED — a fraction, not a bit.
|
|
455
|
+
// Measured over 42 voting regions, `known` as a boolean loses a wide band:
|
|
456
|
+
// 43% of "unknown" regions are PARTIALLY addressed and 5% are fully
|
|
457
|
+
// addressed while failing the whole-region test (grouping churn). Promoting
|
|
458
|
+
// the partial band wholesale was measured too and is over-crediting — it
|
|
459
|
+
// buys test/00 with a region attested 1 window in 5, granting a 20%-attested
|
|
460
|
+
// region the same full exemption a fully-attested one gets.
|
|
461
|
+
//
|
|
462
|
+
// So the coverage SCALES the bar instead of switching it: a region pays the
|
|
463
|
+
// estimator's noise floor in proportion to how much of it is NOT
|
|
464
|
+
// content-addressed. cov=1 pays nothing (identical to the old exemption),
|
|
465
|
+
// cov=0 pays the full floor (identical to the old gate), cov=0.2 pays 0.8
|
|
466
|
+
// of it. No new constant — estimatorNoise is unchanged and the coverage is
|
|
467
|
+
// read off the store by the same content addressing `known` already used.
|
|
468
|
+
const windowCoverage = () => {
|
|
469
|
+
if (end - start < W)
|
|
470
|
+
return 0;
|
|
471
|
+
let tot = 0, hit = 0;
|
|
472
|
+
for (let o = start; o + W <= end; o++) {
|
|
473
|
+
tot++;
|
|
474
|
+
if (resolve(ctx, query.subarray(o, o + W)) !== null)
|
|
475
|
+
hit++;
|
|
476
|
+
}
|
|
477
|
+
return tot === 0 ? 0 : hit / tot;
|
|
478
|
+
};
|
|
479
|
+
// A sub-window region pays in full: below one river window its byte
|
|
480
|
+
// identity is chance, so it has no coverage to claim.
|
|
481
|
+
const cov = subWindow ? 0 : (regions[ri].known ? 1 : windowCoverage());
|
|
482
|
+
const known = cov >= 1 && !subWindow;
|
|
290
483
|
// Trace-only bookkeeping for this region — allocated only under `td`
|
|
291
484
|
// (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
|
|
292
485
|
// RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
|
|
@@ -327,9 +520,11 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
|
|
|
327
520
|
// the resonate() call for most exact regions — the single largest
|
|
328
521
|
// remaining inference sink — with the anchor choice unchanged (the
|
|
329
522
|
// canonical branch already ignored hits[0]).
|
|
330
|
-
let canonicalId =
|
|
331
|
-
?
|
|
332
|
-
:
|
|
523
|
+
let canonicalId = subWindow
|
|
524
|
+
? null
|
|
525
|
+
: (chunk
|
|
526
|
+
? canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo)
|
|
527
|
+
: (regions[ri].id ?? null));
|
|
333
528
|
let canonicalUsable = canonicalId !== null &&
|
|
334
529
|
(ctx.store.hasParents(canonicalId) ||
|
|
335
530
|
ctx.store.hasContainers(canonicalId));
|
|
@@ -574,7 +769,8 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
|
|
|
574
769
|
break;
|
|
575
770
|
}
|
|
576
771
|
contrastiveMargin = margin;
|
|
577
|
-
|
|
772
|
+
// Scaled by what this region does NOT address — see `cov` above.
|
|
773
|
+
const noiseFloor = estimatorNoise(ctx.store.D) * (1 - cov);
|
|
578
774
|
if (margin <= noiseFloor) {
|
|
579
775
|
recordRegion("contrastive-margin-rejection", {
|
|
580
776
|
selected,
|
|
@@ -619,6 +815,8 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
|
|
|
619
815
|
roots: reach.roots,
|
|
620
816
|
w,
|
|
621
817
|
wFocus,
|
|
818
|
+
// The pool sees VOTES, not regions — carry the region's standing with it.
|
|
819
|
+
...(regions[ri].corroborating ? { corroborating: true } : {}),
|
|
622
820
|
});
|
|
623
821
|
if (ctx.trace) {
|
|
624
822
|
regionVoter[ri] = { id: voterId, score, w: wf };
|
|
@@ -742,6 +940,41 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
|
|
|
742
940
|
const support = new Map();
|
|
743
941
|
const regionSupport = new Map();
|
|
744
942
|
const regionSpans = new Map();
|
|
943
|
+
// ONE POOLED AXIOM = ONE REGION VOTE. Counted separately from the spans
|
|
944
|
+
// below because the two are different quantities: a JOINT binding is a
|
|
945
|
+
// single vote whose evidence sits in several separate places (RegionVote.
|
|
946
|
+
// parts), so its span count exceeds its axiom count. Reading the axiom
|
|
947
|
+
// count off `regionSpans.length` conflated them and broke the accounting
|
|
948
|
+
// both ways — contributingEvidence (absorbed-weighted, one term per
|
|
949
|
+
// REGION) could read below it, and it could exceed the query's whole
|
|
950
|
+
// candidate-region count.
|
|
951
|
+
const regionAxioms = new Map();
|
|
952
|
+
// ANCHORS THE QUERY ITSELF POINTED AT. votesIdf is keyed by anchor node,
|
|
953
|
+
// but root election has to know something about the REGIONS underneath it:
|
|
954
|
+
// whether at least one of them is a structure the query wove, rather than a
|
|
955
|
+
// form its cut split and collectRegions recovered (Region.corroborating).
|
|
956
|
+
// An anchor standing on corroborating evidence ALONE is a real, well-priced
|
|
957
|
+
// vote — it just is not a point of attention the query made, so it must not
|
|
958
|
+
// enter the distribution the root cut is read from, nor the breadth ratio.
|
|
959
|
+
//
|
|
960
|
+
// REFUTED: barring such anchors from ROOT CANDIDACY outright. It defeats
|
|
961
|
+
// the purpose — in the log case the CORRECT record (request_id=1042, one
|
|
962
|
+
// context of 205) is addressable ONLY through the form the cut split, so
|
|
963
|
+
// rejecting it handed the answer back to the near-miss 1050 (vote 5.60 ->
|
|
964
|
+
// 3.81). Grounding follows where the evidence points; what a corroborating
|
|
965
|
+
// region must not do is make the query look like it wove one more topic
|
|
966
|
+
// than it did.
|
|
967
|
+
const anchored = new Set();
|
|
968
|
+
// The LARGEST single region's contribution to this anchor's pooled vote.
|
|
969
|
+
// The pool is a SUM (deliberately — see the pooling note above), so it says
|
|
970
|
+
// how much evidence there is in total, never whether any ONE place in the
|
|
971
|
+
// query carries evidence on its own. Consumers that hold an anchor to
|
|
972
|
+
// consensusFloor(N) = ln(N) + 1/2 need the latter: that bar prices ONE
|
|
973
|
+
// region's maximally-discriminative evidence (ln N is the IDF of content
|
|
974
|
+
// reaching a single context), so comparing a six-region sum against it is a
|
|
975
|
+
// dimensional error. Recorded here, beside the count, because this is the
|
|
976
|
+
// only place the per-region contributions are still separable.
|
|
977
|
+
const regionPeak = new Map();
|
|
745
978
|
const steps = [];
|
|
746
979
|
let order = 0;
|
|
747
980
|
for (const pc of pool.values()) {
|
|
@@ -757,7 +990,16 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
|
|
|
757
990
|
continue;
|
|
758
991
|
seenRi.add(p0.ri);
|
|
759
992
|
const rv = regionVotes[p0.ri];
|
|
760
|
-
|
|
993
|
+
// Breadth is a ratio over the query's OWN candidate points of
|
|
994
|
+
// attention (see below), and a corroborating region is not one of
|
|
995
|
+
// those — it enters neither side of that ratio, so breadth reads
|
|
996
|
+
// exactly as it did before such regions existed. Its evidence still
|
|
997
|
+
// counts everywhere else: it is a premise, and it is a separate
|
|
998
|
+
// PLACE for cluster counting — corroborating is what it is for.
|
|
999
|
+
if (!rv.corroborating) {
|
|
1000
|
+
breadthSum += rv.absorbed ?? 1;
|
|
1001
|
+
anchored.add(pc.item.id);
|
|
1002
|
+
}
|
|
761
1003
|
premises.push({ kind: "form", span: [rv.start, rv.end] });
|
|
762
1004
|
// A vote knows where its own evidence sits: `parts` when it stands on
|
|
763
1005
|
// several separate places (a joint binding), the merged span
|
|
@@ -770,7 +1012,31 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
|
|
|
770
1012
|
spans.push([rv.start, rv.end]);
|
|
771
1013
|
}
|
|
772
1014
|
regionSupport.set(pc.item.id, breadthSum);
|
|
773
|
-
|
|
1015
|
+
// A span is a PLACE, and the same place reached through two different
|
|
1016
|
+
// votes (a standalone region and one part of a joint binding) is still
|
|
1017
|
+
// one place — listing it twice reports evidence the query does not
|
|
1018
|
+
// separately hold. Measured on test/50's fixture: span [18,21)
|
|
1019
|
+
// appeared twice among the top anchor's five.
|
|
1020
|
+
const seenSpan = new Set();
|
|
1021
|
+
regionSpans.set(pc.item.id, spans.filter((sp) => {
|
|
1022
|
+
const key = `${sp[0]}:${sp[1]}`;
|
|
1023
|
+
if (seenSpan.has(key))
|
|
1024
|
+
return false;
|
|
1025
|
+
seenSpan.add(key);
|
|
1026
|
+
return true;
|
|
1027
|
+
}));
|
|
1028
|
+
regionAxioms.set(pc.item.id, seenRi.size);
|
|
1029
|
+
let peak = 0;
|
|
1030
|
+
for (const c of pc.contributions) {
|
|
1031
|
+
const p0 = c.premises[0].item;
|
|
1032
|
+
if (p0.kind !== "region")
|
|
1033
|
+
continue;
|
|
1034
|
+
const rv = regionVotes[p0.ri];
|
|
1035
|
+
const own = rv.wFocus ?? rv.w;
|
|
1036
|
+
if (own > peak)
|
|
1037
|
+
peak = own;
|
|
1038
|
+
}
|
|
1039
|
+
regionPeak.set(pc.item.id, peak);
|
|
774
1040
|
steps.push({
|
|
775
1041
|
order: order++,
|
|
776
1042
|
move: "pool-vote",
|
|
@@ -800,7 +1066,17 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
|
|
|
800
1066
|
}
|
|
801
1067
|
}
|
|
802
1068
|
}
|
|
803
|
-
return {
|
|
1069
|
+
return {
|
|
1070
|
+
votes,
|
|
1071
|
+
votesIdf,
|
|
1072
|
+
support,
|
|
1073
|
+
regionSupport,
|
|
1074
|
+
regionSpans,
|
|
1075
|
+
regionAxioms,
|
|
1076
|
+
regionPeak,
|
|
1077
|
+
anchored,
|
|
1078
|
+
steps,
|
|
1079
|
+
};
|
|
804
1080
|
}
|
|
805
1081
|
/** The number of DISTINCT clusters a root's contributing regions form —
|
|
806
1082
|
* see Attention.clusters. Two regions belong to the same cluster iff the
|
|
@@ -832,7 +1108,7 @@ function countClusters(spans, W) {
|
|
|
832
1108
|
return clusters;
|
|
833
1109
|
}
|
|
834
1110
|
export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg) {
|
|
835
|
-
const { votes, votesIdf, support, regionSupport, regionSpans, steps } = pooled;
|
|
1111
|
+
const { votes, votesIdf, support, regionSupport, regionSpans, regionAxioms, regionPeak, anchored, steps, } = pooled;
|
|
836
1112
|
if (votes.size === 0) {
|
|
837
1113
|
traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
|
|
838
1114
|
return { roots: [], ranked: [] };
|
|
@@ -841,13 +1117,20 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
|
|
|
841
1117
|
// is the query's OWN full candidate count (most never vote at all), the
|
|
842
1118
|
// same denominator the "N of M sub-regions voted" rationale text already
|
|
843
1119
|
// reports; regionSupport is that same accounting read PER ANCHOR.
|
|
844
|
-
|
|
1120
|
+
// Corroborating regions are excluded from the denominator for the same
|
|
1121
|
+
// reason they are excluded from the numerator (see poolVotes): they are
|
|
1122
|
+
// not candidate points of attention the query wove, so counting them would
|
|
1123
|
+
// silently shrink every anchor's breadth — measured: test/36's genuine
|
|
1124
|
+
// second topic fell 6/11 -> 6/12 and fusion's dispersion gate dropped it,
|
|
1125
|
+
// with nothing else about the climb changed.
|
|
1126
|
+
const totalRegions = Math.max(1, regions.filter((r) => !r.corroborating).length);
|
|
845
1127
|
const ranked = [...votes.entries()]
|
|
846
1128
|
.map(([anchor, vote]) => {
|
|
847
1129
|
const s = support.get(anchor);
|
|
848
1130
|
return {
|
|
849
1131
|
anchor,
|
|
850
1132
|
vote,
|
|
1133
|
+
peak: regionPeak.get(anchor) ?? 0,
|
|
851
1134
|
start: s.start,
|
|
852
1135
|
end: s.end,
|
|
853
1136
|
breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
|
|
@@ -856,7 +1139,18 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
|
|
|
856
1139
|
})
|
|
857
1140
|
.sort((a, b) => b.vote - a.vote);
|
|
858
1141
|
const overlaps = (a, b) => a.start < b.end && b.start < a.end;
|
|
859
|
-
|
|
1142
|
+
// Read the root cut from the anchors the QUERY pointed at. A vote standing
|
|
1143
|
+
// only on corroborating evidence (a form the query's cut split, recovered
|
|
1144
|
+
// by lookup — Region.corroborating) is evidence for someone else's anchor,
|
|
1145
|
+
// never a point of attention of its own: the query never wove it as an
|
|
1146
|
+
// independent structure, the fold did that. Letting such votes into
|
|
1147
|
+
// idfDesc shifts naturalBreak — they are exact, hence high-IDF, hence they
|
|
1148
|
+
// land at the top of the distribution — and a 2-topic query then elects 3
|
|
1149
|
+
// roots (test/24:404, and the answered-continuation exclusion probe).
|
|
1150
|
+
const idfDesc = [...votesIdf.entries()]
|
|
1151
|
+
.filter(([anchor]) => anchored.has(anchor))
|
|
1152
|
+
.map(([, v]) => v)
|
|
1153
|
+
.sort((a, b) => b - a);
|
|
860
1154
|
const rootCut = naturalBreak(idfDesc);
|
|
861
1155
|
// A FURTHER point of attention (beyond the dominant one, which always
|
|
862
1156
|
// grounds) must clear the same absolute significance floor
|
|
@@ -879,7 +1173,7 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
|
|
|
879
1173
|
pooledVote: point.vote,
|
|
880
1174
|
idfVote: votesIdf.get(point.anchor) ?? 0,
|
|
881
1175
|
candidateBreadth: regions.length,
|
|
882
|
-
contributingVotes:
|
|
1176
|
+
contributingVotes: regionAxioms.get(point.anchor) ?? 0,
|
|
883
1177
|
contributingEvidence: regionSupport.get(point.anchor) ?? 0,
|
|
884
1178
|
breadth: point.breadth,
|
|
885
1179
|
contributingSpans: regionSpans.get(point.anchor) ?? [],
|
|
@@ -1515,6 +1809,18 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
|
|
|
1515
1809
|
// cumulative dialogue multiplies bounded work into tens of seconds. Small
|
|
1516
1810
|
// corpora retain exhaustive exact traversal: below this same scale the
|
|
1517
1811
|
// budget would be smaller than the structures the tests deliberately build.
|
|
1812
|
+
//
|
|
1813
|
+
// MEASURED 2026-07-29, NOT YET RESOLVED. This gate never engages at real
|
|
1814
|
+
// scale: on the trained store N = 325,608 with k = 24 and W = 4, so the
|
|
1815
|
+
// threshold is 96³ = 884,736 and a third of a million contexts still runs
|
|
1816
|
+
// unbudgeted at hubBound·W = 2,280 pops PER PAIR — 160,210 junction pops,
|
|
1817
|
+
// 5.9s, 31% of think. Sharing one hubBound·W allowance across all pairs
|
|
1818
|
+
// instead cuts that to 22,418 pops and 2.6s (think −19%), but is measurably
|
|
1819
|
+
// too tight below ~10³ contexts: test/36 (N = 8, budget 8) loses the
|
|
1820
|
+
// `red circle` binding root and test/14 (N = 120, budget 40) recalls 39/40.
|
|
1821
|
+
// The sharing is the right shape; hubBound·W is the wrong size for it, and
|
|
1822
|
+
// fitting a size to those two points would repeat the mistake the cube
|
|
1823
|
+
// already makes — pricing the gate on the synthetic corpora.
|
|
1518
1824
|
const marketScale = k * ctx.space.maxGroup;
|
|
1519
1825
|
const corpusScale = N > marketScale ** 3;
|
|
1520
1826
|
const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
@@ -1927,6 +2233,30 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
|
|
|
1927
2233
|
}
|
|
1928
2234
|
}
|
|
1929
2235
|
}
|
|
2236
|
+
// COMPOSING TWO SPLIT FORMS DOES NOT WEAVE A POINT OF ATTENTION.
|
|
2237
|
+
// Region.corroborating marks a form the query's own cut SPLIT and
|
|
2238
|
+
// collectRegions recovered by lookup; poolVotes and commitVotes keep
|
|
2239
|
+
// such evidence out of the breadth ratio and out of the root cut's
|
|
2240
|
+
// distribution. This path bypassed both: a junction vote is minted
|
|
2241
|
+
// fresh here and carried nothing, so evidence the query never wove
|
|
2242
|
+
// re-entered the root election as a first-class anchor.
|
|
2243
|
+
//
|
|
2244
|
+
// Measured over the suite: 130 accepted junctions, 44 standing on at
|
|
2245
|
+
// least one corroborating region and 12 standing on NOTHING ELSE (both
|
|
2246
|
+
// endpoints corroborating, all structural-resonance tier). Those 12
|
|
2247
|
+
// are precisely the leak — the query wove neither endpoint.
|
|
2248
|
+
//
|
|
2249
|
+
// The flag is inherited only when EVERY part is corroborating. One
|
|
2250
|
+
// genuine fold region among the parts means the query did point here,
|
|
2251
|
+
// and the junction anchors on it; that also preserves the case
|
|
2252
|
+
// Region.corroborating's doc calls out as REFUTED to bar (the correct
|
|
2253
|
+
// log record reachable only through a split form still grounds, because
|
|
2254
|
+
// it grounds as evidence for an anchor, not as a topic of its own).
|
|
2255
|
+
// Safe against the explaining-away accounting because that is EXACT
|
|
2256
|
+
// tier only (spec §15) and an all-corroborating junction has no exact
|
|
2257
|
+
// ordinary vote to absorb.
|
|
2258
|
+
const jointCorroborating = [cand[a], cand[b], ...bestExtras]
|
|
2259
|
+
.every((ri) => regions[ri].corroborating === true);
|
|
1930
2260
|
out.push({
|
|
1931
2261
|
start: spanStart,
|
|
1932
2262
|
end: spanEnd,
|
|
@@ -1935,6 +2265,7 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
|
|
|
1935
2265
|
w,
|
|
1936
2266
|
wFocus: w,
|
|
1937
2267
|
absorbed: 1 + explainedAway,
|
|
2268
|
+
...(jointCorroborating ? { corroborating: true } : {}),
|
|
1938
2269
|
// The places this junction actually stands on — its two endpoints and
|
|
1939
2270
|
// any N-ary extras, NOT the merged span [spanStart, spanEnd], which
|
|
1940
2271
|
// swallows the gap and reads as one neighbourhood. See
|
package/dist/src/mind/bridge.js
CHANGED
|
@@ -553,30 +553,54 @@ async function bridgeImpl(ctx, query, proposed) {
|
|
|
553
553
|
// hundreds of roots), so this is where its proposals are first sized.
|
|
554
554
|
//
|
|
555
555
|
// Candidate bytes are read LAZILY — on first access during the seed
|
|
556
|
-
// check — not eagerly for every collected id.
|
|
557
|
-
// the
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
556
|
+
// check — not eagerly for every collected id. Most climb-proposed
|
|
557
|
+
// candidates fail the seed check and never reach the expensive identity
|
|
558
|
+
// and frame-consensus gates.
|
|
559
|
+
//
|
|
560
|
+
// Frame unanimity is different: once any candidate reaches that gate, it
|
|
561
|
+
// must be evaluated against the COMPLETE collected candidate population,
|
|
562
|
+
// not only the prefix whose bytes happened to be loaded earlier. The full
|
|
563
|
+
// phrase-scale population is therefore materialised once, lazily, on the
|
|
564
|
+
// first unanimous() call and reused afterward.
|
|
565
|
+
//
|
|
566
|
+
// Null results are memoised too, so an empty or over-cap candidate is never
|
|
567
|
+
// read repeatedly by the candidate loop and the population materialiser.
|
|
568
|
+
const candidateByteMemo = new Map();
|
|
569
|
+
/** Read one candidate at most once. Returns null when it exceeds the
|
|
570
|
+
* phrase-scale cap or has no content. */
|
|
569
571
|
const bytesOfCandidate = (sid) => {
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
572
|
-
return
|
|
572
|
+
const cached = candidateByteMemo.get(sid);
|
|
573
|
+
if (cached !== undefined)
|
|
574
|
+
return cached;
|
|
573
575
|
const b = candidateBytes(sid);
|
|
574
|
-
|
|
575
|
-
seededBytes.set(sid, b);
|
|
576
|
+
candidateByteMemo.set(sid, b);
|
|
576
577
|
return b;
|
|
577
578
|
};
|
|
578
|
-
|
|
579
|
-
|
|
579
|
+
let framePopulation = null;
|
|
580
|
+
/** Return the complete phrase-scale candidate population.
|
|
581
|
+
*
|
|
582
|
+
* This is intentionally lazy: queries that never reach frame unanimity
|
|
583
|
+
* keep the cheap per-candidate seed path. Once required, every candidate
|
|
584
|
+
* is bounded by candidateBytes(), loaded at most once, and all subsequent
|
|
585
|
+
* unanimity checks observe the same order-independent population.
|
|
586
|
+
*/
|
|
587
|
+
const ensureFramePopulation = () => {
|
|
588
|
+
if (framePopulation !== null) {
|
|
589
|
+
return framePopulation;
|
|
590
|
+
}
|
|
591
|
+
const complete = new Map();
|
|
592
|
+
for (const sid of candidates) {
|
|
593
|
+
const bytes = bytesOfCandidate(sid);
|
|
594
|
+
if (bytes !== null) {
|
|
595
|
+
complete.set(sid, bytes);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
framePopulation = complete;
|
|
599
|
+
if (diagnostics) {
|
|
600
|
+
diagnostics.phraseScale = complete.size;
|
|
601
|
+
}
|
|
602
|
+
return complete;
|
|
603
|
+
};
|
|
580
604
|
// FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
|
|
581
605
|
// groundable only when the collected candidates — the store's own sample
|
|
582
606
|
// of contexts sharing the query's content — are unanimous about the
|
|
@@ -595,7 +619,8 @@ async function bridgeImpl(ctx, query, proposed) {
|
|
|
595
619
|
// the substitution was accepted). "Unanimous" must mean the store's own
|
|
596
620
|
// instances agree, which requires at least one instance to consult.
|
|
597
621
|
const unanimous = (u, c, lf, rf) => {
|
|
598
|
-
|
|
622
|
+
const population = ensureFramePopulation();
|
|
623
|
+
for (const bytes of population.values()) {
|
|
599
624
|
let from = 0;
|
|
600
625
|
for (;;) {
|
|
601
626
|
const i = indexOf(bytes, lf, from);
|
|
@@ -230,8 +230,24 @@ unordered = false) {
|
|
|
230
230
|
// abstains here in a handful of pops and falls through to the resonance
|
|
231
231
|
// tier. Below the page bound the read IS the full container list, so
|
|
232
232
|
// the walk stays exact exactly where identity evidence discriminates.
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
// READ ONE PAST THE PAGE TO TELL "FULL" FROM "SATURATED". The guard
|
|
234
|
+
// below means "this node's containers fill a whole page, so its
|
|
235
|
+
// containment ancestry is a non-discriminative slice of the corpus".
|
|
236
|
+
// But the read itself is CAPPED at the page size, so `length` can never
|
|
237
|
+
// exceed it and `length < bound` really asks "did the capped read come
|
|
238
|
+
// back full?" — which is the same answer for a genuine hub and for a
|
|
239
|
+
// node that has exactly `bound` containers and not one more. Reading
|
|
240
|
+
// bound + 1 separates them: only a node with MORE than a page is a hub.
|
|
241
|
+
//
|
|
242
|
+
// Measured on test/34 (8 deposits, so the page is √N = 3): "blue" has
|
|
243
|
+
// exactly 3 containers and was suppressed, while "red" has 2 and was
|
|
244
|
+
// expanded — so "red then circle" composed and "blue then square" fell
|
|
245
|
+
// through to the synonym tier, which substituted "red" and answered
|
|
246
|
+
// "red square", a context NEITHER attribute attends to alone. The two
|
|
247
|
+
// cross-cuts are structurally identical; only the container count
|
|
248
|
+
// differed, and only by one.
|
|
249
|
+
const containers = cachedContainers(ctx, cache, x, bound + 1);
|
|
250
|
+
if (containers.length <= bound) {
|
|
235
251
|
for (const c of containers) {
|
|
236
252
|
if (!seen.has(c)) {
|
|
237
253
|
seen.add(c);
|
package/dist/src/mind/match.d.ts
CHANGED
|
@@ -114,6 +114,17 @@ export declare function analogyStrength(ctx: MindContext, a: number, b: number):
|
|
|
114
114
|
* is maxGroup, the same quantum differsByOneWindow and canonicalChunkId
|
|
115
115
|
* measure by; no tuned constants. */
|
|
116
116
|
export declare function sharedFrameStrength(ctx: MindContext, a: number, b: number): number;
|
|
117
|
+
/** The same measure over BYTES, for callers holding a role-establishing
|
|
118
|
+
* CONTEXT rather than the node whose role it establishes — CAST's comparison
|
|
119
|
+
* reads the tier this way when two candidate analogs are fillers (bare entity
|
|
120
|
+
* names) rather than frame-bearing structures themselves. A role is a
|
|
121
|
+
* property of the context that establishes a filler, never of the filler's
|
|
122
|
+
* own bytes: measured on test/29's corpus, "Michelangelo" against "Homer"
|
|
123
|
+
* reads 0.000 while their establishing contexts ("The David was sculpted
|
|
124
|
+
* by…" against "The Iliad was written by…") read 0.452, and a context in a
|
|
125
|
+
* genuinely different frame ("Water boils at…") still reads 0.000 — the tier
|
|
126
|
+
* discriminates, it was simply being asked about the wrong bytes. */
|
|
127
|
+
export declare function sharedFrameStrengthOf(ctx: MindContext, A: Uint8Array, B: Uint8Array): number;
|
|
117
128
|
/** FORWARD through a synonym: the continuation an edge-less node borrows from
|
|
118
129
|
* a concept (halo) sibling — resonate the node's halo, take the first
|
|
119
130
|
* sibling above the concept threshold that itself has a direct edge. */
|
package/dist/src/mind/match.js
CHANGED
|
@@ -403,9 +403,20 @@ export async function analogyStrength(ctx, a, b) {
|
|
|
403
403
|
* is maxGroup, the same quantum differsByOneWindow and canonicalChunkId
|
|
404
404
|
* measure by; no tuned constants. */
|
|
405
405
|
export function sharedFrameStrength(ctx, a, b) {
|
|
406
|
+
return sharedFrameStrengthOf(ctx, read(ctx, a), read(ctx, b));
|
|
407
|
+
}
|
|
408
|
+
/** The same measure over BYTES, for callers holding a role-establishing
|
|
409
|
+
* CONTEXT rather than the node whose role it establishes — CAST's comparison
|
|
410
|
+
* reads the tier this way when two candidate analogs are fillers (bare entity
|
|
411
|
+
* names) rather than frame-bearing structures themselves. A role is a
|
|
412
|
+
* property of the context that establishes a filler, never of the filler's
|
|
413
|
+
* own bytes: measured on test/29's corpus, "Michelangelo" against "Homer"
|
|
414
|
+
* reads 0.000 while their establishing contexts ("The David was sculpted
|
|
415
|
+
* by…" against "The Iliad was written by…") read 0.452, and a context in a
|
|
416
|
+
* genuinely different frame ("Water boils at…") still reads 0.000 — the tier
|
|
417
|
+
* discriminates, it was simply being asked about the wrong bytes. */
|
|
418
|
+
export function sharedFrameStrengthOf(ctx, A, B) {
|
|
406
419
|
const W = ctx.space.maxGroup;
|
|
407
|
-
const A = read(ctx, a);
|
|
408
|
-
const B = read(ctx, b);
|
|
409
420
|
if (A.length < W || B.length < W)
|
|
410
421
|
return 0;
|
|
411
422
|
// Mark every byte of the shorter side covered by a learnt W-window that
|