@hviana/sema 0.4.4 → 0.4.7
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/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/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/rabitq-ivf/src/rabitq.js +31 -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/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/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/rabitq-ivf/src/rabitq.ts +31 -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
- package/test/65-ann-recall.test.mjs +331 -0
package/src/mind/attention.ts
CHANGED
|
@@ -34,7 +34,14 @@ import {
|
|
|
34
34
|
estimatorNoise,
|
|
35
35
|
type StructuralPart,
|
|
36
36
|
} from "../geometry.js";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
foldTree,
|
|
39
|
+
gistOf,
|
|
40
|
+
latin1Key,
|
|
41
|
+
perceive,
|
|
42
|
+
read,
|
|
43
|
+
resolve,
|
|
44
|
+
} from "./primitives.js";
|
|
38
45
|
import { recognise } from "./recognition.js";
|
|
39
46
|
import { leafIdRun } from "./canonical.js";
|
|
40
47
|
import {
|
|
@@ -556,6 +563,18 @@ export async function computeAttention(
|
|
|
556
563
|
// positional accident this work exists to remove.
|
|
557
564
|
chunk: false,
|
|
558
565
|
known: true, // a recognised site IS a stored form
|
|
566
|
+
// …and CARRY WHICH ONE. `known: true` claimed exactness while the
|
|
567
|
+
// identity itself was dropped, leaving the climb to re-derive it from
|
|
568
|
+
// the gist through the ANN — so which stored node an exact site voted
|
|
569
|
+
// with turned on approximate rank. Measured on test/34: the site
|
|
570
|
+
// "square" ([10,16), payload 40) resonated to "quare" (119) instead;
|
|
571
|
+
// the exact junction tier then found no container holding both "blue"
|
|
572
|
+
// and 119, fell through to the single-synonym tier, and "blue then
|
|
573
|
+
// square" attended to "red square" — a context NEITHER attribute
|
|
574
|
+
// attends to alone. This is the same exact-first economy chunks
|
|
575
|
+
// already get from canonicalChunkId, and it REMOVES an ANN query
|
|
576
|
+
// rather than adding one.
|
|
577
|
+
id: s.payload,
|
|
559
578
|
});
|
|
560
579
|
}
|
|
561
580
|
|
|
@@ -704,6 +723,98 @@ export function collectRegions(ctx: MindContext, query: Uint8Array): Region[] {
|
|
|
704
723
|
// left the sentences CAST needs with no free run at all (C2, C3). A
|
|
705
724
|
// region must come from the fold, not from a stride over it.
|
|
706
725
|
});
|
|
726
|
+
|
|
727
|
+
// ─── FORMS THE QUERY'S OWN CUT SPLIT ────────────────────────────────────
|
|
728
|
+
// The walk above enumerates FOLD NODES ONLY, so a stored form the query's
|
|
729
|
+
// content-defined cut happens to split is not addressable at all — however
|
|
730
|
+
// discriminative it is. Measured: `request_id=1042` against a 200-record
|
|
731
|
+
// log, the query's best match, cut as `...uest_id=|10|42 and r`; "1042"
|
|
732
|
+
// reaches exactly ONE context of 205 (maximal IDF) and cast no vote, while
|
|
733
|
+
// the scaffolding "=10" — which matches every record 1000–1099 — did. The
|
|
734
|
+
// climb was voting on the only evidence it could address, and that was the
|
|
735
|
+
// non-discriminative kind.
|
|
736
|
+
//
|
|
737
|
+
// The WRITE path already made these reachable: canonicalWindows interns a
|
|
738
|
+
// form at both lengths precisely so one straddling a cut resolves from
|
|
739
|
+
// either side. The read path simply never used the guarantee. So this is
|
|
740
|
+
// recovered here by lookup — the fold, its invariants and the write path
|
|
741
|
+
// are untouched.
|
|
742
|
+
//
|
|
743
|
+
// Admitting every resolvable window is REFUTED (it is the ascent-sites
|
|
744
|
+
// failure): on a 5-context corpus a 26-byte query yielded 17 "unique"
|
|
745
|
+
// windows that were all fragments of ONE word (" pai" "pain" "aint" …).
|
|
746
|
+
// No per-window threshold separates that from the log case — the two need
|
|
747
|
+
// the same windows ADMITTED and COLLAPSED at identical per-window IDF. It
|
|
748
|
+
// is a REDUNDANCY problem, so overlapping admitted windows are COALESCED
|
|
749
|
+
// into maximal spans: the log query then yields the two disjoint records it
|
|
750
|
+
// names, and the 17 fragments yield the one span " painted the Mona Lisa".
|
|
751
|
+
const W = ctx.space.maxGroup;
|
|
752
|
+
if (query.length > W) {
|
|
753
|
+
const N = corpusN(ctx);
|
|
754
|
+
const reachMemo = sharedReachMemo(ctx);
|
|
755
|
+
// Coalesce while sweeping left to right: a window overlapping (or just
|
|
756
|
+
// touching) the span under construction extends it. Merging cannot
|
|
757
|
+
// inflate what the climb pays for this evidence — the merged span votes
|
|
758
|
+
// as ITSELF, and a longer span is at least as discriminative as its most
|
|
759
|
+
// discriminative part, i.e. its reach is bounded by the MIN over the
|
|
760
|
+
// windows that built it.
|
|
761
|
+
const spans: Array<{ start: number; end: number }> = [];
|
|
762
|
+
// COVERAGE BY PREFIX MAXIMUM, NOT BY RESCANNING THE REGIONS.
|
|
763
|
+
// The containment test below is the loop's hot path — it rejects 97% of
|
|
764
|
+
// windows — and asking it as `regions.some(...)` re-walked every region
|
|
765
|
+
// at every offset: O(|query| · |regions|). That is quadratic in the
|
|
766
|
+
// input, and the region count grows with it — measured 1,510 regions on
|
|
767
|
+
// an 8,195-byte query, i.e. ~12.4M predicate evaluations in ONE call,
|
|
768
|
+
// against the constant-KB/s law test/14 asserts.
|
|
769
|
+
//
|
|
770
|
+
// A region contains the window [o, o+W) exactly when it starts at or
|
|
771
|
+
// before `o` and ends at or after `o+W`. So the only thing the test
|
|
772
|
+
// needs from the regions is, per offset, the FARTHEST end among those
|
|
773
|
+
// starting at or before it — a prefix maximum, built in one pass and
|
|
774
|
+
// read in O(1). Identical verdict by construction, no behaviour change.
|
|
775
|
+
const maxEndFrom = new Int32Array(query.length + 1);
|
|
776
|
+
for (const r of regions) {
|
|
777
|
+
if (r.start <= query.length && r.end > maxEndFrom[r.start]) {
|
|
778
|
+
maxEndFrom[r.start] = r.end;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (let i = 1; i <= query.length; i++) {
|
|
782
|
+
if (maxEndFrom[i - 1] > maxEndFrom[i]) maxEndFrom[i] = maxEndFrom[i - 1];
|
|
783
|
+
}
|
|
784
|
+
for (let o = 0; o + W <= query.length; o++) {
|
|
785
|
+
// A window some fold region wholly contains offers no address the walk
|
|
786
|
+
// above did not already offer.
|
|
787
|
+
if (maxEndFrom[o] >= o + W) continue;
|
|
788
|
+
const ids = leafIdRun(ctx, query, o, o + W);
|
|
789
|
+
if (ids === null) continue;
|
|
790
|
+
const wid = ctx.store.findBranch(ids);
|
|
791
|
+
if (wid === null) continue;
|
|
792
|
+
const reach = edgeAncestors(ctx, wid, N, reachMemo);
|
|
793
|
+
// Saturated = the climb ABSTAINED; no roots = it reached nothing that
|
|
794
|
+
// could corroborate anything. Neither is evidence.
|
|
795
|
+
if (reach.saturated || reach.roots.length === 0) continue;
|
|
796
|
+
const last = spans[spans.length - 1];
|
|
797
|
+
if (last && o <= last.end) last.end = o + W;
|
|
798
|
+
else spans.push({ start: o, end: o + W });
|
|
799
|
+
}
|
|
800
|
+
for (const { start, end } of spans) {
|
|
801
|
+
// The same wrapper filter the fold regions pass through.
|
|
802
|
+
if (dominates(end - start, query.length) && regions.length > 0) continue;
|
|
803
|
+
regions.push({
|
|
804
|
+
v: gistOf(ctx, query.subarray(start, end)),
|
|
805
|
+
start,
|
|
806
|
+
end,
|
|
807
|
+
// NOT a chunk: `chunk` means "a smallest grouped unit the FOLD
|
|
808
|
+
// produced", and this span was assembled here. Setting it is
|
|
809
|
+
// REFUTED — it cost 5 tests (honest silence, fusion direction, both
|
|
810
|
+
// test/50 probes) where chunk:false costs none.
|
|
811
|
+
chunk: false,
|
|
812
|
+
known: true,
|
|
813
|
+
// EVIDENCE, NOT A POINT OF ATTENTION — see Region.corroborating.
|
|
814
|
+
corroborating: true,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
707
818
|
return regions;
|
|
708
819
|
}
|
|
709
820
|
|
|
@@ -732,7 +843,87 @@ export async function voteRegions(
|
|
|
732
843
|
// `v`/`start`/`end` are rebindable: a long approximate segment may vote
|
|
733
844
|
// with the sub-span that actually carries its evidence — see below.
|
|
734
845
|
let { v, start, end } = regions[ri];
|
|
735
|
-
const { chunk
|
|
846
|
+
const { chunk } = regions[ri];
|
|
847
|
+
// BELOW ONE RIVER WINDOW, BYTE IDENTITY IS NOT EVIDENCE. The same
|
|
848
|
+
// principle identityBar states and recognition's own `emit` already
|
|
849
|
+
// enforces on sites ("below one river window, byte overlap is chance"),
|
|
850
|
+
// applied to what the climb calls EXACT. It was unnecessary while the
|
|
851
|
+
// fold grouped at fixed arity — every chunk was then exactly W bytes —
|
|
852
|
+
// but content-defined cuts run from W-1 up to the keyring's seat count,
|
|
853
|
+
// so sub-window segments now exist, and a 3-byte string is interned by
|
|
854
|
+
// triviality rather than by evidence.
|
|
855
|
+
//
|
|
856
|
+
// Such a region is NOT dropped: it still votes on its gist, through the
|
|
857
|
+
// contrastive-margin gate every approximate region pays. Dropping them
|
|
858
|
+
// outright was measured and REFUTED — the suite fell 441 -> 406, because
|
|
859
|
+
// short regions do carry real evidence; what they must not carry is the
|
|
860
|
+
// EXACT tier's full mutual weight and its exemption from the margin.
|
|
861
|
+
//
|
|
862
|
+
// Measured on test/50's junk query: the 3-byte chunk "of " voted exact
|
|
863
|
+
// at mutual 1.00 with idf 4.22, and an unrelated haiku exemplar's pooled
|
|
864
|
+
// vote went 1.13 -> 5.94 — past consensusFloor (5.82), making a junk root
|
|
865
|
+
// TRUSTED and licensing CAST to compare content the query never named.
|
|
866
|
+
// consensusFloor did not drift; what fed it stopped being evidence.
|
|
867
|
+
//
|
|
868
|
+
// A region spanning the WHOLE query is exempt, exactly as the site rule
|
|
869
|
+
// exempts a whole-query span: it is then not a fragment of something
|
|
870
|
+
// longer, it is the question ("red" asked on its own — test/34).
|
|
871
|
+
const subWindow = end - start < W &&
|
|
872
|
+
!(start === 0 && end === query.length);
|
|
873
|
+
// EXACTNESS IS A PROPERTY OF THE CONTENT, NOT OF THIS QUERY'S GROUPING.
|
|
874
|
+
// `known` used to mean "these bytes resolve to ONE stored node", which
|
|
875
|
+
// conflates two different things: whether the store has seen the content,
|
|
876
|
+
// and whether this query's cut happened to group it the same way the
|
|
877
|
+
// deposit did. Under fixed-arity folding those coincided; under
|
|
878
|
+
// content-defined cuts they routinely do not.
|
|
879
|
+
//
|
|
880
|
+
// Measured over 42 voting regions (attributes / capitals / artists),
|
|
881
|
+
// against a graded reading — what fraction of the region's river windows
|
|
882
|
+
// are content-addressed:
|
|
883
|
+
//
|
|
884
|
+
// known=true cov=1.0 90% cov=0 0% 0<cov<1 10%
|
|
885
|
+
// known=false cov=1.0 5% cov=0 52% 0<cov<1 43%
|
|
886
|
+
//
|
|
887
|
+
// The 5% are regions where EVERY window resolves — the content is
|
|
888
|
+
// entirely in the store — yet the region was called unknown purely
|
|
889
|
+
// because this cut grouped it differently, so it paid the contrastive
|
|
890
|
+
// margin as though it were an approximate gist. That is grouping churn
|
|
891
|
+
// taxed as uncertainty.
|
|
892
|
+
//
|
|
893
|
+
// Only the fully-addressed case is promoted here: every window resolving
|
|
894
|
+
// is exact evidence about the content by the same content-addressing the
|
|
895
|
+
// whole-region test uses, just read at the window scale identityBar
|
|
896
|
+
// already calls the floor below which overlap is chance. The partial band
|
|
897
|
+
// (43%) is deliberately NOT promoted — it is genuinely mixed evidence and
|
|
898
|
+
// the margin is the right price for it. Paid only when the cheap whole-
|
|
899
|
+
// region test already failed, and bounded by the region's own length.
|
|
900
|
+
// HOW MUCH OF THIS REGION IS CONTENT-ADDRESSED — a fraction, not a bit.
|
|
901
|
+
// Measured over 42 voting regions, `known` as a boolean loses a wide band:
|
|
902
|
+
// 43% of "unknown" regions are PARTIALLY addressed and 5% are fully
|
|
903
|
+
// addressed while failing the whole-region test (grouping churn). Promoting
|
|
904
|
+
// the partial band wholesale was measured too and is over-crediting — it
|
|
905
|
+
// buys test/00 with a region attested 1 window in 5, granting a 20%-attested
|
|
906
|
+
// region the same full exemption a fully-attested one gets.
|
|
907
|
+
//
|
|
908
|
+
// So the coverage SCALES the bar instead of switching it: a region pays the
|
|
909
|
+
// estimator's noise floor in proportion to how much of it is NOT
|
|
910
|
+
// content-addressed. cov=1 pays nothing (identical to the old exemption),
|
|
911
|
+
// cov=0 pays the full floor (identical to the old gate), cov=0.2 pays 0.8
|
|
912
|
+
// of it. No new constant — estimatorNoise is unchanged and the coverage is
|
|
913
|
+
// read off the store by the same content addressing `known` already used.
|
|
914
|
+
const windowCoverage = (): number => {
|
|
915
|
+
if (end - start < W) return 0;
|
|
916
|
+
let tot = 0, hit = 0;
|
|
917
|
+
for (let o = start; o + W <= end; o++) {
|
|
918
|
+
tot++;
|
|
919
|
+
if (resolve(ctx, query.subarray(o, o + W)) !== null) hit++;
|
|
920
|
+
}
|
|
921
|
+
return tot === 0 ? 0 : hit / tot;
|
|
922
|
+
};
|
|
923
|
+
// A sub-window region pays in full: below one river window its byte
|
|
924
|
+
// identity is chance, so it has no coverage to claim.
|
|
925
|
+
const cov = subWindow ? 0 : (regions[ri].known ? 1 : windowCoverage());
|
|
926
|
+
const known = cov >= 1 && !subWindow;
|
|
736
927
|
// Trace-only bookkeeping for this region — allocated only under `td`
|
|
737
928
|
// (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
|
|
738
929
|
// RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
|
|
@@ -776,9 +967,11 @@ export async function voteRegions(
|
|
|
776
967
|
// the resonate() call for most exact regions — the single largest
|
|
777
968
|
// remaining inference sink — with the anchor choice unchanged (the
|
|
778
969
|
// canonical branch already ignored hits[0]).
|
|
779
|
-
let canonicalId =
|
|
780
|
-
?
|
|
781
|
-
:
|
|
970
|
+
let canonicalId = subWindow
|
|
971
|
+
? null
|
|
972
|
+
: (chunk
|
|
973
|
+
? canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo)
|
|
974
|
+
: (regions[ri].id ?? null));
|
|
782
975
|
let canonicalUsable = canonicalId !== null &&
|
|
783
976
|
(ctx.store.hasParents(canonicalId) ||
|
|
784
977
|
ctx.store.hasContainers(canonicalId));
|
|
@@ -1037,7 +1230,8 @@ export async function voteRegions(
|
|
|
1037
1230
|
break;
|
|
1038
1231
|
}
|
|
1039
1232
|
contrastiveMargin = margin;
|
|
1040
|
-
|
|
1233
|
+
// Scaled by what this region does NOT address — see `cov` above.
|
|
1234
|
+
const noiseFloor = estimatorNoise(ctx.store.D) * (1 - cov);
|
|
1041
1235
|
if (margin <= noiseFloor) {
|
|
1042
1236
|
recordRegion("contrastive-margin-rejection", {
|
|
1043
1237
|
selected,
|
|
@@ -1085,6 +1279,8 @@ export async function voteRegions(
|
|
|
1085
1279
|
roots: reach.roots,
|
|
1086
1280
|
w,
|
|
1087
1281
|
wFocus,
|
|
1282
|
+
// The pool sees VOTES, not regions — carry the region's standing with it.
|
|
1283
|
+
...(regions[ri].corroborating ? { corroborating: true } : {}),
|
|
1088
1284
|
});
|
|
1089
1285
|
if (ctx.trace) {
|
|
1090
1286
|
regionVoter[ri] = { id: voterId, score, w: wf };
|
|
@@ -1140,6 +1336,14 @@ export function poolVotes(
|
|
|
1140
1336
|
regionSupport: Map<number, number>;
|
|
1141
1337
|
/** Per-anchor contributing region spans — see Attention.clusters. */
|
|
1142
1338
|
regionSpans: Map<number, Array<[number, number]>>;
|
|
1339
|
+
/** Per-anchor count of contributing region VOTES (pooled axioms), which is
|
|
1340
|
+
* not the length of `regionSpans`: a joint binding is one vote sitting in
|
|
1341
|
+
* several places. */
|
|
1342
|
+
regionAxioms: Map<number, number>;
|
|
1343
|
+
/** Per-anchor LARGEST single-region contribution — see Attention.peak. */
|
|
1344
|
+
regionPeak: Map<number, number>;
|
|
1345
|
+
/** Anchors with support from at least one NON-corroborating region. */
|
|
1346
|
+
anchored: Set<number>;
|
|
1143
1347
|
steps: DerivationStep[];
|
|
1144
1348
|
} {
|
|
1145
1349
|
const eligible: number[] = [];
|
|
@@ -1232,6 +1436,41 @@ export function poolVotes(
|
|
|
1232
1436
|
>();
|
|
1233
1437
|
const regionSupport = new Map<number, number>();
|
|
1234
1438
|
const regionSpans = new Map<number, Array<[number, number]>>();
|
|
1439
|
+
// ONE POOLED AXIOM = ONE REGION VOTE. Counted separately from the spans
|
|
1440
|
+
// below because the two are different quantities: a JOINT binding is a
|
|
1441
|
+
// single vote whose evidence sits in several separate places (RegionVote.
|
|
1442
|
+
// parts), so its span count exceeds its axiom count. Reading the axiom
|
|
1443
|
+
// count off `regionSpans.length` conflated them and broke the accounting
|
|
1444
|
+
// both ways — contributingEvidence (absorbed-weighted, one term per
|
|
1445
|
+
// REGION) could read below it, and it could exceed the query's whole
|
|
1446
|
+
// candidate-region count.
|
|
1447
|
+
const regionAxioms = new Map<number, number>();
|
|
1448
|
+
// ANCHORS THE QUERY ITSELF POINTED AT. votesIdf is keyed by anchor node,
|
|
1449
|
+
// but root election has to know something about the REGIONS underneath it:
|
|
1450
|
+
// whether at least one of them is a structure the query wove, rather than a
|
|
1451
|
+
// form its cut split and collectRegions recovered (Region.corroborating).
|
|
1452
|
+
// An anchor standing on corroborating evidence ALONE is a real, well-priced
|
|
1453
|
+
// vote — it just is not a point of attention the query made, so it must not
|
|
1454
|
+
// enter the distribution the root cut is read from, nor the breadth ratio.
|
|
1455
|
+
//
|
|
1456
|
+
// REFUTED: barring such anchors from ROOT CANDIDACY outright. It defeats
|
|
1457
|
+
// the purpose — in the log case the CORRECT record (request_id=1042, one
|
|
1458
|
+
// context of 205) is addressable ONLY through the form the cut split, so
|
|
1459
|
+
// rejecting it handed the answer back to the near-miss 1050 (vote 5.60 ->
|
|
1460
|
+
// 3.81). Grounding follows where the evidence points; what a corroborating
|
|
1461
|
+
// region must not do is make the query look like it wove one more topic
|
|
1462
|
+
// than it did.
|
|
1463
|
+
const anchored = new Set<number>();
|
|
1464
|
+
// The LARGEST single region's contribution to this anchor's pooled vote.
|
|
1465
|
+
// The pool is a SUM (deliberately — see the pooling note above), so it says
|
|
1466
|
+
// how much evidence there is in total, never whether any ONE place in the
|
|
1467
|
+
// query carries evidence on its own. Consumers that hold an anchor to
|
|
1468
|
+
// consensusFloor(N) = ln(N) + 1/2 need the latter: that bar prices ONE
|
|
1469
|
+
// region's maximally-discriminative evidence (ln N is the IDF of content
|
|
1470
|
+
// reaching a single context), so comparing a six-region sum against it is a
|
|
1471
|
+
// dimensional error. Recorded here, beside the count, because this is the
|
|
1472
|
+
// only place the per-region contributions are still separable.
|
|
1473
|
+
const regionPeak = new Map<number, number>();
|
|
1235
1474
|
const steps: DerivationStep[] = [];
|
|
1236
1475
|
let order = 0;
|
|
1237
1476
|
for (const pc of pool.values()) {
|
|
@@ -1246,7 +1485,16 @@ export function poolVotes(
|
|
|
1246
1485
|
if (p0.kind !== "region" || seenRi.has(p0.ri)) continue;
|
|
1247
1486
|
seenRi.add(p0.ri);
|
|
1248
1487
|
const rv = regionVotes[p0.ri];
|
|
1249
|
-
|
|
1488
|
+
// Breadth is a ratio over the query's OWN candidate points of
|
|
1489
|
+
// attention (see below), and a corroborating region is not one of
|
|
1490
|
+
// those — it enters neither side of that ratio, so breadth reads
|
|
1491
|
+
// exactly as it did before such regions existed. Its evidence still
|
|
1492
|
+
// counts everywhere else: it is a premise, and it is a separate
|
|
1493
|
+
// PLACE for cluster counting — corroborating is what it is for.
|
|
1494
|
+
if (!rv.corroborating) {
|
|
1495
|
+
breadthSum += rv.absorbed ?? 1;
|
|
1496
|
+
anchored.add(pc.item.id);
|
|
1497
|
+
}
|
|
1250
1498
|
premises.push({ kind: "form", span: [rv.start, rv.end] });
|
|
1251
1499
|
// A vote knows where its own evidence sits: `parts` when it stands on
|
|
1252
1500
|
// several separate places (a joint binding), the merged span
|
|
@@ -1256,7 +1504,31 @@ export function poolVotes(
|
|
|
1256
1504
|
} else spans.push([rv.start, rv.end]);
|
|
1257
1505
|
}
|
|
1258
1506
|
regionSupport.set(pc.item.id, breadthSum);
|
|
1259
|
-
|
|
1507
|
+
// A span is a PLACE, and the same place reached through two different
|
|
1508
|
+
// votes (a standalone region and one part of a joint binding) is still
|
|
1509
|
+
// one place — listing it twice reports evidence the query does not
|
|
1510
|
+
// separately hold. Measured on test/50's fixture: span [18,21)
|
|
1511
|
+
// appeared twice among the top anchor's five.
|
|
1512
|
+
const seenSpan = new Set<string>();
|
|
1513
|
+
regionSpans.set(
|
|
1514
|
+
pc.item.id,
|
|
1515
|
+
spans.filter((sp) => {
|
|
1516
|
+
const key = `${sp[0]}:${sp[1]}`;
|
|
1517
|
+
if (seenSpan.has(key)) return false;
|
|
1518
|
+
seenSpan.add(key);
|
|
1519
|
+
return true;
|
|
1520
|
+
}),
|
|
1521
|
+
);
|
|
1522
|
+
regionAxioms.set(pc.item.id, seenRi.size);
|
|
1523
|
+
let peak = 0;
|
|
1524
|
+
for (const c of pc.contributions) {
|
|
1525
|
+
const p0 = c.premises[0].item;
|
|
1526
|
+
if (p0.kind !== "region") continue;
|
|
1527
|
+
const rv = regionVotes[p0.ri];
|
|
1528
|
+
const own = rv.wFocus ?? rv.w;
|
|
1529
|
+
if (own > peak) peak = own;
|
|
1530
|
+
}
|
|
1531
|
+
regionPeak.set(pc.item.id, peak);
|
|
1260
1532
|
steps.push({
|
|
1261
1533
|
order: order++,
|
|
1262
1534
|
move: "pool-vote",
|
|
@@ -1283,7 +1555,17 @@ export function poolVotes(
|
|
|
1283
1555
|
}
|
|
1284
1556
|
}
|
|
1285
1557
|
}
|
|
1286
|
-
return {
|
|
1558
|
+
return {
|
|
1559
|
+
votes,
|
|
1560
|
+
votesIdf,
|
|
1561
|
+
support,
|
|
1562
|
+
regionSupport,
|
|
1563
|
+
regionSpans,
|
|
1564
|
+
regionAxioms,
|
|
1565
|
+
regionPeak,
|
|
1566
|
+
anchored,
|
|
1567
|
+
steps,
|
|
1568
|
+
};
|
|
1287
1569
|
}
|
|
1288
1570
|
|
|
1289
1571
|
/** The number of DISTINCT clusters a root's contributing regions form —
|
|
@@ -1322,6 +1604,9 @@ export function commitVotes(
|
|
|
1322
1604
|
support: Map<number, { start: number; end: number; w: number }>;
|
|
1323
1605
|
regionSupport: Map<number, number>;
|
|
1324
1606
|
regionSpans: Map<number, Array<[number, number]>>;
|
|
1607
|
+
regionAxioms: Map<number, number>;
|
|
1608
|
+
regionPeak: Map<number, number>;
|
|
1609
|
+
anchored: Set<number>;
|
|
1325
1610
|
steps: DerivationStep[];
|
|
1326
1611
|
},
|
|
1327
1612
|
sat: SaturationInfo,
|
|
@@ -1331,8 +1616,17 @@ export function commitVotes(
|
|
|
1331
1616
|
td?: TraceDraft,
|
|
1332
1617
|
cfg?: ClimbConsensusCfg,
|
|
1333
1618
|
): AttentionRead {
|
|
1334
|
-
const {
|
|
1335
|
-
|
|
1619
|
+
const {
|
|
1620
|
+
votes,
|
|
1621
|
+
votesIdf,
|
|
1622
|
+
support,
|
|
1623
|
+
regionSupport,
|
|
1624
|
+
regionSpans,
|
|
1625
|
+
regionAxioms,
|
|
1626
|
+
regionPeak,
|
|
1627
|
+
anchored,
|
|
1628
|
+
steps,
|
|
1629
|
+
} = pooled;
|
|
1336
1630
|
if (votes.size === 0) {
|
|
1337
1631
|
traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
|
|
1338
1632
|
return { roots: [], ranked: [] };
|
|
@@ -1342,13 +1636,23 @@ export function commitVotes(
|
|
|
1342
1636
|
// is the query's OWN full candidate count (most never vote at all), the
|
|
1343
1637
|
// same denominator the "N of M sub-regions voted" rationale text already
|
|
1344
1638
|
// reports; regionSupport is that same accounting read PER ANCHOR.
|
|
1345
|
-
|
|
1639
|
+
// Corroborating regions are excluded from the denominator for the same
|
|
1640
|
+
// reason they are excluded from the numerator (see poolVotes): they are
|
|
1641
|
+
// not candidate points of attention the query wove, so counting them would
|
|
1642
|
+
// silently shrink every anchor's breadth — measured: test/36's genuine
|
|
1643
|
+
// second topic fell 6/11 -> 6/12 and fusion's dispersion gate dropped it,
|
|
1644
|
+
// with nothing else about the climb changed.
|
|
1645
|
+
const totalRegions = Math.max(
|
|
1646
|
+
1,
|
|
1647
|
+
regions.filter((r) => !r.corroborating).length,
|
|
1648
|
+
);
|
|
1346
1649
|
const ranked = [...votes.entries()]
|
|
1347
1650
|
.map(([anchor, vote]) => {
|
|
1348
1651
|
const s = support.get(anchor)!;
|
|
1349
1652
|
return {
|
|
1350
1653
|
anchor,
|
|
1351
1654
|
vote,
|
|
1655
|
+
peak: regionPeak.get(anchor) ?? 0,
|
|
1352
1656
|
start: s.start,
|
|
1353
1657
|
end: s.end,
|
|
1354
1658
|
breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
|
|
@@ -1359,10 +1663,20 @@ export function commitVotes(
|
|
|
1359
1663
|
};
|
|
1360
1664
|
})
|
|
1361
1665
|
.sort((a, b) => b.vote - a.vote);
|
|
1362
|
-
|
|
1363
1666
|
const overlaps = (a: Attention, b: Attention) =>
|
|
1364
1667
|
a.start < b.end && b.start < a.end;
|
|
1365
|
-
|
|
1668
|
+
// Read the root cut from the anchors the QUERY pointed at. A vote standing
|
|
1669
|
+
// only on corroborating evidence (a form the query's cut split, recovered
|
|
1670
|
+
// by lookup — Region.corroborating) is evidence for someone else's anchor,
|
|
1671
|
+
// never a point of attention of its own: the query never wove it as an
|
|
1672
|
+
// independent structure, the fold did that. Letting such votes into
|
|
1673
|
+
// idfDesc shifts naturalBreak — they are exact, hence high-IDF, hence they
|
|
1674
|
+
// land at the top of the distribution — and a 2-topic query then elects 3
|
|
1675
|
+
// roots (test/24:404, and the answered-continuation exclusion probe).
|
|
1676
|
+
const idfDesc = [...votesIdf.entries()]
|
|
1677
|
+
.filter(([anchor]) => anchored.has(anchor))
|
|
1678
|
+
.map(([, v]) => v)
|
|
1679
|
+
.sort((a, b) => b - a);
|
|
1366
1680
|
const rootCut = naturalBreak(idfDesc);
|
|
1367
1681
|
// A FURTHER point of attention (beyond the dominant one, which always
|
|
1368
1682
|
// grounds) must clear the same absolute significance floor
|
|
@@ -1393,7 +1707,7 @@ export function commitVotes(
|
|
|
1393
1707
|
pooledVote: point.vote,
|
|
1394
1708
|
idfVote: votesIdf.get(point.anchor) ?? 0,
|
|
1395
1709
|
candidateBreadth: regions.length,
|
|
1396
|
-
contributingVotes:
|
|
1710
|
+
contributingVotes: regionAxioms.get(point.anchor) ?? 0,
|
|
1397
1711
|
contributingEvidence: regionSupport.get(point.anchor) ?? 0,
|
|
1398
1712
|
breadth: point.breadth,
|
|
1399
1713
|
contributingSpans: regionSpans.get(point.anchor) ?? [],
|
|
@@ -2281,6 +2595,18 @@ async function crossRegionVotes(
|
|
|
2281
2595
|
// cumulative dialogue multiplies bounded work into tens of seconds. Small
|
|
2282
2596
|
// corpora retain exhaustive exact traversal: below this same scale the
|
|
2283
2597
|
// budget would be smaller than the structures the tests deliberately build.
|
|
2598
|
+
//
|
|
2599
|
+
// MEASURED 2026-07-29, NOT YET RESOLVED. This gate never engages at real
|
|
2600
|
+
// scale: on the trained store N = 325,608 with k = 24 and W = 4, so the
|
|
2601
|
+
// threshold is 96³ = 884,736 and a third of a million contexts still runs
|
|
2602
|
+
// unbudgeted at hubBound·W = 2,280 pops PER PAIR — 160,210 junction pops,
|
|
2603
|
+
// 5.9s, 31% of think. Sharing one hubBound·W allowance across all pairs
|
|
2604
|
+
// instead cuts that to 22,418 pops and 2.6s (think −19%), but is measurably
|
|
2605
|
+
// too tight below ~10³ contexts: test/36 (N = 8, budget 8) loses the
|
|
2606
|
+
// `red circle` binding root and test/14 (N = 120, budget 40) recalls 39/40.
|
|
2607
|
+
// The sharing is the right shape; hubBound·W is the wrong size for it, and
|
|
2608
|
+
// fitting a size to those two points would repeat the mistake the cube
|
|
2609
|
+
// already makes — pricing the gate on the synthetic corpora.
|
|
2284
2610
|
const marketScale = k * ctx.space.maxGroup;
|
|
2285
2611
|
const corpusScale = N > marketScale ** 3;
|
|
2286
2612
|
const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
@@ -2750,6 +3076,30 @@ async function crossRegionVotes(
|
|
|
2750
3076
|
}
|
|
2751
3077
|
}
|
|
2752
3078
|
|
|
3079
|
+
// COMPOSING TWO SPLIT FORMS DOES NOT WEAVE A POINT OF ATTENTION.
|
|
3080
|
+
// Region.corroborating marks a form the query's own cut SPLIT and
|
|
3081
|
+
// collectRegions recovered by lookup; poolVotes and commitVotes keep
|
|
3082
|
+
// such evidence out of the breadth ratio and out of the root cut's
|
|
3083
|
+
// distribution. This path bypassed both: a junction vote is minted
|
|
3084
|
+
// fresh here and carried nothing, so evidence the query never wove
|
|
3085
|
+
// re-entered the root election as a first-class anchor.
|
|
3086
|
+
//
|
|
3087
|
+
// Measured over the suite: 130 accepted junctions, 44 standing on at
|
|
3088
|
+
// least one corroborating region and 12 standing on NOTHING ELSE (both
|
|
3089
|
+
// endpoints corroborating, all structural-resonance tier). Those 12
|
|
3090
|
+
// are precisely the leak — the query wove neither endpoint.
|
|
3091
|
+
//
|
|
3092
|
+
// The flag is inherited only when EVERY part is corroborating. One
|
|
3093
|
+
// genuine fold region among the parts means the query did point here,
|
|
3094
|
+
// and the junction anchors on it; that also preserves the case
|
|
3095
|
+
// Region.corroborating's doc calls out as REFUTED to bar (the correct
|
|
3096
|
+
// log record reachable only through a split form still grounds, because
|
|
3097
|
+
// it grounds as evidence for an anchor, not as a topic of its own).
|
|
3098
|
+
// Safe against the explaining-away accounting because that is EXACT
|
|
3099
|
+
// tier only (spec §15) and an all-corroborating junction has no exact
|
|
3100
|
+
// ordinary vote to absorb.
|
|
3101
|
+
const jointCorroborating = [cand[a], cand[b], ...bestExtras]
|
|
3102
|
+
.every((ri) => regions[ri].corroborating === true);
|
|
2753
3103
|
out.push({
|
|
2754
3104
|
start: spanStart,
|
|
2755
3105
|
end: spanEnd,
|
|
@@ -2758,6 +3108,7 @@ async function crossRegionVotes(
|
|
|
2758
3108
|
w,
|
|
2759
3109
|
wFocus: w,
|
|
2760
3110
|
absorbed: 1 + explainedAway,
|
|
3111
|
+
...(jointCorroborating ? { corroborating: true } : {}),
|
|
2761
3112
|
// The places this junction actually stands on — its two endpoints and
|
|
2762
3113
|
// any N-ary extras, NOT the merged span [spanStart, spanEnd], which
|
|
2763
3114
|
// swallows the gap and reads as one neighbourhood. See
|
package/src/mind/junction.ts
CHANGED
|
@@ -307,8 +307,24 @@ export function junctionContainersFrom(
|
|
|
307
307
|
// abstains here in a handful of pops and falls through to the resonance
|
|
308
308
|
// tier. Below the page bound the read IS the full container list, so
|
|
309
309
|
// the walk stays exact exactly where identity evidence discriminates.
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
// READ ONE PAST THE PAGE TO TELL "FULL" FROM "SATURATED". The guard
|
|
311
|
+
// below means "this node's containers fill a whole page, so its
|
|
312
|
+
// containment ancestry is a non-discriminative slice of the corpus".
|
|
313
|
+
// But the read itself is CAPPED at the page size, so `length` can never
|
|
314
|
+
// exceed it and `length < bound` really asks "did the capped read come
|
|
315
|
+
// back full?" — which is the same answer for a genuine hub and for a
|
|
316
|
+
// node that has exactly `bound` containers and not one more. Reading
|
|
317
|
+
// bound + 1 separates them: only a node with MORE than a page is a hub.
|
|
318
|
+
//
|
|
319
|
+
// Measured on test/34 (8 deposits, so the page is √N = 3): "blue" has
|
|
320
|
+
// exactly 3 containers and was suppressed, while "red" has 2 and was
|
|
321
|
+
// expanded — so "red then circle" composed and "blue then square" fell
|
|
322
|
+
// through to the synonym tier, which substituted "red" and answered
|
|
323
|
+
// "red square", a context NEITHER attribute attends to alone. The two
|
|
324
|
+
// cross-cuts are structurally identical; only the container count
|
|
325
|
+
// differed, and only by one.
|
|
326
|
+
const containers = cachedContainers(ctx, cache, x, bound + 1);
|
|
327
|
+
if (containers.length <= bound) {
|
|
312
328
|
for (const c of containers) {
|
|
313
329
|
if (!seen.has(c)) {
|
|
314
330
|
seen.add(c);
|
package/src/mind/match.ts
CHANGED
|
@@ -499,10 +499,26 @@ export function sharedFrameStrength(
|
|
|
499
499
|
ctx: MindContext,
|
|
500
500
|
a: number,
|
|
501
501
|
b: number,
|
|
502
|
+
): number {
|
|
503
|
+
return sharedFrameStrengthOf(ctx, read(ctx, a), read(ctx, b));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** The same measure over BYTES, for callers holding a role-establishing
|
|
507
|
+
* CONTEXT rather than the node whose role it establishes — CAST's comparison
|
|
508
|
+
* reads the tier this way when two candidate analogs are fillers (bare entity
|
|
509
|
+
* names) rather than frame-bearing structures themselves. A role is a
|
|
510
|
+
* property of the context that establishes a filler, never of the filler's
|
|
511
|
+
* own bytes: measured on test/29's corpus, "Michelangelo" against "Homer"
|
|
512
|
+
* reads 0.000 while their establishing contexts ("The David was sculpted
|
|
513
|
+
* by…" against "The Iliad was written by…") read 0.452, and a context in a
|
|
514
|
+
* genuinely different frame ("Water boils at…") still reads 0.000 — the tier
|
|
515
|
+
* discriminates, it was simply being asked about the wrong bytes. */
|
|
516
|
+
export function sharedFrameStrengthOf(
|
|
517
|
+
ctx: MindContext,
|
|
518
|
+
A: Uint8Array,
|
|
519
|
+
B: Uint8Array,
|
|
502
520
|
): number {
|
|
503
521
|
const W = ctx.space.maxGroup;
|
|
504
|
-
const A = read(ctx, a);
|
|
505
|
-
const B = read(ctx, b);
|
|
506
522
|
if (A.length < W || B.length < W) return 0;
|
|
507
523
|
// Mark every byte of the shorter side covered by a learnt W-window that
|
|
508
524
|
// also occurs in the longer side.
|