@hviana/sema 0.4.4 → 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.
Files changed (51) hide show
  1. package/AUTHORS.md +0 -1
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -2
  4. package/dist/src/geometry.d.ts +6 -0
  5. package/dist/src/geometry.js +224 -44
  6. package/dist/src/mind/attention.d.ts +11 -0
  7. package/dist/src/mind/attention.js +344 -13
  8. package/dist/src/mind/junction.js +18 -2
  9. package/dist/src/mind/match.d.ts +11 -0
  10. package/dist/src/mind/match.js +13 -2
  11. package/dist/src/mind/mechanisms/cast.js +366 -34
  12. package/dist/src/mind/mechanisms/confluence.js +17 -1
  13. package/dist/src/mind/mechanisms/recall.js +17 -3
  14. package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
  15. package/dist/src/mind/pipeline-mechanism.js +96 -40
  16. package/dist/src/mind/pipeline.js +31 -3
  17. package/dist/src/mind/reasoning.d.ts +4 -2
  18. package/dist/src/mind/reasoning.js +29 -4
  19. package/dist/src/mind/recognition.js +67 -2
  20. package/dist/src/mind/resonance.d.ts +14 -2
  21. package/dist/src/mind/resonance.js +0 -0
  22. package/dist/src/mind/types.d.ts +43 -1
  23. package/dist/src/sema.d.ts +11 -1
  24. package/dist/src/sema.js +16 -2
  25. package/dist/src/store.d.ts +64 -1
  26. package/dist/src/store.js +107 -8
  27. package/index.html +2 -3
  28. package/package.json +1 -1
  29. package/src/geometry.ts +231 -43
  30. package/src/mind/attention.ts +366 -15
  31. package/src/mind/junction.ts +18 -2
  32. package/src/mind/match.ts +18 -2
  33. package/src/mind/mechanisms/cast.ts +376 -43
  34. package/src/mind/mechanisms/confluence.ts +16 -1
  35. package/src/mind/mechanisms/recall.ts +17 -2
  36. package/src/mind/pipeline-mechanism.ts +96 -36
  37. package/src/mind/pipeline.ts +33 -3
  38. package/src/mind/reasoning.ts +31 -4
  39. package/src/mind/recognition.ts +65 -2
  40. package/src/mind/resonance.ts +0 -0
  41. package/src/mind/types.ts +43 -1
  42. package/src/sema.ts +21 -2
  43. package/src/store.ts +106 -5
  44. package/test/00-extract.test.mjs +28 -0
  45. package/test/15-decomposition-gap.test.mjs +0 -0
  46. package/test/24-generalization.test.mjs +67 -19
  47. package/test/29-counterfactual.test.mjs +106 -42
  48. package/test/33-multi-candidate.test.mjs +56 -12
  49. package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
  50. package/test/63-fold-invariants.test.mjs +489 -0
  51. 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, known } = regions[ri];
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 = chunk
331
- ? canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo)
332
- : null;
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
- const noiseFloor = estimatorNoise(ctx.store.D);
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
- breadthSum += rv.absorbed ?? 1;
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
- regionSpans.set(pc.item.id, spans);
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 { votes, votesIdf, support, regionSupport, regionSpans, steps };
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
- const totalRegions = Math.max(1, regions.length);
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
- const idfDesc = [...votesIdf.values()].sort((a, b) => b - a);
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: regionSpans.get(point.anchor)?.length ?? 0,
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
@@ -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
- const containers = cachedContainers(ctx, cache, x, bound);
234
- if (containers.length < bound) {
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);
@@ -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. */
@@ -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